"""Resolve template text roles once for reuse within a component render.""" from __future__ import annotations from dataclasses import dataclass import math import template_ref as ref @dataclass(frozen=True) class TextRole: family: str size: float weight: int line_height: float @property def svg_weight(self): return "normal" if self.weight == 400 else "bold" if self.weight == 700 else str(self.weight) @property def bold(self): return self.weight >= 600 @property def line_size(self): return self.size * self.line_height def resolve_role(style: dict, name: str) -> TextRole: typography = style["typography"] ref.require(name in typography["roles"], f"unknown typography role: {name}") role = typography["roles"][name] family = typography["families"][role["family_role"]] face = family["family"] ref.require(isinstance(face, str) and bool(face.strip()), f"{name}: font family required") ratio, points = style["canvas"]["pt_to_px"], role["size_pt"] line_height = role.get("line_height", 1.25) for value, label in ((ratio, "pt_to_px"), (points, "size_pt"), (line_height, "line_height")): ref.require(type(value) in (int, float) and math.isfinite(value) and value > 0, f"{name}: positive finite {label} required") # A role can refine its family weight without losing the family's defaults. raw = role.get("weight_number", role.get("weight", family.get("weight_number", family.get("weight", "Regular")))) if isinstance(raw, str): weights = {"thin": 100, "extralight": 200, "light": 300, "regular": 400, "normal": 400, "medium": 500, "semibold": 600, "bold": 700, "extrabold": 800, "black": 900} raw = weights.get(raw.lower().replace(" ", "").replace("-", ""), raw) if isinstance(raw, str) and raw.isdigit(): raw = int(raw) ref.require(type(raw) is int and 1 <= raw <= 1000, f"{name}: invalid font weight") return TextRole(face, points * ratio, raw, line_height)