Integrations
merlion render [<input>] [-o <output>] [--width <px>] [--direction auto] [--edge-style orthogonal|polyline|spline] [--font link|embed|system] [--hint <previous.svg>] [--strict] [--outline <file>] [--css <file>] [--theme <name>] [--auto-dark <name>] [--no-auto-tone]merlion css [<input.css>] [-o <output.css>] [--strict] [--follow-symlinks]merlion check [<input>...] [--strict] [--fix]merlion outline [<input>] [--follow-symlinks]merlion --version- The input defaults to stdin and the output to stdout. A Markdown input (
.md,.mdx) renders every```mermaidblock, and-othen names a directory: blockn(1-based) of<name>.mdis written to<dir>/<name>-<n>.svg. A block that fails leaves its previous output file untouched; the other blocks are still written. --hintdefaults to the existing output file when one exists, so re-rendering in place is stable without any extra flag.--no-hintforces a fresh layout.checkparses without rendering and prints diagnostics asfile:line:col: severity code message.--fixapplies everyRepairfix to the file.merlion csscompiles a stylesheet (svg-output.md) from a file or stdin and writes the page CSS to-oor stdout; diagnostics use thecheckformat under the stylesheet’s name.--strictturns everyW017–W019into an error, and an error writes nothing.E013exits3.--cssbakes a stylesheet into the output (svg-output.md).--theme <name>picks the[data-theme="<name>"]block over:root; the default is:rootalone.--auto-dark <name>adds the named block as theprefers-color-scheme: darkvariant. A name the stylesheet does not define is a usage error (exit 2). Neither flag reads the:root:not([data-theme])media block. A Markdown input parses the stylesheet once for all its blocks. Without--css, built-in roles still render in their default tones (svg-output.md).- Decisions, stores, terminals and top-level subgraphs take automatic tones (svg-output.md);
--no-auto-tonesetsauto_tone: falseand draws them untoned, byte-identical to the core with that option. - Front matter and
%%{init}%%never name a stylesheet. - Exit codes:
0every diagram rendered (warnings allowed);1at least one diagram failed to parse or render;2usage error;3at least one input exceeds limits (TooLarge) and none failed otherwise.
File handling
Section titled “File handling”The CLI runs in CI against repositories it doesn’t control, so every write assumes the tree is hostile.
- Every write (
-o,--outline,--fix) goes to a temporary file in the target’s directory, is flushed, and is renamed over the target. The rename replaces a symbolic link rather than writing through it, and a crash never leaves a half-written file. - Unless
--follow-symlinksis given, the CLI refuses to read an input or a hint from, or write to, a path that is a symbolic link or whose resolved directory lies outside the current working directory. This coversrender,check,outlineandcssinputs,--cssstylesheets, Markdown files, and every--batchdirectory entry; a symbolic link in a--batchdirectory is skipped. A link planted in the tree therefore cannot echo a file from outside it (/proc/self/environ, a credentials file) into CI logs through a diagnostic. Standard input is always read. - A hint file larger than the 1 MiB input limit is ignored with
I022 LayoutHintInvalid. - A stylesheet larger than 64 KiB, checked from file metadata before reading, fails with
E013 StylesheetTooLarge. - Arguments are parsed by hand with the standard library (supply-chain.md).
- Distributed as prebuilt binaries for macOS (arm64, x86_64), Linux (x86_64, arm64, musl static) and Windows (x86_64), and through
cargo install.
@fractalbox/merlion-wasm
Section titled “@fractalbox/merlion-wasm”export function init(wasm?: BufferSource | URL | Response): Promise<void>; // browserexport function initSync(wasm: BufferSource): void; // Node, at build timeexport function render(source: string, options?: RenderOptions): RenderResult;export function check(source: string, options?: { strict?: boolean }): Diagnostic[];export function compileStylesheet( css: string, options?: { theme?: string; autoDark?: string; strict?: boolean },): { css: string | null; palette: Palette | null; diagnostics: Diagnostic[] };
interface RenderOptions { width?: number; // container width in px; default 720 direction?: "auto" | "source"; edgeStyle?: "orthogonal" | "polyline" | "spline"; font?: "link" | "embed" | "system"; strict?: boolean; idPrefix?: string; // [a-z][a-z0-9-]{0,31} hint?: string; // the previous SVG, for stable layout fuel?: number; autoTone?: boolean; // automatic tones; default true, false draws them untoned palette?: Palette; // from compileStylesheet; validated against the token grammars}type Palette = { roles: Record<string, string>; // token name without `--merlion-` → value: `#` hex colours, `stroke` as a number string, `c-{name}-{fill|stroke}` may be `none` tones?: Record<string, { tone?: string; dash?: number[] }>; // node and edge roles, in cascade order; `dash: []` is `none` clusterTones?: Record<string, { tone?: string; dash?: number[] }>; dark?: Record<string, string>; darkTones?: Record<string, { tone?: string; dash?: number[] }>; darkClusterTones?: Record<string, { tone?: string; dash?: number[] }>;};interface Diagnostic { severity: "error" | "warning" | "repair" | "info"; code: string; line: number; column: number; // 1-based; 0 without a location byteStart: number; byteEnd: number; message: string; fix: { byteStart: number; byteEnd: number; replacement: string } | null;}interface RenderResult { svg: string | null; outline: string | null; diagnostics: Diagnostic[]; error: RenderError | null; fuelUsed: number;}-
packages/merlion-wasm/index.d.tsis the authoritative JavaScript contract; the rehype plugin, the Astro integration and the docs site’s playground use its names and shapes. Option names are camelCase. The core’s own names (target_width,id_prefix,edge_style,auto_tone) and any other unknown key throw aTypeErrornaming the key (for the core’s names, also the camelCase option), so an older module never silently ignorespalette. Acssstring above 64 KiB returnsE013without crossing the boundary. -
compileStylesheetthrowsRangeErrorfor athemeorautoDarkthe stylesheet does not define. The glue turnspaletteinto the canonical string ofPalette::canonical(shape and separators checked in JavaScript); the core parses it withPalette::parse, which checks every value against its token’s grammar and refuses what no compiled stylesheet produces: more than 256 tones in one table, or a canonical string over 128 KiB. A compiled stylesheet is at most 64 KiB and every palette entry is shorter than the CSS line it comes from, so a light plus a dark table always fit. So a render fromcompileStylesheet’s palette and a CLI render with the same--css/--themeare byte-identical. -
The module returns the JSON of
merlion render --json(merlion_render::json, shared by both surfaces); the glue converts it to the camelCase shape above. -
renderis synchronous after initialisation. Its worst-case time is bounded by the fuel limit (ADR-0008), not by a clock. For source the page doesn’t control, run it in a Web Worker so a heavy diagram never blocks the main thread; the package exports aworker.jsentry that wrapsrenderin a message handler. -
The glue is hand-written: strings cross the boundary as UTF-8 pointer-and-length pairs through exported
alloc/deallocfunctions. Nowasm-bindgenis involved. The glue:- creates a fresh
Uint8Arrayview overmemory.bufferafter every call that may allocate, becausememory.growdetaches earlier views; - checks every returned pointer and length against
memory.buffer.byteLengthbefore reading, and decodes withnew TextDecoder("utf-8", { fatal: true }); - on any trap (allocation failure,
unreachable, stack exhaustion) discards the instance and instantiates a new one from the cachedWebAssembly.Module, then returns{ svg: null, outline: null, diagnostics: [<E001 InternalError>] }, because a trapped instance’s allocator state is undefined; - serialises calls:
renderis not re-entrant on one instance.
- creates a fresh
-
The package’s
dependenciesfield is empty and it has no install scripts.package.jsonrecords the.wasmfile’s SHA-256 undermerlion.wasmSha256. That value sits in the same tarball as the file, so it proves nothing by itself; it is the value to compare against the release’s build attestation and published checksums (supply-chain.md).
@fractalbox/merlion-rehype
Section titled “@fractalbox/merlion-rehype”import rehypeMerlion from "@fractalbox/merlion-rehype";unified().use(remarkParse).use(remarkRehype).use(rehypeMerlion, { width: 720, // RenderOptions.width strict: false, source: "details", // "details" | "none": keep the Mermaid source in a collapsed <details> cacheDir: ".merlion", // previous renders, used as layout hints viewer: true, // wrap each SVG in <merlion-view> fontCss: true, // the page loads @fractalbox/merlion-themes/merlion-font.css; silences the font warning stylesheet: "diagram.css", // compiled once per build; never passed to inline renders});-
Replaces every
pre > code.language-mermaidelement with:<figure id="diagram-{n}" class="merlion-figure"><merlion-view>{svg}</merlion-view><figcaption>{accTitle or title, if any}</figcaption><details><summary>Diagram source</summary><pre><code class="language-mermaid">…</code></pre></details></figure> -
It walks the tree by hand, so it has no
unist-util-visitdependency;@types/hastis a development dependency only. -
It passes
idPrefix=m+ the first 8 hex characters of FNV-1a 64 over the file’s path relative to the project root +-+n, so diagrams from several files on one page keep unique ids (svg-output.md). -
The SVG enters the tree as a
rawnode containing only the core’s output.figcaptionand the<details>source are hast text nodes, so the serialiser escapes them. -
A parse error leaves the code block in place and reports the diagnostic through the unified
vfile(file.message), which fails the build whenstrictis set. -
cacheDirholds one entry per (file path, block index). The entry’s filename is the FNV-1a 64 hash of that pair (with the path relative to the project root) in hex, so no source path can name a location outsidecacheDir. It stores the last SVG and the content hash of the source that produced it. Every build renders every block, with the stored SVG as the layout hint; the stored SVG is never inlined, because anyone who can writecacheDir(a pull-request author committing it, for example) controls its bytes and can compute the public FNV hash (security.md). An entry is rewritten only when the hash or the SVG changes. When a diagram is inserted above others the indices shift and a hint lands on a different diagram; the core then discards it as having too few surviving nodes. -
Writes to
cacheDirfollow the CLI’s file handling rules. -
stylesheetis read under the CLI’s file handling rules (insideroot, not a symbolic link, at most 64 KiB from its metadata), compiled once per plugin instance throughcompileStylesheet(thecompileStylesheetoption replaces the WASM compiler), and exposed asfile.data.merlion.csson files with a diagram. Its warnings and errors are reported once, on the first such file, with the stylesheet’s path and position; understrictan error fails that file. Inline diagrams are rendered without a palette; the page’s cascade themes them. -
The fence’s info string after the language carries per-block options as
key=valuewords:width=<px>(a positive number) sets that block’s container width (```mermaid width=1600) and enters the cache hash. An invalid value is reported with rulefence-metaand the block renders atwidth; other keys are ignored. -
@fractalbox/merlion-rehype/satteriexports the same rendering as a Sätteri hast plugin factory (hastPlugins, Astro 7’s default Markdown processor), with the same figure, ids, cache and stylesheet handling. Sätteri has no vfile, so every message goes toonMessage({ reason, ruleId, file, line, column, fatal }), by defaultconsole.warnasfile:line:col: reason; a fatal message (an error understrict) throws and fails the document.
@fractalbox/merlion-astro
Section titled “@fractalbox/merlion-astro”Registers @fractalbox/merlion-rehype where the configured Markdown processor runs it: first in processor.options.hastPlugins (the Sätteri adapter, Astro 7’s default), first in processor.options.rehypePlugins (unified()), or in markdown.rehypePlugins when Astro has no markdown.processor (Astro 5 and 6). First, so it claims mermaid blocks before a code-block transformer such as Starlight’s Expressive Code rewrites them; any other processor fails the build. Diagnostics go to the Astro logger. It adds merlion-themes.css, merlion-font.css (both from @fractalbox/merlion-themes), the compiled stylesheet when one is set (compiled once in astro:config:setup, written to <cacheDir>/merlion/stylesheet.css and imported after the theme tokens; a refused path, E013 or a failed compile fails the build, warnings are logged), and the <merlion-view> script, only on pages that contain a diagram. Its options match the rehype plugin’s, except that fontCss names the font stylesheet to import (default @fractalbox/merlion-themes/merlion-font.css, false to skip it) and the plugin receives fontCss: true whenever one is imported.
Editors
Section titled “Editors”The same core serves editors in two ways. Rendering puts SVG in the Markdown preview. Language service provides diagnostics, quick fixes and outlines while typing. In an editor, stable layout keeps the preview from jumping on every keystroke: the previous render of each block, held in memory, is the next render’s hint.
Language server: merlion lsp
Section titled “Language server: merlion lsp”A CLI subcommand speaking the Language Server Protocol over stdio, covering .mmd/.mermaid files and ```mermaid fences in Markdown.
textDocument/publishDiagnosticsfrom the parser’s diagnostics (parser.md).textDocument/codeAction: one quick fix perRepair, plus “apply all repairs”.textDocument/documentSymbol: nodes and clusters.textDocument/hoveron a node id: its label and edges, taken from the outline.- JSON-RPC and JSON parsing are hand-written, in keeping with zero runtime dependencies (supply-chain.md). A
Content-Lengthabove 16 MiB or JSON nesting beyond 64 ends the session with an error before any allocation for the body. - Positions: the server offers
positionEncoding: "utf-8"(LSP 3.17) and falls back to UTF-16 code units, converting the parser’s columns (parser.md).
VS Code and Cursor: merlion-vscode
Section titled “VS Code and Cursor: merlion-vscode”Cursor runs VS Code extensions (installed from Open VSX), so one extension serves both. It is published to the Visual Studio Marketplace and to Open VSX. TODO(owner): the Marketplace publisher id.
- Preview: contributes
markdown.markdownItPluginsand extends the built-in preview’s markdown-it. Mermaid fences render through@fractalbox/merlion-wasm(initSync) in the extension host, synchronously, so the preview webview receives finished SVG and runs no renderer. - Theme: maps VS Code theme colours (
--vscode-editor-background,--vscode-editor-foreground, …) onto--merlion-*in a preview stylesheet. The diagram follows the editor theme with no re-render. - Zoom:
@fractalbox/merlion-viewis loaded as a preview script. - Language features: starts
merlion lspfrom the bundled binary for the current platform. The extension build takes each binary from the signed release and checks its SHA-256 against the release attestation; it never rebuilds them. - Rendering cost: the extension host is shared by every extension, so the preview renders with a lower
fuellimit than the CLI and shows the diagnostic in place of a diagram that exceeds it. - Standalone files: a custom editor for
.mmd/.mermaidshows source and preview side by side.
The incumbent, bierner.markdown-mermaid (MIT; 777,735 Open VSX downloads as of 2026-09-22), renders with mermaid inside the preview webview. Merlion’s differences: no renderer in the webview, stable layout while typing, theme-following colours, and quick fixes.
Zed’s Markdown preview already renders Mermaid, natively, through the merman crate (crates/mermaid_render). Zed extensions cannot draw custom UI or images as of 2026-09-22: the visual extension API (discussion #53403) and file-preview API (#59598) are proposals. Two routes follow:
- Now: a Zed extension that registers
merlion lspfor Markdown and Mermaid files, giving diagnostics and quick fixes next to Zed’s own rendering. - Rendering: propose
merlion-renderto Zed as an alternative behindmermaid_render’s interface. Merlion is ano_stdcrate with no dependencies, and its MIT licence is compatible with Zed’s GPL-3.0-or-later. This needs Zed maintainers to agree, and the benchmark results againstmermanare the argument. Zed’s contribution terms (checked 2026-09-22) require a signed Contributor License Agreement before merge and ask that a larger feature start as a GitHub discussion following its feature process, not as a pull request; the proposal starts there.
JetBrains and others
Section titled “JetBrains and others”Out of scope before 1.0. Any editor with an LSP client gets the language server; JetBrains preview rendering would need a plugin built on the CLI.
Markdown and LLM outputs
Section titled “Markdown and LLM outputs”For every rendered page, merlion outline and the rehype plugin’s outline hook produce the plain-text outline (svg-output.md) alongside the Mermaid source. This is what sites put in .md mirrors and llms-full.txt.