Files
Forge-Engine/document_engine/labels/labels.md
T
williamandClaude Sonnet 5 4db1269348 Audit complet de mise en forme — Liste à puces/numérotée (4e élément)
Implémente toutes les options manquantes identifiées pour les listes :
typographie complète, style/position/couleur/taille de puce (validés
selon le kind), puce personnalisée en SVG pour les listes à puces,
padding uniforme par élément (nouveau, il n'y en avait aucun), espacement
entre éléments réglable, et tous les attributs de boîte partagés sur la
liste entière. Bordure/fond/padding par élément individuel et sous-listes
imbriquées volontairement différés (portée actée avec l'utilisateur
avant implémentation : transformeraient le stockage des éléments en
objets structurés, chantier bien plus lourd).

Trois ajouts transversaux bénéficiant à plusieurs éléments : sections
"Contenu"/"Conteneur" dans tous les panneaux de propriétés, alignement
vertical du contenu dans son bloc (Titre/Paragraphe/Liste/Image
légendée), et une option pour retirer un thème appliqué ("Aucun modèle"
dans la modale, avec une nouvelle fonction db.remove_document_theme).

Quatre bugs réels trouvés et corrigés en chaîne pendant la validation
avec le thème "Sécurité incendie" : un badge de thème s'affichait
au-dessus du texte au lieu d'à côté ; le correctif a d'abord fait
disparaître les puces/numéros natifs de TOUTES les listes (bug plus
grave que celui corrigé) ; puis un marqueur natif redondant apparaissait
à côté du badge du thème ; puis une règle CSS site-large de spécificité
supérieure empêchait silencieusement ce dernier correctif. Chaque étape
vérifiée par navigateur automatisé sur un support jetable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 15:13:14 +02:00

18 KiB

document_engine/labels/

Catalogue statique des types d'éléments du support de formation : bibliothèque groupée par catégorie (panneau gauche de l'éditeur), libellés d'affichage, et attributs par défaut posés à la création de chaque type.

CONTENT_KINDS: tuple[str, ...]

("titre", "paragraphe", "image", "bouton", "liste_puces", "liste_numerotee", "badge", "carte") — éléments du flux, peuvent être top-niveau ou enfants d'une rangée. "liste_puces"/"liste_numerotee" partagent la même structure d'attributs (items) ; c'est le kind lui-même qui décide <ul> ou <ol> au rendu (voir document_engine/rendering/render_document_element.py::_render_list), jamais un attribut ordered redondant. "carte" reste du contenu PUR, sans aucun attribut de style/couleur/forme (décision du 24/09/2026 : le moteur ne porte que contenu et mécanisme, tout habillage visuel revient à un futur système de templates). "badge" a depuis gagné des attributs de mise en forme PAR ÉLÉMENT (svg_markup/width/ border_radius/bold/uppercase, retour utilisateur du 24/09/2026) — même esprit que bold/align/color déjà présents sur "titre"/"paragraphe" : des réglages posés par le créateur ou un thème au cas par cas, jamais une valeur figée dans le moteur. Voir aussi le mode SVG inline de "image" (svg_markup) et la pièce jointe téléchargeable de "bouton" (attachment_filename), ci-dessous.

MINIGAME_KINDS: tuple[str, ...]

("quiz", "association", "memory", "mots", "scenario", "zones") — "quiz", "association" et "memory" sont implémentés (voir quiz_config.py/association_config.py/memory_config.py ci-dessous) ; les 3 autres gardent un panneau Propriétés minimal (emplacement réservé), formulaires de contenu dédiés hors périmètre de cette passe.

ELEMENT_LIBRARY: dict[str, dict[str, Any]]

Bibliothèque affichée dans le panneau gauche, groupée par catégorie (contenu / minigames), chaque entrée portant un label et sa liste de kinds. "row" n'y apparaît jamais — créé implicitement par le moteur de layout, jamais choisi directement dans la bibliothèque.

ELEMENT_KIND_LABELS: dict[str, str]

Libellé d'affichage pour chaque kind, y compris "row" (pour l'affichage/debug hors bibliothèque).

element_default_attributes(kind: str) -> dict[str, Any]

Attributs posés à la création d'un élément de ce type (voir document_engine/elements/add_document_element.py).

  • Retour : un dict d'attributs par défaut, dépendant du kind (les "attributs de boîte partagés" mentionnés ci-dessous — padding/margin/ background_color/border_radius/width/max_width/height/min_height/ max_height/min_width/box_shadow/opacity/align_self/content_align/ border — sont toujours les mêmes, voir rendering/box_style.py : tous vides, False ou "none"/"stretch"/"top" par défaut = comportement historique inchangé pour le kind qui les gagne ; content_align — alignement vertical du CONTENU dans son bloc, retour utilisateur du 26/09/2026 — n'est cependant appliqué au rendu QUE par les kinds dont le conteneur est en colonne (texte, liste, figure d'une image légendée), jamais par le Bouton, voir rendering.md) : texte (content/style + bold/italic/underline/strikethrough/align/ color/font_family/font_size/line_height/letter_spacing/ text_transform/text_shadow + les attributs de boîte partagés), image (src/alt/svg_markup — svg_markup prend le pas sur src au rendu, voir rendering.md — object_fit="cover" et height="220px" par défaut (retour utilisateur du 26/09/2026 : "ce cadre ne devrait pas changer de taille en fonction de la taille de l'image mais être fixe et contraindre l'image dedans" — un cadre FIXE, jamais dicté par la résolution native du fichier importé ; "Taille réelle" reste un choix explicite possible via le panneau), aspect_ratio/filter_preset vides par défaut, click_behavior (""/"link"/"lightbox") + link_url + lazy_load + caption : comportement/contenu, pas du style + les attributs de boîte partagés), bouton (label/target/ attachment_stored_name/attachment_filename — la pièce jointe est indépendante de target, réservé à la navigation — + bold/italic/ text_transform/font_family/font_size/letter_spacing/text_color (typo, jamais gérés par box_style.py) + svg_markup/icon_position ("before"/"after")/icon_size (icône optionnelle, voir rendering.md)
    • les attributs de boîte partagés), liste à puces/numérotée (items, une liste de chaînes — style sur la LISTE ENTIÈRE uniquement, jamais par élément individuel ni de sous-listes, portée actée avec l'utilisateur le 26/09/2026 : bold/italic/ underline/font_family/font_size/line_height/text_color (typo) + list_style_type (valide selon le kind, voir rendering.md)/ list_style_position/marker_color/marker_size/svg_markup (puce personnalisée, liste à puces uniquement) + item_padding ("6px" par défaut, PAS vide — retour utilisateur explicite : "il faut un padding de base par élément de liste car y en a pas aujourd'hui", UNIFORME sur tous les éléments, jamais réglable individuellement) + item_spacing + les attributs de boîte partagés), badge (content/svg_markup/width/border_radius/bold/uppercase — tous vides ou False par défaut = comportement historique inchangé, voir rendering.md), carte (label/title/description, contenu pur sans couleur/forme), rangée (gap/align/justify), quiz (DEFAULT_QUIZ_CONFIG, voir quiz_config.py), association (DEFAULT_ASSOCIATION_CONFIG, voir association_config.py), memory (DEFAULT_MEMORY_CONFIG, voir memory_config.py), autre mini-jeu (theme_color), ou {} pour un kind inconnu.
  • Exceptions : aucune.

quiz_config.py — modèle de données du mini-jeu Quiz

Seul mini-jeu réellement implémenté (les autres MINIGAME_KINDS restent un emplacement réservé). Même convention resolve_X/sanitize_X que game_engine/rendering/quiz_box_config.py côté jeu, mais sans aucun import croisé (voir docs/plan/PLAN.md).

MIN_CHOICES, MAX_CHOICES, MIN_TIMER_SECONDS, MAX_TIMER_SECONDS, DEFAULT_TIMER_SECONDS: int

Bornes de validation (2/4 choix, 5/300 secondes, 30 par défaut).

DEFAULT_QUIZ_CONFIG: dict[str, Any]

{"theme_color": "#ff5f2e", "timer_enabled": False, "timer_seconds": 30, "questions": []}.

sanitize_quiz_config(raw_config: Any) -> dict[str, Any]

Valide/nettoie une config de quiz arbitraire (JSON venu du client) — jamais lève, renvoie toujours un dict COMPLET fusionné sur DEFAULT_QUIZ_CONFIG. Chaque question de raw_config["questions"] est validée indépendamment (voir _sanitize_question, privée) : texte non vide, au moins 2 choix non vides (tronqués à 4), correct_index remis à 0 s'il est absent/hors bornes/non-entier (un bool — qui est un int en Python — est explicitement rejeté), points codé en entier ≥ 0. Une question invalide est silencieusement supprimée de la liste (jamais une levée qui ferait échouer tout le reste du quiz).

  • Retour : dict complet (mêmes clés que DEFAULT_QUIZ_CONFIG).
  • Exceptions : aucune.

quiz_total_points(config: dict[str, Any]) -> int

Somme du points de toutes les questions d'une config déjà sanitizée — mirroir de db/dialogue_lines.py::sum_question_rewards côté jeu, utile le jour où un export calculera un score maximum.

  • Retour : entier ≥ 0.
  • Exceptions : aucune.

association_config.py — modèle de données du mini-jeu Association

Deuxième mini-jeu implémenté : l'apprenant relie chaque carte de gauche ("terme") à sa correspondance de droite ("définition") par glisser-déposer (voir document_engine/rendering/render_document_element.py:: _render_association_player). Même convention que quiz_config.py.

MIN_PAIRS, MAX_PAIRS: int

Bornes de validation (2/8 paires). MIN_PAIRS n'est pas imposé par sanitize_association_config (une seule paire valide reste acceptée, comme un quiz à une seule question) — c'est une recommandation pour le panneau Propriétés, pas une contrainte technique du rendu.

DEFAULT_ASSOCIATION_CONFIG: dict[str, Any]

{"theme_color": "#ff5f2e", "pairs": []}.

sanitize_association_config(raw_config: Any) -> dict[str, Any]

Valide/nettoie une config d'association arbitraire (JSON venu du client) — jamais ne lève, renvoie toujours un dict COMPLET fusionné sur DEFAULT_ASSOCIATION_CONFIG. Chaque paire de raw_config["pairs"] est validée indépendamment (voir _sanitize_pair, privée) : les deux côtés (left/right) doivent être non vides une fois .strip()-és, sinon la paire entière est silencieusement supprimée de la liste (jamais une levée qui ferait échouer tout le reste du mini-jeu). La liste finale est tronquée à MAX_PAIRS.

  • Retour : dict complet (mêmes clés que DEFAULT_ASSOCIATION_CONFIG).
  • Exceptions : aucune.

memory_config.py — modèle de données du mini-jeu Memory

Troisième mini-jeu implémenté : l'apprenant retourne des cartes pour constituer des paires identiques (mode "paire") ou simplement révéler chaque carte une fois (mode "single", sans appariement — voir document_engine/rendering/render_document_element.py:: _render_memory_player). Même convention que quiz_config.py.

MIN_CARDS, MAX_CARDS: int

Bornes de validation (2/8 cartes DÉFINIES par le créateur — en mode "paire", le plateau affiche le double, chaque carte étant dupliquée). MIN_CARDS n'est pas imposé par sanitize_memory_config (même logique que MIN_PAIRS côté Association) — recommandation pour le panneau Propriétés, pas une contrainte technique du rendu.

CARD_MODES: tuple[str, ...]

("paire", "single").

DEFAULT_MEMORY_CONFIG: dict[str, Any]

{"theme_color": "#ff5f2e", "mode": "paire", "cards": []}.

sanitize_memory_config(raw_config: Any) -> dict[str, Any]

Valide/nettoie une config de memory arbitraire (JSON venu du client) — jamais ne lève, renvoie toujours un dict COMPLET fusionné sur DEFAULT_MEMORY_CONFIG. mode retombe sur "paire" s'il n'est pas dans CARD_MODES. Chaque carte de raw_config["cards"] est validée indépendamment (voir _sanitize_card/_sanitize_card_face, privées) : chaque face (recto/verso) a un image et un text indépendants et tous deux optionnels, MAIS le verso doit avoir au moins l'un des deux non vide (rien à révéler/apparier sinon) — le recto, lui, peut rester entièrement vide (dos de carte générique "?" par défaut côté rendu). Une carte invalide est silencieusement supprimée de la liste. La liste finale est tronquée à MAX_CARDS.

  • Retour : dict complet (mêmes clés que DEFAULT_MEMORY_CONFIG).
  • Exceptions : aucune.

mots_config.py — modèle de données du mini-jeu Mots mêlés

Quatrième mini-jeu implémenté : l'apprenant retrouve chaque mot caché dans une grille de lettres (horizontalement, verticalement, ou en diagonale — voir document_engine/rendering/render_document_element.py:: _render_mots_player, qui construit la grille elle-même). Même convention que quiz_config.py.

MIN_WORDS, MAX_WORDS: int

Bornes de validation (5/10 mots). MIN_WORDS n'est pas imposé par sanitize_mots_config (même logique que MIN_PAIRS/MIN_CARDS côté Association/Memory) — recommandation pour le panneau Propriétés, pas une contrainte technique du rendu : un seul mot valide reste accepté, la grille se construit quand même autour de lui.

MIN_WORD_LENGTH, MAX_WORD_LENGTH: int

Bornes de longueur d'un mot individuel une fois nettoyé (2/20 lettres) — un mot trop court n'a pas de sens à chercher, un mot trop long compliquerait inutilement le calcul de la taille de la grille (voir _mots_grid_size_for_words dans render_document_element.py).

DEFAULT_MOTS_CONFIG: dict[str, Any]

{"theme_color": "#ff5f2e", "words": []}.

sanitize_mots_config(raw_config: Any) -> dict[str, Any]

Valide/nettoie une config de mots mêlés arbitraire (JSON venu du client) — jamais ne lève, renvoie toujours un dict COMPLET fusionné sur DEFAULT_MOTS_CONFIG. Chaque mot de raw_config["words"] est nettoyé indépendamment (voir _sanitize_word, privée) : mis en MAJUSCULES, les accents sont retirés (décomposition NFKD + filtrage ASCII, pour que deux mots qui se croisent sur une même case de la grille puissent partager exactement la même lettre), seules les lettres sont conservées. Un mot qui ne contient plus rien d'exploitable (ou moins de MIN_WORD_LENGTH lettres) une fois nettoyé est silencieusement supprimé de la liste, de même qu'un doublon exact d'un mot déjà retenu (un mot répété deux fois n'aurait rien de plus à faire trouver). La liste finale est tronquée à MAX_WORDS.

  • Retour : dict complet (mêmes clés que DEFAULT_MOTS_CONFIG).
  • Exceptions : aucune.

scenario_config.py — modèle de données du mini-jeu Scénario

Cinquième mini-jeu implémenté : l'apprenant lit une situation initiale, choisit une option, puis découvre la conséquence de SON choix — cette conséquence peut elle-même mener à de nouveaux choix, et ainsi de suite (arbre de décision, voir document_engine/rendering/ render_document_element.py::_render_scenario_player). 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 explore simplement les branches jusqu'à une fin. Le créateur peut définir plusieurs scénarios (plusieurs arbres indépendants), joués les uns après les autres dans l'ORDRE d'écriture (jamais mélangés, contrairement à Association/Memory/Mots mêlés). Même convention resolve_X/sanitize_X que quiz_config.py.

Structure d'un scénario : {"title": str, "nodes": [...]}. nodes[0] est TOUJOURS la situation initiale (racine de l'arbre, jamais supprimable depuis le panneau Propriétés — voir forgeDocScenarioGraphRenderInspector, static/document/js/ document-editor.js). Chaque nœud est {"id": str, "text": str, "x": float, "y": float, "choices": [...]} — id est une référence STABLE (générée côté client, jamais recalculée côté serveur) vers laquelle un choix d'un AUTRE nœud peut pointer via son target_id ; x/y positionnent le nœud sur le canevas du graphe visuel (voir forgeDocRenderScenarioTreeModal) ; un nœud SANS choix est une fin de branche (état normal, pas une erreur — contrairement à l'ancien modèle qui exigeait 2 à 4 choix par scénario). Un choix est {"text": str, "target_id": str | None} — target_id à None signifie "fin de branche à ce choix".

