Skip to content

build: upgrade Mermaid to 11.17.2 in the notebook renderer and the diagram preview, unpin cytoscape - #1189

Merged
juliasilge merged 9 commits into
quarto-dev:mainfrom
nealrichardson:build/mermaid-11
Sep 30, 2026
Merged

juliasilge merged 9 commits into
quarto-dev:mainfrom
nealrichardson:build/mermaid-11

Conversation

@nealrichardson

@nealrichardson nealrichardson commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Upgrades the Mermaid in both of the extension's Mermaid previews to 11.17.2:

  • the notebook Markdown cell renderer (apps/vscode-markdownit, npm mermaid), from 9.4.3;
  • the Diagram preview (apps/vscode/assets/www/diagram/mermaid.min.js, vendored), from 11.12.0.

It also releases the cytoscape 3.23.0 hold from #1176.

Why 11.17.2 and not Quarto CLI's 11.12:

  • Snyk. Six Mermaid advisories affect every 11.12.x release and are fixed in 11.14.1 or 11.16.1: GHSA-c4c3-pg64-4m4v, GHSA-6x64-9x62-f2gx, GHSA-87f9-hvmw-gh4p, GHSA-xcj9-5m2h-648r, GHSA-ghcm-xqfw-q4vr and GHSA-6m6c-36f7-fhxh. They cover config prototype pollution, CSS and HTML injection, and a Gantt infinite loop. 11.17.2 has none.
  • The syntax changes are additive. 11.13–11.17 add new diagram types and node shapes (e.g. venn-beta, and person used below). They deprecate flowchart.htmlLabels and mermaidAPI.setConfig(), and nothing here uses either.
  • We drift only by being ahead. A diagram that quarto render (11.12) accepts also renders here. The reverse doesn't always hold, since newer syntax can preview here and then fail in the CLI. mermaidVersion.test.ts already allows us to be ahead of the CLI; it fails only if we fall behind.
  • One visible default change: in 11.17, classDiagram and C4 moved to Mermaid's unified (v2) renderer. Class and C4 diagrams will look somewhat different from the CLI's 11.12 output until the CLI catches up.

I'll open a quarto-cli issue to bump its Mermaid once this merges.

Out of scope, and still deferred to the UX review: whether to keep our notebook Mermaid rendering next to VS Code 1.121's built-in notebook Mermaid, and securityLevel: "loose" vs. inline SVG. The notebook output model is unchanged: an inert <img> with the SVG as a data URL.

Changes

Packages. mermaid ^9.1.7 → ~11.17.2 in apps/vscode-markdownit, the only workspace that imports it.

  • I script-deleted the lock entries that only Mermaid reached and re-ran yarn install. Entries shared with other workspaces kept their locks (d3-array/d3-dsv for OJS, lodash-es, lodash, …). No resolutions.
Package Before After
mermaid 9.4.3 11.17.2
cytoscape 3.23.0 (held) 3.34.3
cytoscape-fcose / -cose-bilkent 2.2.0 / 4.1.0 same
d3 7.9.0 same
dompurify 2.4.3 3.4.16
dagre-d3-es 7.0.9 7.0.14
@mermaid-js/parser — 1.2.1 (Langium and Chevrotain are bundled in it)
katex — 0.16.47 (lazy chunk)
uuid 9.0.x 14.0.2
new — marked 16.4.2, roughjs 4.6.6, @iconify/utils 3.1.7, es-toolkit 1.52.0, fastdom 1.0.12 (+ strictdom), @upsetjs/venn.js 2.0.0, dayjs 1.11.23
removed present elkjs, web-worker, non-layered-tidy-tree-layout, heap
  • Duplicates: there's one copy each of cytoscape, dompurify, katex, d3 and @mermaid-js/parser. The rest:
    • d3-sankey 0.12.3 (a Mermaid dependency) still requires the d3 v1/v2 line, so d3-array 2.12.1, d3-path 1, d3-shape 1 and internmap 1 sit next to the v3 copies, as they do upstream.
    • marked 16 (Mermaid) is separate from marked 18 (vsce, dev-only).
    • uuid 14 (Mermaid) is separate from uuid 8 (exceljs, OJS, dev-only).
    • dayjs 1.11.23 (Mermaid) is separate from 1.11.13 (exceljs, OJS, dev-only).

