¿Es posible mostrar números de línea en el bloque de código?

¡Hola! Uso Markdown (principalmente con el software Typora para crear y exportar a PDF) para crear documentación. Utilizo el siguiente código para mostrar números de línea en los bloques de código:

{.language .numberLines}

Ejemplo de código:

Resultado:

También uso un foro para publicar anuncios y me gustaría compartir algo de código en las publicaciones del foro. Pero parece que este truco no funciona en Discourse :(. ¿Existe alguna forma de mostrar números de línea en los bloques de código del foro?

Utilizamos highlight.js para nuestros bloques de código y, a mi entender, no admiten números de línea… hay una explicación al respecto en su documentación aquí: Line numbers — highlight.js 11.9.0 documentation

Sin duda pueden resultar útiles como referencia, por ejemplo: «puedes cambiar el tamaño de la fuente en la línea 3»… así que, si en algún momento decidimos dejar de usar highlight.js, eso sería algo a tener en cuenta.

¡Hola Kris, muchas gracias por la referencia!

Intenté aplicar un plugin mediante la personalización del tema, pero sin éxito. Probablemente alguien que conozca bien cómo usar JS/CSS personalizado en el tema podría ayudar :slight_smile:.

Sí. Además, en instalaciones donde las líneas realmente se rompen (en lugar de activar el desplazamiento horizontal), ver los números de línea deja claro que las líneas largas se han envuelto.

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. :nerd_face:

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>

¡Gran trabajo! :clap: :clap: :clap:

¡Muchas gracias, Arjan! Todo funciona tanto en el tema claro como en el oscuro por defecto.


изображение

Comparte mi configuración:
css.txt (5.7 KB)
head.txt (1.2 KB)

Personalizaciones CSS:

// Colores de código en línea
// Pulgares verdes hacia arriba
// Icono de asignado verde
// Encabezados grises o azules
// Centrar imágenes
// Ajuste de línea en bloques de código + contenido completo
// Corregir márgenes enormes de temas
// Corrección del color de fuente del componente de tema de Gitlab (más oscuro)
// Sabores de Markdown de Gitlab originales <kbd></kbd> sobre el componente de tema
// Numeración de líneas en bloques de código por @Arjan

Componentes instalados:

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

¡Genial, eso fue rápido! Supongo que querrás usar --lines-bgcolor: white; para eliminar algunos de los grises. :sunglasses:

He editado mi publicación para corregir el SCSS, para aplicar correctamente las sombras. Esto ahora permite establecer --lines-shadow-width en cualquier tamaño y aún así desvanecerse/aparecer correctamente.

También incluye un ejemplo para radial-gradient ahora. Pero eso no cambiará mucho para los bloques de código altos: