From cc33cf5fbc1823f01fbfe708979bba7e90557c43 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 06:37:52 +0000 Subject: [PATCH] =?UTF-8?q?Informations=20:=20=C3=A9diteur=20visuel=20?= =?UTF-8?q?=C3=A0=20la=20place=20de=20la=20syntaxe=20Markdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 dont Docs enveloppe tout document mettrait le texte entier en gras. - Un collage passe par DOMParser, dont le document est inerte : un 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 Claude-Session: https://claude.ai/code/session_014QsLjRAnuLwivqxCbM4WeP --- CLAUDE.md | 44 ++- src/app/backend/actions.ts | 4 +- .../backend/informations/InformationForm.tsx | 99 ++----- src/app/globals.css | 86 ++++++ src/components/RichTextEditor.tsx | 262 ++++++++++++++++++ src/lib/rich-text-dom.ts | 190 +++++++++++++ src/lib/rich-text.ts | 82 +++++- tests/rich-text-dom.test.ts | 204 ++++++++++++++ tests/rich-text.test.ts | 32 +++ tests/security.test.ts | 44 +++ 10 files changed, 956 insertions(+), 91 deletions(-) create mode 100644 src/components/RichTextEditor.tsx create mode 100644 src/lib/rich-text-dom.ts create mode 100644 tests/rich-text-dom.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 794bea9..fe92971 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -206,7 +206,9 @@ même raison. journal, limitation des connexions, chiffrement des secrets, coordonnées du référent, **mots de passe temporaires jamais mis en file**, secrets de messagerie jamais renvoyés au navigateur, aucune copie cachée dans la chaîne - d'envoi, et rien rendu en HTML brut côté informations. + d'envoi, rien rendu en HTML brut côté informations, et **le HTML de l'éditeur + visuel qui ne quitte jamais la page** (formulaire à champ caché, collage passé + par `DOMParser`, réinjection limitée à `richTextToEditorHtml`). ## Référencement (SEO) @@ -309,16 +311,38 @@ porte ce que publie le bureau depuis `/backend/informations`. Deux tables : `information_reads`. - **Texte riche** : `src/lib/rich-text.ts` est **pur** (`tests/rich-text.test.ts`) - et analyse un sous-ensemble de Markdown — `**gras**`, `*italique*`, `- puce`, - `1. numéro`, `[texte](https://…)`, `## sous-titre`. Deux rendus, un seul + et analyse un sous-ensemble de Markdown — `**gras**`, `_italique_`, `- puce`, + `1. numéro`, `[texte](https://…)`, `## sous-titre`. Trois rendus, un seul analyseur : `richTextNodes()` pour l'écran (des éléments React, jamais - `dangerouslySetInnerHTML`), `richTextToEmailHtml()` pour le message. L'aperçu - du formulaire passe par le premier, il est donc fidèle par construction. Un - lien hors `http`/`https` perd sa cible et ne garde que son libellé - (`safeHttpUrl`, partagé avec `email-templates.ts`). -- **Pas de WYSIWYG** : il produirait du HTML, qu'il faudrait stocker puis - assainir — seconde dépendance, seconde surface d'attaque. La saisie est un - `