Native Mermaid diagrams in pure Swift — no JavaScript, no WebView. Parse, lay out, and render 30 Mermaid diagram types, drawn with CoreGraphics/CoreText on Apple platforms and with Silica (Cairo/FontConfig) on Linux.
MermaidKit renders natively on six platforms — macOS/iOS, Linux, Android
(Kotlin Canvas), Windows/.NET (SkiaSharp), WebAssembly, and Flutter
(Dart CustomPainter) — from one Swift layout core, plus any bare surface (a
Raspberry Pi framebuffer, an SDL2/GPU window) via the platform-free raw raster. A
scene contract (SceneWire) carries the fully-resolved diagram across the C ABI;
each platform paints it with a thin native renderer, and the core's output is
proven byte-identical across the five it compiles to (Android, Windows, WASM,
Linux, macOS): CI gates macOS, Linux, WASM and Windows on every PR and push to
main, and Android was verified on an emulator. On Android, Windows, and Flutter an app passes a Mermaid
source string — via JNI, P/Invoke, and dart:ffi — and gets a drawn diagram,
no Swift toolchain of its own. See
docs/notes/cross-platform-conformance.md,
android/, windows/, and flutter/.
Add the package with SwiftPM, then depend on the MermaidRender product (or
MermaidLayout alone for headless parsing and layout):
.package(url: "https://github.com/2389-research/MermaidKit.git", from: "2.7.0")import MermaidRender
struct ReleaseFlow: View {
var body: some View {
MermaidView("""
flowchart TD
A[Start] --> B{Choice}
B -->|yes| C[Do it]
B -->|no| D[Skip]
""")
}
}MermaidView follows the environment's light/dark scheme, sizes to the
diagram (scaling down, never up), and degrades unrecognized sources to
readable monospaced text. Prefer images? One call:
let image = MermaidRenderer.image(
source: "sequenceDiagram\n Alice->>Bob: Hello",
theme: DiagramTheme(prefersDark: false)
)Embedding Mermaid today usually means shipping mermaid.js inside a
WKWebView: a JS runtime per diagram, async round-trips, non-native text,
and a web process in your memory footprint. MermaidKit renders the same
source natively and synchronously — most diagram types render cold in
under ~15 ms on Apple silicon (worst ~25 ms, a dense sankey; parse
sub-millisecond), and results are cached per (source, theme, spacing).
| MermaidKit | mermaid.js + WKWebView | BeautifulMermaid | |
|---|---|---|---|
| Diagram types | 30 | all | 6 |
| Native platforms | macOS, iOS, Linux, Android, Windows, WASM, Flutter (+ framebuffer/SDL) | anywhere a webview runs | Apple |
| Runtime | Swift + CoreGraphics | JS engine + web process | Swift (elk-swift layout) |
| Dependencies | none | mermaid.js bundle | elk-swift |
| Rendering | sync, ~ms, cached | async round-trip | sync + async |
| Output | NSImage/UIImage, PDF, SVG, NSAttributedString, SwiftUI |
HTML/SVG in webview | image, SVG, ASCII |
| Layout engine | network-simplex layering, label-space reservation, fixed-side ports | dagre / ELK | elk-swift |
| Layout verification | geometric linter in CI + stability tests | — | — |
| Density control | DiagramSpacing presets + opt-in grid snap (gridSnap:) + Dynamic Type text sizes (textScale:) |
config | — |
| Syntax coverage | core syntax per type (see matrix) | reference | core syntax, 6 types |
If you need iOS 15 or pixel-parity with mermaid.js, those other rows are good choices. MermaidKit's bet is breadth of native type coverage with zero dependencies and machine-checkable layout quality (and it now exports SVG too — see SVG export).
Every type, rendered by MermaidKit itself, one image per diagram (light and dark): docs/GALLERY.md.
One Swift layout core; a thin native renderer per platform. The parse → layout →
RenderScene pipeline is platform-free, so it cross-compiles to every target and
its output is byte-identical across the five it compiles to (CI gates macOS,
Linux, WASM and Windows; Android was verified on an emulator —
cross-platform-conformance.md). The
fully-resolved scene crosses the C ABI as the language-neutral SceneWire
JSON; each platform paints it.
| Platform | Renderer | Bridge | Get it |
|---|---|---|---|
| macOS / iOS / visionOS | CoreGraphics / CoreText | in-process | import MermaidRender |
| Linux | Silica (Cairo/FontConfig) | in-process | LinuxRaster trait |
| Android | Kotlin Canvas (Skia) |
JNI → .so |
implementation("ai.2389:mermaidkit-android:…") via mavenLocal() until it's on Maven Central — android/ |
| Windows / .NET | SkiaSharp | P/Invoke → .dll |
MermaidKit .NET library (NuGet package planned) — windows/ |
| Flutter | Dart CustomPainter (Skia) |
dart:ffi → shared lib |
flutter/ — the painter runs on every Flutter target; the dart:ffi bridge on native ones |
| WebAssembly | SVG / SceneWire |
WASI | swift build --swift-sdk …wasm |
Beyond those, the platform-free raw raster (MermaidRenderer.rgbaRaster, on
Apple and Linux) drives any bare surface with no display server — a Raspberry
Pi framebuffer (/dev/fb0), an SDL2 window, a GPU texture. See
tools/pi-canvas for an infinite, pannable canvas composited
into a framebuffer.
On Android, the whole integration is one line — and it themes itself to the app and follows the system font size:
MermaidDiagram("flowchart LR\n A[Start] --> B[End]", Modifier.fillMaxWidth())On Windows/.NET and Flutter, a source string in, a Skia diagram out:
var scene = MermaidNative.Scene("flowchart LR\n A[Start] --> B[End]"); // .NET, via P/Invoke
new SceneRenderer().Draw(scene, canvas);final scene = MermaidNative.scene("flowchart LR\n A[Start] --> B[End]"); // Flutter, via dart:ffi
CustomPaint(painter: MermaidPainter(scene!));Both go source string → native Swift core → SceneWire → native renderer,
with no Swift toolchain or scene-building on the consumer's side. Android adds
Material theming (MermaidTheme.fromMaterial), device-font measurement, and
contentDescription from the narration; Windows the same shape via SkiaSharp.
Every bridge takes a text scale for Dynamic Type (textScale, over
mmk_scene_json_options): Flutter apps pass
MediaQuery.textScalerOf(context).scale(12) / 12 (the scale at the diagrams' 12pt
base size; Flutter's scaling can be nonlinear), .NET apps their text scale factor.
Mermaid is the primary input, but it isn't the only one. Four more front-ends
parse into the same IR (Flowchart / ERDiagram / GitGraph) and render
through the same layout and the raster backends — CoreGraphics/CoreText on Apple,
Silica/Cairo on Linux — and, for DOT/Dippin flowcharts and git log, the terminal,
with no re-serialization back to Mermaid text:
- Graphviz DOT —
DOTParser.parse(_:)turns a.dotsource into aFlowchart, handling subgraphs/clusters, attribute defaults,dir=back, and shape mapping. The inverse ships too:DOTExporter.export(_:)emits aFlowchartback as DOT, so MermaidKit doubles as a Mermaid ⇄ DOT converter — flat charts round-trip exactly except for what DOT can't express (subroutine,asymmetricandparallelogramAltcome back as their nearest DOT shape, a--xcross head as an arrow); clustered charts round-trip structurally. Thick (style=bold), invisible (style=invis), circle heads (arrowhead=dot) and link length (minlen) carry both ways. - Dippin (
.dip) —DippinParser.parse(_:)maps Dippin's eight node kinds (agent, tool, human, conditional, parallel, fan_in, subgraph, manager_loop) to flowchart shapes and collapses simplewhenequalities to concise edge labels. - SQL DDL —
SQLDDLParser.parse(_:)turns aCREATE TABLEschema dump into anERDiagram: typed columns;PRIMARY/FOREIGN/UNIQUEkeys, inline and table-level, rendered asPK/FK/UKbadges (ERDiagram.Attribute.keys); andREFERENCESmapped to one-to-many crow's-foot relationships. It copes with dialect quoting ("x",`x`,[x]) and comments, ignores unknown clauses, and degrades tonilon malformed or oversized input. - git log —
GitLogParser.parse(_:)turns rawgit logoutput — piped straight in — into aGitGraph. Topology is resolved in two passes (so the parse never depends on the caller's--date-order/--topo-orderchoice), and branch lanes are derived from the ref decorations ((HEAD -> main, origin/main)), then propagated backward along first-parent ancestry so a feature branch's interior commits share the tip's lane.mermaidkit-term --format gitlogrenders it straight to the terminal.
Each hands its parsed diagram straight to MermaidRenderer.pngData(diagram:theme:) /
image(diagram:theme:). Or skip the parser entirely: the format-aware entry points
take (source, format) and dispatch to the right front-end, so a DOT or Dippin
block gets the same image, sized/themed text attachment, and accessibility
narration that Mermaid does — MermaidRenderer.attachmentString(source:format:theme:),
altText(source:format:), image(source:format:theme:), pngData(source:format:theme:),
with format: .mermaid | .dot | .dippin | .sqlDDL | .gitLog.
All 30 types parse their core syntax — the constructs in the mermaid.js
docs' primary examples, which is what the dense fixtures in
Fixtures/diagrams/ exercise. MermaidKit is not a syntax-complete port of
mermaid.js, and the failure mode is deliberate:
- Unknown diagram dialects →
MermaidParser.parsereturnsnil; hosts show the fenced source (that's whatMermaidViewdoes), andMermaidParser.diagnoseexplains why — with a did-you-mean for typos. - Flowchart statements it can't read → left out of the diagram, and
MermaidParser.diagnosereturns a.notefor each, at its line, so a host can show the source instead of a diagram with parts missing. - Styling/interaction directives (
%%{init:}%%,classDef/class,style,linkStyle,click) → ignored, not fatal: the diagram still parses and renders with MermaidKit's own theme. Ditto comments (%%). - Structural syntax that goes beyond the core — much of it works:
YAML front-matter (all types — the
title:renders as mermaid.js's centred caption, other keys likeconfig:are tolerated and ignored);accTitle:/accDescr:accessibility statements (all types — they feed the generated alt text and read back viaMermaidParser.metadata(in:), never appearing as content); flowchart;-separated statements (graph LR; A --> B; B --> C), entities in labels (#quot;,#35;,&,♥), chained edges (A --> B --> C),&fan-out, inline-- text -->labels, bidirectional<-->, longer links (--->spans two ranks), thick==>and invisible~~~links, circle--oand cross--xheads, edge IDs,:::classtolerance, nestedsubgraphgroup boxes (with innerdirectionand edges that target a group id), ids with-and.(api-gateway,auth.service), the hexagon{{ }}, subroutine[[ ]], double circle((( ))), slanted[/ /][\ \][/ \][\ /]and asymmetric> ]node shapes, and@{ shape: …, label: … }shape data; the full everyday sequence set — combined fragments (loop/alt+else/opt/par/critical/break, nested) withrectbands, activation bars (->>+/->>-,activate),boxgroupings, notes with<br/>line breaks,actorfigures, typed participants (@{ "type": "database" }), create/destroy, true arrow heads for every token (-x,-),<<->>), autonumber badges; gantt directive lines (never phantom bars) andy/M/sdurations; radar positional values; packet+Nrelative widths; treemap:::class; gitGraphcherry-pick; class generics (Box~T~readsBox<T>, as in mermaid); ER attribute keys; state composites, forks, choices. Some still doesn't (mermaid's newer named flowchart shapes, like@{ shape: doc }, draw as rectangles, with their labels intact). If your diagram parses but drops something you wrote, that's a gap: please open an issue with the source.
Not supported anywhere: HTML in labels other than <br/> line breaks (every
backend, raster and SVG/SceneWire alike, breaks the line at <br/> in every
label; flowchart labels drop inline formatting tags like <b> and <code>
for their text, other types render tags as text),
FontAwesome icons, click callbacks, animations, and mermaid.js theming
directives (theming is DiagramTheme's job).
| Type | Header | Type | Header |
|---|---|---|---|
| architecture | architecture-beta |
packet | packet-beta |
| block | block-beta |
pie | pie |
| C4 | C4Context/C4Container… |
quadrant | quadrantChart |
| class | classDiagram |
radar | radar-beta |
| entity-relationship | erDiagram |
requirement | requirementDiagram |
| flowchart | flowchart/graph |
sankey | sankey-beta |
| gantt | gantt |
sequence | sequenceDiagram |
| gitGraph | gitGraph |
state | stateDiagram-v2 |
| journey | journey |
timeline | timeline |
| kanban | kanban |
treemap | treemap-beta |
| mindmap | mindmap |
xychart | xychart-beta |
| zenuml | zenuml |
treeview | treeView-beta |
| venn | venn-beta |
cynefin | cynefin-beta |
| wardley | wardley-beta |
ishikawa | ishikawa-beta |
| eventmodeling | eventmodeling |
swimlane | swimlane-beta |
Cold parse → layout → render to rasterized pixels (the benchmark forces
rasterization — a deferred-drawing NSImage would flatter the numbers), best
of 3, on an Apple-silicon Mac
(the dense per-type fixtures in this repo — real-world diagrams are usually
smaller). Samples are round-robin across types (sequential per-type sampling
biased late types with accumulated heat — a measured 2x swing), best of
three rounds. RenderBenchmarks is a correctness smoke — every fixture must
parse, render, and rasterize — with no wall-clock assertion (timing gates
flake under CI load, so performance never gates a merge). The table below is
opt-in: BENCH_TABLE=1 swift test --filter RenderBenchmarks measures and
prints it. These numbers are machine-specific and drift run-to-run, so this
README keeps only a range, not a table that can re-drift.
On an Apple-silicon Mac, cold: parse is sub-millisecond for every type
except flowchart (~2 ms); most types render under ~15 ms total; the worst
is a dense sankey at ~25 ms, whose translucent flow ribbons dominate. The full
internally-consistent per-type table — and where the time actually goes, plus
whether any of it is worth optimizing — lives in
docs/notes/performance.md, the single source of
truth for these figures.
Rendering is synchronous by design: at these times a first render in a
SwiftUI body is cheaper than a state round-trip, and repeat renders hit the
cache. Input is bounded the same way mermaid.js bounds it:
MermaidParser.maxTextSize (50k chars) caps every source, and
maxEdges (500) caps flowcharts — the one type whose layered layout is
super-linear in edge count. Oversized sources return nil fast; per-type
numeric fields are clamped at parse (durations, bit ranges, tick counts).
Swift 6 language mode, zero concurrency warnings.
Every diagram describes itself: MermaidView exposes a full content
description to VoiceOver ("Flowchart with 12 nodes and 14 connections:
Fenced mermaid block, ..."), attachmentString sets the same text on the
embedded image, and MermaidRenderer.altText(source:) hands it to hosts
directly. Descriptions are generated from the parsed model — type, honest
counts, leading names — deterministically, for all 30 types.
Diagram text also follows the reader's text size. MermaidView tracks
Dynamic Type (dynamicTypeSize), and DiagramSpacing(textScale:) sets it
anywhere else. Text is laid out at the larger size, so boxes, rows and the gaps
labels sit in grow around it, while lines, arrowheads and markers keep their
weight. A scaled-up image would thicken every line and soften bitmaps instead.
The default size is byte-identical to a render without it. Android's
MermaidDiagram/MermaidView follow the system font size the same way.
Beyond that one-liner, MermaidAltText.narrate(_:) produces a step-by-step
walkthrough — a richer companion to describe's summary. It follows a
flowchart's edges through its decisions, reads a state machine from its initial
state, spells out an ER schema's cardinalities, and replays a sequence message
by message; every other type falls back to describe. Deterministic and
length-bounded, and it mirrors describe's API (narrate(_:),
narrate(_:metadata:), narrate(source:)).
The parser and layout engines never crash on hostile input — empty/garbage
sources, 100k-character labels, deep nesting, duplicate/self-referencing
nodes, NaN/Infinity/1e308 values, CRLF, RTL text. An adversarial suite
(AdversarialInputTests) runs the full parse → layout → lint pipeline on all
of it in CI. Numeric input is sanitized at the parser boundary
(non-finite rejected, magnitude clamped) so geometry can't be poisoned.
mermaidkit-term is a CLI that renders Mermaid, DOT, Dippin, or git log source
straight to the terminal — no display server, no window. By default it picks the
best tier the terminal will answer to: Kitty graphics (a real inline image) →
half-block truecolor (1×2 color pixels) → monochrome Unicode box-drawing,
with OSC 11 background detection and capability probing to choose; --mode box
selects colored box-drawing. The raster tiers draw every type; the
box-drawing tiers are flowchart-only and come from the platform-free
MermaidLayout, so they run headless on Linux/CI (the raster tiers need
MermaidRender, which on Linux means the LinuxRaster trait).
swift run mermaidkit-term flowchart.mmd
cat pipeline.dot | mermaidkit-term --format dot --mode plain
swift run mermaidkit-term flowchart.mmd --mode kitty --text-scale 1.5 # larger text
Every diagram also renders to a standalone SVG document — no CoreGraphics, no
platform image. MermaidRenderer.svg(source:theme:) returns the SVG string for
any of the 30 diagram types; MermaidRenderer.renderScene(source:theme:)
hands back the RenderScene behind it.
let svg = MermaidRenderer.svg(
source: "flowchart TD\n A --> B",
theme: DiagramTheme(prefersDark: false)
) // -> "<svg …>…</svg>"RenderScene is a public, Codable, platform-free render IR (in
MermaidLayout): a complete display list — shaped nodes, arrowed edges, text,
containers — with every shape's geometry resolved exactly once, so a backend
just paints primitives in order rather than re-deriving a diamond's vertices or
a cylinder's arcs. Every diagram type lowers to it, and the SVG backend
(SVGRenderer) consumes RenderScene alone — it needs no drawing surface, so
the scene → SVG path runs headless on Linux and in CI. Because the whole tree is
Codable, a scene can also cross a process boundary intact; a cross-process
determinism gate diffs both the raster and the RenderScene/SVG signatures in CI.
RenderScene — projected to the explicit, language-neutral SceneWire JSON
contract — is the foundation for the shipped Android, Windows/.NET and Flutter
renderers and the WebAssembly build. Each consumes the same scene and paints it
natively (Android with Canvas, Windows with SkiaSharp, Flutter with a Dart
CustomPainter), so all thirty diagram
types render on every platform with no per-type work on the consumer side. The
core's SceneWire/SVG output is byte-identical across macOS, Linux, Android,
WASM, and Windows (CI gates all but Android, which was verified on an emulator) — see
docs/notes/android.md,
docs/notes/windows.md, and
docs/notes/cross-platform-conformance.md.
Two library targets carry the pipeline, plus MermaidKitC — the mmk_* C ABI
over MermaidLayout that Android, Windows and Flutter call, shipped static as
MermaidKitC and as the MermaidKitCShared library — and the mermaidkit-term
CLI:
- MermaidLayout — platform-free.
MermaidParser.parse(String)→ per-type models →DiagramLayoutEngine.layout(_:measure:)→ pure geometry (frames, polylines). Text measurement is injected (DiagramTextMeasurer), so layout is fully testable without a display server. - MermaidRender — CoreGraphics/CoreText drawing on macOS 14+, iOS 17+,
and visionOS 1+, and optional Silica (Cairo/FontConfig) drawing on Linux.
The Linux raster backend is behind the
LinuxRasterpackage trait (default OFF): afrom:-pinned consumer resolves a Silica-free graph on every platform — no unstable branch dependency, and Apple hosts never fetch the Cairo/PureSwift stack. Linux users opt in with.package(url: …, from: "2.7.0", traits: ["LinuxRaster"])(orswift build --traits LinuxRaster). Building requires Xcode 26 / Swift 6.2 (package traits, and Silica's graph when enabled, set that toolchain floor). The styling inputs areDiagramTheme(six colors, a categorical palette, a dark-mode flag, and an optionalfontFamilythe raster renderers set and measure every label in) andDiagramSpacing(layout density and text size:textScalelays labels out larger or smaller — boxes, rows and canvases grow around the text instead of the image being scaled — andMermaidViewfollows the environment's Dynamic Type size).
The layered types (flowchart, class, ER, state, swimlane) use network-simplex layer assignment — the same strategy ELK Layered and Graphviz dot default to — with label-space reservation and declaration-order stability, so diagrams stay compact, labels stay readable, and small edits don't reshuffle the layout (all three properties are enforced by tests).
MermaidLayout includes something unusual: every diagram lowers to a
DiagramScene — a Codable, machine-readable IR of boxes, edge routes, and
labels — and DiagramLayoutLinter checks it against geometric invariants of
good layout (no edge through a box, no edge slicing through bare label text,
no overlapping nodes, no off-canvas or colliding labels, no marks escaping a
plot). The linter runs in this
package's test suite over dense fixtures for all 30 types, so layout
regressions fail CI as geometry ("edge #3 passes through node 'DiagramScene' (165pt inside)"), not as pixel diffs.
Separately, DiagramLayoutLinter.gridAlignment(_:unit:) measures how much of a
box-family layout sits on a pixel grid (a GridAlignment metric, not a
violation), and per-type floors in the test suite may only rise.
The scene IR is also the extension seam — and it's now realized across five
platforms. Alongside the lint-focused DiagramScene, a render-focused
RenderScene (see SVG export) lowers from
every type and is consumed by the built-in SVGRenderer to export standalone
SVG, all inside the platform-free MermaidLayout target without touching parsing
or layout. Projected to the SceneWire JSON contract and crossed via the C ABI,
that same scene now drives the shipped native Android (Kotlin Canvas),
Windows/.NET (SkiaSharp) and Flutter (Dart CustomPainter) renderers and
the WebAssembly build
(docs/notes/android.md, docs/notes/windows.md). Further backends welcome.
MermaidView(source, theme:spacing:)— SwiftUI drop-in; theme defaults to the environment color scheme, text size follows Dynamic Type;spacingis the density knob (.compact/.regular/.comfortable, or custom gaps — consulted by flowchart, class, ER, state, and architecture layouts;DiagramSpacing(gridSnap: 4)additionally snaps flowchart geometry onto a 4pt grid).DiagramTheme— six colors + a categoricalpalette(node tints, pie slices, sankey bands…); override the palette to re-skin all 30 types at once. See the Theming article in the DocC docs.MermaidRenderer.image(source:theme:spacing:)— one-shot render, auto-sized;renderImage(...)is the async sibling that renders off the calling thread and propagates cancellation (deliberately a distinct name, so the cheap sync cache-hit path stays reachable from async contexts).MermaidRenderer.attachmentString(source:theme:spacing:)— the diagram as a single-attachmentNSAttributedStringfor embedding in text views.MermaidRenderer.pdfData(source:theme:spacing:)— single-page vector PDF from the same layout and draw code; the export/print path.MermaidRenderer.svg(source:theme:spacing:)— a standalone SVG document for any of the 30 types, andMermaidRenderer.renderScene(source:theme:spacing:)the platform-freeRenderScenebehind it.RenderScene/SVGRenderer(inMermaidLayout) — theCodable, CoreGraphics-free render display list and its SVG backend; consumeRenderSceneto reach a new output format without touching parsing or layout.MermaidRenderer.pngData(diagram:theme:)/image(diagram:theme:)/rgbaRaster(diagram:theme:)— render an already-parsedMermaidDiagramwithout re-serializing to Mermaid text; the path the DOT/Dippin/SQL/git-log front-ends take.DOTParser.parse(_:),DippinParser.parse(_:),SQLDDLParser.parse(_:),GitLogParser.parse(_:)— the non-Mermaid front-ends (Graphviz DOT, Dippin, SQL DDL, andgit logoutput → gitgraph) into the same IR;DOTExporter.export(_:)is the inverse (Flowchart → DOT).MermaidRenderer.attachmentString(source:format:theme:)/altText(source:format:)/image(source:format:theme:)/pngData(source:format:theme:)— format-aware entry points that dispatch onDiagramSourceFormat(.mermaid/.dot/.dippin/.sqlDDL/.gitLog), so a non-Mermaid block gets the same attachment, narration, and image without the consumer touching a parser (.mermaid= the source-only methods). Diagram-basedattachmentString(diagram:title:theme:)/altText(diagram:)too.MermaidRenderer.altText(source:)— a VoiceOver-ready description of the diagram's content, andMermaidAltText.narrate(source:)a step-by-step walkthrough (see Accessibility above).DiagramSpacing(textScale:)— Dynamic Type for diagrams (0.5–3): text laid out at that multiple of its size, the boxes around it grown to fit. Every entry point that takes a spacing honors it, including the headless engines.MermaidRenderer.textMeasurer— the renderer's own CoreText measurer; pass it toDiagramLayoutEngine.layout(_:measure:spacing:)/DiagramScene.lower(_:measure:spacing:)with the same spacing when you want layout or lint geometry to match the render exactly.MermaidParser.parse(_:),MermaidDiagram.typeName, and the per-type layout engines are public for hosts that want geometry without pixels.MermaidParser.diagnose(_:)— why a source failed to parse, with line numbers and did-you-mean suggestions for typo'd headers; for a flowchart that parses, a.notefor each statement it left out.
DocC catalogs ship with the package (Xcode: Product → Build Documentation; Swift Package Index hosts them):
- MermaidRender — Getting Started · Theming (brand themes, palettes, canvas rules) · Embedding in Text Views
- MermaidLayout — Headless Layout (measurer injection, other backends, programmatic diagrams) · Scene Geometry and Linting · Adding a Diagram Type (the full five-file walkthrough)
Plus CONTRIBUTING.md for the review rules (geometry-first) and the most-wanted list.
Does it render on Linux? Yes, since v0.11.0 — with the LinuxRaster package
trait enabled (off by default since v0.12.0), MermaidRender draws with
Silica (Cairo/FontConfig) on Linux, sharing the exact layout and per-type draw
code as the Apple backend (docs/notes/linux-rendering-via-silica.md). The
layout target (MermaidLayout) was always platform-free — and a vector SVG
backend over the scene IR now ships from there too:
MermaidRenderer.svg(source:theme:) exports standalone SVG for all 30 types via
the CoreGraphics-free SVGRenderer (see
SVG export). Without the trait, SVG is still
available headlessly: RenderScene.from(_:theme:measure:) + SVGRenderer in
MermaidLayout.
Does it run on Android / Windows / Flutter / the web? Yes — natively. The
Swift layout core cross-compiles to each; a thin native renderer paints the shared
SceneWire scene (Kotlin Canvas on Android, SkiaSharp on Windows/.NET, a Dart
CustomPainter on Flutter, SVG on WebAssembly). On Android, Windows,
and Flutter an app passes a Mermaid source string — via JNI, P/Invoke, and
dart:ffi — and gets a drawn diagram, no Swift toolchain of its own. See
android/, windows/, flutter/, and the
platform notes in docs/notes/.
Can I render on a Raspberry Pi / SDL / a bare framebuffer? Yes —
MermaidRenderer.rgbaRaster (Apple and Linux) returns raw RGBA pixels with no
display server, so you can blit to /dev/fb0, an SDL2 texture, or a GPU surface.
tools/pi-canvas is an infinite, pannable canvas composited
into a 640×480 framebuffer (with an SDL2 backend), verified on the Pi's aarch64
architecture.
Is the output really the same on every platform? The scene is: the core's
SceneWire/SVG output is byte-identical across macOS, Linux, Android, WASM, and
Windows. A conformance gate checks
macOS, Linux, WASM and Windows on every PR and push to main (Android was
verified on an emulator). Getting there
surfaced (and fixed) two real cross-platform determinism bugs — a Double→JSON
formatting difference between Foundation implementations, and a ceil that
amplified a 1-ULP sin/cos difference in pie tessellation.
Why doesn't the output look exactly like mermaid.js? Deliberate.
MermaidKit renders diagrams in a native Apple aesthetic (system fonts or
your theme's fontFamily, your theme's colors) rather than pixel-cloning mermaid.js's default skin. Same
structure, native skin.
Why macOS 14 / iOS 17? Those are the floors of the app MermaidKit was extracted from. Nothing fundamental blocks lower floors; it's tracked, and PRs verifying older OSes are welcome.
Is it safe to render untrusted input? That's the design point of the input caps, numeric sanitation, and the adversarial suite. No network, no JS, no dynamic code — parse and draw.
Open an issue with (1) the Mermaid source, (2) what you expected (a mermaid.live screenshot is perfect), (3) what MermaidKit did. Parser gaps are usually small, contained fixes — the per-type parser + layout + renderer files are deliberately independent.
MIT.
