Implémente toutes les options manquantes identifiées pour l'élément Image : dimensionnement/ratio/object-fit, filtres CSS, upload de fichier (en plus de l'URL), lien/plein écran au clic, chargement différé, légende, et tous les attributs de boîte partagés déjà créés pour Titre/Paragraphe (padding/margin/fond/bordure/ombre/opacité/ position du bloc). Système de pages : un support peut désormais avoir 0 page (un nouveau support démarre vide), suppression de toutes les pages en un clic, et la pagination automatique insère intelligemment la nouvelle page juste après celle qui déborde plutôt qu'en toute fin de liste. Bugs réels trouvés et corrigés en cours de route : le style de bloc (dont align-self) ciblait l'élément interne au lieu de son enveloppe (légende/lien) ; une image à sa taille native pouvait déclencher une pagination infinie ; upload/mise à jour d'attribut ne déclenchaient jamais le contrôle de débordement. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
306 lines
17 KiB
Markdown
306 lines
17 KiB
Markdown
# 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/border` — sont
|
|
toujours les mêmes, voir `rendering/box_style.py` : tous vides,
|
|
`False` ou `"none"`/`"stretch"` par défaut = comportement historique
|
|
inchangé pour le kind qui les gagne) :
|
|
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), liste à
|
|
puces/numérotée (`items`, une liste de chaînes), 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).
|