mermaidtext renders a subset of Mermaid diagram source as deterministic
Unicode box-drawing art for terminal applications. It is a faithful Go port of
xAI's terminal Mermaid renderer from the
grok-build repository (Apache-2.0),
pinned to commit b189869b7755d2b482969acf6c92da3ecfeffd36,
file crates/codegen/xai-grok-markdown/src/mermaid.rs.
The module is pure Go with no dependencies: no cgo, no Node, no browser, no Mermaid.js, no external process, no network access. Unicode display widths come from vendored tables matching the unicode-width crate upstream uses.
$ printf 'flowchart LR\n A[Draft] --> B{Approve?}\n B -->|yes| C[Ship]\n' \
| go run ./cmd/mermaidtext -width 80
┌───────┐ ╭──────────╮ yes ┌──────┐
│ Draft ├─────▶│ Approve? ├─────▶│ Ship │
└───────┘ ╰──────────╯ └──────┘(The first output line is blank: the renderer reserves a routing row above the boxes, matching upstream.)
This is parity with the pinned upstream file, not with Mermaid.js. Five
diagram families are supported; everything else (for example pie, gantt,
mindmap) renders as a framed raw-source fallback. Per-family details,
including exact parsing rules and upstream line anchors, are in
PORTING.md §2.
- Directions
TB/TD(down, the default),LR,RL,BT. - Node brackets map to three box geometries: rectangle (
[..],[[..]],>..]), rounded ((..),((..)),([..]),[(..)]), and "diamond" ({..},{{..}}) — which draws with rounded corners exactly like a rounded box; there is no diamond geometry. - Solid
--, dotted-.-, and thick==edges; arrow>, circleo, and crossxheads on either end; bidirectional and reversed edges; pipe labels|text|, inline labelsA -- text --> B, chained statementsA --> B --> C, fan-outA & B --> C & D. - Nested titled subgraphs (quoted, bracketed, or id-only titles).
- Literal
\nescapes in labels render as line breaks (in every diagram family's boxed labels) — a deliberate divergence from the pinned upstream, which renders the two characters; see PORTING.md §7.<br>still strips to a space, as upstream does. - Ignored without failing:
classDef,class,style,linkStyle,click,direction. - Not supported: styling and interactivity; triple-circle
A(((x)))misparses; exceeding the node/edge/group caps fails the whole parse into the fallback.
stateDiagramandstateDiagram-v2headers;[*]start/end markers (rendered as●nodes); transitions-->with chains and per-segment: label;state "desc" as id;<<choice>>stereotype (diamond shape);id : description;direction. Notes are skipped without failing.- Composite
state X { ... }bodies are flattened: inner statements parse, but no frame is drawn. - Not supported: rendered notes, composite-state frames, fork/join bars, concurrency regions. A free-text statement or dangling chain aborts the parse into the fallback.
class X { ... }bodies andid : membercolon-form members; annotations rendered as«ann»; 14 relation operators (triangle inheritance, filled and open diamond, arrow, dotted arrow, bare association); cardinalities folded into the edge label; genericsList~T~displayed asList<T>;direction.- Members are capped at 8 per list (attributes and methods separately); the
9th becomes
…. - Ignored without failing:
note,callback,click,link,style,cssClass,classDef,namespace(bodies parse flat). - Not supported: dotted composition/aggregation (
*..,o..); lollipop interfaces; class names containing whitespace. Any unrecognized statement aborts the parse into the fallback.
- Entity declarations, with or without attribute blocks; bracket aliases
p[Person]; relationships with the six-character cardinality operators (||--o{and friends, solid or dotted), with: labelfolded into the edge label —CUSTOMER ||--o{ ORDER : placesrenders the label1 places 0..*. ER edges never have arrowheads. - Attributes are capped at 8 per entity, the 9th becomes
…. - Not supported:
direction(aborts into the fallback); an aliased entity containing spaces on a relationship line breaks the diagram; any unknown statement aborts the parse into the fallback.
participant/actorwithasaliases; participant order is first-mention order; message operators->,-->,->>,-->>,-x,--x,-),--)(async-)/--)render identically to->>/-->>); self-messages;Note over A,B,note left of,note right of;autonumber(prefixes messages withN.);loop/alt/opt/par/critical/breakwithelse/and/optionrendered as full-width labeled divider rules (no frames or nesting);rect/boxblocks are invisible;+/-activation shorthand is stripped.- No-ops:
activate,deactivate,create,destroy,title,acctitle,accdescr,links,link,properties. Note thatcreate participant Ddoes not register D. - Not supported: bidirectional
<<->>(misparses); activation bars; block frames;autonumber offstill enables numbering. Participant boxes are drawn twice, at the top and bottom rows. Malformed notes or messages abort the parse into the fallback.
Every nonblank input renders something. When a diagram cannot be drawn, the
raw source is preserved inside a rounded frame titled
mermaid: <first word of source>:
$ printf 'pie\n "Dogs": 42\n "Cats": 58\n' | go run ./cmd/mermaidtext -width 80
╭ mermaid: pie ──╮
│ pie │
│ "Dogs": 42 │
│ "Cats": 58 │
╰────────────────╯| Trigger | Output | Art.FallbackReason |
|---|---|---|
No parser matched the header (e.g. pie) |
Framed source | FallbackUnsupported |
| A parser matched the header but a statement failed, or a node/edge/group cap was exceeded | Framed source (identical to unsupported) | FallbackUnsupported |
Layout wider than MaxWidth |
Framed source plus a hint line below the frame ("This diagram is too wide to display here — open the image to view it in full.") | FallbackTooWide |
| Canvas would exceed the cell cap | Framed source, no hint | FallbackOversize |
| Blank (whitespace-only) input | No art at all; Render returns ok=false |
— |
FallbackInvalid is defined but currently never reported: parsers decline on
any parse failure exactly as upstream does, so invalid input is
indistinguishable from an unsupported diagram type. See PORTING.md §4 and §7
for details.
import "github.com/spacedock-dev/mermaidtext"
src := "flowchart LR\n A[Draft] --> B{Approve?}\n B -->|yes| C[Ship]\n"
art, ok := mermaidtext.Render(src, mermaidtext.Options{MaxWidth: 80})
if !ok {
// Blank input: the only case with no art.
return
}
if art.Fallback {
// art holds the framed raw source; art.FallbackReason says why.
}
// Plain text output:
for _, line := range art.PlainLines {
fmt.Println(line)
}
// Styled output: map each span's Role to your own styling system.
for _, line := range art.Lines {
for _, span := range line {
emit(span.Text, span.Role) // RolePlain, RoleBorder, RoleNodeText,
// RoleEdge, RoleEdgeLabel, RoleTitle
}
}Invariants:
- Concatenating the span texts of each
Linesentry equals the correspondingPlainLinesentry. - Output is deterministic for identical source and options.
- The renderer never emits ANSI escapes of its own;
Rolevalues are the only styling signal. (Escape bytes present in the diagram source are preserved verbatim in the output, exactly as upstream preserves them.) Rendernever mutates the caller's source. Invalid UTF-8 byte sequences are replaced with U+FFFD in the output; valid sources pass through byte-exact.Art.DiagramTypenames the recognized family ("flowchart","stateDiagram","classDiagram","erDiagram","sequenceDiagram") on success, or the source's first word when falling back.
mermaidtext [-width N] [file]
Build or install from a checkout of this module:
go build ./cmd/mermaidtext # or: go install ./cmd/mermaidtext- Reads stdin when
fileis absent or-. - Writes the plain lines to stdout, one trailing newline per line; never colorizes.
-width Nsets the display-column limit;N <= 0(the default) is unbounded.- Exits zero for supported renders and visible fallbacks alike. Blank input produces no output and exits zero. Nonzero exits are reserved for CLI and I/O errors (bad flags, more than one file argument, unreadable file).
$ printf 'sequenceDiagram\n participant A as Alice\n participant B as Bob\n A->>B: Hello\n B-->>A: Hi back\n' \
| go run ./cmd/mermaidtext -width 80
┌───────┐ ┌─────┐
│ Alice │ │ Bob │
└───┬───┘ └──┬──┘
│ │
│ Hello │
├────────▶│
│ │
│ Hi back │
│◄╌╌╌╌╌╌╌╌┤
│ │
┌───┴───┐ ┌──┴──┐
│ Alice │ │ Bob │
└───────┘ └─────┘Options.MaxWidth(and the CLI's-width) is a terminal display-column limit, not a byte or rune count. A value<= 0means unbounded.- Widths match the Rust unicode-width crate at 0.2.0 (the version upstream
pins) exactly, for chars and for strings: wide CJK glyphs count as 2
columns, combining marks as 0, East Asian ambiguous characters as 1
(narrow), and string measurement clusters
"\r\n", emoji ZWJ/VS16/keycap sequences, and script ligatures such as Arabic lam-alef the way that crate does. Environment variables never affect output. - If a diagram lays out wider than
MaxWidth, it falls back to the framed source with the too-wide hint rather than being clipped or rescaled. - Fallback-frame minimum width: fallback body lines are hard-chunked to
max(MaxWidth - 4, 8)columns and the frame title is never truncated, so at very smallMaxWidthvalues the fallback frame itself can exceedMaxWidth. This is the only case where an output line exceeds the limit.
Arbitrary input terminates quickly and cannot allocate an unbounded canvas. The named limits (values pinned to upstream; the full table with upstream line anchors is in PORTING.md §3):
| Limit | Value | Effect when exceeded |
|---|---|---|
| Nodes / sequence participants | 128 | Parse fails → framed fallback |
| Edges / sequence items | 512 | Parse fails → framed fallback |
| Subgraphs | 24 | Parse fails → framed fallback |
| Subgraph nesting depth | 6 | Parse fails → framed fallback |
| Canvas cells (width × height) | 2,097,152 | Oversize → framed fallback |
| Class members per list / ER attributes | 8 | 9th entry becomes …, rest dropped |
| Node-label wrap width | 24 columns | Labels wrap |
| Wrapped label lines | 4 | Overflow truncated with … |
| Edge-label width | 28 columns | Labels truncated with … |
This module is a derived port of the terminal Mermaid renderer in xAI's
grok-build repository and is licensed under the Apache License 2.0.
- Upstream repository: https://github.com/xai-org/grok-build
- Pinned commit:
b189869b7755d2b482969acf6c92da3ecfeffd36 - Source file:
crates/codegen/xai-grok-markdown/src/mermaid.rs(a verbatim copy is vendored atupstream/mermaid.rsfor provenance and parity comparison)
See LICENSE for the license text, THIRD_PARTY_NOTICES.md for the attribution notice, and PORTING.md for the porting contract: the detailed supported subset, safety caps, fallback modes, the Rust-to-Go file map, and every intentional behavioral difference from upstream.