Godstrument/build_manual.py
type-two 297ca8457f 📖 add a polished standalone user manual (viz/manual.html) + generator
The public site only had the in-app grimoire (behind the ✦ overlay); there was no
shareable, linkable user manual. build_manual.py extracts the grimoire (the canon,
per CLAUDE.md) + its styles from viz/index.html into a self-contained, always-
visible page with a generated 27-section table of contents and a 'play the
instrument →' back-link. Single source → no drift; re-run after editing the grimoire.

Served at godstrument.pro/manual.html (docroot is viz/). Verified: renders as a
normal page (overlay/close-button overridden away), TOC anchors resolve, all
feature sections present (zero, OMNI panel, vibes, dimensions), console clean.

Also refine the deploy convention: exclude dev docs/scripts (CLAUDE.md, README,
ZERO_OMNI_BRIEF.md, GODSTRUMENT_MANUAL_SOURCE.md, godstrument.txt, build_manual.py,
tests) so working notes never land on the public box.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 06:49:25 +10:00

106 lines
5.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Generate viz/manual.html — the polished standalone user manual — from the
in-app grimoire in viz/index.html (the single source of truth per CLAUDE.md).
Re-run whenever the grimoire changes: python3 build_manual.py
ponytail: string-slice extraction, no HTML parser dep. The grimoire is a flat,
well-formed block; if its shape ever changes, this fails loudly (asserts) rather
than emitting a broken page.
"""
import re
import sys
import pathlib
HERE = pathlib.Path(__file__).parent
SRC = HERE / "viz" / "index.html"
OUT = HERE / "viz" / "manual.html"
html = SRC.read_text(encoding="utf-8")
# --- 1. the whole <style> block (includes :root vars + all #grimoire rules) ---
m = re.search(r"<style>(.*?)</style>", html, re.S)
assert m, "no <style> block found"
css = m.group(1)
# --- 2. the #grimoire block, balance-matched on <div>/</div> ------------------
start = html.find('<div id="grimoire">')
assert start != -1, "no #grimoire div found"
i, depth = start, 0
for tok in re.finditer(r"<div\b|</div>", html[start:]):
depth += 1 if tok.group() == "<div" else -1
if depth == 0:
end = start + tok.end()
break
else:
sys.exit("unbalanced #grimoire div")
grim = html[start:end]
# strip the in-app chrome: the × close button (the TOC lives outside this div)
grim = grim.replace('<span class="x">×</span>', "")
# --- 3. a generated table of contents from the <h2> headings -----------------
def slug(t):
return re.sub(r"[^a-z0-9]+", "-", re.sub(r"<[^>]+>", "", t).lower()).strip("-")
heads = re.findall(r"<h2>(.*?)</h2>", grim, re.S)
seen, toc = set(), []
for h in heads:
s = slug(h) or "section"
while s in seen:
s += "-x"
seen.add(s)
grim = grim.replace(f"<h2>{h}</h2>", f'<h2 id="{s}">{h}</h2>', 1)
toc.append(f'<a href="#{s}">{re.sub(r"<[^>]+>", "", h)}</a>')
assert toc, "no <h2> sections to build a TOC from"
toc_html = '<nav class="manual-toc"><div class="toc-h">Contents</div>' + "".join(toc) + "</nav>"
# --- 4. wrap into a standalone, always-visible page --------------------------
page = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Godstrument — User Manual</title>
<meta name="description" content="How to play Godstrument — the instrument played by the living world. Every feature, from the world's feeds to the OMNI synth, zero mode, vibes and MIDI/CV out.">
<style>{css}</style>
<style>
/* standalone-manual overrides — render the grimoire as a normal scrolling page */
html, body {{ margin: 0; }}
body {{ background: radial-gradient(ellipse at 50% 0%, #0c1533 0%, #070c1c 60%, #03050e 100%);
min-height: 100vh; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; }}
#grimoire {{ position: static !important; display: block !important; inset: auto !important;
overflow: visible !important; background: none !important; z-index: auto !important; }}
#grimoire .x {{ display: none !important; }}
.manual-bar {{ position: sticky; top: 0; z-index: 5; display: flex; align-items: center; justify-content: space-between;
padding: 11px 22px; background: rgba(6,10,20,0.72); -webkit-backdrop-filter: blur(9px); backdrop-filter: blur(9px);
border-bottom: 1px solid rgba(120,150,200,0.16); }}
.manual-bar .brand {{ color: #d6e2f4; font-weight: 800; letter-spacing: 2px; font-size: 14px; }}
.manual-bar a {{ color: #9fd0ff; text-decoration: none; font-size: 13px; letter-spacing: 0.4px; }}
.manual-bar a:hover {{ color: #fff; }}
.manual-toc {{ max-width: 720px; margin: 26px auto 8px; padding: 16px 22px; border-radius: 14px;
background: rgba(20,28,46,0.5); border: 1px solid rgba(120,150,220,0.2);
display: grid; grid-template-columns: 1fr 1fr; gap: 4px 22px; }}
.manual-toc .toc-h {{ grid-column: 1 / -1; font-weight: 800; letter-spacing: 1px; color: #d3e2ff; font-size: 13px;
text-transform: uppercase; margin-bottom: 4px; }}
.manual-toc a {{ color: rgba(180,200,235,0.85); text-decoration: none; font-size: 12.5px; line-height: 1.9;
border-bottom: 1px solid transparent; }}
.manual-toc a:hover {{ color: #eaf1ff; border-bottom-color: rgba(160,185,225,0.4); }}
#grimoire h2 {{ scroll-margin-top: 60px; }}
@media (max-width: 560px) {{ .manual-toc {{ grid-template-columns: 1fr; }} }}
</style>
</head>
<body>
<div class="manual-bar"><span class="brand">✦ GODSTRUMENT — the manual</span><a href="/">▶ play the instrument →</a></div>
{grim.replace('<div class="wrap">', '<div class="wrap">' + toc_html, 1)}
</body>
</html>
"""
OUT.write_text(page, encoding="utf-8")
print(f"wrote {OUT} ({len(page):,} bytes, {len(toc)} sections)")
# self-check: the page must be self-contained and carry the real feature sections
assert "OMNI" in page and "Building from zero" in page and "dimensions" in page.lower(), "manual missing key feature sections"
assert "id=\"grimoire\"" in page and page.count("<h2") >= 8, "manual structure looks wrong"
print("ok: self-contained, has OMNI / zero / dimensions sections")