"""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", "width": "", "max_width": "", "height": "", "min_height": "", "max_height": "", "min_width": "", "box_shadow": "", "opacity": "", "content_align": "top", } _CONTENT_ALIGN_TO_JUSTIFY_CONTENT = {"center": "center", "bottom": "flex-end"} # (clé d'attribut, propriété CSS) — chaque paire suit exactement le même # patron (lire/nettoyer/ajouter si non vide) ; une simple table de # correspondance ici évite un enchaînement de blocs `if` identiques # (complexité cognitive réduite, voir _render_simple_properties). _SIMPLE_PROPERTIES = ( ("padding", "padding"), ("margin", "margin"), ("background_color", "background-color"), ("border_radius", "border-radius"), ("width", "width"), ("max_width", "max-width"), ("height", "height"), ("min_height", "min-height"), ("max_height", "max-height"), ("min_width", "min-width"), ("box_shadow", "box-shadow"), ("opacity", "opacity"), ) def _render_simple_properties(a: dict[str, Any]) -> list[str]: parts = [] for attr_key, css_prop in _SIMPLE_PROPERTIES: value = str(a.get(attr_key, "")).strip() if value: parts.append(f"{css_prop}:{html_lib.escape(value)};") return parts def _render_border(a: dict[str, Any]) -> list[str]: """Un côté à `style="none"` (ou absent) ne produit aucune déclaration pour ce côté, jamais un `border-top:none` explicite.""" parts = [] 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};") return parts def render_box_style(a: dict[str, Any]) -> str: """Construit les déclarations CSS inline communes à plusieurs kinds à partir des attributs listés dans `_SIMPLE_PROPERTIES` + `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"}`. - **Retour** : les déclarations CSS (`"propriete:valeur; ..."`), jamais vide ni `None`. - **Exceptions** : aucune.""" parts = _render_simple_properties(a) + _render_border(a) # 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) def render_content_align(a: dict[str, Any]) -> str: """Alignement vertical du CONTENU à l'intérieur de son propre bloc — utile UNIQUEMENT une fois qu'une hauteur fixe/minimale dépasse la hauteur naturelle du contenu (retour utilisateur du 26/09/2026 : "je peux augmenter la hauteur d'un conteneur mais pas l'alignement vertical à l'intérieur"). Jamais fusionné dans `render_box_style` : contrairement à `align_self` (position du BLOC dans SON parent, la même logique convient à tout consommateur), l'alignement du CONTENU dépend de l'axe interne du conteneur — correct en `justify-content` pour un conteneur en colonne (texte, liste), mais un bouton (rangée : icône + texte) gère déjà cet axe autrement (`align-items`, voir static/document/document-editor.css, .docButton) : chaque renderer qui veut ce comportement l'appelle donc explicitement lui- même (voir _render_text/_render_list/_render_image), jamais automatiquement pour tous les kinds. - **Retour** : `""` si `content_align` est absent ou `"top"` (défaut, comportement historique inchangé), sinon la déclaration `justify-content:...;`. - **Exceptions** : aucune.""" content_align = str(a.get("content_align", "top")) justify_content = _CONTENT_ALIGN_TO_JUSTIFY_CONTENT.get(content_align) return f"justify-content:{justify_content};" if justify_content else ""