Est-il possible d'afficher les numéros de ligne dans un bloc de code ?

Bonjour, j’utilise Markdown (principalement avec le logiciel Typora pour créer et exporter vers PDF) afin de créer de la documentation. J’utilise le code suivant pour afficher les numéros de ligne dans les blocs de code :

{.language .numberLines}

Exemple de code :

Résultat :

J’utilise également un forum pour publier des annonces et je souhaiterais partager du code dans les messages du forum. Mais il semble que cette astuce ne fonctionne pas sur Discourse :(. Existe-t-il un moyen d’afficher les numéros de ligne dans les blocs de code du forum ?

Nous utilisons highlight.js pour nos blocs de code, et je ne pense pas qu’ils prennent en charge les numéros de ligne… leur documentation explique la logique derrière cela ici : Line numbers — highlight.js 11.9.0 documentation

Ils peuvent certainement s’avérer utiles pour référence, par exemple : « vous pouvez modifier la taille de la police à la ligne 3 »… donc si nous envisageons un jour de nous éloigner de highlight.js, ce serait un point à prendre en considération.

Salut Kris, merci beaucoup pour la référence !

J’ai essayé d’appliquer un plugin via la personnalisation du thème, mais sans succès. Probablement que quelqu’un qui connaît bien comment utiliser du JS/CSS personnalisé dans un thème pourrait aider :slight_smile: .

Oui. De plus, sur les installations où les lignes se cassent réellement (plutôt que de déclencher un défilement horizontal), voir les numéros de ligne indique clairement que les longues lignes ont été renvoyées à la ligne.

J’ai expérimenté l’ajout de numéros de ligne pour le retour à la ligne et le défilement sur le forum Docker. Cette fonctionnalité n’est pas encore en ligne, mais j’aimerais savoir si quelqu’un a des commentaires. Voici d’abord quelques captures d’écran :

Contenu avec retour à la ligne

Ci-dessus, les numéros de ligne aident à identifier où une ligne s’arrête et où une autre commence. Bien sûr, il existe d’autres façons de rendre cela, mais ce sujet porte spécifiquement sur les numéros de ligne. :nerd_face:

Contenu défilant

Avec un remplissage limité sur le(s) côté(s) où le contenu est masqué, pour indiquer que le défilement révélera plus :

Ou avec des ombres pour indiquer où le contenu est masqué horizontalement :

Certains retours que j’ai reçus ont montré que les petites ombres pourraient être confondues avec des barres de défilement, donc un style différent pourrait être préférable.

Ajout des numéros de ligne côté client

Lorsqu’on utilise un défilement horizontal, chaque ligne de contenu occupe une seule ligne à l’écran. Donc, en théorie, on pourrait utiliser JavaScript pour injecter un <div> vertical contenant une plage de numéros de ligne et le positionner à côté du contenu défilant. Mais lors du retour à la ligne, un seul numéro de ligne peut s’appliquer à plusieurs lignes de contenu, ce qui rend l’utilisation de CSS span::before avec content: counter(...) plus simple. C’est ce qui est utilisé ici.

Pour utiliser ::before, chaque ligne du bloc de code préformaté doit d’abord avoir un élément parent, comme être enveloppée dans un <span> en utilisant api.decorateCookedElement. Cela nécessite que ce dernier s’exécute avant highlight.js, car parfois highlight.js ne ferme pas les balises sur la même ligne où elles ont été ouvertes. Par exemple, lorsqu’il pense que quelque chose est Mustache/Handlebars :

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

Ce qui précède produit la fermeture </span> sur la ligne suivante :

<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>

Bien sûr, cela pourrait être un bug temporaire que je devrais probablement signaler. Mais dans tous les cas, mieux vaut être prudent. Et la bonne nouvelle : il semble qu’aujourd’hui, api.decorateCookedElement(...) s’exécute bien avant highlight.js.

Le script JavaScript suivant enveloppe chaque ligne dans un <span> et ajoute la classe CSS lines-scroll ou lines-wrap pour indiquer le style souhaité (on pourrait même permettre à l’utilisateur de basculer entre les deux). Il ajoute également lines-shadow pour activer les ombres de défilement. Et il ajoute quelques classes CSS aux éléments parents <pre> et <code> pour supporter le style et s’assurer que le décorateur ne s’exécute qu’une seule fois.

Décorateur JavaScript du thème

Ce qui suit active également les numéros de ligne et le reste du style dans l’aperçu de l’éditeur. Modifiez la dernière ligne pour inclure onlyStream: true afin de désactiver cela :

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

Ajoutez tout ce qui suit dans la section « Common, Head » de votre composant dans /admin/customize/themes/ :

<script type="text/discourse-plugin" version="0.8">
const decorateLines = (post) => {
  try {
    // Combinaisons de 'number', 'wrap', 'scroll' et/ou '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 utilise `<aside>` pour les citations d'autres posts du 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(' ');

      // Supposons qu'il n'y ait aucun écouteur d'événement sur la ligne,
      // et supprimons les lignes vides à la fin
      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>

Stylisation Sass SCSS

Ce qui suit prend en charge le retour à la ligne avec et sans numérotation, ainsi que le défilement avec ou sans numérotation et/ou ombres. Il supprime également la numérotation lorsqu’il n’y a qu’une seule ligne.

Cela n’ajoute pas de numéros de ligne aux citations d’autres posts, car si une telle citation n’est qu’une citation partielle, elle commencera toujours à numéroter à partir de 1. Pour ajouter la numérotation aux citations, supprimez :not(.lines-in-quote) de la ligne ci-dessous (qui apparaît plusieurs fois dans le SCSS) :

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

C’est également ici que la numérotation des blocs de code sur une seule ligne est supprimée.

Stylisation Sass du thème

Ajoutez ceci à la section « Common, CSS » de votre composant dans /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);
  // Pas de padding pour le PRE externe :
  // - les barres de défilement ne couvrent pas le CODE mais sont proches des bords extérieurs
  // - le texte est proche des bords lorsqu'il y a du contenu masqué
  // - idem pour les ombres
  padding: 0;
  // #f5f5f5 dans `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 {
    // Hauteur minimale pour obtenir la bordure et l'arrière-plan des lignes vides (Firefox
    // n'en a besoin que pour le ::before, mais Chrome aussi pour le span)
    min-height: 1.5em;

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

  &.lines-shadow {
    background-image:
      // Les deux premières couvertures défilent avec le contenu, apparaissant donc
      // devant les ombres lorsqu'aucun défilement n'est possible. Ces dernières
      // correspondent à la couleur d'arrière-plan pour les 25 % premiers, puis un dégradé
      // jusqu'à une transparence totale (en supposant que les couvertures sont 4 fois la
      // largeur des ombres).
      linear-gradient(to right, var(--lines-bgcolor) 25%, transparent),
      linear-gradient(to left, var(--lines-bgcolor) 25%, transparent),
      // Et derrière cela, deux ombres à des positions fixes, pour indiquer
      // le contenu défilable masqué si non couvert.
      linear-gradient(to right, var(--lines-shadow-color), transparent),
      linear-gradient(to left, var(--lines-shadow-color), transparent);
      // Ou :
      // 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:
      // Définir la couverture à 4 fois la largeur de l'ombre réelle, pour correspondre
      // au point d'arrêt à 25 % du dégradé de la couverture
      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 les numéros de ligne sont visibles, nous devons déplacer l'ombre
      --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);
    }
  }
}

