Skip to content

zed-industries/merman

 
 

Repository files navigation

merman

Mermaid, but headless, in Rust.

CI Crates.io Documentation Crates.io Downloads Made with Rust

License: MIT License: Apache 2.0

Think of merman as Mermaid's headless twin: same language, same diagrams, no browser required.

merman is a Rust, headless re-implementation of Mermaid (baseline: mermaid@11.15.0). Parity is enforced with golden semantic/layout snapshots and upstream SVG DOM baselines, so changes that affect semantics, layout, or rendering are caught and reviewed.

Try it in the browser: Merman Playground.

Choose Your Entry Point

You want to... Start with Notes
Try or share Mermaid diagrams in the browser Merman Playground Static live editor powered by the wasm web package.
Render Mermaid from Rust merman Enable render for SVG, ascii for terminal text, raster for PNG/JPG/PDF.
Use a command-line tool merman-cli Detect, parse, layout, render SVG, render raster formats, and render ASCII/Unicode text.
Embed in a browser or TypeScript app @merman/web wasm-bindgen output plus TypeScript helpers for SVG, JSON, validation, metadata, and DOM rendering.
Parse Mermaid or produce semantic JSON merman-core Parser, metadata, semantic JSON, and typed render models without layout/render dependencies.
Embed from C, C++, Swift, Kotlin, Dart, Python, or another native host merman-ffi Stable C ABI plus platform wrappers. See FFI protocol, Android, Apple, Flutter/Dart, and Python UniFFI.
Work on layout/rendering internals merman-render Low-level layout and SVG stack used by the public merman facade.

What Merman Outputs

  • Semantic JSON for Mermaid diagrams.
  • Layout JSON with computed geometry and routes.
  • Mermaid-like SVG from a fully headless Rust renderer.
  • ASCII/Unicode diagrams for terminals, logs, and documentation snippets.
  • PNG, JPG, and PDF via SVG rasterization/conversion.

Diagram coverage and current parity status live in docs/alignment/STATUS.md.

Install

# Command-line tool
cargo install merman-cli

# Rust library: SVG rendering
cargo add merman --features render

# Rust library: ASCII/Unicode text output
cargo add merman --features ascii

# Rust library: SVG + PNG/JPG/PDF
cargo add merman --features raster

From a local checkout:

cargo install --path crates/merman-cli
cargo build -p merman-ffi --release

Use crates/merman-ffi/include/merman.h and link the platform-specific library artifact from target/release for native embedding.

MSRV is rust-version = 1.87.

Contents

Quickstart (library)

For most Rust applications, start with merman::render::HeadlessRenderer:

use merman::render::HeadlessRenderer;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let renderer = HeadlessRenderer::new().with_diagram_id("readme-example");
    let svg = renderer
        .render_svg_sync("flowchart TD\nA[Start] --> B[Done]")?
        .unwrap();

    println!("{svg}");
    Ok(())
}

Use render_svg_sync() when you want Mermaid-parity SVG. Use render_svg_resvg_safe_sync() when the result will be rasterized or shown by an SVG engine that does not support <foreignObject> well. Use the ascii feature and merman::ascii::HeadlessAsciiRenderer for terminal text output.

Quickstart (CLI)

# Detect diagram type
merman-cli detect path/to/diagram.mmd

# Parse -> semantic JSON
merman-cli parse path/to/diagram.mmd --pretty

# Layout -> layout JSON
merman-cli layout path/to/diagram.mmd --pretty

# Render SVG
merman-cli render path/to/diagram.mmd --out out.svg

# Render terminal text output
merman-cli render --format unicode path/to/diagram.mmd
merman-cli render --format ascii path/to/diagram.mmd

# Terminal text supports common flowchart directions, labels, shapes, and simple subgraphs
printf "flowchart TB\nsubgraph one\nA((Start)) -- go --> B[(DB)]\nend\n" |
  merman-cli render --format ascii -

# Render raster formats
merman-cli render --format png --out out.png path/to/diagram.mmd
merman-cli render --format jpg --out out.jpg path/to/diagram.mmd
merman-cli render --format pdf --out out.pdf path/to/diagram.mmd

