Skip to content

Latest commit

 

History

39 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mermaidtext

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.)

Supported Mermaid subset

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.

graph / flowchart

  • 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 >, circle o, and cross x heads on either end; bidirectional and reversed edges; pipe labels |text|, inline labels A -- text --> B, chained statements A --> B --> C, fan-out A & B --> C & D.
  • Nested titled subgraphs (quoted, bracketed, or id-only titles).
  • Literal \n escapes 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.

stateDiagram

  • stateDiagram and stateDiagram-v2 headers; [*] 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.

classDiagram

  • class X { ... } bodies and id : member colon-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; generics List~T~ displayed as List<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.

erDiagram

  • 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 : label folded into the edge label — CUSTOMER ||--o{ ORDER : places renders the label 1 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.

sequenceDiagram

  • participant / actor with as aliases; 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 with N. ); loop/alt/opt/par/ critical/break with else/and/option rendered as full-width labeled divider rules (no frames or nesting); rect/box blocks are invisible; +/- activation shorthand is stripped.
  • No-ops: activate, deactivate, create, destroy, title, acctitle, accdescr, links, link, properties. Note that create participant D does not register D.
  • Not supported: bidirectional <<->> (misparses); activation bars; block frames; autonumber off still enables numbering. Participant boxes are drawn twice, at the top and bottom rows. Malformed notes or messages abort the parse into the fallback.

Fallback behavior

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.

Library API

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 Lines entry equals the corresponding PlainLines entry.
  • Output is deterministic for identical source and options.
  • The renderer never emits ANSI escapes of its own; Role values are the only styling signal. (Escape bytes present in the diagram source are preserved verbatim in the output, exactly as upstream preserves them.)
  • Render never 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.DiagramType names the recognized family ("flowchart", "stateDiagram", "classDiagram", "erDiagram", "sequenceDiagram") on success, or the source's first word when falling back.

CLI

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 file is absent or -.
  • Writes the plain lines to stdout, one trailing newline per line; never colorizes.
  • -width N sets 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 │
└───────┘  └─────┘

Width semantics

  • Options.MaxWidth (and the CLI's -width) is a terminal display-column limit, not a byte or rune count. A value <= 0 means 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 small MaxWidth values the fallback frame itself can exceed MaxWidth. This is the only case where an output line exceeds the limit.

Safety caps

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

Attribution and license

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 at upstream/mermaid.rs for 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.

About

Terminal Mermaid renderer in pure Go: deterministic Unicode box-drawing art. A faithful port of xAI's grok-build mermaid.rs.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages