src/ast.ts
The discriminated Node union. Every other module imports types from here. Adding a node variant breaks every renderer's exhaustive switch — that is the desired safety property.
The whole codebase fits in one mental model: source → parser → AST → renderer. There are no plugins, no parser combinators, no build pipelines. This is intentional.
.noma source ──► fmt.ts ──► .noma source (table re-alignment)
│
▼ parser.ts (or book.ts for manifests)
typed AST ─────────► validator.ts ─► diagnostics
│
├──► renderer-html.ts ─► HTML
├──► renderer-llm.ts ─► LLM context (optionally selected/budgeted)
├──► renderer-json.ts ─► JSON
└──► renderer-noma.ts ─► .noma source (roundtrip + patch)
│
├──► ids.ts ─► ID / alias registry
│
▼ scripts/render-pdf.ts
PDF
Each arrow is a pure function. The parser does not validate. The renderers do not parse. The validator does not render. The patch layer has two paths: AST-level patch(doc, op) for callers already operating on a tree, and source-level patchSource(source, ops) for the CLI/MCP path that rewrites only the addressed source spans.
The discriminated Node union. Every other module imports types from here. Adding a node variant breaks every renderer's exhaustive switch — that is the desired safety property.
Hand-written line-based recursive descent. ~250 lines. Parses frontmatter, headings, fenced code, lists, quotes, thematic breaks, and arbitrary-depth directive blocks via colon-counting.
Tiny inline markup parser plus the shared splitPipeRow utility used by the parser, the formatter, and the ::table renderer. Three entry points: inlineToHtml, inlineToPlain, splitPipeRow.
AST → semantic HTML. Special-cases the typed blocks (claim, evidence, grid, card, plot, agent_task, state_change, table, ...), resolves ::plot{dataset=, column=} against a per-render dataset registry, and falls through to a generic wrapper for unknown directive names.
AST → deterministic plain text with [TAG attr=value] markers. Designed to maximize signal density inside LLM context windows. Supports selection by node type or directive name plus a character budget for agent context windows.
AST → .noma source. Roundtrip-safe printer that backs noma render --to noma. parse → renderNoma → parse preserves the AST modulo positions.
AST → diagnostics list. Detects duplicate IDs, broken references (incl. [[wikilink]] targets), plot/dataset linkage errors, plot/figure issues, claim-without-evidence, risk/decision/agent-task shape rules, stale citations, escape-hatch trust, state_change shape, and out-of-profile-directive when the document declares a profile.
Block-level patch ops: replace_block, add_block, delete_block, update_attribute, rename_id. AST-level patching returns a new tree; source-level patching reparses between ops and splices only the target ranges. rename_id retargets reference attributes and [[wikilink]] references across the document.
AST → canonical ID registry. Used by noma ids to expose document-order IDs, alias mappings, source lines, and basic block metadata for agent discovery.
YAML manifest loader. loadBook concatenates each chapter's parsed AST into a single DocumentNode, so every renderer works on books with no per-target wiring.
Source formatter. Walks the file line-by-line, recognises GitHub-style pipe tables, rebuilds them to a single column width, and leaves everything else byte-identical (including tables inside fenced code blocks).
The noma init|parse|render|ids|check|export|patch|fmt|verify|diff command-line entry. Glue around parser/renderer/validator/patch modules; transaction-shaped --ops payloads are validated here before writing.
Reusable GitHub Action wrapper. Installs the CLI from the action checkout by default, optionally runs noma check, renders the requested target, and uploads the output as a workflow artifact.
The HTML renderer is theme-agnostic — it only emits semantic class names. The default theme is a single CSS file with custom properties for easy reskinning.
A plugin API will arrive once three things are true: the AST is stable, two or more independent block packs exist in the wild, and at least one community-contributed renderer ships externally.
The parser is hand-written precisely so optimization stays straightforward — if Noma ever needs to render a million-block book in under a second, the path is to port parser.ts to a streaming variant or to Rust, without touching the AST or any renderer.
utils/ dumping ground.noUncheckedIndexedAccess is on — array access returns T | undefined.tsx runs TypeScript directly. npm run build exists for the published npm package.