Diagram preview. assets/www/diagram/mermaid.min.js is now dist/mermaid.min.js from the mermaid@11.17.2 npm tarball.

  • That's the same provenance as before: the 11.12.0 file it replaces was byte-identical to mermaid@11.12.0's dist/mermaid.min.js, which is what quarto-cli vendors.
  • It still sets globalThis.mermaid, and diagram.js works unchanged with its async parse/render and initialize({ startOnLoad: false }).
  • Size: 2,748,992 → 3,572,661 B.

Async rendering in the notebook renderer (apps/vscode-markdownit/src/mermaid.ts). Mermaid 10 removed mermaidAPI.render's synchronous callback, but markdown-it renderer rules have to return a string synchronously. So now:

  • The fence rule emits <div class="quarto-mermaid" id="quarto-mermaid-N"></div>. N comes from a module-wide counter, so ids stay unique across renders. It wraps md.renderer.render to collect the diagrams from that call.
  • VS Code's notebook Markdown renderer sets previewNode.innerHTML synchronously right after md.render returns. So a microtask then loads Mermaid (on the first diagram only, with import("mermaid")), calls mermaid.initialize, and awaits mermaid.render for each diagram. Afterwards it looks the placeholder up in the cell's shadow root, via env.outputItem.id, the same hook VS Code's built-in Mermaid renderer uses. If that fails, it searches the open shadow roots.
  • The placeholder gets an <img> built with DOM APIs. Its max-width is taken from the SVG's root style, since the old code read it from Mermaid 9's temporary element. On an error, the placeholder instead gets a <pre>Failed to render mermaid diagram. …</pre>, set through textContent. The error only replaces that one diagram, so other diagrams and cells still render. suppressErrorRendering: true stops Mermaid from drawing its own error diagram.
  • If a cell re-renders while an earlier render is still running, the earlier render can't find its (now removed) placeholder, so it drops its result. No stale SVGs.
  • Nothing is written into the page as markup, so the Restricted Mode sanitizer isn't bypassed, and securityLevel: "loose" stays as inert as before inside the <img>. The placeholder class is quarto-mermaid, not mermaid, so VS Code's built-in renderer (it selects .mermaid) ignores it.
  • Theme: dark or default, from the same vscode-dark/vscode-high-contrast body classes as before. It's now read on every render instead of once at activation, so a re-rendered cell picks up a theme switch. The callout colors in index.ts are still read once.
  • AMD define: another renderer in the notebook webview can load an AMD loader (RequireJS). fastdom's UMD wrapper (new in Mermaid 11.17) then calls define() instead of setting module.exports, and Mermaid fails to load with "h.default.extend is not a function". vite.config.ts now defines define as undefined, so no bundled UMD wrapper checks for it.
  • I dropped the ...options spread into mermaid.initialize, which passed our { dark } through as a Mermaid config key. I also removed the stale // TODO: mermaid breaks other plugins. It dates from April 2023, when the plugin was briefly commented out, and was left in when it was re-enabled the next day. The rendering check below renders Mermaid next to callouts, divs and footnotes.

