feat(tooling): port the docgen site generator to Ludic (no Python) (#41)
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 <noreply@anthropic.com>
This commit is contained in:
parent
109d8c5b4f
commit
9ed0070039
13 changed files with 2027 additions and 946 deletions
|
|
@ -14,7 +14,7 @@ Closes #
|
||||||
- [ ] `ludic-fmt` leaves the touched files unchanged (2-space, LF, UTF-8).
|
- [ ] `ludic-fmt` leaves the touched files unchanged (2-space, LF, UTF-8).
|
||||||
- [ ] New/changed stdlib symbols are documented under `docs/language/**` and
|
- [ ] New/changed stdlib symbols are documented under `docs/language/**` and
|
||||||
registered in `tools/docgen/inventory.json`
|
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).
|
- [ ] Commits follow [Conventional Commits](https://www.conventionalcommits.org).
|
||||||
- [ ] No new C / Python / JS in tooling (Ludic only), and no generated
|
- [ ] No new C / Python / JS in tooling (Ludic only), and no generated
|
||||||
artifacts committed outside `build/` / `bin/`.
|
artifacts committed outside `build/` / `bin/`.
|
||||||
|
|
|
||||||
|
|
@ -17,20 +17,20 @@ jobs:
|
||||||
# advertises `docker`, not the GitHub-ism `ubuntu-latest`.
|
# advertises `docker`, not the GitHub-ism `ubuntu-latest`.
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
# Reuse the runner's own base image (Debian bookworm with git + node already
|
# 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
|
# 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) and
|
# (LLVM 16 — the IR uses opaque pointers, so clang 15+ is required). The docs
|
||||||
# python3 for the docs/vocabulary checks. A prebuilt image with these baked
|
# generator and its guards are now Ludic, so the job carries no Python. A
|
||||||
# in is the obvious future speed-up (see issue #33's packaging work).
|
# prebuilt image with clang baked in is the obvious future speed-up (see
|
||||||
|
# issue #33's packaging work).
|
||||||
container: node:20-bookworm
|
container: node:20-bookworm
|
||||||
steps:
|
steps:
|
||||||
- name: Install clang-16 and python3
|
- name: Install clang-16
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
apt-get update -qq
|
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
|
clang-16 --version | head -1
|
||||||
python3 --version
|
|
||||||
|
|
||||||
- name: Check out the triggering commit
|
- name: Check out the triggering commit
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -75,11 +75,12 @@ jobs:
|
||||||
- name: Docs cover the implementation
|
- name: Docs cover the implementation
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
# check-impl / check-vocabulary are written in Ludic and run through
|
# The whole docs toolchain is written in Ludic and runs through x —
|
||||||
# x — no Python. (check-vocabulary also runs in `x test-tools`.) The
|
# no Python anywhere. check-impl / check-vocabulary / check-docs guard
|
||||||
# site generator (gen.py) is still Python; its port is tracked.
|
# 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-impl
|
||||||
bin/x check-vocabulary
|
bin/x check-vocabulary
|
||||||
bin/x check-docs
|
bin/x check-docs
|
||||||
python3 tools/docgen/gen.py --out build/pages
|
bin/x docs-gen --out build/pages
|
||||||
python3 tools/docgen/check.py build/pages
|
bin/x docs-check build/pages
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ on:
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
- 'tools/docgen/**'
|
- 'tools/docgen/**'
|
||||||
|
- 'tools/x/**'
|
||||||
- '.forgejo/workflows/docs.yml'
|
- '.forgejo/workflows/docs.yml'
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
|
|
@ -23,11 +24,20 @@ jobs:
|
||||||
# GitHub-ism this runner does not register, so a job requesting it sits in
|
# GitHub-ism this runner does not register, so a job requesting it sits in
|
||||||
# "Waiting" forever with "no online runner found matching this label".
|
# "Waiting" forever with "no online runner found matching this label".
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
# Run in a Python image: the generator is pure-Python stdlib, and this image
|
# The generator is now Ludic, so this builds the toolchain from its IR seed
|
||||||
# already has git for the clone + publish. No node actions are used, so the
|
# (clang assembles the seed into bin/ludicc, which compiles bin/x) exactly
|
||||||
# job never depends on the runner's base image having python installed.
|
# like the ci workflow, then runs `x docs-gen`. node:20-bookworm carries git
|
||||||
container: python:3.12
|
# for the clone + publish; clang-16 is the only extra the bootstrap needs.
|
||||||
|
container: node:20-bookworm
|
||||||
steps:
|
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
|
- name: Generate the documentation site
|
||||||
env:
|
env:
|
||||||
SOURCE_REF: ${{ github.ref_name }}
|
SOURCE_REF: ${{ github.ref_name }}
|
||||||
|
|
@ -36,9 +46,19 @@ jobs:
|
||||||
git config --global --add safe.directory '*'
|
git config --global --add safe.directory '*'
|
||||||
git clone --depth 1 --branch "${SOURCE_REF:-main}" \
|
git clone --depth 1 --branch "${SOURCE_REF:-main}" \
|
||||||
https://git.workshopsoft.io/workshopsoft/ludic.git src
|
https://git.workshopsoft.io/workshopsoft/ludic.git src
|
||||||
python3 --version
|
cd src
|
||||||
python3 src/tools/docgen/gen.py --out public
|
# The toolchain is macOS-first; on this Linux runner it links against a
|
||||||
python3 src/tools/docgen/check.py public
|
# 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 ---"
|
echo "--- generated files ---"
|
||||||
ls -la public
|
ls -la public
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
# no allocation, identical codegen to writing the hex by hand. Unknown names are
|
# no allocation, identical codegen to writing the hex by hand. Unknown names are
|
||||||
# a compile error (color_lookup returns -1, which emit_expr reports).
|
# a compile error (color_lookup returns -1, which emit_expr reports).
|
||||||
#
|
#
|
||||||
# GENERATED by scratchpad/palette.py from the single source-of-truth palette.
|
# GENERATED by `x docs-palette` from the single source-of-truth palette table
|
||||||
# Edit the palette there and regenerate; do not hand-edit this file.
|
# in tools/x/docgen.ludic. Edit the palette there and regenerate; do not hand-edit.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
function color_lookup(name: pointer) -> int {
|
function color_lookup(name: pointer) -> int {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ docs/
|
||||||
language/<category>/<id>.md one file per symbol — keyword, type, phase,
|
language/<category>/<id>.md one file per symbol — keyword, type, phase,
|
||||||
builtin, namespace method, operator, annotation
|
builtin, namespace method, operator, annotation
|
||||||
language/<category>/_section.md section title + blurb + order
|
language/<category>/_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/site.json landing-page messaging (hero, features, …)
|
||||||
site/snippets/*.ludic the code shown on the landing page (real programs)
|
site/snippets/*.ludic the code shown on the landing page (real programs)
|
||||||
tools/docgen/inventory.json the authoritative symbol set the coverage guard checks
|
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
|
## 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
|
```bash
|
||||||
python3 tools/docgen/gen.py --out build/pages # generate the whole site
|
bin/x docs-gen --out build/pages # generate the whole site
|
||||||
python3 tools/docgen/check.py build/pages # coverage + duplicate-token + link guard
|
bin/x docs-check build/pages # coverage + duplicate-token + link guard
|
||||||
bin/x check-docs # parse every ```ludic doc fence (Ludic; no Python)
|
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
|
`docs-check` fails CI if any symbol in `inventory.json` lacks a page, if a token
|
||||||
documented on two pages, or if a highlighter link points at a missing page — so
|
is documented on two pages, or if a highlighter link points at a missing page —
|
||||||
"every symbol is documented, autogenerated each time" is enforced. `validate.py`
|
so "every symbol is documented, autogenerated each time" is enforced. It needs a
|
||||||
needs a built `bin/ludicc`; the deploy CI is Python-only, so run it locally or in
|
built `bin/x` (bootstrapped from the IR seed with clang alone).
|
||||||
a toolchain-enabled job.
|
|
||||||
|
|
||||||
No third-party dependencies — Python standard library only.
|
|
||||||
|
|
||||||
## Publish
|
## Publish
|
||||||
|
|
||||||
`.forgejo/workflows/docs.yml` runs `gen.py` + `check.py` on every push to `main`
|
`.forgejo/workflows/docs.yml` bootstraps the toolchain from the IR seed and runs
|
||||||
that touches `docs/**` or `tools/docgen/**`, and publishes the result to the
|
`x docs-gen` + `x docs-check` on every push to `main` that touches `docs/**`,
|
||||||
`pages` branch root. `index.html` + `.nojekyll` always stay at the root.
|
`tools/docgen/**` or `tools/x/**`, and publishes the result to the `pages` branch
|
||||||
|
root. `index.html` + `.nojekyll` always stay at the root.
|
||||||
|
|
||||||
## Colors
|
## Colors
|
||||||
|
|
||||||
`palette.json` is generated by `tools/docgen/palette.py` from a single `PALETTE`
|
`palette.json` and `selfhost/backend/stdlib/emit_color.ludic` are both generated
|
||||||
table, which also generates `selfhost/emit_color.ludic`. Regenerate colors there.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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")))
|
|
||||||
|
|
@ -1,462 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
"""gen.py — the Ludic documentation generator (v2).
|
|
||||||
|
|
||||||
Single source of truth:
|
|
||||||
docs/language/<category>/<id>.md one file per symbol (front-matter + body)
|
|
||||||
docs/language/<category>/_section.md section title/blurb/order
|
|
||||||
docs/language/colors/palette.json the named-color palette
|
|
||||||
docs/site/site.json landing-page messaging
|
|
||||||
docs/site/snippets/*.ludic real programs shown on the landing page
|
|
||||||
|
|
||||||
Outputs (into --out, default build/pages) — the whole pages-branch payload:
|
|
||||||
index.html the landing page
|
|
||||||
api.html the reference index, with fuzzy search
|
|
||||||
<id>.html ONE page per symbol (kw-handler.html, screen-clear.html…)
|
|
||||||
ns-<name>.html one overview page per namespace (Screen/Color/Input/…)
|
|
||||||
ludic-highlight.js the highlighter — its symbol tables, tips, per-item link
|
|
||||||
targets, per-parameter anchors and hover-card data all
|
|
||||||
generated from the sources above
|
|
||||||
symbols.json the machine-readable index (drives search + hover cards)
|
|
||||||
.nojekyll
|
|
||||||
|
|
||||||
Every keyword/type/phase/builtin/namespace-method/annotation/operator/color is
|
|
||||||
addressable by its own page; named parameters and named colors are addressable
|
|
||||||
by anchor. Python standard library only.
|
|
||||||
"""
|
|
||||||
import json, os, html, re, argparse
|
|
||||||
|
|
||||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
||||||
ASSETS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
|
|
||||||
LANG = os.path.join(ROOT, "docs", "language")
|
|
||||||
SITE = os.path.join(ROOT, "docs", "site")
|
|
||||||
HEADER_OFFSET = 84 # sticky nav height; used for scroll-margin
|
|
||||||
|
|
||||||
def esc(s): return html.escape(s or "", quote=False)
|
|
||||||
def escattr(s): return html.escape(s or "", quote=True)
|
|
||||||
|
|
||||||
def fill(tmpl, mapping):
|
|
||||||
for k, v in mapping.items():
|
|
||||||
tmpl = tmpl.replace("@@" + k + "@@", str(v))
|
|
||||||
return tmpl
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# parsing
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
FENCE = re.compile(r"```ludic\n(.*?)\n```", re.S)
|
|
||||||
PARAM = re.compile(r"^-\s*`?(\w+)`?\s*[—-]+\s*(.*)$")
|
|
||||||
|
|
||||||
def parse_doc(path):
|
|
||||||
text = open(path, encoding="utf-8").read()
|
|
||||||
meta, body = {}, text
|
|
||||||
if text.startswith("---"):
|
|
||||||
end = text.find("\n---", 3)
|
|
||||||
if end != -1:
|
|
||||||
for line in text[3:end].strip("\n").split("\n"):
|
|
||||||
if line.strip() and ":" in line:
|
|
||||||
k, v = line.split(":", 1)
|
|
||||||
meta[k.strip()] = v.strip()
|
|
||||||
body = text[end + 4:].lstrip("\n")
|
|
||||||
return meta, body
|
|
||||||
|
|
||||||
def codeify(s):
|
|
||||||
return re.sub(r"`([^`]+)`", r"<code>\1</code>", s)
|
|
||||||
|
|
||||||
def split_body(body):
|
|
||||||
examples = FENCE.findall(body)
|
|
||||||
body = FENCE.sub("", body)
|
|
||||||
# pull out a "Parameters:" block
|
|
||||||
params, keep = [], []
|
|
||||||
lines = body.split("\n")
|
|
||||||
i, n = 0, len(lines)
|
|
||||||
while i < n:
|
|
||||||
if lines[i].strip().lower().startswith("parameters:"):
|
|
||||||
i += 1
|
|
||||||
while i < n and lines[i].strip():
|
|
||||||
m = PARAM.match(lines[i].strip())
|
|
||||||
if m:
|
|
||||||
params.append({"name": m.group(1), "desc": codeify(m.group(2).strip())})
|
|
||||||
i += 1
|
|
||||||
else:
|
|
||||||
keep.append(lines[i]); i += 1
|
|
||||||
desc = "\n".join(keep).strip()
|
|
||||||
paras = [codeify(p.strip()) for p in re.split(r"\n\s*\n", desc) if p.strip()]
|
|
||||||
return "</p><p>".join(paras), examples, params
|
|
||||||
|
|
||||||
def load_sections():
|
|
||||||
sections = []
|
|
||||||
for cat in sorted(os.listdir(LANG)):
|
|
||||||
cdir = os.path.join(LANG, cat)
|
|
||||||
if not os.path.isdir(cdir):
|
|
||||||
continue
|
|
||||||
smeta, sblurb = {}, ""
|
|
||||||
sp = os.path.join(cdir, "_section.md")
|
|
||||||
if os.path.exists(sp):
|
|
||||||
smeta, sbody = parse_doc(sp)
|
|
||||||
sblurb = codeify(sbody.strip())
|
|
||||||
entries = []
|
|
||||||
for fn in sorted(os.listdir(cdir)):
|
|
||||||
if not fn.endswith(".md") or fn == "_section.md":
|
|
||||||
continue
|
|
||||||
meta, body = parse_doc(os.path.join(cdir, fn))
|
|
||||||
desc, examples, params = split_body(body)
|
|
||||||
meta["desc_html"] = desc
|
|
||||||
meta["examples"] = examples
|
|
||||||
meta["params"] = params
|
|
||||||
meta["tokens_list"] = meta.get("tokens", "").split()
|
|
||||||
meta["related_list"] = meta.get("related", "").split()
|
|
||||||
entries.append(meta)
|
|
||||||
entries.sort(key=lambda e: (int(e.get("order", 999)), e.get("name", "")))
|
|
||||||
# A namespace section is discovered, not hardcoded: it is any section
|
|
||||||
# whose symbols are `namespace-method`s, named by their `ns:` field. So
|
|
||||||
# adding a namespace is just adding its docs dir — gen.py needs no edit.
|
|
||||||
ns_name = next((e["ns"] for e in entries
|
|
||||||
if e.get("kind") == "namespace-method" and e.get("ns")), None)
|
|
||||||
sections.append({"id": smeta.get("id", cat), "title": smeta.get("title", cat.title()),
|
|
||||||
"order": int(smeta.get("order", 999)), "blurb": sblurb,
|
|
||||||
"cat": cat, "entries": entries, "ns_name": ns_name})
|
|
||||||
sections.sort(key=lambda s: (s["order"], s["title"]))
|
|
||||||
return sections
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# symbol model for the highlighter + search + cards
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
def ns_page(nsname): return "ns-" + nsname.lower()
|
|
||||||
|
|
||||||
def build_symbols(sections, palette):
|
|
||||||
sym = {"keywords": {}, "types": {}, "phases": {}, "builtins": {},
|
|
||||||
"nsmethods": {}, "annotations": {}, "namespaces": {}, "tips": {},
|
|
||||||
"cards": {}, "colors_page": "ns-color"}
|
|
||||||
items = {} # id -> full record for search
|
|
||||||
for s in sections:
|
|
||||||
for e in s["entries"]:
|
|
||||||
eid = e["id"]; kind = e.get("kind", ""); tip = e.get("tip", "")
|
|
||||||
page = eid + ".html"
|
|
||||||
items[eid] = {"id": eid, "name": e.get("name", ""), "sig": e.get("sig", ""),
|
|
||||||
"kind": kind, "category": s["id"], "section": s["title"],
|
|
||||||
"tip": tip, "page": page}
|
|
||||||
sym["cards"][eid] = {"name": e.get("name",""), "sig": e.get("sig",""),
|
|
||||||
"tip": tip, "kind": kind, "page": page, "section": s["title"]}
|
|
||||||
for tok in e["tokens_list"]:
|
|
||||||
if kind == "keyword": sym["keywords"][tok] = eid
|
|
||||||
elif kind == "type": sym["types"][tok] = eid
|
|
||||||
elif kind == "phase": sym["phases"][tok] = eid
|
|
||||||
elif kind == "builtin":
|
|
||||||
sym["builtins"][tok] = {"id": eid, "params": [p["name"] for p in e["params"]]}
|
|
||||||
elif kind == "namespace-method":
|
|
||||||
sym["nsmethods"][tok] = {"id": eid, "params": [p["name"] for p in e["params"]]}
|
|
||||||
elif kind == "annotation":
|
|
||||||
sym["annotations"][tok] = eid
|
|
||||||
if tip: sym["tips"][tok] = tip
|
|
||||||
# namespaces are discovered from the ns methods present, plus Color; each
|
|
||||||
# namespace's tip falls back to its section blurb so no hardcoded list of
|
|
||||||
# namespaces is needed here when a new one is added.
|
|
||||||
ns_blurb = {s["ns_name"]: plain_text(s["blurb"]) for s in sections if s.get("ns_name")}
|
|
||||||
nsnames = set()
|
|
||||||
for tok in sym["nsmethods"]:
|
|
||||||
if "." in tok: nsnames.add(tok.split(".", 1)[0])
|
|
||||||
nsnames.add("Color")
|
|
||||||
for nn in sorted(nsnames):
|
|
||||||
sym["namespaces"][nn] = ns_page(nn)
|
|
||||||
sym["cards"][ns_page(nn)] = {"name": nn, "sig": nn + ".*", "kind": "namespace",
|
|
||||||
"tip": ns_tip(nn, ns_blurb), "page": ns_page(nn) + ".html",
|
|
||||||
"section": "Namespaces"}
|
|
||||||
return sym, items
|
|
||||||
|
|
||||||
def plain_text(s, limit=160):
|
|
||||||
"""The first sentence of a blurb, stripped of tags — for use as a one-line tip."""
|
|
||||||
t = re.sub(r"<[^>]+>", "", s or "").strip()
|
|
||||||
m = re.match(r"(.+?[.!?])(\s|$)", t)
|
|
||||||
if m: t = m.group(1)
|
|
||||||
return t if len(t) <= limit else t[:limit].rsplit(" ", 1)[0] + "…"
|
|
||||||
|
|
||||||
def ns_tip(nsname, ns_blurb):
|
|
||||||
"""A namespace's tip: an optional curated override, else its section blurb."""
|
|
||||||
return NS_TIP.get(nsname) or ns_blurb.get(nsname, "")
|
|
||||||
|
|
||||||
# Optional short overrides for a namespace's one-line tip. A namespace NOT
|
|
||||||
# listed here falls back to the first sentence of its section blurb (see
|
|
||||||
# ns_tip), so a newly added namespace needs no edit here. Color has no docs
|
|
||||||
# section of its own, so its override is required.
|
|
||||||
NS_TIP = {"Color": "The named color palette."}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# shared chrome
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
def nav_html(cfg, links=None, cta_href="api.html"):
|
|
||||||
out = []
|
|
||||||
for n in (links if links is not None else cfg["nav_links"]):
|
|
||||||
cls = 'class="nav-cta" ' if n.get("href") == cta_href else ""
|
|
||||||
out.append('<a %shref="%s">%s</a>' % (cls, n["href"], esc(n["label"])))
|
|
||||||
return "".join(out)
|
|
||||||
|
|
||||||
def head_html(title, desc, css_file):
|
|
||||||
css = open(os.path.join(ASSETS, css_file)).read()
|
|
||||||
return fill("""<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>@@TITLE@@</title>
|
|
||||||
<meta name="description" content="@@DESC@@">
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
@@CSS@@
|
|
||||||
</style>
|
|
||||||
</head>""", {"TITLE": esc(title), "DESC": escattr(desc), "CSS": css})
|
|
||||||
|
|
||||||
def topbar(cfg):
|
|
||||||
refnav = nav_html(cfg, links=cfg.get("ref_nav_links", cfg["nav_links"]), cta_href=None) \
|
|
||||||
+ '<a class="nav-cta" href="%s">Source ↗</a>' % cfg["repo_url"]
|
|
||||||
return ('<header class="nav"><div class="wrap nav-in">'
|
|
||||||
'<a class="brand" href="index.html"><span class="logo">L</span> %s</a>'
|
|
||||||
'<button class="nav-toggle" aria-label="Toggle menu" aria-expanded="false">☰</button>'
|
|
||||||
'<nav class="nav-links">%s</nav></div></header>') % (esc(cfg["brand"]), refnav)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# per-item page
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
def render_params(e):
|
|
||||||
if not e["params"]:
|
|
||||||
return ""
|
|
||||||
rows = "".join(
|
|
||||||
'<div class="param" id="param-%s"><code class="pname">%s</code>'
|
|
||||||
'<span class="pdesc">%s</span></div>' % (p["name"], esc(p["name"]), p["desc"])
|
|
||||||
for p in e["params"])
|
|
||||||
return '<div class="params"><h2>Parameters</h2>%s</div>' % rows
|
|
||||||
|
|
||||||
def render_examples(e):
|
|
||||||
if not e["examples"]:
|
|
||||||
return ""
|
|
||||||
blocks = "".join('<pre data-lang="ludic">%s</pre>' % esc(code) for code in e["examples"])
|
|
||||||
return '<div class="examples"><h2>Example</h2>%s</div>' % blocks
|
|
||||||
|
|
||||||
def render_related(e, items):
|
|
||||||
rel = [r for r in e["related_list"] if r in items]
|
|
||||||
if not rel:
|
|
||||||
return ""
|
|
||||||
links = "".join('<a class="rel" href="%s.html">%s</a>' % (r, esc(items[r]["name"])) for r in rel)
|
|
||||||
return '<div class="related"><h2>Related</h2><div class="rel-row">%s</div></div>' % links
|
|
||||||
|
|
||||||
def item_page(e, section, cfg, items):
|
|
||||||
kindlabel = {"namespace-method": "method", "builtin": "function"}.get(e.get("kind",""), e.get("kind",""))
|
|
||||||
body = fill("""@@TOPBAR@@
|
|
||||||
<main class="wrap item">
|
|
||||||
<div class="crumbs"><a href="api.html">API Reference</a> <span>›</span> <a href="@@SECHREF@@">@@SECTION@@</a> <span>›</span> <span class="here">@@NAME@@</span></div>
|
|
||||||
<div class="item-head">
|
|
||||||
<span class="kind-badge kind-@@KIND@@">@@KINDLABEL@@</span>
|
|
||||||
<h1 id="top">@@NAME@@</h1>
|
|
||||||
</div>
|
|
||||||
<code class="sig">@@SIG@@</code>
|
|
||||||
<div class="desc"><p>@@DESC@@</p></div>
|
|
||||||
@@PARAMS@@
|
|
||||||
@@EXAMPLES@@
|
|
||||||
@@RELATED@@
|
|
||||||
<a class="back" href="api.html">← All symbols</a>
|
|
||||||
</main>
|
|
||||||
<script src="ludic-highlight.js"></script>
|
|
||||||
<script>Ludic.highlightAll(); Ludic.installCards(); Ludic.flashTarget();</script>
|
|
||||||
</body></html>""", {
|
|
||||||
"TOPBAR": topbar(cfg),
|
|
||||||
"SECHREF": ns_page(section["ns_name"]) + ".html" if section.get("ns_name") else "api.html#" + section["id"],
|
|
||||||
"SECTION": esc(section["title"]),
|
|
||||||
"NAME": esc(e.get("name","")),
|
|
||||||
"KIND": e.get("kind","").replace("namespace-method","method"),
|
|
||||||
"KINDLABEL": esc(kindlabel),
|
|
||||||
"SIG": esc(e.get("sig","")),
|
|
||||||
"DESC": e.get("desc_html","") or esc(e.get("tip","")),
|
|
||||||
"PARAMS": render_params(e),
|
|
||||||
"EXAMPLES": render_examples(e),
|
|
||||||
"RELATED": render_related(e, items),
|
|
||||||
})
|
|
||||||
return head_html(e.get("name","") + " — Ludic", e.get("tip",""), "item.css") + "\n<body>\n" + body
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# namespace overview pages
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
def ns_overview_page(nsname, section, cfg):
|
|
||||||
rows = ""
|
|
||||||
for e in section["entries"]:
|
|
||||||
rows += ('<a class="ns-method" href="%s.html"><code class="nm-sig">%s</code>'
|
|
||||||
'<span class="nm-tip">%s</span></a>') % (e["id"], esc(e.get("sig","")), esc(e.get("tip","")))
|
|
||||||
body = fill("""@@TOPBAR@@
|
|
||||||
<main class="wrap item">
|
|
||||||
<div class="crumbs"><a href="api.html">API Reference</a> <span>›</span> <span class="here">@@NAME@@</span></div>
|
|
||||||
<div class="item-head"><span class="kind-badge kind-namespace">namespace</span><h1 id="top">@@NAME@@</h1></div>
|
|
||||||
<p class="ns-blurb">@@BLURB@@</p>
|
|
||||||
<div class="ns-methods">@@ROWS@@</div>
|
|
||||||
<a class="back" href="api.html">← All symbols</a>
|
|
||||||
</main>
|
|
||||||
<script src="ludic-highlight.js"></script>
|
|
||||||
<script>Ludic.installCards(); Ludic.flashTarget();</script>
|
|
||||||
</body></html>""", {"TOPBAR": topbar(cfg), "NAME": esc(nsname), "BLURB": section["blurb"], "ROWS": rows})
|
|
||||||
return head_html(nsname + " — Ludic", NS_TIP.get(nsname) or plain_text(section["blurb"]), "item.css") + "\n<body>\n" + body
|
|
||||||
|
|
||||||
def color_page(palette, cfg):
|
|
||||||
groups = ""
|
|
||||||
for grp in palette["groups"]:
|
|
||||||
sw = ""
|
|
||||||
for col in grp["colors"]:
|
|
||||||
h = col["hex"]; nm = col["name"]; aid = nm.lower()
|
|
||||||
sw += ('<div class="swatch" id="%s"><span class="chip" style="background:#%s"></span>'
|
|
||||||
'<span class="cname">Color.%s</span><span class="chex">#%s</span></div>'
|
|
||||||
) % (aid, h, esc(nm), h)
|
|
||||||
groups += '<div class="swatch-group"><h3>%s</h3><div class="swatch-row">%s</div></div>' % (esc(grp["name"]), sw)
|
|
||||||
body = fill("""@@TOPBAR@@
|
|
||||||
<main class="wrap item">
|
|
||||||
<div class="crumbs"><a href="api.html">API Reference</a> <span>›</span> <span class="here">Color</span></div>
|
|
||||||
<div class="item-head"><span class="kind-badge kind-namespace">namespace</span><h1 id="top">Color</h1></div>
|
|
||||||
<p class="ns-blurb">@@BLURB@@ Every <code>Color.Name</code> lowers to a plain <code>0xRRGGBB</code> integer at compile time — no runtime cost. @@COUNT@@ names are built in.</p>
|
|
||||||
@@GROUPS@@
|
|
||||||
<a class="back" href="api.html">← All symbols</a>
|
|
||||||
</main>
|
|
||||||
<script src="ludic-highlight.js"></script>
|
|
||||||
<script>Ludic.installCards(); Ludic.flashTarget();</script>
|
|
||||||
</body></html>""", {"TOPBAR": topbar(cfg), "BLURB": "The named color palette.",
|
|
||||||
"COUNT": palette["count"], "GROUPS": groups})
|
|
||||||
return head_html("Color — Ludic", "The Ludic named-color palette.", "item.css") + "\n<body>\n" + body
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# api index with fuzzy search
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
def api_index(sections, cfg):
|
|
||||||
cards = ""
|
|
||||||
for s in sections:
|
|
||||||
if s["cat"] == "colors":
|
|
||||||
cards += ('<section class="idx-sec" data-sec="%s"><h2 id="%s">%s</h2>'
|
|
||||||
'<p class="sec-blurb">%s</p><div class="idx-grid">'
|
|
||||||
'<a class="idx-item" href="ns-color.html" data-name="Color" data-tip="The named color palette.">'
|
|
||||||
'<code>Color</code><span>The named color palette (221 names).</span></a></div></section>'
|
|
||||||
) % (s["id"], s["id"], esc(s["title"]), s["blurb"])
|
|
||||||
continue
|
|
||||||
rows = ""
|
|
||||||
for e in s["entries"]:
|
|
||||||
rows += ('<a class="idx-item" href="%s.html" data-name="%s" data-tip="%s"><code>%s</code><span>%s</span></a>'
|
|
||||||
) % (e["id"], escattr(e.get("name","")), escattr(e.get("tip","")),
|
|
||||||
esc(e.get("name","")), esc(e.get("tip","")))
|
|
||||||
cards += ('<section class="idx-sec" data-sec="%s"><h2 id="%s">%s</h2>'
|
|
||||||
'<p class="sec-blurb">%s</p><div class="idx-grid">%s</div></section>'
|
|
||||||
) % (s["id"], s["id"], esc(s["title"]), s["blurb"], rows)
|
|
||||||
body = fill("""@@TOPBAR@@
|
|
||||||
<main class="wrap ref">
|
|
||||||
<div class="ref-intro">
|
|
||||||
<div class="kicker">Reference</div>
|
|
||||||
<h1>API Reference</h1>
|
|
||||||
<p>Every keyword, type, phase, builtin, namespace method, annotation and color in Ludic — each on its own page. Search, or browse by section. In any code sample across this site, hover a token for a summary and click to jump to its page.</p>
|
|
||||||
<input id="search" class="search" type="search" placeholder="Search symbols… (e.g. handler, fill_rectangle, @Sync)" autocomplete="off" autofocus>
|
|
||||||
<div id="noresults" class="noresults" hidden>No symbols match.</div>
|
|
||||||
</div>
|
|
||||||
@@CARDS@@
|
|
||||||
</main>
|
|
||||||
<script src="ludic-highlight.js"></script>
|
|
||||||
<script>Ludic.installCards(); Ludic.installSearch();</script>
|
|
||||||
</body></html>""", {"TOPBAR": topbar(cfg), "CARDS": cards})
|
|
||||||
return head_html("Ludic — API Reference", "The complete Ludic API Reference: every keyword, type, builtin, namespace, annotation and color, each on its own page, with fuzzy search.", "item.css") + "\n<body>\n" + body
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# landing page (data-driven, unchanged structure; highlighter now deep-links)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
def read_snippet(rel): return open(os.path.join(ROOT, rel), encoding="utf-8").read().rstrip("\n")
|
|
||||||
|
|
||||||
def render_index(cfg):
|
|
||||||
css = open(os.path.join(ASSETS, "site.css")).read()
|
|
||||||
hero = cfg["hero"]
|
|
||||||
navlinks = nav_html(cfg)
|
|
||||||
hero_code = esc(read_snippet(hero["snippet"]))
|
|
||||||
stages = ""
|
|
||||||
for st in hero["pipeline"]:
|
|
||||||
hl = " hl" if st == "ludicc" else ""
|
|
||||||
stages += '<span class="stage%s">%s</span>' % (hl, esc(st))
|
|
||||||
if st != hero["pipeline"][-1]: stages += '<span class="arrow">→</span>'
|
|
||||||
feats = "".join('<div class="feat reveal"><div class="ico">%s</div><h3>%s</h3><p>%s</p></div>'
|
|
||||||
% (c["icon"], c["title"], c["html"]) for c in cfg["features"]["cards"])
|
|
||||||
phil = cfg["philosophy"]
|
|
||||||
phil_paras = "".join("<p>%s</p>" % p for p in phil["paras"])
|
|
||||||
phil_stats = "".join('<div class="stat"><div class="big">%s</div><div class="lbl">%s</div></div>'
|
|
||||||
% (s["big"], esc(s["lbl"])) for s in phil["stats"])
|
|
||||||
start = cfg["start"]
|
|
||||||
steps = "".join('<div class="step reveal"><div class="n">%d</div><div><h4>%s</h4><p>%s</p></div></div>'
|
|
||||||
% (i, s["title"], s["html"]) for i, s in enumerate(start["steps"], 1))
|
|
||||||
term = ""
|
|
||||||
for t in start["terminal"]:
|
|
||||||
if t.get("blank"): term += "\n"
|
|
||||||
elif "comment" in t: term += '<span class="t-com"># %s</span>\n' % esc(t["comment"])
|
|
||||||
elif "cmd" in t: term += '<span class="prompt">$</span> %s\n' % esc(t["cmd"])
|
|
||||||
elif "out" in t: term += '<span class="out">%s</span>\n' % esc(t["out"])
|
|
||||||
ed = cfg["editors"]
|
|
||||||
eds = "".join('<div class="ed"><span class="k">◆</span> %s</div>' % esc(x) for x in ed["list"])
|
|
||||||
samples = [{"name": s["name"], "label": s["label"], "note": s["note"], "code": read_snippet(s["file"])}
|
|
||||||
for s in cfg["showcase"]["samples"]]
|
|
||||||
footlinks = "".join('<a href="%s">%s</a>' % (n["href"], esc(n["label"])) for n in cfg["nav_links"])
|
|
||||||
footlinks += '<a href="%s">Source ↗</a>' % cfg["repo_url"]
|
|
||||||
m = cfg["meta"]
|
|
||||||
tmpl = open(os.path.join(ASSETS, "index.tmpl.html")).read()
|
|
||||||
return pct(tmpl, dict(
|
|
||||||
css=css, brand=esc(cfg["brand"]), tagline=esc(cfg["tagline"]), repo=cfg["repo_url"],
|
|
||||||
title=esc(m["title"]), desc=escattr(m["description"]), ogt=esc(m["og_title"]), ogd=escattr(m["og_description"]),
|
|
||||||
navlinks=navlinks, pill=esc(hero["pill"]), title_pre=esc(hero["title_pre"]), title_accent=esc(hero["title_accent"]),
|
|
||||||
lead=hero["lead"], pcta_h=hero["primary_cta"]["href"], pcta_l=esc(hero["primary_cta"]["label"]),
|
|
||||||
scta_h=hero["secondary_cta"]["href"], scta_l=esc(hero["secondary_cta"]["label"]),
|
|
||||||
hero_name=esc(hero["snippet_name"]), hero_code=hero_code, stages=stages, pipenote=hero["pipeline_note"],
|
|
||||||
feat_kicker=esc(cfg["features"]["kicker"]), feat_title=esc(cfg["features"]["title"]), feat_intro=cfg["features"]["intro"], feats=feats,
|
|
||||||
sc_kicker=esc(cfg["showcase"]["kicker"]), sc_title=esc(cfg["showcase"]["title"]), sc_intro=cfg["showcase"]["intro"],
|
|
||||||
phil_kicker=esc(phil["kicker"]), phil_title=esc(phil["title"]), phil_paras=phil_paras, phil_stats=phil_stats,
|
|
||||||
start_kicker=esc(start["kicker"]), start_title=esc(start["title"]), start_intro=esc(start["intro"]),
|
|
||||||
steps=steps, term_name=esc(start["terminal_name"]), term=term,
|
|
||||||
ed_kicker=esc(ed["kicker"]), ed_title=esc(ed["title"]), ed_intro=ed["intro"], eds=eds, ed_note=ed["note"],
|
|
||||||
footlinks=footlinks, samples=json.dumps(samples),
|
|
||||||
))
|
|
||||||
|
|
||||||
def pct(tmpl, mapping):
|
|
||||||
return re.sub(r"%\((\w+)\)s", lambda mm: mapping[mm.group(1)], tmpl)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
def render_highlighter(symbols):
|
|
||||||
tmpl = open(os.path.join(ASSETS, "ludic-highlight.tmpl.js")).read()
|
|
||||||
return tmpl.replace("/*__SYMBOLS__*/{}", json.dumps(symbols, ensure_ascii=False))
|
|
||||||
|
|
||||||
def main():
|
|
||||||
ap = argparse.ArgumentParser()
|
|
||||||
ap.add_argument("--out", default=os.path.join(ROOT, "build", "pages"))
|
|
||||||
args = ap.parse_args()
|
|
||||||
out = args.out
|
|
||||||
os.makedirs(out, exist_ok=True)
|
|
||||||
|
|
||||||
sections = load_sections()
|
|
||||||
palette = json.load(open(os.path.join(LANG, "colors", "palette.json")))
|
|
||||||
symbols, items = build_symbols(sections, palette)
|
|
||||||
cfg = json.load(open(os.path.join(SITE, "site.json")))
|
|
||||||
|
|
||||||
W = lambda name, content: open(os.path.join(out, name), "w").write(content)
|
|
||||||
|
|
||||||
# per-item pages
|
|
||||||
npages = 0
|
|
||||||
for s in sections:
|
|
||||||
if s["cat"] == "colors":
|
|
||||||
continue
|
|
||||||
for e in s["entries"]:
|
|
||||||
W(e["id"] + ".html", item_page(e, s, cfg, items)); npages += 1
|
|
||||||
# namespace overview pages — one per discovered namespace section
|
|
||||||
for s in sections:
|
|
||||||
if s["ns_name"]:
|
|
||||||
W(ns_page(s["ns_name"]) + ".html", ns_overview_page(s["ns_name"], s, cfg)); npages += 1
|
|
||||||
W("ns-color.html", color_page(palette, cfg)); npages += 1
|
|
||||||
# index + landing + assets
|
|
||||||
W("api.html", api_index(sections, cfg))
|
|
||||||
W("index.html", render_index(cfg))
|
|
||||||
W("ludic-highlight.js", render_highlighter(symbols))
|
|
||||||
W("symbols.json", json.dumps({"items": items, "highlight": symbols}, indent=2, ensure_ascii=False))
|
|
||||||
W(".nojekyll", "")
|
|
||||||
|
|
||||||
n_entries = sum(len(s["entries"]) for s in sections)
|
|
||||||
print("docs generated -> %s" % out)
|
|
||||||
print(" sections: %d symbols: %d colors: %d item pages: %d"
|
|
||||||
% (len(sections), n_entries, palette["count"], npages))
|
|
||||||
print(" highlighter: %d kw / %d type / %d phase / %d builtin / %d ns-method / %d annot / %d ns"
|
|
||||||
% (len(symbols["keywords"]), len(symbols["types"]), len(symbols["phases"]),
|
|
||||||
len(symbols["builtins"]), len(symbols["nsmethods"]), len(symbols["annotations"]),
|
|
||||||
len(symbols["namespaces"])))
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -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)")
|
|
||||||
638
tools/x/docgen.ludic
Normal file
638
tools/x/docgen.ludic
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# `<code>` backtick spans: `x` -> <code>x</code> (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, "<code>")
|
||||||
|
var k = i + 1
|
||||||
|
while k < j { sb_putc(b, s[k]); k = k + 1 }
|
||||||
|
sb_puts(b, "</code>")
|
||||||
|
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
|
||||||
|
}
|
||||||
288
tools/x/docgen_check.ludic
Normal file
288
tools/x/docgen_check.ludic
Normal file
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
1014
tools/x/docgen_gen.ludic
Normal file
1014
tools/x/docgen_gen.ludic
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -19,6 +19,9 @@ program X {
|
||||||
import "test.ludic"
|
import "test.ludic"
|
||||||
import "json.ludic"
|
import "json.ludic"
|
||||||
import "checks.ludic"
|
import "checks.ludic"
|
||||||
|
import "docgen.ludic"
|
||||||
|
import "docgen_gen.ludic"
|
||||||
|
import "docgen_check.ludic"
|
||||||
import "release.ludic"
|
import "release.ludic"
|
||||||
|
|
||||||
function usage() -> void {
|
function usage() -> void {
|
||||||
|
|
@ -43,6 +46,9 @@ program X {
|
||||||
print(" x check-impl every implemented feature has a docs/language page")
|
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 check-vocabulary the vocabulary is in sync across grammar / lexer / header / parser")
|
||||||
print(" x lint-asset <file> validate one editor .json / .xml asset")
|
print(" x lint-asset <file> 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("")
|
||||||
print("release:")
|
print("release:")
|
||||||
print(" x version print the toolchain version (ludicc --version)")
|
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-impl") { exit(cmd_check_impl()) }
|
||||||
if (cmd == "check-vocabulary") { exit(cmd_check_vocab()) }
|
if (cmd == "check-vocabulary") { exit(cmd_check_vocab()) }
|
||||||
if (cmd == "lint-asset") { exit(cmd_lint_asset()) }
|
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 == "golden") { exit(cmd_golden()) }
|
||||||
if (cmd == "bootstrap") { exit(cmd_bootstrap()) }
|
if (cmd == "bootstrap") { exit(cmd_bootstrap()) }
|
||||||
if (cmd == "bootstrap-cfree") { exit(cmd_bootstrap_cfree()) }
|
if (cmd == "bootstrap-cfree") { exit(cmd_bootstrap_cfree()) }
|
||||||
|
|
|
||||||
|
|
@ -186,5 +186,20 @@ function cmd_test() -> int {
|
||||||
if (rc == 42) { ok("ludic app.ludic -> compiles, runs, forwards exit code") }
|
if (rc == 42) { ok("ludic app.ludic -> compiles, runs, forwards exit code") }
|
||||||
else { bad2("ludic run: expected exit 42", `got {string(rc)}`) }
|
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()
|
return report()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue