tabnasrailroad

package module
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 7 Imported by: 0

README

github.com/tabnas/railroad/go

Go port of @tabnas/railroad — a railroad (syntax) diagram generator for the tabnas parsing engine.

It does not parse anything itself: it introspects a live Tabnas instance that already has a grammar installed and emits three artifacts from that grammar —

  • a declarative, JSON-serializable GrammarModel (the interchange format: one node tree per rule),
  • a vertical-flow SVG (one anchored, linked track per rule), and
  • a vertical ASCII diagram (Unicode box-drawing, or plain | - +).

It also ships the tabnas-railroad CLI. Diagrams bias toward verticality (tall and narrow) so they read on laptops and phones: sequences run top-to-bottom, choices fan out sideways, optional / repetition rails run on the side.

The Go package tracks the canonical TypeScript implementation in ../ts: the extracted GrammarModel JSON matches the TS model for the same grammar (same start / rules / structure), and the SVG/ASCII renderers produce the same vertical-flow output.

Documentation

Four-quadrant Diátaxis docs:

The canonical TypeScript implementation lives in ../ts.

Install

go get github.com/tabnas/railroad/go

Use

package main

import (
	"fmt"

	jsonplugin "github.com/tabnas/json/go"
	tabnas "github.com/tabnas/parser/go"
	tabnasrailroad "github.com/tabnas/railroad/go"
)

func main() {
	tn := tabnas.Make()
	jsonplugin.Json(tn, nil) // install a grammar to diagram
	tabnasrailroad.Plugin(tn, nil) // decorate the instance

	api := tabnasrailroad.Of(tn)
	model := api.ToJson()    // *GrammarModel
	svg, _ := api.ToSvg()    // whole-grammar SVG
	ascii, _ := api.ToAscii(tabnasrailroad.AsciiOptions{}) // whole-grammar ASCII

	fmt.Println(model.Start) // val
	_ = svg
	_ = ascii
}

Instance-free use of the extractor and renderers:

model := tabnasrailroad.ExtractGrammar(tn)
svg, _ := tabnasrailroad.ModelToSvg(model)
ascii, _ := tabnasrailroad.ModelToAscii(model, tabnasrailroad.AsciiOptions{Plain: true})
text, _ := tabnasrailroad.ToText(model.Rules["val"])

Hand-build a diagram node tree and render a single node:

node := tabnasrailroad.Diagram(tabnasrailroad.Sequence(
	tabnasrailroad.Terminal("GET"), tabnasrailroad.NonTerminal("path")))
svg, _ := tabnasrailroad.RenderNodeSvg(node)

Exports

  • ExtractGrammar(tn, *ExtractOptions) *GrammarModel — introspect a live instance into the model. The heart of the package.
  • ModelToSvg, ModelToAscii, RenderNodeSvg, RenderNodeAscii, ToText — renderers.
  • Node constructors: Terminal, NonTerminal, Comment, SkipNode, Sequence, Choice, MustChoice, Optional, OneOrMore, ZeroOrMore, Diagram.
  • Plugin(tn, opts) + Of(tn) — the plugin wiring and API accessor.
  • Types: RailroadNode, GrammarModel, LegendEntry, RailroadError.

Notes on the Go port

The TypeScript extractor reads the raw #KEY / #VAL token-set names off each alt to render readable set labels. The Go engine resolves those names to []Tin sets on the live RuleSpec, so the Go extractor recovers the set name by matching the resolved tins to a named token set (disambiguating identical sets — e.g. KEY vs VAL — by position role: a slot followed by a colon is a map key). Rule-map key order is not part of the cross-language contract; the Go model orders user rules deterministically (the engine's RSM is an unordered map).

Sample output

The json grammar rendered to a vertical-flow diagram (go run ./cmd/tabnas-railroad --grammar json -o examples):

railroad diagram of the json grammar

The same grammar as an ASCII diagram (excerpt — the val choice and the pair loop; full output in ../examples/json-grammar.txt):

val:
              │
   ┌──────────┼──────────┐
┌──┴──┐   ┌───┴──┐   ╭───┴───╮
│ map │   │ list │   │ "VAL" │
└──┬──┘   └───┬──┘   ╰───┬───╯
   └──────────┼──────────┘
              │

pair:
    │
    ├────┐
╭───┴───╮│
│ "KEY" ││
╰───┬───╯│
    │    │
 ╭──┴──╮ │
 │ ":" │ │,
 ╰──┬──╯ │
    │    │
 ┌──┴──┐ │
 │ val │ │
 └──┬──┘ │
    ├────┘
    │

CLI

go run ./cmd/tabnas-railroad --grammar json -o diagrams      # write all three
go run ./cmd/tabnas-railroad -f diagrams/grammar.railroad.json --ascii
go run ./cmd/tabnas-railroad --grammar json --text -o /tmp/rr

License

MIT.

Documentation

Overview

ascii.go Vertical-flow ASCII renderer for railroad diagrams. Same flow model as the SVG renderer (sequences stack downward, choices fan sideways, loops return on the right), painted onto a character grid.

Rails are tracked as direction bits per cell (Up/Down/Left/Right) so junctions resolve automatically to the right box-drawing glyph. Boxes draw literal corners/sides. Plain mode (the CLI --ascii-plain) swaps the Unicode glyph set for plain | - +.

This file mirrors ts/src/ascii.ts.

extract.go Build a railroad GrammarModel by introspecting a live tabnas instance. Reads the rule set (tn.RSM()) and resolved config (tn.Config()) and reverse-maps the alt-based rule machine into railroad constructs (sequence / choice / optional / repetition).

Mapping (see doc + tests against @tabnas/json):

  • open alt: the first sN-b token positions are consumed terminals; a P push appends a nonterminal. b == sN (pure peek) consumes nothing — render only the ref.
  • several open alts -> choice.
  • close alt R == <self> (+ guard token) -> repetition (OneOrMore with the guard token on the return path). R == <other> -> continuation. A close alt that consumes a token with no b/R is this rule's own closing terminal (append it). A backup close (b>0) leaves the token for the parent -> drop. End-of-source / pure-pop -> drop.
  • synthetic helper rules (name has `$` or `_gen…`) are inlined.
  • normalization factors common prefix/suffix across choice branches and turns an empty branch into Optional.

This file mirrors ts/src/extract.ts. The one structural difference from the TS port is the engine introspection shape: the Go engine resolves an alt's token spec into [][]Tin (the raw "#KEY"/"#VAL" set-name strings are not retained on the live RuleSpec), so the extractor recovers a readable set name by matching the resolved Tin set against the instance's named token sets, disambiguating identical sets (KEY vs VAL) by position role.

Package railroad is the Go port of @tabnas/railroad: it introspects a live tabnas grammar instance and renders railroad / syntax diagrams as a declarative JSON GrammarModel, vertical-flow SVG, and vertical ASCII.

The model (this file) is a small, engine-agnostic tree of nodes (terminal, nonterminal, sequence, choice, optional, repetition) plus the GrammarModel envelope (one node per grammar rule). It is pure, JSON-serializable data: the interchange format that the SVG and ASCII renderers consume, and that the extractor produces from a live tabnas instance.

This file mirrors ts/src/model.ts.

railroad.go Railroad-diagram plugin for the tabnas parser, the Go port of ts/src/railroad.ts. Loading the plugin decorates the instance with a "railroad" decoration: a *RailroadApi that introspects THIS instance's installed grammar and returns a declarative GrammarModel, plus helpers to render that grammar to a vertical-flow SVG or ASCII diagram.

tn := tabnas.Make()
json.Json(tn, nil)
railroad.Plugin(tn, nil)
api := railroad.Of(tn)
model, _ := api.ToJson()   // GrammarModel
svg, _ := api.ToSvg()      // whole-grammar SVG
ascii, _ := api.ToAscii()  // whole-grammar ASCII

The extraction + rendering logic lives in extract.go / svg.go / ascii.go; this file is the plugin wiring plus the package-level entry points for instance-free use.

