He estado experimentando con números de línea tanto para el ajuste de texto como para el desplazamiento en el foro de Docker. Esto aún no está en vivo, pero me gustaría saber si alguien tiene algún comentario. Así que, primero, algunas capturas de pantalla:
Contenido ajustado
Arriba, los números de línea ayudan a identificar cuándo termina una línea y comienza otra. Por supuesto, hay otras formas de renderizar esto, pero este tema se centra en los números de línea. 
Contenido desplazado
Con un relleno limitado en el lado (o lados) donde el contenido está oculto, para indicar que el desplazamiento revelará más:
O con sombras para indicar dónde está oculto el contenido horizontalmente:
Algunos comentarios que recibí indicaron que las pequeñas sombras podrían confundirse con las barras de desplazamiento, por lo que un estilo diferente podría ser ideal.
Agregar los números de línea del lado del cliente
Al usar desplazamiento horizontal, cada línea de contenido ocupará una línea en la pantalla. Así que, en teoría, se podría usar JavaScript para inyectar un div vertical con un rango de números de línea y colocarlo junto al contenido desplazable. Pero al ajustar el texto, un solo número de línea puede aplicarse a varias líneas de contenido, para lo cual usar CSS span::before junto con content: counter(...) parece más sencillo. Eso es lo que se usa aquí.
Para usar ::before, cada línea en el bloque de código preformateado primero necesita algún elemento padre, como estar envuelta en un span usando api.decorateCookedElement. Esto requiere que este último se ejecute antes que highlight.js, ya que a veces highlight.js no cierra las etiquetas en la misma línea en la que las abrió. Por ejemplo, cuando piensa que algo es Mustache/Handlebars:
docker image ls --format '{{slice (printf "%s:%s" .Repository .Tag) 0 11}}'
Lo anterior produce el cierre </span> en la siguiente línea:
<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>
Por supuesto, esto podría ser un error temporal que probablemente debería reportar. Pero, independientemente, es mejor prevenir. Y la buena noticia: parece que, hoy en día, api.decorateCookedElement(...) efectivamente se ejecuta antes que highlight.js.
El siguiente script de JavaScript envuelve cada línea en un span y agrega la clase CSS lines-scroll o lines-wrap para indicar qué estilo se desea (uno podría incluso permitir que el usuario alternar eso). También agrega lines-shadow para habilitar las sombras de desplazamiento. Y agrega algunas clases CSS al pre y code padres para apoyar el estilo y garantizar que las cosas solo se decoren una vez.
Decorador de tema JavaScript
Lo siguiente también habilita los números de línea y el otro estilo en la vista previa del editor. Cambie la última línea para incluir onlyStream: true para desactivar eso:
api.decorateCookedElement(decorateLines, {id: 'decorate-pre-code-lines', onlyStream: true});
Agregue todo lo siguiente a la sección «Common, Head» de su componente en /admin/customize/themes/:
<script type="text/discourse-plugin" version="0.8">
const decorateLines = (post) => {
try {
// Combinaciones de 'number', 'wrap', 'scroll' y/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> para citas de otros posts del foro
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(' ');
// Asumimos que no hay oyentes de eventos en la línea,
// y recortamos para ocultar líneas en blanco al final
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>
Estilizado con Sass SCSS
Lo siguiente soporta el ajuste de texto con y sin numeración de líneas, y el desplazamiento con o sin numeración de líneas y/o sombras. También suprime la numeración de líneas cuando solo hay una línea.
Esto no agrega números de línea a citas de otros posts, ya que si tal cita es solo una cita parcial, aún comenzará la numeración desde 1. Para agregar numeración a las citas, elimine :not(.lines-in-quote) de la línea de abajo (que aparece varias veces en el SCSS):
&.lines-number:not(.lines-count-1):not(.lines-in-quote) {
Lo anterior es también donde se suprime la numeración para bloques de código de una sola línea.
Estilizado de tema con Sass
Agregue esto a la sección «Common, CSS» de su componente en /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);
// Sin relleno para el PRE exterior:
// - barras de desplazamiento no cubriendo CODE sino cerca de los bordes exteriores
// - texto cerca de los bordes cuando hay contenido oculto
// - igualmente para las sombras
padding: 0;
// #f5f5f5 en `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 {
// Altura mínima para obtener borde y fondo para líneas vacías (Firefox
// solo necesita esto para ::before, pero Chrome también para el span)
min-height: 1.5em;
&::before {
position: absolute;
left: 0;
}
}
&.lines-shadow {
background-image:
// Las primeras dos cubiertas se desplazan con el contenido, por lo que aparecen
// delante de las sombras cuando no es posible el desplazamiento. Estas
// coinciden con el color de fondo para el primer 25%, y luego un degradado
// hacia transparencia total (por lo que se asume que las cubiertas son 4 veces el
// ancho de las sombras).
linear-gradient(to right, var(--lines-bgcolor) 25%, transparent),
linear-gradient(to left, var(--lines-bgcolor) 25%, transparent),
// Y detrás de eso dos sombras en posiciones fijas, para indicar
// contenido desplazable oculto si no está cubierto.
linear-gradient(to right, var(--lines-shadow-color), transparent),
linear-gradient(to left, var(--lines-shadow-color), transparent);
// O:
// 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:
// Establezca la cubierta a 4 veces el ancho de la sombra real, para coincidir
// con el punto de parada del 25% en el degradado de la cubierta
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) {
// Si los números de línea son visibles, necesitamos mover la sombra
--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);
}
}
}
Desarrollo
Para el desarrollo usé el siguiente código, que permite establecer parámetros de consulta de URL como ?avbPreview para habilitar el gancho del decorador de JavaScript, y cosas como ?avbPreview&avbClasses=number,scroll para probar números de línea con desplazamiento pero sin sombras. Como todo el CSS está encapsulado, esto no debería afectar a otros usuarios durante las pruebas.
Versión de desarrollo/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> para citas de otros posts del foro
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('===== Reescribiendo; fuente\n' + elem.innerHTML.trim());
console.log('===== Reescribiendo; resultado\n' + elem.innerHTML.trim().replace(split, `<span class="${lineClass}">$1</span>`));
}
// Asumimos que no hay oyentes de eventos en la línea,
// y recortamos para ocultar líneas en blanco al final
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>