From 9ed0070039613f0d7cdb57037d49d36f5378b987 Mon Sep 17 00:00:00 2001
From: Orkuncakilkaya
Date: Mon, 31 Aug 2026 02:12:32 +0300
Subject: [PATCH] feat(tooling): port the docgen site generator to Ludic (no
Python) (#41)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Follow-up to #31: the doc/lint/grammar checks moved to Ludic there; this ports
the remaining docgen piece (gen.py / check.py / palette.py) so nothing in the
documentation pipeline is Python any more.
Three new `x` subcommands, all in Ludic and compiled by Ludic:
- x docs-gen [--out DIR] the static-site generator: parses docs/language/**
front-matter + bodies (fences, Parameters:), builds the section/symbol
model, reads the asset templates, and emits every page + ns/color/api pages
+ the landing page + ludic-highlight.js + symbols.json + .nojekyll.
- x docs-check [DIR] the coverage / integrity guard (required files, a
page per inventory.json symbol, duplicate-token and one-dir-per-namespace
guards, highlighter link targets).
- x docs-palette the named-colour source of truth: the palette table
moved into tools/x/docgen.ludic, emitting emit_color.ludic (pointer, not
ptr) + palette.json.
Verified against the Python oracle: `x docs-gen` reproduces all 466 output files
BYTE-FOR-BYTE (a Ludic json.dumps/html.escape/front-matter port — ordered dicts,
indent=2 vs compact, ensure_ascii \uXXXX, codepoint-aware truncation), and
`x docs-check` matches check.py's pass/fail output. Wired into `x test` as a
gate (docs-gen -> docs-check on a fresh site; docs-palette stays byte-identical).
CI swap: ci.yml and docs.yml call the Ludic generator; docs.yml drops the
python:3.12 container and bootstraps the toolchain from the IR seed instead.
tools/docgen/{gen,check,palette}.py deleted; only assets/ + inventory.json
remain. Completes #31's criterion 3.
Co-Authored-By: Claude Opus 4.8
---
.forgejo/pull_request_template.md | 2 +-
.forgejo/workflows/ci.yml | 25 +-
.forgejo/workflows/docs.yml | 34 +-
selfhost/backend/stdlib/emit_color.ludic | 4 +-
tools/docgen/README.md | 36 +-
tools/docgen/check.py | 148 ----
tools/docgen/gen.py | 462 ----------
tools/docgen/palette.py | 298 -------
tools/x/docgen.ludic | 638 ++++++++++++++
tools/x/docgen_check.ludic | 288 ++++++
tools/x/docgen_gen.ludic | 1014 ++++++++++++++++++++++
tools/x/main.ludic | 9 +
tools/x/test.ludic | 15 +
13 files changed, 2027 insertions(+), 946 deletions(-)
delete mode 100644 tools/docgen/check.py
delete mode 100644 tools/docgen/gen.py
delete mode 100644 tools/docgen/palette.py
create mode 100644 tools/x/docgen.ludic
create mode 100644 tools/x/docgen_check.ludic
create mode 100644 tools/x/docgen_gen.ludic
diff --git a/.forgejo/pull_request_template.md b/.forgejo/pull_request_template.md
index 7202e9f3..b39680fc 100644
--- a/.forgejo/pull_request_template.md
+++ b/.forgejo/pull_request_template.md
@@ -14,7 +14,7 @@ Closes #
- [ ] `ludic-fmt` leaves the touched files unchanged (2-space, LF, UTF-8).
- [ ] New/changed stdlib symbols are documented under `docs/language/**` and
registered in `tools/docgen/inventory.json`
- (`python3 tools/docgen/check.py` passes).
+ (`bin/x docs-gen && bin/x docs-check build/pages` passes).
- [ ] Commits follow [Conventional Commits](https://www.conventionalcommits.org).
- [ ] No new C / Python / JS in tooling (Ludic only), and no generated
artifacts committed outside `build/` / `bin/`.
diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml
index 7f242aeb..b09b91ba 100644
--- a/.forgejo/workflows/ci.yml
+++ b/.forgejo/workflows/ci.yml
@@ -17,20 +17,20 @@ jobs:
# advertises `docker`, not the GitHub-ism `ubuntu-latest`.
runs-on: docker
# Reuse the runner's own base image (Debian bookworm with git + node already
- # present) and add just the two things the toolchain needs: a modern clang
- # (LLVM 16 — the IR uses opaque pointers, so clang 15+ is required) and
- # python3 for the docs/vocabulary checks. A prebuilt image with these baked
- # in is the obvious future speed-up (see issue #33's packaging work).
+ # present) and add just the one thing the toolchain needs: a modern clang
+ # (LLVM 16 — the IR uses opaque pointers, so clang 15+ is required). The docs
+ # generator and its guards are now Ludic, so the job carries no Python. A
+ # prebuilt image with clang baked in is the obvious future speed-up (see
+ # issue #33's packaging work).
container: node:20-bookworm
steps:
- - name: Install clang-16 and python3
+ - name: Install clang-16
run: |
set -eu
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
- apt-get install -y -qq --no-install-recommends clang-16 python3 git ca-certificates
+ apt-get install -y -qq --no-install-recommends clang-16 git ca-certificates
clang-16 --version | head -1
- python3 --version
- name: Check out the triggering commit
run: |
@@ -75,11 +75,12 @@ jobs:
- name: Docs cover the implementation
run: |
set -eu
- # check-impl / check-vocabulary are written in Ludic and run through
- # x — no Python. (check-vocabulary also runs in `x test-tools`.) The
- # site generator (gen.py) is still Python; its port is tracked.
+ # The whole docs toolchain is written in Ludic and runs through x —
+ # no Python anywhere. check-impl / check-vocabulary / check-docs guard
+ # the sources; docs-gen builds the site and docs-check is its coverage
+ # + integrity guard. (check-vocabulary also runs in `x test-tools`.)
bin/x check-impl
bin/x check-vocabulary
bin/x check-docs
- python3 tools/docgen/gen.py --out build/pages
- python3 tools/docgen/check.py build/pages
+ bin/x docs-gen --out build/pages
+ bin/x docs-check build/pages
diff --git a/.forgejo/workflows/docs.yml b/.forgejo/workflows/docs.yml
index f1edaec7..2a5fb4ad 100644
--- a/.forgejo/workflows/docs.yml
+++ b/.forgejo/workflows/docs.yml
@@ -10,6 +10,7 @@ on:
paths:
- 'docs/**'
- 'tools/docgen/**'
+ - 'tools/x/**'
- '.forgejo/workflows/docs.yml'
workflow_dispatch: {}
@@ -23,11 +24,20 @@ jobs:
# GitHub-ism this runner does not register, so a job requesting it sits in
# "Waiting" forever with "no online runner found matching this label".
runs-on: docker
- # Run in a Python image: the generator is pure-Python stdlib, and this image
- # already has git for the clone + publish. No node actions are used, so the
- # job never depends on the runner's base image having python installed.
- container: python:3.12
+ # The generator is now Ludic, so this builds the toolchain from its IR seed
+ # (clang assembles the seed into bin/ludicc, which compiles bin/x) exactly
+ # like the ci workflow, then runs `x docs-gen`. node:20-bookworm carries git
+ # for the clone + publish; clang-16 is the only extra the bootstrap needs.
+ container: node:20-bookworm
steps:
+ - name: Install clang-16
+ run: |
+ set -eu
+ export DEBIAN_FRONTEND=noninteractive
+ apt-get update -qq
+ apt-get install -y -qq --no-install-recommends clang-16 git ca-certificates
+ clang-16 --version | head -1
+
- name: Generate the documentation site
env:
SOURCE_REF: ${{ github.ref_name }}
@@ -36,9 +46,19 @@ jobs:
git config --global --add safe.directory '*'
git clone --depth 1 --branch "${SOURCE_REF:-main}" \
https://git.workshopsoft.io/workshopsoft/ludic.git src
- python3 --version
- python3 src/tools/docgen/gen.py --out public
- python3 src/tools/docgen/check.py public
+ cd src
+ # The toolchain is macOS-first; on this Linux runner it links against a
+ # tiny C-free IR shim supplying the Darwin stdout/stderr globals over
+ # glibc's, injected through LUDIC_CC. docs-gen is a pure CLI (no
+ # windowing), so the C-free bootstrap is all it needs.
+ export LUDIC_CC="clang-16 $(pwd)/tools/ci/linux_stdio_shim.ll"
+ export LUDIC_HOME="$(pwd)"
+ mkdir -p bin
+ clang-16 tools/ci/linux_stdio_shim.ll selfhost/ludicc.seed.ll -o bin/ludicc
+ bin/ludicc tools/x/main.ludic -o bin/x
+ bin/x docs-gen --out ../public
+ bin/x docs-check ../public
+ cd ..
echo "--- generated files ---"
ls -la public
diff --git a/selfhost/backend/stdlib/emit_color.ludic b/selfhost/backend/stdlib/emit_color.ludic
index 8c3f2b0f..4ceaa973 100644
--- a/selfhost/backend/stdlib/emit_color.ludic
+++ b/selfhost/backend/stdlib/emit_color.ludic
@@ -5,8 +5,8 @@
# no allocation, identical codegen to writing the hex by hand. Unknown names are
# a compile error (color_lookup returns -1, which emit_expr reports).
#
-# GENERATED by scratchpad/palette.py from the single source-of-truth palette.
-# Edit the palette there and regenerate; do not hand-edit this file.
+# GENERATED by `x docs-palette` from the single source-of-truth palette table
+# in tools/x/docgen.ludic. Edit the palette there and regenerate; do not hand-edit.
# ============================================================================
function color_lookup(name: pointer) -> int {
diff --git a/tools/docgen/README.md b/tools/docgen/README.md
index 11243ddd..2378b137 100644
--- a/tools/docgen/README.md
+++ b/tools/docgen/README.md
@@ -10,7 +10,7 @@ docs/
language//.md one file per symbol — keyword, type, phase,
builtin, namespace method, operator, annotation
language//_section.md section title + blurb + order
- language/colors/palette.json the 221 named colors (generated by palette.py)
+ language/colors/palette.json the 221 named colors (generated by `x docs-palette`)
site/site.json landing-page messaging (hero, features, …)
site/snippets/*.ludic the code shown on the landing page (real programs)
tools/docgen/inventory.json the authoritative symbol set the coverage guard checks
@@ -61,27 +61,31 @@ hovering any token shows a summary card from the real API data.
## Build & check
+The generator and its guards are written in Ludic and run through `x` — there is
+no Python in the pipeline. `assets/` (the CSS/HTML/JS templates) and
+`inventory.json` are the only inputs the tools here still read directly.
+
```bash
-python3 tools/docgen/gen.py --out build/pages # generate the whole site
-python3 tools/docgen/check.py build/pages # coverage + duplicate-token + link guard
-bin/x check-docs # parse every ```ludic doc fence (Ludic; no Python)
+bin/x docs-gen --out build/pages # generate the whole site
+bin/x docs-check build/pages # coverage + duplicate-token + link guard
+bin/x check-docs # parse every ```ludic doc fence
```
-`check.py` fails CI if any symbol in `inventory.json` lacks a page, if a token is
-documented on two pages, or if a highlighter link points at a missing page — so
-"every symbol is documented, autogenerated each time" is enforced. `validate.py`
-needs a built `bin/ludicc`; the deploy CI is Python-only, so run it locally or in
-a toolchain-enabled job.
-
-No third-party dependencies — Python standard library only.
+`docs-check` fails CI if any symbol in `inventory.json` lacks a page, if a token
+is documented on two pages, or if a highlighter link points at a missing page —
+so "every symbol is documented, autogenerated each time" is enforced. It needs a
+built `bin/x` (bootstrapped from the IR seed with clang alone).
## Publish
-`.forgejo/workflows/docs.yml` runs `gen.py` + `check.py` on every push to `main`
-that touches `docs/**` or `tools/docgen/**`, and publishes the result to the
-`pages` branch root. `index.html` + `.nojekyll` always stay at the root.
+`.forgejo/workflows/docs.yml` bootstraps the toolchain from the IR seed and runs
+`x docs-gen` + `x docs-check` on every push to `main` that touches `docs/**`,
+`tools/docgen/**` or `tools/x/**`, and publishes the result to the `pages` branch
+root. `index.html` + `.nojekyll` always stay at the root.
## Colors
-`palette.json` is generated by `tools/docgen/palette.py` from a single `PALETTE`
-table, which also generates `selfhost/emit_color.ludic`. Regenerate colors there.
+`palette.json` and `selfhost/backend/stdlib/emit_color.ludic` are both generated
+by `bin/x docs-palette` from a single palette table — the `pal_add(...)` rows in
+`tools/x/docgen.ludic`. Edit the table there and regenerate; do not hand-edit the
+generated files.
diff --git a/tools/docgen/check.py b/tools/docgen/check.py
deleted file mode 100644
index 687fb5ac..00000000
--- a/tools/docgen/check.py
+++ /dev/null
@@ -1,148 +0,0 @@
-#!/usr/bin/env python3
-"""check.py — coverage + integrity guard for the generated docs site.
-
-Fails (exit 1) if:
- * the pages contract is broken (index.html / api.html / .nojekyll missing);
- * any symbol in tools/docgen/inventory.json lacks a source file AND a page;
- * any per-symbol source file still carries only its one-line seed (i.e. was
- scaffolded but never written up) — so "every symbol is really documented";
- * a highlighter link target (page) does not exist in the output.
-
-Usage: python3 tools/docgen/check.py [site-dir] (default build/pages)
-"""
-import json, os, re, sys
-
-ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-LANG = os.path.join(ROOT, "docs", "language")
-DOCGEN = os.path.join(ROOT, "tools", "docgen")
-
-def parse_front(path):
- t = open(path, encoding="utf-8").read()
- meta = {}
- if t.startswith("---"):
- end = t.find("\n---", 3)
- if end != -1:
- for line in t[3:end].strip("\n").split("\n"):
- if line.strip() and ":" in line:
- k, v = line.split(":", 1); meta[k.strip()] = v.strip()
- body = t[end+4:].strip()
- else:
- body = t
- else:
- body = t
- return meta, body
-
-def main(site):
- problems, warnings = [], []
-
- # 1) pages contract
- for f in ("index.html", "api.html", ".nojekyll"):
- if not os.path.exists(os.path.join(site, f)):
- problems.append("missing required file: " + f)
-
- # 2) coverage against the authoritative inventory
- inv = json.load(open(os.path.join(DOCGEN, "inventory.json")))
- # map id -> source path
- id2src = {}
- thin = []
- for cat in sorted(os.listdir(LANG)):
- cdir = os.path.join(LANG, cat)
- if not os.path.isdir(cdir):
- continue
- for fn in os.listdir(cdir):
- if not fn.endswith(".md") or fn == "_section.md":
- continue
- meta, body = parse_front(os.path.join(cdir, fn))
- sid = meta.get("id", fn[:-3]); id2src[sid] = os.path.join(cdir, fn)
- # a "thin" file = body (minus a Parameters block and its tip line) too short
- tipline = meta.get("tip", "")
- btext = re.sub(r"(?is)parameters:.*", "", body).strip()
- btext = btext.replace(tipline, "").strip()
- if len(btext) < 40:
- thin.append(sid)
- for cat, ids in inv.items():
- for sid in ids:
- if sid not in id2src:
- problems.append("no source file for inventory symbol: %s (%s)" % (sid, cat))
- elif site and not os.path.exists(os.path.join(site, sid + ".html")):
- problems.append("no generated page for symbol: %s.html" % sid)
-
- # 2b) duplicate token → symbol conflicts (same token documented on two pages)
- tok2ids = {}
- for cat in sorted(os.listdir(LANG)):
- cdir = os.path.join(LANG, cat)
- if not os.path.isdir(cdir):
- continue
- for fn in os.listdir(cdir):
- if not fn.endswith(".md") or fn == "_section.md":
- continue
- meta, _ = parse_front(os.path.join(cdir, fn))
- for tok in meta.get("tokens", "").split():
- tok2ids.setdefault((meta.get("kind",""), tok), set()).add(meta.get("id",""))
- for (kind, tok), ids in sorted(tok2ids.items()):
- if len(ids) > 1:
- problems.append("token %r (%s) documented on multiple pages: %s" % (tok, kind, ", ".join(sorted(ids))))
-
- # 2c) one docs directory per namespace / section. Two dirs covering the same
- # runtime namespace (e.g. a `network`/`networking` or `date`/`datetime`
- # split) invite drift — a symbol documented in one and not the other — so
- # fail if any `ns:` is declared from more than one directory, or if two
- # sections share a section id or (case-folded) title.
- ns2dirs, id2dirs, title2dirs = {}, {}, {}
- for cat in sorted(os.listdir(LANG)):
- cdir = os.path.join(LANG, cat)
- if not os.path.isdir(cdir):
- continue
- sp = os.path.join(cdir, "_section.md")
- if os.path.exists(sp):
- smeta, _ = parse_front(sp)
- id2dirs.setdefault(smeta.get("id", cat), set()).add(cat)
- title2dirs.setdefault(smeta.get("title", "").strip().lower(), set()).add(cat)
- for fn in os.listdir(cdir):
- if not fn.endswith(".md") or fn == "_section.md":
- continue
- meta, _ = parse_front(os.path.join(cdir, fn))
- ns = meta.get("ns", "")
- if ns:
- ns2dirs.setdefault(ns, set()).add(cat)
- for ns, dirs in sorted(ns2dirs.items()):
- if len(dirs) > 1:
- problems.append("namespace %r documented from multiple dirs: %s" % (ns, ", ".join(sorted(dirs))))
- for sid, dirs in sorted(id2dirs.items()):
- if len(dirs) > 1:
- problems.append("section id %r declared by multiple dirs: %s" % (sid, ", ".join(sorted(dirs))))
- for title, dirs in sorted(title2dirs.items()):
- if title and len(dirs) > 1:
- problems.append("section title %r shared by multiple dirs: %s" % (title, ", ".join(sorted(dirs))))
-
- # 3) thin (un-expanded) symbols — warn, not fail (lets infra land before prose)
- if thin:
- warnings.append("%d symbols still have only seed text: %s%s"
- % (len(thin), ", ".join(sorted(thin)[:12]), " …" if len(thin) > 12 else ""))
-
- # 4) highlighter targets exist
- sj = os.path.join(site, "symbols.json")
- if os.path.exists(sj):
- h = json.load(open(sj))["highlight"]
- targets = set()
- for grp in ("keywords", "types", "phases", "annotations"):
- targets |= set(h.get(grp, {}).values())
- for grp in ("builtins", "nsmethods"):
- targets |= set(v["id"] for v in h.get(grp, {}).values())
- targets |= set(h.get("namespaces", {}).values())
- for tgt in sorted(targets):
- if not os.path.exists(os.path.join(site, tgt + ".html")):
- problems.append("highlighter links to %s.html but it was not generated" % tgt)
-
- for w in warnings:
- print(" warning:", w)
- if problems:
- print("docs check FAILED:")
- for p in problems:
- print(" -", p)
- return 1
- print("docs check OK: %s (%d symbols documented)" % (site, len(id2src)))
- return 0
-
-if __name__ == "__main__":
- sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, "build", "pages")))
diff --git a/tools/docgen/gen.py b/tools/docgen/gen.py
deleted file mode 100644
index eef8638b..00000000
--- a/tools/docgen/gen.py
+++ /dev/null
@@ -1,462 +0,0 @@
-#!/usr/bin/env python3
-"""gen.py — the Ludic documentation generator (v2).
-
-Single source of truth:
- docs/language//.md one file per symbol (front-matter + body)
- docs/language//_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
- .html ONE page per symbol (kw-handler.html, screen-clear.html…)
- ns-.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"\1", 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 "
".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('%s' % (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("""
-
-
' % (p["name"], esc(p["name"]), p["desc"])
- for p in e["params"])
- return '
Parameters
%s
' % rows
-
-def render_examples(e):
- if not e["examples"]:
- return ""
- blocks = "".join('
%s
' % esc(code) for code in e["examples"])
- return '
Example
%s
' % blocks
-
-def render_related(e, items):
- rel = [r for r in e["related_list"] if r in items]
- if not rel:
- return ""
- links = "".join('%s' % (r, esc(items[r]["name"])) for r in rel)
- return '