sdxc

Type to search, or start from one of these:

[ Content & formats ]

@sdxc/diagram

Mermaid sequence, class, state and flowchart diagrams to SVG, with a markdown walk visitor and a component

npm add @sdxc/diagram
pnpm add @sdxc/diagram
yarn add @sdxc/diagram
bun add @sdxc/diagram
Depends on
remix
Used by
blog

Mermaid sequence, class, state and flowchart diagrams to SVG, with a markdown walk visitor and a component.

Diagrams are written in Mermaid's text syntax, the one GitHub renders in a fenced block marked mermaid, and drawn as SVG on the server: no client script, no stylesheet and no web font. The SVG strokes and fills with currentColor and the Canvas system color, so a diagram takes the color of the text around it and follows the page into dark mode.

Installation

npm add @sdxc/diagram

The root entry depends only on @sdxc/result. @sdxc/diagram/markdown walks documents from @sdxc/markdown, and @sdxc/diagram/ui renders with remix/component; both install alongside this package.

Usage

Draw A Diagram

import { toSVG } from "@sdxc/diagram";
import { isFailure } from "@sdxc/result";

let result = toSVG(`sequenceDiagram
    Browser->>+API: POST /posts
    API-->>-Browser: 201 Created`);
if (isFailure(result)) throw result.error;

result.data; // '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 …" role="img"><title>Sequence diagram</title>…'

The string is safe to place in HTML or XHTML, or to save as an .svg file, as it is.

Render Diagrams In Markdown

import { diagram, renderDiagram } from "@sdxc/diagram/markdown";
import { Markdown } from "@sdxc/markdown";
import { toHTML } from "@sdxc/markdown/html";
import { isFailure } from "@sdxc/result";

let parsed = Markdown.parse("```mermaid\nflowchart LR\n  Parse --> Walk --> Render\n```\n");
if (isFailure(parsed)) throw parsed.error;

let walked = Markdown.walk(parsed.data.document, diagram);
if (isFailure(walked)) throw walked.error;

let html = toHTML(walked.data, { tags: { diagram: renderDiagram } });

The visitor turns every fenced block whose language is mermaid into a diagram tag node carrying { source }, and leaves every other code block alone, so it composes with a highlighter through Markdown.compose.

Render With Remix

import { Diagram, DiagramTag } from "@sdxc/diagram/ui";
import { toRemix } from "@sdxc/markdown/remix";

<Diagram source={"stateDiagram-v2\n  [*] --> Draft\n  Draft --> Published"} />;

<article>{toRemix(walked.data, { components: { diagram: DiagramTag } })}</article>;

API

toSVG(source: string): Result<string, DiagramError>

Draws the diagram as an SVG string, every value escaped. Every element has an explicit closing tag, so HTML and XML parsers read it the same way.

parseDiagram(source: string): Result<SvgElement, DiagramError>

Reads the diagram into the tree toSVG serializes, rooted at the svg element. The tree is plain JSON, so it caches and travels in a payload.

let tree = parseDiagram("flowchart LR\nA --> B");
// { type: "element", name: "svg", attributes: { xmlns: "…", viewBox: "0 0 … …", … }, children: [ … ] }

The root carries width, height and a matching viewBox, with max-width: 100% so it shrinks to a narrow column. Its first child is a title naming the diagram, read out as its accessible name, followed by a desc when the source has an accDescr.

DiagramError

The first statement the parser refused. reason says what went wrong; index (0-based), line and column (1-based) locate it in the source, and the message ends with line:column.

parseDiagram("sequenceDiagram\n  A->>B: hi\n  end"); // failure: '"end" without a block to close at 3:3'

Types

SvgNode, SvgElement, SvgText

A node is an element, { type: "element", name, attributes, children } with string attribute values, or text, { type: "text", value } holding the unescaped characters.

@sdxc/diagram/markdown

diagram

The Markdown.walk visitor. A diagram that does not parse fails the walk: the failure's position is the fence, and its cause is the DiagramError locating the statement inside the diagram. Its handler is synchronous, so the walk returns a Result, never a promise.

createDiagramVisitor(options?: DiagramVisitorOptions)

