mirror of
https://github.com/R0m1k3/PleinR.git
synced 2026-10-11 17:27:54 +02:00
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:
10 files changed
+956
-91
No files matched your search
@@ -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
|
||||
`<textarea>` avec une barre qui encadre la sélection (`setRangeText`).
|
||||
`dangerouslySetInnerHTML`), `richTextToEmailHtml()` pour le message,
|
||||
`richTextToEditorHtml()` pour remplir l'éditeur. Un lien hors `http`/`https`
|
||||
perd sa cible et ne garde que son libellé (`safeHttpUrl`, partagé avec
|
||||
`email-templates.ts`). L'italique s'écrit avec des tirets bas : `***x***`
|
||||
serait ambigu pour l'analyseur, `**_x_**` ne l'est pas ; `*étoiles*` reste
|
||||
accepté en lecture, et un tiret bas au milieu d'un mot
|
||||
(`fichier_de_sauvegarde`) n'ouvre rien.
|
||||
- **La syntaxe ne se montre jamais** : `RichTextEditor` est un
|
||||
`contentEditable` avec une barre d'outils (G, I, Sous-titre, listes, lien).
|
||||
L'adhérent qui rédige voit du gras, pas des étoiles. Le balisage voyage dans
|
||||
un `<input type="hidden">`.
|
||||
- **Le WYSIWYG porte sur la saisie, pas sur le stockage.** C'est l'invariant à
|
||||
ne pas casser : à chaque frappe, `src/lib/rich-text-dom.ts`
|
||||
(`serializeToRichText`, **pur**, `tests/rich-text-dom.test.ts`) retraverse le
|
||||
contenu édité et n'en garde que le gras, l'italique, les listes, les
|
||||
sous-titres et les liens. Le HTML de l'éditeur ne quitte jamais la page : la
|
||||
base ne reçoit que le format balisé, que le serveur ré-analyse avec le même
|
||||
analyseur qu'avant. Aucun assainisseur HTML n'est donc nécessaire.
|
||||
Le module lit aussi le gras/italique **codés en style CSS**
|
||||
(`<span style="font-weight:700">` de Google Docs), le style explicite
|
||||
l'emportant sur la balise — sans quoi le `<b style="font-weight:normal">` dont
|
||||
Docs enveloppe tout document mettrait le texte entier en gras.
|
||||
- **Un collage est analysé par `DOMParser`**, jamais par une affectation
|
||||
d'`innerHTML` : le document produit est inerte, donc un `<img onerror>` collé
|
||||
ne s'exécute pas. Ce qui est réinjecté dans l'éditeur est du HTML **produit
|
||||
par nous** (`richTextToEditorHtml`), jamais celui du presse-papier.
|
||||
- **Les sauts de ligne sont normalisés à l'enregistrement** : l'encodage des
|
||||
formulaires HTML convertit tout `\n` en `\r\n`, et le balisage stocké
|
||||
différerait sinon de celui que l'éditeur a produit (`saveInformation`).
|
||||
- **Une seule image**, en couverture, jamais dans le corps : une data-URI
|
||||
recopiée dans chaque ligne de la file pèserait 3 Mo par destinataire. Elle est
|
||||
servie aux clients mail par `/api/informations/[id]/image`, qui ne répond que
|
||||
|
||||
Reference in new issue
Block a user