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)
135 lines
4.7 KiB
Python
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()
|