Skip to content

Bundle Mermaid so diagrams need no extra install - #11

Merged
chris-colinsky merged 5 commits into
mainfrom
feature/10-bundle-mermaid
Sep 24, 2026
Merged

chris-colinsky merged 5 commits into
mainfrom
feature/10-bundle-mermaid

Conversation

@chris-colinsky

@chris-colinsky chris-colinsky commented Sep 24, 2026 •

Copy link
Copy Markdown
Member

Closes #10.

What changes

A .mmd image no longer needs the Mermaid CLI, Node or Chromium. The repo now carries everything needed to build a deck.

  • Mermaid is vendored in src/akceo/assets/vendor/mermaid/ (12.0.0). It comes with its license, notices for every package in the bundle, and a manifest with the version, npm integrity hash and SHA-256. A test checks the file against that SHA-256.
  • Diagrams are checked at build time. mermaid.py runs Mermaid's own parse() in V8 through mini-racer, a new runtime dependency. V8 runs in a child process (python -m akceo.mermaid), because mini-racer can't interrupt async JavaScript: the child is killed if a diagram takes over 10 s, on Ctrl-C, and when the build ends, so a runaway parse can't hang the build. A syntax error stops the build and names the line in the .mmd file. That line is right even after %% comments, %%{init}%% lines and front matter, which Mermaid strips before parsing. YAML errors in front matter or @{…} shape data get their file line too, and a diagram over Mermaid's 50,000-character limit stops the build rather than drawing Mermaid's "too large" notice.
  • The page can't reach outside itself. Every built page now carries a Content-Security-Policy (default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:), so the browser refuses any outside load, whatever the syntax. Mermaid fetches label images while it draws, so nothing done to the SVG afterwards could stop that load.
  • Diagrams that reach outside stop the build with a clear message. The build scans the whole source for links (click, link, links, $link, <a href>), images (img:, src, every srcset candidate, and similar) and CSS (url(), @import, including themeCSS in front matter or %%{init}%%) and names the line. Only # anchors and data: URIs pass. The page also drops any outside link from the drawn SVG.
  • Diagrams are drawn in the page as inline SVG, sized from their viewBox so they fit the frame the same way an <img> does. Unframed diagrams use the theme's colors; framed ones keep Mermaid's defaults. Inline SVG leaves room for interactive diagrams later.
  • akceo check deck.md runs every check a build runs and writes nothing.

Costs

  • A deck with at least one diagram grows by about 5.6 MB. Decks without diagrams are unchanged.
  • mini-racer is about 61 MB installed (a 15–22 MB download).
  • A few errors only show when drawing, such as an invalid gantt date. The slide shows Mermaid's message instead of the diagram, and the other diagrams still draw.
  • Because of the CSP, a web font or @import in a custom theme no longer loads, even online. None of the built-in themes use one. The docs now suggest installed fonts or a data: @font-face.

Licensing

The bundle includes code under MIT, ISC, BSD-3-Clause, Apache-2.0, MPL-2.0/Apache-2.0 (DOMPurify), Unlicense and EPL-2.0 (elkjs).

  • pyproject.toml now has a combined license expression.
  • The wheel includes the vendored LICENSE and THIRD_PARTY_NOTICES.
  • The notices come from the pnpm-lock.yaml at Mermaid's release tag, so every version matches the bundle. They include langium, which @mermaid-js/parser compiles into its output.
  • Every built deck with a diagram carries the full notices in a comment, including where to get the elkjs source.

Updating Mermaid

scripts/update_mermaid.py VERSION rewrites the vendor folder. It checks every download against npm's or the lockfile's integrity hash and needs nothing but Python. It follows optional dependencies too, and writes nothing until every step succeeds. It stops if Mermaid's parser package gains a devDependency it hasn't seen, if the walk misses a package the bundle certainly holds, or if a package has no license file. See the README in the vendor folder.

Testing

  • uv run pytest: 168 passed. The Mermaid tests run the real parser, not a mock.
  • ruff, ruff format --check, pyright and every pre-commit hook pass.
  • Built wheel installed in a clean environment: akceo build and akceo check work on the demo.
  • Headless Chrome, all as expected:
    • framed and unframed colors;
    • a tall diagram scaled to fit;
    • the error box for a bad gantt date;
    • diagrams on hidden slides draw at the same size as visible ones;
    • with the build check skipped, outside links and image sources are stripped and # links kept;
    • a failed diagram reads as plain text, without the image role;
    • under the CSP, the demo draws with no violations, and outside images from themeCSS, style= and <img> are refused while the diagram still draws;
    • a failing diagram before two good ones: both good ones draw, and no Mermaid error graphic is left in the page.
  • Ctrl-C during a build exits in about 0.2 s with no child process left.
  • Breaking each new guard in turn (the CSP, the url() scan, the URL stand-in, the outside-reference scan) turns the tests red.

Bring mermaid.min.js into the package so decks with diagrams no longer
need the Mermaid CLI, Node or Chromium. The folder holds Mermaid's
license, notices for every package the bundle includes (among them
elkjs under EPL-2.0), and a manifest with the version, npm integrity
hash and the file's SHA-256.

scripts/update_mermaid.py rewrites the folder from an npm release. It
checks the tarball against npm's integrity hash, escapes the one raw
control character that sits in a string literal, and regenerates the
notices, which needs npm. The pre-commit file hooks skip the folder so
the file stays byte for byte, and git marks it as vendored.
A .mmd image used to need the Mermaid CLI at build time. Now the build
checks each diagram with Mermaid's own parse(), run in V8 through
mini-racer, so a syntax error still stops the build and names the line
in the .mmd file. The page then draws the diagrams as inline SVG with
the vendored Mermaid, which is embedded only when a slide has a
diagram, under a comment carrying its license notices. Inline SVG
leaves room for interactive diagrams later.

Add akceo check, which runs every check a build runs and writes
nothing, so authors and agents get errors on the command line fast.

The demo's image slide now uses a Mermaid diagram, and the docs,
changelog and license metadata cover the new dependency and the
bundled code.

Closes #10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Runtime SVG URLs can violate self-contained output, and additional diagnostics, accessibility, and license-inventory issues remain.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 4 Medium severity

Open (4)
What changed in this PR

Bundles Mermaid for self-contained diagram validation and browser rendering without Node or Chromium.

Changes:

  • Adds V8-based Mermaid syntax checking and browser SVG rendering.
  • Adds akceo check, dependency/package updates, and vendoring tooling.
  • Updates tests, documentation, examples, and licensing metadata.
File Description
.gitattributes Marks Mermaid assets as vendored.
.pre-commit-config.yaml Excludes vendored assets from hooks.
CHANGELOG.md Records Mermaid and check-command changes.
CLAUDE.md Updates repository guidance.
README.md Documents bundled diagrams and checking.
docs/​how-it-works.md Describes the revised pipeline.
docs/​syntax.md Documents Mermaid behavior.
examples/​demo/​deck.md Uses the Mermaid example.
examples/​demo/​flow.mmd Adds the example diagram source.
examples/​demo/​speaker-notes.md Updates demo narration.
pyproject.toml Adds dependency and license metadata.
scripts/​update_mermaid.py Adds Mermaid vendoring automation.
src/​akceo/​assets/​base.css Styles rendered diagrams and errors.
src/​akceo/​assets/​diagrams.js Renders Mermaid SVGs in-browser.
src/​akceo/​assets/​mermaid-shims.js Provides build-time V8 shims.
src/​akceo/​assets/​page.html Adds diagram-script placeholder.
src/​akceo/​assets/​vendor/​mermaid/​LICENSE Includes Mermaid’s license.
src/​akceo/​assets/​vendor/​mermaid/​README.md Documents vendored assets.
src/​akceo/​assets/​vendor/​mermaid/​THIRD_PARTY_NOTICES Includes dependency notices.
src/​akceo/​assets/​vendor/​mermaid/​manifest.json Records source and checksum metadata.
src/​akceo/​assets/​vendor/​mermaid/​mermaid.min.js Vendors the Mermaid browser bundle.
src/​akceo/​cli.py Adds the check command.
src/​akceo/​images.py Removes external Mermaid CLI rendering.
src/​akceo/​mermaid.py Adds parsing, diagnostics, and embedding.
src/​akceo/​render.py Integrates diagram checking and output.
tests/​test_cli.py Tests CLI and self-contained output.
tests/​test_images.py Removes obsolete Mermaid CLI tests.
tests/​test_mermaid.py Tests the new Mermaid integration.
tests/​test_render.py Tests diagram markup rendering.
uv.lock Locks the mini-racer dependency.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread scripts/update_mermaid.py Outdated
Comment thread src/akceo/assets/diagrams.js
Comment thread src/akceo/assets/diagrams.js Outdated
Comment thread src/akceo/mermaid.py Outdated
Build the license notices from the pnpm-lock.yaml at Mermaid's release
tag, so each version matches the bundle; resolving ranges picked newer
ones. The walk also follows the devDependencies that the parser
workspace compiles in, which brings in langium. A new, unsorted one
stops the update. Tarballs come straight from the registry and are
checked against the lockfile, so npm is no longer needed.

Stop the build on a diagram that would load or link to anything
outside the deck: Mermaid fetches images while it draws, so this can't
wait for the page. The page also strips any such reference as a
backstop. Add the URL stand-in the check needed for click links.

Point unknown diagram type errors at the right line, and let screen
readers read the message when a diagram fails to draw.
Add a Content-Security-Policy to the page so the browser refuses any
outside load, however a diagram or theme spells it. Mermaid fetches
label images while it draws, so nothing done to the SVG afterwards
could stop them. A web font or @import in a custom theme no longer
loads as a result.

Widen the build-time scan to every link, image and CSS form the
review found, over the whole source, so it still stops the build with
a clear message and line. Stop on diagrams over Mermaid's size limit,
and give YAML and unknown-shape errors their line in the file.

Run the V8 check in a child process. mini-racer can't interrupt async
JavaScript, so a runaway parse or Ctrl-C used to hang the build; the
child is now killed on a timeout, on Ctrl-C and when the build ends.

In the page, one diagram's failure no longer stops the rest drawing,
and Mermaid no longer leaves its error graphic behind. The update
script walks optional dependencies, stops on a missing anchor package
or license file, and writes nothing until every step succeeds.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The external-reference scanner rejects valid label text and misses later URLs in multi-candidate srcset attributes.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (4)

Comment thread src/akceo/mermaid.py Outdated
A srcset holds a list of candidates, and the scan stopped at the first
comma, so a data: URI candidate hid an outside one after it. Capture
the whole value and split it the way the HTML spec does, where a URL
runs to the next whitespace, so a data: URI's own commas don't split
it.

Also let the patterns skip a backslash before a quote, so a URL in a
JSON string in an %%{init}%% line is reported as the URL rather than
as a backslash.
@chris-colinsky
chris-colinsky merged commit 8ad29c3 into main Sep 24, 2026
1 check passed
@chris-colinsky
chris-colinsky deleted the feature/10-bundle-mermaid branch September 24, 2026 15:14
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.

Bundle Mermaid so decks with diagrams need no extra install

2 participants