feat(docs): generate Starlight content from the manual tree

Add build_manual.build_site(), which walks docs/manual and emits the
Astro Starlight content collection: plain pages copied with pipeline-only
frontmatter (man/site/manTitle/helpKeywords) stripped, and *-functions/
category files exploded into one page per function plus a CardGrid/LinkCard
overview. Writes src/sidebar.json for astro.config.mjs to import. Wires
--site alongside the existing --concat flag.

Fixes two latent defects found while building the real site against the
strict content.config.ts schema:
- _split_entries now tracks fenced code blocks (like
  manualtools.shift_headings does) so a `## ` inside a fence can't be
  mistaken for an entry boundary.
- LinkCard title/description are escaped for JSX attribute context, since
  shell synopses routinely contain `<placeholder>` angle brackets that
  would otherwise open unterminated MDX/JSX parsing.

Also fixes the generated sidebar shape for the functions category: Starlight
0.39+ dropped support for a bare `autogenerate` sibling of `label` on a
top-level group, so the autogenerate config now nests inside `items`.

Verified with a full `astro build` (temporarily pointing astro.config.mjs
at the generated sidebar.json, then reverted since replacing that config
is a later task's deliverable): 120 pages built cleanly, no
content-collection/frontmatter/MDX errors.
This commit is contained in:
2026-07-25 22:44:28 -04:00
parent c096433b5d
commit a4054de4f6
2 changed files with 189 additions and 8 deletions
+21
View File
@@ -192,6 +192,27 @@ def test_every_index_keyword_resolves():
assert not missing, "unresolvable index keywords:\n " + "\n ".join(missing)
def test_site_build_produces_function_pages():
import tempfile
import build_manual
docs = Path(__file__).parent
with tempfile.TemporaryDirectory() as d:
out = Path(d)
sidebar = build_manual.build_site(docs / "manual", out)
pages = list(out.rglob("*.md*"))
assert (out / "index.md").exists(), "landing page missing"
fn_pages = [p for p in pages if "functions" in p.parts and p.name != "index.mdx"]
assert len(fn_pages) > 80, f"expected >80 function pages, got {len(fn_pages)}"
assert not (out / "00-name.md").exists(), "site:false page was published"
assert sidebar, "sidebar structure is empty"
for page in pages:
fm, _ = mt.parse(page)
assert "manTitle" not in fm, f"{page.name} leaked manTitle into site output"
assert "title" in fm, f"{page.name} has no title"
TESTS = [v for k, v in sorted(globals().items()) if k.startswith("test_")]