J'ai maintenant écris quelques entrées de journal (presqu'une dizaine) et je commence à avoir une petite idée de ce qui me manque pour écrire des articles :
- Coloration syntaxique pour le code
- Des notes de bas de page
- Changement de langue au sein du document
- Gestion des liens externes (automatique ou manuelle)
- Une table des matières
- Des remplacements automatiques (ajout d'espaces insécables, etc.)
Coloration syntaxique pour le code
Intégrer la librairie
Pour la coloration syntaxique, il semble que le consensus soit d'utiliser la librairie prism JS (je l'ai retrouvé sur plusieurs sites utilisant une syntaxe Markdown), d'ailleurs eleventy propose un plugin qui exécute tout le traitement du code par prism JS lors de la génération du site (évite d'avoir à importer toute la bibliothèque JS dans nos pages) : Documentation du plugin Eleventy(S'ouvre dans un nouvelle fenêtre)
L'installation est très simple : il faut juste que je n'oublie pas l'attribut tabindex="0" sur la balise <pre>[1].
import syntaxHighlight from "@11ty/eleventy-plugin-syntaxhighlight";
export default async function(eleventyConfig) {
eleventyConfig.addPlugin(syntaxHighlight, {
preAttributes: {
tabindex: 0
}
});
}
Là où je rencontre un souci, c'est sur l'ajout du style de prism JS. Le plugin m'a bien installé les dépendances nécessaires, mais ces fichiers sont dans mon dossier /node_modules/ et ne seront pas importés.
Pour régler ça, j'ai tenté de créer un fichier Liquid dont le seul rôle aurait été d'exposer le CSS du thème choisi et de l'importer dans le dossier /_src/css/ de mon site :
---
permalink: prism-theme.css
---
{% render "/node_modules/prismjs/themes/prism-okaidia.min.css" %}
Ce fut un échec cuisant. Il semble que dans Eleventy, Liquid ne génère que des fichiers HTML. J'ai vu des sources qui semblaient indiquer le contraire mais j'ai bien l'impression que ce n'est pas possible ici.[2]
Reconfiguration du projet pour insérer des styles avec Sass
Je me rabat donc sur la solution que je connais déjà, le préprocesseur Sass, qui est supporté par Eleventy. Ça implique aussi de supprimer ma mécanique actuelle de copie passthrough du css (mais je garde cette mécanique pour les polices de caractères).
import path from "node:path";
import * as sass from "sass";
export default async function(eleventyConfig) {
eleventyConfig.addTemplateFormats("scss")
eleventyConfig.addExtension("scss", {
outputFileExtension: "css",
useLayouts: false, // opt-out of Eleventy Layouts
compile: async function (inputContent, inputPath) {
let parsed = path.parse(inputPath);
// Don’t compile file names that start with an underscore
if(parsed.name.startsWith("_")) {
return;
}
let result = sass.compileString(inputContent, {
loadPaths: [
parsed.dir || ".",
this.config.dir.includes,
"node_modules/prismjs/themes" // Where to find prismJS themes
],
style: "compressed"
});
// Map dependencies for incremental builds
this.addDependencies(inputPath, result.loadedUrls);
return async () => {
return result.css;
};
},
});
}
Pour que mes fichiers SCSS soient traités, je suis obligé de les déplacer dans mon dossier content, en plus d'ajouter un fichier de donnée Eleventy pour éviter que mon fichier scss soit intégré aux collections de page d'Eleventy :
/src
/font
/…
/content
/src
/css
/base.scss
/css.json (contient juste la propriété "eleventyExcludeFromCollections": true)
/… (pages et articles du site)
Je ne suis pas fan de l'arborescence résultante mais tant pis. Au moins cela fonctionne. Et grâce au chemin vers les fichiers node de prism JS que j'ai ajouté dans la propriété loadPaths, il est super simple d'ajouter le thème de mon choix au CSS du site :
// base.scss
@use "sass:meta";
code {
// j'ai ajouté cette règle pour avoir un style approchant dans les morceaux de codes intégrés directement dans le texte.
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
background: #272822;
color: #f8f8f2;
}
@include meta.load-css("prism-okaidia");
Note : Initialement, j'avais utilisé la fonction @import mais il semble que cette fonction soit dépréciée : Breaking Change: @import and global built-in functions(S'ouvre dans un nouvelle fenêtre)
Reste à faire accessibilité
Il manque encore quelque chose pour parfaire l'accessibilité de ces zones. Il faudrait que j'ajoute la possibilité d'ajouter une description de la zone, comme l'évoque Steve Faulkner dans son article Short note on improving usability of scrollable regions(S'ouvre dans un nouvelle fenêtre).
Mais le plugin eleventy ne propose pas l'ajout de plugins prism JS pour personnaliser son intégration (au-delà des attributs statiques comme tabindex="0"). Une solution serait de créer un shortcode(S'ouvre dans un nouvelle fenêtre) avec ma propre intégration. Comme cela semble assez complexe, je laisse ce point de côté pour l'instant et reviendrait dessus ultérieurement.
Notes de bas de page
Je craignais que l'extension de markdown soit complexe, en particulier pour l'ajout des notes de bas de page mais au final, après un peu de recherches c'est assez simple :
- Il suffit d'installer et importer le plugin voulu (la documentation de bibliothèque "Markdown IT" liste les plugin compatibles(S'ouvre dans un nouvelle fenêtre))
- Ensuite, une simple ligne dans notre fichier de configuration suffit à ajouter l’extension
import markdownIt from "markdown-it";
import markdownItFootnote from "markdown-it-footnote";
export default async function(eleventyConfig) {
// Setup Markdown
let options = {
html: true,
breaks: true,
linkify: false
};
eleventyConfig.setLibrary("md", markdownIt(options));
// Adding footnote support :
eleventyConfig.amendLibrary("md", (mdLib) => mdLib.use(markdownItFootnote));
}
Changement de langue au sein du document
Du fait de la nature de mes articles[3], j'utilise beaucoup de termes anglais et devoir saisir à chaque fois une balise <span> ou <em> est vite fatiguant.
Pour résoudre cela, j'ai hésité entre plusieurs approches, notamment :
- Ajouter ma propre syntaxe (par exemple :
(english word)(en)) - Ajouter une liste de mots récurrents à encapsuler automatiquement
Mais je n'ai pour l'instant trouvé aucune ressources "clefs en main" me permettant de faire cela, et ces approches me semblent assez lourdes à implémenter. Je vais donc les laisser dans un coin de ma tête et opter pour une approche plus générique, et qui aura en plus le mérite d'être utile dans plein d'autre cas : l'injection d'attribut avec @mdit/plugin-attrs(S'ouvre dans un nouvelle fenêtre)
Comme pour les notes de bas de page, l'installation est très simple et le plugin répond à mon attente première : je peux maintenant saisir simplement _english word_{lang=en}. Ce n'est pas aussi concis que ce que j'envisageais initialement mais c'est déjà bien plus confortable que d'avoir à saisir des balises HTML.
Gestion des liens externes
Le besoin
Avec l'injection d'attributs, je suis en mesure de gérer les liens externes, mais c'est encore assez fastidieux, car il faut à chaque fois :
- Ajouter l'attribut
target="_blank" - Ajouter l'attribut
rel="noopener"[4] - Ajouter la mention "(Ouverture dans un nouvel onglet)" en texte masqué (pas dans un attribut
title, parce que lestitle, c'est nul[5])
J'ai donc cherché des bibliothèque qui permettent d'étoffer automatiquement tous les liens pointant vers l'extérieur. Malheureusement aucune ne me convient, pour les raisons suivantes :
- Aucune ne gère l'ajout de la mention "(Ouverture dans un nouvel onglet)"
- Elles ont en général la main lourde, appliquant aussi l'attribut
rel="noreferrer"pour empêcher les sites vers lesquels je renvois de savoir que le traffic vient de chez moi.[6] - Elles surchargent (plus ou moins violemment) le code généré par Markdown
Créer mon propre Transformer
J'ai donc décidé de m'inspirer d'un de ces plugins pour construire un Transformer qui réponde à mes propres besoins. J'ai choisi comme source d'inspiration ce plugin : @sardine/eleventy-plugin-external-links(Opens in a new window)
La première étape est d'installer la dépendance utilisée par @sardine dans son script : linkedom(Opens in a new window). Cette librairie est sensée faciliter le parcours du HTML pour trouver et éditer les liens (le transformer s’exécutera sur le HTML final, une fois le Markdown converti).
Je reprends ensuite son code pour récupérer les liens du documents (je réécrirais seulement le traitement effectué sur les liens). Au lieu de simplement appliquer target="_blank" à tous les liens externes, je vérifie le contenu de l'attribut et adapte le traitement en fonction :
switch(link.target) {
case "":
if (/^(https?:\/\/|\/\/)/i.test(link.href)) { // If is an external link
link.target = "_blank"; // Add target="_blank"
// Add hidden text
}
case "_blank":
// Add hidden text (for cases where target was set manually)
break;
default: // Do nothing and keep the attribute value the author set manually
}
Il ne me reste plus qu'à ajouter le code de mon texte caché :
let hiddenMessage = document.createElement("span")
hiddenMessage.textContent = hiddenMessageI18n(link.lang) // Poorly written i18n function I'll have to rewrite later
hiddenMessage.className = "visually-hidden"
// Add hidden text
link.appendChild(hiddenMessage)
Mon Transformer est maintenant prêt, il ne reste plus qu'à le brancher dans le générateur de site.
Ajouter le Transformer dans eleventy
Le branchement est assez facile, je dois seulement faire attention à deux points :
- Traiter uniquement les fichiers HTML
- Quoi qu'il arrive, toujours renvoyer le contenu passé à la fonction (même si aucune transformation n'a été effectuée)
Je m’inspire à nouveau du travail de @sardine et obtiens un Transformer fonctionnel cette configuration :
import { extlinks } from "./transformers.js";
export default async function(eleventyConfig) {
eleventyConfig.addTransform(
"auto-external-links",
(content, outputPath) => {
try {
if (outputPath?.endsWith(".html")) {
content = extlinks(content);
};
} catch (error) {
console.error(error);
};
return content;
}
);
}
Touche finale : un peu de mise en forme
Ma génération automatique de lien externes accessibles est presque prête ! Il ne me manque plus qu'un tout petit détail : le CSS pour masquer ce texte !
Heureusement je sais où aller chercher : Masquage accessible de pointe(S'ouvre dans un nouvelle fenêtre)
Je pourrais m'arrêter là, mais j'aimerais bien que mes utilisateurices voyants aient aussi l'information que le lien s'ouvre dans un nouvel onglet, et sans se reposer sur le peu fiable attribut title[5:1]. Je décide donc d'ajouter un petit symbole (que je masquerais aux lecteurs d'écrans) : U+238B ⎋
L'ajout se fait facilement, avec la même méthode que précédemment. Le seul point remarquable est que linkedom ne semble pas supporter la propriété aria-hidden, il faut donc passer par la méthode setAttribute()
let visibleMarker = document.createElement("span")
visibleMarker.innerHTML = "⎋"
visibleMarker.className = ("visible-marker")
visibleMarker.setAttribute("aria-hidden", "true")
link.appendChild(visibleMarker)
Note : L'orientation de la flèche ne me plaisait pas, alors j'ai voulu tourner le symbole. Il est intéressant à noter que la propriété rotate ne fonctionne pas avec les contenus inline. Pour permettre la rotation, j'ai dû appliquer display: inline-block;
Autre petit souci CSS, le rendu du caractère Unicode change selon l'appareil (mais ça, c'est le risque quand on souhaite faire l'économie d'une image svg bien maîtrisée). Sur mon iPhone en particulier, le caractère est vraiment trop petit. Je suis donc allé chercher un petit hack pour augmenter sa taille uniquement sur iPhone :
@supports (-webkit-text-size-adjust:none) and (font: -apple-system-body) {
.visible-marker {
font-size: 1.5em;
line-height: 0;
}
}
Je n'aime pas trop utiliser ce genre de hack (c'est fragile) mais l'enjeu est faible, avec un fort bénéfice en confort de lecture. Donc je m'y autorise, pour cette fois !
Reste à faire
Je n'ai pas fini de traiter tous les points que je m'étais fixé pour aujourd'hui (pour cette année ?) :
- Une table des matières
- Encore plus de remplacements automatiques
Mais ces points seront à traiter un autre jour…
Rendre les balises
<pre>focusable est nécessaire pour les personnes n'utilisant pas la souris. Lorsque qu'un défilement horizontal est actif dans une zone de la page (cas fréquent avec les extraits de code), ces utilisateurices ont besoin de pouvoir déplacer le focus sur la zone défilable. Sans ça il leur est impossible de défiler latéralement et révéler le contenu masqué. ↩︎D'ailleurs la doc d'Eleventy ne propose que des intégrations de CSS inline ou avec des préprocesseurs dédiés. ↩︎
Et c'est sans compter mon côté tatillon qui me pousse à préférer les sources en langue original et à utiliser les termes originaux quand je ne suis pas d'accord avec leur traduction. ↩︎
Je ne suis pas sûr que cet attribut soit encore nécessaire. Apparemment les navigateurs modernes traitent tous les liens avec
target="_blankcomme un lien noopener(Opens in a new window) ↩︎Le support de l'attribut
titlevarie énormément en fonction du lecteur d'écran et du mode de navigation. C'est pourquoi je préfère l'usage d'un texte supplémentaire, masqué aux voyants.
Pour plus de détails à ce sujet, voir cet article de François-Xavier Lair : Title, ce faux ami de l’accessibilité(S'ouvre dans un nouvelle fenêtre) ↩︎ ↩︎En vrai, moi ça me ferait plaisir que les gens que je cite savent qu'ils le sont, cités. (Et dans le cas où je parlerais de gens que je ne peux pas voir en photo, faites moi confiance pour ajouter
rel="noreferrer"à la main) ↩︎