Visitar URL original
feat: replace Mermaid.js with Pele across editor and exports by erseco · Pull Request #2556 · exelearning/exelearning · GitHub
Skip to content

feat: replace Mermaid.js with Pele across editor and exports - #2556

Open
erseco wants to merge 12 commits into
mainfrom
feat/replace-mermaid-with-pele
Open

erseco wants to merge 12 commits into
mainfrom
feat/replace-mermaid-with-pele

Conversation

@erseco

@erseco erseco commented Oct 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

eXeLearning now renders Mermaid diagrams with Pele instead of Mermaid.js, everywhere: the editor (saved iDevice views), browser previews and print previews, browser exports (HTML5, single page, SCORM 1.2/2004, IMS, EPUB3, ELPX) and server-side exports (CLI, HTTP export routes, API v1, public view).

Existing content keeps working unchanged: diagrams are still authored as <pre class="mermaid">…</pre>, the TinyMCE exemermaid plugin is the same, and pre-rendered diagrams keep the exe-mermaid-rendered wrapper with the source in data-mermaid. Only the rendering library changes.

Why Pele?

Pele is an MIT-licensed open-source Mermaid-compatible renderer created by Steph Ango (@kepano), CEO of Obsidian.

Pele has its own Mermaid parsers and layout engine, and it generates SVG without browser DOM measurement APIs, so the same code runs in a browser and in Bun. It was built for Obsidian and supports the diagram types built into Mermaid 12.1.0 (compatibility). It has no runtime dependencies.

That removes the reason server exports were broken. Mermaid.js needs a real browser to measure text and SVG boxes, and our jsdom shim could not provide that (#2544). Making it work would mean maintaining the kind of measurement layer that mermaid-js/mermaid#8210 is still building upstream.

Upstream benchmark figures (published by Pele, not measured here)

Metric Pele Mermaid.js
Gzipped bundle ~230 KB ~1.6 MB
Render time ~0.05–0.3 ms ~6–82 ms
Direct runtime dependencies 0 23

Benefits measured in eXeLearning

See Performance and dependency comparison for methodology.

  • Server/CLI exports render diagrams again. On main, every server-side export leaves diagrams as source text and still reports success (CLI export cannot pre-render Mermaid diagrams (CSSStyleSheet is not defined) and leaves them as plain text #2544). Here they are SVG.
  • Browser renderer is 75% smaller: mermaid.min.js 2,748,992 B (787,899 B gzip) → pele.global.js 682,005 B (251,920 B gzip). It is still loaded only on demand, and never in exports.
  • 184 fewer production packages (315 → 131): mermaid (113-package tree) and jsdom (39-package tree) are gone, and pele adds one package with no dependencies. Production node_modules: 474,160 KB → 292,444 KB (−38%).
  • Smaller exported pages: the SVG for 29 representative diagrams is 465,991 B with Mermaid.js and 88,803 B with Pele (−81%; −87% after deflate, as in a ZIP).
  • Faster rendering: median 1.8 ms → 0.1 ms per diagram in Chromium (p95 8.2 → 0.3 ms).
  • Simpler code: jsdom setup, global DOM injection, getBBox/text-measurement polyfills, hidden-element retry loops, two separate renderers (browser MermaidPreRenderer.js and server ServerMermaidPreRenderer.ts) and the /libs/mermaid routes are gone. One DOM-free MermaidPreRenderer.ts serves the browser and the server. Net diff: +1,449 / −5,816 lines (−2.7 MB of vendored JavaScript).
  • One renderer version: the browser previously ran a hand-vendored Mermaid 11.12.0 while the server used npm Mermaid 11.17.2. Both now use the pinned pele@1.6.0.

Implementation details

Shared renderer: src/shared/export/prerender/MermaidPreRenderer.ts

  • String-based and DOM-free, so it works in the exporters bundle and in Bun. The caller passes Pele's render function: the server imports pele, and the browser hook uses window.pele. That keeps Pele (~660 KB) out of exporters.bundle.js, which loads at workarea start-up; it stays at ~314 KB.
  • Detects <pre> elements whose class list contains the exact mermaid token. The old regex also matched language-mermaid (code highlighted as Mermaid) and the exe-mermaid-* classes.
  • Decodes named and numeric entities and <br> line breaks (TinyMCE stores Enter inside a <pre> as <br>). It strips an SVG left inside the <pre> by the old Mermaid.js runtime and renders the remaining source.
  • Keeps the max-width/max-height style set by the TinyMCE dialog on the wrapper. Before, the style was dropped in exports.
  • Also processes diagrams inside JSON iDevice data (data-idevice-json-data), as before.
  • Accurate accounting: the result now includes failures: [{ code, message, source }], where code is Pele's syntax / unsupported-diagram / semantic / limit, or error. A failed diagram keeps its <pre class="mermaid"> source, so it can still be recovered and edited. mermaidRendered only means the HTML changed. It no longer stands for "everything rendered".

Server and CLI

  • elp:export uses the shared renderer. If any diagram fails, the command fails without writing the package and lists each diagram with Pele's message, e.g. "flowchart LR": Parse error on line 3: Expecting 'NODE_STRING', got 'EOF'. Before, it printed SUCCESS and shipped source text.
  • routes/export.ts, routes/api/v1/export.ts and services/public-view-content.ts pre-rendered LaTeX but never Mermaid, so those exports also showed diagrams as text. They now get the Mermaid hook too. They do not fail on an invalid diagram; it stays as source. That avoids changing the API v1 error contract for integrations.
  • jsdom is removed (it was only used for Mermaid SSR), and so is --external jsdom in the build. jsdom@26 remains in the lockfile only as an optional dependency of the dev-only fabric.

Browser

  • exporters.bundle.js → getMermaidPreRendererHooks() loads Pele on demand through the editor runtime, the same way the LaTeX path reuses $exe.math.loadMathJax. Failures are logged with console.warn. It is exposed on SharedExporters so that YjsProjectBridge.exportToElpx uses it instead of the removed window.MermaidPreRenderer.
  • common.js → $exe.mermaid now has enginePath() / load() (a promise, loaded once) / renderDiagrams() / init(). It renders every pre.mermaid:not([data-processed]) with pele.render(). Pele does not measure the DOM, so the visibility and width checks, the pending state and the retry timers are gone, and so is the matching re-render in menuStructureBehaviour.js. An invalid diagram keeps its source and gets a <p class="exe-mermaid-error" role="alert"> with Pele's message (set via textContent). In exported packages enginePath() is null, so nothing is ever requested (before, libs/mermaid/mermaid.min.js got a 404).
  • I used render() rather than mount(): the diagrams are static, and mount()'s resize/font observers would need teardown on every page change in the workarea for no visible benefit.
  • The TinyMCE plugin preloads with $exe.mermaid.load() and ignores a failed preload, since init() reports it.

Packaging

  • scripts/vendor-pele.ts copies pele.global.js and its LICENSE from the pinned package into the gitignored public/app/common/pele/, like vendor-edicuatex (whose drift/write helpers it reuses). It runs in build:all, the Dockerfile checks it with --check, and the e2e workflow uploads it with the other generated assets.
  • Removed: the mermaid and jsdom dependencies, public/app/common/mermaid/mermaid.min.js, public/app/common/MermaidPreRenderer.js (+ test), ServerMermaidPreRenderer.ts (+ spec), the MermaidPreRenderer.js script tags in workarea.njk / static-index.html, the /libs/mermaid routes, and getPreRenderedMermaidCss() with the mermaidWasRendered plumbing in seven exporters.
  • Docs, the third-party list in public/libs/README.md, the OSS-scanner threat model and the bun-runtime skill (both copies) are updated.

Visual compatibility

Pele does not implement Mermaid's named themes, and its layout engine places nodes and routes edges differently. To keep existing content familiar, public/style/workarea/base.css defines Pele's CSS variables once on .pele. Their values were read from what Mermaid 11.12 (the version the editor shipped) actually renders with theme: 'default': node fill #ececff, border #9370db, lines and text #333, subgraph fill #ffffde, square corners (--pele-radius: 0) and the "trebuchet ms", verdana, arial, sans-serif font stack. Pele also writes that stack into each SVG and measures labels with it. base.css is both the workarea stylesheet and every export's content/css/base.css, so the editor, previews and all formats share this single definition. eXe styles can override the variables. Explicit style, classDef and linkStyle colours from authors are kept.

Remaining differences:

  • Layout: node positions and edge routes differ. For example, Pele honours direction inside a subgraph that is linked to outside nodes, and Mermaid ignores it.
  • Chart palettes (pie, timeline, mindmap and gitGraph categories) use Pele's default series colours. Pele draws white labels on them, and Mermaid's near-white pastels left those labels unreadable.
  • Subgraph borders use the node border colour (purple instead of Mermaid's olive #aaaa33). Pele has a single border variable.
  • Mindmaps are drawn in Pele's lighter style instead of Mermaid's filled blocks.

Mermaid 11.12 (left) vs Pele 1.6 with this theme, server-rendered (right):

flowchart_long_2490

flowchart_subgraphs

flowchart_styles

sequence

gantt

pie

More diagram types (nested subgraphs, class, state, ER, mindmap, timeline, gitGraph, journey)

nested_complex

class

state

er

mindmap

timeline

gitgraph

journey

HTML5 export produced by the CLI, opened offline in WebKit:

HTML5 export in WebKit

Fixed issues

  • Fixes CLI export cannot pre-render Mermaid diagrams (CSSStyleSheet is not defined) and leaves them as plain text #2544. The issue's minimal project, exported with the built dist/cli.js, now contains <div class="exe-mermaid-rendered" data-mermaid="…"><svg class="pele pele-flowchart" viewBox="0 0 160 64">. There is no <pre class="mermaid"> and no reference to libs/mermaid/mermaid.min.js, and the page renders offline in Chromium, Firefox and WebKit. Gantt charts get a real width (800 px instead of 0), and flowcharts get a content-sized viewBox instead of -8 -8 50656 32. The regression tests in elp-export.spec.ts export the issue's project to html5, html5-sp, scorm12, scorm2004, ims and epub3 and check every SVG's viewBox. A broken diagram now makes the CLI fail instead of reporting success. The shared renderer's spec requires a real SVG for every supported type; nothing accepts the unrendered fallback any more.
  • Fixes Mermaid: long node labels are cut off in Chrome and Edge at some zoom levels, and the cut is saved in previews and exports #2490. Mermaid's clipping came from comparing a browser-measured width for exact equality. Pele wraps labels from its own text measurement (canvas measureText in browsers, a built-in width table on the server) and draws them as SVG text in a box sized from that measurement. Regression tests:
    • mermaid-pele.spec.ts (Playwright, Chromium and Firefox) renders the issue's diagram at device scale factors 0.9, 1 and 1.75 through the browser export hook and through the editor runtime. It asserts that the label wraps onto several lines with no text lost, and that its bounding box fits inside the node's box.
    • text.spec.ts checks the same label in the preview generated from a real iDevice.
    • MermaidPreRenderer.spec.ts checks the server output.
    • Outside the repo suite, all 73 labels of 29 diagrams fit their shapes in Chromium 156, Firefox 157 and WebKit 27.2 at all three scales, both server- and browser-rendered (minimum slack 16.7 px). They still fit when the font is forced to Verdana, the widest common fallback.

Related pull requests

Performance and dependency comparison

All local figures were measured on this branch versus main (0d5451a), on macOS (Apple silicon), Bun 1.4.3.

Metric Before (main) After Difference
Browser renderer, raw 2,748,992 B (mermaid.min.js 11.12.0) 682,005 B (pele.global.js 1.6.0) −2,066,987 B (−75.2%)
Browser renderer, gzip -9 787,899 B 251,920 B −535,979 B (−68.0%)
exporters.bundle.js ~314 KB ~314 KB unchanged (Pele is not inlined)
Production packages (bun install --production) 315 131 −184 (−58.4%)
Production node_modules 474,160 KB 292,444 KB −181,716 KB (−38.3%)
Packages pulled in by mermaid@11.17.2 / jsdom@30.0.1 alone 113 (159,316 KB) / 39 (27,808 KB) pele@1.6.0: 1 (3,560 KB) —
SVG for 29 diagrams, raw 465,991 B (Mermaid.js in Chromium) 88,803 B −81.0%
SVG for 29 diagrams, deflate -9 (ZIP-like) 93,120 B 12,204 B −86.9%
Browser render, Chromium 156, median / p95 1.8 / 8.2 ms 0.1 / 0.3 ms ~18× / ~27× faster
Server render (Bun), median / p95 renders nothing (CSSStyleSheet is not defined) 0.076 / 0.266 ms —
CLI html5 export, 2 pages / 10 diagrams, wall time not comparable (0 diagrams rendered) 0.36 s —
Representative HTML5 ZIP (#2544 project) 249,803 B (diagram left as text) 250,236 B (rendered SVG) not comparable: before ships no diagram
Static distribution / Docker image / desktop package not measured not measured the vendored tree in app/common/ shrinks by 2,066,987 B (−75%)

Methodology:

  • Packages: package.json + bun.lock from each side, copied to an empty directory; bun install --production --frozen-lockfile --ignore-scripts, then du -sk node_modules. The per-package figures come from installing each package alone into an empty project.
  • Render times: 29 representative diagrams (all built-in types; see Testing), one warm-up round and then 5 rounds. Mermaid.js 11.12.0 (from main) runs mermaid.render() with theme: 'default', and Pele runs render(), in the same Chromium page; the Bun figures use pele.render().
  • The CLI wall time is the median of 3 runs of bun dist/cli.js elp:export.

In practice: the editor downloads about 536 KB less (gzip) the first time a page with a diagram opens. Exported packages never contained a renderer and still don't, but their SVGs are about 5× smaller. Server, Docker and CLI exports render diagrams at all. Two fewer runtime dependencies (184 fewer installed packages) means less to install, audit and keep updated. And one renderer, one version and one theme replace two code paths that had drifted apart.

Testing

Run locally on this branch:

Command Result
make fix clean (exit 0; only pre-existing warnings)
bun run build:all ok
make test-unit 9093 pass, 0 fail; all 265 files meet the 90% coverage threshold
make test-frontend 308 files, 17564 tests pass
make test-integration 770 pass
bun x playwright test mermaid-pele.spec.ts idevices/text.spec.ts -g "Pele|mermaid|Mermaid" --project=chromium --project=firefox 24 passed

The full make test-e2e and make test-e2e-static runs are left to CI.

New and updated tests:

Also checked outside the repo suite, using Playwright 1.64.1 because the repo's pinned 1.58.2 has no WebKit build installed and the webkit project is commented out in playwright.config.ts:

  • A 2-page, 10-diagram project exported by the built CLI to html5, html5-sp, scorm12, scorm2004, ims, epub3 and elpx, then opened offline (file://) in Chromium, Firefox and WebKit. Every format and engine shows all 10 SVGs at a non-zero size with the theme applied. No pre.mermaid remains, the EPUB XHTML has no XML parse errors, and there are no requests for any renderer.
  • Label fit for 29 diagrams × 3 engines × 3 scale factors (see Mermaid: long node labels are cut off in Chrome and Edge at some zoom levels, and the cut is saved in previews and exports #2490 above).
  • Security behaviour: javascript:/data: links are dropped, HTML in labels is escaped or stripped (<img onerror> and <script> never reach the SVG), style values with url(javascript:)/expression() are rejected as syntax errors, sources over 50,000 characters are rejected with limit, and the SVGs contain no id attributes, so several diagrams on one page cannot collide.

The only line not covered is vendor-pele.ts's if (import.meta.main) entry guard, the same as in vendor-edicuatex.ts.

Compatibility and limitations

  • Existing Mermaid content stays editable and compatible: the source format, the TinyMCE dialog, data-mermaid and ELPX content.xml are unchanged. Nothing in existing projects is migrated or rewritten.
  • Pele differs from Mermaid.js (compatibility):
    • Arbitrary HTML labels are not supported. <b>/<strong>/<i>/<em> and <br> work; <u>, <sub> and <sup> keep their text but lose the formatting.
    • click JavaScript callbacks are ignored; links still work for http, https, mailto, tel and relative URLs.
    • fa: icons are dropped and their text is kept. The editor never loaded Font Awesome for Mermaid, so these did not render before either.
    • %%{init}%% themes and themeVariables no longer change colours.
    • ZenUML and other external plugins are not supported.
    • Diagrams in a <div class="mermaid"> (as opposed to <pre>) are not rendered in the editor any more. They never rendered in exports, and the TinyMCE plugin only creates <pre>.
  • Exported SVG colours use CSS custom properties. Every current browser and the WebKit/Blink/Gecko-based EPUB readers support them; readers without CSS custom-property support (such as the old Adobe RMSDK) were not tested.
  • Browser exports, previews and the HTTP/API routes leave an invalid diagram as source (logged with console.warn in the browser) rather than failing the whole export. The author already sees Pele's error in the editor. Only the CLI fails the export.

Render Mermaid diagrams with Pele (https://pele.run), a dependency-free
Mermaid renderer that computes its own layout without a DOM, in the editor,
browser previews/exports and server/CLI exports.

- Add a shared, DOM-free MermaidPreRenderer used by the exporters bundle and
  the server. It reports every diagram it could not render instead of
  claiming success, keeps the source in data-mermaid, preserves the
  max-width/max-height style and matches the exact `mermaid` class token.
- The CLI fails, without writing the package, when a diagram cannot be
  rendered, instead of shipping it as source code (#2544).
- Pre-render Mermaid in the HTTP export routes, API v1 and the public view,
  which only pre-rendered LaTeX.
- The editor runtime ($exe.mermaid) loads Pele's browser build, vendored from
  the pinned package by scripts/vendor-pele.ts, and shows parse errors.
- Approximate Mermaid's default theme with Pele CSS variables in base.css,
  shared by the workarea and every export format.
- Remove the mermaid and jsdom dependencies, the vendored mermaid.min.js,
  the jsdom-based ServerMermaidPreRenderer, the /libs/mermaid routes and the
  per-exporter Mermaid CSS append.

Fixes #2544
Fixes #2490
@erseco erseco self-assigned this Oct 18, 2026
@codecov

codecov Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.36%. Comparing base (bce8f99) to head (c7fb139).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2556      +/-   ##
==========================================
+ Coverage   92.25%   92.36%   +0.11%     
==========================================
  Files         479      478       -1     
  Lines       89517    89024     -493     
==========================================
- Hits        82584    82228     -356     
+ Misses       6933     6796     -137     
Flag Coverage Δ
frontend 92.36% <100.00%> (+0.11%) ⬆️
unittests 92.36% <100.00%> (+0.11%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Deploying exelearning with  Cloudflare Pages  Cloudflare Pages

Latest commit: c7fb139
Status: ✅  Deploy successful!
Preview URL: https://1d1bf659.exelearning.pages.dev
Branch Preview URL: https://feat-replace-mermaid-with-pe.exelearning.pages.dev

View logs

@codecov

codecov Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 1.12kB (0.07%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
app-bundle 991.31kB -89 bytes (-0.01%) ⬇️
exporters-bundle 693.02kB 1.21kB (0.18%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: app-bundle

Assets Changed:

Asset Name Size Change Total Size Change (%)
app.bundle.js -89 bytes 991.31kB -0.01%
view changes for bundle: exporters-bundle

Assets Changed:

Asset Name Size Change Total Size Change (%)
exporters.bundle.js 1.21kB 693.02kB 0.18%

Keep main's vendor pipeline and jsdom (sanitizeHtml.test.js and
--external jsdom) and replace mermaid with pele 1.6.0 in the live copy
step, gitignore, e2e artifact, and Dependabot group. bun.lock was
regenerated with bun install.
@erseco

erseco commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator Author

#2571 holds Dependabot on mermaid 11.x (Mermaid 12 breaks the server pre-render, see #2528 / #2544). Its ignore entry should be removed once this Pele replacement lands and drops the mermaid dependency.

…with-pele

# Conflicts:
#	bun.lock
#	package.json
Drop public/app/common/MermaidPreRenderer.test.js: #2538 trimmed its
timeouts on main, but this branch removes the module it tested. The
replacement pre-renderer has its own spec under src/shared/export.
# Conflicts:
#	bun.lock
#	scripts/static-bundle/minify-dist.ts
#	scripts/static-bundle/static-index.html
#	src/shared/export/prerender/ServerMermaidPreRenderer.spec.ts
#	src/shared/export/prerender/ServerMermaidPreRenderer.ts
#	views/workarea/workarea.njk
Main added server-mermaid-rendering.spec.ts against ServerMermaidPreRenderer,
which this branch removes. Render through MermaidPreRenderer with Pele's
render, as CLI and server exports do, and match Pele's node markup
(g.pele-node, its rect). Every case and geometry assertion is kept.
# Conflicts:
#	bun.lock
#	package.json

This branch has not been deployed

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

Labels

bug Something isn't working cleanup dependencies Pull requests that update a dependency file enhancement New feature or request optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI export cannot pre-render Mermaid diagrams (CSSStyleSheet is not defined) and leaves them as plain text Mermaid: long node labels are cut off in Chrome and Edge at some zoom levels, and the cut is saved in previews and exports

1 participant