Audit complet de mise en forme — Titre/Paragraphe (1er élément du tableau)

Nouveau module partagé document_engine/rendering/box_style.py
(padding/margin/background_color/border_radius/border par côté/height/
min-height/max-height/min-width/box-shadow/opacity/align_self) —
réutilisable tel quel par tous les kinds suivants du tableau d'audit.

Titre/Paragraphe gagnent : barré, police de caractère, taille de
police, hauteur de ligne, espacement des lettres, majuscules/
minuscules/capitales, ombre du texte, et tous les attributs de boîte
partagés ci-dessus. Interface entièrement à base de curseurs/cases à
cocher/listes déroulantes/sélecteurs de couleur natifs — plus aucun
champ de texte libre pour une valeur CSS (retour utilisateur).

Deux bugs transversaux corrigés au passage (concernent tout
l'éditeur) :
- Le panneau Propriétés n'était jamais reconstruit après un clic sur un
  bouton (gras/alignement/segments...) — il fallait recharger la page
  pour voir l'état réel. Corrigé dans forgeDocUpdateAttributes, point
  d'entrée unique de toute mise à jour d'attribut.
- Les cases à cocher et curseurs héritaient à tort le style d'un champ
  de texte (padding/bordure/fond/largeur 100%) via la règle générique
  .docField input.

Ajoute docs/plan/AUDIT_MISE_EN_FORME.md : suivi de l'audit élément par
élément (Titre/Paragraphe traité, Image ensuite).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-26 09:25:30 +02:00
co-authored by Claude Sonnet 5
parent e03bea39c5
commit 6c7675fad0
10 changed files with 942 additions and 25 deletions
+25 -2
View File
@@ -5,6 +5,7 @@ défaut posés à la création de chaque type."""
from typing import Any
from ..rendering.box_style import BOX_DEFAULTS, default_border
from .association_config import DEFAULT_ASSOCIATION_CONFIG
from .memory_config import DEFAULT_MEMORY_CONFIG
from .mots_config import DEFAULT_MOTS_CONFIG
@@ -44,12 +45,26 @@ _TEXT_DEFAULTS = {
"bold": False,
"italic": False,
"underline": False,
"strikethrough": False,
"align": "left",
"color": "var(--forge-text)",
# Vide par défaut = pleine largeur de .docPageContent (comportement
# historique) ; une valeur CSS libre (ex. "60ch", "480px") restreint
# la largeur du bloc — voir render_document_element._render_text.
"max_width": "",
# Vides par défaut = valeurs du préréglage `style` inchangées (voir
# _STYLE_PRESETS dans render_document_element.py) — une valeur
# explicite les remplace (retour utilisateur du 26/09/2026, audit des
# réglages manquants).
"font_family": "",
"font_size": "",
"line_height": "",
"letter_spacing": "",
"text_transform": "none",
"text_shadow": "",
# Attributs de boîte partagés avec d'autres kinds (padding/margin/
# background_color/border_radius/align_self) — voir rendering/box_style.py.
**BOX_DEFAULTS,
}
@@ -58,9 +73,17 @@ def element_default_attributes(kind: str) -> dict[str, Any]:
docs/plan/PLAN.md §3.3/§3.4/§3.5 pour la liste des propriétés
éditables par panneau, ici juste leur valeur de départ."""
if kind == "titre":
return {"content": "Nouveau titre", "style": "titre1", **_TEXT_DEFAULTS}
# "border" copié à chaque appel (jamais un dict partagé/muté par
# référence entre plusieurs éléments — même raison que la copie
# de "questions" pour le quiz plus bas).
return {"content": "Nouveau titre", "style": "titre1", **_TEXT_DEFAULTS, "border": default_border()}
if kind == "paragraphe":
return {"content": "Nouveau paragraphe de texte.", "style": "paragraphe", **_TEXT_DEFAULTS}
return {
"content": "Nouveau paragraphe de texte.",
"style": "paragraphe",
**_TEXT_DEFAULTS,
"border": default_border(),
}
if kind == "image":
# svg_markup (optionnel) prend le pas sur src au rendu (voir
# render_document_element._render_image) — un contenu vectoriel
+9 -4
View File
@@ -45,10 +45,15 @@ l'affichage/debug hors bibliothèque).
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` :
texte (`content/style` + `bold/italic/underline/align/color/max_width` —
`max_width` vide par défaut = pleine largeur de `.docPageContent`,
une valeur CSS libre ex. `"60ch"`/`"480px"` restreint le bloc, voir
`rendering.md`),
texte (`content/style` + `bold/italic/underline/strikethrough/align/
color/max_width/font_family/font_size/line_height/letter_spacing/
text_transform/text_shadow` + les attributs de boîte partagés
`padding/margin/background_color/border_radius/height/min_height/
max_height/min_width/box_shadow/opacity/align_self/border`, voir
`rendering/box_style.py` — tous vides, `False` ou `"none"`/`"stretch"`
par défaut = comportement historique inchangé ; `max_width`
restreint la largeur du bloc, une valeur CSS libre ex. `"60ch"`/
`"480px"`, voir `rendering.md`),
image (`src/alt/svg_markup` — `svg_markup` prend le pas sur `src` au
rendu, voir rendering.md), bouton (`label/target/
attachment_stored_name/attachment_filename` — la pièce jointe est
+109
View File
@@ -0,0 +1,109 @@
"""Attributs de mise en forme de "boîte" PARTAGÉS par plusieurs kinds de
contenu (padding/margin/background_color/border_radius/border/align_self)
— un seul et même jeu d'attributs et une seule fonction de rendu pour ne
jamais dupliquer cette logique entre `_render_text`/`_render_image`/
`_render_button`/etc. (voir retour utilisateur du 26/09/2026 : audit
complet des réglages manquants, à ajouter élément par élément en
réutilisant CE module à chaque fois plutôt que de le réécrire)."""
import html as html_lib
from typing import Any
BORDER_SIDES = ("top", "right", "bottom", "left")
_DEFAULT_BORDER_SIDE = {"style": "none", "width": "1px", "color": "var(--doc-border)"}
def default_border() -> dict[str, dict[str, str]]:
"""Nouveau dict à chaque appel (jamais un littéral partagé/muté par
référence entre plusieurs éléments, même précaution que
DEFAULT_QUIZ_CONFIG côté labels)."""
return {side: dict(_DEFAULT_BORDER_SIDE) for side in BORDER_SIDES}
BOX_DEFAULTS = {
"padding": "",
"margin": "",
"background_color": "",
"border_radius": "",
"align_self": "stretch",
"height": "",
"min_height": "",
"max_height": "",
"min_width": "",
"box_shadow": "",
"opacity": "",
}
def render_box_style(a: dict[str, Any]) -> str:
"""Construit les déclarations CSS inline communes à plusieurs kinds à
partir des attributs `padding`/`margin`/`background_color`/
`border_radius`/`height`/`min_height`/`max_height`/`min_width`/
`box_shadow`/`opacity`/`border`/`align_self` de `a` — chaîne vide pour
tout attribut absent ou à sa valeur par défaut (aucun style ajouté,
comportement historique inchangé). `border` est un dict à 4 clés
(`BORDER_SIDES`), chacune `{"style", "width", "color"}` — un côté à
`style="none"` (ou absent) ne produit aucune déclaration pour ce
côté, jamais un `border-top:none` explicite.
- **Retour** : les déclarations CSS (`"propriete:valeur; ..."`),
jamais vide ni `None`.
- **Exceptions** : aucune."""
parts: list[str] = []
padding = str(a.get("padding", "")).strip()
if padding:
parts.append(f"padding:{html_lib.escape(padding)};")
margin = str(a.get("margin", "")).strip()
if margin:
parts.append(f"margin:{html_lib.escape(margin)};")
background_color = str(a.get("background_color", "")).strip()
if background_color:
parts.append(f"background-color:{html_lib.escape(background_color)};")
border_radius = str(a.get("border_radius", "")).strip()
if border_radius:
parts.append(f"border-radius:{html_lib.escape(border_radius)};")
height = str(a.get("height", "")).strip()
if height:
parts.append(f"height:{html_lib.escape(height)};")
min_height = str(a.get("min_height", "")).strip()
if min_height:
parts.append(f"min-height:{html_lib.escape(min_height)};")
max_height = str(a.get("max_height", "")).strip()
if max_height:
parts.append(f"max-height:{html_lib.escape(max_height)};")
min_width = str(a.get("min_width", "")).strip()
if min_width:
parts.append(f"min-width:{html_lib.escape(min_width)};")
box_shadow = str(a.get("box_shadow", "")).strip()
if box_shadow:
parts.append(f"box-shadow:{html_lib.escape(box_shadow)};")
opacity = str(a.get("opacity", "")).strip()
if opacity:
parts.append(f"opacity:{html_lib.escape(opacity)};")
border = a.get("border") or {}
for side in BORDER_SIDES:
side_border = border.get(side) or {}
style = str(side_border.get("style", "none"))
if style and style != "none":
width = html_lib.escape(str(side_border.get("width", "1px")))
color = html_lib.escape(str(side_border.get("color", "var(--doc-border)")))
parts.append(f"border-{side}:{width} {html_lib.escape(style)} {color};")
# align-self ne fait quoi que ce soit d'utile QUE si l'élément a par
# ailleurs une taille bornée (max_width/width) — voir la note dans
# element_kind_labels.md — mais reste toujours sûr à poser seul
# ("stretch" est déjà le comportement par défaut d'un enfant flex en
# colonne, donc jamais ajouté explicitement pour ne rien changer).
align_self = str(a.get("align_self", "stretch"))
if align_self and align_self != "stretch":
parts.append(f"align-self:{html_lib.escape(align_self)};")
return " ".join(parts)
@@ -3,6 +3,7 @@ import json
import random
from typing import Any
from .box_style import render_box_style
from .sanitize_svg_markup import sanitize_svg_markup
@@ -51,16 +52,48 @@ def _render_text(el: dict[str, Any], _children_by_parent: dict[int | None, list[
a = el["attributes"]
content = html_lib.escape(str(a.get("content", "")))
style_name = a.get("style", "paragraphe")
font_size, base_weight, line_height = _STYLE_PRESETS.get(style_name, _STYLE_PRESETS["paragraphe"])
preset_font_size, base_weight, preset_line_height = _STYLE_PRESETS.get(style_name, _STYLE_PRESETS["paragraphe"])
weight = 800 if a.get("bold") else base_weight
font_style = "italic" if a.get("italic") else "normal"
text_decoration = "underline" if a.get("underline") else "none"
# underline/strikethrough se combinent (text-decoration-line accepte
# plusieurs valeurs) — retour utilisateur du 26/09/2026 : "barré"
# manquait à côté du souligné déjà existant.
decoration_parts = []
if a.get("underline"):
decoration_parts.append("underline")
if a.get("strikethrough"):
decoration_parts.append("line-through")
text_decoration = " ".join(decoration_parts) if decoration_parts else "none"
align = html_lib.escape(str(a.get("align", "left")))
color = html_lib.escape(str(a.get("color", "var(--forge-text)")))
# font_size/line_height : vides par défaut = valeurs du préréglage
# `style` (titre1/titre2/paragraphe/légende) inchangées ; une valeur
# explicite les remplace SANS changer `weight` (qui reste piloté par
# le préréglage + `bold`).
font_size = html_lib.escape(str(a.get("font_size", "")).strip()) or preset_font_size
line_height = html_lib.escape(str(a.get("line_height", "")).strip()) or str(preset_line_height)
style = (
f"font-size:{font_size}; font-weight:{weight}; line-height:{line_height}; "
f"font-style:{font_style}; text-decoration:{text_decoration}; text-align:{align}; color:{color};"
)
text_transform = str(a.get("text_transform", "none"))
if text_transform and text_transform != "none":
style += f" text-transform:{html_lib.escape(text_transform)};"
font_family = str(a.get("font_family", "")).strip()
if font_family:
style += f" font-family:{html_lib.escape(font_family)};"
letter_spacing = str(a.get("letter_spacing", "")).strip()
if letter_spacing:
style += f" letter-spacing:{html_lib.escape(letter_spacing)};"
text_shadow = str(a.get("text_shadow", "")).strip()
if text_shadow:
style += f" text-shadow:{html_lib.escape(text_shadow)};"
# max_width optionnel (ex. "60ch", "480px") — vide par défaut (pleine
# largeur de .docPageContent, comportement inchangé). Retour
# utilisateur du 24/09/2026 : un paragraphe doit pouvoir rester plus
@@ -69,6 +102,11 @@ def _render_text(el: dict[str, Any], _children_by_parent: dict[int | None, list[
max_width = str(a.get("max_width", "")).strip()
if max_width:
style += f" max-width:{html_lib.escape(max_width)};"
box_style = render_box_style(a)
if box_style:
style += f" {box_style}"
return f'<div class="docText" data-element-id="{el["id"]}" data-kind="{el["kind"]}" style="{style}">{content}</div>'
+59 -6
View File
@@ -29,12 +29,24 @@ regroupement à chaque appel.
`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`.
`max_width` (optionnel, ex. `"60ch"`, `"480px"`) ajoute `max-width` au
style inline quand non vide — pleine largeur de `.docPageContent` par
défaut, retour utilisateur du 24/09/2026 (un paragraphe doit pouvoir
rester plus étroit que la page, sans dépendre d'une rangée qui en
partagerait la largeur avec un frère).
taille/graisse/interligne) et `bold`/`italic`/`underline`/`strikethrough`/
`align`/`color`. `underline`/`strikethrough` se combinent dans un seul
`text-decoration` (`"underline line-through"` si les deux sont actifs).
`font_size`/`line_height` (vides par défaut) remplacent les valeurs du
préréglage `style` SANS toucher `font-weight` (toujours piloté par le
préréglage + `bold`). `text_transform` (`"none"` par défaut) ajoute
`text-transform` quand différent de `"none"`. `font_family`/
`letter_spacing`/`text_shadow` (vides par défaut) ajoutent leur
déclaration CSS respective quand non vides. `max_width` (optionnel, ex.
`"60ch"`, `"480px"`) ajoute `max-width` au style inline quand non vide —
pleine largeur de `.docPageContent` par défaut, retour utilisateur du
24/09/2026 (un paragraphe doit pouvoir rester plus étroit que la page,
sans dépendre d'une rangée qui en partagerait la largeur avec un frère).
Termine par `render_box_style(a)` (voir `box_style.py` ci-dessous) pour
`padding`/`margin`/`background_color`/`border_radius`/`border`/
`align_self` — attributs PARTAGÉS avec d'autres kinds, jamais dupliqués
ici (audit du 26/09/2026, réglages manquants à couvrir élément par
élément en réutilisant ce module).
- **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
@@ -180,6 +192,47 @@ regroupement à chaque appel.
emplacement réservé, formulaire de contenu dédié hors périmètre de
cette passe.
## `box_style.py` — attributs de "boîte" partagés entre plusieurs kinds
Audit du 26/09/2026 (réglages manquants, à couvrir élément par élément) :
padding/margin/couleur de fond/arrondi/bordure par côté/position du bloc
sont des besoins IDENTIQUES pour la plupart des kinds de contenu — un
seul jeu d'attributs et une seule fonction de rendu ici, jamais réécrits
à chaque kind (voir `_render_text` pour le premier usage réel).
### `BORDER_SIDES: tuple[str, ...]`
`("top", "right", "bottom", "left")`.
### `default_border() -> dict[str, dict[str, str]]`
Un dict à 4 clés (`BORDER_SIDES`), chacune `{"style": "none", "width":
"1px", "color": "var(--doc-border)"}`.
- **Retour** : un NOUVEAU dict à chaque appel (jamais un littéral partagé
muté par référence entre deux éléments — même précaution que
`DEFAULT_QUIZ_CONFIG` côté `labels/`).
- **Exceptions** : aucune.
### `BOX_DEFAULTS: dict[str, Any]`
`{"padding": "", "margin": "", "background_color": "", "border_radius":
"", "align_self": "stretch", "height": "", "min_height": "",
"max_height": "", "min_width": "", "box_shadow": "", "opacity": ""}` —
`border` n'y figure PAS (voir `default_border()`, à ajouter séparément
par chaque appelant pour éviter le partage par référence).
### `render_box_style(a: dict[str, Any]) -> str`
Construit les déclarations CSS inline pour `padding`/`margin`/
`background_color`/`border_radius`/`height`/`min_height`/`max_height`/
`min_width`/`box_shadow`/`opacity`/`border`/`align_self` de `a` — un
attribut absent ou à sa valeur par défaut ne produit AUCUNE déclaration
(comportement historique inchangé). `border` est un dict à 4 clés
(`BORDER_SIDES`), chacune `{"style", "width", "color"}` — un côté à
`style="none"` (ou absent) ne produit rien pour ce côté, jamais un
`border-top:none` explicite. `align-self` n'est ajouté que si différent
de `"stretch"` (déjà le comportement par défaut d'un enfant flex en
colonne).
- **Retour** : les déclarations CSS (`"propriete:valeur; ..."`), chaîne
vide si rien à ajouter.
- **Exceptions** : aucune.
## `sanitize_svg_markup.py` — nettoyage du contenu SVG inline d'une image
### `sanitize_svg_markup(markup: str) -> str`