[ Content & formats ]
@sdxc/diagram
Mermaid sequence, class, state and flowchart diagrams to SVG, with a markdown walk visitor and a component
- Installs with
- @sdxc/markdown@sdxc/result
- Depends on
remix- Used by
- blog
- Source
- packages/diagram
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 toCanvas.--diagram-tint: notes, groups and block tabs. Defaults tocurrentColormixed 8% intoCanvas.
.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 eitherasa display nameMessages
->>-->>->-->-x--x-)--)<<->><<-->>, with or without: text, to another participant or to the sender itselfActivations:
activate A/deactivate A, or+and-before the receiverNote left of A,Note right of A,Note over AandNote over A,Bloop,alt/else,opt,par/and,critical/option,breakandrect, each closed withendautonumber
flowchart and graph
Directions
TB,TD,BT,LRandRL, in the header or asdirectionShapes
A[ ],A( ),A([ ]),A[[ ]],A[( )],A(( )),A((( ))),A{ },A{{ }},A[/ /],A[\ \],A[/ \],A[\ /]andA> ], with quoted text for labels that hold bracketsLinks
-->----.->-.-==>===~~~, heads>oxat either end, extra dashes for a longer link, and text as-->|text|or-- text -->Chains
A --> B --> Cand groupsA & B --> C, and;between statementssubgraph id [Title]…end, nested, with adirectionof its own; a node named inside a subgraph belongs to it, and a link may name the subgraph as an endclassDef,class,style,linkStyle,clickand:::classare 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 : +memberor in the body; parentheses make a method, a trailing$static (underlined) and*abstract (italic), and~T~writes as<T>Annotations
<<interface>> Aor inside the bodyRelations
<|--*--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 itnote for A "text",note "text",namespace N { }anddirection
stateDiagram and stateDiagram-v2
A --> B : label, with[*]as each scope's start or endstate "Name" as A,A : description, andstate A <<choice>>,<<fork>>,<<join>>Composite states
state A { … }, nested, with adirectionof their ownnote left of A : textand multi-line notes closed withend 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 } });
}