MAX_SCENARIO_CHOICES: int

Borne technique du nombre de choix par nœud (4) — évite une UI disproportionnée, aucun lien avec une notion de bonne/mauvaise réponse. Nommée explicitement "SCENARIO" (pas juste MAX_CHOICES) pour éviter toute collision avec la constante de même nom de quiz_config.py, qui borne un concept différent (le nombre de réponses d'une question de quiz) — document_engine/__init__.py ré-exporte l'intégralité de l'API publique du paquet à plat, un nom générique entrerait en collision. Contrairement à l'ancien modèle, il n'existe PAS de minimum : un nœud peut avoir 0 choix (fin de branche), c'est un état valide, pas une erreur à filtrer.

DEFAULT_SCENARIO_CONFIG: dict[str, Any]

{"theme_color": "#ff5f2e", "scenarios": []}.

sanitize_scenario_config(raw_config: Any) -> dict[str, Any]

Valide/nettoie une config de scénario arbitraire (JSON venu du client) — jamais ne lève, renvoie toujours un dict COMPLET fusionné sur DEFAULT_SCENARIO_CONFIG. Chaque scénario de raw_config["scenarios"] est validé indépendamment (voir _sanitize_scenario/ _sanitize_scenario_node/_sanitize_scenario_choice, privées) : le title doit être non vide, chaque nœud doit avoir un id (chaîne non vide) et un text non vide une fois .strip()-é (sinon le nœud entier est supprimé), et ses choix sont tronqués à MAX_SCENARIO_CHOICES. Un x/y manquant ou invalide (pas un nombre, ou un booléen — bool hérite de int en Python) retombe sur un quadrillage en cascade dérivé de l'INDEX du nœud dans la liste (_scenario_node_fallback_position), jamais (0, 0) pour tous les nœuds, ce qui les empilerait exactement au même endroit sur le graphe visuel. Le scénario entier est supprimé s'il ne reste plus aucun nœud valide après nettoyage (il faut au moins la situation initiale). Une fois l'ensemble des ids valides connu, tout target_id qui ne pointe plus vers un nœud existant (nœud invalide/supprimé) est silencieusement remis à None ("fin de branche") plutôt que de faire échouer tout le scénario — même philosophie que le reste de ce module : ne jamais faire échouer une structure entière pour une seule référence cassée.

  • Retour : dict complet (mêmes clés que DEFAULT_SCENARIO_CONFIG).
  • Exceptions : aucune.

sanitize_element_attributes.py — point d'entrée unique de revalidation par kind

Dispatch kind -> sanitize_X_config (_SANITIZERS, privée) pour les 5
mini-jeux à structure garantie (Quiz/Association/Memory/Mots
mêlés/Scénario) — les autres kinds n'ont pas de sanitizer et sont
renvoyés tels quels. Utilisé à la fois à l'ÉCRITURE
(routes/document/document_element_update.py) ET à la LECTURE
(routes/document/document_edit.py, routes/document/document_render.py)
sans ce second usage, des attributs stockés dans un schéma devenu obsolète (ex. Scénario, passé d'une liste plate à un arbre de décision) atteindraient le panneau Propriétés côté client TELS QUELS, qui suppose la forme ACTUELLE — bug réel constaté le 21/09/2026, un élément Scénario créé avant la refonte en arbre faisait planter silencieusement le panneau Propriétés (scenario.nodes inexistant sur l'ancienne forme).

sanitize_element_attributes(kind: str, attributes: Any) -> Any

  • Retour : attributes revalidé si kind a un sanitizer, sinon attributes tel quel.
  • Exceptions : aucune (délègue à des sanitizers qui ne lèvent jamais).