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
|
|
@ -10,7 +10,7 @@ docs/
|
|||
language/<category>/<id>.md one file per symbol — keyword, type, phase,
|
||||
builtin, namespace method, operator, annotation
|
||||
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/snippets/*.ludic the code shown on the landing page (real programs)
|
||||
tools/docgen/inventory.json the authoritative symbol set the coverage guard checks
|
||||
|
|
@ -61,27 +61,31 @@ hovering any token shows a summary card from the real API data.
|
|||
|
||||
## Build & check
|
||||
|
||||
The generator and its guards are written in Ludic and run through `x` — there is
|
||||
no Python in the pipeline. `assets/` (the CSS/HTML/JS templates) and
|
||||
`inventory.json` are the only inputs the tools here still read directly.
|
||||
|
||||
```bash
|
||||
python3 tools/docgen/gen.py --out build/pages # generate the whole site
|
||||
python3 tools/docgen/check.py build/pages # coverage + duplicate-token + link guard
|
||||
bin/x check-docs # parse every ```ludic doc fence (Ludic; no Python)
|
||||
bin/x docs-gen --out build/pages # generate the whole site
|
||||
bin/x docs-check build/pages # coverage + duplicate-token + link guard
|
||||
bin/x check-docs # parse every ```ludic doc fence
|
||||
```
|
||||
|
||||
`check.py` fails CI if any symbol in `inventory.json` lacks a page, if a token is
|
||||
documented on two pages, or if a highlighter link points at a missing page — so
|
||||
"every symbol is documented, autogenerated each time" is enforced. `validate.py`
|
||||
needs a built `bin/ludicc`; the deploy CI is Python-only, so run it locally or in
|
||||
a toolchain-enabled job.
|
||||
|
||||
No third-party dependencies — Python standard library only.
|
||||
`docs-check` fails CI if any symbol in `inventory.json` lacks a page, if a token
|
||||
is documented on two pages, or if a highlighter link points at a missing page —
|
||||
so "every symbol is documented, autogenerated each time" is enforced. It needs a
|
||||
built `bin/x` (bootstrapped from the IR seed with clang alone).
|
||||
|
||||
## Publish
|
||||
|
||||
`.forgejo/workflows/docs.yml` runs `gen.py` + `check.py` on every push to `main`
|
||||
that touches `docs/**` or `tools/docgen/**`, and publishes the result to the
|
||||
`pages` branch root. `index.html` + `.nojekyll` always stay at the root.
|
||||
`.forgejo/workflows/docs.yml` bootstraps the toolchain from the IR seed and runs
|
||||
`x docs-gen` + `x docs-check` on every push to `main` that touches `docs/**`,
|
||||
`tools/docgen/**` or `tools/x/**`, and publishes the result to the `pages` branch
|
||||
root. `index.html` + `.nojekyll` always stay at the root.
|
||||
|
||||
## Colors
|
||||
|
||||
`palette.json` is generated by `tools/docgen/palette.py` from a single `PALETTE`
|
||||
table, which also generates `selfhost/emit_color.ludic`. Regenerate colors there.
|
||||
`palette.json` and `selfhost/backend/stdlib/emit_color.ludic` are both generated
|
||||
by `bin/x docs-palette` from a single palette table — the `pal_add(...)` rows in
|
||||
`tools/x/docgen.ludic`. Edit the table there and regenerate; do not hand-edit the
|
||||
generated files.
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
Loading…
Add table
Add a link
Reference in a new issue