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:
co-authored by
Claude Sonnet 5
parent
e03bea39c5
commit
6c7675fad0
@@ -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>'
|
||||
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user