Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// @vitest-environment jsdom

import { beforeEach, describe, expect, it, vi } from 'vitest';

const mermaidRun = vi.fn();

vi.mock('../../utils/diagram', () => ({
default: vi.fn(async (name: string) => {
if (name === 'mermaid') {
return {
initialize: vi.fn(),
run: mermaidRun,
};
}
throw new Error(`unexpected renderer ${name}`);
}),
}));

// Import after the mock is registered so MarkdownToHtml uses the fake renderer.
const { MarkdownToHtml } = await import('../markdownToHtml');

beforeEach(() => {
mermaidRun.mockReset();
});

const INVALID_MERMAID = [
'# Title',
'',
'Intro paragraph.',
'',
'```mermaid',
'graph LR',
'H[a|b|c]',
'```',
'',
'Trailing paragraph.',
'',
].join('\n');

describe('#4812: mermaid syntax error must not abort export', () => {
it('renderHtml resolves even when a mermaid diagram fails to parse', async () => {
mermaidRun.mockRejectedValue(new Error('Parse error'));

const html = await new MarkdownToHtml(INVALID_MERMAID).renderHtml();

expect(html).toContain('Title');
expect(html).toContain('Intro paragraph.');
expect(html).toContain('Trailing paragraph.');
expect(html).toContain('< Invalid Diagram >');
});

it('one broken diagram does not stop a later valid diagram from rendering', async () => {
mermaidRun
.mockRejectedValueOnce(new Error('Parse error'))
.mockResolvedValueOnce(undefined);

const markdown = [
'```mermaid',
'graph LR',
'H[a|b|c]',
'```',
'',
'```mermaid',
'graph TD; A-->B',
'```',
'',
].join('\n');

const html = await new MarkdownToHtml(markdown).renderHtml();

expect(mermaidRun).toHaveBeenCalledTimes(2);
expect(html).toContain('< Invalid Diagram >');
});

it('does not load mermaid when the document contains no mermaid diagrams', async () => {
const html = await new MarkdownToHtml('plain paragraph').renderHtml();

expect(html).toContain('plain paragraph');
expect(mermaidRun).not.toHaveBeenCalled();
});
});
19 changes: 16 additions & 3 deletions third_party/muya/src/state/markdownToHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,29 @@ export class MarkdownToHtml {
mermaidContainer.classList.add('mermaid');
preEle.replaceWith(mermaidContainer);
}
const nodes = [...this._exportContainer!.querySelectorAll('div.mermaid')];
if (nodes.length === 0)
return;

const mermaid = await loadRenderer('mermaid');
// We only export light theme, so set mermaid theme to `default`, in the future, we can choose which theme to export.
mermaid.initialize({
startOnLoad: false,
securityLevel: 'strict',
theme: 'default',
});
await mermaid.run({
nodes: [...this._exportContainer!.querySelectorAll('div.mermaid')],
});
// Render each diagram in isolation: `mermaid.run` rejects the whole
// batch on the first parse error, so one invalid diagram used to abort
// the entire export (#4812). Contain the failure to that diagram and
// fall back to the same placeholder the other diagram renderers use.
for (const node of nodes) {
try {
await mermaid.run({ nodes: [node] });
}
catch {
node.innerHTML = '< Invalid Diagram >';
}
}
if (this._muya) {
mermaid.initialize({
securityLevel: 'strict',
Expand Down
Loading