ADR 0010 – Callouts et coloration du code : extension `marked`, highlight.js (core + 13 langages), classes autorisées explicitement
Accepté (2026-10-15)
ADR 0010 – Callouts et coloration du code : extension marked, highlight.js (core + 13 langages), classes autorisées explicitement
Statut : Accepté (2026-10-15)
Décisions prises en mode autonome (Aurélien, 2026-10-15 : options les plus simples et sûres). Elles s’ajoutent à l’ADR 0008 (rendu assaini, > [!type] préservé) sans changer le format stocké.
Contexte
Le cahier (jalon 1, « Callouts et code ») demande des encadrés info/attention et la coloration syntaxique. L’ADR 0008 conserve déjà > [!type] (nœud PreservedInline) et la langue des blocs de code (```ts) ; il laisse le rendu des callouts et la coloration à cette tranche. Le rendu passe par marked puis DOMPurify, dont le hook afterSanitizeAttributes retirait jusqu’ici toute classe sauf language-* sur code.
Décision
Callouts (rendu)
- Syntaxe façon Obsidian : une citation dont la première ligne est
[!type](lettres et-, insensible à la casse), suivie d’un repli facultatif+(ouvert) ou-(replié) et d’un titre facultatif en Markdown en ligne :> [!warning]- Titre. Le reste de la citation est le corps, en Markdown complet (listes, code, citations imbriquées, liens wiki). - Types reconnus (13) avec leurs alias Obsidian :
note,abstract(summary,tldr),info,todo,tip(hint,important),success(check,done),question(help,faq),warning(caution,attention),failure(fail,missing),danger(error),bug,example,quote(cite). Chaque type a un titre par défaut en français, une icône Material Symbols et une couleur. - Type inconnu : rendu comme
note, avec le nom saisi (première lettre en capitale) comme titre par défaut. Une citation dont la première ligne n’est pas[!type]reste une citation. - HTML produit :
<div class="callout" data-callout="<type connu>" role="note"><div class="callout-title">…</div><div class="callout-body">…</div></div>; un callout repliable est un<details class="callout">(openpour+) avec un<summary class="callout-title">. Le type passe pardata-callout, valeur toujours tirée de la liste fermée (jamais le texte saisi). Le titre est du texte visible, donc lisible par un lecteur d’écran sans libellé supplémentaire ; l’icône (pseudo-élément CSS) est décorative. - Implémentation : un renderer
blockquotedemarked(callout-marked.ts) ; le titre et le premier paragraphe repassent par la même instancemarked(parseInline), donc par le même assainissement final.
Assainissement : classes autorisées explicitement
Le hook ne retire plus toutes les classes : il applique une règle par balise (CLASS_RULES) et supprime le reste. Autorisé : code : [hljs ]language-* ; span : jetons hljs-* et suffixes xxx_ ; div : callout, callout-title, callout-body ; details : callout ; summary : callout-title. Les classes de l’application ou de Material posées par du HTML brut restent retirées. Aucun style en ligne (l’attribut style reste interdit) : les couleurs viennent de variables CSS.
Coloration : highlight.js « core + langages choisis »
| Poids dans le chunk | Sortie | Licence, maintenance | |
|---|---|---|---|
| highlight.js 11 (retenu) | core + 13 langages, mesuré à +74 Ko bruts / +20 Ko gzip dans le chunk de l’éditeur | <span class="hljs-…">, texte échappé |
BSD-3-Clause, projet très ancien et actif |
| shiki | moteur Oniguruma (WASM) ou JS, grammaires TextMate, thèmes en JSON : plus lourd, chargement asynchrone | spans avec style="color:…" par défaut |
MIT, actif ; le style en ligne est exclu par notre politique |
| prism | léger | spans avec classes token |
MIT, évolution lente ; pas de détection, mêmes classes pour tous les thèmes |
Retenu : highlight.js, parce qu’il produit uniquement des classes (compatibles avec l’assainissement et un thème clair/sombre par CSS), s’importe par langage (highlight.js/lib/core) et reste synchrone, ce que demande renderMarkdown. Version exacte 11.12.0 (comme les @tiptap/*) : les jetons de classe sont du contrat avec le CSS, une montée de version se fait volontairement.
- Langages chargés (13) :
bash,css,diff,java,javascript,json,markdown,plaintext,python,sql,typescript,xml(aliashtml),yaml, avec leurs alias (ts,js,sh,yml,html…). Liste fermée, danscode-highlight.ts. - Pas de détection automatique : un bloc sans langue ou de langue inconnue reste du texte (ni coût, ni fausses couleurs). La sortie est
<pre><code class="hljs language-x">. - Thème : variables
--hl-*enlight-dark()sous.sb-markdown .hljs(styles globaux), donc clair/sombre automatique seloncolor-scheme, sans feuille de style highlight.js. - Chargement : highlight.js n’est importé que par le module de rendu, donc par le chunk différé de l’éditeur et de l’aperçu (vérifié dans le build : absent du bundle initial). Les futurs dashboards qui rendent du Markdown le tireront dans leur propre chunk paresseux.
Éditeur visuel
- La syntaxe
> [!type], le repli et le titre restent préservés tels quels parPreservedInline: l’éditeur affiche une citation dont la première ligne porte le marqueur, sans rendu en encadré. Pas de régression d’aller-retour (cas ajoutés à la table). - Reporté : callout rendu comme encadré dans l’éditeur visuel, et coloration des blocs de code de l’éditeur (extension
lowlight+ décorations ProseMirror). Les deux demandent un nœud ou un plugin Tiptap dédié et un nouveau test d’aller-retour ; ils s’ajouteront sans changer le format stocké. En attendant, ils s’affichent colorés en mode aperçu.
Alternatives écartées
- Extension
markeden bloc (tokenizer) à la place du rendererblockquote: plus de code et un second lexer pour un résultat identique, la citation étant déjà lexée. - Type de callout libre dans l’HTML (
class="callout-<type>") : laisserait le texte saisi dans un attribut de classe. - Styles en ligne de shiki : contraires à l’interdiction de
styledu rendu assaini. - Détection automatique du langage : lente sur de longs blocs, imprévisible.
Conséquences
- Dépendance front ajoutée :
highlight.js11.12.0 (exacte), dans le chunk différé uniquement. Le chunkmarkdown-editorpasse d’environ 591 à 666 Ko bruts (162 à 182 Ko estimés transférés). Le CSS global augmente d’environ 3,6 Ko bruts (0,7 Ko transférés) à cause des callouts et du thème de code ; le bundle initial JS ne bouge pas. - Coût de highlight.js : il est quadratique sur certaines entrées (un bloc bash de 200 000 « 1 » prend environ 19 s). Un bloc de plus de 20 000 caractères reste du texte brut, et un rendu colore au plus 100 000 caractères au total (compteur remis à zéro à chaque
renderMarkdown, car l’aperçu se relance à chaque frappe). Au-delà, les blocs sont affichés sans couleur. - Un callout peut être forgé en HTML brut (
<div class="callout" data-callout="…">) : sans risque, les classes et attributs sont de simples habillages et le reste passe par l’assainissement ; assumé. - Un callout dont le corps est vide s’affiche avec son titre seul.
- Ajouter un type de callout ou un langage est une modification de code (liste fermée) : le domaine au sens de
config/n’est pas concerné, ces types sont du Markdown générique.
Complément (2026-10-15) : callouts dans l’éditeur visuel
Le marqueur [!type] reste un nœud preservedInline : il est sérialisé à l’identique, et aucun nœud Tiptap n’est ajouté. La décoration CalloutDecoration agit seulement sur une citation dont le premier paragraphe commence par un marqueur sans marque, comme au rendu. Elle pose :
calloutetdata-calloutsur la citation, ce qui lui donne les couleurs et l’icône du rendu ;callout-marker,data-callout-markeretdata-labelsur le marqueur de tête. Le CSS remplace alors la valeur brute par l’icône et le libellé du type.
Un marqueur placé ailleurs (au milieu d’un texte, dans une liste, le second de deux marqueurs) reste affiché brut, comme dans l’aperçu.
La table des types et des alias vit dans callout-kinds.ts, partagé par le rendu et l’éditeur. Restent reportés :
- le repli
+/-dans l’éditeur, qui y est affiché ouvert ; - la coloration du code dans l’éditeur ;
- une commande d’insertion de callout.