Contenu et mécanisme uniquement, aucun style ajouté (voir consigne du 24/09/2026) : deux nouveaux kinds de contenu (badge/carte, rendu en div brutes sans CSS), un mode SVG inline pour l'image (svg_markup, nettoyé par un nouveau sanitizer allow-list avant chaque rendu) et un fichier téléchargeable joignable à un bouton (upload/download routes, stockage sous db.support_dir). Le futur système de templates portera l'habillage visuel de ces éléments. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
185 lines
12 KiB
Markdown
185 lines
12 KiB
Markdown
# document_engine/rendering/
|
|
|
|
Rendu HTML du document — point d'entrée unique utilisé à la fois par le
|
|
canevas d'édition et par le Mode Aperçu (même fonction, voir
|
|
`docs/plan/PLAN.md` — "Aperçu réel").
|
|
|
|
## `render_document(elements: list[dict[str, Any]]) -> str`
|
|
Assemble le document entier à partir de la liste à plat renvoyée par
|
|
`document_engine.list_document_elements` : regroupe les éléments par
|
|
`parent_id`, puis rend récursivement les éléments top-niveau dans
|
|
l'ordre (une rangée rend elle-même ses propres enfants côte à côte).
|
|
- **Retour** : le HTML complet du document.
|
|
- **Exceptions** : aucune.
|
|
|
|
## `render_document_element(el: dict[str, Any], children_by_parent: dict[int | None, list[dict[str, Any]]]) -> str`
|
|
HTML d'un seul élément — dispatch par dict (table `_RENDERERS`) selon
|
|
`el["kind"]`, plutôt qu'un enchaînement de `if` (mirroir de l'esprit de
|
|
`game_engine/scenes/render_scene_object.py`). `children_by_parent` est le
|
|
regroupement précalculé par `render_document`, transmis pour que les
|
|
rangées puissent rendre récursivement leurs enfants sans refaire le
|
|
regroupement à chaque appel.
|
|
- **Retour** : le HTML de cet élément (et de ses enfants s'il s'agit
|
|
d'une rangée).
|
|
- **Exceptions** : aucune ; un `kind` inconnu produit un bloc
|
|
`docUnknown` visible plutôt qu'une levée d'exception.
|
|
|
|
### Détail des rendus par catégorie (fonctions privées, table de dispatch)
|
|
- **Rangée** (`row`) : conteneur flex (`gap`/`align-items`/
|
|
`justify-content` réels depuis `attributes`), enfants rendus
|
|
récursivement.
|
|
- **Texte** (`titre`/`paragraphe`) : `<div>` stylé selon `style` (préréglage
|
|
taille/graisse/interligne) et `bold`/`italic`/`underline`/`align`/`color`.
|
|
- **Image** : `<img>`, ou un bloc placeholder si `src` est vide — OU, si
|
|
`attributes["svg_markup"]` est non vide (prioritaire sur `src`), un
|
|
`<div>` portant directement ce fragment SVG nettoyé par
|
|
`sanitize_svg_markup` (voir `sanitize_svg_markup.py` ci-dessous) : un
|
|
contenu vectoriel dessiné/collé par le créateur plutôt qu'un fichier
|
|
hébergé, sans aucun style qui lui soit propre.
|
|
- **Bouton** : `<button>` avec son `label`, un `data-target` optionnel
|
|
(navigation) et un `data-attachment-filename` optionnel — marqueur
|
|
mécanique posé quand un fichier a été joint (voir
|
|
`routes/document/document_element_upload_attachment.py`), jamais
|
|
l'URL de téléchargement elle-même (ce renderer ne connaît pas le slug
|
|
du support ; `static/document/js/document-editor.js` l'assemble à
|
|
partir de `data-element-id` + `FORGE_DOCUMENT.slug`, même principe que
|
|
le reste des appels AJAX de l'éditeur).
|
|
- **Étiquette** (`badge`) : `<div>` portant `attributes["content"]`
|
|
échappé — contenu pur, aucun attribut de style.
|
|
- **Carte** (`carte`) : `<div>` composé de trois blocs enfants
|
|
(`label`/`title`/`description`, tous échappés) — contenu pur, aucune
|
|
couleur/forme choisie ici (voir `element_kind_labels.md`).
|
|
- **Liste à puces/numérotée** (`liste_puces`/`liste_numerotee`) :
|
|
`<ul>` ou `<ol>` selon le `kind` (fonction privée `_render_list`,
|
|
partagée par les deux) — un `<li>` par entrée de `attributes["items"]`.
|
|
Une liste vide rend `<ul>`/`<ol>` sans enfant plutôt qu'un placeholder :
|
|
contrairement à une image sans fichier, ce n'est pas un état anormal.
|
|
- **Quiz** : toujours une carte résumant la config réelle (nombre de
|
|
questions, total des points via `quiz_total_points`, minuteur si
|
|
activé) — sanitizée (`sanitize_quiz_config`) avant lecture, jamais un
|
|
rendu direct d'`attributes` brut. Si au moins une question existe,
|
|
s'y ajoute (fonction privée `_render_quiz_player`) le questionnaire
|
|
RÉEL et interactif affiché en Mode Aperçu (voir docs/plan/maquettes/
|
|
document-formation-web.html — référence visuelle), masqué en édition
|
|
par CSS (`.docQuizPlayer`, voir static/document/document-editor.css) :
|
|
les questions sont embarquées en JSON dans un attribut `data-quiz-config`
|
|
(jamais un `<script>` par élément), échappé pour l'HTML
|
|
(`html.escape(..., quote=True)`) — static/document/js/document-editor.js
|
|
lit cet attribut et gère tout le déroulé (réponse/score/question
|
|
suivante/résultat) côté client, sans aucun aller-retour serveur.
|
|
- **Association** : toujours une carte résumant la config réelle (nombre
|
|
de paires) — sanitizée (`sanitize_association_config`) avant lecture.
|
|
Si au moins une paire existe, s'y ajoute (fonction privée
|
|
`_render_association_player`) le plateau de glisser-déposer RÉEL et
|
|
interactif affiché en Mode Aperçu, masqué en édition par CSS
|
|
(`.docAssocPlayer`) : les deux colonnes (termes/correspondances) sont
|
|
mélangées INDÉPENDAMMENT (`random.shuffle`, mélange d'affichage — voir
|
|
`CODE_QUALITY.md`) puis embarquées en JSON dans un attribut
|
|
`data-assoc-config`, échappé pour l'HTML — même principe que le Quiz,
|
|
aucun aller-retour serveur pendant qu'on joue. Contrairement au Quiz,
|
|
le plateau reste affiché en permanence une fois la partie terminée :
|
|
seul le bouton "Recommencer" (`.docMinigameRestartBar`, partagé avec
|
|
Memory) apparaît, jamais d'écran de résultat séparé qui le
|
|
remplacerait.
|
|
- **Memory** : toujours une carte résumant la config réelle (nombre de
|
|
cartes définies, mode paire/simple) — sanitizée
|
|
(`sanitize_memory_config`) avant lecture. Si au moins une carte existe,
|
|
s'y ajoute (fonction privée `_render_memory_player`) le plateau de
|
|
retournement RÉEL et interactif : en mode `"paire"`, chaque carte
|
|
définie est DUPLIQUÉE en deux instances partageant le même
|
|
`card_index` (l'appariement se fait dessus, classique Memory) ; en mode
|
|
`"single"`, une seule instance par carte (simple retournement, sans
|
|
appariement). Les instances sont mélangées (`random.shuffle`, mélange
|
|
d'affichage — voir `CODE_QUALITY.md`) puis embarquées en JSON dans un
|
|
attribut `data-memory-config`, échappé pour l'HTML — même principe que
|
|
le Quiz/l'Association, aucun aller-retour serveur pendant qu'on joue.
|
|
Comme l'Association, le plateau reste affiché une fois toutes les
|
|
paires trouvées (ou toutes les cartes révélées en mode `"single"`) :
|
|
seul le bouton "Recommencer" (`.docMinigameRestartBar`) apparaît.
|
|
- **Mots mêlés** : toujours une carte résumant la config réelle (nombre
|
|
de mots) — sanitizée (`sanitize_mots_config`) avant lecture. Si au
|
|
moins un mot existe, s'y ajoute (fonction privée `_render_mots_player`)
|
|
la grille RÉELLE et interactive. La grille ET la position exacte de
|
|
chaque mot sont calculées ICI côté serveur (`_build_mots_grid`, jamais
|
|
recalculées côté client) : chaque mot est placé horizontalement,
|
|
verticalement, ou en diagonale (haut-gauche→bas-droite ou
|
|
haut-droite→bas-gauche — jamais à l'envers), les lettres restantes
|
|
tirées au hasard (`random.choice`/`random.randint`, tirage de jeu —
|
|
voir `CODE_QUALITY.md`) ; si un mot ne trouve pas sa place, la grille
|
|
entière est agrandie et le placement retenté depuis zéro, plutôt que
|
|
d'abandonner silencieusement ce mot. Le tout (grille + coordonnées de
|
|
chaque mot) est embarqué en JSON dans un attribut `data-mots-config`,
|
|
échappé pour l'HTML — même principe que les autres mini-jeux, aucun
|
|
aller-retour serveur pendant qu'on joue : `static/document/js/
|
|
document-editor.js` compare les coordonnées EXACTES sélectionnées par
|
|
l'apprenant à celles de chaque mot (jamais une simple comparaison de
|
|
texte, qui se tromperait sur des lettres partagées entre deux mots qui
|
|
se croisent). Comme l'Association/Memory, la grille reste affichée une
|
|
fois tous les mots trouvés : seul le bouton "Recommencer"
|
|
(`.docMinigameRestartBar`) apparaît.
|
|
- **Scénario** : toujours une carte résumant la config réelle (nombre de
|
|
scénarios) — sanitizée (`sanitize_scenario_config`) avant lecture. Si
|
|
au moins un scénario existe, s'y ajoute (fonction privée
|
|
`_render_scenario_player`) la mise en situation RÉELLE et interactive :
|
|
un scénario est un ARBRE DE DÉCISION (voir `scenario_config.py` pour la
|
|
forme exacte — `nodes[0]` = situation initiale, chaque choix pointe
|
|
vers un autre nœud via `target_id`, un nœud sans choix est une fin de
|
|
branche), pas une simple question à une seule conséquence. Aucune
|
|
notion de bonne/mauvaise réponse (retour utilisateur du 20/09/2026 :
|
|
"il n'y a pas de notion vrai/faux, l'utilisateur observe les
|
|
conséquences") : l'apprenant choisit une option, le texte du nœud visé
|
|
REMPLACE l'affichage du nœud précédent (les boutons de choix
|
|
disparaissent avec lui), et ainsi de suite jusqu'à une fin de branche
|
|
— volontairement PAS le comportement du Quiz, où la question resterait
|
|
affichée à côté d'un encart de feedback séparé. Une fois une fin de
|
|
branche atteinte, passage au scénario (arbre) suivant. Les scénarios
|
|
gardent l'ORDRE d'écriture du créateur (jamais mélangés, contrairement
|
|
à Association/Memory/Mots mêlés — ce sont des mises en situation
|
|
séquentielles, pas des éléments à faire correspondre/retrouver).
|
|
Réutilise les classes visuelles du Quiz (`.docQuizOptions`/
|
|
`.docQuizNextBar`/`.docQuizQuestionText`) plutôt que de dupliquer ces
|
|
règles. L'arbre complet (tous les nœuds/choix de tous les scénarios)
|
|
est embarqué en JSON dans un attribut `data-scenario-config`, échappé
|
|
pour l'HTML — même principe que les autres mini-jeux, aucun
|
|
aller-retour serveur pendant qu'on joue : toute la navigation dans
|
|
l'arbre se fait côté client. Comme l'Association/Memory/Mots mêlés
|
|
(mais contrairement au Quiz), le dernier scénario reste affiché une
|
|
fois une fin de branche atteinte : seul le bouton "Recommencer"
|
|
(`.docMinigameRestartBar`) apparaît. L'arbre lui-même se construit
|
|
dans une MODALE dédiée depuis le panneau Propriétés (voir
|
|
`forgeDocOpenScenarioTreeModal`, static/document/js/document-editor.js)
|
|
— trop de structure (nœuds + choix + destinations) pour la colonne
|
|
étroite du panneau Propriétés, contrairement aux autres mini-jeux. La
|
|
modale est un VRAI graphe visuel (retour utilisateur du 21/09/2026) :
|
|
chaque nœud a une position `x`/`y` (voir `scenario_config.py`),
|
|
affiché comme une carte déplaçable à la souris sur un canevas ; chaque
|
|
choix relié à un `target_id` est dessiné comme une flèche SVG
|
|
étiquetée par son texte, jamais un simple menu déroulant. Éditer le
|
|
texte/les choix d'un nœud se fait dans l'inspecteur (panneau de
|
|
droite) du nœud sélectionné ; relier un choix se fait en cliquant
|
|
"Relier" puis le nœud cible sur le graphe (mode connexion, Échap
|
|
annule).
|
|
- **Autres mini-jeux** (`zones`) : carte placeholder portant le libellé
|
|
du type (voir `document_engine/labels/element_kind_labels.py`) —
|
|
emplacement réservé, formulaire de contenu dédié hors périmètre de
|
|
cette passe.
|
|
|
|
## `sanitize_svg_markup.py` — nettoyage du contenu SVG inline d'une image
|
|
|
|
### `sanitize_svg_markup(markup: str) -> str`
|
|
Nettoie un fragment SVG selon une LISTE BLANCHE de balises/attributs
|
|
(`_ALLOWED_TAGS`/`_ALLOWED_ATTRS`, privées) — construit sur
|
|
`html.parser.HTMLParser` (tokenizer de balises pur, sans DTD ni
|
|
résolution d'entité externe) plutôt qu'un analyseur XML, qui resterait
|
|
exposé aux attaques classiques d'entité externe sur une entrée non
|
|
fiable. Toute balise absente de la liste blanche (`<script>`,
|
|
`<foreignObject>`, `<a>`, `<use>`, `<image>`...) disparaît AVEC son
|
|
contenu ; tout attribut absent (`on*`, `style`, `href`/`xlink:href`,
|
|
`class`...) disparaît seul, la balise porteuse étant conservée si elle
|
|
est autorisée. Appelé à CHAQUE rendu (`_render_image`), jamais
|
|
seulement à l'écriture — même défense en profondeur que
|
|
`html.escape` sur les autres kinds.
|
|
- **Retour** : le fragment SVG nettoyé, sûr à insérer tel quel dans le
|
|
HTML rendu.
|
|
- **Exceptions** : aucune.
|