لقد قمت بتجربة أرقام الأسطر لكل من التغليف والتمرير في منتدى Docker. هذا غير نشط بعد، لكنني أود معرفة ما إذا كان لدى أحد أي تعليقات. لذا، إليك بعض لقطات الشاشة أولاً:
محتوى مُغلف
في الأعلى، تساعد أرقام الأسطر في تحديد متى تتوقف سطر وتبدأ سطر آخر. بالطبع، توجد طرق أخرى لعرض ذلك، لكن هذا الموضوع يتعلق بأرقام الأسطر. 
محتوى مُمرَّر
مع حشوة محدودة على الجانب (أو الجوانب) حيث يكون المحتوى مخفيًا، للإشارة إلى أن التمرير سيكشف المزيد:
أو مع ظلال للإشارة إلى مكان إخفاء المحتوى أفقيًا:
بعض التعليقات التي حصلت عليها أظهرت أن الظلال الصغيرة قد تُخلط بين أشرطة التمرير، لذا قد يكون التنسيق المختلف رائعًا.
إضافة أرقام الأسطر على جانب العميل
عند استخدام التمرير الأفقي، ستستخدم كل سطر من المحتوى سطرًا واحدًا على الشاشة. لذا، نظريًا، يمكن استخدام 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>