feat(markdown): mermaid diagram support

This commit is contained in:
Delioos
2026-05-07 18:31:28 +02:00
committed by Rūdolfs Ošiņš
parent c39af0a8e2
commit faadd2906b
5 changed files with 1215 additions and 5 deletions
+1150 -3
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -78,6 +78,7 @@
"marked-linkify-it": "^3.1.15",
"md5": "^2.3.0",
"svelte": "^5.55.4",
"mermaid": "^11.14.0",
"twemoji": "^14.0.2",
"zod": "^4.3.6"
}
+47 -2
View File
@@ -3,6 +3,7 @@
import matter from "@radicle/gray-matter";
import { afterUpdate } from "svelte";
import { toDom } from "hast-util-to-dom";
import mermaid from "mermaid";
import * as router from "@app/lib/router";
import * as modal from "@app/lib/modal";
@@ -100,6 +101,12 @@
}
afterUpdate(async () => {
// Initialize mermaid with error handling configuration
mermaid.initialize({
startOnLoad: false,
securityLevel: 'loose',
});
for (const e of container.querySelectorAll("a")) {
try {
const url = new URL(e.href);
@@ -210,7 +217,7 @@
// Replaces code blocks in the background with highlighted code.
const prefix = "language-";
const nodes = Array.from(document.body.querySelectorAll("pre code"));
const nodes = Array.from(container.querySelectorAll("pre code"));
const treeChanges: Promise<void>[] = [];
@@ -231,8 +238,36 @@
);
if (!className) continue;
const language = className.slice(prefix.length);
// Handle mermaid diagrams
if (language === 'mermaid') {
const mermaidCode = node.textContent || '';
try {
// First, validate the syntax without rendering
const isValid = await mermaid.parse(mermaidCode, { suppressErrors: true });
if (isValid) {
// Generate a unique id for this diagram
const diagramId = `mermaid-${Math.random().toString(36).substr(2, 9)}`;
// Try to render the mermaid diagram
const { svg } = await mermaid.render(diagramId, mermaidCode);
// Replace the code block with the rendered diagram
const mermaidDiv = document.createElement('div');
mermaidDiv.innerHTML = svg;
mermaidDiv.classList.add('mermaid-diagram');
preWrapper.replaceWith(mermaidDiv);
}
} catch (error) {
// On any error, keep the raw text visible (do nothing, let it render as code)
}
continue;
}
treeChanges.push(
highlight(node.textContent ?? "", className.slice(prefix.length))
highlight(node.textContent ?? "", language)
.then(tree => {
if (tree) {
node.replaceChildren(toDom(tree, { fragment: true }));
@@ -500,6 +535,16 @@
.markdown :global(dl dd) {
margin: 0 0 0 2rem;
}
.markdown :global(.mermaid-diagram) {
margin: 1rem 0;
text-align: center;
}
.markdown :global(.mermaid-diagram svg) {
max-width: 100%;
height: auto;
}
</style>
{#if frontMatter && frontMatter.length > 0}
Binary file not shown.
+17
View File
@@ -125,6 +125,23 @@ test("math", async ({ page }) => {
await expect(page).toHaveScreenshot({ fullPage: true });
});
test("mermaid diagram", async ({ page }) => {
await page.goto(`${markdownUrl}/tree/main/mermaid.md`, {
waitUntil: "networkidle",
});
await expect(page.locator(".mermaid-diagram svg")).toBeVisible();
await expect(page).toHaveScreenshot({ fullPage: true });
});
test("broken mermaid falls back to code block", async ({ page }) => {
await page.goto(`${markdownUrl}/tree/main/mermaid-broken.md`, {
waitUntil: "networkidle",
});
await expect(page.locator("pre code.language-mermaid")).toBeVisible();
await expect(page.locator(".mermaid-diagram")).toHaveCount(0);
await expect(page).toHaveScreenshot({ fullPage: true });
});
test("relative image not able to being loaded", async ({ page }) => {
await page.goto(`${markdownUrl}/tree/main/loading-image.md`, {
waitUntil: "networkidle",