#!/usr/bin/env python3 """Prüft, ob App und Weboberfläche dieselben Farben und Formen benutzen. **Warum es das braucht.** Die Werte stehen an mehreren Stellen von Hand abgeschrieben — in Swift für die Apps, in CSS für den Browser. Sie sind auseinandergelaufen, und zwar unsichtbar: `#1D1D21` gegen `#1D1D22`, `#A0A0A8` gegen `#B4B4BE`, Radius 8 gegen 6. Einzeln merkt das niemand. Zusammen sorgen sie dafür, dass App und Browser nicht wie dasselbe Programm aussehen. **Warum kein Generator.** Einer müsste Swift erzeugen, und erzeugter Swift lässt sich schlechter lesen und kommentieren als geschriebener. Diese Prüfung kostet weniger und fängt dasselbe ab: sie sagt, dass etwas auseinanderläuft, und überlässt die Entscheidung dem Menschen. Aufruf: python3 Tools/tokens-pruefen.py Rückgabe: 0, wenn alles übereinstimmt; sonst 1. """ from __future__ import annotations import re import sys from pathlib import Path WURZEL = Path(__file__).resolve().parent.parent SWIFT_FARBEN = WURZEL / "Packages/FlaroKit/Sources/FlaroDesign/Color/FlaroColor.swift" SWIFT_AKZENT = WURZEL / "Packages/FlaroKit/Sources/FlaroDesign/Color/AccentOption.swift" SWIFT_FORMEN = WURZEL / "Packages/FlaroKit/Sources/FlaroDesign/FlaroFont.swift" CSS = WURZEL / "web/app/src/style.css" # Was womit zusammengehört. Links der Swift-Name, rechts das CSS-Token. FARBEN = { "canvas": "--canvas", "surface": "--surface", "elevated": "--elevated", "placeholder": "--placeholder", "textPrimary": "--text", "textSecondary": "--text-dim", "textMuted": "--text-muted", "destructive": "--destructive", } # Radien: Swift-Name → CSS-Token. `sheet` und `panel` haben im Browser kein # Gegenstück — dort gibt es keine Blätter und keine schwebende Seitenleiste. RADIEN = { "card": "--r-karte", "control": "--r-flaeche", } def hex_aus_swift(text: str, name: str) -> str | None: """Liest `static let name = RGB(hex: 0xAABBCC)` oder `RGB(0xAABBCC)`.""" m = re.search(rf"\b{name}\b[^\n=]*=\s*[^\n]*?0x([0-9A-Fa-f]{{6}})", text) return "#" + m.group(1).upper() if m else None def ohne_tvos(text: str) -> str: """Entfernt die tvOS-Zweige. **Sonst vergleicht man den falschen Wert.** `FlaroRadius.card` ist 8 — und auf tvOS 12, weil aus drei Metern eine stärkere Rundung nötig ist. Der Browser läuft nie auf einem Fernseher; massgeblich ist der andere Zweig. """ return re.sub(r"#if os\(tvOS\).*?(?:#else(.*?))?#endif", lambda m: m.group(1) or "", text, flags=re.S) def zahl_aus_swift(text: str, name: str) -> float | None: """Liest den ersten Zahlenwert hinter `static let name`.""" m = re.search(rf"\b{name}\b\s*:\s*CGFloat\s*(?:\{{[^}}]*?return\s*)?([0-9]+(?:\.[0-9]+)?)", text, re.S) if m: return float(m.group(1)) m = re.search(rf"\b{name}\b[^\n=]*=\s*([0-9]+(?:\.[0-9]+)?)", text) return float(m.group(1)) if m else None def token_aus_css(text: str, name: str) -> str | None: m = re.search(rf"{re.escape(name)}\s*:\s*([^;]+);", text) return m.group(1).strip() if m else None def main() -> int: fehlend = [p for p in (SWIFT_FARBEN, SWIFT_AKZENT, SWIFT_FORMEN, CSS) if not p.exists()] if fehlend: for p in fehlend: print(f"Nicht gefunden: {p.relative_to(WURZEL)}") return 1 farben_swift = SWIFT_FARBEN.read_text(encoding="utf-8") akzent_swift = SWIFT_AKZENT.read_text(encoding="utf-8") formen_swift = ohne_tvos(SWIFT_FORMEN.read_text(encoding="utf-8")) css = CSS.read_text(encoding="utf-8") abweichungen: list[str] = [] geprueft = 0 for swift_name, css_name in FARBEN.items(): a = hex_aus_swift(farben_swift, swift_name) b = token_aus_css(css, css_name) if a is None or b is None: abweichungen.append(f" {swift_name} / {css_name}: nicht gefunden ({a} / {b})") continue geprueft += 1 if a.upper() != b.upper(): abweichungen.append(f" {swift_name}: Swift {a}, CSS {css_name} {b}") # Die Vorgabefarbe der App gegen den Akzent im Browser. vorgabe = re.search(r'identifier\s*==\s*"([a-z]+)"', akzent_swift) if not vorgabe: vorgabe = re.search(r'\.first\s*\{\s*\$0\.id\s*==\s*"([a-z]+)"', akzent_swift) if vorgabe: hex_vorgabe = hex_aus_swift(akzent_swift, vorgabe.group(1)) akzent_css = token_aus_css(css, "--accent") if hex_vorgabe and akzent_css: geprueft += 1 if hex_vorgabe.upper() != akzent_css.upper(): abweichungen.append( f" Vorgabe-Akzent: Swift {vorgabe.group(1)} {hex_vorgabe}, CSS {akzent_css}") for swift_name, css_name in RADIEN.items(): a = zahl_aus_swift(formen_swift, swift_name) b = token_aus_css(css, css_name) if a is None or b is None: abweichungen.append(f" Radius {swift_name} / {css_name}: nicht gefunden ({a} / {b})") continue geprueft += 1 b_zahl = float(re.sub(r"[^0-9.]", "", b) or 0) if abs(a - b_zahl) > 0.01: abweichungen.append(f" Radius {swift_name}: Swift {a:g}, CSS {css_name} {b}") if abweichungen: print(f"App und Weboberfläche laufen auseinander ({len(abweichungen)} Stelle(n)):") print("\n".join(abweichungen)) print("\nFlaroDesign ist massgeblich — die CSS zieht nach.") return 1 print(f"App und Weboberfläche stimmen überein ({geprueft} Werte geprüft).") return 0 if __name__ == "__main__": sys.exit(main())