npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

archyne-render

v0.5.2-alpha.1

Published

Render Mermaid diagrams to a self-contained SVG string, with orthogonal edge routing and ELK layout

Readme

archyne-render

Draw a Mermaid diagram as a self-contained SVG string — real geometry, real text, and its own colours inlined, so the picture survives the page that made it.

This is Archyne's renderer, extracted. It is not an alternative to Mermaid: Mermaid does the parsing, and this draws what it parsed. If you already have Mermaid, you already have half of it.

Install

npm install archyne-render

mermaid is an optional peer dependency, needed only for the /mermaid entry point. elkjs comes along, and is loaded lazily — a document that carries its own layout never touches it.

Two entry points

They differ in what they cost, which is why they are separate.

import { renderSvg } from "archyne-render";

Draws a graph you have already parsed and laid out. Runs anywhere JavaScript does — no DOM, no Mermaid, no dependencies at all.

import { render } from "archyne-render/mermaid";

const { svg, width, height } = await render(`
flowchart TD
  a["Start"] --> b["Finish"]
`);

Parses with Mermaid and lays out with ELK. Needs a DOM: Mermaid's parser runs every label through DOMPurify, which wants a window. In a browser or a webview that is free; in Node, give it jsdom before importing:

import { JSDOM } from "jsdom";
const dom = new JSDOM("<!doctype html><html><body></body></html>");
globalThis.window = dom.window;
globalThis.document = dom.window.document;
const { render } = await import("archyne-render/mermaid");

Text is measured a little differently in the two places, and it is worth knowing which you are in. A browser is asked directly, through canvas measureText, and the answer is exact. Node has no canvas — jsdom does not implement one — so widths are summed from a table of per-glyph advances measured from the same fonts. On a typical label the two agree to the rounding; what the table cannot see is kerning, so a string like "AVATAR" comes out a few percent wide. It errs wide rather than narrow, which is the harmless direction: a box slightly too big clips nothing.

Three ways to plug it in

Whatever draws your Markdown — VS Code's built-in preview, Markdown Preview Enhanced, a static site generator, a build script — it is one of three shapes. The difference between them is not the tool: it is where the code runs, and therefore whether there is a DOM.

The code below is not an illustration. tests/e2e-recipes.mts in the Archyne repository extracts these blocks from this file and runs them, so a recipe that stopped working fails a build rather than wasting your afternoon.

1. In a preview that runs your script

VS Code's built-in Markdown preview, MPE's preview, any Electron page. A webview is a real browser, so this is the easy one: Mermaid's parser has its DOM and the text measurement is exact.

import { render } from "archyne-render/mermaid";

// Inside a function, not at the top level. A preview script is injected as a
// classic `<script>`, which cannot have top-level `await` — and bundling one to
// `iife` fails outright rather than at run time.
async function drawAll() {
  // Whatever your host turns a mermaid fence into. In VS Code's preview a
  // markdown-it plugin makes `<pre class="diagram">source</pre>`.
  for (const el of document.querySelectorAll("pre.diagram")) {
    const source = el.dataset.source ?? el.textContent ?? "";
    if (el.dataset.done === source) continue; // previews redraw on every keystroke
    el.dataset.source = source;
    el.dataset.done = source;
    const { svg } = await render(source, { theme: "dark", background: false });
    el.innerHTML = svg;
  }
}

// Previews swap their DOM in on every edit, so redraw when it changes.
new MutationObserver(() => void drawAll()).observe(document.body, {
  childList: true,
  subtree: true,
});
void drawAll();

Bundle it: a webview has no module resolution, so Mermaid and ELK have to be in the file you ship. That is a few megabytes, and it is what drawing diagrams in a preview costs — Mermaid's own extension pays the same.

2. In Node, before the page is built

MPE's ~/.crossnote/parser.js, a markdown-it plugin in a static site generator, anything that rewrites the document server-side. There is no DOM here, and Mermaid's parser wants one — so bring jsdom.

The payoff is that the SVG is in the HTML: it survives export to HTML and PDF, which a preview-only script does not.

import { JSDOM } from "jsdom";

const dom = new JSDOM("<!doctype html><html><body></body></html>");
globalThis.window = dom.window;
globalThis.document = dom.window.document;

const { render } = await import("archyne-render/mermaid");

/** Replace every mermaid fence in a Markdown document with its picture. */
export async function drawFences(markdown) {
  const fences = [...markdown.matchAll(/```mermaid\n([\s\S]*?)```/g)];
  let out = markdown;
  for (const [whole, source] of fences) {
    const { svg } = await render(source, { background: false });
    out = out.replace(whole, svg);
  }
  return out;
}

3. Rendered to files, ahead of time

A build step or a file watcher that turns each fence into a .svg beside the document and rewrites the fence to an image. Ugly, and it works everywhere — GitHub, a wiki, a PDF pipeline, a preview that has never heard of Archyne. It asks nobody's permission.

import { writeFile } from "node:fs/promises";
import { JSDOM } from "jsdom";

const dom = new JSDOM("<!doctype html><html><body></body></html>");
globalThis.window = dom.window;
globalThis.document = dom.window.document;

const { render } = await import("archyne-render/mermaid");

/** Write each fence to `<name>-<n>.svg` and point the document at it. */
export async function extractFences(markdown, name, dir) {
  const fences = [...markdown.matchAll(/```mermaid\n([\s\S]*?)```/g)];
  let out = markdown;
  for (const [i, [whole, source]] of fences.entries()) {
    const file = `${name}-${i + 1}.svg`;
    const { svg } = await render(source);
    await writeFile(`${dir}/${file}`, svg, "utf8");
    out = out.replace(whole, `![](${file})`);
  }
  return out;
}

Note what this one needs that the others do not: the SVG has to render as an <img>, with no help from the page. It does — the labels are <text>, not a <foreignObject>, which is the difference between this working and coming back as boxes with no words in them.

Rendering a whole directory

render() is safe to call concurrently and will not go any faster for it. Mermaid's parser keeps one database per diagram family, as a module singleton that every parse clears before it starts, so all parsing here goes through a lock. Promise.all over five hundred fences is correct and finishes in the same time as a for loop: throughput is one diagram at a time, per process.

If that is too slow, the unit to multiply is the process — a worker pool, one render() at a time in each — not the number of calls in flight. A rejected parse does not strand the ones queued behind it, so one malformed diagram in a directory costs you that diagram and nothing else.

What the output is

Geometry as <rect>, <path> and <polygon>; labels as <text> with the line breaks worked out from measured widths; the palette inlined as a <style> inside the <svg>. Nothing resolves against the host page, so the file renders in an <img>, on GitHub, in resvg or librsvg, and in a PDF pipeline — not only in a browser.

Mermaid's own default output puts labels in a <foreignObject>, which those renderers do not paint. This does not.

It does not look like Mermaid

That is the point rather than an oversight. Edges are routed orthogonally, with stubs off each face, rounded corners, hops where two lines cross, and parallel edges spread apart. Connections sharing a face are given berths along it rather than all meeting its centre, so five edges leaving one node's underside fan out instead of coinciding. Labels are placed knowing about one another, so one does not come to rest on a box's words, on another label, or on a line it does not name. Nodes are sized and laid out by ELK. The same source drawn by Mermaid will look different.

Families

All seven that Archyne edits: flowchart, state, ER, class, C4, architecture and sequence. canRender(kind) says so, and renderSvg throws UnsupportedFamilyError for a kind it does not know rather than returning half a picture.

A sequence diagram needs its statement stream as well as its nodes and edges — its rows are its layout. render() supplies it from the parse; a caller of renderSvg passes seqItems.

Icons

Architecture services draw an icon when you supply one, and a labelled box when you do not. They are injected rather than bundled — five Iconify collections is 1.8 MB, and a flowchart should not pay for them:

import { iconNames } from "archyne-render/mermaid";

const icons = Object.fromEntries(
  await Promise.all(iconNames(nodes).map(async (n) => [n, await yourLoader(n)])),
);
renderSvg(nodes, edges, "architecture", { icons });

Markup passed this way is inserted verbatim. Sanitise anything you did not author.

Options

| Option | Meaning | | ------------ | ------------------------------------- | | theme | "dark" (default) or "light" | | padding | Space around the diagram, in px | | background | false leaves the ground transparent | | icons | Resolved icon markup, by name |

Licence

MIT, the same as Archyne.