ludic/tools/docgen/gen.py
Orkuncakilkaya a38195128f feat(stdlib): namespaced standard library (issue #2)
Implement the bulk of the namespaced-stdlib proposal (workshopsoft/ludic#2):
156 namespace methods across Math, Text, List, Ease, Collide, World, Net,
Sys, Save, Mem, extended Screen, Color functions, extended Random, and Time.
All deterministic fixed-point; self-hosting (C-free bootstrap fixpoint holds).

Compiler (selfhost/):
- Math.*: sqrt/sin/cos/tan/atan2/asin/acos (fixed-point runtime prelude —
  bit-by-bit isqrt, 256-entry interpolated sine table, Ross atan2), plus
  hypot/dist/dist2/deg_to_rad/rad_to_deg/posmod/wrap/ping_pong/snapped/
  move_toward/smoothstep/lerp/remap/sign/floor/ceil/round.
- Text.* (complete): upper/lower/trim/repeat/pad, split/join/replace,
  and the libc-backed queries.
- List.* (complete): insert/remove_at/remove/sort plus the earlier ops.
- Ease.* (in/out/in_out/back/bounce) and Collide.* (rects/point_rect/
  circles/rect_circle).
- Phase 3: World/Net/Sys/Save namespaced over the bare builtins (byte-
  identical IR) and Mem.* (bytes/words/copy/fill/peek/poke).
- Screen.* extended (line/circle/fill_circle/triangle/fill_triangle via new
  runtime primitives; sprite/sprite_scaled aliases), Color.* functions,
  Random.* (value/int/sign), Time.* (frame/delta/elapsed/now — new
  game-loop frame counter).
- Fix a lexer bug: fixed-point literals with >4 fractional digits overflowed.

Docs & tooling:
- 129 new per-symbol doc pages; gen.py made data-driven (namespaces
  discovered from the docs, no hardcoded list); new check-impl.py enforces
  that every implemented namespace method / keyword / type / phase has a
  doc page, wired into `x test-tools`. Document the previously-undocumented
  keywords (break/continue/where/entry/new/public + and/or/not tokens).
- LSP: namespaced signature help (ns_method_sig) covering every namespace.

Tests: 12 new self-host/regression tests + a golden render for the drawing
primitives. All suites green (selfhost 21, regression 45, tools 29).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-30 00:26:19 +03:00

462 lines
23 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
"""gen.py — the Ludic documentation generator (v2).
Single source of truth:
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:
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
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, 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 or "", quote=False)
def escattr(s): return html.escape(s or "", quote=True)
def fill(tmpl, mapping):
for k, v in mapping.items():
tmpl = tmpl.replace("@@" + k + "@@", str(v))
return tmpl
# ---------------------------------------------------------------------------
# parsing
# ---------------------------------------------------------------------------
FENCE = re.compile(r"```ludic\n(.*?)\n```", re.S)
PARAM = re.compile(r"^-\s*`?(\w+)`?\s*[—-]+\s*(.*)$")
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:
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")
return meta, body
def codeify(s):
return re.sub(r"`([^`]+)`", r"<code>\1</code>", s)
def split_body(body):
examples = FENCE.findall(body)
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
def load_sections():
sections = []
for cat in sorted(os.listdir(LANG)):
cdir = os.path.join(LANG, cat)
if not os.path.isdir(cdir):
continue
smeta, sblurb = {}, ""
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 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, params = split_body(body)
meta["desc_html"] = desc
meta["examples"] = examples
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", "")))
# A namespace section is discovered, not hardcoded: it is any section
# whose symbols are `namespace-method`s, named by their `ns:` field. So
# adding a namespace is just adding its docs dir — gen.py needs no edit.
ns_name = next((e["ns"] for e in entries
if e.get("kind") == "namespace-method" and e.get("ns")), None)
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, "ns_name": ns_name})
sections.sort(key=lambda s: (s["order"], s["title"]))
return sections
# ---------------------------------------------------------------------------
# symbol model for the highlighter + search + cards
# ---------------------------------------------------------------------------
def ns_page(nsname): return "ns-" + nsname.lower()
def build_symbols(sections, palette):
sym = {"keywords": {}, "types": {}, "phases": {}, "builtins": {},
"nsmethods": {}, "annotations": {}, "namespaces": {}, "tips": {},
"cards": {}, "colors_page": "ns-color"}
items = {} # id -> full record for search
for s in sections:
for e in s["entries"]:
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] = eid
elif kind == "type": sym["types"][tok] = eid
elif kind == "phase": sym["phases"][tok] = eid
elif kind == "builtin":
sym["builtins"][tok] = {"id": eid, "params": [p["name"] for p in e["params"]]}
elif kind == "namespace-method":
sym["nsmethods"][tok] = {"id": eid, "params": [p["name"] for p in e["params"]]}
elif kind == "annotation":
sym["annotations"][tok] = eid
if tip: sym["tips"][tok] = tip
# namespaces are discovered from the ns methods present, plus Color; each
# namespace's tip falls back to its section blurb so no hardcoded list of
# namespaces is needed here when a new one is added.
ns_blurb = {s["ns_name"]: plain_text(s["blurb"]) for s in sections if s.get("ns_name")}
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(nn, ns_blurb), "page": ns_page(nn) + ".html",
"section": "Namespaces"}
return sym, items
def plain_text(s, limit=160):
"""The first sentence of a blurb, stripped of tags — for use as a one-line tip."""
t = re.sub(r"<[^>]+>", "", s or "").strip()
m = re.match(r"(.+?[.!?])(\s|$)", t)
if m: t = m.group(1)
return t if len(t) <= limit else t[:limit].rsplit(" ", 1)[0] + "…"
def ns_tip(nsname, ns_blurb):
"""A namespace's tip: an optional curated override, else its section blurb."""
return NS_TIP.get(nsname) or ns_blurb.get(nsname, "")
# Optional short overrides for a namespace's one-line tip. A namespace NOT
# listed here falls back to the first sentence of its section blurb (see
# ns_tip), so a newly added namespace needs no edit here. Color has no docs
# section of its own, so its override is required.
NS_TIP = {"Color": "The named color palette."}
# ---------------------------------------------------------------------------
# shared chrome
# ---------------------------------------------------------------------------
def nav_html(cfg, links=None, cta_href="api.html"):
out = []
for n in (links if links is not None else cfg["nav_links"]):
cls = 'class="nav-cta" ' if n.get("href") == cta_href else ""
out.append('<a %shref="%s">%s</a>' % (cls, n["href"], esc(n["label"])))
return "".join(out)
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>@@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>""", {"TITLE": esc(title), "DESC": escattr(desc), "CSS": css})
def topbar(cfg):
refnav = nav_html(cfg, links=cfg.get("ref_nav_links", cfg["nav_links"]), cta_href=None) \
+ '<a class="nav-cta" href="%s">Source ↗</a>' % cfg["repo_url"]
return ('<header class="nav"><div class="wrap nav-in">'
'<a class="brand" href="index.html"><span class="logo">L</span> %s</a>'
'<button class="nav-toggle" aria-label="Toggle menu" aria-expanded="false">☰</button>'
'<nav class="nav-links">%s</nav></div></header>') % (esc(cfg["brand"]), refnav)
# ---------------------------------------------------------------------------
# per-item page
# ---------------------------------------------------------------------------
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_page(section["ns_name"]) + ".html" if section.get("ns_name") 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) or plain_text(section["blurb"]), "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 = nav_html(cfg)
hero_code = esc(read_snippet(hero["snippet"]))
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>'
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"])
start = cfg["start"]
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"])
ed = cfg["editors"]
eds = "".join('<div class="ed"><span class="k">◆</span> %s</div>' % esc(x) for x in ed["list"])
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 = 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=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"],
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(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()
return tmpl.replace("/*__SYMBOLS__*/{}", json.dumps(symbols, ensure_ascii=False))
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", default=os.path.join(ROOT, "build", "pages"))
args = ap.parse_args()
out = args.out
os.makedirs(out, exist_ok=True)
sections = load_sections()
palette = json.load(open(os.path.join(LANG, "colors", "palette.json")))
symbols, items = build_symbols(sections, palette)
cfg = json.load(open(os.path.join(SITE, "site.json")))
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 — one per discovered namespace section
for s in sections:
if s["ns_name"]:
W(ns_page(s["ns_name"]) + ".html", ns_overview_page(s["ns_name"], s, 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 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()