Files
onepager/tools/patch-theme.py
mAi a06a94ff58 feat: #13 Light/Dark + EN/DE Toggle — Shift-2 Rollout
Rollout des Toggle-Patterns auf alle 57 statischen Sites (dasbes.de + dumusst.com sind dynamic, kein index.html).

1. **Bulk-Wiring (53 Sites)** via tools/patch-theme.py:
   - Anti-FOUC inline IIFE im <head> (vor erstem Paint)
   - <link rel="stylesheet" href="/shared/css/theme.css">
   - <script src="/shared/theme.js"> + toggles.js (i18n.js bleibt, hängt sich ans neue Widget)

2. **Per-Site Light-Overrides (14 Sites)** via tools/patch-light-overrides.py:
   - 6034, allainallain, commanderkin, hallofraumaier, heygoldi, keinefreun, lexsiebels, machesdocheinfach, matthiasbreier, orakil, osterai, patentonkel, traihard, wartebitte
   - Pro Site nur die failing accent-vars darkened (--green-dim, --text-faint, --warm-dim, --gold-dim, etc.)
   - AA 4.5:1+ auf white bg gesichert; Brand-Akzent erhalten

3. **data-theme-lock="dark" (4 Sites)** auf <html>:
   - kilibri, killusion, killionaer, killuminati
   - Aesthetisch dark-only — toggles.js blendet Theme-Button automatisch aus, Lang-Button bleibt

4. **Footer-Toggle Removal (52 Sites)** via tools/remove-footer-toggle.py:
   - Bestehende footer [data-i18n-toggle] Buttons entfernt — top-right widget übernimmt
   - Disclaimer-Information in tooltip des neuen Buttons + ai-disclosure.js footer

QA:
- ./build.sh: 59/59 sites built clean
- contrast-audit.py --both: 0/59 dark fail, 0/59 light fail
- anti-ai-lint: 0/57 sites flagged

Tools committed (idempotent, für Wiederverwendung):
- tools/patch-theme.py (--all wired alle Sites)
- tools/patch-light-overrides.py (per-site OVERRIDES dict)
- tools/remove-footer-toggle.py (4 Patterns für versch. Footer-Strukturen)
2026-05-08 11:16:15 +02:00

135 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Patch onepager sites with theme-toggle wiring.
Inserts (idempotent):
1. anti-FOUC inline IIFE after <meta name="viewport">
2. <link rel="stylesheet" href="/shared/css/theme.css"> before first <style>
3. <script src="/shared/theme.js"> before <script src="/shared/i18n.js">
4. <script src="/shared/toggles.js"> after <script src="/shared/i18n.js">
Skips sites that already have any of the markers.
Usage:
python3 tools/patch-theme.py <site-dir> [<site-dir> ...]
python3 tools/patch-theme.py --all # all sites with index.html, skip dynamic
"""
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SITES_DIR = ROOT / "sites"
ANTI_FOUC = "<script>(function(){try{var t=localStorage.getItem('onepager-theme');if(!t)t=matchMedia('(prefers-color-scheme: light)').matches?'light':'dark';document.documentElement.setAttribute('data-theme',t);}catch(e){document.documentElement.setAttribute('data-theme','dark');}})();</script>"
THEME_LINK = '<link rel="stylesheet" href="/shared/css/theme.css">'
THEME_SCRIPT = '<script src="/shared/theme.js"></script>'
TOGGLES_SCRIPT = '<script src="/shared/toggles.js"></script>'
def detect_indent(line):
"""Return leading whitespace prefix of a line."""
m = re.match(r"^(\s*)", line)
return m.group(1) if m else ""
def patch(html):
"""Apply 4 insertions idempotently. Returns (new_html, changed_bool)."""
changed = False
# 1) Anti-FOUC after <meta name="viewport"...>
if "onepager-theme" not in html:
m = re.search(r'^([ \t]*)<meta\s+name="viewport"[^>]*>\s*$', html, re.MULTILINE)
if m:
indent = m.group(1)
insert = f"\n{indent}{ANTI_FOUC}"
html = html[: m.end()] + insert + html[m.end():]
changed = True
else:
return html, False # can't anchor — bail
# 2) <link rel="stylesheet" href="/shared/css/theme.css"> before first <style>
if THEME_LINK not in html:
m = re.search(r'^([ \t]*)<style[ >]', html, re.MULTILINE)
if m:
indent = m.group(1)
insert = f"{indent}{THEME_LINK}\n"
html = html[: m.start()] + insert + html[m.start():]
changed = True
# 3) theme.js before i18n.js
if "/shared/theme.js" not in html:
m = re.search(r'^([ \t]*)<script\s+src="/shared/i18n\.js"></script>\s*$', html, re.MULTILINE)
if m:
indent = m.group(1)
insert = f"{indent}{THEME_SCRIPT}\n"
html = html[: m.start()] + insert + html[m.start():]
changed = True
# 4) toggles.js after i18n.js
if "/shared/toggles.js" not in html:
m = re.search(r'^([ \t]*)<script\s+src="/shared/i18n\.js"></script>\s*$', html, re.MULTILINE)
if m:
indent = m.group(1)
# Insert AFTER the i18n.js line
end = m.end()
# Make sure we put on a new line
insert = f"\n{indent}{TOGGLES_SCRIPT}"
html = html[:end] + insert + html[end:]
changed = True
return html, changed
def main():
if "--all" in sys.argv:
sites = []
for d in sorted(SITES_DIR.iterdir()):
if d.is_dir() and (d / "index.html").exists():
sites.append(d)
else:
sites = []
for arg in sys.argv[1:]:
p = Path(arg)
if not p.is_absolute():
p = SITES_DIR / arg
if not p.is_dir():
print(f"skip: {arg} not a directory", file=sys.stderr)
continue
if not (p / "index.html").exists():
print(f"skip: {p}/index.html missing", file=sys.stderr)
continue
sites.append(p)
if not sites:
print("no sites to patch", file=sys.stderr)
sys.exit(2)
patched, skipped, missing_anchor = 0, 0, []
for site in sites:
path = site / "index.html"
before = path.read_text()
after, changed = patch(before)
if not changed:
if "onepager-theme" in before and "/shared/toggles.js" in before:
skipped += 1
print(f" [unchanged] {site.name}")
else:
missing_anchor.append(site.name)
print(f" [no-anchor] {site.name}")
else:
path.write_text(after)
patched += 1
print(f" [patched] {site.name}")
print(f"\nPatched: {patched}, Already-up-to-date: {skipped}, No-anchor: {len(missing_anchor)}")
if missing_anchor:
print("Missing anchors (manual fix needed):")
for s in missing_anchor:
print(f" - {s}")
sys.exit(1)
if __name__ == "__main__":
main()