"""Audit aller ``style=""``-Vorkommen in app/templates/. Wird vom Test ``tests/test_inline_styles_baseline.py`` als Anti-Regression- Wache benutzt: die aktuelle Zählung pro Datei darf nicht über die Baseline gehen. Damit verhindert der Test neue Inline-Styles, ohne existierende sofort migrieren zu müssen (#184). Ausgabe (CLI): - ``--audit``: Total + per-Cluster + Top-Files - ``--baseline``: Maschinen-lesbare Zählung als JSON {file: count} """ import argparse import json import re import sys from collections import defaultdict from pathlib import Path ROOT = Path(__file__).resolve().parent.parent / "app" / "templates" PAT = re.compile(r'style="([^"]+)"') def count_per_file() -> dict[str, int]: """{relative_path: count} für alle .html-Templates.""" out: dict[str, int] = {} for f in sorted(set(ROOT.rglob("*.html"))): rel = str(f.relative_to(ROOT.parent.parent)) n = sum(1 for _ in PAT.finditer(f.read_text(errors="replace"))) if n > 0: out[rel] = n return out def cluster_decl(declarations: str) -> str: decl = declarations.strip().split(";")[0].strip() if ":" not in decl: return "unparseable" prop = decl.split(":", 1)[0].strip().lower() if prop in ("color", "background", "background-color", "border", "border-color", "border-left", "border-top"): return "color" if prop in ("font-size", "font-family", "font-weight", "letter-spacing", "line-height", "text-transform"): return "typography" if prop in ("padding", "padding-left", "padding-right", "padding-top", "padding-bottom", "margin", "margin-left", "margin-right", "margin-top", "margin-bottom", "gap", "width", "height", "max-width", "min-width", "max-height", "min-height", "display", "flex", "flex-direction", "flex-wrap", "align-items", "justify-content", "position", "top", "left", "right", "bottom", "z-index"): return "layout" return f"other ({prop})" def cluster_summary() -> dict[str, int]: out: dict[str, int] = defaultdict(int) for f in sorted(set(ROOT.rglob("*.html"))): for m in PAT.finditer(f.read_text(errors="replace")): out[cluster_decl(m.group(1))] += 1 return dict(out) def main(): p = argparse.ArgumentParser() p.add_argument("--baseline", action="store_true", help="emittiere {file: count} JSON für Test-Baseline") p.add_argument("--audit", action="store_true", help="Cluster + Top-Files (default)") args = p.parse_args() counts = count_per_file() if args.baseline: json.dump(counts, sys.stdout, indent=2, sort_keys=True) sys.stdout.write("\n") return total = sum(counts.values()) print(f"Total style=\"\"-Vorkommen: {total}") print() print("Per-Cluster:") for k, v in sorted(cluster_summary().items(), key=lambda x: -x[1]): print(f" {k:25s} {v:5d}") print() print("Top-15 Files:") for f, n in sorted(counts.items(), key=lambda x: -x[1])[:15]: print(f" {n:5d} {f}") if __name__ == "__main__": main()