Informations : éditeur visuel à la place de la syntaxe Markdown

La barre d'outils écrivait des étoiles dans un textarea : l'adhérent qui
rédigeait devait connaître une syntaxe pour obtenir du gras. On édite
désormais comme dans un traitement de texte — on sélectionne, on clique sur
G, le texte devient gras à l'écran.

Le format stocké ne change pas d'un octet. C'est tout l'enjeu : le WYSIWYG
porte sur la saisie, pas sur le stockage. À chaque frappe, le navigateur
retraverse ce qu'il a édité (serializeToRichText) et n'en garde que le gras,
l'italique, les listes, les sous-titres et les liens ; le balisage part dans
un champ caché et le serveur le ré-analyse avec l'analyseur existant. Le HTML
de l'éditeur ne quitte jamais la page, donc aucun assainisseur HTML n'entre
dans le produit — le risque n'a jamais été la surface d'édition, mais la base.

- rich-text-dom.ts : sérialiseur pur, testable sans navigateur (MinimalNode).
  Il lit aussi le gras codé en style CSS, pour qu'un brouillon collé depuis
  Google Docs ne perde pas sa mise en forme ; le style explicite l'emporte sur
  la balise, sinon le <b style="font-weight:normal"> dont Docs enveloppe tout
  document mettrait le texte entier en gras.
- Un collage passe par DOMParser, dont le document est inerte : un <img
  onerror> collé ne s'exécute pas. Seul du HTML produit par nous est réinjecté.
- L'italique s'écrit _ainsi_ : **_x_** est lisible par l'analyseur là où ***x***
  serait ambigu. Un tiret bas au milieu d'un mot n'ouvre rien.
- saveInformation normalise les CRLF que l'encodage des formulaires introduit.

Vérifié au navigateur contre une base réelle : aller-retour identique après
enregistrement, et un collage hostile (script, img onerror, iframe, style,
lien javascript:) ressort en texte structuré sans qu'une seule alerte parte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QsLjRAnuLwivqxCbM4WeP
This commit is contained in:
Claude committed 2026-09-15 06:37:52 +00:00
1 parent 90b121a3c1
commit cc33cf5fbc
10 files changed
+956 -91

No files matched your search

+32
View File
@@ -5,6 +5,7 @@ import {
parseRichText,
richTextExcerpt,
richTextNodes,
richTextToEditorHtml,
richTextToEmailHtml,
richTextToPlain,
safeHttpUrl,
@@ -164,3 +165,34 @@ test("une saisie vide ne produit aucun bloc", () => {
assert.equal(richTextToEmailHtml(""), "");
assert.equal(richTextExcerpt(""), "");
});
test("le tiret bas vaut italique, mais seulement en bord de mot", () => {
assert.ok(richTextToEmailHtml("_doux_").includes("<em>doux</em>"));
assert.ok(richTextToEmailHtml("**_gras italique_**").includes("<em>gras italique</em>"));
// Un nom de fichier ne doit pas devenir italique en son milieu.
assert.equal(richTextToPlain("fichier_de_sauvegarde.pdf"), "fichier_de_sauvegarde.pdf");
assert.ok(!richTextToEmailHtml("fichier_de_sauvegarde.pdf").includes("<em>"));
assert.equal(richTextToPlain("un _ seul"), "un _ seul");
});
test("le rendu vers l'éditeur reprend les balises que le sérialiseur sait relire", () => {
const html = richTextToEditorHtml("## Titre\n\nDu **gras** et un [lien](https://pleinr.fr)\n\n- un\n- deux\n\n1. a");
assert.ok(html.includes("<h4>Titre</h4>"));
assert.ok(html.includes("<strong>gras</strong>"));
assert.ok(html.includes('<a href="https://pleinr.fr/">lien</a>'));
assert.ok(html.includes("<ul><li>un</li><li>deux</li></ul>"));
assert.ok(html.includes("<ol><li>a</li></ol>"));
});
test("un contenu vide offre une ligne où écrire", () => {
assert.equal(richTextToEditorHtml(""), "<p><br></p>");
assert.equal(richTextToEditorHtml(" "), "<p><br></p>");
});
test("le rendu vers l'éditeur échappe le texte et refuse les cibles hostiles", () => {
const html = richTextToEditorHtml('<script>alert(1)</script> & "guillemets"');
assert.ok(html.includes("&lt;script&gt;"));
assert.ok(html.includes("&amp;"));
assert.ok(!html.includes("<script"));
assert.ok(!richTextToEditorHtml("[x](javascript:alert(1))").includes("<a "));
});