#!/usr/bin/env python3 """ wrap_textbook_xp.py — Add `data-gamified` attribute to XP UI in textbooks. Part of Phase 1 of the gamification kill-switch (Variant 2 of plan §E.3). The CSS rule `body.no-gamification [data-gamified] { display:none }` catches every wrapped block in one selector, so future authors don't have to invent new class names just to be hidden by the master switch. What we wrap: •
— chapter & hub hero badges •
— hub overall-progress XP pill •
— chapter inline progress card • inline JS that generates `
` HTML strings • `.xp-bar` containers when they're standalone Idempotent: if `data-gamified` is already present on an opening tag the script skips it. Reports each file touched. Run from repo root: python backend/scripts/wrap_textbook_xp.py """ import re import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] / "frontend" / "textbooks" if not ROOT.is_dir(): print(f"[wrap-xp] textbook dir not found: {ROOT}", file=sys.stderr) sys.exit(1) # Selectors that mark an opening tag as an XP block. The first group # captures ``. PATTERNS = [ # <... id="hero-xp-badge" ...> re.compile(r'(<[a-zA-Z][^>]*?\bid="hero-xp-badge"[^>]*?)(>)'), # <... class="po-xp" ...> or class="...po-xp..." re.compile(r'(<[a-zA-Z][^>]*?\bclass="[^"]*\bpo-xp\b[^"]*"[^>]*?)(>)'), # <... class="...xp-card..."> — covers HTML and inline JS `'
'` re.compile(r'(<[a-zA-Z][^>]*?\bclass="[^"]*\bxp-card\b[^"]*"[^>]*?)(>)'), # Standalone class="hero-xp-badge" re.compile(r'(<[a-zA-Z][^>]*?\bclass="[^"]*\bhero-xp-badge\b[^"]*"[^>]*?)(>)'), ] # Already-wrapped check RX_HAS_ATTR = re.compile(r'\bdata-gamified\b') def patch_file(path: Path) -> int: """Return number of opening tags newly given data-gamified.""" text = path.read_text(encoding="utf-8") new = text changes = 0 for rx in PATTERNS: def sub(m, _rx=rx): nonlocal changes head, close = m.group(1), m.group(2) if RX_HAS_ATTR.search(head): return m.group(0) changes += 1 sep = "" if head.endswith(" ") else " " return f'{head}{sep}data-gamified{close}' new = rx.sub(sub, new) if new != text: path.write_text(new, encoding="utf-8") return changes def main(): files = sorted(ROOT.glob("*.html")) total_files = 0 total_tags = 0 for f in files: try: n = patch_file(f) except Exception as e: print(f"[wrap-xp] {f.name}: ERROR {e}", file=sys.stderr) continue if n: print(f"[wrap-xp] {f.name}: +{n} tag(s)") total_files += 1 total_tags += n print(f"[wrap-xp] done — patched {total_tags} tags across {total_files} files " f"(scanned {len(files)})") if __name__ == "__main__": main()