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