Skip to content

Add native Mermaid diagram support - #300

Closed
KiaraGrouwstra wants to merge 7 commits into
feel-co:mainfrom
KiaraGrouwstra:mermaid
Closed

KiaraGrouwstra wants to merge 7 commits into
feel-co:mainfrom
KiaraGrouwstra:mermaid

Conversation

@KiaraGrouwstra

@KiaraGrouwstra KiaraGrouwstra commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

This adds native support for Mermaid diagrams in mermaid fenced code blocks.

Design

  • ndg-commonmark has a new mermaid option. When it is set, the processor renders each mermaid code block as <pre class="mermaid"> with the diagram source as text, and does not highlight it. Other code blocks are highlighted as before.
  • ndg-config has a new [mermaid] section:
    [mermaid]
    enable = true
    # A URL, or a local file that NDG copies to `assets/`.
    script = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"
    script defaults to Mermaid 11 from jsDelivr. A value that starts with https://, http:// or // is a URL. Any other value is a local path, and validation fails if the file does not exist.
  • When Mermaid is enabled, NDG adds assets/mermaid-init.js to each page, with the library location in data-mermaid-src. The script loads the library only on a page that has a diagram, because the library is large. It initializes Mermaid with startOnLoad: false, securityLevel: "strict" and the Mermaid theme that agrees with data-theme on <html>, or with the color scheme of the browser. On ndg:themechange (from the theme toggle), it draws the diagrams again in the new theme. A MutationObserver draws diagrams that the page adds later.
  • NDG copies a local library to assets/ as is, without the JS postprocessing, because the library is already minified. Users can override mermaid-init.js with a template directory, as with main.js.
  • ndg-builder has a mermaid option. It uses the library from mdbook-mermaid, so that a site (and a ZIM archive) does not need a CDN.

Mermaid is off by default, so the output does not change for existing sites.

Alternatives

  • Render diagrams at build time (for example with mermaid-cli). This needs a headless browser at build time, so I did not do this.
  • Always use a CDN. This does not work offline or for ZIM archives, so script also takes a local file.

Tests

  • ndg-commonmark/tests/mermaid.rs: the block conversion, with highlighting on and off.
  • ndg-config: the [mermaid] overrides and the URL or file decision.
  • ndg-html/tests/custom_scripts.rs: the loader tag for a URL and for a local file.
  • ndg/tests/integration.rs: the asset copy, also with JS minification.
  • mermaid-init.test.js: the library loads only with diagrams, loads once, draws diagrams added later, and draws them again when the theme changes.
  • checks.mermaid: an ndg-builder site with a diagram.

Assisted-by: Claude:claude-opus-5-5

https://claude.ai/code/session_01RJvbghuj5GtCBiCjmBepNJ

With `mermaid` set, the processor renders each `mermaid` fenced code block as a `<pre class="mermaid">` element with the diagram source as text. It does not highlight these blocks. The page must load Mermaid to draw the diagrams.

Assisted-by: Claude:claude-opus-5-5
Claude-Session: https://claude.ai/code/session_01RJvbghuj5GtCBiCjmBepNJ
`mermaid.enable` enables Mermaid diagrams. `mermaid.script` sets the Mermaid library as a URL or as a local file. It defaults to Mermaid 11 from jsDelivr. Validation fails if a local file does not exist.

Assisted-by: Claude:claude-opus-5-5
Claude-Session: https://claude.ai/code/session_01RJvbghuj5GtCBiCjmBepNJ
When Mermaid is enabled, NDG renders `mermaid` code blocks for Mermaid and adds `assets/mermaid-init.js` to each page. The script loads the Mermaid library only on a page with a diagram, and draws the diagrams that the page adds later. NDG copies a local Mermaid library to `assets/` without postprocessing.

Assisted-by: Claude:claude-opus-5-5
Claude-Session: https://claude.ai/code/session_01RJvbghuj5GtCBiCjmBepNJ
The option enables Mermaid with the library from `mdbook-mermaid`, so that the site does not need a CDN.

Assisted-by: Claude:claude-opus-5-5
Claude-Session: https://claude.ai/code/session_01RJvbghuj5GtCBiCjmBepNJ
# Conflicts:
#	CHANGELOG.md
#	crates/ndg-config/src/templates.rs
The script selects the Mermaid theme from `data-theme` on `<html>`, or from the system theme. On `ndg:themechange`, it restores the source of each diagram and draws it again.

Assisted-by: Claude:claude-opus-5-5
Claude-Session: https://claude.ai/code/session_01RJvbghuj5GtCBiCjmBepNJ

@NotAShelf NotAShelf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the PR, and for your patience. I got caught up in an influx of PRs demanding immediate attention. I have a few concerns, mainly architectural than technical.

My main concern is that it solves a few layers of problems locally without making them agree. A bit hard to put it, but I sense it's a common problem with AI-assisted code. Simply put, I think there is plenty of configuration, copying, loading, observation, testing, more even, but the integration rules remain implicit and vague.

  1. The mermaid init script watches every child-list mutation anywhere in the body, scans the document, and queues another render pass. NDG already knows this. It does so before replacing page content and loading option chunks, for example. Having an implementation that does so separately ignores all boundaries between different components (which we have many of, unfortunately) and causes even more side effects. Though, a MutationObserver is not inehrently wrong.

  2. I am not sure how I copy about Mermaid getting its own separate asset copying policy. We're introducing a new route for copying JavaScript explicitly for script_paths and the semantics are different:

  • It bypasses configured JS postprocessing.
  • It flattens the source path to its basename.
  • It does not check destination collisions.

Your comment says it's "already minified" but in the configuration we accept any local file which might be, in fact, not minified. Another problem is in overrides. Set mermaid.script to a valid library file named vendor/main.js, and the copy order overwrites NDG's own assets/main.js. A library named mermaid-init.js (unlikely but not impossible) overwrites the loader itself.

  1. The floating mermaid URL is a little annoying. It's both a reproducibility concern and a security issue. I also should note here that "${mdbook-mermaid.src}/src/bin/assets/mermaid.min.js" is a hack.

Think I made my point.

While ndg's main inspiration (mdbook) doesn't support mermaid, I'm all in for it. However, I do think the implementation needs more work to be concrete.

Alternatively, I think this is a perfect candidate for an external plugin system I was planning to implement for the purposes of more mdbook parity. If we are doing this in the core, I would much rather we do it right so it doesn't come back to bite me in the back.

@KiaraGrouwstra

Copy link
Copy Markdown
Contributor Author

that sounds fair! if you'd prefer using plugins instead, i can maybe watch the repo releases to learn more about that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants