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

merslim

v0.2.3

Published

A slimmer mermaid — SVG + ASCII diagram renderer for mermaid-style syntax. 14 native diagram types, zero mermaid runtime dependency.

Readme

merslim

A slimmer mermaid — SVG-first diagram renderer for mermaid-style syntax. 14 native diagram types, zero mermaid runtime dependency.

| | | |---|---| | Bundle | ~160 KB minified ESM (vs. mermaid's ~3 MB lazy-loaded / ~7 MB full) | | Runtime deps | dagre only | | Peer deps | react ^18 \|\| ^19, react-dom ^18 \|\| ^19 | | Mermaid | none — parsers and renderers are native | | Outputs | Standalone SVG, PNG (via canvas), and plain-text ASCII (Unicode box-drawing) | | License | MIT |

Why

Mermaid is great, but it's heavy, opaque, and not easy to extend. merslim re-implements the popular subset of mermaid syntax with:

  • A small, typed intermediate representation (IR) you can build programmatically — no need to round-trip through text if you have structured data.
  • A pluggable renderer registry. Diagrams are React components; lazy-loaded by type so you only pay for what you use.
  • A serializer that walks the live DOM, inlines getComputedStyle() values onto a clone, and emits a self-contained SVG that opens identically in browsers, Inkscape, and Office.
  • No vendor lock-in to a single visual style — every renderer is ~150–400 lines of plain React/SVG, easy to fork.

Supported diagram types

flowchart · sequenceDiagram · erDiagram · classDiagram · stateDiagram-v2 · gantt · timeline · pie · quadrantChart · journey · mindmap · architecture-beta · C4Context (Container / Component / Deployment) · gitGraph

Install

npm install merslim

Quick start

import { DiagramRenderer, bootstrapDiagramRenderers } from 'merslim';

bootstrapDiagramRenderers(); // call once at app startup

const source = `
flowchart LR
  A[Edit] --> B{Render}
  B --> C[Export]
`;

export function App() {
  return <DiagramRenderer source={source} />;
}

With the export toolbar

import { useRef } from 'react';
import {
  DiagramRenderer,
  DiagramExportToolbar,
  type RendererHandle,
} from 'merslim';

export function MyDiagram({ source }: { source: string }) {
  const handleRef = useRef<RendererHandle | null>(null);

  return (
    <div className="group relative">
      <DiagramRenderer source={source} handleRef={handleRef} />
      <DiagramExportToolbar
        source={() => handleRef.current?.getSvgElement() ?? null}
        className="absolute top-2 right-2 opacity-0 group-hover:opacity-100"
      />
    </div>
  );
}

The toolbar gives you four buttons — copy SVG, copy PNG, download SVG, download PNG — all routed through the same standalone-SVG serializer. Pass an optional asciiSource to add two more (copy ASCII, download .txt); see ASCII output below.

Headless / SSR

If you only need an SVG string (e.g. to generate diagrams at build time for an MDX blog), skip the React component entirely:

import { parseToIR, flowchartToSvg, type FlowchartIR } from 'merslim';

const result = await parseToIR(`
flowchart LR
  A --> B
`);

if (result.ok && result.type === 'flowchart') {
  const svg = flowchartToSvg(result.ir as FlowchartIR, { dark: false });
  // write to disk, embed, ship to a CDN...
}

Two builder flavors. For each graph-shaped diagram there's a one-call convenience builder (flowchartToSvg, classToSvg, erToSvg) that runs layout internally, and a position-taking power-user builder (buildFlowchartSvg(ir, positions, opts)) for callers who want custom layout. Chart-shaped diagrams (pie, quadrant, journey, gantt, timeline, c4, architecture, gitgraph) are one-call already. See examples/headless/generate.ts for the full pattern.

ASCII output

Every one of the 14 diagram types also renders to plain text using Unicode box-drawing characters — useful for terminals, CI logs, code review comments, plain-text emails, and LLM tool outputs.

import { sourceToAscii } from 'merslim';

const text = await sourceToAscii(`
flowchart LR
  A[Start] --> B --> C[End]
`);
console.log(text);
//  ┌───────┐    ┌─────┐    ┌─────┐
//  │ Start │────▶  B  │────▶ End │
//  └───────┘    └─────┘    └─────┘

If you already have an IR (e.g. from parseToIR or hand-built), use the synchronous asciiFromIR(ir) instead:

import { asciiFromIR, parseToIR } from 'merslim';

const result = await parseToIR(source);
if (result.ok) {
  const text = asciiFromIR(result.ir);
}

Per-type builders are also exported when you only need one: buildFlowchartAscii, buildSequenceAscii, buildErAscii, buildClassAscii, buildStateAscii, buildMindmapAscii, buildGanttAscii, buildJourneyAscii, buildPieAscii, buildTimelineAscii, buildQuadrantAscii, buildGitGraphAscii, buildArchitectureAscii, buildC4Ascii.

In the toolbar

Pass an asciiSource function to <DiagramExportToolbar/> to surface two extra buttons ("ASCII" copy / ".TXT" download):

import {
  DiagramRenderer,
  DiagramExportToolbar,
  asciiFromIR,
  parseToIR,
  type DiagramIR,
  type RendererHandle,
} from 'merslim';

export function MyDiagram({ source }: { source: string }) {
  const handleRef = useRef<RendererHandle | null>(null);
  const [ir, setIr] = useState<DiagramIR | null>(null);

  useEffect(() => {
    parseToIR(source).then((r) => setIr(r.ok ? r.ir : null));
  }, [source]);

  return (
    <div className="group relative">
      <DiagramRenderer source={source} handleRef={handleRef} />
      <DiagramExportToolbar
        source={() => handleRef.current?.getSvgElement() ?? null}
        asciiSource={() => (ir ? asciiFromIR(ir) : null)}
      />
    </div>
  );
}

Build your own IR

The parser is one way to produce an IR; you can produce one any way you like. If you have structured data (a list of orders, a service topology, a customer journey) you can skip mermaid syntax entirely:

import { flowchartToSvg, type FlowchartIR } from 'merslim';

const ir: FlowchartIR = {
  type: 'flowchart',
  direction: 'LR',
  nodes: [
    { id: 'a', label: 'Order received', kind: 'start' },
    { id: 'b', label: 'Validate payment', kind: 'decision' },
    { id: 'c', label: 'Ship', kind: 'end' },
  ],
  edges: [
    { source: 'a', target: 'b' },
    { source: 'b', target: 'c', label: 'paid' },
  ],
};

const svg = flowchartToSvg(ir, { dark: false });

Selective renderer registration

bootstrapDiagramRenderers() is a convenience that registers all 14 native renderers (lazy-loaded, so unused ones stay out of your initial bundle). If you only need a subset and want to skip even the lazy chunks, call register() directly:

import { register, DiagramRenderer } from 'merslim';

register({
  type: 'flowchart',
  loader: () =>
    import('merslim').then((m) => ({ default: m.FlowchartRenderer })),
});
// Now <DiagramRenderer/> only knows about flowcharts. Any other diagram type
// surfaces a "no renderer registered" error.

If you already have an IR and want to skip the source-string parser entirely, the 14 renderers are also exported directly and can be mounted as standalone components — <FlowchartRenderer ir={ir} dark={dark}/>, <PieRenderer/>, <SequenceRenderer/>, etc.

Dark mode

Pass a dark prop to <DiagramRenderer/>, or use the helper to track a .dark class on <html>:

import { isDarkMode, watchDarkMode } from 'merslim';

const [dark, setDark] = useState(isDarkMode);
useEffect(() => watchDarkMode(setDark), []);

API surface

Components

| Export | Purpose | |---|---| | <DiagramRenderer source dark handleRef onError/> | Parses a source string, dispatches to the matching renderer, exposes a RendererHandle ref. | | <DiagramExportToolbar source asciiSource filenameBase pngScale .../> | Copy/download toolbar — 4 buttons by default (SVG/PNG copy + download), plus 2 more (ASCII copy + .txt download) when asciiSource is supplied. | | <FlowchartRenderer/> <SequenceRenderer/> <ERRenderer/> <ClassRenderer/> <StateRenderer/> <GanttRenderer/> <TimelineRenderer/> <PieRenderer/> <QuadrantRenderer/> <JourneyRenderer/> <MindmapRenderer/> <ArchitectureRenderer/> <C4Renderer/> <GitGraphRenderer/> | Direct-mount renderer per diagram type. Same RendererProps<T> signature: { ir, dark, handleRef }. Use these when you already have an IR. |

Parser / IR

| Export | Type | Purpose | |---|---|---| | parseToIR(source) | (string) => Promise<ParseResult> | Mermaid syntax → typed IR. On success, narrows to { ok: true, type: DiagramType, ir: DiagramIR }. | | detectDiagramType(source) | (string) => Promise<RecognizedDiagramType \| null> | Lightweight first-line check. Returns null for empty input, 'unsupported' for unrecognized headers. |

Builders (headless)

| Convenience (auto-layout) | Power-user (explicit positions) | |---|---| | flowchartToSvg(ir, opts) | buildFlowchartSvg(ir, positions, opts) | | classToSvg(ir, opts) | buildClassSvg(ir, positions, opts) | | erToSvg(ir, opts) | buildErSvg(ir, positions, opts) | | — | buildStateSvg(ir, { topLevel, children }, opts) | | — | buildMindmapSvg(ir, positions, opts) |

Plus the chart-shaped diagrams which never need positions: buildPieSvg, buildQuadrantSvg, buildJourneySvg, buildGanttSvg, buildTimelineSvg, buildArchitectureSvg, buildC4Svg, buildGitGraphSvg.

All builders take a final { dark, padding } options object and return a self-contained SVG string with role="img" and an aria-label.

ASCII builders (headless)

| Export | Type | Purpose | |---|---|---| | sourceToAscii(source) | (string) => Promise<string \| null> | One-call mermaid source → text. null only when parsing fails. | | asciiFromIR(ir) | (DiagramIR) => string \| null | Synchronous IR → text dispatch. Covers all 14 diagram types. | | build{Type}Ascii(ir) | (IR) => string | Per-type builder. One per diagram type — same naming as the SVG builders. |

Export pipeline

| Export | Purpose | |---|---| | toSvgString(source, opts) | Serialize any SvgSource to a standalone SVG string. | | svgToPngBlob(svg, opts) | Rasterize an SVG string to a PNG Blob. | | downloadSvg / downloadPng / downloadText | Trigger a file download (SVG, PNG, or plain text). | | copySvgToClipboard / copyPngToClipboard / copyTextToClipboard | Write SVG (text), PNG (image), or arbitrary text to the clipboard. | | getSvgDimensions(svg) | Best-effort intrinsic size from viewBox / attrs. |

Registry

| Export | Purpose | |---|---| | register({ type, loader }) | Register a renderer for a diagram type. | | bootstrapDiagramRenderers() | One-shot registration of all built-in renderers. | | getRenderer(type) / hasRenderer(type) | Introspect the registry. |

Examples

  • examples/playground/ — Vite + React playground with all 14 diagram types, a live source editor, dark-mode toggle, and the export toolbar (SVG + PNG + ASCII). npm install && npm run dev (or npm run build for a static dist/ that opens directly from file://).
  • examples/headless/ — Node script that emits one self-contained SVG and one Unicode-box-drawing .txt per diagram type. npm install && npm start.
  • examples/react/App.tsx — Minimal React snippet showing the same component wiring (including ASCII toolbar buttons) without the playground chrome.

Development

npm install
npm run type-check   # tsc --noEmit
npm test             # vitest run
npm run build        # tsup → dist/ (ESM + CJS + .d.ts)

Notes & limitations

  • The built-in renderers use a handful of Tailwind utility classes for surrounding chrome (loading / error states). The diagrams themselves are pure SVG and render correctly without Tailwind; only the wrapper container styling looks bare. PRs to make this opt-out are welcome.
  • parseToIR is asynchronous because some builders (gantt, timeline) defer parsing work. Today the body is synchronous but the signature is stable.

Mermaid compatibility

merslim parses a curated subset of mermaid syntax. The full contract is enforced by test/compatCorpus.ts — every entry there is a parse-time test that runs in CI.

Known gaps (parse but produce a partial IR, or fail outright):

| Diagram | Gap | |---|---| | flowchart | Multi-target shorthand A & B --> C & D | | flowchart | Trapezoid shape [/Foo\] (falls back to rect) | | flowchart | <br/> in labels treated as literal text, not a line break | | sequence | loop/alt/opt/par blocks parse but render flat (no visual nesting) | | sequence | autonumber keyword accepted but not honored | | sequence | Bidirectional <<->> arrow | | class | Generics class Container~T~ | | class | Cardinality labels "1" --> "*" | | class | namespace { ... } blocks | | state | Parallel/concurrent regions (-- separator) | | state | <<choice>>/<<fork>>/<<join>> pseudo-states | | gantt | excludes/todayMarker/tickInterval accepted but not modeled |

If you hit a case not listed here, add it to the corpus as a known-gap entry — that converts an issue into an executable spec.

License

MIT. See LICENSE.