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(""" - - - - -@@TITLE@@ - - - - - -""", {"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) \ - + 'Source ↗' % cfg["repo_url"] - return ('

') % (esc(cfg["brand"]), refnav) - -# --------------------------------------------------------------------------- -# per-item page -# --------------------------------------------------------------------------- -def render_params(e): - if not e["params"]: - return "" - rows = "".join( - '
%s' - '%s
' % (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 '' % links - -def item_page(e, section, cfg, items): - kindlabel = {"namespace-method": "method", "builtin": "function"}.get(e.get("kind",""), e.get("kind","")) - body = fill("""@@TOPBAR@@ -
-
API Reference › @@SECTION@@ › @@NAME@@
-
- @@KINDLABEL@@ -

@@NAME@@

-
- @@SIG@@ -

@@DESC@@

- @@PARAMS@@ - @@EXAMPLES@@ - @@RELATED@@ - ← All symbols -
- - -""", { - "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\n" + body - -# --------------------------------------------------------------------------- -# namespace overview pages -# --------------------------------------------------------------------------- -def ns_overview_page(nsname, section, cfg): - rows = "" - for e in section["entries"]: - rows += ('%s' - '%s') % (e["id"], esc(e.get("sig","")), esc(e.get("tip",""))) - body = fill("""@@TOPBAR@@ -
-
API Reference › @@NAME@@
-
namespace

@@NAME@@

-

@@BLURB@@

-
@@ROWS@@
- ← All symbols -
- - -""", {"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\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 += ('
' - 'Color.%s#%s
' - ) % (aid, h, esc(nm), h) - groups += '

%s

%s
' % (esc(grp["name"]), sw) - body = fill("""@@TOPBAR@@ -
-
API Reference › Color
-
namespace

Color

-

@@BLURB@@ Every Color.Name lowers to a plain 0xRRGGBB integer at compile time — no runtime cost. @@COUNT@@ names are built in.

- @@GROUPS@@ - ← All symbols -
- - -""", {"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\n" + body - -# --------------------------------------------------------------------------- -# api index with fuzzy search -# --------------------------------------------------------------------------- -def api_index(sections, cfg): - cards = "" - for s in sections: - if s["cat"] == "colors": - cards += ('

%s

' - '

%s

' - ) % (s["id"], s["id"], esc(s["title"]), s["blurb"]) - continue - rows = "" - for e in s["entries"]: - rows += ('%s%s' - ) % (e["id"], escattr(e.get("name","")), escattr(e.get("tip","")), - esc(e.get("name","")), esc(e.get("tip",""))) - cards += ('

%s

' - '

%s

%s
' - ) % (s["id"], s["id"], esc(s["title"]), s["blurb"], rows) - body = fill("""@@TOPBAR@@ -
-
-
Reference
-

API Reference

-

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.

- - -
- @@CARDS@@ -
- - -""", {"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\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 += '%s' % (hl, esc(st)) - if st != hero["pipeline"][-1]: stages += '→' - feats = "".join('
%s

%s

%s

' - % (c["icon"], c["title"], c["html"]) for c in cfg["features"]["cards"]) - phil = cfg["philosophy"] - phil_paras = "".join("

%s

" % p for p in phil["paras"]) - phil_stats = "".join('
%s
%s
' - % (s["big"], esc(s["lbl"])) for s in phil["stats"]) - start = cfg["start"] - steps = "".join('
%d

%s

%s

' - % (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 += '# %s\n' % esc(t["comment"]) - elif "cmd" in t: term += '$ %s\n' % esc(t["cmd"]) - elif "out" in t: term += '%s\n' % esc(t["out"]) - ed = cfg["editors"] - eds = "".join('
◆ %s
' % 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('%s' % (n["href"], esc(n["label"])) for n in cfg["nav_links"]) - footlinks += 'Source ↗' % 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() diff --git a/tools/docgen/palette.py b/tools/docgen/palette.py deleted file mode 100644 index 5e4b73e3..00000000 --- a/tools/docgen/palette.py +++ /dev/null @@ -1,298 +0,0 @@ -#!/usr/bin/env python3 -# Single source of truth for Ludic's named color palette. -# Emits: selfhost/backend/stdlib/emit_color.ludic (compiler lookup) and palette.json (docs). -import json, sys, os - -# (Name, 0xRRGGBB, group). Names are PascalCase, unique. >200 entries. -PALETTE = [ - # ---- Whites & off-whites ---- - ("White", 0xFFFFFF, "Whites"), - ("Snow", 0xFFFAFA, "Whites"), - ("Ivory", 0xFFFFF0, "Whites"), - ("EggShellWhite", 0xF0EAD6, "Whites"), - ("FloralWhite", 0xFFFAF0, "Whites"), - ("SeaShell", 0xFFF5EE, "Whites"), - ("Linen", 0xFAF0E6, "Whites"), - ("AntiqueWhite", 0xFAEBD7, "Whites"), - ("OldLace", 0xFDF5E6, "Whites"), - ("Beige", 0xF5F5DC, "Whites"), - ("Cream", 0xFFFDD0, "Whites"), - ("Honeydew", 0xF0FFF0, "Whites"), - ("MintCream", 0xF5FFFA, "Whites"), - ("Azure", 0xF0FFFF, "Whites"), - ("AliceBlue", 0xF0F8FF, "Whites"), - ("GhostWhite", 0xF8F8FF, "Whites"), - ("WhiteSmoke", 0xF5F5F5, "Whites"), - ("Lavender", 0xE6E6FA, "Whites"), - ("Bone", 0xE3DAC9, "Whites"), - ("Parchment", 0xF1E9D2, "Whites"), - - # ---- Grays & neutrals ---- - ("Gainsboro", 0xDCDCDC, "Grays"), - ("LightGray", 0xD3D3D3, "Grays"), - ("Silver", 0xC0C0C0, "Grays"), - ("Ash", 0xB2BEB5, "Grays"), - ("DarkGray", 0xA9A9A9, "Grays"), - ("Gray", 0x808080, "Grays"), - ("DimGray", 0x696969, "Grays"), - ("Nickel", 0x727472, "Grays"), - ("Slate", 0x708090, "Grays"), - ("SlateGray", 0x708090, "Grays"), - ("LightSlateGray", 0x778899, "Grays"), - ("Gunmetal", 0x2A3439, "Grays"), - ("Charcoal", 0x36454F, "Grays"), - ("Graphite", 0x1C1C1C, "Grays"), - ("Onyx", 0x353839, "Grays"), - ("Jet", 0x343434, "Grays"), - ("Black", 0x000000, "Grays"), - ("EerieBlack", 0x1B1B1B, "Grays"), - ("RaisinBlack", 0x242124, "Grays"), - ("Ebony", 0x555D50, "Grays"), - - # ---- Reds ---- - ("Red", 0xFF0000, "Reds"), - ("Crimson", 0xDC143C, "Reds"), - ("Scarlet", 0xFF2400, "Reds"), - ("Vermilion", 0xE34234, "Reds"), - ("FireBrick", 0xB22222, "Reds"), - ("Cinnabar", 0xE44D2E, "Reds"), - ("DarkRed", 0x8B0000, "Reds"), - ("Maroon", 0x800000, "Reds"), - ("Ruby", 0xE0115F, "Reds"), - ("Cardinal", 0xC41E3A, "Reds"), - ("IndianRed", 0xCD5C5C, "Reds"), - ("Rust", 0xB7410E, "Reds"), - ("Sangria", 0x92000A, "Reds"), - ("Redwood", 0xA45A52, "Reds"), - ("Cerise", 0xDE3163, "Reds"), - ("Amaranth", 0xE52B50, "Reds"), - ("Carmine", 0x960018, "Reds"), - ("Chestnut", 0x954535, "Reds"), - ("Brick", 0xCB4154, "Reds"), - ("TerraCotta", 0xE2725B, "Reds"), - - # ---- Pinks ---- - ("Pink", 0xFFC0CB, "Pinks"), - ("LightPink", 0xFFB6C1, "Pinks"), - ("HotPink", 0xFF69B4, "Pinks"), - ("DeepPink", 0xFF1493, "Pinks"), - ("PaleVioletRed", 0xDB7093, "Pinks"), - ("Rose", 0xFF007F, "Pinks"), - ("Blush", 0xDE5D83, "Pinks"), - ("Salmon", 0xFA8072, "Pinks"), - ("LightSalmon", 0xFFA07A, "Pinks"), - ("DarkSalmon", 0xE9967A, "Pinks"), - ("Coral", 0xFF7F50, "Pinks"), - ("Watermelon", 0xFC6C85, "Pinks"), - ("Flamingo", 0xFC8EAC, "Pinks"), - ("Bubblegum", 0xFFC1CC, "Pinks"), - ("Fuchsia", 0xFF00FF, "Pinks"), - ("Magenta", 0xFF00FF, "Pinks"), - ("Mauve", 0xE0B0FF, "Pinks"), - ("Puce", 0xCC8899, "Pinks"), - ("Thistle", 0xD8BFD8, "Pinks"), - ("Orchid", 0xDA70D6, "Pinks"), - - # ---- Oranges ---- - ("Orange", 0xFFA500, "Oranges"), - ("DarkOrange", 0xFF8C00, "Oranges"), - ("Tangerine", 0xF28500, "Oranges"), - ("Pumpkin", 0xFF7518, "Oranges"), - ("Apricot", 0xFBCEB1, "Oranges"), - ("Peach", 0xFFE5B4, "Oranges"), - ("Cantaloupe", 0xFFA62B, "Oranges"), - ("Amber", 0xFFBF00, "Oranges"), - ("Bronze", 0xCD7F32, "Oranges"), - ("Copper", 0xB87333, "Oranges"), - ("Marigold", 0xEAA221, "Oranges"), - ("Carrot", 0xED9121, "Oranges"), - ("Persimmon", 0xEC5800, "Oranges"), - ("Papaya", 0xFF9E2C, "Oranges"), - ("Sunset", 0xFAD6A5, "Oranges"), - - # ---- Yellows ---- - ("Yellow", 0xFFFF00, "Yellows"), - ("LightYellow", 0xFFFFE0, "Yellows"), - ("Gold", 0xFFD700, "Yellows"), - ("Goldenrod", 0xDAA520, "Yellows"), - ("Lemon", 0xFFF700, "Yellows"), - ("Canary", 0xFFEF00, "Yellows"), - ("Mustard", 0xFFDB58, "Yellows"), - ("Flax", 0xEEDC82, "Yellows"), - ("Wheat", 0xF5DEB3, "Yellows"), - ("Corn", 0xFBEC5D, "Yellows"), - ("Dandelion", 0xF0E130, "Yellows"), - ("Saffron", 0xF4C430, "Yellows"), - ("Khaki", 0xF0E68C, "Yellows"), - ("DarkKhaki", 0xBDB76B, "Yellows"), - ("Straw", 0xE4D96F, "Yellows"), - - # ---- Browns ---- - ("Brown", 0x8B4513, "Browns"), - ("SaddleBrown", 0x8B4513, "Browns"), - ("Sienna", 0xA0522D, "Browns"), - ("Chocolate", 0xD2691E, "Browns"), - ("Peru", 0xCD853F, "Browns"), - ("Tan", 0xD2B48C, "Browns"), - ("BurlyWood", 0xDEB887, "Browns"), - ("Sand", 0xC2B280, "Browns"), - ("Coffee", 0x6F4E37, "Browns"), - ("Espresso", 0x4B3621, "Browns"), - ("Mahogany", 0xC04000, "Browns"), - ("Walnut", 0x773F1A, "Browns"), - ("Umber", 0x635147, "Browns"), - ("Sepia", 0x704214, "Browns"), - ("Taupe", 0x483C32, "Browns"), - ("Fawn", 0xE5AA70, "Browns"), - ("Caramel", 0xC68E17, "Browns"), - ("Cocoa", 0xD2691E, "Browns"), - ("Hazel", 0x8E7618, "Browns"), - ("Wenge", 0x645452, "Browns"), - - # ---- Greens ---- - ("Green", 0x008000, "Greens"), - ("Lime", 0x00FF00, "Greens"), - ("LimeGreen", 0x32CD32, "Greens"), - ("LawnGreen", 0x7CFC00, "Greens"), - ("Chartreuse", 0x7FFF00, "Greens"), - ("GreenYellow", 0xADFF2F, "Greens"), - ("SpringGreen", 0x00FF7F, "Greens"), - ("MintGreen", 0x98FF98, "Greens"), - ("SeaGreen", 0x2E8B57, "Greens"), - ("MediumSeaGreen", 0x3CB371, "Greens"), - ("ForestGreen", 0x228B22, "Greens"), - ("DarkGreen", 0x006400, "Greens"), - ("OliveDrab", 0x6B8E23, "Greens"), - ("Olive", 0x808000, "Greens"), - ("Moss", 0x8A9A5B, "Greens"), - ("Fern", 0x4F7942, "Greens"), - ("Emerald", 0x50C878, "Greens"), - ("Jade", 0x00A86B, "Greens"), - ("Malachite", 0x0BDA51, "Greens"), - ("Shamrock", 0x009E60, "Greens"), - ("Pistachio", 0x93C572, "Greens"), - ("Avocado", 0x568203, "Greens"), - ("Pine", 0x01796F, "Greens"), - ("Sage", 0x9CAF88, "Greens"), - ("Kelly", 0x4CBB17, "Greens"), - ("Hunter", 0x355E3B, "Greens"), - ("Basil", 0x579229, "Greens"), - ("Clover", 0x2E8B57, "Greens"), - ("Juniper", 0x6D9A79, "Greens"), - ("Neon", 0x39FF14, "Greens"), - - # ---- Cyans / teals ---- - ("Cyan", 0x00FFFF, "Cyans"), - ("Aqua", 0x00FFFF, "Cyans"), - ("LightCyan", 0xE0FFFF, "Cyans"), - ("PaleTurquoise", 0xAFEEEE, "Cyans"), - ("Aquamarine", 0x7FFFD4, "Cyans"), - ("Turquoise", 0x40E0D0, "Cyans"), - ("MediumTurquoise",0x48D1CC, "Cyans"), - ("DarkTurquoise", 0x00CED1, "Cyans"), - ("Teal", 0x008080, "Cyans"), - ("DarkCyan", 0x008B8B, "Cyans"), - ("CadetBlue", 0x5F9EA0, "Cyans"), - ("Lagoon", 0x018E8E, "Cyans"), - ("Seafoam", 0x93E9BE, "Cyans"), - ("Cerulean", 0x007BA7, "Cyans"), - ("SkyBlueLight", 0x80DAEB, "Cyans"), - ("Robin", 0x00CCCC, "Cyans"), - ("Verdigris", 0x43B3AE, "Cyans"), - ("Celadon", 0xACE1AF, "Cyans"), - - # ---- Blues ---- - ("Blue", 0x0000FF, "Blues"), - ("LightBlue", 0xADD8E6, "Blues"), - ("PowderBlue", 0xB0E0E6, "Blues"), - ("SkyBlue", 0x87CEEB, "Blues"), - ("LightSkyBlue", 0x87CEFA, "Blues"), - ("DeepSkyBlue", 0x00BFFF, "Blues"), - ("DodgerBlue", 0x1E90FF, "Blues"), - ("CornflowerBlue", 0x6495ED, "Blues"), - ("SteelBlue", 0x4682B4, "Blues"), - ("RoyalBlue", 0x4169E1, "Blues"), - ("MediumBlue", 0x0000CD, "Blues"), - ("DarkBlue", 0x00008B, "Blues"), - ("Navy", 0x000080, "Blues"), - ("MidnightBlue", 0x191970, "Blues"), - ("Cobalt", 0x0047AB, "Blues"), - ("Sapphire", 0x0F52BA, "Blues"), - ("Denim", 0x1560BD, "Blues"), - ("Indigo", 0x4B0082, "Blues"), - ("Prussian", 0x003153, "Blues"), - ("Ultramarine", 0x3F00FF, "Blues"), - ("Periwinkle", 0xCCCCFF, "Blues"), - ("Iris", 0x5A4FCF, "Blues"), - ("Glaucous", 0x6082B6, "Blues"), - ("Zaffre", 0x0014A8, "Blues"), - ("Berry", 0x2E2D88, "Blues"), - - # ---- Purples ---- - ("Purple", 0x800080, "Purples"), - ("Violet", 0xEE82EE, "Purples"), - ("DarkViolet", 0x9400D3, "Purples"), - ("BlueViolet", 0x8A2BE2, "Purples"), - ("MediumPurple", 0x9370DB, "Purples"), - ("Amethyst", 0x9966CC, "Purples"), - ("Plum", 0x8E4585, "Purples"), - ("Eggplant", 0x614051, "Purples"), - ("Grape", 0x6F2DA8, "Purples"), - ("Wine", 0x722F37, "Purples"), - ("Mulberry", 0xC54B8C, "Purples"), - ("Lilac", 0xC8A2C8, "Purples"), - ("Wisteria", 0xC9A0DC, "Purples"), - ("Heliotrope", 0xDF73FF, "Purples"), - ("Byzantium", 0x702963, "Purples"), - ("Tyrian", 0x66023C, "Purples"), - ("RebeccaPurple", 0x663399, "Purples"), - ("Orchid2", 0xAF69EF, "Purples"), -] - -def guard_check(): - names = [p[0] for p in PALETTE] - dup = set(n for n in names if names.count(n) > 1) - if dup: - print("DUPLICATE NAMES:", dup, file=sys.stderr); sys.exit(1) - return names - -def emit_ludic(path): - lines = [] - lines.append("# ============================================================================") - lines.append("# emit_color.ludic — the named-color palette, resolved at compile time.") - lines.append("#") - lines.append("# `Color.Name` in a game lowers to a plain 0xRRGGBB int here: no runtime cost,") - lines.append("# no allocation, identical codegen to writing the hex by hand. Unknown names are") - lines.append("# a compile error (color_lookup returns -1, which emit_expr reports).") - lines.append("#") - lines.append("# GENERATED by scratchpad/palette.py from the single source-of-truth palette.") - lines.append("# Edit the palette there and regenerate; do not hand-edit this file.") - lines.append("# ============================================================================") - lines.append("") - lines.append("function color_lookup(name: pointer) -> int {") - for (nm, hexv, grp) in PALETTE: - lines.append(f' if (name == "{nm}") {{ return 0x{hexv:06X} }}') - lines.append(" return -1") - lines.append("}") - lines.append("") - with open(path, "w") as f: - f.write("\n".join(lines) + "\n") - -def emit_json(path): - groups = {} - order = [] - for (nm, hexv, grp) in PALETTE: - if grp not in groups: - groups[grp] = []; order.append(grp) - groups[grp].append({"name": nm, "hex": f"{hexv:06X}"}) - out = {"count": len(PALETTE), "groups": [{"name": g, "colors": groups[g]} for g in order]} - with open(path, "w") as f: - json.dump(out, f, indent=2) - -if __name__ == "__main__": - names = guard_check() - root = os.path.dirname(os.path.abspath(__file__)) - repo = "/Users/orkuncakilkaya/workspace/gpp" - emit_ludic(os.path.join(repo, "selfhost", "backend", "stdlib", "emit_color.ludic")) - emit_json(os.path.join(root, "palette.json")) - print(f"OK {len(PALETTE)} colors ({len(set(names))} unique names)") diff --git a/tools/x/docgen.ludic b/tools/x/docgen.ludic new file mode 100644 index 00000000..f53b2063 --- /dev/null +++ b/tools/x/docgen.ludic @@ -0,0 +1,638 @@ +# docgen.ludic — the Ludic documentation-site generator and its guards, ported +# off Python (gen.py / check.py / palette.py). Three x subcommands: +# +# x docs-palette emit selfhost/backend/stdlib/emit_color.ludic + +# docs/language/colors/palette.json from the palette +# table below (the named-colour source of truth). +# x docs-gen [--out DIR] parse docs/language/** + docs/site/*, read the asset +# templates, emit every page + symbols.json + the +# highlighter + .nojekyll into DIR (default build/pages). +# x docs-check [DIR] the coverage/integrity guard over a generated site. +# +# String work is over NUL-terminated byte buffers; helpers here mirror gen.py's +# html.escape / json.dumps / front-matter parsing closely enough to reproduce the +# Python generator's output byte-for-byte (verified against the oracle in x test). + +# ---- a growable byte buffer (avoids O(n^2) string building) ------------------ +property Sb { data: pointer = null, len: int = 0, cap: int = 0 } + +function sb_new() -> Sb { + let s = new Sb + s.cap = 256 + s.data = bytes(256) + s.len = 0 + return s +} +function sb_grow(s: Sb, need: int) -> void { + if s.len + need + 1 <= s.cap { return } + var nc = s.cap * 2 + while nc < s.len + need + 1 { nc = nc * 2 } + let nd = bytes(nc) + var i = 0 + while i < s.len { nd[i] = s.data[i]; i = i + 1 } + s.data = nd + s.cap = nc +} +function sb_puts(s: Sb, t: pointer) -> void { + let n = slen(t) + sb_grow(s, n) + var i = 0 + while i < n { s.data[s.len] = t[i]; s.len = s.len + 1; i = i + 1 } +} +function sb_putc(s: Sb, c: int) -> void { + sb_grow(s, 1) + s.data[s.len] = c + s.len = s.len + 1 +} +function sb_str(s: Sb) -> pointer { + s.data[s.len] = 0 + return s.data +} + +# ---- HTML escaping (html.escape) -------------------------------------------- +# quote=false escapes & < > ; quote=true also escapes " and ' (Python entities: +# & < > " '). Two-pass so a big code block stays O(n). +function esc_q(s: pointer, quote: bool) -> pointer { + if s == null { return "" } + let n = slen(s) + let b = sb_new() + var i = 0 + while i < n { + let c = s[i] + if c == 38 { sb_puts(b, "&") } + else if c == 60 { sb_puts(b, "<") } + else if c == 62 { sb_puts(b, ">") } + else if quote and c == 34 { sb_puts(b, """) } + else if quote and c == 39 { sb_puts(b, "'") } + else { sb_putc(b, c) } + i = i + 1 + } + return sb_str(b) +} +function hesc(s: pointer) -> pointer { return esc_q(s, false) } +function hescattr(s: pointer) -> pointer { return esc_q(s, true) } + +# ---- generic byte/character predicates -------------------------------------- +# Python \s = [ \t\n\r\f\v] +function is_space_all(c: int) -> bool { + return c == 32 or c == 9 or c == 10 or c == 13 or c == 12 or c == 11 +} +# Python \w = [A-Za-z0-9_] +function is_word(c: int) -> bool { + return (c >= 48 and c <= 57) or (c >= 65 and c <= 90) or (c >= 97 and c <= 122) or c == 95 +} +# strip only leading '\n' (Python str.lstrip("\n")) +function lstrip_nl(s: pointer) -> pointer { + var i = 0 + while s[i] == 10 { i = i + 1 } + return sslice(s, i, slen(s)) +} +# lowercase ASCII A-Z +function lower_ascii(s: pointer) -> pointer { + let n = slen(s) + let b = bytes(n + 1) + var i = 0 + while i < n { + var c = s[i] + if c >= 65 and c <= 90 { c = c + 32 } + b[i] = c + i = i + 1 + } + b[n] = 0 + return b +} +# Python str.title(): capitalise the first letter of each alpha run, lower the rest +function title_case(s: pointer) -> pointer { + let n = slen(s) + let b = bytes(n + 1) + var i = 0 + var prev_alpha = false + while i < n { + var c = s[i] + let al = (c >= 65 and c <= 90) or (c >= 97 and c <= 122) + if al { + if prev_alpha { if c >= 65 and c <= 90 { c = c + 32 } } + else { if c >= 97 and c <= 122 { c = c - 32 } } + } + b[i] = c + prev_alpha = al + i = i + 1 + } + b[n] = 0 + return b +} +# lexicographic byte compare: <0, 0, >0 +function str_cmp(a: pointer, b: pointer) -> int { + var i = 0 + while a[i] != 0 and b[i] != 0 { + let ca = a[i] & 255 + let cb = b[i] & 255 + if ca != cb { return ca - cb } + i = i + 1 + } + return (a[i] & 255) - (b[i] & 255) +} +function str_gt(a: pointer, b: pointer) -> bool { return str_cmp(a, b) > 0 } +# does s end with suffix suf? +function s_ends(s: pointer, suf: pointer) -> bool { + let ls = slen(s) + let lf = slen(suf) + if lf > ls { return false } + return s_starts_at(s, ls - lf, suf) +} +# split on runs of ASCII whitespace (Python str.split() with no arg) +function split_ws(s: pointer) -> []pointer { + let out = new []pointer + let n = slen(s) + var i = 0 + while i < n { + while i < n and is_space_all(s[i]) { i = i + 1 } + var e = i + while e < n and not is_space_all(s[e]) { e = e + 1 } + if e > i { push(out, sslice(s, i, e)) } + i = e + } + return out +} +# stable insertion sort of a []pointer, ascending byte order (Python sorted()) +function strs_sort(a: []pointer) -> void { + var i = 1 + while i < len(a) { + let cur = a[i] + var j = i - 1 + while j >= 0 and str_gt(a[j], cur) { a[j + 1] = a[j]; j = j - 1 } + a[j + 1] = cur + i = i + 1 + } +} + +# `` backtick spans: `x` -> x (inner text NOT escaped). +# Mirrors codeify's re.sub(r"`([^`]+)`", ...): needs >=1 non-backtick inside. +function codeify(s: pointer) -> pointer { + let n = slen(s) + let b = sb_new() + var i = 0 + while i < n { + if s[i] == 96 { + var j = i + 1 + while j < n and s[j] != 96 { j = j + 1 } + if j < n and j > i + 1 { + sb_puts(b, "") + var k = i + 1 + while k < j { sb_putc(b, s[k]); k = k + 1 } + sb_puts(b, "") + i = j + 1 + } else { + sb_putc(b, 96) + i = i + 1 + } + } else { + sb_putc(b, s[i]) + i = i + 1 + } + } + return sb_str(b) +} + +# ---- a Python-json.dumps-compatible JSON writer over the JVal tree ---------- +function jvs(s: pointer) -> JVal { let v = jval_new(JV_STR); v.s = s; return v } +function jvi(n: int) -> JVal { let v = jval_new(JV_NUM); v.num = n; return v } +function jvo() -> JVal { return jval_new(JV_OBJ) } +function jva() -> JVal { return jval_new(JV_ARR) } +# like Python dict assignment: update in place at an existing key (keeping its +# first-insertion position, taking the new value), else append. Matters where a +# token collides across kinds (e.g. `exit`/`string`/`fixed`) in sym["tips"]. +function jvput(o: JVal, k: pointer, v: JVal) -> void { + var i = 0 + while i < len(o.keys) { if o.keys[i] == k { o.kids[i] = v; return }; i = i + 1 } + push(o.keys, k) + push(o.kids, v) +} +function jvpush(a: JVal, v: JVal) -> void { push(a.kids, v) } + +function hex_lower(n: int) -> int { + if n < 10 { return 48 + n } + return 87 + n +} +function jemit_u(b: Sb, cp: int) -> void { + sb_putc(b, 92); sb_putc(b, 117) # \u + sb_putc(b, hex_lower((cp >> 12) & 15)) + sb_putc(b, hex_lower((cp >> 8) & 15)) + sb_putc(b, hex_lower((cp >> 4) & 15)) + sb_putc(b, hex_lower(cp & 15)) +} +# emit the JSON-escaped content of s (no surrounding quotes). ensure_ascii: emit +# non-ASCII as \uXXXX (UTF-8 decoded, surrogate pairs for astral); else raw bytes. +function json_escape_into(b: Sb, s: pointer, ensure_ascii: bool) -> void { + let n = slen(s) + var i = 0 + while i < n { + let c = s[i] & 255 + if c == 34 { sb_putc(b, 92); sb_putc(b, 34); i = i + 1 } + else if c == 92 { sb_putc(b, 92); sb_putc(b, 92); i = i + 1 } + else if c == 8 { sb_putc(b, 92); sb_putc(b, 98); i = i + 1 } + else if c == 9 { sb_putc(b, 92); sb_putc(b, 116); i = i + 1 } + else if c == 10 { sb_putc(b, 92); sb_putc(b, 110); i = i + 1 } + else if c == 12 { sb_putc(b, 92); sb_putc(b, 102); i = i + 1 } + else if c == 13 { sb_putc(b, 92); sb_putc(b, 114); i = i + 1 } + else if c < 32 { + sb_putc(b, 92); sb_putc(b, 117); sb_putc(b, 48); sb_putc(b, 48) + sb_putc(b, hex_lower((c >> 4) & 15)); sb_putc(b, hex_lower(c & 15)) + i = i + 1 + } + else if c < 128 { sb_putc(b, c); i = i + 1 } + else if not ensure_ascii { sb_putc(b, c); i = i + 1 } + else { + var cp = 0 + var nb = 1 + if c < 224 { cp = c - 192; nb = 2 } + else if c < 240 { cp = c - 224; nb = 3 } + else { cp = c - 240; nb = 4 } + var k = 1 + while k < nb and i + k < n { cp = cp * 64 + ((s[i + k] & 255) - 128); k = k + 1 } + i = i + nb + if cp <= 65535 { jemit_u(b, cp) } + else { + let c2 = cp - 65536 + jemit_u(b, 55296 + (c2 >> 10)) + jemit_u(b, 56320 + (c2 & 1023)) + } + } + } +} +function json_indent(b: Sb, depth: int) -> void { + var i = 0 + while i < depth { sb_putc(b, 32); sb_putc(b, 32); i = i + 1 } +} +# pretty=true reproduces json.dumps(indent=2); pretty=false the compact default +# (item sep ", ", key sep ": "). ensure_ascii toggles \uXXXX for non-ASCII. +function jwrite(b: Sb, v: JVal, pretty: bool, depth: int, ea: bool) -> void { + if v.t == JV_NULL { sb_puts(b, "null"); return } + if v.t == JV_BOOL { + if v.b == 1 { sb_puts(b, "true") } else { sb_puts(b, "false") } + return + } + if v.t == JV_NUM { sb_puts(b, string(v.num)); return } + if v.t == JV_STR { sb_putc(b, 34); json_escape_into(b, v.s, ea); sb_putc(b, 34); return } + if v.t == JV_ARR { + let m = len(v.kids) + if m == 0 { sb_puts(b, "[]"); return } + sb_putc(b, 91) + var i = 0 + while i < m { + if pretty { sb_putc(b, 10); json_indent(b, depth + 1) } + jwrite(b, v.kids[i], pretty, depth + 1, ea) + if i < m - 1 { sb_putc(b, 44); if not pretty { sb_putc(b, 32) } } + i = i + 1 + } + if pretty { sb_putc(b, 10); json_indent(b, depth) } + sb_putc(b, 93) + return + } + let m = len(v.keys) + if m == 0 { sb_puts(b, "{}"); return } + sb_putc(b, 123) + var i = 0 + while i < m { + if pretty { sb_putc(b, 10); json_indent(b, depth + 1) } + sb_putc(b, 34); json_escape_into(b, v.keys[i], ea); sb_putc(b, 34) + sb_putc(b, 58); sb_putc(b, 32) + jwrite(b, v.kids[i], pretty, depth + 1, ea) + if i < m - 1 { sb_putc(b, 44); if not pretty { sb_putc(b, 32) } } + i = i + 1 + } + if pretty { sb_putc(b, 10); json_indent(b, depth) } + sb_putc(b, 125) +} +function json_dumps(v: JVal, pretty: bool, ea: bool) -> pointer { + let b = sb_new() + jwrite(b, v, pretty, 0, ea) + return sb_str(b) +} + +# ============================================================================ +# palette — the named-colour source of truth (ported from palette.py) +# ============================================================================ +var pal_names: []pointer = new []pointer +var pal_hexes: []pointer = new []pointer # 6-digit uppercase, no prefix +var pal_groups: []pointer = new []pointer +var pal_ready: int = 0 + +function pal_add(nm: pointer, hx: pointer, grp: pointer) -> void { + push(pal_names, nm) + push(pal_hexes, hx) + push(pal_groups, grp) +} + +function palette_init() -> void { + if pal_ready == 1 { return } + pal_ready = 1 + pal_names = new []pointer + pal_hexes = new []pointer + pal_groups = new []pointer + pal_add("White", "FFFFFF", "Whites") + pal_add("Snow", "FFFAFA", "Whites") + pal_add("Ivory", "FFFFF0", "Whites") + pal_add("EggShellWhite", "F0EAD6", "Whites") + pal_add("FloralWhite", "FFFAF0", "Whites") + pal_add("SeaShell", "FFF5EE", "Whites") + pal_add("Linen", "FAF0E6", "Whites") + pal_add("AntiqueWhite", "FAEBD7", "Whites") + pal_add("OldLace", "FDF5E6", "Whites") + pal_add("Beige", "F5F5DC", "Whites") + pal_add("Cream", "FFFDD0", "Whites") + pal_add("Honeydew", "F0FFF0", "Whites") + pal_add("MintCream", "F5FFFA", "Whites") + pal_add("Azure", "F0FFFF", "Whites") + pal_add("AliceBlue", "F0F8FF", "Whites") + pal_add("GhostWhite", "F8F8FF", "Whites") + pal_add("WhiteSmoke", "F5F5F5", "Whites") + pal_add("Lavender", "E6E6FA", "Whites") + pal_add("Bone", "E3DAC9", "Whites") + pal_add("Parchment", "F1E9D2", "Whites") + pal_add("Gainsboro", "DCDCDC", "Grays") + pal_add("LightGray", "D3D3D3", "Grays") + pal_add("Silver", "C0C0C0", "Grays") + pal_add("Ash", "B2BEB5", "Grays") + pal_add("DarkGray", "A9A9A9", "Grays") + pal_add("Gray", "808080", "Grays") + pal_add("DimGray", "696969", "Grays") + pal_add("Nickel", "727472", "Grays") + pal_add("Slate", "708090", "Grays") + pal_add("SlateGray", "708090", "Grays") + pal_add("LightSlateGray", "778899", "Grays") + pal_add("Gunmetal", "2A3439", "Grays") + pal_add("Charcoal", "36454F", "Grays") + pal_add("Graphite", "1C1C1C", "Grays") + pal_add("Onyx", "353839", "Grays") + pal_add("Jet", "343434", "Grays") + pal_add("Black", "000000", "Grays") + pal_add("EerieBlack", "1B1B1B", "Grays") + pal_add("RaisinBlack", "242124", "Grays") + pal_add("Ebony", "555D50", "Grays") + pal_add("Red", "FF0000", "Reds") + pal_add("Crimson", "DC143C", "Reds") + pal_add("Scarlet", "FF2400", "Reds") + pal_add("Vermilion", "E34234", "Reds") + pal_add("FireBrick", "B22222", "Reds") + pal_add("Cinnabar", "E44D2E", "Reds") + pal_add("DarkRed", "8B0000", "Reds") + pal_add("Maroon", "800000", "Reds") + pal_add("Ruby", "E0115F", "Reds") + pal_add("Cardinal", "C41E3A", "Reds") + pal_add("IndianRed", "CD5C5C", "Reds") + pal_add("Rust", "B7410E", "Reds") + pal_add("Sangria", "92000A", "Reds") + pal_add("Redwood", "A45A52", "Reds") + pal_add("Cerise", "DE3163", "Reds") + pal_add("Amaranth", "E52B50", "Reds") + pal_add("Carmine", "960018", "Reds") + pal_add("Chestnut", "954535", "Reds") + pal_add("Brick", "CB4154", "Reds") + pal_add("TerraCotta", "E2725B", "Reds") + pal_add("Pink", "FFC0CB", "Pinks") + pal_add("LightPink", "FFB6C1", "Pinks") + pal_add("HotPink", "FF69B4", "Pinks") + pal_add("DeepPink", "FF1493", "Pinks") + pal_add("PaleVioletRed", "DB7093", "Pinks") + pal_add("Rose", "FF007F", "Pinks") + pal_add("Blush", "DE5D83", "Pinks") + pal_add("Salmon", "FA8072", "Pinks") + pal_add("LightSalmon", "FFA07A", "Pinks") + pal_add("DarkSalmon", "E9967A", "Pinks") + pal_add("Coral", "FF7F50", "Pinks") + pal_add("Watermelon", "FC6C85", "Pinks") + pal_add("Flamingo", "FC8EAC", "Pinks") + pal_add("Bubblegum", "FFC1CC", "Pinks") + pal_add("Fuchsia", "FF00FF", "Pinks") + pal_add("Magenta", "FF00FF", "Pinks") + pal_add("Mauve", "E0B0FF", "Pinks") + pal_add("Puce", "CC8899", "Pinks") + pal_add("Thistle", "D8BFD8", "Pinks") + pal_add("Orchid", "DA70D6", "Pinks") + pal_add("Orange", "FFA500", "Oranges") + pal_add("DarkOrange", "FF8C00", "Oranges") + pal_add("Tangerine", "F28500", "Oranges") + pal_add("Pumpkin", "FF7518", "Oranges") + pal_add("Apricot", "FBCEB1", "Oranges") + pal_add("Peach", "FFE5B4", "Oranges") + pal_add("Cantaloupe", "FFA62B", "Oranges") + pal_add("Amber", "FFBF00", "Oranges") + pal_add("Bronze", "CD7F32", "Oranges") + pal_add("Copper", "B87333", "Oranges") + pal_add("Marigold", "EAA221", "Oranges") + pal_add("Carrot", "ED9121", "Oranges") + pal_add("Persimmon", "EC5800", "Oranges") + pal_add("Papaya", "FF9E2C", "Oranges") + pal_add("Sunset", "FAD6A5", "Oranges") + pal_add("Yellow", "FFFF00", "Yellows") + pal_add("LightYellow", "FFFFE0", "Yellows") + pal_add("Gold", "FFD700", "Yellows") + pal_add("Goldenrod", "DAA520", "Yellows") + pal_add("Lemon", "FFF700", "Yellows") + pal_add("Canary", "FFEF00", "Yellows") + pal_add("Mustard", "FFDB58", "Yellows") + pal_add("Flax", "EEDC82", "Yellows") + pal_add("Wheat", "F5DEB3", "Yellows") + pal_add("Corn", "FBEC5D", "Yellows") + pal_add("Dandelion", "F0E130", "Yellows") + pal_add("Saffron", "F4C430", "Yellows") + pal_add("Khaki", "F0E68C", "Yellows") + pal_add("DarkKhaki", "BDB76B", "Yellows") + pal_add("Straw", "E4D96F", "Yellows") + pal_add("Brown", "8B4513", "Browns") + pal_add("SaddleBrown", "8B4513", "Browns") + pal_add("Sienna", "A0522D", "Browns") + pal_add("Chocolate", "D2691E", "Browns") + pal_add("Peru", "CD853F", "Browns") + pal_add("Tan", "D2B48C", "Browns") + pal_add("BurlyWood", "DEB887", "Browns") + pal_add("Sand", "C2B280", "Browns") + pal_add("Coffee", "6F4E37", "Browns") + pal_add("Espresso", "4B3621", "Browns") + pal_add("Mahogany", "C04000", "Browns") + pal_add("Walnut", "773F1A", "Browns") + pal_add("Umber", "635147", "Browns") + pal_add("Sepia", "704214", "Browns") + pal_add("Taupe", "483C32", "Browns") + pal_add("Fawn", "E5AA70", "Browns") + pal_add("Caramel", "C68E17", "Browns") + pal_add("Cocoa", "D2691E", "Browns") + pal_add("Hazel", "8E7618", "Browns") + pal_add("Wenge", "645452", "Browns") + pal_add("Green", "008000", "Greens") + pal_add("Lime", "00FF00", "Greens") + pal_add("LimeGreen", "32CD32", "Greens") + pal_add("LawnGreen", "7CFC00", "Greens") + pal_add("Chartreuse", "7FFF00", "Greens") + pal_add("GreenYellow", "ADFF2F", "Greens") + pal_add("SpringGreen", "00FF7F", "Greens") + pal_add("MintGreen", "98FF98", "Greens") + pal_add("SeaGreen", "2E8B57", "Greens") + pal_add("MediumSeaGreen", "3CB371", "Greens") + pal_add("ForestGreen", "228B22", "Greens") + pal_add("DarkGreen", "006400", "Greens") + pal_add("OliveDrab", "6B8E23", "Greens") + pal_add("Olive", "808000", "Greens") + pal_add("Moss", "8A9A5B", "Greens") + pal_add("Fern", "4F7942", "Greens") + pal_add("Emerald", "50C878", "Greens") + pal_add("Jade", "00A86B", "Greens") + pal_add("Malachite", "0BDA51", "Greens") + pal_add("Shamrock", "009E60", "Greens") + pal_add("Pistachio", "93C572", "Greens") + pal_add("Avocado", "568203", "Greens") + pal_add("Pine", "01796F", "Greens") + pal_add("Sage", "9CAF88", "Greens") + pal_add("Kelly", "4CBB17", "Greens") + pal_add("Hunter", "355E3B", "Greens") + pal_add("Basil", "579229", "Greens") + pal_add("Clover", "2E8B57", "Greens") + pal_add("Juniper", "6D9A79", "Greens") + pal_add("Neon", "39FF14", "Greens") + pal_add("Cyan", "00FFFF", "Cyans") + pal_add("Aqua", "00FFFF", "Cyans") + pal_add("LightCyan", "E0FFFF", "Cyans") + pal_add("PaleTurquoise", "AFEEEE", "Cyans") + pal_add("Aquamarine", "7FFFD4", "Cyans") + pal_add("Turquoise", "40E0D0", "Cyans") + pal_add("MediumTurquoise", "48D1CC", "Cyans") + pal_add("DarkTurquoise", "00CED1", "Cyans") + pal_add("Teal", "008080", "Cyans") + pal_add("DarkCyan", "008B8B", "Cyans") + pal_add("CadetBlue", "5F9EA0", "Cyans") + pal_add("Lagoon", "018E8E", "Cyans") + pal_add("Seafoam", "93E9BE", "Cyans") + pal_add("Cerulean", "007BA7", "Cyans") + pal_add("SkyBlueLight", "80DAEB", "Cyans") + pal_add("Robin", "00CCCC", "Cyans") + pal_add("Verdigris", "43B3AE", "Cyans") + pal_add("Celadon", "ACE1AF", "Cyans") + pal_add("Blue", "0000FF", "Blues") + pal_add("LightBlue", "ADD8E6", "Blues") + pal_add("PowderBlue", "B0E0E6", "Blues") + pal_add("SkyBlue", "87CEEB", "Blues") + pal_add("LightSkyBlue", "87CEFA", "Blues") + pal_add("DeepSkyBlue", "00BFFF", "Blues") + pal_add("DodgerBlue", "1E90FF", "Blues") + pal_add("CornflowerBlue", "6495ED", "Blues") + pal_add("SteelBlue", "4682B4", "Blues") + pal_add("RoyalBlue", "4169E1", "Blues") + pal_add("MediumBlue", "0000CD", "Blues") + pal_add("DarkBlue", "00008B", "Blues") + pal_add("Navy", "000080", "Blues") + pal_add("MidnightBlue", "191970", "Blues") + pal_add("Cobalt", "0047AB", "Blues") + pal_add("Sapphire", "0F52BA", "Blues") + pal_add("Denim", "1560BD", "Blues") + pal_add("Indigo", "4B0082", "Blues") + pal_add("Prussian", "003153", "Blues") + pal_add("Ultramarine", "3F00FF", "Blues") + pal_add("Periwinkle", "CCCCFF", "Blues") + pal_add("Iris", "5A4FCF", "Blues") + pal_add("Glaucous", "6082B6", "Blues") + pal_add("Zaffre", "0014A8", "Blues") + pal_add("Berry", "2E2D88", "Blues") + pal_add("Purple", "800080", "Purples") + pal_add("Violet", "EE82EE", "Purples") + pal_add("DarkViolet", "9400D3", "Purples") + pal_add("BlueViolet", "8A2BE2", "Purples") + pal_add("MediumPurple", "9370DB", "Purples") + pal_add("Amethyst", "9966CC", "Purples") + pal_add("Plum", "8E4585", "Purples") + pal_add("Eggplant", "614051", "Purples") + pal_add("Grape", "6F2DA8", "Purples") + pal_add("Wine", "722F37", "Purples") + pal_add("Mulberry", "C54B8C", "Purples") + pal_add("Lilac", "C8A2C8", "Purples") + pal_add("Wisteria", "C9A0DC", "Purples") + pal_add("Heliotrope", "DF73FF", "Purples") + pal_add("Byzantium", "702963", "Purples") + pal_add("Tyrian", "66023C", "Purples") + pal_add("RebeccaPurple", "663399", "Purples") + pal_add("Orchid2", "AF69EF", "Purples") +} + +# emit selfhost/backend/stdlib/emit_color.ludic +function palette_emit_ludic(path: pointer) -> bool { + let b = sb_new() + sb_puts(b, "# ============================================================================\n") + sb_puts(b, "# emit_color.ludic — the named-color palette, resolved at compile time.\n") + sb_puts(b, "#\n") + sb_puts(b, "# `Color.Name` in a game lowers to a plain 0xRRGGBB int here: no runtime cost,\n") + sb_puts(b, "# no allocation, identical codegen to writing the hex by hand. Unknown names are\n") + sb_puts(b, "# a compile error (color_lookup returns -1, which emit_expr reports).\n") + sb_puts(b, "#\n") + sb_puts(b, "# GENERATED by `x docs-palette` from the single source-of-truth palette table\n") + sb_puts(b, "# in tools/x/docgen.ludic. Edit the palette there and regenerate; do not hand-edit.\n") + sb_puts(b, "# ============================================================================\n") + sb_puts(b, "\n") + sb_puts(b, "function color_lookup(name: pointer) -> int {\n") + var i = 0 + while i < len(pal_names) { + sb_puts(b, ` if (name == "{pal_names[i]}") {{ return 0x{pal_hexes[i]} }}\n`) + i = i + 1 + } + sb_puts(b, " return -1\n") + sb_puts(b, "}\n") + sb_puts(b, "\n") + return write_file(path, sb_str(b)) +} + +# emit docs/language/colors/palette.json (json.dump indent=2, no trailing nl) +function palette_emit_json(path: pointer) -> bool { + let b = sb_new() + sb_puts(b, "{\n") + sb_puts(b, ` "count": {string(len(pal_names))},\n`) + sb_puts(b, ` "groups": [` + "\n") + # groups in first-appearance order + let seen = new []pointer + var gi = 0 + var firstg = true + while gi < len(pal_groups) { + let g = pal_groups[gi] + if not set_has(seen, g) { + push(seen, g) + if not firstg { sb_puts(b, ",\n") } + firstg = false + sb_puts(b, " {\n") + sb_puts(b, ` "name": "{g}",\n`) + sb_puts(b, ` "colors": [` + "\n") + var ci = 0 + var firstc = true + while ci < len(pal_names) { + if pal_groups[ci] == g { + if not firstc { sb_puts(b, ",\n") } + firstc = false + sb_puts(b, " {\n") + sb_puts(b, ` "name": "{pal_names[ci]}",\n`) + sb_puts(b, ` "hex": "{pal_hexes[ci]}"\n`) + sb_puts(b, " }") + } + ci = ci + 1 + } + sb_puts(b, "\n ]\n") + sb_puts(b, " }") + } + gi = gi + 1 + } + sb_puts(b, "\n ]\n") + sb_puts(b, "}") + return write_file(path, sb_str(b)) +} + +function cmd_docs_palette() -> int { + palette_init() + # guard: duplicate names + let seen = new []pointer + var i = 0 + while i < len(pal_names) { + if set_has(seen, pal_names[i]) { err(`DUPLICATE NAME: {pal_names[i]}\n`); return 1 } + push(seen, pal_names[i]) + i = i + 1 + } + if not palette_emit_ludic("selfhost/backend/stdlib/emit_color.ludic") { err("cannot write emit_color.ludic\n"); return 1 } + if not palette_emit_json("docs/language/colors/palette.json") { err("cannot write palette.json\n"); return 1 } + print(`OK {string(len(pal_names))} colors`) + return 0 +} diff --git a/tools/x/docgen_check.ludic b/tools/x/docgen_check.ludic new file mode 100644 index 00000000..46096a59 --- /dev/null +++ b/tools/x/docgen_check.ludic @@ -0,0 +1,288 @@ +# docgen_check.ludic — `x docs-check [DIR]`: the coverage / integrity guard over +# a generated docs site, ported from tools/docgen/check.py. Fails (exit 1) if the +# pages contract is broken, an inventory symbol lacks a source file or page, a +# token is documented on two pages, a namespace/section is split across dirs, or +# a highlighter link target was not generated. Thin (un-expanded) symbols warn. + +# a key -> set-of-values bucket (for token/dir conflict detection) +property Bucket { k1: pointer = "", k2: pointer = "", vals: []pointer } + +function bucket_get(bs: []Bucket, k1: pointer, k2: pointer) -> Bucket { + var i = 0 + while i < len(bs) { + if bs[i].k1 == k1 and bs[i].k2 == k2 { return bs[i] } + i = i + 1 + } + let b = new Bucket + b.k1 = k1 + b.k2 = k2 + b.vals = new []pointer + push(bs, b) + return b +} +function bucket_add(bs: []Bucket, k1: pointer, k2: pointer, v: pointer) -> void { + let b = bucket_get(bs, k1, k2) + var i = 0 + while i < len(b.vals) { if b.vals[i] == v { return }; i = i + 1 } + push(b.vals, v) +} +# ", ".join(sorted(vals)) +function join_sorted(vals: []pointer) -> pointer { + strs_sort(vals) + let b = sb_new() + var i = 0 + while i < len(vals) { + if i > 0 { sb_puts(b, ", ") } + sb_puts(b, vals[i]) + i = i + 1 + } + return sb_str(b) +} + +# parse_front: like parse_doc but body is stripped on both ends (check.py) +function parse_front(path: pointer) -> Doc { + let d = new Doc + d.m = meta_new() + let text = read_file(path) + if text == null { d.body = ""; return d } + d.body = text + if not s_starts(text, "---") { return d } + let end = s_index(text, "\n---", 3) + if end < 0 { return d } + let fm = sslice(text, 3, end) + let fn2 = slen(fm) + var j = 0 + while j < fn2 { + let ln = line_at(fm, j) + j = j + slen(ln) + 1 + let t = s_trim(ln) + if slen(t) == 0 { continue } + let c = s_index(ln, ":", 0) + if c < 0 { continue } + meta_put(d.m, s_trim(sslice(ln, 0, c)), s_trim(sslice(ln, c + 1, slen(ln)))) + } + d.body = s_trim(sslice(text, end + 4, slen(text))) + return d +} + +# count Unicode codepoints (a byte is a lead unless (b & 0xC0) == 0x80) +function cp_len(s: pointer) -> int { + let n = slen(s) + var c = 0 + var i = 0 + while i < n { if (s[i] & 192) != 128 { c = c + 1 }; i = i + 1 } + return c +} +function replace_all(hay: pointer, needle: pointer, repl: pointer) -> pointer { + if slen(needle) == 0 { return hay } + let b = sb_new() + let nl = slen(needle) + let n = slen(hay) + var i = 0 + while i < n { + if s_starts_at(hay, i, needle) { sb_puts(b, repl); i = i + nl } + else { sb_putc(b, hay[i]); i = i + 1 } + } + return sb_str(b) +} +# strip a filename's trailing ".md" -> the fallback id (fn[:-3]) +function drop_md(fn: pointer) -> pointer { + let n = slen(fn) + if n >= 3 { return sslice(fn, 0, n - 3) } + return fn +} + +function cmd_docs_check() -> int { + var site = "build/pages" + if arg_count() >= 3 { site = arg(2) } + let problems = new []pointer + let warnings = new []pointer + + # 1) pages contract + if not file_exists(`{site}/index.html`) { push(problems, "missing required file: index.html") } + if not file_exists(`{site}/api.html`) { push(problems, "missing required file: api.html") } + if not file_exists(`{site}/.nojekyll`) { push(problems, "missing required file: .nojekyll") } + + # 2) coverage + thin detection over docs/language/** + let id2src = meta_new() + let thin = new []pointer + let cats = list_sorted("docs/language") + var ci = 0 + while ci < len(cats) { + let cat = cats[ci] + ci = ci + 1 + let cdir = "docs/language/" + cat + if not is_dir(cdir) { continue } + let files = list_sorted(cdir) + var fi = 0 + while fi < len(files) { + let fn = files[fi] + fi = fi + 1 + if not s_ends(fn, ".md") { continue } + if fn == "_section.md" { continue } + let d = parse_front(cdir + "/" + fn) + let sid = meta_get(d.m, "id", drop_md(fn)) + meta_put(id2src, sid, cdir + "/" + fn) + # thin: drop a Parameters: block (to end, case-insensitive) + the tip line + let tipline = meta_get(d.m, "tip", "") + let low = lower_ascii(d.body) + var btext = d.body + let pp = s_index(low, "parameters:", 0) + if pp >= 0 { btext = sslice(d.body, 0, pp) } + btext = s_trim(btext) + if slen(tipline) > 0 { btext = s_trim(replace_all(btext, tipline, "")) } + if cp_len(btext) < 40 { push(thin, sid) } + } + } + # coverage against the authoritative inventory + let invtext = read_file("tools/docgen/inventory.json") + if invtext != null { + let inv = json_parse(invtext) + if inv.t == JV_OBJ { + var k = 0 + while k < len(inv.keys) { + let cat = inv.keys[k] + let ids = inv.kids[k] + k = k + 1 + if ids.t != JV_ARR { continue } + var m = 0 + while m < len(ids.kids) { + let sid = ids.kids[m].s + m = m + 1 + if meta_get(id2src, sid, "") == "" { push(problems, `no source file for inventory symbol: {sid} ({cat})`) } + else if not file_exists(`{site}/{sid}.html`) { push(problems, `no generated page for symbol: {sid}.html`) } + } + } + } + } + + # 2b/2c) duplicate tokens + one-dir-per-namespace/section + let tok2ids = new []Bucket # k1=kind, k2=tok -> ids + let ns2dirs = new []Bucket # k1="", k2=ns -> dirs + let id2dirs = new []Bucket # k1="", k2=section id -> dirs + let title2dirs = new []Bucket # k1="", k2=lower(title) -> dirs + ci = 0 + while ci < len(cats) { + let cat = cats[ci] + ci = ci + 1 + let cdir = "docs/language/" + cat + if not is_dir(cdir) { continue } + let secp = cdir + "/_section.md" + if file_exists(secp) { + let sd = parse_front(secp) + bucket_add(id2dirs, "", meta_get(sd.m, "id", cat), cat) + bucket_add(title2dirs, "", lower_ascii(s_trim(meta_get(sd.m, "title", ""))), cat) + } + let files = list_sorted(cdir) + var fi = 0 + while fi < len(files) { + let fn = files[fi] + fi = fi + 1 + if not s_ends(fn, ".md") { continue } + if fn == "_section.md" { continue } + let d = parse_front(cdir + "/" + fn) + let kind = meta_get(d.m, "kind", "") + let id = meta_get(d.m, "id", "") + let toks = split_ws(meta_get(d.m, "tokens", "")) + var ti = 0 + while ti < len(toks) { bucket_add(tok2ids, kind, toks[ti], id); ti = ti + 1 } + let ns = meta_get(d.m, "ns", "") + if slen(ns) > 0 { bucket_add(ns2dirs, "", ns, cat) } + } + } + var bi = 0 + while bi < len(tok2ids) { + let b = tok2ids[bi] + bi = bi + 1 + if len(b.vals) > 1 { push(problems, `token '{b.k2}' ({b.k1}) documented on multiple pages: {join_sorted(b.vals)}`) } + } + bi = 0 + while bi < len(ns2dirs) { + let b = ns2dirs[bi] + bi = bi + 1 + if len(b.vals) > 1 { push(problems, `namespace '{b.k2}' documented from multiple dirs: {join_sorted(b.vals)}`) } + } + bi = 0 + while bi < len(id2dirs) { + let b = id2dirs[bi] + bi = bi + 1 + if len(b.vals) > 1 { push(problems, `section id '{b.k2}' declared by multiple dirs: {join_sorted(b.vals)}`) } + } + bi = 0 + while bi < len(title2dirs) { + let b = title2dirs[bi] + bi = bi + 1 + if slen(b.k2) > 0 and len(b.vals) > 1 { push(problems, `section title '{b.k2}' shared by multiple dirs: {join_sorted(b.vals)}`) } + } + + # 3) thin warning + if len(thin) > 0 { + strs_sort(thin) + let b = sb_new() + var i = 0 + while i < len(thin) and i < 12 { + if i > 0 { sb_puts(b, ", ") } + sb_puts(b, thin[i]) + i = i + 1 + } + var tail = "" + if len(thin) > 12 { tail = " …" } + push(warnings, `{string(len(thin))} symbols still have only seed text: {sb_str(b)}{tail}`) + } + + # 4) highlighter targets exist + let sjtext = read_file(`{site}/symbols.json`) + if sjtext != null { + let sj = json_parse(sjtext) + let hl = j_get(sj, "highlight") + let targets = new []pointer + collect_targets_str(j_get(hl, "keywords"), targets) + collect_targets_str(j_get(hl, "types"), targets) + collect_targets_str(j_get(hl, "phases"), targets) + collect_targets_str(j_get(hl, "annotations"), targets) + collect_targets_id(j_get(hl, "builtins"), targets) + collect_targets_id(j_get(hl, "nsmethods"), targets) + collect_targets_str(j_get(hl, "namespaces"), targets) + strs_sort(targets) + var i = 0 + while i < len(targets) { + let tgt = targets[i] + i = i + 1 + if not file_exists(`{site}/{tgt}.html`) { push(problems, `highlighter links to {tgt}.html but it was not generated`) } + } + } + + # report + var wi = 0 + while wi < len(warnings) { print(` warning: {warnings[wi]}`); wi = wi + 1 } + if len(problems) > 0 { + print("docs check FAILED:") + var pi = 0 + while pi < len(problems) { print(` - {problems[pi]}`); pi = pi + 1 } + return 1 + } + print(`docs check OK: {site} ({string(len(id2src.keys))} symbols documented)`) + return 0 +} + +# add every string value of an object's members (set semantics via the caller) +function collect_targets_str(o: JVal, out: []pointer) -> void { + if o.t != JV_OBJ { return } + var i = 0 + while i < len(o.kids) { + let v = o.kids[i] + i = i + 1 + if v.t == JV_STR { set_add(out, v.s) } + } +} +# add each member value's ["id"] (builtins / nsmethods) +function collect_targets_id(o: JVal, out: []pointer) -> void { + if o.t != JV_OBJ { return } + var i = 0 + while i < len(o.kids) { + let v = o.kids[i] + i = i + 1 + let id = j_get(v, "id") + if id.t == JV_STR { set_add(out, id.s) } + } +} diff --git a/tools/x/docgen_gen.ludic b/tools/x/docgen_gen.ludic new file mode 100644 index 00000000..63f429ce --- /dev/null +++ b/tools/x/docgen_gen.ludic @@ -0,0 +1,1014 @@ +# docgen_gen.ludic — `x docs-gen`: the documentation-site generator, ported from +# tools/docgen/gen.py. Parses docs/language/** (front-matter + bodies) and +# docs/site/*, reads the asset templates in tools/docgen/assets, and emits the +# whole pages-branch payload (per-symbol pages, namespace/color/api pages, the +# landing page, ludic-highlight.js, symbols.json, .nojekyll) into --out. Output +# is byte-for-byte identical to the Python generator (gated in x test). +# +# Shared helpers (Sb, hesc/hescattr, the JSON writer, codeify, string utils) +# live in docgen.ludic; the JSON reader (json_parse/j_get) in json.ludic. + +# ---- the parsed-doc model --------------------------------------------------- +property Meta { keys: []pointer, vals: []pointer } +property Param { name: pointer = "", desc: pointer = "" } +property Split { desc: pointer = "", examples: []pointer, params: []Param } +property Doc { m: Meta, body: pointer = "" } +property Entry { + id: pointer = "", name: pointer = "", sig: pointer = "", kind: pointer = "", + tip: pointer = "", ns: pointer = "", order: int = 999, + desc_html: pointer = "", examples: []pointer, params: []Param, + tokens_list: []pointer, related_list: []pointer +} +property Section { + id: pointer = "", title: pointer = "", order: int = 999, blurb: pointer = "", + cat: pointer = "", ns_name: pointer = "", entries: []Entry +} +property Symbols { items: JVal, sym: JVal } + +function meta_new() -> Meta { + let m = new Meta + m.keys = new []pointer + m.vals = new []pointer + return m +} +function meta_put(m: Meta, k: pointer, v: pointer) -> void { + var i = 0 + while i < len(m.keys) { if m.keys[i] == k { m.vals[i] = v; return }; i = i + 1 } + push(m.keys, k) + push(m.vals, v) +} +function meta_get(m: Meta, k: pointer, dflt: pointer) -> pointer { + var i = 0 + while i < len(m.keys) { if m.keys[i] == k { return m.vals[i] }; i = i + 1 } + return dflt +} + +# ---- asset / snippet IO ----------------------------------------------------- +function read_asset(name: pointer) -> pointer { + let s = read_file("tools/docgen/assets/" + name) + if s == null { return "" } + return s +} +# read a snippet file, rstrip("\n") (trailing newlines only) +function read_snippet(rel: pointer) -> pointer { + let s = read_file(rel) + if s == null { return "" } + var n = slen(s) + while n > 0 and s[n - 1] == 10 { n = n - 1 } + return sslice(s, 0, n) +} + +# ---- front-matter + body parsing (parse_doc) -------------------------------- +function parse_doc(path: pointer) -> Doc { + let d = new Doc + d.m = meta_new() + let text = read_file(path) + if text == null { d.body = ""; return d } + d.body = text + if not s_starts(text, "---") { return d } + let end = s_index(text, "\n---", 3) + if end < 0 { return d } + let fm = sslice(text, 3, end) + let fn2 = slen(fm) + var j = 0 + while j < fn2 { + let ln = line_at(fm, j) + j = j + slen(ln) + 1 + let t = s_trim(ln) + if slen(t) == 0 { continue } + let c = s_index(ln, ":", 0) + if c < 0 { continue } + let key = s_trim(sslice(ln, 0, c)) + let val = s_trim(sslice(ln, c + 1, slen(ln))) + meta_put(d.m, key, val) + } + d.body = lstrip_nl(sslice(text, end + 4, slen(text))) + return d +} + +# match ^-\s*`?(\w+)`?\s*[—-]+\s*(.*)$ against an already-trimmed line +function param_match(line: pointer, p: Param) -> bool { + let n = slen(line) + var i = 0 + if i < n and line[i] == 45 { i = i + 1 } else { return false } + while i < n and is_ws(line[i]) { i = i + 1 } + if i < n and line[i] == 96 { i = i + 1 } + let ws = i + while i < n and is_word(line[i]) { i = i + 1 } + if i == ws { return false } + let name = sslice(line, ws, i) + if i < n and line[i] == 96 { i = i + 1 } + while i < n and is_ws(line[i]) { i = i + 1 } + var sep = 0 + while i < n { + if line[i] == 45 { i = i + 1; sep = sep + 1 } + else if (line[i] & 255) == 226 and i + 2 < n and (line[i + 1] & 255) == 128 and (line[i + 2] & 255) == 148 { i = i + 3; sep = sep + 1 } + else { break } + } + if sep == 0 { return false } + while i < n and is_ws(line[i]) { i = i + 1 } + p.name = name + p.desc = codeify(s_trim(sslice(line, i, n))) + return true +} + +# split desc on /\n\s*\n/ into trimmed, non-empty paragraphs +function split_paras(desc: pointer) -> []pointer { + let out = new []pointer + let n = slen(desc) + var start = 0 + var i = 0 + while i < n { + if desc[i] == 10 { + var jj = i + var nl = 0 + while jj < n and is_space_all(desc[jj]) { if desc[jj] == 10 { nl = nl + 1 }; jj = jj + 1 } + if nl >= 2 { + let part = s_trim(sslice(desc, start, i)) + if slen(part) > 0 { push(out, part) } + start = jj + i = jj + continue + } + i = jj + } else { + i = i + 1 + } + } + let last = s_trim(sslice(desc, start, n)) + if slen(last) > 0 { push(out, last) } + return out +} + +function split_body(body: pointer) -> Split { + let r = new Split + r.examples = new []pointer + r.params = new []Param + # 1) pull ```ludic fences out, keep the rest + let wo = sb_new() + let n = slen(body) + var i = 0 + while i < n { + let p = s_index(body, "```ludic\n", i) + if p < 0 { + var k = i + while k < n { sb_putc(wo, body[k]); k = k + 1 } + i = n + } else { + var k = i + while k < p { sb_putc(wo, body[k]); k = k + 1 } + let cs = p + 9 + let q = s_index(body, "\n```", cs) + if q < 0 { + var k2 = p + while k2 < cs { sb_putc(wo, body[k2]); k2 = k2 + 1 } + i = cs + } else { + push(r.examples, sslice(body, cs, q)) + i = q + 4 + } + } + } + let body2 = sb_str(wo) + # 2) pull a Parameters: block, keep every other line + let m2 = slen(body2) + let keep = sb_new() + var firstkeep = true + var li = 0 + while li < m2 { + let ln = line_at(body2, li) + let lnlen = slen(ln) + li = li + lnlen + 1 + let low = lower_ascii(s_trim(ln)) + if s_starts(low, "parameters:") { + while li < m2 { + let ln2 = line_at(body2, li) + let l2 = slen(ln2) + let t2 = s_trim(ln2) + if slen(t2) == 0 { break } + li = li + l2 + 1 + let pp = new Param + if param_match(t2, pp) { push(r.params, pp) } + } + } else { + if not firstkeep { sb_putc(keep, 10) } + firstkeep = false + var kk = 0 + while kk < lnlen { sb_putc(keep, ln[kk]); kk = kk + 1 } + } + } + # 3) desc -> paragraphs joined

