docs(api): per-symbol pages, fuzzy search, deep token linking, hover cards
All checks were successful
docs / build-and-deploy (push) Successful in 2s

Rebuild the API Reference around one page per symbol and richer, verified content.

Pages & navigation
- One HTML page per symbol (kw-*, type-*, phase-*, screen-*, fn-*, annot-*, op-*)
  instead of a single scrolling page; namespace overview pages (ns-screen …
  ns-color) and a searchable index (api.html) with client-side fuzzy search.
- Sticky-header scroll offset (scroll-margin) so a jumped-to entry/param/color is
  never hidden, plus a flash highlight on the scrolled-to target.

Deep linking in every snippet & example
- Namespace members split: `Screen`→namespace page, `fill_rectangle`→method page;
  `Color`→palette page, `Charcoal`→its swatch — separately.
- Named arguments (`width:`) link to that parameter's anchor on the method page.
- Hover any token for a summary card built from the real API data (symbols.json).

Content & coverage
- Full authoritative surface documented from the compiler: every keyword, type,
  the 6 phases (Start/Input/FixedUpdate/Update/LateUpdate/Render, each its own
  page), all 22 annotations, namespace methods with parameter docs, builtins,
  the world_* reflection ABI, networking, operators — 155 symbols.
- Longer, clearer explanations; "model"/"model instance" terminology, not "entity";
  descriptive identifiers in every example (Position{column,row}, Velocity{delta_x,
  delta_y}, Health{current,maximum}, Player/Enemy) — no Pos/Seg/x/dx.
- Accuracy fixes from compiler ground-truth: world_count() takes no arg,
  world_query_next(property, cursor) arg order, event fields bind by name; dropped
  `when` and `module` (not in the self-hosted parser).

Tooling
- inventory.json + check.py: coverage guard (every symbol has a page), duplicate-
  token guard, and broken-link guard — fail CI so docs can't drift.
