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. ![]()
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>




