Ho sperimentato con i numeri di riga sia per il wrapping che per lo scrolling sul forum di Docker. Questa funzionalità non è ancora attiva, ma vorrei sapere se qualcuno ha commenti. Quindi, prima alcune schermate:
Contenuto con wrapping
Qui sopra, i numeri di riga aiutano a identificare dove finisce una riga e inizia un’altra. Naturalmente, esistono altri modi per renderizzare questo aspetto, ma questo argomento riguarda i numeri di riga. 
Contenuto con scrolling
Con un padding limitato sui lati dove il contenuto è nascosto, per indicare che lo scrolling rivelerà altro:
O con ombre per indicare dove il contenuto è nascosto orizzontalmente:
Alcuni feedback ricevuti hanno mostrato che le piccole ombre potrebbero essere confuse con le barre di scorrimento, quindi uno stile diverso potrebbe essere utile.
Aggiunta dei numeri di riga lato client
Quando si utilizza lo scrolling orizzontale, ogni riga di contenuto occuperà una riga sullo schermo. Quindi, in teoria, si potrebbe usare JavaScript per inserire un div verticale con una serie di numeri di riga e posizionarlo accanto al contenuto scorrevole. Ma quando si usa il wrapping, un singolo numero di riga può applicarsi a più righe di contenuto, per cui l’uso di CSS span::before insieme a content: counter(...) sembra più semplice. È questo che viene utilizzato qui.
Per usare ::before, ogni riga nel blocco di codice preformattato deve prima avere un elemento genitore, ad esempio essere racchiusa in uno span usando api.decorateCookedElement. Questo richiede che api.decorateCookedElement venga eseguito prima di highlight.js, poiché a volte highlight.js non chiude i tag sulla stessa riga in cui li ha aperti. Ad esempio, quando pensa che qualcosa sia Mustache/Handlebars:
docker image ls --format '{{slice (printf "%s:%s" .Repository .Tag) 0 11}}'
L’esempio sopra produce la chiusura </span> sulla riga successiva:
<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>
Naturalmente, questo potrebbe essere un bug temporaneo che dovrei probabilmente segnalare. Ma indipendentemente da ciò, è meglio essere prudenti. E la buona notizia: sembra che, oggi, api.decorateCookedElement(...) venga effettivamente eseguito prima di highlight.js.
Il successivo script JavaScript avvolge ogni riga in uno span e aggiunge la classe CSS lines-scroll o lines-wrap per indicare quale stile si desidera (si potrebbe persino permettere all’utente di alternare questa opzione). Aggiunge anche lines-shadow per abilitare le ombre di scorrimento. Inoltre, aggiunge alcune classi CSS agli elementi genitore pre e code per supportare lo stile e garantire che gli elementi vengano decorati solo una volta.
Decoratore JavaScript del tema
Il codice seguente abilita anche i numeri di riga e gli altri stili nell’anteprima dell’editor. Modifica l’ultima riga includendo onlyStream: true per disabilitare questa funzionalità:
api.decorateCookedElement(decorateLines, {id: 'decorate-pre-code-lines', onlyStream: true});
Aggiungi tutto il seguente codice alla sezione “Common, Head” del tuo componente in /admin/customize/themes/:
<script type="text/discourse-plugin" version="0.8">
const decorateLines = (post) => {
try {
// Combinazioni di 'number', 'wrap', 'scroll' e/o '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 usa <aside> per le citazioni di altri post del forum
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(' ');
// Assumiamo che non ci siano listener di eventi sulla riga,
// e tagliamo per nascondere le righe vuote finali
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>
Stile Sass SCSS
Il seguente codice supporta il wrapping con e senza numerazione delle righe, e lo scrolling con o senza numerazione delle righe e/o ombre. Inoltre, sopprime la numerazione delle righe quando c’è solo una riga.
Questo non aggiunge numeri di riga alle citazioni di altri post, poiché se una citazione è parziale, inizierà comunque la numerazione da 1. Per aggiungere la numerazione alle citazioni, rimuovi :not(.lines-in-quote) dalla riga seguente (che appare più volte nel file SCSS):
&.lines-number:not(.lines-count-1):not(.lines-in-quote) {
La riga sopra è anche dove viene soppressa la numerazione per i blocchi di codice a riga singola.
Stile Sass del tema
Aggiungi questo alla sezione “Common, CSS” del tuo componente in /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);
// Nessun padding per il PRE esterno:
// - le barre di scorrimento non coprono il CODE ma sono vicine ai bordi esterni
// - il testo è vicino ai bordi quando c'è contenuto nascosto
// - lo stesso per le ombre
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 {
// Altezza minima per ottenere bordo e sfondo per le righe vuote (Firefox
// ne ha bisogno solo per ::before, ma Chrome anche per lo span)
min-height: 1.5em;
&::before {
position: absolute;
left: 0;
}
}
&.lines-shadow {
background-image:
// Le prime due coperture scorrono con il contenuto, quindi appaiono
// davanti alle ombre quando non è possibile lo scrolling. Queste
// corrispondono al colore di sfondo per il primo 25%, e poi un gradiente
// fino alla trasparenza completa (quindi assumendo che le coperture siano 4 volte la
// larghezza delle ombre).
linear-gradient(to right, var(--lines-bgcolor) 25%, transparent),
linear-gradient(to left, var(--lines-bgcolor) 25%, transparent),
// E dietro di esse due ombre in posizioni fisse, per indicare
// contenuto scorrevole nascosto se non coperto.
linear-gradient(to right, var(--lines-shadow-color), transparent),
linear-gradient(to left, var(--lines-shadow-color), transparent);
// Oppure:
// 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:
// Imposta la copertura a 4 volte la larghezza dell'ombra effettiva, per corrispondere
// al punto di arresto del 25% nel gradiente della copertura
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) {
// Se i numeri di riga sono visibili, dobbiamo spostare l'ombra
--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);
}
}
}
Sviluppo
Per lo sviluppo ho usato il codice seguente, che permette di impostare parametri di query URL come ?avbPreview per abilitare il gancio del decoratore JavaScript, e cose come ?avbPreview&avbClasses=number,scroll per testare i numeri di riga con scrolling ma senza ombre. Poiché tutto il CSS è limitato all’ambito, questo non dovrebbe influenzare altri utenti durante i test.
Versione di sviluppo/debug
<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 usa <aside> per le citazioni di altri post del forum
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('===== Riscrittura; sorgente\n' + elem.innerHTML.trim());
console.log('===== Riscrittura; risultato\n' + elem.innerHTML.trim().replace(split, `<span class="${lineClass}">$1</span>`));
}
// Assumiamo che non ci siano listener di eventi sulla riga,
// e tagliamo per nascondere le righe vuote finali
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>