[ 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
- 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 and drawn as SVG on the
server: no client script, no stylesheet and no web font. Lines and text draw in currentColor
and fills fall back to 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 uses @sdxc/result.
@sdxc/diagram/markdown walks documents from
@sdxc/markdown, and @sdxc/diagram/ui
renders with remix/component from remix.
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 goes into HTML or XHTML, or into 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 fence = "`".repeat(3);
let parsed = Markdown.parse(`${fence}mermaid
flowchart LR
Parse --> Walk --> Render
${fence}
`);
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 fence marked mermaid into a diagram tag carrying { source } and
leaves every other code block to the next visitor, 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, options?: DiagramOptions): Result<string, DiagramError>
Draws the diagram as an SVG string with every value escaped and every element explicitly closed, so HTML and XML parsers read it the same way.
parseDiagram(source: string, options?: DiagramOptions): Result<SvgElement, DiagramError>
Reads the diagram into the tree toSVG serializes, rooted at an svg element with width,
height, a matching viewBox and max-width: 100%. 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 first child is a title, the drawing's accessible name: options.alt, else the source's
title or accTitle, else the diagram's kind. A desc follows 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, 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
DiagramOptions
{ alt?: string }: the drawing's accessible name, written as its title over the one the
source gives it. An empty alt keeps the source's name.
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. It also copies a fence's {% alt="…" %} annotation onto the tag
as alt. A diagram that does not parse fails the walk with a MarkdownWalkError whose
position is the fence and whose cause is the DiagramError. Its handler is synchronous,
so the walk returns a Result.
createDiagramVisitor(options?: { invalid?: "fail" | "keep" }): Markdown.Visitor
Builds the visitor. invalid: "keep" leaves a diagram that does not parse as the code block it
was written as; "fail" is the default, the behavior of diagram.
renderDiagram
A tag renderer for toHTML's tags option, naming the drawing by the tag's alt. 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 and an optional alt. It builds the SVG as
elements, and renders the source in a <pre><code> block when it does not parse.
DiagramTag
Diagram shaped for toRemix's components option, taking the tag's source and alt as
props.
Theming
Lines and text draw in currentColor. Two custom properties set the fills:
--diagram-fill: node bodies, and the boxes behind line labels. Defaults toCanvas.--diagram-tint: notes, groups and block labels. Defaults tocurrentColormixed 8% intoCanvas.
Labels are set in ui-sans-serif, system-ui and measured with room to spare, so every box
holds its text in 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>. Anything else is a
DiagramError naming the statement.
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 --> C, groupsA & B --> C, and;between statementssubgraph id [Title]…end, nested, with its owndirection; a node named inside a subgraph belongs to it, and a link may name the subgraph as an endclassDef,class,style,linkStyle,clickand:::classare accepted, and the diagram draws in the page's colors
classDiagram and classDiagram-v2
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 { }anddirectionstyle,cssClass,click,linkandcallbackare accepted, and the diagram draws in the page's colors
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 their owndirectionnote left of A : textand multi-line notes closed withend noteclassDef,classandstyleare accepted, and the diagram draws in the page's colors
Pattern: Keep A Live Preview Rendering While A Diagram Is Half-Written
A preview pane renders what it can while an author types. Walk with invalid: "keep" so a
half-written diagram shows as code and the rest of the page still renders.
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 } });
}
Pattern: Fail A Build At The Broken Line
A publish step walks with the default diagram visitor so a broken diagram stops the build.
The walk failure locates the fence and its cause locates the statement inside it; together
they give the line to open in the document.
import { DiagramError } from "@sdxc/diagram";
import { diagram } from "@sdxc/diagram/markdown";
import { Markdown } from "@sdxc/markdown";
import { isFailure } from "@sdxc/result";
function check(file: string, source: string): string | null {
let parsed = Markdown.parse(source);
if (isFailure(parsed)) return `${file}: ${parsed.error.message}`;
let walked = Markdown.walk(parsed.data.document, diagram);
if (!isFailure(walked)) return null;
let fence = walked.error.position?.start.line ?? 0;
let cause = walked.error.cause;
if (cause instanceof DiagramError) {
return `${file}:${fence + cause.line}:${cause.column} ${cause.reason}`;
}
return `${file}:${fence} ${walked.error.message}`;
}
Pattern: Name A Diagram For Screen Readers
The title a screen reader announces comes from the source's title or accTitle unless the
caller supplies an alt, and accDescr adds a longer description. Each entry point takes the
alt in its own place.
import { toSVG } from "@sdxc/diagram";
import { Diagram } from "@sdxc/diagram/ui";
let svg = toSVG(
`flowchart LR
accDescr: An order moves from the cart to payment, then to confirmation
Cart --> Payment --> Confirmation`,
{ alt: "The checkout flow" },
);
<Diagram source={"flowchart LR\n Cart --> Payment"} alt="The checkout flow" />;
let fence = "`".repeat(3);
let markdown = `${fence}mermaid {% alt="The checkout flow" %}
flowchart LR
Cart --> Payment
${fence}
`;
The diagram visitor carries the fence's alt onto the tag, and both renderDiagram and
DiagramTag pass it on.
Pattern: Match The Page's Colors
A diagram inherits color from its container, so styling the container themes the drawing,
in light and dark mode alike.
.prose svg {
color: var(--text-muted);
--diagram-fill: var(--surface);
--diagram-tint: color-mix(in srgb, var(--accent) 12%, var(--surface));
}
Pattern: Serve A Diagram As An SVG File
The string toSVG returns is a complete SVG document, so a request handler can answer with it
directly, for an <img> source, a feed or a download.
import { toSVG } from "@sdxc/diagram";
import { isFailure } from "@sdxc/result";
async function handle(request: Request): Promise<Response> {
let result = toSVG(await request.text());
if (isFailure(result)) {
return Response.json(
{ reason: result.error.reason, line: result.error.line, column: result.error.column },
{ status: 422 },
);
}
return new Response(result.data, {
headers: { "Content-Type": "image/svg+xml" },
});
}