Minimal end-to-end example:

cat > example.mmd <<'EOF'
flowchart TD
  A[Start] --> B{Decision}
  B -->|Yes| C[Do thing]
  B -->|No| D[Do other thing]
EOF

merman-cli render example.mmd --out example.svg
merman-cli render --format ascii example.mmd
@'
flowchart TD
  A[Start] --> B{Decision}
  B -->|Yes| C[Do thing]
  B -->|No| D[Do other thing]
'@ | Set-Content -Encoding utf8 example.mmd

merman-cli render example.mmd --out example.svg

Library API details

The merman crate is a convenience wrapper around merman-core (parsing) and output crates such as merman-render (layout + SVG) and merman-ascii (ASCII/Unicode text). Enable the render feature when you want layout + SVG, ascii when you want text output, and raster when you also need PNG/JPG/PDF from Rust (no CLI required).

use merman_core::{Engine, ParseOptions};
use merman::render::{
    headless_layout_options, render_svg_sync, sanitize_svg_id, SvgRenderOptions,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let engine = Engine::new();

    let layout = headless_layout_options();

    // For UIs that inline multiple diagrams, set a per-diagram SVG id to avoid internal `<defs>`
    // and accessibility id collisions.
    let svg_opts = SvgRenderOptions {
        diagram_id: Some(sanitize_svg_id("example-diagram")),
        ..SvgRenderOptions::default()
    };

    // Executor-free synchronous entrypoint (the work is CPU-bound and does not perform I/O).
    let svg = render_svg_sync(
        &engine,
        "flowchart TD; A-->B;",
        ParseOptions::default(),
        &layout,
        &svg_opts,
    )?
    .unwrap();

    println!("{svg}");
    Ok(())
}

If you prefer a bundled "pipeline" instead of passing multiple option structs per call, use merman::render::HeadlessRenderer.

If you already know the diagram type (e.g. from a Markdown fence info string), prefer Engine::parse_diagram_as_sync(...) to skip type detection.

If your downstream renderer does not support SVG <foreignObject> (common for rasterizers), prefer HeadlessRenderer::render_svg_resvg_safe_sync(). Use HeadlessRenderer::render_svg_readable_sync() when you want to keep the original <foreignObject> nodes and add best-effort <text>/<tspan> fallback overlays.

The split is intentional:

  • render_svg_sync is for Mermaid-parity snapshots and callers that want the raw SVG contract.
  • render_svg_readable_sync is for inline previews that can keep <foreignObject> but still want readable fallback text.
  • render_svg_resvg_safe_sync or SvgPipeline::resvg_safe() is for PNG/JPG/PDF export and tools built on resvg / usvg.
  • SvgPostprocessor and ScopedCssPostprocessor are for host applications that need product-specific theme or cleanup passes after a built-in preset.

render_svg_sync intentionally stays Mermaid-parity by default. For consumer-oriented output, use an explicit SVG pipeline:

use merman::render::{
    CssOverridePolicy, HeadlessRenderer, ScopedCssPostprocessor, SvgPipeline,
};

let renderer = HeadlessRenderer::new().with_diagram_id("readme-diagram");
let pipeline = SvgPipeline::resvg_safe().with_postprocessor(
    ScopedCssPostprocessor::new(
        r#"
.node rect {
  stroke: #2563eb;
  stroke-width: 2px;
}
.merman-foreignobject-fallback-text {
  fill: #111827;
}
"#,
    )
    .with_override_policy(CssOverridePolicy::StripExistingImportant),
);
let svg = renderer
    .render_svg_with_pipeline_sync("flowchart TD; A[Layer 7\\nHTTP]-->B;", &pipeline)?
    .unwrap();
# Ok::<(), Box<dyn std::error::Error>>(())

See docs/rendering/SVG_OUTPUT_PIPELINE.md for preset behavior, custom postprocessors that can read diagram type/title/svg id, and scoped CSS examples.

Runnable example:

cargo run -p merman --features render --example svg_pipeline < fixtures/flowchart/basic.mmd > out.svg

Quickstart (FFI and native hosts)

The merman-ffi crate exposes a stable C ABI for non-Rust hosts. The first release candidate supports SVG rendering, ASCII text rendering, semantic JSON, layout JSON, validation JSON, binding metadata, and explicit Rust-owned buffer release.

#include "merman.h"

static const uint8_t source[] = "flowchart TD\nA[Hello] --> B[World]";

MermanResult result = merman_render_svg(source, sizeof(source) - 1, NULL, 0);
if (result.code == MERMAN_OK) {
    /* result.data contains UTF-8 SVG bytes. */
}
merman_buffer_free(result.data);

Every non-empty MermanResult.data buffer must be released with merman_buffer_free. See docs/bindings/FFI_PROTOCOL.md for result codes, options JSON, threading, and compatibility rules.

Higher-level wrappers build on the same ABI:

Math Labels

Math rendering is optional. Enable ratex-math to render supported $$...$$ labels through the pure-Rust RaTeX backend. Flowchart and Sequence support math-only labels and single-formula prose/math labels such as Solve: $$x^2$$:

printf "flowchart LR\nA[\"$$x^2$$\"] --> B\n" |
  cargo run -p merman-cli --features ratex-math -- render --math-renderer ratex -

ASCII/Unicode text output

Enable the ascii feature when you want terminal-friendly text instead of SVG:

Current public text support covers flowchart/graph, sequenceDiagram, classDiagram, erDiagram, and xychart through merman::ascii::render_ascii_sync, typed merman::ascii::render_model, the direct typed helpers (render_flowchart, render_sequence, render_class, render_er, render_xychart), and merman-cli render --format ascii|unicode.

Flowchart text output covers LR/TD/TB/BT/RL root directions, boxed nodes, common terminal shape approximations, labels, open/dotted/thick edges, length spacing, and titled/nested subgraphs with multiline and wrapped title rows.

Sequence text output covers common messages, notes, lifecycle rows, participant boxes, and the primary Mermaid control-block subset: loop, opt, break, rect, par_over, alt, par, and critical. Mermaid-compatible output keeps bottom participant boxes disabled by default; AsciiRenderOptions::with_sequence_mirror_actors(true) and merman-cli render --format ascii|unicode --sequence-mirror-actors enable mirrored participant boxes for terminal output.

Class, ER, and XYChart text output intentionally ship bounded terminal-native subsets: class and ER support boxes, labels, single relationships, layered chain/star multi-relationship layouts, and adjacent-layer crossing layouts resolved by layer reordering. Same-endpoint and simple mixed-parallel relationships render as distinct lanes, simple spanning-level relationships route through side lanes, and isolated unrelated classes/entities render as standalone components beside the relationship layout. Cyclic and denser graph shapes still return clear diagnostics. XYChart renders deterministic compact bars, lines, mixed plots, titles, and axes instead of SVG coordinates.

use merman::ascii::{AsciiRenderOptions, HeadlessAsciiRenderer};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let renderer = HeadlessAsciiRenderer::new()
        .with_strict_parsing()
        .with_ascii_options(AsciiRenderOptions::unicode());
    let text = renderer
        .render_ascii_sync("sequenceDiagram\nparticipant A\nparticipant B\nA->>B: Hello")?
        .unwrap();

    println!("{text}");
    Ok(())
}

Runnable examples:

cargo run -p merman --features ascii --example ascii_output
cargo run -p merman --features ascii --example ascii_output -- --ascii
printf "flowchart LR\nA --> B\n" | cargo run -p merman-cli --features ascii -- render --format ascii -

Showcase

All screenshots below are produced by merman-cli (headless) and committed under docs/assets/showcase/. Each example links to an existing fixture so the README stays honest and reproducible.

Architecture (many groups + sparse services)

Architecture diagram: many groups + sparse services

Fixture: fixtures/architecture/stress_architecture_batch4_many_groups_sparse_services_069.mmd

Mermaid source
architecture-beta
%% Authored stress fixture (Mermaid@11.12.3): many groups with sparse services (group rect bounds).

group g1(cloud)[G1]
group g2(cloud)[G2]
group g3(cloud)[G3]
group g4(cloud)[G4]

service a(server)[A] in g1
service b(server)[B] in g2
service c(server)[C] in g3
service d(server)[D] in g4

a:R -- L:b
b:R -- L:c
c:R -- L:d
Loading

Mindmap (line breaks in labels)

Mindmap diagram: label line break variants

Fixture: fixtures/mindmap/stress_mindmap_br_variants_031.mmd

Mermaid source
mindmap
  %% Authored stress fixture (Mermaid@11.12.3): <br> variants inside labels.
  root((Root))
    n1["line 1<br>line 2"]
    n2["line 1<br/>line 2"]
    n3["line 1<br />line 2"]
    n4["line 1<br \t/>line 2"]
    %% plus whitespace variants (see the fixture for the full set)
Loading

Sankey (dense shared nodes)

Sankey diagram: dense shared nodes

Fixture: fixtures/sankey/stress_sankey_batch1_dense_shared_nodes_007.mmd

Mermaid source
%%{init: {"sankey": {"width": 900, "height": 420, "useMaxWidth": true, "showValues": false, "linkColor": "source", "nodeAlignment": "justify"}}}%%
sankey

%% Source: repo-ref/mermaid/packages/mermaid/src/docs/syntax/sankey.md (dense graphs) + authored stress
In,A,10
In,B,8
In,C,6
A,X,5
A,Y,5
B,Y,3
B,Z,5
C,X,2
C,Z,4
X,Out 1,7
X,Out 2,0.5
Y,Out 1,6
Y,Out 3,2
Z,Out 2,7
Z,Loss,2
Loading

Gantt (date math + excludes)

Gantt diagram: date math + excludes

Fixture: fixtures/gantt/upstream_docs_gantt_syntax_002.mmd

Mermaid source
gantt
    dateFormat  YYYY-MM-DD
    title       Adding GANTT diagram functionality to mermaid
    excludes    weekends
    %% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)

    section A section
    Completed task            :done,    des1, 2014-01-06,2014-01-08
    Active task               :active,  des2, 2014-01-09, 3d
    Future task               :         des3, after des2, 5d
    Future task2              :         des4, after des3, 5d

    section Critical tasks
    Completed task in the critical line :crit, done, 2014-01-06,24h
    Implement parser and jison          :crit, done, after des1, 2d
    Create tests for parser             :crit, active, 3d
    Future task in critical line        :crit, 5d
    Create tests for renderer           :2d
    Add to mermaid                      :until isadded
    Functionality added                 :milestone, isadded, 2014-01-25, 0d

    section Documentation
    Describe gantt syntax               :active, a1, after des1, 3d
    Add gantt diagram to demo page      :after a1  , 20h
    Add another diagram to demo page    :doc1, after a1  , 48h

    section Last section
    Describe gantt syntax               :after doc1, 3d
    Add gantt diagram to demo page      :20h
    Add another diagram to demo page    :48h
Loading

Stress gallery (more fixtures)

Architecture (ports + routing) Mindmap (deep + wide)
Architecture diagram: cross-region services + crosslinks
Fixture: fixtures/architecture/stress_architecture_batch5_services_outside_groups_crosslinks_078.mmd
Note: Architecture diagonal arrowheads are oriented from the rendered edge segment; DOM parity still normalizes geometry against upstream Mermaid.
Mindmap diagram: deep + wide tree
Fixture: fixtures/mindmap/stress_deep_wide_combo_011.mmd