Développement

Pour le développement, j’ai utilisé le code suivant, permettant de définir des paramètres de requête URL tels que ?avbPreview pour activer le crochet du décorateur JavaScript, et des choses comme ?avbPreview&avbClasses=number,scroll pour tester les numéros de ligne avec défilement mais sans ombres. Comme tout le CSS est encapsulé, cela ne devrait pas affecter les autres utilisateurs pendant les tests.

Version de développement/débogage
<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 utilise `<aside>` pour les citations d'autres posts du 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('===== Réécriture ; source\n' + elem.innerHTML.trim());
        console.log('===== Réécriture ; résultat\n' + elem.innerHTML.trim().replace(split, `<span class="${lineClass}">$1</span>`));
      }

      // Supposons qu'il n'y ait aucun écouteur d'événement sur la ligne,
      // et supprimons les lignes vides à la fin
      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>

Excellent travail ! :clap: :clap: :clap:

Merci beaucoup, Arjan ! Tout fonctionne aussi bien en thème clair par défaut qu’en thème sombre.


изображение

Partagez ma configuration :
css.txt (5.7 Ko)
head.txt (1.2 Ko)

Personnalisations CSS :

// Couleurs du code en ligne
// Pouces verts vers le haut
// Icône d'assigné verte
// En-têtes gris ou bleus
// Centrer les images
// Retour à la ligne dans le bloc de code + contenu complet
// Corriger les marges énormes des sujets
// Correction de la couleur de la police du composant de thème Gitlab (plus sombre)
// Composant de thème des saveurs Markdown Gitlab sur l'original <kbd>
// Numérotation des lignes dans les blocs de code par @Arjan

Composants installés :

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

Bien, c’était rapide ! Je suppose que vous voudrez utiliser --lines-bgcolor: white; pour vous débarrasser de certains gris. :sunglasses:

J’ai modifié mon message pour corriger le SCSS, afin d’appliquer correctement les ombres. Cela permet maintenant de définir --lines-shadow-width à n’importe quelle taille tout en continuant à faire apparaître/disparaître correctement.

Il comprend également un exemple pour radial-gradient maintenant. Mais cela ne changera pas grand-chose pour les longs blocs de code :