Builds the visitor with options. invalid: "keep" leaves a diagram that does not parse as the code block it was written as; invalid: "fail" is the default.

renderDiagram

A tag renderer for toHTML's tags option. A tag whose source does not parse renders as the escaped source in <pre><code class="language-mermaid">.

@sdxc/diagram/ui

Diagram

A remix/component component taking source. It builds the SVG as elements, and renders the source in a code block when it does not parse.

DiagramTag

Diagram shaped for toRemix's components option, which hands every component its children alongside the tag's attributes.

Theming

Lines and text draw in currentColor. Two custom properties set the fills, and each falls back to the page's background:

  • --diagram-fill: node bodies, and the boxes behind line labels. Defaults to Canvas.

  • --diagram-tint: notes, groups and block tabs. Defaults to currentColor mixed 8% into Canvas.

.prose svg {
	color: var(--text-muted);
	--diagram-fill: var(--surface);
}

Labels are measured from a typical sans-serif face and set in ui-sans-serif, system-ui, with a little room to spare, so every box holds its text whichever system font the reader has.

Supported Syntax

Every kind accepts %% comment lines, a title statement, accTitle: and accDescr:, and a frontmatter block with a title. Labels break onto a new line at <br>.

sequenceDiagram

  • participant A, actor A, and either as a display name

  • Messages ->> -->> -> --> -x --x -) --) <<->> <<-->>, with or without : text, to another participant or to the sender itself

  • Activations: activate A / deactivate A, or + and - before the receiver

  • Note left of A, Note right of A, Note over A and Note over A,B

  • loop, alt / else, opt, par / and, critical / option, break and rect, each closed with end

  • autonumber

flowchart and graph

  • Directions TB, TD, BT, LR and RL, in the header or as direction

  • Shapes A[ ], A( ), A([ ]), A[[ ]], A[( )], A(( )), A((( ))), A{ }, A{{ }}, A[/ /], A[\ \], A[/ \], A[\ /] and A> ], with quoted text for labels that hold brackets

  • Links --> --- -.-> -.- ==> === ~~~, heads > o x at either end, extra dashes for a longer link, and text as -->|text| or -- text -->

  • Chains A --> B --> C and groups A & B --> C, and ; between statements

  • subgraph id [Title] … end, nested, with a direction of its own; a node named inside a subgraph belongs to it, and a link may name the subgraph as an end

  • classDef, class, style, linkStyle, click and :::class are read and the diagram draws in the page's colors

classDiagram

  • class A, class A~T~, class A["Label"] and bodies in { }

  • Members as A : +member or in the body; parentheses make a method, a trailing $ static (underlined) and * abstract (italic), and ~T~ writes as <T>

  • Annotations <<interface>> A or inside the body

  • Relations <|-- *-- o-- --> -- ..> ..|> .., in either direction and with both ends, "1" cardinalities at either end and : label; a parent or whole sits above the class pointing at it

  • note for A "text", note "text", namespace N { } and direction

stateDiagram and stateDiagram-v2

  • A --> B : label, with [*] as each scope's start or end

  • state "Name" as A, A : description, and state A <<choice>>, <<fork>>, <<join>>

  • Composite states state A { … }, nested, with a direction of their own

  • note left of A : text and multi-line notes closed with end note

Anything else is a DiagramError naming the statement.

Pattern: Keep Building When A Diagram Breaks

A preview pane renders what it can while an author types. Walk with invalid: "keep" so a half-written diagram stays code instead of failing the page.

import { createDiagramVisitor, renderDiagram } from "@sdxc/diagram/markdown";
import { Markdown } from "@sdxc/markdown";
import { toHTML } from "@sdxc/markdown/html";
import { isFailure } from "@sdxc/result";

let preview = createDiagramVisitor({ invalid: "keep" });

function render(source: string): string {
	let parsed = Markdown.parse(source);
	if (isFailure(parsed)) return "";
	let walked = Markdown.walk(parsed.data.document, preview);
	if (isFailure(walked)) return "";
	return toHTML(walked.data, { tags: { diagram: renderDiagram } });
}

Written by Sergio Xalambrí. Follow @sergiodxa for new packages, or sponsor the work.