هل من الممكن إظهار أرقام الأسطر في كتلة التعليمات البرمجية؟

مرحبًا، أستخدم markdown (في الغالب، هذا برنامج Typora لإنشاء المستندات وتصديرها بصيغة PDF) لإنشاء التوثيق. أستخدم الكود التالي لإظهار أرقام الأسطر في كتل الكود:

{.language .numberLines}

مثال على الكود:

النتيجة:

أيضًا، أستخدم منتدى لنشر الإعلانات وأود مشاركة بعض الأكواد في منشورات المنتدى. لكن يبدو أن هذه الحيلة لا تعمل في Discourse :frowning: . هل هناك طريقة لإظهار أرقام الأسطر في كتل الكود في المنتدى؟

نستخدم highlight.js لمربعات الكود الخاصة بنا، ولا أعتقد أنهم يدعمون ترقيم الأسطر… هناك بعض التبريرات في وثائقهم هنا: Line numbers — highlight.js 11.9.0 documentation

يمكن أن تكون مفيدة بالتأكيد كمرجع، على سبيل المثال: “يمكنك تغيير حجم الخط في السطر 3”… لذا إذا كنا نبحث يومًا عن الانتقال بعيدًا عن highlight.js، فسيكون ذلك أحد الأمور التي يجب مراعاتها.

مرحبًا كريس، شكرًا جزيلاً على المرجع!

حاولت تطبيق إضافة عبر تخصيص السمة ولكن دون نجاح. ربما يستطيع شخص ما يعرف جيدًا كيفية استخدام JS/CSS المخصصين في السمة المساعدة :slight_smile:.

نعم. أيضًا، في التثبيتات التي تنكسر فيها الأسطر فعليًا (بدلاً من تشغيل التمرير الأفقي)، فإن رؤية أرقام الأسطر توضح أن الأسطر الطويلة قد تم التفافها.

لقد قمت بتجربة أرقام الأسطر لكل من التغليف والتمرير في منتدى Docker. هذا غير نشط بعد، لكنني أود معرفة ما إذا كان لدى أحد أي تعليقات. لذا، إليك بعض لقطات الشاشة أولاً:

محتوى مُغلف

في الأعلى، تساعد أرقام الأسطر في تحديد متى تتوقف سطر وتبدأ سطر آخر. بالطبع، توجد طرق أخرى لعرض ذلك، لكن هذا الموضوع يتعلق بأرقام الأسطر. :nerd_face:

محتوى مُمرَّر

مع حشوة محدودة على الجانب (أو الجوانب) حيث يكون المحتوى مخفيًا، للإشارة إلى أن التمرير سيكشف المزيد:

أو مع ظلال للإشارة إلى مكان إخفاء المحتوى أفقيًا:

بعض التعليقات التي حصلت عليها أظهرت أن الظلال الصغيرة قد تُخلط بين أشرطة التمرير، لذا قد يكون التنسيق المختلف رائعًا.

إضافة أرقام الأسطر على جانب العميل

عند استخدام التمرير الأفقي، ستستخدم كل سطر من المحتوى سطرًا واحدًا على الشاشة. لذا، نظريًا، يمكن استخدام JavaScript لحقن div عمودي يحتوي على نطاق من أرقام الأسطر، ووضعها بجانب المحتوى المُمَرَّر. لكن عند التغليف، قد ينطبق رقم سطر واحد على أسطر متعددة من المحتوى، وهو ما يبدو أسهل باستخدام CSS span::before مع content: counter(...). وهذا ما تم استخدامه هنا.

لاستخدام ::before، تحتاج كل سطر في كتلة الكود المنسقة مسبقًا إلى عنصر أب، مثل التغليف في span باستخدام api.decorateCookedElement. هذا يتطلب أن يعمل الأخير قبل تشغيل highlight.js، لأن highlight.js أحيانًا لا يغلق الوسوم في نفس السطر الذي فتحها فيه. على سبيل المثال، عندما يعتقد أن شيئًا ما هو Mustache/Handlebars:

docker image ls --format '{{slice (printf "%s:%s" .Repository .Tag) 0 11}}'

يعطي ما سبق إغلاق </span> في السطر التالي:

<code class="lang-handlebars hljs"><span class="xml">docker image ls --format '</span><span class="hljs-template-variable">{{<span class="hljs-name">slice</span> (<span class="hljs-name">printf</span> <span class="hljs-string">"%s:%s"</span> .Repository .Tag) <span class="hljs-number">0</span> <span class="hljs-number">11</span>}}</span><span class="xml">'
</span></code>

بالطبع، قد يكون هذا خطأ مؤقتًا يجب عليّ الإبلاغ عنه. لكن بغض النظر، من الأفضل أن تكون آمنًا. والأخبار الجيدة: يبدو أنه اليوم، يعمل api.decorateCookedElement(...) بالفعل قبل highlight.js.

يُغلف JavaScript التالي كل سطر في span ويضيف فئة CSS lines-scroll أو lines-wrap للإشارة إلى النمط المطلوب (يمكن حتى السماح للمستخدم بالتبديل بينهما). كما يضيف lines-shadow لتمكين ظلال التمرير. ويضيف بعض فئات CSS إلى العنصر الأب pre و code لدعم التنسيق، ولضمان أن يتم تزيين العناصر مرة واحدة فقط.

مُزخرف JavaScript للموضوع

يتيح ما يلي أيضًا أرقام الأسطر والتنسيق الآخر في معاينة المحرر. غيّر السطر الأخير لتشمل onlyStream: true لتعطيل ذلك:

api.decorateCookedElement(decorateLines, {id: 'decorate-pre-code-lines', onlyStream: true});

أضف كل ما يلي إلى “Common, Head” لمكونك في /admin/customize/themes/:

<script type="text/discourse-plugin" version="0.8">
const decorateLines = (post) => {
  try {
    // Combinations of 'number', 'wrap', 'scroll' and/or 'shadow'
    const classes = ['decorator', 'number', 'scroll', 'shadow'].map(c => `lines-${c}`);

    const split = /^(.*)$/mg;
    const elems = post.querySelectorAll('pre code:not(.lines-decorator)');
    elems.forEach(elem => {
      const count = elem.innerHTML.trim().match(split).length;

      // Discourse uses `<aside>` for quotes of other forum posts
      const quote = elem.closest('aside') ? ['lines-in-quote'] : []

      elem.parentElement.classList.add(`lines-count-${count}`, ...classes, ...quote);
      elem.classList.add(`lines-count-${count}`, ...classes, ...quote);
      const lineClass = ['lines-line', ...quote].join(' ');

      // Assume we don't have any event listeners on the line,
      // and trim to hide trailing blank lines
      elem.innerHTML = elem.innerHTML.trim().replace(/^(.*)$/mg, `<span class="${lineClass}">$1</span>`);
    });
  } catch (e) {
    console.error(e);
  }
}

api.decorateCookedElement(decorateLines, {id: 'decorate-pre-code-lines'});
</script>

تنسيق Sass SCSS

يدعم ما يلي التغليف مع أو بدون ترقيم الأسطر، والتمرير مع أو بدون ترقيم الأسطر و/أو الظلال. كما يقوم بكبت ترقيم الأسطر عندما يكون هناك سطر واحد فقط.

هذا لا يضيف أرقام الأسطر إلى اقتباسات المنشورات الأخرى، لأنه إذا كان الاقتباس جزئيًا فقط، فسيبدأ الترقيم من 1. لإضافة الترقيم إلى الاقتباسات، احذف :not(.lines-in-quote) من السطر أدناه (الذي يظهر عدة مرات في SCSS):

&.lines-number:not(.lines-count-1):not(.lines-in-quote) {

هذا هو المكان الذي يتم فيه كبت الترقيم لكتل الكود ذات السطر الواحد أيضًا.

تنسيق Sass للموضوع

أضف هذا إلى “Common, CSS” لمكونك في /admin/customize/themes/:

pre.lines-decorator code.lines-decorator span.lines-line {
  &::before {
    box-sizing: content-box;
  }
}

pre.lines-decorator {
  --lines-bgcolor: var(--hljs-bg);
  --lines-number-width: 2em;
  --lines-number-padding-to-right-border: .5em;
  --lines-shadow-width: .8em;
  --lines-shadow-color: rgba(0, 0, 0, .15);
  // No padding for outer PRE:
  // - scrollbars not covering CODE but close to outermost edges
  // - text close to the edges when there is hidden content
  // - likewise for the shadows
  padding: 0;
  // #f5f5f5 in `code.less`
  background-color: var(--lines-bgcolor);
}

code.lines-decorator {
  padding: 1.5em;

  &.lines-wrap {
    white-space: pre-wrap;

    &:not(.lines-count-1) {
      span.lines-line {
        display: inline-block;
        position: relative;
        padding-left: 3em;
      }
    }
  }

  &.lines-scroll {
    white-space: pre;

    span.lines-line {
      padding-right: 1.5em;

      &::before {
        position: sticky;
        left: 0;
      }
    }
  }

  &.lines-wrap span.lines-line {
    // Minimum height to get border and background for empty lines (Firefox
    // only needs this for the ::before, but Chrome also for the span)
    min-height: 1.5em;

    &::before {
      position: absolute;
      left: 0;
    }
  }

  &.lines-shadow {
    background-image:
      // First two covers that scroll with the content, hence appearing
      // in front of the shadows when no scrolling is possible. These
      // match the background color for the first 25%, and next a gradient
      // to full transparency (hence assuming the covers are 4 times the
      // width of the shadows).
      linear-gradient(to right, var(--lines-bgcolor) 25%, transparent),
      linear-gradient(to left, var(--lines-bgcolor) 25%, transparent),
      // And behind that two shadows at fixed positions, to indicate
      // scrollable, hidden content if not covered.
      linear-gradient(to right, var(--lines-shadow-color), transparent),
      linear-gradient(to left, var(--lines-shadow-color), transparent);
      // Or:
      // radial-gradient(farthest-side at 0px 50%, var(--lines-shadow-color), transparent),
      // radial-gradient(farthest-side at 100% 50%, var(--lines-shadow-color), transparent);

    background-size:
      // Set the cover to 4 times the width of the actual shadow, to match
      // the 25% stop in the gradient of the cover
      calc(4 * var(--lines-shadow-width)) 100%,
      calc(4 * var(--lines-shadow-width)) 100%,
      var(--lines-shadow-width) 100%,
      var(--lines-shadow-width) 100%;

    --shadow-left: 0;

    &.lines-number:not(.lines-count-1):not(.lines-in-quote) {
      // If line numbers are visible, we need to move the shadow
      --shadow-left: calc(var(--lines-number-width) + var(--lines-number-padding-to-right-border));
    }

    background-position: var(--shadow-left) center, right center, var(--shadow-left) center, right center;

    background-attachment: local, local, scroll, scroll;
    background-repeat: no-repeat;
  }

  &.lines-number:not(.lines-count-1):not(.lines-in-quote) {
    padding-left: 0;
    counter-reset: line-numbering;

    span.lines-line::before {
      display: inline-block;
      content: counter(line-numbering);
      counter-increment: line-numbering;
      border-right: 1px solid #ccc;
      width: var(--lines-number-width);
      padding-right: var(--lines-number-padding-to-right-border);
      margin-right: .5em;
      height: 100%;
      text-align: right;
      font-family: var(--font-family);
      color: var(--primary-medium);
      background-color: var(--lines-bgcolor);
    }
  }
}

التطوير

للتطوير، استخدمت الكود التالي، الذي يسمح بتعيين معلمات استعلام URL مثل ?avbPreview لتمكين مُزخرف JavaScript، وأشياء مثل ?avbPreview&avbClasses=number,scroll لاختبار أرقام الأسطر مع التمرير دون ظلال. وبما أن كل CSS محدد نطاقًا، فلن يؤثر ذلك على مستخدمين آخرين أثناء الاختبار.

إصدار التطوير/التصحيح
<script type="text/discourse-plugin" version="0.8">
const decorateLines = (post) => {
  try {
    const params = new URL(document.location.href).searchParams;
    const preview = params.has('avbPreview');
    if (!preview) {
      return;
    }
    const classes = ['decorator'].concat(
      (params.get('avbClasses') || 'number,scroll,shadow').split(',')).map(c => `lines-${c}`);
    const debug = params.has('avbDebug');
    const split = /^(.*)$/mg;
    const elems = post.querySelectorAll('pre code:not(.lines-decorator)');
    elems.forEach(elem => {
      const count = elem.innerHTML.trim().match(split).length;
      // Discourse uses `<aside>` for quotes of other forum posts
      const quote = elem.closest('aside') ? ['lines-in-quote'] : []
      elem.parentElement.classList.add(`lines-count-${count}`, ...classes, ...quote);
      elem.classList.add(`lines-count-${count}`, ...classes, ...quote);
      const lineClass = ['lines-line', ...quote].join(' ');

      if (debug) {
        console.log('===== Rewriting; source\n' + elem.innerHTML.trim());
        console.log('===== Rewriting; result\n' + elem.innerHTML.trim().replace(split, `<span class="${lineClass}">$1</span>`));
      }

      // Assume we don't have any event listeners on the line,
      // and trim to hide trailing blank lines
      elem.innerHTML = elem.innerHTML.trim().replace(/^(.*)$/mg, `<span class="${lineClass}">$1</span>`);
    });
  } catch (e) {
    console.error(e);
  }
}

api.decorateCookedElement(decorateLines, {id: 'decorate-pre-code-lines'});
</script>

عمل رائع! :clap: :clap: :clap:

شكراً جزيلاً، Arjan! كل شيء يعمل في كل من سمة الإضاءة الافتراضية والسمة الداكنة.


изображение

شارك إعداداتي:
css.txt (5.7 KB)
head.txt (1.2 KB)

تخصيصات CSS:

// ألوان الكود المضمن
// إبهام أخضر لأعلى
// أيقونة الموكل إليه باللون الأخضر
// رؤوس باللون الرمادي أو الأزرق
// توسيط الصور
// التفاف الكلمات في كتلة الكود + المحتوى الكامل
// إصلاح هوامش الموضوع الضخمة
// إصلاح لون خط مكون سمة Gitlab (أغمق)
// نكهات Markdown الأصلية <kbd> فوق مكون سمة نكهات Markdown Gitlab
// ترقيم الأسطر في كتل الكود بواسطة @Arjan

المكونات المثبتة:

https://github.com/discourse/discourse-search-banner
https://github.com/keegangeorge/discourse-markdown-flavors.git
https://github.com/pacharanero/discourse-topic-width-desktop.git

جميل، كان ذلك سريعًا! أعتقد أنك سترغب في استخدام --lines-bgcolor: white; للتخلص من بعض اللون الرمادي. :sunglasses:

لقد قمت بتحرير مشاركتي لإجراء إصلاح في SCSS، لتطبيق أغطية الظل بشكل صحيح. يتيح هذا الآن تعيين --lines-shadow-width إلى أي حجم مع الاستمرار في التلاشي والدخول/الخروج بشكل صحيح.

كما أنه يتضمن مثالاً لـ radial-gradient الآن. لكن هذا لن يغير الكثير بالنسبة لكتل التعليمات البرمجية الطويلة: