diff --git a/packages/core/src/extensions/mermaid.test.ts b/packages/core/src/extensions/mermaid.test.ts new file mode 100644 index 0000000000..a950e1e77e --- /dev/null +++ b/packages/core/src/extensions/mermaid.test.ts @@ -0,0 +1,47 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { initRenderer } from '../renderer/renderer-impl' +import { renderMarkdown } from '../utils/markdownHelpers' + +const mermaid = vi.hoisted(() => { + let release: (() => void) | undefined + const gate = new Promise((resolve) => { + release = resolve + }) + return { + initialize: vi.fn(), + render: vi.fn(async (id: string) => { + await gate + return { svg: `` } + }), + release: () => release!(), + } +}) + +vi.mock(`mermaid`, () => ({ + default: { initialize: mermaid.initialize, render: mermaid.render }, +})) + +function render(md: string): string { + return renderMarkdown(md, initRenderer({ themeMode: `light` })).html +} + +describe(`mermaid extension`, () => { + afterEach(() => { + document.body.innerHTML = `` + }) + + it(`shares a pending render so re-renders cannot clash on the element id`, async () => { + const markdown = `\`\`\`mermaid\ngraph TD\n A --> B\n\`\`\`` + const host = document.createElement(`div`) + document.body.append(host) + + host.innerHTML = render(markdown) + host.innerHTML = render(markdown) + await vi.waitFor(() => expect(mermaid.render).toHaveBeenCalled()) + mermaid.release() + + await vi.waitFor(() => expect(host.querySelector(`.mermaid-diagram svg`)).not.toBeNull()) + expect(mermaid.render).toHaveBeenCalledTimes(1) + expect(render(markdown)).toContain(`viewBox="0 0 10 10"`) + }) +}) diff --git a/packages/core/src/extensions/mermaid.ts b/packages/core/src/extensions/mermaid.ts index a46b0ed93a..6cbcb9592a 100644 --- a/packages/core/src/extensions/mermaid.ts +++ b/packages/core/src/extensions/mermaid.ts @@ -54,17 +54,29 @@ function buildCacheKey(code: string, themeMode?: DiagramThemeMode): string { const svgCache = createSVGCache(50) +// A re-render while a diagram is still rendering (e.g. on `md:mathjax-ready`) would +// start a second mermaid.render with the same element id; the two runs then measure +// each other's temporary elements and cache an SVG without viewBox or size. +const pendingRenders = new Map>() + async function renderMermaidSvg(code: string, themeMode?: DiagramThemeMode): Promise { const cacheKey = buildCacheKey(code, themeMode) const cached = svgCache.get(cacheKey) if (cached) return cached - const mermaid = await getMermaid() - mermaid.initialize(getMermaidThemeConfig(themeMode)) - const result = await mermaid.render(`mermaid-svg-${cacheKey}`, code) - svgCache.set(cacheKey, result.svg) - return result.svg + let pending = pendingRenders.get(cacheKey) + if (!pending) { + pending = (async () => { + const mermaid = await getMermaid() + mermaid.initialize(getMermaidThemeConfig(themeMode)) + const result = await mermaid.render(`mermaid-svg-${cacheKey}`, code) + svgCache.set(cacheKey, result.svg) + return result.svg + })().finally(() => pendingRenders.delete(cacheKey)) + pendingRenders.set(cacheKey, pending) + } + return pending } function renderMermaid(id: string, code: string, cacheKey: string, themeMode?: DiagramThemeMode) {