Parity and coverage

  • Baseline: Mermaid @11.15.0.
  • Alignment is enforced via upstream SVG DOM baselines plus semantic/layout golden snapshots.
  • DOM parity checks normalize geometry numeric tokens to 3 decimals (--dom-decimals 3) and compare the canonicalized DOM, not byte-identical SVG text.
  • Corpus size: 3400+ upstream SVG baselines across 23 diagrams.
  • Mermaid diagram families that are present upstream but not implemented here are listed in docs/alignment/STATUS.md.
  • Current coverage and gates: docs/alignment/STATUS.md.
  • ZenUML is supported in a headless compatibility mode (subset; not parity-gated). See docs/adr/0061-external-diagrams-zenuml.md.

Quality gates

This repo is built around reproducible alignment layers and CI-friendly gates:

  • Semantic snapshots: fixtures/**/*.golden.json
  • Layout snapshots: fixtures/**/*.layout.golden.json
  • Upstream SVG baselines: fixtures/upstream-svgs/**
  • DOM parity gates: xtask compare-all-svgs --check-dom (see docs/adr/0050-release-quality-gates.md)

The goal is not “it looks similar”, but “it stays aligned”.

Quick confidence check:

cargo run -p xtask -- verify

Release-level check:

cargo run -p xtask -- verify --strict

--strict adds all-features compilation, the public feature matrix (merman no-default/render/raster and merman-core no-default), workspace clippy, override no-growth, nextest, SVG DOM parity, and full SVG root parity.

For a quick “does raster output look sane?” sweep across fixtures (dev-only):

  • pwsh -NoProfile -ExecutionPolicy Bypass -File tools/preview/export-fixtures-png.ps1 -BuildReleaseCli -CleanOutDir

Limitations

  • SVG <foreignObject> HTML labels are not universally supported (especially in rasterizers). If you need a more compatible output, prefer render_svg_resvg_safe_sync() or the explicit SvgPipeline::resvg_safe() preset.
  • Architecture compound layout and root viewport parity are still geometry-normalized against upstream Cytoscape/FCoSE output; dense compound graphs can still have layout-level differences (see docs/alignment/STATUS.md).
  • Determinism is a goal: output is stabilized via goldens, DOM canonicalization, and vendored/forked dependencies where needed (see roughr-merman).

Architecture notes

  • merman-core owns detection, parsing, stable semantic JSON, and typed render models for the render-optimized path.
  • merman-render owns layout and SVG emission. The default SVG helper uses parse_diagram_for_render_model_sync -> layout_parsed_render_layout_only -> render_layout_svg_parts_for_render_model_with_config, so typed diagrams avoid rebuilding the owned semantic JSON payload.
  • layout_diagram_sync and render_layouted_svg remain compatibility paths for callers that need owned semantic/layout JSON between steps.
  • Parity renderers live under svg/parity/*; large renderers are split by diagram responsibility and generated overrides are treated as compatibility data, not as default model fixes.

Workspace crates

Crate Role
merman Public Rust facade. Enable render, ascii, and/or raster depending on output needs.
merman-cli Command-line interface for detect/parse/layout/render workflows.
merman-core Detection, parsing, metadata, semantic JSON, and typed render models.
merman-render Headless layout, SVG rendering, SVG pipelines, and raster-friendly postprocessing.
merman-ascii ASCII/Unicode terminal rendering for typed models.
merman-ffi Stable C ABI for native hosts and platform wrappers.
merman-bindings-core Shared safe facade behind C ABI and UniFFI bindings.
merman-uniffi UniFFI-generated binding surface, currently used for Python packaging.
dugong Dagre-compatible layout port.
dugong-graphlib Graph container APIs ported from dagrejs/graphlib.
manatee COSE/FCoSE-style compound graph layout ports.
roughr-merman Forked Rough.js-style renderer dependency stabilized for Mermaid parity.

Links

Changelog

See CHANGELOG.md.

License

Dual-licensed under MIT or Apache-2.0. See LICENSE, LICENSE-MIT, LICENSE-APACHE.

About

Mermaid, but headless, in Rust. Mermaid is a Rust, headless, 1:1 re-implementation of Mermaid

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • Rust 84.2%
  • Mermaid 9.7%
  • TypeScript 3.4%
  • JavaScript 1.4%
  • Python 0.7%
  • Dart 0.1%
  • Other 0.5%