Architecture
Package graph

- Every package depends on
@choo-choo/core(directly or viaparser-utils). - Parsers depend on
parser-utilsfor lexer primitives and oncorefor IR construction. - Framework bindings depend only on
core. They do not depend on any parser.- Consumers choose which parser to import.
Intermediate representation
The intermediate representation (IR) is a flat discriminated union of Node kinds. It’s pure data, without methods, or SVG awareness. It is the only contract shared between parsers and the renderer. The exact shape of each node is specified in IR.
Grammar parsers
Every grammar parser implements a single interface exported from @choo-choo/core:
id— a short identifier (ebnf,antlr,peg, orpython-peg) used by bindings and the playground.parse(source: string): ParsedGrammar— consumes a grammar source and returns an ordered list of namedGrammarRulevalues. Each rule carries aDiagramIR tree, its name, and an optionalSourceRangepointing back at the rule’s definition in the source.
Parsers are standalone packages. Adding a new grammar (ABNF, classic BNF, …) requires no changes to core or to any binding.
Shared lexer primitives — reader, tokenizer, regex-based specification table — live in @choo-choo/parser-utils so each grammar package is not forced to reinvent them. The three launch parsers share enough lexical structure to reuse those primitives but diverge where it matters:
- EBNF follows ISO/IEC 14977: explicit
{ }/[ ]for repetition and optionality,=for definitions,|for alternation. - ANTLR uses
:/;rule delimiters,?/*/+cardinalities, rule labels, and token vs parser rule conventions. - PEG (peggy) adds ordered-choice semantics (first match wins) and lookahead predicates (
&,!) that change how alternatives are interpreted — a semantic difference, not just surface syntax. - Python PEG (pegen) is a sibling PEG dialect with a meaningfully different surface (
:/|instead of=//, brackets for optionality, the cut and eager-parse operators, separator-aware repetition). Lives in its own package so consumers writing for one dialect aren’t forced to think about the other.
Renderer
- Input: a
diagramIR node. - Output: an SVG string.
- Strategy: a visitor dispatching on
node.kind. Layout is computed top-down (width, height, up, down — mirroring the legacy project’s geometry) and emitted as SVG strings. No DOM APIs are called. - SSR: guaranteed — the renderer is pure and deterministic.
- Styling: an optional stylesheet (
railroad.css) ships withcore; every binding re-exports it so consumers opt in with a single import.
The renderer’s surface is specified in docs/rendering.md when milestone M1 lands.
Bindings
All bindings share one prop shape:
source?: string+parser?: GrammarParser— grammar-driven path.ir?: Node— already-built IR tree (from the manual builder or a custom source).- Exactly one of the two must be provided.
Binding-specific behaviour:
@choo-choo/react— functional component usingdangerouslySetInnerHTML. Works in both server and client components; no"use client"directive required.@choo-choo/vue— Vue 3 single-file component usingv-html.@choo-choo/astro—.astrocomponent; renders at build time by default (zero client JS).@choo-choo/vanilla— exposes (a) an imperativemount(element, options)and (b) a<choo-choo>custom element. The grammar parser is dynamically imported based on agrammarattribute / option so the baseline bundle stays small.
Each binding’s exact prop/attribute API is specified under docs/bindings/*.md when its milestone lands.