31 July 2026

Mermaid Diagrams in Cryogen

This blog now renders Mermaid diagrams from ordinary fenced code blocks. Write a ```mermaid fence and you get a diagram:

graph LR
  A[Markdown] --> B[flexmark]
  B --> C[HTML]
  C --> D{diagram?}
  D -->|yes| E[render with mermaid]
  D -->|no| F[highlight as code]

The interesting part wasn't the rendering. It was that Cryogen already did most of the work, and that the obvious implementation had a security flaw.

flexmark already emits the hook

Cryogen parses Markdown through cryogen-flexmark, which configures the renderer like this:

(.set HtmlRenderer/FENCED_CODE_LANGUAGE_CLASS_PREFIX "")

That empty prefix means a ```mermaid fence renders as <pre><code class="mermaid">: a bare class, not the CommonMark-conventional language-mermaid. So the hook to find diagrams already existed. No parser changes, no fork.

highlight.js gets there first

The catch: this theme runs hljs.initHighlightingOnLoad() over every pre code on the page. Left alone, highlight.js happily "highlights" the diagram source, injecting <span> tags into it and handing Mermaid a soup of markup instead of a graph definition.

Cryogen has hit this before. Its Klipse integration tags blocks with a nohighlight class specifically so the highlighter skips them. The fix here reaches the same outcome structurally: a script swaps each <pre><code class="mermaid"> for a <div class="mermaid"> during body parse, which happens before highlight.js's DOMContentLoaded handler runs. By the time hljs looks for code blocks, the diagram is no longer one.

That also gives the no-JavaScript story for free. If the script never runs, the original <pre><code> is left untouched and the diagram degrades to a readable code block. Feed readers see neither, as it turns out: cryogen builds RSS descriptions from paragraphs and headings only, so code blocks never reach the feed at all.

The ESM build makes SRI meaningless

I wanted the library pinned and verified with Subresource Integrity, the way this theme already loads Bootstrap and jQuery. The modern instinct is to reach for the ESM build:

<script type="module">
  import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11.16.0/dist/mermaid.esm.min.mjs';
</script>

That would have been a mistake. An integrity attribute covers only the file it's attached to. It does not extend to modules that file imports. And mermaid's ESM entry point is a 29 KB shim that references 52 of the 103 chunk files shipped in the package: 16 shared chunk-* modules plus 36 lazily imported per-diagram modules (flowDiagram-*, sequenceDiagram-*, and so on):

graph TD
  E["mermaid.esm.min.mjs<br/>29 KB - hash checked"] --> C1["16 shared chunk-* modules<br/>unverified"]
  E --> C2["36 per-diagram modules<br/>unverified"]

Verifying 29 KB of a 3.4 MB payload while looking fully protected is worse than not bothering. The UMD build has no dynamic imports at all: one self-contained file, fully covered by one hash. That's what this site loads, and the integrity hash actually means something.

Only paying for it when it's used

The bundle is about 950 KB gzipped, which is a lot to hand every reader of a mostly-text blog. So the bootstrap script bails out before touching the network when there's nothing to draw:

var codes = document.querySelectorAll('pre > code.mermaid');
if (!codes.length) { return; }   /* no diagrams -> never fetch the library */

Pages without a diagram (most of them) issue no request at all.

Theme toggling

Mermaid bakes its colors into the SVG it generates, so the dark/light toggle in the navbar can't just restyle a drawn diagram; it has to redraw it. Each diagram keeps its original source in a data-src attribute, and the toggle handler clears Mermaid's data-processed marker and re-renders with the new theme.

Diagrams are rendered one at a time rather than in a single batch. Mermaid's run() surfaces only the first error from a batch, and it blanks each element before parsing it, and with suppressErrorRendering on nothing refills that blank when a diagram fails. So one bad diagram in a batch is an unattributable gap. Rendering one diagram per call means a diagram with a typo in it shows its own source and leaves every other diagram on the page intact.

Sequence diagrams work too

sequenceDiagram
  participant B as Browser
  participant S as b12n.net
  participant C as jsDelivr
  B->>S: GET /posts/some-post/
  S-->>B: HTML with <code class="mermaid">
  Note over B: swap to div, before hljs runs
  B->>C: GET mermaid.min.js (SRI checked)
  C-->>B: 3.4 MB UMD bundle
  Note over B: render, themed to match the page

The whole thing is a template change and a stylesheet block. No new build dependency, no Node in the deploy path, and the Markdown stays plain text.

Tags: cryogen tools clojure