mermaid-text
Render Mermaid graph/flowchart diagrams as
Unicode box-drawing text — no browser, no image protocol, pure Rust.
Demo
Input
graph LR; A[Build] --> B[Test] --> C[Deploy]
Output
┌───────┐ ┌──────┐ ┌────────┐
│ Build │─────▸│ Test │─────▸│ Deploy │
└───────┘ └──────┘ └────────┘
Installation
[]
= "0.1"
or:
Usage
Library API
For width-constrained output (terminal-friendly):
let output = render_with_width.unwrap;
The output is a plain String — deterministic, newline-delimited, no ANSI
escapes. Agents and pipelines can parse it line-by-line or search for node
labels by substring.
CLI
Build and run from the crate root:
# From a file
# From stdin
|
# With a column budget
|
After installing (cargo install mermaid-text):
|
Supported Syntax
| Feature | Supported |
|---|---|
graph/flowchart keyword |
yes |
Directions: LR, TD/TB, RL, BT |
yes |
Rectangle A[text] |
yes |
Rounded A(text) |
yes |
Diamond A{text} |
yes |
Circle A((text)) |
yes |
Stadium A([text]) |
yes |
Subroutine A[[text]] |
yes |
Cylinder A[(text)] |
yes |
Hexagon A{{text}} |
yes |
Asymmetric A>text] |
yes |
Parallelogram A[/text/] |
yes |
Trapezoid A[/text\] |
yes |
Double circle A(((text))) |
yes |
Solid arrow --> |
yes |
Plain line --- |
yes |
Dotted arrow -.-> |
yes |
Thick arrow ==> |
yes |
Bidirectional <--> |
yes |
Circle endpoint --o |
yes |
Cross endpoint --x |
yes |
Edge labels |label| and -- label --> |
yes |
Subgraphs (subgraph … end) |
yes |
| Nested subgraphs | yes |
Per-subgraph direction override |
partial |
style <id> fill:#…,stroke:#…,color:#… (color output) |
yes (opt-in) |
linkStyle <i> stroke:#…[,color:#…] (color output) |
yes (opt-in) |
classDef name fill:#…,stroke:#…,color:#… (color output) |
yes (opt-in) |
class id1,id2 className |
yes (opt-in) |
id:::className shorthand (stackable: A:::a:::b) |
yes (opt-in) |
stateDiagram / stateDiagram-v2 |
yes (transformed to flowchart) |
sequenceDiagram (incl. autonumber, notes, activation bars activate/deactivate/inline +/-) |
yes (separate pipeline) |
pie, gantt, journey, erDiagram, etc. |
not supported |
ANSI color (opt-in)
Mermaid style <id> … and linkStyle <indexes> … directives can drive
24-bit ANSI color output. The default rendering is unchanged — zero ANSI
bytes — so existing pipelines and snapshot tests are unaffected.
CLI:
Library:
use ;
let opts = RenderOptions ;
let out = render_with_options?;
// `out` contains \x1b[38;2;…m / \x1b[48;2;…m sequences
Recognised attributes:
| Directive | Attribute | Effect |
|---|---|---|
style <id> |
fill:#RRGGBB |
node interior background |
style <id> |
stroke:#RRGGBB |
node border foreground |
style <id> |
color:#RRGGBB |
node label foreground |
linkStyle <i|default> |
stroke:#RRGGBB |
edge glyph + label foreground (label falls back to stroke if color is omitted) |
linkStyle <i|default> |
color:#RRGGBB |
edge label foreground only |
#RGB and #RRGGBB hex forms are both accepted; unknown keys and bad
hex values are silently ignored so a stray attribute can never break an
otherwise-valid diagram. 24-bit truecolor (xterm-direct / truecolor)
is required — most modern terminals (iTerm2, Kitty, Ghostty, WezTerm,
Alacritty, Terminal.app, GNOME Terminal, Windows Terminal) qualify.
Reusable palettes via classDef + class / ::::
let opts = RenderOptions ;
let out = render_with_options?;
classDef defines a named style; class id1,id2 … and the inline
A:::className shorthand (stackable as A:::base:::overlay) apply
it. Forward references work — class A foo may appear before
classDef foo … in the source. Subgraphs (composite states /
flowchart subgraphs) coloured via class CompositeId styleName
get a coloured border.
Out of scope for now: classDef DEFAULT special semantics
(merge into every class), click directives, sequence-diagram
colors (separate pipeline), subgraph interior fill, and
256-color / 16-color fallbacks.
State diagrams
Both stateDiagram and stateDiagram-v2 are accepted (they share the
same grammar in upstream Mermaid; only the JS renderer differs). State
diagrams are transformed into the flowchart Graph type at parse time,
so the entire flowchart pipeline — layout, edge routing, ANSI color,
ASCII fallback, width compaction — applies to them unchanged.
Example — circuit-breaker FSM:
stateDiagram-v2
direction LR
[*] --> CLOSED
CLOSED --> OPEN : 5 consecutive failures
OPEN --> HALF_OPEN : probe interval elapsed
HALF_OPEN --> CLOSED : probe succeeds
HALF_OPEN --> OPEN : probe fails
CLOSED : All DB calls pass through
CLOSED : Counting consecutive failures
OPEN : DB calls skipped
HALF_OPEN : One probe call allowed
The [*] start marker renders as a small rounded circle with ●; the
[*] end marker renders as a double-circle with ● (visually
distinguishing start from end). State descriptions stack into a
multi-line label inside the box. Default direction is LR —
mermaid-text intentionally diverges from Mermaid's TB default here
because text rendering inserts layer_gap blank rows between each
row of nodes, so TB balloons vertically. Add direction TB near the
top if you want the Mermaid default.
Composite states (state X { … }) render as a rounded rectangle
labeled with the composite's id, enclosing its inner states. Recursive
nesting works. External edges to / from a composite id are rewritten
at parse time so the arrow visibly lands on the composite's inner
start (incoming) or end (outgoing) marker:
stateDiagram-v2
[*] --> Active
state Active {
[*] --> Idle
Idle --> Working : start
Working --> Idle : done
}
Active --> [*]
Each composite has its own [*] scope — [*] inside Active refers
to Active's start/end, distinct from the top-level. Per-composite
direction LR works inside the body.
Supported subset (the "Always" / "Common" tiers of the Mermaid spec):
| Feature | Supported |
|---|---|
[*] start / end markers |
yes |
A --> B / A --> B : label transitions |
yes |
| Self-transitions | yes |
STATE : description (multi-line accumulating) |
yes |
state "Display" as Id |
yes |
state Id bare declaration |
yes |
direction LR/TB/BT/RL |
yes |
%% comments |
yes |
Composite states state X { … } (recursive nesting) |
yes |
| External edge to / from a composite ID | yes (rewritten to scoped [*]) |
Per-composite direction override |
yes |
Concurrent regions -- |
not yet |
<<choice>> shape modifier |
yes (renders as decision Diamond) |
<<fork>> / <<join>> shape modifiers |
yes (renders as Bar perpendicular to flow) |
Notes (note left of X : … / note right of … / note over …, single + multi-line) |
yes |
Floating notes (note "text" as N1) |
silently ignored |
note over X,Y multi-anchor |
silently ignored |
classDef / class / :::className / style / linkStyle |
yes (opt-in via --color) |
class CompositeId styleName (subgraph border colour) |
yes (opt-in) |
click |
silently ignored |
ASCII fallback
For environments that cannot render Unicode box-drawing characters — SSH sessions to old hosts, CI log viewers that strip non-ASCII bytes, terminals configured with legacy code pages — an ASCII-only mode is available:
Library:
let out = render_ascii.unwrap;
// Every character is guaranteed to be < 0x80.
assert!;
CLI:
|
Example output (same source, Unicode vs ASCII):
Unicode:
+-------+ +------+ +--------+
| Build |-----▸| Test |-----▸| Deploy |
+-------+ +------+ +--------+
ASCII:
+-------+ +------+ +--------+
| Build |----->| Test |----->| Deploy |
+-------+ +------+ +--------+
The mapping used: ─ ━ ┄ → -, │ ┃ ┆ → |, all corners/junctions → +,
▸ ◂ ▾ ▴ → > < v ^, ◇ → *, ○ ◯ → o, × → x.
Examples
State machine
graph TD
Idle -->|event| Running
Running -->|done| Done
Running -->|error| Failed
Failed -->|retry| Idle
┌──────┐
│ Idle │
└──────┘
│ event
▾
┌─────────┐ ┌────────┐
│ Running │──────▸│ Done │
└─────────┘ done └────────┘
│ error
▾
┌────────┐
│ Failed │
└────────┘
│ retry
▾
┌──────┐
│ Idle │
└──────┘
Supervisor pattern
graph LR
subgraph Supervisor
direction TB
F[Factory] -->|creates| W[Worker]
W -->|panics| F
end
W -->|beat| HB[Heartbeat]
HB --> WD[Watchdog]
CI/CD pipeline with edge styles
graph LR
subgraph CI
L[Lint] ==> B[Build] ==> T[Test]
end
T ==>|pass| D[Deploy]
T -.->|skip| D
(Thick ==> = critical path; dotted -.-> = optional path.)
Dependency graph
graph LR
App --> DB[(PostgreSQL)]
App --> Cache[(Redis)]
App --> Queue[(RabbitMQ)]
Queue --> Worker[Worker]
Worker --> DB
How It Works
Parse — The hand-rolled parser (parser/flowchart.rs) splits the input
on newlines and semicolons, identifies node definitions, edge chains, and
subgraph blocks, and builds a Graph struct containing typed Node, Edge,
and Subgraph values. Edge style (solid/dotted/thick) and endpoint type
(arrow/circle/cross/none) are parsed and stored.
Layer + order — The layered layout (layout/layered.rs) assigns each node
to a layer via longest-path from sources, then runs an iterative barycenter
heuristic (up to 8 passes with early termination after 4 non-improving passes,
keeping the best-seen ordering) to minimise edge crossings within each layer.
Per-subgraph direction overrides are handled by collapsing the subgraph's
nodes to a single parent layer and then re-running longest-path for downstream
nodes.
Render — A 2D character grid (layout/grid.rs) stores one char per cell
plus a parallel 4-bit direction-mask layer. Each cell's direction mask encodes
which sides have an outgoing line segment; a lookup table converts the mask to
the correct box-drawing glyph (┼, ├, ┤, ┬, ┴, ─, │, etc.)
automatically. Edges are routed via A* pathfinding that treats node bounding
boxes as hard obstacles and already-drawn edges as soft obstacles. After
routing, styled (thick/dotted) edges overwrite the solid glyphs; endpoint
markers (circle, cross, bidirectional arrows) are placed last.
Performance Notes
The library is synchronous and depends only on unicode-width. The A*
router has O(W×H) memory for the grid and O(E × W×H log(W×H)) time in the
worst case, where W and H are the grid dimensions. In practice, graphs of
up to ~100 nodes render in well under 10 ms on modern hardware. Very dense
graphs (hundreds of nodes or edges) may produce large grids that increase
render time proportionally.
Limitations
- Dotted junctions render as solid — Unicode has no dotted T-junction or cross glyphs, so dotted segments revert to solid box-drawing characters at intersections.
- RL/BT subgraph direction does not reverse internal order — nodes inside a RL/BT subgraph are laid out in forward order.
- Deeply-nested alternating
directionoverrides — only the top-level graph direction is used when evaluating whether a subgraph is orthogonal. LR-inside-TB-inside-LR collapses the inner LR band but does not propagate the fix outward. - Long labels in narrow columns — compaction reduces gaps but cannot
reflow labels; extremely narrow
max_widthvalues may produce overlapping node boxes.
Contributing
- Run
cargo test -p mermaid-text --all-targets— all tests must pass. - Run
cargo clippy -p mermaid-text --all-targets -- -D warnings— no warnings. - Run
cargo doc -p mermaid-text --no-deps— no doc warnings. - Add or update tests for any behavioural change.
- Update this README and
CHANGELOG.mdif the change is user-visible.
License
MIT
Acknowledgements
Rendering techniques — including the direction-bit canvas, barycenter heuristic constants, and subgraph border padding — were adapted from termaid, a Python prior art library for rendering Mermaid diagrams as terminal text.