Tests:

  • Node doesn't load Mermaid anymore (the fence rule doesn't touch it), so the test-only Mermaid/DOM stub is gone.
  • The quarto-syntax snapshot changes only where the two Mermaid fences were. They used to be <img>s of the stub's fake SVG, and now they're the two placeholders.
  • A new test checks that re-rendering the same source produces fresh ids.
  • A new Notebook renderer build test fails if any built chunk in out/markdownit still has a typeof define check.
  • mermaidVersion.test.ts:
    • The notebook renderer and the Diagram preview must be on the exact same Mermaid version. Both come from the same npm release, and re-vendoring is a file copy from node_modules/mermaid/dist/. So an exact match is cheap to keep, and it makes a diagram render identically in both. The version is read from the renderer's hashed chunks, and the test fails if stale chunks contain more than one version.
    • Neither may be behind the installed Quarto CLI. Being ahead is fine. On the pre-release channel this is still only a warning.
    • Version reading: Mermaid 11.17 builds no longer embed name:"mermaid",version:"…". The reader now also recognizes the version literal that render() passes to each diagram's renderer (renderer.draw(text, id, "11.17.2", diagram)), in both minified and unminified builds. It still reads 11.12-style builds, such as the CLI's current one.

Verification

On Node 24.21:

  • yarn install --frozen-lockfile leaves the lockfile unchanged.

  • yarn build --force: 14/14. yarn test-packages: 13/13, including the renderer snapshot tests. yarn lint --continue: 17/17, 0 errors, the same 17 warnings.

  • yarn test-vscode, headless (Xvfb in a linux-arm64 container, VS Code 1.139.1, Quarto 1.10.18 with Mermaid 11.12.0, a clean extract of this branch): main 169 passing and 3 pending (main is at 168; the extra one is the new Mermaid version test), r-project 3 passing. All three Mermaid version tests pass, including "not behind the installed Quarto CLI" against the CLI's 11.12.0.

  • The other bundles are unaffected. grep -c mermaidAPI gives 0 for out/main.js, out/lsp/lsp.js and assets/www/editor/index.js, and their sizes are byte-identical to main (4,345,005 / 2,869,803 / 10,350,698 B).

  • out/markdownit (measured after a clean build):

    main (Mermaid 9.4.3) this branch (11.17.2)
    files 7 (index.js, 5 chunks, styles.css) 97 (index.js, 95 chunks, styles.css)
    total 3,927,967 B 4,535,087 B
    index.js 117,260 B, statically importing the 1,141,217 B Mermaid chunk at activation 118,553 B; Mermaid (113 KB core plus the diagram's chunks) loads with the first diagram
    largest chunk 1,993,034 B (flowchart-elk) 820,917 B

    Mermaid 11 doesn't bundle ELK (it's the separate @mermaid-js/layout-elk package, which the IIFE build doesn't include either). The build is still an ES library with lazy chunks, which rolldown#11001 doesn't affect (that's a UMD issue), and the chunks load (below).

  • Notebook renderer check in headless Chromium. I served the built out/markdownit and mimicked vscode.markdown-it-renderer: a markdown-it instance with VS Code's options and highlight, plus a renderOutputItem that renders into a shadow root and passes { outputItem: { id } }.

    • I rendered, in light, dark, and dark with DOMPurify using VS Code's untrusted-workspace ALLOWED_TAGS (simulating Restricted Mode):
      • flowchart, sequenceDiagram, mindmap, timeline, gantt, classDiagram, stateDiagram-v2 and a C4Context;
      • an A@{ shape: text } / cyl node, an A@{ shape: person } node, and a venn-beta diagram;
      • an invalid flowchart, and two diagrams in one cell (flowchart + pie);
      • a diagram between a callout, a .column-margin div and a footnote;
      • a {mermaid} attribute fence, and a cell re-rendered twice synchronously.
    • Every diagram became an <img> with a non-zero size, with the dark theme's colors in dark. The class and C4 diagrams render with the new renderer.
    • The invalid one showed "Failed to render mermaid diagram. Parse error on line 2: …", and the rest of the page still rendered.
    • The shape syntax rendered its labels (not "undefined").
    • The re-rendered cell showed only the second version.
    • 52 chunks loaded. There were no console errors and no failed requests.
    • That page had no global define, so it missed the AMD failure above. After the fix, I loaded each of the 95 built chunks in Node with a stub define (and define.amd) global, and they all load. Before the fix, the core chunk and architectureDiagram threw "h.default.extend is not a function".
  • Diagram preview check in headless Chromium. I loaded the preview page's scripts (lodash, the new mermaid.min.js, d3, graphviz, diagram.js) with a stubbed acquireVsCodeApi, and posted render messages.

    • window.mermaid.parse/render/initialize are functions.
    • A flowchart with a person node and a classDiagram rendered as SVG.
    • An invalid diagram showed the parse error banner.
    • There were no console errors.

Manual checks, please (Extension Development Host, and Positron's legacy Jupyter notebook editor; the default Positron Notebook editor does its own Markdown and Mermaid rendering and doesn't use this renderer). Open an .ipynb whose Markdown cells contain:

  1. The diagram types above (flowchart, sequence, mindmap, timeline, gantt, class, state-v2, C4, A@{ shape: text }, venn-beta), and check that they render and look reasonable next to quarto render's output.
  2. Class diagram look: compare a classDiagram (with members, methods and a few relationships) against quarto render on the CLI's 11.12. Differences are expected from the unified renderer; check that nothing is broken or unreadable, in both light and dark.
  3. An invalid diagram: the error shows up in place of the diagram, and the other diagrams and cells still render.
  4. Two diagrams in one cell. Then edit a cell several times: no stale or duplicated diagrams.
  5. Mermaid next to callouts, divs and footnotes.
  6. Theme switching: switch between light, dark and high contrast, then edit a cell. Its diagram should follow the new theme (the callout colors still need a reload).
  7. Restricted Mode (untrusted workspace): diagrams still render.
  8. Developer: Open Webview Developer Tools on the notebook: no CSP violations or failed chunk loads.
  9. Diagram preview: in a .qmd, "Preview Diagram" on a {mermaid} cell (including a classDiagram) renders, and an invalid one shows the error banner.
  10. VS Code ≥ 1.121 (built-in Mermaid Markdown Features enabled): ```mermaid fences should render once, with our renderer (the <img>), and not twice. Note any interaction with the built-in renderer (its ::: mermaid container syntax is separate). This is input for the deferred UX review.

🤖 Generated with Claude Code

nealrichardson and others added 3 commits September 30, 2026 08:33
…ew's

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cape

Mermaid ~11.12.0 (resolves 11.12.3) matches Quarto CLI's 11.12. Its
render API is async, so the fence rule now emits an empty placeholder
with a unique id, and once the host has written the cell's HTML, each
diagram is rendered and the placeholder filled with the same inert
<img> data URL as before (or an error <pre>). Mermaid is loaded on the
first diagram, and the theme is read at render time. The snapshot tests
no longer need a Mermaid stub (removed in the previous commit).

The lockfile drops mermaid 9's closure (script-deleted, then re-resolved
by yarn install), which also releases the cytoscape 3.23.0 hold.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@posit-snyk-bot

posit-snyk-bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

nealrichardson and others added 4 commits September 30, 2026 09:04
11.12.x is affected by six Mermaid advisories that are fixed in
11.14.1/11.16.1. Lockfile re-resolved by script-deleting mermaid 11.12's
closure and re-running yarn install.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A copy of dist/mermaid.min.js from the mermaid 11.17.2 npm package. The
11.12.0 file it replaces was byte-identical to that release's
dist/mermaid.min.js, which is also what Quarto CLI vendors.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mermaid 11.17 builds no longer embed package.json, so the version is
also read from the render() call that passes it to diagram renderers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@nealrichardson nealrichardson changed the title build: upgrade Mermaid to 11.12 in the notebook renderer (matching Quarto CLI), unpin cytoscape build: upgrade Mermaid to 11.17.2 in the notebook renderer and the diagram preview, unpin cytoscape Sep 30, 2026
# Conflicts:
#	apps/vscode/CHANGELOG.md
#	yarn.lock

@juliasilge juliasilge left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Unfortunately, I do think something's not quite right with this one currently.

For an .ipynb in the legacy Jupyter notebook editor, every Mermaid diagram fails with the same error, for all diagram types:

Failed to render mermaid diagram. h.default.extend is not a function
Image

I think that the cause is fastdom, which Mermaid 11.17 uses. fastdom has a UMD wrapper. If a global AMD define exists, the wrapper calls define() and does not set module.exports. Rolldown wraps fastdom as a CommonJS module, so h.default is {}. Then Mermaid core fails at load time on this line, before it parses any diagram:

var rt = h.default.extend({ raf(e) { ... } }).extend(ee.default);

I reproduced this in Node with the built chunk (out/markdownit/chunk-GMAD6QVW-*.js):

  • With no global define, the chunk loads.
  • With globalThis.define = function () {}, the chunk throws h.default.extend is not a function.

I did not yet find what sets define in the notebook webview. I think that another notebook renderer in the same webview loads RequireJS, for example the Jupyter widget renderer maybe? The headless Chromium check in this PR did not catch this problem because that page had no define global.

One possible fix is to add define: 'undefined' to the define block in apps/vscode-markdownit/vite.config.ts, because then typeof define in bundled UMD code is always "undefined". What do you think about that? A regression test could load the chunks with a stub define global.

Also FWIW the notebook checks in this PR apply to Positron only in the legacy Jupyter notebook editor. The Positron Notebook editor is the default, and it uses its own Markdown and Mermaid rendering, not the Quarto renderer.

… define

Another renderer in the notebook webview can load an AMD loader. fastdom's
UMD wrapper then calls define() instead of setting module.exports, so
Mermaid 11.17 fails to load with "h.default.extend is not a function".
Define `define` as undefined in the renderer build, and test that no
built chunk still checks for it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@nealrichardson nealrichardson left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Thanks, you're right, and I reproduced it: with a stub globalThis.define (plus define.amd), the core chunk and architectureDiagram both throw h.default.extend is not a function. Fixed in 4488ce2, as you suggested:

  • apps/vscode-markdownit/vite.config.ts now has define: 'undefined', so no bundled UMD wrapper finds a define to call. After a clean rebuild, none of the 95 chunks contains typeof define, and all of them load in Node with the stub define global.
  • Regression test: a new Notebook renderer build suite in mermaidVersion.test.ts fails if any chunk in out/markdownit still checks typeof define. It checks the build output as text instead of importing the chunks with a stub define, because the extension tests are CommonJS and run in the extension host. It would have flagged 5 chunks before the fix.

I haven't tracked down what sets define in the webview either. The fix holds whichever renderer it is.

I also updated the description: in Positron, the manual checks apply only to the legacy Jupyter notebook editor.

@nealrichardson

Copy link
Copy Markdown
Contributor Author

🤖 Thanks, you're right, and I reproduced it: with a stub globalThis.define (plus define.amd), the core chunk and architectureDiagram both throw h.default.extend is not a function. Fixed in 4488ce2, as you suggested:

  • apps/vscode-markdownit/vite.config.ts now has define: 'undefined', so no bundled UMD wrapper finds a define to call. After a clean rebuild, none of the 95 chunks contains typeof define, and all of them load in Node with the stub define global.
  • Regression test: a new Notebook renderer build suite in mermaidVersion.test.ts fails if any chunk in out/markdownit still checks typeof define. It checks the build output as text instead of importing the chunks with a stub define, because the extension tests are CommonJS and run in the extension host. It would have flagged 5 chunks before the fix.

I haven't tracked down what sets define in the webview either. The fix holds whichever renderer it is.

I also updated the description: in Positron, the manual checks apply only to the legacy Jupyter notebook editor.

@juliasilge juliasilge left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thank you so much!

Image

@juliasilge
juliasilge merged commit a83c5cc into quarto-dev:main Sep 30, 2026
8 checks passed
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.

3 participants