gwoe-antragspruefer/tools/audit_inline_styles.py
Dotty Dotter 57e11b3da7 chore(ci): Anti-Regression-Wache gegen neue Inline-Styles (#184)
Nicht alle 1322 Inline-Style-Vorkommen in einer Sitzung migrieren — aber
zumindest verhindern, dass es mehr werden. Drei Bausteine:

1. ``tools/audit_inline_styles.py`` — CLI für Audit (Cluster, Top-Files)
   und Baseline-Erzeugung (--baseline → JSON {file: count}).

2. ``tools/inline_styles_baseline.json`` — eingefrorene IST-Zählung pro
   Template-Datei. Wird vom Test als obere Schranke gelesen.

3. ``tests/test_inline_styles_baseline.py`` (3 Tests) — pro Datei
   und global: aktuelle Anzahl <= Baseline. Schlägt Alarm bei neuen
   Inline-Styles und auch bei neuen Templates mit Inline-Styles, die
   noch nicht in der Baseline stehen.

Workflow für künftige Migrationen: Inline-Styles in einer Datei nach
benannten Klassen überführen, Baseline neu einfrieren via
``python3 tools/audit_inline_styles.py --baseline > tools/inline_styles_baseline.json``.

Cluster-Verteilung der 1322 Treffer:
- layout: 625, typography: 323, color: 262, sonstige: 112.

Top-Brennpunkte: index.html (463, Classic-UI Legacy),
auswertungen.html (125), antrag_detail.html v2 (119),
aktuelle-themen.html (82).

1220/1220 Tests grün.
2026-05-09 02:32:35 +02:00

93 lines
3.2 KiB
Python

"""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()