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>
12 KiB
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
kindinconnu produit un blocdocUnknownvisible 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-contentréels depuisattributes), enfants rendus récursivement. - Texte (
titre/paragraphe) :<div>stylé selonstyle(préréglage taille/graisse/interligne) etbold/italic/underline/align/color. - Image :
<img>, ou un bloc placeholder sisrcest vide — OU, siattributes["svg_markup"]est non vide (prioritaire sursrc), un<div>portant directement ce fragment SVG nettoyé parsanitize_svg_markup(voirsanitize_svg_markup.pyci-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 sonlabel, undata-targetoptionnel (navigation) et undata-attachment-filenameoptionnel — marqueur mécanique posé quand un fichier a été joint (voirroutes/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.jsl'assemble à partir dedata-element-id+FORGE_DOCUMENT.slug, même principe que le reste des appels AJAX de l'éditeur). - Étiquette (
badge) :<div>portantattributes["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 (voirelement_kind_labels.md). - Liste à puces/numérotée (
liste_puces/liste_numerotee) :<ul>ou<ol>selon lekind(fonction privée_render_list, partagée par les deux) — un<li>par entrée deattributes["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'attributesbrut. 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 attributdata-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 — voirCODE_QUALITY.md) puis embarquées en JSON dans un attributdata-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êmecard_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 — voirCODE_QUALITY.md) puis embarquées en JSON dans un attributdata-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 — voirCODE_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 attributdata-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.jscompare 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 (voirscenario_config.pypour la forme exacte —nodes[0]= situation initiale, chaque choix pointe vers un autre nœud viatarget_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 attributdata-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 (voirforgeDocOpenScenarioTreeModal, 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 positionx/y(voirscenario_config.py), affiché comme une carte déplaçable à la souris sur un canevas ; chaque choix relié à untarget_idest 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 (voirdocument_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.