, each codeified + let desc = s_trim(sb_str(keep)) + let paras = split_paras(desc) + let ob = sb_new() + var pi = 0 + while pi < len(paras) { + if pi > 0 { sb_puts(ob, "

") } + sb_puts(ob, codeify(paras[pi])) + pi = pi + 1 + } + r.desc = sb_str(ob) + return r +} + +# the first sentence of a blurb, tags stripped, truncated to 160 with an ellipsis +function plain_text(s: pointer) -> pointer { + let sb = sb_new() + let n = slen(s) + var i = 0 + while i < n { + if s[i] == 60 { + while i < n and s[i] != 62 { i = i + 1 } + if i < n { i = i + 1 } + } else { + sb_putc(sb, s[i]) + i = i + 1 + } + } + let t0 = s_trim(sb_str(sb)) + let tn = slen(t0) + var cut = 0 - 1 + var j = 0 + while j < tn { + let c = t0[j] + if c == 10 { break } + if (c == 46 or c == 33 or c == 63) and j >= 1 { + if j + 1 >= tn { cut = j + 1; break } + if is_space_all(t0[j + 1]) { cut = j + 1; break } + } + j = j + 1 + } + var t = t0 + if cut >= 0 { t = sslice(t0, 0, cut) } + # Python len()/slicing is by codepoint; count codepoints (a byte is a lead when + # (b & 0xC0) != 0x80) and truncate at the 160th, not the 160th byte. + let tl = slen(t) + var cpc = 0 + var bi = 0 + while bi < tl { if (t[bi] & 192) != 128 { cpc = cpc + 1 }; bi = bi + 1 } + if cpc <= 160 { return t } + var idx = 0 + var seen = 0 + while idx < tl and seen < 160 { + idx = idx + 1 + while idx < tl and (t[idx] & 192) == 128 { idx = idx + 1 } + seen = seen + 1 + } + let pre = sslice(t, 0, idx) + var sp = slen(pre) - 1 + while sp >= 0 and pre[sp] != 32 { sp = sp - 1 } + var head = pre + if sp >= 0 { head = sslice(pre, 0, sp) } + return head + "…" +} + +# ---- load the section/entry model ------------------------------------------- +function list_sorted(dir: pointer) -> []pointer { + let out = new []pointer + let s = capture(`LC_ALL=C ls -1A {dir} 2>/dev/null`) + let n = slen(s) + var i = 0 + while i < n { + let ln = line_at(s, i) + i = i + slen(ln) + 1 + if slen(ln) > 0 { push(out, ln) } + } + return out +} +function is_dir(path: pointer) -> bool { return shq(`test -d {path}`) } + +function build_entry(path: pointer) -> Entry { + let d = parse_doc(path) + let sp = split_body(d.body) + let e = new Entry + e.id = meta_get(d.m, "id", "") + e.name = meta_get(d.m, "name", "") + e.sig = meta_get(d.m, "sig", "") + e.kind = meta_get(d.m, "kind", "") + e.tip = meta_get(d.m, "tip", "") + e.ns = meta_get(d.m, "ns", "") + e.order = str_to_int(meta_get(d.m, "order", "999")) + e.desc_html = sp.desc + e.examples = sp.examples + e.params = sp.params + e.tokens_list = split_ws(meta_get(d.m, "tokens", "")) + e.related_list = split_ws(meta_get(d.m, "related", "")) + return e +} + +function entry_gt(a: Entry, b: Entry) -> bool { + if a.order != b.order { return a.order > b.order } + return str_gt(a.name, b.name) +} +function entries_sort(es: []Entry) -> void { + var i = 1 + while i < len(es) { + let cur = es[i] + var j = i - 1 + while j >= 0 and entry_gt(es[j], cur) { es[j + 1] = es[j]; j = j - 1 } + es[j + 1] = cur + i = i + 1 + } +} +function section_gt(a: Section, b: Section) -> bool { + if a.order != b.order { return a.order > b.order } + return str_gt(a.title, b.title) +} +function sections_sort(ss: []Section) -> void { + var i = 1 + while i < len(ss) { + let cur = ss[i] + var j = i - 1 + while j >= 0 and section_gt(ss[j], cur) { ss[j + 1] = ss[j]; j = j - 1 } + ss[j + 1] = cur + i = i + 1 + } +} + +function load_sections() -> []Section { + let sections = new []Section + let cats = list_sorted("docs/language") + var ci = 0 + while ci < len(cats) { + let cat = cats[ci] + ci = ci + 1 + let cdir = "docs/language/" + cat + if not is_dir(cdir) { continue } + var sid = cat + var stitle = title_case(cat) + var sorder = 999 + var sblurb = "" + let secp = cdir + "/_section.md" + if file_exists(secp) { + let sd = parse_doc(secp) + sid = meta_get(sd.m, "id", cat) + stitle = meta_get(sd.m, "title", title_case(cat)) + sorder = str_to_int(meta_get(sd.m, "order", "999")) + sblurb = codeify(s_trim(sd.body)) + } + let entries = new []Entry + let files = list_sorted(cdir) + var fi = 0 + while fi < len(files) { + let fn = files[fi] + fi = fi + 1 + if not s_ends(fn, ".md") { continue } + if fn == "_section.md" { continue } + push(entries, build_entry(cdir + "/" + fn)) + } + entries_sort(entries) + var nsname = "" + var ei = 0 + while ei < len(entries) { + let e = entries[ei] + if e.kind == "namespace-method" and slen(e.ns) > 0 { nsname = e.ns; break } + ei = ei + 1 + } + let sec = new Section + sec.id = sid + sec.title = stitle + sec.order = sorder + sec.blurb = sblurb + sec.cat = cat + sec.entries = entries + sec.ns_name = nsname + push(sections, sec) + } + sections_sort(sections) + return sections +} + +# ---- symbol / highlighter model (build_symbols) ----------------------------- +function ns_tip(nn: pointer, sections: []Section) -> pointer { + if nn == "Color" { return "The named color palette." } + var i = 0 + while i < len(sections) { + if sections[i].ns_name == nn { return plain_text(sections[i].blurb) } + i = i + 1 + } + return "" +} + +function build_symbols(sections: []Section) -> Symbols { + let items = jvo() + let sym = jvo() + let kw = jvo() + let types = jvo() + let phases = jvo() + let builtins = jvo() + let nsm = jvo() + let annot = jvo() + let namespaces = jvo() + let tips = jvo() + let cards = jvo() + jvput(sym, "keywords", kw) + jvput(sym, "types", types) + jvput(sym, "phases", phases) + jvput(sym, "builtins", builtins) + jvput(sym, "nsmethods", nsm) + jvput(sym, "annotations", annot) + jvput(sym, "namespaces", namespaces) + jvput(sym, "tips", tips) + jvput(sym, "cards", cards) + jvput(sym, "colors_page", jvs("ns-color")) + var si = 0 + while si < len(sections) { + let s = sections[si] + si = si + 1 + var ei = 0 + while ei < len(s.entries) { + let e = s.entries[ei] + ei = ei + 1 + let eid = e.id + let kind = e.kind + let tip = e.tip + let page = eid + ".html" + let it = jvo() + jvput(it, "id", jvs(eid)) + jvput(it, "name", jvs(e.name)) + jvput(it, "sig", jvs(e.sig)) + jvput(it, "kind", jvs(kind)) + jvput(it, "category", jvs(s.id)) + jvput(it, "section", jvs(s.title)) + jvput(it, "tip", jvs(tip)) + jvput(it, "page", jvs(page)) + jvput(items, eid, it) + let cd = jvo() + jvput(cd, "name", jvs(e.name)) + jvput(cd, "sig", jvs(e.sig)) + jvput(cd, "tip", jvs(tip)) + jvput(cd, "kind", jvs(kind)) + jvput(cd, "page", jvs(page)) + jvput(cd, "section", jvs(s.title)) + jvput(cards, eid, cd) + var ti = 0 + while ti < len(e.tokens_list) { + let tok = e.tokens_list[ti] + ti = ti + 1 + if kind == "keyword" { jvput(kw, tok, jvs(eid)) } + else if kind == "type" { jvput(types, tok, jvs(eid)) } + else if kind == "phase" { jvput(phases, tok, jvs(eid)) } + else if kind == "builtin" { + let o = jvo() + jvput(o, "id", jvs(eid)) + let pa = jva() + var qi = 0 + while qi < len(e.params) { jvpush(pa, jvs(e.params[qi].name)); qi = qi + 1 } + jvput(o, "params", pa) + jvput(builtins, tok, o) + } + else if kind == "namespace-method" { + let o = jvo() + jvput(o, "id", jvs(eid)) + let pa = jva() + var qi = 0 + while qi < len(e.params) { jvpush(pa, jvs(e.params[qi].name)); qi = qi + 1 } + jvput(o, "params", pa) + jvput(nsm, tok, o) + } + else if kind == "annotation" { jvput(annot, tok, jvs(eid)) } + if slen(tip) > 0 { jvput(tips, tok, jvs(tip)) } + } + } + } + let nsnames = new []pointer + var ki = 0 + while ki < len(nsm.keys) { + let tok = nsm.keys[ki] + ki = ki + 1 + let dot = s_index(tok, ".", 0) + if dot >= 0 { set_add(nsnames, sslice(tok, 0, dot)) } + } + set_add(nsnames, "Color") + strs_sort(nsnames) + var ni = 0 + while ni < len(nsnames) { + let nn = nsnames[ni] + ni = ni + 1 + let pg = "ns-" + lower_ascii(nn) + jvput(namespaces, nn, jvs(pg)) + let cd = jvo() + jvput(cd, "name", jvs(nn)) + jvput(cd, "sig", jvs(nn + ".*")) + jvput(cd, "kind", jvs("namespace")) + jvput(cd, "tip", jvs(ns_tip(nn, sections))) + jvput(cd, "page", jvs(pg + ".html")) + jvput(cd, "section", jvs("Namespaces")) + jvput(cards, pg, cd) + } + let res = new Symbols + res.items = items + res.sym = sym + return res +} + +# ---- shared chrome ---------------------------------------------------------- +function js_get(v: JVal, key: pointer) -> pointer { + let x = j_get(v, key) + if x.t == JV_STR { return x.s } + return "" +} +function j_has(v: JVal, key: pointer) -> bool { + if v.t != JV_OBJ { return false } + var i = 0 + while i < len(v.keys) { if v.keys[i] == key { return true }; i = i + 1 } + return false +} + +function emit_head(b: Sb, title: pointer, desc: pointer, cssfile: pointer) -> void { + sb_puts(b, `\n\n\n\n\n{hesc(title)}\n\n\n\n\n\n`) +} + +# nav links; a link whose href equals cta_href gets the nav-cta class +function nav_html(b: Sb, links: JVal, cta_href: pointer) -> void { + if links.t != JV_ARR { return } + var i = 0 + while i < len(links.kids) { + let nlk = links.kids[i] + i = i + 1 + let href = js_get(nlk, "href") + let label = js_get(nlk, "label") + if href == cta_href { sb_puts(b, `{hesc(label)}`) } + else { sb_puts(b, `{hesc(label)}`) } + } +} + +function topbar(b: Sb, cfg: JVal) -> void { + var links = j_get(cfg, "ref_nav_links") + if links.t != JV_ARR { links = j_get(cfg, "nav_links") } + sb_puts(b, `

`) +} + +# ---- per-item page ---------------------------------------------------------- +function render_params(e: Entry) -> pointer { + if len(e.params) == 0 { return "" } + let b = sb_new() + sb_puts(b, `

Parameters

`) + var i = 0 + while i < len(e.params) { + let p = e.params[i] + i = i + 1 + sb_puts(b, `
{hesc(p.name)}{p.desc}
`) + } + sb_puts(b, `
`) + return sb_str(b) +} +function render_examples(e: Entry) -> pointer { + if len(e.examples) == 0 { return "" } + let b = sb_new() + sb_puts(b, `

Example

`) + var i = 0 + while i < len(e.examples) { + sb_puts(b, `
{hesc(e.examples[i])}
`) + i = i + 1 + } + sb_puts(b, `
`) + return sb_str(b) +} +function render_related(e: Entry, items: JVal) -> pointer { + let b = sb_new() + var cnt = 0 + var i = 0 + while i < len(e.related_list) { + let r = e.related_list[i] + i = i + 1 + let it = j_get(items, r) + if it.t == JV_NULL { continue } + if cnt == 0 { sb_puts(b, ``) + return sb_str(b) +} + +function render_item(b: Sb, e: Entry, s: Section, cfg: JVal, items: JVal) -> void { + emit_head(b, e.name + " — Ludic", e.tip, "item.css") + sb_puts(b, "\n\n") + topbar(b, cfg) + sb_putc(b, 10) + var sechref = "api.html#" + s.id + if slen(s.ns_name) > 0 { sechref = "ns-" + lower_ascii(s.ns_name) + ".html" } + var kindc = e.kind + if e.kind == "namespace-method" { kindc = "method" } + var kindlabel = e.kind + if e.kind == "namespace-method" { kindlabel = "method" } + else if e.kind == "builtin" { kindlabel = "function" } + var descv = e.desc_html + if slen(descv) == 0 { descv = hesc(e.tip) } + sb_puts(b, `
\n`) + sb_puts(b, `
API Reference › {hesc(s.title)} › {hesc(e.name)}
\n`) + sb_puts(b, `
\n`) + sb_puts(b, ` {hesc(kindlabel)}\n`) + sb_puts(b, `

{hesc(e.name)}

\n`) + sb_puts(b, `
\n`) + sb_puts(b, ` {hesc(e.sig)}\n`) + sb_puts(b, `

{descv}

\n`) + sb_puts(b, ` {render_params(e)}\n`) + sb_puts(b, ` {render_examples(e)}\n`) + sb_puts(b, ` {render_related(e, items)}\n`) + sb_puts(b, ` ← All symbols\n`) + sb_puts(b, `
\n`) + sb_puts(b, `\n`) + sb_puts(b, `\n`) + sb_puts(b, ``) +} + +# ---- namespace overview page ------------------------------------------------ +function render_ns(b: Sb, nsname: pointer, s: Section, cfg: JVal) -> void { + emit_head(b, nsname + " — Ludic", plain_text(s.blurb), "item.css") + sb_puts(b, "\n\n") + topbar(b, cfg) + sb_putc(b, 10) + sb_puts(b, `
\n`) + sb_puts(b, `
API Reference › {hesc(nsname)}
\n`) + sb_puts(b, `
namespace

{hesc(nsname)}

\n`) + sb_puts(b, `

{s.blurb}

\n`) + sb_puts(b, `
`) + var i = 0 + while i < len(s.entries) { + let e = s.entries[i] + i = i + 1 + sb_puts(b, `{hesc(e.sig)}{hesc(e.tip)}`) + } + sb_puts(b, `
\n`) + sb_puts(b, ` ← All symbols\n`) + sb_puts(b, `
\n`) + sb_puts(b, `\n`) + sb_puts(b, `\n`) + sb_puts(b, ``) +} + +# ---- color page (from the in-memory palette) -------------------------------- +function render_color(b: Sb, cfg: JVal) -> void { + palette_init() + emit_head(b, "Color — Ludic", "The Ludic named-color palette.", "item.css") + sb_puts(b, "\n\n") + topbar(b, cfg) + sb_putc(b, 10) + sb_puts(b, `
\n`) + sb_puts(b, `
API Reference › Color
\n`) + sb_puts(b, `
namespace

Color

\n`) + sb_puts(b, `

The named color palette. Every Color.Name lowers to a plain 0xRRGGBB integer at compile time — no runtime cost. {string(len(pal_names))} names are built in.

\n`) + sb_puts(b, ` `) + # groups in first-appearance order + let seen = new []pointer + var gi = 0 + while gi < len(pal_groups) { + let g = pal_groups[gi] + gi = gi + 1 + if set_has(seen, g) { continue } + push(seen, g) + sb_puts(b, `

{hesc(g)}

`) + var ci = 0 + while ci < len(pal_names) { + if pal_groups[ci] == g { + let nm = pal_names[ci] + let hx = pal_hexes[ci] + sb_puts(b, `
Color.{hesc(nm)}#{hx}
`) + } + ci = ci + 1 + } + sb_puts(b, `
`) + } + sb_puts(b, `\n`) + sb_puts(b, ` ← All symbols\n`) + sb_puts(b, `
\n`) + sb_puts(b, `\n`) + sb_puts(b, `\n`) + sb_puts(b, ``) +} + +# ---- API index -------------------------------------------------------------- +function render_api(b: Sb, sections: []Section, cfg: JVal) -> void { + emit_head(b, "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") + sb_puts(b, "\n\n") + topbar(b, cfg) + sb_putc(b, 10) + sb_puts(b, `
\n`) + sb_puts(b, `
\n`) + sb_puts(b, `
Reference
\n`) + sb_puts(b, `

API Reference

\n`) + sb_puts(b, `

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.

\n`) + sb_puts(b, ` \n`) + sb_puts(b, ` \n`) + sb_puts(b, `
\n`) + sb_puts(b, ` `) + var si = 0 + while si < len(sections) { + let s = sections[si] + si = si + 1 + if s.cat == "colors" { + sb_puts(b, `

{hesc(s.title)}

{s.blurb}

`) + continue + } + sb_puts(b, `

{hesc(s.title)}

{s.blurb}

`) + var ei = 0 + while ei < len(s.entries) { + let e = s.entries[ei] + ei = ei + 1 + sb_puts(b, `{hesc(e.name)}{hesc(e.tip)}`) + } + sb_puts(b, `
`) + } + sb_puts(b, `\n`) + sb_puts(b, `
\n`) + sb_puts(b, `\n`) + sb_puts(b, `\n`) + sb_puts(b, ``) +} + +# ---- landing page (index.tmpl.html via %(key)s substitution) ---------------- +function put(mk: []pointer, mv: []pointer, k: pointer, v: pointer) -> void { + push(mk, k) + push(mv, v) +} +function pct(tmpl: pointer, mk: []pointer, mv: []pointer) -> pointer { + let n = slen(tmpl) + let b = sb_new() + var i = 0 + while i < n { + if tmpl[i] == 37 and i + 1 < n and tmpl[i + 1] == 40 { + var j = i + 2 + while j < n and is_word(tmpl[j]) { j = j + 1 } + if j > i + 2 and j + 1 < n and tmpl[j] == 41 and tmpl[j + 1] == 115 { + let key = sslice(tmpl, i + 2, j) + var k = 0 + var found = false + while k < len(mk) { if mk[k] == key { sb_puts(b, mv[k]); found = true; break }; k = k + 1 } + i = j + 2 + continue + } + } + sb_putc(b, tmpl[i]) + i = i + 1 + } + return sb_str(b) +} + +function build_stages(hero: JVal) -> pointer { + let b = sb_new() + let pl = j_get(hero, "pipeline") + if pl.t != JV_ARR { return "" } + let m = len(pl.kids) + var i = 0 + while i < m { + let st = pl.kids[i].s + var hl = "" + if st == "ludicc" { hl = " hl" } + sb_puts(b, `{hesc(st)}`) + if i < m - 1 { sb_puts(b, `→`) } + i = i + 1 + } + return sb_str(b) +} +function build_feats(feats: JVal) -> pointer { + let b = sb_new() + let cards = j_get(feats, "cards") + if cards.t != JV_ARR { return "" } + var i = 0 + while i < len(cards.kids) { + let c = cards.kids[i] + i = i + 1 + sb_puts(b, `
{js_get(c,"icon")}

{js_get(c,"title")}

{js_get(c,"html")}

`) + } + return sb_str(b) +} +function build_phil_paras(phil: JVal) -> pointer { + let b = sb_new() + let ps = j_get(phil, "paras") + if ps.t != JV_ARR { return "" } + var i = 0 + while i < len(ps.kids) { sb_puts(b, `

{ps.kids[i].s}

`); i = i + 1 } + return sb_str(b) +} +function build_phil_stats(phil: JVal) -> pointer { + let b = sb_new() + let ss = j_get(phil, "stats") + if ss.t != JV_ARR { return "" } + var i = 0 + while i < len(ss.kids) { + let s = ss.kids[i] + i = i + 1 + sb_puts(b, `
{js_get(s,"big")}
{hesc(js_get(s,"lbl"))}
`) + } + return sb_str(b) +} +function build_steps(start: JVal) -> pointer { + let b = sb_new() + let st = j_get(start, "steps") + if st.t != JV_ARR { return "" } + var i = 0 + while i < len(st.kids) { + let s = st.kids[i] + sb_puts(b, `
{string(i + 1)}

{js_get(s,"title")}

{js_get(s,"html")}

`) + i = i + 1 + } + return sb_str(b) +} +function build_term(start: JVal) -> pointer { + let b = sb_new() + let tm = j_get(start, "terminal") + if tm.t != JV_ARR { return "" } + var i = 0 + while i < len(tm.kids) { + let t = tm.kids[i] + i = i + 1 + let bl = j_get(t, "blank") + if bl.t == JV_BOOL and bl.b == 1 { sb_putc(b, 10) } + else if j_has(t, "comment") { sb_puts(b, `# {hesc(js_get(t,"comment"))}\n`) } + else if j_has(t, "cmd") { sb_puts(b, `$ {hesc(js_get(t,"cmd"))}\n`) } + else if j_has(t, "out") { sb_puts(b, `{hesc(js_get(t,"out"))}\n`) } + } + return sb_str(b) +} +function build_eds(ed: JVal) -> pointer { + let b = sb_new() + let ls = j_get(ed, "list") + if ls.t != JV_ARR { return "" } + var i = 0 + while i < len(ls.kids) { sb_puts(b, `
◆ {hesc(ls.kids[i].s)}
`); i = i + 1 } + return sb_str(b) +} +function build_footlinks(cfg: JVal) -> pointer { + let b = sb_new() + let nl = j_get(cfg, "nav_links") + if nl.t == JV_ARR { + var i = 0 + while i < len(nl.kids) { + let n = nl.kids[i] + i = i + 1 + sb_puts(b, `{hesc(js_get(n,"label"))}`) + } + } + sb_puts(b, `Source ↗`) + return sb_str(b) +} +function build_samples(cfg: JVal) -> pointer { + let sc = j_get(cfg, "showcase") + let sm = j_get(sc, "samples") + let arr = jva() + if sm.t == JV_ARR { + var i = 0 + while i < len(sm.kids) { + let s = sm.kids[i] + i = i + 1 + let o = jvo() + jvput(o, "name", jvs(js_get(s, "name"))) + jvput(o, "label", jvs(js_get(s, "label"))) + jvput(o, "note", jvs(js_get(s, "note"))) + jvput(o, "code", jvs(read_snippet(js_get(s, "file")))) + jvpush(arr, o) + } + } + return json_dumps(arr, false, true) +} + +function render_index(b: Sb, cfg: JVal) -> void { + let hero = j_get(cfg, "hero") + let meta = j_get(cfg, "meta") + let feats = j_get(cfg, "features") + let sc = j_get(cfg, "showcase") + let phil = j_get(cfg, "philosophy") + let start = j_get(cfg, "start") + let ed = j_get(cfg, "editors") + let pcta = j_get(hero, "primary_cta") + let scta = j_get(hero, "secondary_cta") + let mk = new []pointer + let mv = new []pointer + put(mk, mv, "css", read_asset("site.css")) + put(mk, mv, "brand", hesc(js_get(cfg, "brand"))) + put(mk, mv, "tagline", hesc(js_get(cfg, "tagline"))) + put(mk, mv, "repo", js_get(cfg, "repo_url")) + put(mk, mv, "title", hesc(js_get(meta, "title"))) + put(mk, mv, "desc", hescattr(js_get(meta, "description"))) + put(mk, mv, "ogt", hesc(js_get(meta, "og_title"))) + put(mk, mv, "ogd", hescattr(js_get(meta, "og_description"))) + let nb = sb_new() + nav_html(nb, j_get(cfg, "nav_links"), "api.html") + put(mk, mv, "navlinks", sb_str(nb)) + put(mk, mv, "pill", hesc(js_get(hero, "pill"))) + put(mk, mv, "title_pre", hesc(js_get(hero, "title_pre"))) + put(mk, mv, "title_accent", hesc(js_get(hero, "title_accent"))) + put(mk, mv, "lead", js_get(hero, "lead")) + put(mk, mv, "pcta_h", js_get(pcta, "href")) + put(mk, mv, "pcta_l", hesc(js_get(pcta, "label"))) + put(mk, mv, "scta_h", js_get(scta, "href")) + put(mk, mv, "scta_l", hesc(js_get(scta, "label"))) + put(mk, mv, "hero_name", hesc(js_get(hero, "snippet_name"))) + put(mk, mv, "hero_code", hesc(read_snippet(js_get(hero, "snippet")))) + put(mk, mv, "stages", build_stages(hero)) + put(mk, mv, "pipenote", js_get(hero, "pipeline_note")) + put(mk, mv, "feat_kicker", hesc(js_get(feats, "kicker"))) + put(mk, mv, "feat_title", hesc(js_get(feats, "title"))) + put(mk, mv, "feat_intro", js_get(feats, "intro")) + put(mk, mv, "feats", build_feats(feats)) + put(mk, mv, "sc_kicker", hesc(js_get(sc, "kicker"))) + put(mk, mv, "sc_title", hesc(js_get(sc, "title"))) + put(mk, mv, "sc_intro", js_get(sc, "intro")) + put(mk, mv, "phil_kicker", hesc(js_get(phil, "kicker"))) + put(mk, mv, "phil_title", hesc(js_get(phil, "title"))) + put(mk, mv, "phil_paras", build_phil_paras(phil)) + put(mk, mv, "phil_stats", build_phil_stats(phil)) + put(mk, mv, "start_kicker", hesc(js_get(start, "kicker"))) + put(mk, mv, "start_title", hesc(js_get(start, "title"))) + put(mk, mv, "start_intro", hesc(js_get(start, "intro"))) + put(mk, mv, "steps", build_steps(start)) + put(mk, mv, "term_name", hesc(js_get(start, "terminal_name"))) + put(mk, mv, "term", build_term(start)) + put(mk, mv, "ed_kicker", hesc(js_get(ed, "kicker"))) + put(mk, mv, "ed_title", hesc(js_get(ed, "title"))) + put(mk, mv, "ed_intro", js_get(ed, "intro")) + put(mk, mv, "eds", build_eds(ed)) + put(mk, mv, "ed_note", js_get(ed, "note")) + put(mk, mv, "footlinks", build_footlinks(cfg)) + put(mk, mv, "samples", build_samples(cfg)) + sb_puts(b, pct(read_asset("index.tmpl.html"), mk, mv)) +} + +function render_highlighter(sym: JVal) -> pointer { + let tmpl = read_asset("ludic-highlight.tmpl.js") + let payload = json_dumps(sym, false, false) + let p = s_index(tmpl, "/*__SYMBOLS__*/{}", 0) + if p < 0 { return tmpl } + let b = sb_new() + var i = 0 + while i < p { sb_putc(b, tmpl[i]); i = i + 1 } + sb_puts(b, payload) + var j = p + slen("/*__SYMBOLS__*/{}") + let n = slen(tmpl) + while j < n { sb_putc(b, tmpl[j]); j = j + 1 } + return sb_str(b) +} + +# ---- the command ------------------------------------------------------------ +function cmd_docs_gen() -> int { + var out = "build/pages" + var i = 2 + while i < arg_count() { + let a = arg(i) + if a == "--out" { + i = i + 1 + if i < arg_count() { out = arg(i) } + } + i = i + 1 + } + run(`mkdir -p {out}`) + palette_init() + let sections = load_sections() + let syms = build_symbols(sections) + let cfgtext = read_file("docs/site/site.json") + if cfgtext == null { err("cannot read docs/site/site.json\n"); return 1 } + let cfg = json_parse(cfgtext) + var npages = 0 + var si = 0 + while si < len(sections) { + let s = sections[si] + si = si + 1 + if s.cat == "colors" { continue } + var ei = 0 + while ei < len(s.entries) { + let e = s.entries[ei] + ei = ei + 1 + let pb = sb_new() + render_item(pb, e, s, cfg, syms.items) + write_file(`{out}/{e.id}.html`, sb_str(pb)) + npages = npages + 1 + } + } + si = 0 + while si < len(sections) { + let s = sections[si] + si = si + 1 + if slen(s.ns_name) > 0 { + let pb = sb_new() + render_ns(pb, s.ns_name, s, cfg) + write_file(`{out}/ns-{lower_ascii(s.ns_name)}.html`, sb_str(pb)) + npages = npages + 1 + } + } + let cb = sb_new() + render_color(cb, cfg) + write_file(`{out}/ns-color.html`, sb_str(cb)) + npages = npages + 1 + let ab = sb_new() + render_api(ab, sections, cfg) + write_file(`{out}/api.html`, sb_str(ab)) + let ib = sb_new() + render_index(ib, cfg) + write_file(`{out}/index.html`, sb_str(ib)) + write_file(`{out}/ludic-highlight.js`, render_highlighter(syms.sym)) + let root = jvo() + jvput(root, "items", syms.items) + jvput(root, "highlight", syms.sym) + write_file(`{out}/symbols.json`, json_dumps(root, true, false)) + write_file(`{out}/.nojekyll`, "") + print(`docs generated -> {out}`) + print(` pages: {string(npages)}`) + return 0 +} diff --git a/tools/x/main.ludic b/tools/x/main.ludic index 348a282d..39358ed3 100644 --- a/tools/x/main.ludic +++ b/tools/x/main.ludic @@ -19,6 +19,9 @@ program X { import "test.ludic" import "json.ludic" import "checks.ludic" + import "docgen.ludic" + import "docgen_gen.ludic" + import "docgen_check.ludic" import "release.ludic" function usage() -> void { @@ -43,6 +46,9 @@ program X { print(" x check-impl every implemented feature has a docs/language page") print(" x check-vocabulary the vocabulary is in sync across grammar / lexer / header / parser") print(" x lint-asset validate one editor .json / .xml asset") + print(" x docs-gen [--out DIR] generate the documentation site (default build/pages)") + print(" x docs-check [DIR] coverage/integrity guard over a generated docs site") + print(" x docs-palette regenerate emit_color.ludic + palette.json from the palette table") print("") print("release:") print(" x version print the toolchain version (ludicc --version)") @@ -80,6 +86,9 @@ program X { if (cmd == "check-impl") { exit(cmd_check_impl()) } if (cmd == "check-vocabulary") { exit(cmd_check_vocab()) } if (cmd == "lint-asset") { exit(cmd_lint_asset()) } + if (cmd == "docs-palette") { exit(cmd_docs_palette()) } + if (cmd == "docs-gen") { exit(cmd_docs_gen()) } + if (cmd == "docs-check") { exit(cmd_docs_check()) } if (cmd == "golden") { exit(cmd_golden()) } if (cmd == "bootstrap") { exit(cmd_bootstrap()) } if (cmd == "bootstrap-cfree") { exit(cmd_bootstrap_cfree()) } diff --git a/tools/x/test.ludic b/tools/x/test.ludic index e327a64a..d534d213 100644 --- a/tools/x/test.ludic +++ b/tools/x/test.ludic @@ -186,5 +186,20 @@ function cmd_test() -> int { if (rc == 42) { ok("ludic app.ludic -> compiles, runs, forwards exit code") } else { bad2("ludic run: expected exit 42", `got {string(rc)}`) } + # docs site generator + guard, in Ludic (no Python). docs-gen emits the whole + # pages payload; docs-check is its coverage/integrity guard; docs-palette is + # the named-colour source of truth (its output is the tracked emit_color.ludic + # + palette.json, so a clean git diff proves it stayed byte-identical). + print("== docs site (Ludic generator, no Python) ==") + if shq("bin/x docs-gen --out /tmp/x_docs > /tmp/x_docs.out 2>&1") { + ok("x docs-gen -> the pages-branch payload") + } else { bad2("x docs-gen", capture_line("tail -1 /tmp/x_docs.out")) } + if shq("bin/x docs-check /tmp/x_docs > /tmp/x_docs_chk.out 2>&1") { + ok("x docs-check passes on the generated site") + } else { bad2("x docs-check", capture_line("tail -1 /tmp/x_docs_chk.out")) } + if shq("bin/x docs-palette > /dev/null 2>&1") and shq("git diff --quiet selfhost/backend/stdlib/emit_color.ludic docs/language/colors/palette.json") { + ok("x docs-palette regenerates emit_color.ludic + palette.json byte-identically") + } else { bad("x docs-palette drifted (git diff not empty)") } + return report() }