- validate.py: compiles every ```ludic example against bin/ludicc (158 compile).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-29 17:53:22 +03:00
parent 25f987e30d
commit 3c7ec9b016
172 changed files with 5240 additions and 895 deletions

View file

@ -1,78 +1,87 @@
#!/usr/bin/env python3
"""gen.py — the Ludic documentation generator.
"""gen.py — the Ludic documentation generator (v2).
Single source of truth:
docs/language/<category>/<id>.md one file per keyword / type / builtin /
namespace method / operator / annotation
docs/language/<category>/_section.md section title + blurb + order
docs/language/colors/palette.json the named-color palette (from palette.py)
docs/site/site.json landing-page messaging (hero, features, …)
docs/site/snippets/*.ludic the code snippets shown on the landing page
docs/language/<category>/<id>.md one file per symbol (front-matter + body)
docs/language/<category>/_section.md section title/blurb/order
docs/language/colors/palette.json the named-color palette
docs/site/site.json landing-page messaging
docs/site/snippets/*.ludic real programs shown on the landing page
Outputs (into --out, default build/pages) — the whole pages-branch payload:
api.html the full API Reference, one entry per symbol
index.html the landing page (hero + showcase from real .ludic files)
ludic-highlight.js the highlighter, its symbol tables generated from the above
symbols.json the machine-readable symbol index (also useful to editors)
index.html the landing page
api.html the reference index, with fuzzy search
<id>.html ONE page per symbol (kw-handler.html, screen-clear.html…)
ns-<name>.html one overview page per namespace (Screen/Color/Input/…)
ludic-highlight.js the highlighter — its symbol tables, tips, per-item link
targets, per-parameter anchors and hover-card data all
generated from the sources above
symbols.json the machine-readable index (drives search + hover cards)
.nojekyll
Nothing here is hand-maintained twice: add a symbol file and it appears in the
reference, is recognized + tipped + linked in every snippet, and lands in
symbols.json — automatically. Zero third-party dependencies (stdlib only).
Every keyword/type/phase/builtin/namespace-method/annotation/operator/color is
addressable by its own page; named parameters and named colors are addressable
by anchor. Python standard library only.
"""
import json, os, html, re, sys, argparse
import json, os, html, re, argparse
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
ASSETS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
LANG = os.path.join(ROOT, "docs", "language")
SITE = os.path.join(ROOT, "docs", "site")
HEADER_OFFSET = 84 # sticky nav height; used for scroll-margin
def esc(s): return html.escape(s, quote=False)
def esc(s): return html.escape(s or "", quote=False)
def escattr(s): return html.escape(s or "", quote=True)
def fill(tmpl, mapping):
"""Placeholder substitution that never collides with % or { } in CSS/JS."""
for k, v in mapping.items():
tmpl = tmpl.replace("@@" + k + "@@", str(v))
return tmpl
def pct(tmpl, mapping):
"""Substitute only %(name)s markers; leave bare % (CSS 100%, code % 2) alone.
Replacement values are inserted literally and never re-scanned."""
return re.sub(r"%\((\w+)\)s", lambda m: mapping[m.group(1)], tmpl)
# ---------------------------------------------------------------------------
# parsing
# ---------------------------------------------------------------------------
FENCE = re.compile(r"```ludic\n(.*?)\n```", re.S)
PARAM = re.compile(r"^-\s*`?(\w+)`?\s*[—-]+\s*(.*)$")
# ---------------------------------------------------------------------------
# front-matter + body parsing (no yaml dependency)
# ---------------------------------------------------------------------------
def parse_doc(path):
text = open(path, encoding="utf-8").read()
meta, body = {}, text
if text.startswith("---"):
end = text.find("\n---", 3)
if end != -1:
fm = text[3:end].strip("\n")
for line in text[3:end].strip("\n").split("\n"):
if line.strip() and ":" in line:
k, v = line.split(":", 1)
meta[k.strip()] = v.strip()
body = text[end + 4:].lstrip("\n")
for line in fm.split("\n"):
if not line.strip() or ":" not in line:
continue
k, v = line.split(":", 1)
meta[k.strip()] = v.strip()
return meta, body
FENCE = re.compile(r"```ludic\n(.*?)\n```", re.S)
def codeify(s):
return re.sub(r"`([^`]+)`", r"<code>\1</code>", s)
def split_body(body):
"""Return (description_html, [examples]) — fences pulled out as examples."""
examples = FENCE.findall(body)
desc = FENCE.sub("", body).strip()
# author convenience: `code` -> <code>code</code> (leaves existing tags alone)
desc = re.sub(r"`([^`]+)`", r"<code>\1</code>", desc)
paras = [p.strip() for p in re.split(r"\n\s*\n", desc) if p.strip()]
desc_html = "</p><p>".join(paras)
return desc_html, examples
body = FENCE.sub("", body)
# pull out a "Parameters:" block
params, keep = [], []
lines = body.split("\n")
i, n = 0, len(lines)
while i < n:
if lines[i].strip().lower().startswith("parameters:"):
i += 1
while i < n and lines[i].strip():
m = PARAM.match(lines[i].strip())
if m:
params.append({"name": m.group(1), "desc": codeify(m.group(2).strip())})
i += 1
else:
keep.append(lines[i]); i += 1
desc = "\n".join(keep).strip()
paras = [codeify(p.strip()) for p in re.split(r"\n\s*\n", desc) if p.strip()]
return "</p><p>".join(paras), examples, params
# ---------------------------------------------------------------------------
# load the symbol model
# ---------------------------------------------------------------------------
def load_sections():
sections = []
for cat in sorted(os.listdir(LANG)):
@ -80,416 +89,304 @@ def load_sections():
if not os.path.isdir(cdir):
continue
smeta, sblurb = {}, ""
secpath = os.path.join(cdir, "_section.md")
if os.path.exists(secpath):
smeta, sbody = parse_doc(secpath)
sblurb = re.sub(r"`([^`]+)`", r"<code>\1</code>", sbody.strip())
sp = os.path.join(cdir, "_section.md")
if os.path.exists(sp):
smeta, sbody = parse_doc(sp)
sblurb = codeify(sbody.strip())
entries = []
for fn in os.listdir(cdir):
for fn in sorted(os.listdir(cdir)):
if not fn.endswith(".md") or fn == "_section.md":
continue
meta, body = parse_doc(os.path.join(cdir, fn))
desc, examples = split_body(body)
desc, examples, params = split_body(body)
meta["desc_html"] = desc
meta["examples"] = examples
meta["tokens_list"] = meta.get("tokens", "").split() if meta.get("tokens") else []
meta["params"] = params
meta["tokens_list"] = meta.get("tokens", "").split()
meta["related_list"] = meta.get("related", "").split()
entries.append(meta)
entries.sort(key=lambda e: (int(e.get("order", 999)), e.get("name", "")))
sections.append({
"id": smeta.get("id", cat),
"title": smeta.get("title", cat.title()),
"order": int(smeta.get("order", 999)),
"blurb": sblurb,
"entries": entries,
})
sections.append({"id": smeta.get("id", cat), "title": smeta.get("title", cat.title()),
"order": int(smeta.get("order", 999)), "blurb": sblurb,
"cat": cat, "entries": entries})
sections.sort(key=lambda s: (s["order"], s["title"]))
return sections
# ---------------------------------------------------------------------------
# build the highlighter symbol tables from the model
# symbol model for the highlighter + search + cards
# ---------------------------------------------------------------------------
def build_symbols(sections):
def ns_page(nsname): return "ns-" + nsname.lower()
def build_symbols(sections, palette):
sym = {"keywords": {}, "types": {}, "phases": {}, "builtins": {},
"nsmethods": {}, "annotations": {}, "tips": {},
"namespaces": [], "colors_anchor": "colors", "annotations_anchor": "annotations"}
namespaces = set()
"nsmethods": {}, "annotations": {}, "namespaces": {}, "tips": {},
"cards": {}, "colors_page": "ns-color"}
items = {} # id -> full record for search
for s in sections:
for e in s["entries"]:
kind = e.get("kind", "")
anchor = e["id"]
tip = e.get("tip", "")
eid = e["id"]; kind = e.get("kind", ""); tip = e.get("tip", "")
page = eid + ".html"
items[eid] = {"id": eid, "name": e.get("name", ""), "sig": e.get("sig", ""),
"kind": kind, "category": s["id"], "section": s["title"],
"tip": tip, "page": page}
sym["cards"][eid] = {"name": e.get("name",""), "sig": e.get("sig",""),
"tip": tip, "kind": kind, "page": page, "section": s["title"]}
for tok in e["tokens_list"]:
if kind == "keyword":
sym["keywords"][tok] = anchor
elif kind == "type":
sym["types"][tok] = anchor
elif kind == "phase":
sym["phases"][tok] = anchor
if kind == "keyword": sym["keywords"][tok] = eid
elif kind == "type": sym["types"][tok] = eid
elif kind == "phase": sym["phases"][tok] = eid
elif kind == "builtin":
sym["builtins"][tok] = anchor
sym["builtins"][tok] = {"id": eid, "params": [p["name"] for p in e["params"]]}
elif kind == "namespace-method":
sym["nsmethods"][tok] = anchor
if "." in tok:
namespaces.add(tok.split(".", 1)[0])
sym["nsmethods"][tok] = {"id": eid, "params": [p["name"] for p in e["params"]]}
elif kind == "annotation":
sym["annotations"][tok] = anchor
if tip:
sym["tips"][tok] = tip
if s["id"] == "colors":
sym["colors_anchor"] = "colors"
if s["id"] == "annotations":
sym["annotations_anchor"] = "annotations"
namespaces.add("Color") # Color.* is recognized and linked to the palette
sym["namespaces"] = sorted(namespaces)
return sym
sym["annotations"][tok] = eid
if tip: sym["tips"][tok] = tip
# namespaces: Screen/Input/Random/Map from ns methods, plus Color
nsnames = set()
for tok in sym["nsmethods"]:
if "." in tok: nsnames.add(tok.split(".", 1)[0])
nsnames.add("Color")
for nn in sorted(nsnames):
sym["namespaces"][nn] = ns_page(nn)
sym["cards"][ns_page(nn)] = {"name": nn, "sig": nn + ".*", "kind": "namespace",
"tip": NS_TIP.get(nn, ""), "page": ns_page(nn) + ".html",
"section": "Namespaces"}
return sym, items
NS_TIP = {"Screen": "The 2D drawing surface.", "Color": "The named color palette.",
"Input": "Reading the keyboard.", "Random": "The seeded, deterministic RNG.",
"Map": "The character-grid tilemap."}
# ---------------------------------------------------------------------------
# render the API Reference
# shared chrome
# ---------------------------------------------------------------------------
def render_entry(e):
ex = ""
for code in e.get("examples", []):
ex += '<pre data-lang="ludic" class="ex">' + esc(code) + "</pre>"
desc = e.get("desc_html", "")
return (
'<div class="entry" id="{id}">'
'<div class="entry-head"><h3>{name}</h3><a class="anchor" href="#{id}">#</a></div>'
'<code class="sig">{sig}</code>'
'<p>{desc}</p>{ex}</div>'
).format(id=e["id"], name=esc(e.get("name", "")), sig=esc(e.get("sig", "")),
desc=desc, ex=ex)
def nav_html(cfg, active=None):
out = []
for n in cfg["nav_links"]:
cls = 'class="nav-cta" ' if n.get("href") == "api.html" else ""
out.append('<a %shref="%s">%s</a>' % (cls, n["href"], esc(n["label"])))
return "".join(out)
def render_palette(palette):
out = ['<div class="swatches">']
for grp in palette["groups"]:
out.append('<div class="swatch-group"><h4>' + esc(grp["name"]) + '</h4><div class="swatch-row">')
for col in grp["colors"]:
h = col["hex"]
out.append(
'<div class="swatch"><span class="chip" style="background:#{h}"></span>'
'<span class="cname">Color.{n}</span><span class="chex">#{h}</span></div>'
.format(h=h, n=esc(col["name"])))
out.append("</div></div>")
out.append("</div>")
return "\n".join(out)
def render_section(s, palette):
if s["id"] == "colors":
body = render_palette(palette)
else:
body = "\n".join(render_entry(e) for e in s["entries"])
return ('<section class="ref-sec" id="{id}"><h2>{title}</h2>'
'<p class="sec-blurb">{blurb}</p>{body}</section>').format(
id=s["id"], title=esc(s["title"]), blurb=s["blurb"], body=body)
def render_api(sections, palette, cfg):
css = open(os.path.join(ASSETS, "api.css")).read()
nav = '<nav class="side">' + "".join(
'<a href="#{i}">{t}</a>'.format(i=s["id"], t=esc(s["title"])) for s in sections) + "</nav>"
content = "\n".join(render_section(s, palette) for s in sections)
navlinks = "".join(
'<a {cls}href="{h}">{l}</a>'.format(
h=n["href"], l=esc(n["label"]),
cls='class="nav-cta" ' if n.get("href") == "api.html" else "")
for n in cfg["nav_links"])
tmpl = """<!doctype html>
def head_html(title, desc, css_file):
css = open(os.path.join(ASSETS, css_file)).read()
return fill("""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ludic — API Reference</title>
<meta name="description" content="The complete Ludic API Reference: every keyword, type, builtin, the Screen/Color/Input/Random/Map namespaces, and the full named-color palette.">
<title>@@TITLE@@</title>
<meta name="description" content="@@DESC@@">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
@@CSS@@
</style>
</head>
<body>
<header class="nav">
<div class="wrap nav-in">
<a class="brand" href="index.html"><span class="logo">L</span> @@BRAND@@</a>
<nav class="nav-links">@@NAVLINKS@@</nav>
</div>
</header>
<div class="wrap ref-layout">
@@NAV@@
<main class="ref-main">
<div class="ref-intro">
<div class="kicker">Reference</div>
<h1>API Reference</h1>
<p>Every keyword, type, builtin, namespace and color in Ludic. In any code sample across this site, hover a token and click to jump straight to its entry here.</p>
</div>
@@CONTENT@@
</main>
</div>
<script src="ludic-highlight.js"></script>
<script>
Ludic.highlightAll();
const secs = [...document.querySelectorAll('.ref-sec')];
const navlinks = [...document.querySelectorAll('.side a')];
const spy = new IntersectionObserver((entries)=>{
entries.forEach(e=>{ if(e.isIntersecting){
navlinks.forEach(a=>a.classList.toggle('active', a.getAttribute('href')==='#'+e.target.id));
}});
},{rootMargin:'-10% 0px -80% 0px'});
secs.forEach(s=>spy.observe(s));
</script>
</body>
</html>
"""
return fill(tmpl, dict(CSS=css, BRAND=esc(cfg["brand"]), NAVLINKS=navlinks, NAV=nav, CONTENT=content))
</head>""", {"TITLE": esc(title), "DESC": escattr(desc), "CSS": css})
def topbar(cfg):
return ('<header class="nav"><div class="wrap nav-in">'
'<a class="brand" href="index.html"><span class="logo">L</span> %s</a>'
'<nav class="nav-links">%s</nav></div></header>') % (esc(cfg["brand"]), nav_html(cfg))
# ---------------------------------------------------------------------------
# render the landing page
# per-item page
# ---------------------------------------------------------------------------
def read_snippet(rel):
return open(os.path.join(ROOT, rel), encoding="utf-8").read().rstrip("\n")
def render_params(e):
if not e["params"]:
return ""
rows = "".join(
'<div class="param" id="param-%s"><code class="pname">%s</code>'
'<span class="pdesc">%s</span></div>' % (p["name"], esc(p["name"]), p["desc"])
for p in e["params"])
return '<div class="params"><h2>Parameters</h2>%s</div>' % rows
def render_examples(e):
if not e["examples"]:
return ""
blocks = "".join('<pre data-lang="ludic">%s</pre>' % esc(code) for code in e["examples"])
return '<div class="examples"><h2>Example</h2>%s</div>' % blocks
def render_related(e, items):
rel = [r for r in e["related_list"] if r in items]
if not rel:
return ""
links = "".join('<a class="rel" href="%s.html">%s</a>' % (r, esc(items[r]["name"])) for r in rel)
return '<div class="related"><h2>Related</h2><div class="rel-row">%s</div></div>' % links
def item_page(e, section, cfg, items):
kindlabel = {"namespace-method": "method", "builtin": "function"}.get(e.get("kind",""), e.get("kind",""))
body = fill("""@@TOPBAR@@
<main class="wrap item">
<div class="crumbs"><a href="api.html">API Reference</a> <span>›</span> <a href="@@SECHREF@@">@@SECTION@@</a> <span>›</span> <span class="here">@@NAME@@</span></div>
<div class="item-head">
<span class="kind-badge kind-@@KIND@@">@@KINDLABEL@@</span>
<h1 id="top">@@NAME@@</h1>
</div>
<code class="sig">@@SIG@@</code>
<div class="desc"><p>@@DESC@@</p></div>
@@PARAMS@@
@@EXAMPLES@@
@@RELATED@@
<a class="back" href="api.html">← All symbols</a>
</main>
<script src="ludic-highlight.js"></script>
<script>Ludic.highlightAll(); Ludic.installCards(); Ludic.flashTarget();</script>
</body></html>""", {
"TOPBAR": topbar(cfg),
"SECHREF": "ns-" + section["cat"] + ".html" if section["cat"] in ("screen","input","random","map") else "api.html#" + section["id"],
"SECTION": esc(section["title"]),
"NAME": esc(e.get("name","")),
"KIND": e.get("kind","").replace("namespace-method","method"),
"KINDLABEL": esc(kindlabel),
"SIG": esc(e.get("sig","")),
"DESC": e.get("desc_html","") or esc(e.get("tip","")),
"PARAMS": render_params(e),
"EXAMPLES": render_examples(e),
"RELATED": render_related(e, items),
})
return head_html(e.get("name","") + " — Ludic", e.get("tip",""), "item.css") + "\n<body>\n" + body
# ---------------------------------------------------------------------------
# namespace overview pages
# ---------------------------------------------------------------------------
def ns_overview_page(nsname, section, cfg):
rows = ""
for e in section["entries"]:
rows += ('<a class="ns-method" href="%s.html"><code class="nm-sig">%s</code>'
'<span class="nm-tip">%s</span></a>') % (e["id"], esc(e.get("sig","")), esc(e.get("tip","")))
body = fill("""@@TOPBAR@@
<main class="wrap item">
<div class="crumbs"><a href="api.html">API Reference</a> <span>›</span> <span class="here">@@NAME@@</span></div>
<div class="item-head"><span class="kind-badge kind-namespace">namespace</span><h1 id="top">@@NAME@@</h1></div>
<p class="ns-blurb">@@BLURB@@</p>
<div class="ns-methods">@@ROWS@@</div>
<a class="back" href="api.html">← All symbols</a>
</main>
<script src="ludic-highlight.js"></script>
<script>Ludic.installCards(); Ludic.flashTarget();</script>
</body></html>""", {"TOPBAR": topbar(cfg), "NAME": esc(nsname), "BLURB": section["blurb"], "ROWS": rows})
return head_html(nsname + " — Ludic", NS_TIP.get(nsname,""), "item.css") + "\n<body>\n" + body
def color_page(palette, cfg):
groups = ""
for grp in palette["groups"]:
sw = ""
for col in grp["colors"]:
h = col["hex"]; nm = col["name"]; aid = nm.lower()
sw += ('<div class="swatch" id="%s"><span class="chip" style="background:#%s"></span>'
'<span class="cname">Color.%s</span><span class="chex">#%s</span></div>'
) % (aid, h, esc(nm), h)
groups += '<div class="swatch-group"><h3>%s</h3><div class="swatch-row">%s</div></div>' % (esc(grp["name"]), sw)
body = fill("""@@TOPBAR@@
<main class="wrap item">
<div class="crumbs"><a href="api.html">API Reference</a> <span>›</span> <span class="here">Color</span></div>
<div class="item-head"><span class="kind-badge kind-namespace">namespace</span><h1 id="top">Color</h1></div>
<p class="ns-blurb">@@BLURB@@ Every <code>Color.Name</code> lowers to a plain <code>0xRRGGBB</code> integer at compile time — no runtime cost. @@COUNT@@ names are built in.</p>
@@GROUPS@@
<a class="back" href="api.html">← All symbols</a>
</main>
<script src="ludic-highlight.js"></script>
<script>Ludic.installCards(); Ludic.flashTarget();</script>
</body></html>""", {"TOPBAR": topbar(cfg), "BLURB": "The named color palette.",
"COUNT": palette["count"], "GROUPS": groups})
return head_html("Color — Ludic", "The Ludic named-color palette.", "item.css") + "\n<body>\n" + body
# ---------------------------------------------------------------------------
# api index with fuzzy search
# ---------------------------------------------------------------------------
def api_index(sections, cfg):
cards = ""
for s in sections:
if s["cat"] == "colors":
cards += ('<section class="idx-sec" data-sec="%s"><h2 id="%s">%s</h2>'
'<p class="sec-blurb">%s</p><div class="idx-grid">'
'<a class="idx-item" href="ns-color.html" data-name="Color" data-tip="The named color palette.">'
'<code>Color</code><span>The named color palette (221 names).</span></a></div></section>'
) % (s["id"], s["id"], esc(s["title"]), s["blurb"])
continue
rows = ""
for e in s["entries"]:
rows += ('<a class="idx-item" href="%s.html" data-name="%s" data-tip="%s"><code>%s</code><span>%s</span></a>'
) % (e["id"], escattr(e.get("name","")), escattr(e.get("tip","")),
esc(e.get("name","")), esc(e.get("tip","")))
cards += ('<section class="idx-sec" data-sec="%s"><h2 id="%s">%s</h2>'
'<p class="sec-blurb">%s</p><div class="idx-grid">%s</div></section>'
) % (s["id"], s["id"], esc(s["title"]), s["blurb"], rows)
body = fill("""@@TOPBAR@@
<main class="wrap ref">
<div class="ref-intro">
<div class="kicker">Reference</div>
<h1>API Reference</h1>
<p>Every keyword, type, phase, builtin, namespace method, annotation and color in Ludic — each on its own page. Search, or browse by section. In any code sample across this site, hover a token for a summary and click to jump to its page.</p>
<input id="search" class="search" type="search" placeholder="Search symbols… (e.g. handler, fill_rectangle, @Sync)" autocomplete="off" autofocus>
<div id="noresults" class="noresults" hidden>No symbols match.</div>
</div>
@@CARDS@@
</main>
<script src="ludic-highlight.js"></script>
<script>Ludic.installCards(); Ludic.installSearch();</script>
</body></html>""", {"TOPBAR": topbar(cfg), "CARDS": cards})
return head_html("Ludic — API Reference", "The complete Ludic API Reference: every keyword, type, builtin, namespace, annotation and color, each on its own page, with fuzzy search.", "item.css") + "\n<body>\n" + body
# ---------------------------------------------------------------------------
# landing page (data-driven, unchanged structure; highlighter now deep-links)
# ---------------------------------------------------------------------------
def read_snippet(rel): return open(os.path.join(ROOT, rel), encoding="utf-8").read().rstrip("\n")
def render_index(cfg):
css = open(os.path.join(ASSETS, "site.css")).read()
hero = cfg["hero"]
navlinks = "".join(
'<a {cls}href="{h}">{l}</a>'.format(
h=n["href"], l=esc(n["label"]),
cls='class="nav-cta" ' if n.get("href") == "api.html" else "")
for n in cfg["nav_links"])
# hero snippet
navlinks = nav_html(cfg)
hero_code = esc(read_snippet(hero["snippet"]))
# pipeline
stages = ""
for st in hero["pipeline"]:
hl = " hl" if st == "ludicc" else ""
stages += '<span class="stage%s">%s</span>' % (hl, esc(st))
if st != hero["pipeline"][-1]:
stages += '<span class="arrow">→</span>'
# features
feats = ""
for c in cfg["features"]["cards"]:
feats += ('<div class="feat reveal"><div class="ico">%s</div>'
'<h3>%s</h3><p>%s</p></div>') % (c["icon"], c["title"], c["html"])
# philosophy
if st != hero["pipeline"][-1]: stages += '<span class="arrow">→</span>'
feats = "".join('<div class="feat reveal"><div class="ico">%s</div><h3>%s</h3><p>%s</p></div>'
% (c["icon"], c["title"], c["html"]) for c in cfg["features"]["cards"])
phil = cfg["philosophy"]
phil_paras = "".join("<p>%s</p>" % p for p in phil["paras"])
phil_stats = "".join('<div class="stat"><div class="big">%s</div><div class="lbl">%s</div></div>'
% (s["big"], esc(s["lbl"])) for s in phil["stats"])
# get-started steps + terminal
start = cfg["start"]
steps = ""
for i, s in enumerate(start["steps"], 1):
steps += ('<div class="step reveal"><div class="n">%d</div>'
'<div><h4>%s</h4><p>%s</p></div></div>') % (i, s["title"], s["html"])
steps = "".join('<div class="step reveal"><div class="n">%d</div><div><h4>%s</h4><p>%s</p></div></div>'
% (i, s["title"], s["html"]) for i, s in enumerate(start["steps"], 1))
term = ""
for t in start["terminal"]:
if t.get("blank"):
term += "\n"
elif "comment" in t:
term += '<span class="t-com"># %s</span>\n' % esc(t["comment"])
elif "cmd" in t:
term += '<span class="prompt">$</span> %s\n' % esc(t["cmd"])
elif "out" in t:
term += '<span class="out">%s</span>\n' % esc(t["out"])
# editors
if t.get("blank"): term += "\n"
elif "comment" in t: term += '<span class="t-com"># %s</span>\n' % esc(t["comment"])
elif "cmd" in t: term += '<span class="prompt">$</span> %s\n' % esc(t["cmd"])
elif "out" in t: term += '<span class="out">%s</span>\n' % esc(t["out"])
ed = cfg["editors"]
eds = "".join('<div class="ed"><span class="k">◆</span> %s</div>' % esc(x) for x in ed["list"])
# showcase samples -> JS array, code read from real files
samples = []
for s in cfg["showcase"]["samples"]:
samples.append({"name": s["name"], "label": s["label"],
"note": s["note"], "code": read_snippet(s["file"])})
samples_json = json.dumps(samples)
# footer links
samples = [{"name": s["name"], "label": s["label"], "note": s["note"], "code": read_snippet(s["file"])}
for s in cfg["showcase"]["samples"]]
footlinks = "".join('<a href="%s">%s</a>' % (n["href"], esc(n["label"])) for n in cfg["nav_links"])
footlinks += '<a href="%s">Source ↗</a>' % cfg["repo_url"]
m = cfg["meta"]
tmpl = """<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>%(title)s</title>
<meta name="description" content="%(desc)s">
<meta property="og:title" content="%(ogt)s">
<meta property="og:description" content="%(ogd)s">
<meta property="og:type" content="website">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
%(css)s
</style>
</head>
<body>
<header class="nav">
<div class="wrap nav-in">
<a class="brand" href="#top"><span class="logo">L</span> %(brand)s</a>
<nav class="nav-links">%(navlinks)s
<a class="nav-cta" href="%(repo)s">Source ↗</a>
</nav>
</div>
</header>
<a id="top"></a>
<section class="hero">
<div class="wrap hero-grid">
<div>
<span class="pill"><span class="dot"></span> %(pill)s</span>
<h1>%(title_pre)s <span class="accent">%(title_accent)s</span></h1>
<p class="lead">%(lead)s</p>
<div class="cta-row">
<a class="btn btn-primary" href="%(pcta_h)s">%(pcta_l)s</a>
<a class="btn btn-ghost" href="%(scta_h)s">%(scta_l)s</a>
</div>
</div>
<div class="code-card reveal">
<div class="code-top">
<span class="tl r"></span><span class="tl y"></span><span class="tl g"></span>
<span class="code-name">%(hero_name)s</span>
</div>
<pre data-lang="ludic">%(hero_code)s</pre>
</div>
</div>
<div class="wrap" style="margin-top:52px">
<div class="pipeline reveal">%(stages)s</div>
<p style="text-align:center;color:var(--muted);font-size:13.5px;margin:14px 0 0">%(pipenote)s</p>
</div>
</section>
<section id="features">
<div class="wrap">
<div class="sec-head reveal">
<div class="kicker">%(feat_kicker)s</div>
<h2>%(feat_title)s</h2>
<p>%(feat_intro)s</p>
</div>
<div class="feat-grid">%(feats)s</div>
</div>
</section>
<section id="showcase" style="padding-top:24px">
<div class="wrap">
<div class="sec-head reveal">
<div class="kicker">%(sc_kicker)s</div>
<h2>%(sc_title)s</h2>
<p>%(sc_intro)s</p>
</div>
<div class="tabs reveal" id="tabs"></div>
<div id="panels"></div>
<div class="showcase-note reveal"><span class="b">↳</span><span id="note"></span></div>
</div>
</section>
<section style="padding-top:24px">
<div class="wrap">
<div class="banner reveal">
<div>
<div class="kicker" style="color:var(--blue)">%(phil_kicker)s</div>
<h2>%(phil_title)s</h2>
%(phil_paras)s
</div>
<div class="stat-row">%(phil_stats)s</div>
</div>
</div>
</section>
<section id="start">
<div class="wrap">
<div class="sec-head reveal">
<div class="kicker">%(start_kicker)s</div>
<h2>%(start_title)s</h2>
<p>%(start_intro)s</p>
</div>
<div class="steps">
<div>%(steps)s</div>
<div class="term reveal">
<div class="code-top">
<span class="tl r"></span><span class="tl y"></span><span class="tl g"></span>
<span class="code-name">%(term_name)s</span>
</div>
<pre>%(term)s</pre>
</div>
</div>
</div>
</section>
<section id="editors" style="padding-top:24px">
<div class="wrap">
<div class="sec-head reveal">
<div class="kicker">%(ed_kicker)s</div>
<h2>%(ed_title)s</h2>
<p>%(ed_intro)s</p>
</div>
<div class="editors reveal">%(eds)s</div>
<p style="margin-top:22px;color:var(--muted);font-size:14.5px">%(ed_note)s</p>
</div>
</section>
<footer>
<div class="wrap foot-in">
<div>
<div class="brand" style="margin-bottom:8px"><span class="logo">L</span> %(brand)s</div>
<div class="muted">%(tagline)s</div>
</div>
<nav class="foot-links">%(footlinks)s</nav>
</div>
</footer>
<script src="ludic-highlight.js"></script>
<script>
Ludic.highlightAll();
const SAMPLES = %(samples)s;
const tabsEl = document.getElementById("tabs");
const panelsEl = document.getElementById("panels");
const noteEl = document.getElementById("note");
SAMPLES.forEach((s, idx)=>{
const t = document.createElement("button");
t.className = "tab" + (idx===0 ? " active" : "");
t.textContent = s.label;
t.onclick = ()=>select(idx);
tabsEl.appendChild(t);
const card = document.createElement("div");
card.className = "code-card panel-code" + (idx===0 ? " active" : "");
card.innerHTML =
'<div class="code-top"><span class="tl r"></span><span class="tl y"></span><span class="tl g"></span><span class="code-name">'
+ s.name + '</span></div><pre>' + Ludic.highlight(s.code) + '</pre>';
panelsEl.appendChild(card);
});
function select(idx){
[...tabsEl.children].forEach((t,i)=>t.classList.toggle("active", i===idx));
[...panelsEl.children].forEach((p,i)=>p.classList.toggle("active", i===idx));
noteEl.textContent = SAMPLES[idx].note;
}
noteEl.textContent = SAMPLES[0].note;
const io = new IntersectionObserver((entries)=>{
entries.forEach(e=>{ if(e.isIntersecting){ e.target.classList.add("in"); io.unobserve(e.target); } });
},{threshold:.12});
document.querySelectorAll(".reveal").forEach(el=>io.observe(el));
</script>
</body>
</html>
"""
tmpl = open(os.path.join(ASSETS, "index.tmpl.html")).read()
return pct(tmpl, dict(
css=css, brand=esc(cfg["brand"]), tagline=esc(cfg["tagline"]), repo=cfg["repo_url"],
title=esc(m["title"]), desc=esc(m["description"]), ogt=esc(m["og_title"]), ogd=esc(m["og_description"]),
navlinks=navlinks,
pill=esc(hero["pill"]), title_pre=esc(hero["title_pre"]), title_accent=esc(hero["title_accent"]),
title=esc(m["title"]), desc=escattr(m["description"]), ogt=esc(m["og_title"]), ogd=escattr(m["og_description"]),
navlinks=navlinks, pill=esc(hero["pill"]), title_pre=esc(hero["title_pre"]), title_accent=esc(hero["title_accent"]),
lead=hero["lead"], pcta_h=hero["primary_cta"]["href"], pcta_l=esc(hero["primary_cta"]["label"]),
scta_h=hero["secondary_cta"]["href"], scta_l=esc(hero["secondary_cta"]["label"]),
hero_name=esc(hero["snippet_name"]), hero_code=hero_code, stages=stages, pipenote=hero["pipeline_note"],
feat_kicker=esc(cfg["features"]["kicker"]), feat_title=esc(cfg["features"]["title"]),
feat_intro=cfg["features"]["intro"], feats=feats,
sc_kicker=esc(cfg["showcase"]["kicker"]), sc_title=esc(cfg["showcase"]["title"]),
sc_intro=cfg["showcase"]["intro"],
feat_kicker=esc(cfg["features"]["kicker"]), feat_title=esc(cfg["features"]["title"]), feat_intro=cfg["features"]["intro"], feats=feats,
sc_kicker=esc(cfg["showcase"]["kicker"]), sc_title=esc(cfg["showcase"]["title"]), sc_intro=cfg["showcase"]["intro"],
phil_kicker=esc(phil["kicker"]), phil_title=esc(phil["title"]), phil_paras=phil_paras, phil_stats=phil_stats,
start_kicker=esc(start["kicker"]), start_title=esc(start["title"]), start_intro=esc(start["intro"]),
steps=steps, term_name=esc(start["terminal_name"]), term=term,
ed_kicker=esc(cfg["editors"]["kicker"]), ed_title=esc(cfg["editors"]["title"]),
ed_intro=cfg["editors"]["intro"], eds=eds, ed_note=cfg["editors"]["note"],
footlinks=footlinks, samples=samples_json,
ed_kicker=esc(ed["kicker"]), ed_title=esc(ed["title"]), ed_intro=ed["intro"], eds=eds, ed_note=ed["note"],
footlinks=footlinks, samples=json.dumps(samples),
))
def pct(tmpl, mapping):
return re.sub(r"%\((\w+)\)s", lambda mm: mapping[mm.group(1)], tmpl)
# ---------------------------------------------------------------------------
def render_highlighter(symbols):
tmpl = open(os.path.join(ASSETS, "ludic-highlight.tmpl.js")).read()
@ -504,22 +401,39 @@ def main():
sections = load_sections()
palette = json.load(open(os.path.join(LANG, "colors", "palette.json")))
symbols = build_symbols(sections)
symbols, items = build_symbols(sections, palette)
cfg = json.load(open(os.path.join(SITE, "site.json")))
open(os.path.join(out, "api.html"), "w").write(render_api(sections, palette, cfg))
open(os.path.join(out, "index.html"), "w").write(render_index(cfg))
open(os.path.join(out, "ludic-highlight.js"), "w").write(render_highlighter(symbols))
open(os.path.join(out, "symbols.json"), "w").write(json.dumps(symbols, indent=2, ensure_ascii=False))
open(os.path.join(out, ".nojekyll"), "w").write("")
W = lambda name, content: open(os.path.join(out, name), "w").write(content)
# per-item pages
npages = 0
for s in sections:
if s["cat"] == "colors":
continue
for e in s["entries"]:
W(e["id"] + ".html", item_page(e, s, cfg, items)); npages += 1
# namespace overview pages
by_cat = {s["cat"]: s for s in sections}
for nsname, cat in (("Screen","screen"),("Input","input"),("Random","random"),("Map","map")):
if cat in by_cat:
W(ns_page(nsname) + ".html", ns_overview_page(nsname, by_cat[cat], cfg)); npages += 1
W("ns-color.html", color_page(palette, cfg)); npages += 1
# index + landing + assets
W("api.html", api_index(sections, cfg))
W("index.html", render_index(cfg))
W("ludic-highlight.js", render_highlighter(symbols))
W("symbols.json", json.dumps({"items": items, "highlight": symbols}, indent=2, ensure_ascii=False))
W(".nojekyll", "")
n_entries = sum(len(s["entries"]) for s in sections)
print("docs generated -> %s" % out)
print(" sections: %d symbols: %d colors: %d"
% (len(sections), n_entries, palette["count"]))
print(" highlighter tokens: %d kw / %d type / %d builtin / %d ns-method / %d annot"
% (len(symbols["keywords"]), len(symbols["types"]), len(symbols["builtins"]),
len(symbols["nsmethods"]), len(symbols["annotations"])))
print(" sections: %d symbols: %d colors: %d item pages: %d"
% (len(sections), n_entries, palette["count"], npages))
print(" highlighter: %d kw / %d type / %d phase / %d builtin / %d ns-method / %d annot / %d ns"
% (len(symbols["keywords"]), len(symbols["types"]), len(symbols["phases"]),
len(symbols["builtins"]), len(symbols["nsmethods"]), len(symbols["annotations"]),
len(symbols["namespaces"])))
if __name__ == "__main__":
main()