svg.go Vertical-flow SVG renderer for railroad diagrams. Flow runs top->bottom: sequences stack vertically, choice branches fan out side-by-side, and optional / repetition rails run parallel on the side. This biases the output tall-and-narrow, which suits laptop browsers and phones.

Each node measures to a vLayout{width, height, entryX, exitX, draw}. The rail enters the top edge at entryX and leaves the bottom edge at exitX (both equal — all nodes are horizontally symmetric). draw(x,y) emits SVG with the node's bounding box at top-left (x,y).

ModelToSvg stacks one titled, anchored sub-diagram per rule and turns nonterminal boxes into <a href="#rule"> links.

This file mirrors ts/src/svg.ts.

Index

Constants

View Source
const DecorationName = "railroad"

DecorationName is the key under which the plugin stores its API on the instance (retrieve via tn.Decoration(DecorationName) or railroad.Of(tn)).

View Source
const Version = "0.2.1"

Version is the current version of the module.

Variables

This section is empty.

Functions

func ModelToAscii

func ModelToAscii(model *GrammarModel, opts ...AsciiOptions) (string, error)

ModelToAscii renders a whole grammar: each rule as a titled vertical block.

func ModelToSvg

func ModelToSvg(model *GrammarModel, _opts ...SvgOptions) (string, error)

ModelToSvg renders a whole grammar: a vertical stack of titled, anchored rule tracks; nonterminal boxes link to the referenced rule's track.

func NodeEqual added in v0.2.1

func NodeEqual(a, b *RailroadNode) bool

NodeEqual reports deep structural equality of two nodes, used by choice prefix/suffix factoring to detect shared leading/trailing elements. It mirrors the TS `nodeEqual` export; two nil nodes compare equal.

func Plugin

func Plugin(tn *tabnas.Tabnas, _ map[string]any) error

Plugin is the tabnas plugin entry point. Decoration is lazy: every helper re-reads the instance's current grammar when called, so install order does not matter. Mirrors the TS railroad plugin.

func RenderNodeAscii

func RenderNodeAscii(node *RailroadNode, opts ...AsciiOptions) (string, error)

RenderNodeAscii renders a single node to an ASCII block.

func RenderNodeSvg

func RenderNodeSvg(node *RailroadNode, opts ...SvgOptions) (string, error)

RenderNodeSvg renders a single node (wrapped in its own SVG document).

func ToText

func ToText(node *RailroadNode) (string, error)

ToText renders a node as compact EBNF-ish text: terminals quoted, choice in (a | b), optional in [x], zero-or-more in {x}, one-or-more as x+. Mirrors ts/model.ts toText.

Types

type AsciiOptions

type AsciiOptions struct {
	// Plain renders pure-ASCII glyphs (| - +) instead of Unicode.
	Plain bool
}

AsciiOptions configures the ASCII renderer.

type ExtractOptions

type ExtractOptions struct {
	// NoFactor disables prefix/suffix factoring + empty-branch->optional
	// (the TS `factor` defaults to true; this is its inverse).
	NoFactor bool
	// Start overrides the entry rule (defaults to config RuleStart).
	Start string
	// TokenSetNames lists the named token-set candidates the extractor may
	// recover for a resolved Tin set, in preference order. When empty, the
	// standard set ["VAL", "KEY"] is used (the json grammar's sets). A
	// position immediately followed by a colon (CL) prefers "KEY".
	TokenSetNames []string
	// TokenDesc supplies human descriptions for named tokens/sets, keyed by
	// bare or "#"-prefixed name. It is the Go analog of the TS cfg.tokenDesc
	// hook a grammar attaches via config.modify.
	TokenDesc map[string]string
}

ExtractOptions controls extraction.

type GrammarModel

type GrammarModel struct {
	Start     string                   `json:"start"`
	Rules     map[string]*RailroadNode `json:"rules"`
	RuleOrder []string                 `json:"-"`
	Legend    []LegendEntry            `json:"legend,omitempty"`
	Ignored   []LegendEntry            `json:"ignored,omitempty"`
	Meta      map[string]any           `json:"meta,omitempty"`
}

GrammarModel is one whole grammar: an ordered rule map plus the entry rule. This is the declarative artifact emitted as grammar.railroad.json.

RuleOrder preserves the insertion order of rule names (Go maps have no stable order); the renderers and JSON emitter use it so output is deterministic and matches the TS object-key order.

func ExtractGrammar

func ExtractGrammar(tn *tabnas.Tabnas, opts ...*ExtractOptions) *GrammarModel

ExtractGrammar builds a railroad GrammarModel from a live tabnas instance. It mirrors ts/extract.ts extractGrammar(tn).

func (*GrammarModel) MarshalJSON

func (m *GrammarModel) MarshalJSON() ([]byte, error)

MarshalJSON emits the model with rules in RuleOrder (falling back to map order when RuleOrder is empty) so the serialized form is deterministic.

func (*GrammarModel) UnmarshalJSON

func (m *GrammarModel) UnmarshalJSON(data []byte) error

UnmarshalJSON parses a model, recovering RuleOrder from the raw JSON key order so a round-tripped model renders identically.

type LegendEntry

type LegendEntry struct {
	Token   string `json:"token"`
	Meaning string `json:"meaning"`
}

LegendEntry names a token (or token set) that appears in the diagram and gives its human meaning. Used for the diagram legend and the ignored set.

type NodeKind

type NodeKind = string

NodeKind enumerates the railroad node variants (the tagged-union tag).

const (
	KindTerminal    NodeKind = "terminal"
	KindNonTerminal NodeKind = "nonterminal"
	KindComment     NodeKind = "comment"
	KindSkip        NodeKind = "skip"
	KindSeq         NodeKind = "seq"
	KindChoice      NodeKind = "choice"
	KindOptional    NodeKind = "optional"
	KindOneOrMore   NodeKind = "oneOrMore"
	KindZeroOrMore  NodeKind = "zeroOrMore"
	KindDiagram     NodeKind = "diagram"
)

type RailroadApi

type RailroadApi struct {
	// contains filtered or unexported fields
}

RailroadApi is the shape the "railroad" decoration takes: a grammar extractor plus render helpers and the diagram-model constructors. It is the Go analog of the TS callable tn.railroad.

func Of

func Of(tn *tabnas.Tabnas) *RailroadApi

Of returns the RailroadApi previously installed on tn by Plugin, or a freshly-bound API when the plugin has not been loaded (so callers can use the helpers without explicitly installing the plugin first).

func (*RailroadApi) Extract

func (a *RailroadApi) Extract(opts ...*ExtractOptions) *GrammarModel

Extract introspects this instance's grammar and returns its GrammarModel. (Go analog of calling tn.railroad() / tn.railroad.toJson().)

func (*RailroadApi) RenderNode

func (a *RailroadApi) RenderNode(node *RailroadNode, opts ...SvgOptions) (string, error)

RenderNode renders a single node to a standalone SVG.

func (*RailroadApi) RenderNodeAscii

func (a *RailroadApi) RenderNodeAscii(node *RailroadNode, opts ...AsciiOptions) (string, error)

RenderNodeAscii renders a single node to an ASCII block.

func (*RailroadApi) RenderNodeText

func (a *RailroadApi) RenderNodeText(node *RailroadNode) (string, error)

RenderNodeText renders a single node to compact EBNF text.

func (*RailroadApi) ToAscii

func (a *RailroadApi) ToAscii(asciiOpts AsciiOptions, opts ...*ExtractOptions) (string, error)

ToAscii extracts this instance's grammar and renders it to ASCII. A trailing AsciiOptions controls plain mode.

func (*RailroadApi) ToJson

func (a *RailroadApi) ToJson(opts ...*ExtractOptions) *GrammarModel

ToJson is an alias of Extract.

func (*RailroadApi) ToSvg

func (a *RailroadApi) ToSvg(opts ...*ExtractOptions) (string, error)

ToSvg extracts this instance's grammar and renders it to SVG.

type RailroadError

type RailroadError struct {
	Message string
	Node    any
}

RailroadError is raised on a malformed diagram model (empty choice, unknown kind, invalid node). It mirrors the TS RailroadError.

func (*RailroadError) Error

func (e *RailroadError) Error() string

type RailroadNode

type RailroadNode struct {
	Kind  NodeKind
	Text  string
	Items []*RailroadNode
	Item  *RailroadNode
	Rep   *RailroadNode
}

RailroadNode is one node of a railroad diagram tree. It is a faithful port of the TypeScript discriminated union: which fields are populated depends on Kind. Terminal/NonTerminal/Comment use Text; Seq/Choice/ Diagram use Items; Optional uses Item; OneOrMore/ZeroOrMore use Item and the optional Rep; Skip uses nothing.

Custom JSON marshalling reproduces the TS object shapes exactly: each kind serializes only its relevant fields (so a Terminal is {"kind":"terminal","text":"x"} and a Skip is {"kind":"skip"}), and Rep is omitted when absent.

func Choice

func Choice(items ...*RailroadNode) (*RailroadNode, error)

Choice builds a choice of branches. It returns a RailroadError (via panic-free path: callers that may pass zero branches must check) when given no branches, mirroring the TS Choice throwing.

func Comment

func Comment(text string) *RailroadNode

Comment builds an inline comment node.

func Diagram

func Diagram(items ...*RailroadNode) *RailroadNode

Diagram builds a top-level diagram wrapper of nodes.

func MustChoice

func MustChoice(items ...*RailroadNode) *RailroadNode

MustChoice is Choice that panics with a RailroadError on no branches — the ergonomic form used internally where branch count is guaranteed.

func NonTerminal

func NonTerminal(text string) *RailroadNode

NonTerminal builds a nonterminal (rule reference) node.

func Norm added in v0.2.1

func Norm(item any) (*RailroadNode, error)

Norm coerces an item to a valid *RailroadNode. It mirrors the TS `norm` export and its Item union (RailroadNode | string): a bare string is taken to be a terminal — the common case when hand-building diagrams — and a *RailroadNode is validated as-is. Any other value (including nil or a node with an empty Kind) yields a RailroadError.

func OneOrMore

func OneOrMore(item *RailroadNode, rep *RailroadNode) *RailroadNode

OneOrMore builds a one-or-more repetition, with an optional rep node on the return path (e.g. a separator).

func Optional

func Optional(item *RailroadNode) *RailroadNode

Optional builds an optional (bypassable) node.

func Sequence

func Sequence(items ...*RailroadNode) *RailroadNode

Sequence builds a sequence of nodes.

func SkipNode

func SkipNode() *RailroadNode

SkipNode builds a bypass (empty) node.

func Terminal

func Terminal(text string) *RailroadNode

Terminal builds a terminal (literal / named token) node.

func ZeroOrMore

func ZeroOrMore(item *RailroadNode, rep *RailroadNode) *RailroadNode

ZeroOrMore builds a zero-or-more repetition (a bypassable OneOrMore).

func (*RailroadNode) MarshalJSON

func (n *RailroadNode) MarshalJSON() ([]byte, error)

MarshalJSON emits the kind-specific object shape, matching ts/model.ts.

func (*RailroadNode) UnmarshalJSON

func (n *RailroadNode) UnmarshalJSON(data []byte) error

UnmarshalJSON parses a kind-specific object shape into a RailroadNode, so a model round-trips through JSON (used by the render-mode CLI and the round-trip tests).

type SvgOptions

type SvgOptions struct {
	// LinkFor maps a nonterminal name to an href (whole-grammar linking).
	// Returns ("", false) when the name should not be linked.
	LinkFor func(name string) (string, bool)
}

SvgOptions configures the SVG renderer.

Directories

Path Synopsis
cmd
tabnas-railroad command
Command tabnas-railroad renders railroad (syntax) diagrams from a tabnas grammar.
Command tabnas-railroad renders railroad (syntax) diagrams from a tabnas grammar.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL