from __future__ import annotations import re from pathlib import Path ROOT = Path(__file__).resolve().parents[2] TARGETS = [ ROOT / "backend/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions", ROOT / "Programme/contenus_pedagogiques/cycle_3/mathematiques/cm1/01_nombres_calcul_resolution/01B_fractions", ] STYLE_RE = re.compile(r"\.([A-Za-z0-9_-]+)\s*\{([^}]*)\}") FONT_RE = re.compile(r"font-size\s*:\s*([0-9.]+)px?") FRACTION_GROUP_RE = re.compile( r'(?P]*class="[^"]*\bfraction-g\b[^"]*")[^>]*>)(?P.*?)(?P)', re.DOTALL, ) TEXT_RE = re.compile(r"(]*\by=\")[-0-9.]+(\"[^>]*>)") LINE_Y_RE = re.compile(r"]*\by1=\"(?P[-0-9.]+)\"[^>]*\by2=\"(?P[-0-9.]+)\"") DATA_BOX_RE = re.compile(r'data-box="(?P[-0-9.]+) (?P[-0-9.]+) (?P[-0-9.]+) (?P[-0-9.]+)"') def parse_class_font_sizes(svg: str) -> dict[str, float]: sizes: dict[str, float] = {} for class_name, style in STYLE_RE.findall(svg): match = FONT_RE.search(style) if match: sizes[class_name] = float(match.group(1)) return sizes def group_font_size(body: str, class_sizes: dict[str, float]) -> float: class_match = re.search(r']*class="([^"]+)"', body) if class_match: for class_name in class_match.group(1).split(): if class_name in class_sizes: return class_sizes[class_name] direct = re.search(r']*font-size="([0-9.]+)"', body) if direct: return float(direct.group(1)) return 36.0 def compact_group(match: re.Match[str], class_sizes: dict[str, float]) -> str: body = match.group("body") line_match = LINE_Y_RE.search(body) if not line_match: return match.group(0) bar_y = (float(line_match.group("y1")) + float(line_match.group("y2"))) / 2 size = group_font_size(body, class_sizes) numerator_y = bar_y - min(max(8.0, size * 0.38), size * 0.50) # SVG text y is a baseline, not the visible top of the digit. The denominator # baseline must therefore sit lower so the glyph itself clears the bar. denominator_y = bar_y + max(18.0, size * 1.05) replacements = [f"{numerator_y:g}", f"{denominator_y:g}"] def replace_text_y(text_match: re.Match[str]) -> str: if not replacements: return text_match.group(0) return f"{text_match.group(1)}{replacements.pop(0)}{text_match.group(2)}" body = TEXT_RE.sub(replace_text_y, body, count=2) data_box_y = numerator_y - size * 0.85 - 2 data_box_height = denominator_y + size * 0.25 + 2 - data_box_y prefix = DATA_BOX_RE.sub( lambda data_match: ( f'data-box="{data_match.group("x")} {data_box_y:g} ' f'{data_match.group("w")} {data_box_height:g}"' ), match.group("prefix"), count=1, ) return f"{prefix}{body}{match.group('suffix')}" def compact_svg(path: Path) -> None: svg = path.read_text(encoding="utf-8") class_sizes = parse_class_font_sizes(svg) compacted = FRACTION_GROUP_RE.sub(lambda match: compact_group(match, class_sizes), svg) if compacted != svg: path.write_text(compacted, encoding="utf-8") def main() -> None: for target in TARGETS: for path in sorted(target.rglob("*.svg")): compact_svg(path) print(f"compacted {path}") if __name__ == "__main__": main()