In this guide

User Guide

Turn a grammar into a diagram you can follow. Build with Ruby, export SVG or text, and connect named rules in a browsable document.

Try the playgroundBrowse the node gallery

Getting started

RailroadDiagrams runs on Ruby 2.5 or later and has no runtime gem dependencies.

gem install railroad_diagrams

For a Bundler project, add gem "railroad_diagrams" to your Gemfile and run bundle install.

Your first diagram

Save this as select.rb. Strings are literal tokens; nt names another grammar rule; opt adds an optional branch.

require "railroad_diagrams"

diagram = RailroadDiagrams.diagram do
  seq("SELECT", opt("DISTINCT"), nt("column"), "FROM", nt("table"))
end

File.write("select.svg", diagram.to_standalone_svg)

Run ruby select.rb, or bundle exec ruby select.rb in a Bundler project, then open select.svg in your browser.

SELECT, optional DISTINCT, column, FROM, and table railroad diagram
Read from left to right. The bypass above DISTINCT makes it optional. Square boxes refer to other grammar rules.

Building diagrams

The builder DSL keeps nested grammar expressions short. Use RailroadDiagrams.diagram for a complete diagram or RailroadDiagrams.build for a node you will reuse.

columns = RailroadDiagrams.build do
  list(choice("*", nt("column")), sep: ",")
end

diagram = RailroadDiagrams::Diagram.new("SELECT", columns)

The builder accepts these Ruby values directly:

ValueResultExample
StringTerminal"SELECT"
SymbolNonterminal:column
ArraySequence["FROM", :table]
nilEmpty pathchoice(nil, "DISTINCT")
Diagram nodeThe same nodeseq(columns, "FROM", :table)

Direct constructors are also available. Pass strings as children to turn them into terminals, and construct nonterminals explicitly:

diagram = RailroadDiagrams::Diagram.new(
  "SELECT",
  RailroadDiagrams::Optional.new("DISTINCT"),
  RailroadDiagrams::NonTerminal.new("column")
)

A builder block with one argument receives the builder: RailroadDiagrams.build { |b| b.seq("SELECT", :column) }.

Node reference

Choose a node by the shape of your grammar. Follow a name to its rendered example in the gallery. Constructor names below are under the RailroadDiagrams namespace.

NodeUseConstructor example
DiagramComplete diagramDiagram.new("item")
TerminalLiteral tokenTerminal.new("word")
NonTerminalReference to a ruleNonTerminal.new("expression")
SequenceItems in orderSequence.new("a", "b")
StackItems across rowsStack.new("a", "b")
ChoiceOne alternativeChoice.new(0, "a", "b")
OptionalZero or one itemOptional.new("a")
OneOrMoreOne or more itemsOneOrMore.new("a", ",")
ZeroOrMoreZero or more itemsZeroOrMore.new("a", ",")
GroupLabel a groupGroup.new("a", label: "group")
HorizontalChoiceAlternatives side by sideHorizontalChoice.new("a", "b")
OptionalSequenceOptional items in orderOptionalSequence.new("a", "b")
AlternatingSequenceAlternating pathsAlternatingSequence.new("a", "b")
MultipleChoiceOne or more alternativesMultipleChoice.new(0, "any", "a", "b")
ComplexDiagramComplex endpoint markersComplexDiagram.new("item")
RepeatCounted repetitionRepeat.new("item", min: 2, max: 4)
SeparatedListItems with a separatorSeparatedList.new("item", ",")
ExceptExclude a patternExcept.new("letter", "x")
CharClassCharacter classCharClass.new("[a-z]")
SpecialSpecial tokenSpecial.new("EOF")
CommentAnnotationComment.new("note")
SkipEmpty pathSkip.new
StartStart markerStart.new
EndEnd markerEnd.new
BlockCustom dimensionsBlock.new(width: 50)

Choice's first argument selects the default branch by its zero-based index. In the DSL, write choice("a", "b", default: 0).

Optional is a Choice subclass, and ZeroOrMore is an Optional subclass. HorizontalChoice.new and OptionalSequence.new return a Sequence for zero or one child.

See the API reference for constructor options and attributes.

Output formats

Each output method returns a string. Pick the format for the place you will display the diagram.

MethodOutput
to_standalone_svgSVG with CSS, ready to save as a file
to_svgSVG for a page that supplies its own CSS
to_htmlA complete HTML page with SVG and a text alternative
to_textUnicode or ASCII text
to_markdownA fenced text diagram for Markdown
File.write("select.html", diagram.to_html(title: "SELECT syntax"))
puts diagram.to_text
puts diagram.to_text(charset: :ascii)
puts diagram.to_text(charset: :unicode_square, strip_trailing: true)
puts diagram.to_markdown

Text layout accounts for CJK and emoji display widths. Ambiguous-width characters use one column. To embed raw text in HTML, pass escape_html: true.

When displaying Unicode diagrams in a custom page, use a font that gives box characters and spaces the same width, such as Menlo, Consolas, or DejaVu Sans Mono. Use a line height of 1 to keep vertical tracks connected. Generated HTML supplies this styling.

The existing write_svg, write_standalone, and write_text methods accept an IO, a mutable string, or a callable:

File.open("select.svg", "w") { |file| diagram.write_standalone(file) }

write_text escapes HTML by default for compatibility; to_text returns unescaped text by default.

Themes and layout

Built-in themes are default, classic, dark, auto, print, and high_contrast. Compare them in the theme gallery.

File.write("select-dark.svg", diagram.to_standalone_svg(theme: :dark))
File.write("select-wrapped.svg", diagram.to_standalone_svg(max_width: 600))
File.write("select-inline.svg", diagram.to_svg(inline_styles: true))

auto follows the viewer's color scheme. max_width wraps long sequences across rows. inline_styles: true embeds presentation attributes for destinations that strip SVG style elements.

Links and accessible descriptions

diagram = RailroadDiagrams::Diagram.new(
  RailroadDiagrams::NonTerminal.new("expression", href: "#expression"),
  title: "Expression syntax",
  desc: :auto
)
svg = diagram.to_svg(href_mode: :href, locale: :en)

title and desc describe the diagram to screen readers. locale: :ja produces a Japanese automatic description. Use HTTP, HTTPS, email, or relative URLs for links.

precision, arc_radius, char_width, and id_prefix are per-call rendering options. See Options for the available fields.

Linked grammars

A Document combines named rules into one HTML page with links between nonterminals, lists of referring rules, text alternatives, and lint findings.

document = RailroadDiagrams::Document.new(title: "SQL subset")
document.add_rule("query", RailroadDiagrams.build { seq("SELECT", :table) })
document.add_rule("table", RailroadDiagrams.build { choice("users", "accounts") })

File.write("grammar.html", document.to_html)

The table reference links to its rule. References to names that are not defined remain unlinked. Rules and alternatives retain their definition order.

Use index: :alphabetical to sort the navigation or interactive: false to omit the search script:

html = document.to_html(index: :alphabetical, interactive: false)
warnings = document.lint
sections = document.rule_sections

Each section contains name, id, svg, text, and referenced_by, so another tool can place linked rules in its own template.

Import W3C EBNF directly:

grammar = <<~EBNF
  query ::= "SELECT" "DISTINCT"? table
  table ::= "users" | "accounts"
EBNF

document = RailroadDiagrams::Importers::W3cEbnf.parse(grammar)
File.write("grammar.html", document.to_html)

Command line

render reads Ruby DSL files, structural JSON, YAML grammars, or W3C EBNF. It writes linked HTML by default.

railroad_diagrams render grammar.ebnf -o grammar.html --simplify --lint
railroad_diagrams render grammar.yml -o out/ --split --format standalone
railroad_diagrams render grammar.ebnf --format text --charset ascii
railroad_diagrams render grammar.ebnf --rule query --theme dark -o query.html
railroad_diagrams render grammar.ebnf -o grammar.html --watch
railroad_diagrams themes
railroad_diagrams render --help

Use --split to write one file per rule and --rule to choose rules. --simplify removes redundant structure; --lint reports grammar warnings. --watch regenerates output when a file changes.

For standard input, pass - and an explicit --input-format:

cat grammar.yml | railroad_diagrams render - --input-format yaml -o grammar.html

Ruby input executes the named file. Only render Ruby files you trust. YAML, JSON, and EBNF inputs are parsed as data.

To render the bundled examples, use railroad_diagrams demo --format=svg. Demo formats are svg, standalone, ascii, and unicode. The old command without a subcommand still works but emits a deprecation warning.

JSON and YAML

Serialize a node tree to save it or exchange it with another tool. The serialized structure follows the version 1 schema.

json = diagram.to_json
restored = RailroadDiagrams.from_json(json)
puts restored == diagram

yaml = diagram.to_yaml
restored = RailroadDiagrams.from_yaml(yaml)
hash = diagram.to_h
restored = RailroadDiagrams.from_h(hash)

YAML grammar files provide a shorter form for named rules. Arrays are sequences, strings are terminals, and <name> refers to another rule:

title: SQL subset
rules:
  query: [SELECT, "<table>"]
  table:
    choice: [users, accounts]

Render this with railroad_diagrams render grammar.yml -o grammar.html, or pass the YAML string to RailroadDiagrams::Importers::YamlGrammar.parse.

each_node walks the tree in depth-first order. Structural equality compares nodes and their children, making a serialized round trip comparable to the original.

Integrations

Rails

Require railroad_diagrams/rails to register the view helper, then use it in an ERB view:

<%= railroad_diagram { seq("SELECT", :table) } %>

The helper returns an SVG marked safe for the view. See the sample view.

Jekyll and Asciidoctor

The separate Jekyll and Asciidoctor adapter projects render railroad blocks from YAML grammar definitions. Their READMEs explain installation and include sample sites.

Configuration and compatibility

Prefer options on individual output calls when different diagrams need different settings. Use global configuration for application-wide defaults:

RailroadDiagrams.configure do |options|
  options[:theme] = :dark
  options[:precision] = 3
end

The API stability guide lists stable, experimental, and internal APIs. Stable APIs retain source compatibility throughout 1.x; removal requires 2.0.

By default, SVG number precision remains unrestricted and links use xlink:href. Set precision: or href_mode: explicitly to change these choices. Legacy constants such as AR, VS, and CHAR_WIDTH remain available.

For upgrades from 0.x, read the migration guide. Pass links and labels as keyword arguments. Use inspect for debugging instead of the deprecated to_str.