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.