Second Brain documentation

ADR 0010 – Callouts et coloration du code : extension `marked`, highlight.js (core + 13 langages), classes autorisées explicitement

Accepté (2026-10-15)

← Toutes les décisions

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"> (open pour +) avec un <summary class="callout-title">. Le type passe par data-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 blockquote de marked (callout-marked.ts) ; le titre et le premier paragraphe repassent par la même instance marked (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 (alias html), yaml, avec leurs alias (ts, js, sh, yml, html…). Liste fermée, dans code-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-* en light-dark() sous .sb-markdown .hljs (styles globaux), donc clair/sombre automatique selon color-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 par PreservedInline : 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 marked en bloc (tokenizer) à la place du renderer blockquote : 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 style du rendu assaini.
  • Détection automatique du langage : lente sur de longs blocs, imprévisible.

Conséquences

  • Dépendance front ajoutée : highlight.js 11.12.0 (exacte), dans le chunk différé uniquement. Le chunk markdown-editor passe 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 :

  • callout et data-callout sur la citation, ce qui lui donne les couleurs et l’icône du rendu ;
  • callout-marker, data-callout-marker et data-label sur 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.