Skip to content

Commit 9bbb204

Browse files
authored
fix: Revert back to mermaid.js for diagram rendering & update styling (#2949)
1 parent 3666218 commit 9bbb204

4 files changed

Lines changed: 864 additions & 80 deletions

File tree

components/Mermaid.tsx

Lines changed: 228 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,243 @@
1-
import { renderMermaidSVG } from "beautiful-mermaid";
2-
import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock";
1+
"use client";
32

4-
interface MermaidProps {
5-
chart: string;
6-
}
3+
import { useEffect, useState } from "react";
4+
import type { MermaidConfig } from "mermaid";
5+
import { Loader2 } from "lucide-react";
6+
import { useTheme } from "next-themes";
77

8-
// beautiful-mermaid is stricter than mermaid.js, so normalize a couple of
9-
// legacy patterns that still exist in docs content before rendering.
10-
function normalizeMermaidInput(chart: string): string {
11-
const lines = chart.split("\n");
8+
const renderedSvgCache = new Map<string, string>();
9+
const renderedSvgByThemeModeCache = new Map<string, string>();
10+
const pendingRenderCache = new Map<string, Promise<string>>();
1211

13-
let startIndex = 0;
14-
if (lines[0]?.trim() === "---") {
15-
const closingIndex = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
16-
if (closingIndex !== -1) {
17-
startIndex = closingIndex + 1;
18-
}
19-
}
12+
let nextMermaidRenderId = 0;
13+
let mermaidRenderQueue = Promise.resolve();
2014

21-
return lines
22-
.slice(startIndex)
23-
.map((line) => line.replace(/;\s*$/, ""))
24-
.join("\n")
15+
function getCssVariableValue(name: string): string {
16+
return getComputedStyle(document.documentElement)
17+
.getPropertyValue(name)
2518
.trim();
2619
}
2720

28-
// The library inlines Google Fonts imports into the SVG. Strip those so
29-
// diagrams inherit the site's existing font setup instead of loading fonts.
30-
function normalizeMermaidSvgFonts(svg: string): string {
31-
return svg.replace(/@import\s+url\('https:\/\/fonts\.googleapis\.com\/[^']+'\);\s*/g, "");
21+
function getMermaidThemeValues() {
22+
return {
23+
surface1: getCssVariableValue("--surface-1"),
24+
surface2: getCssVariableValue("--surface-2"),
25+
textPrimary: getCssVariableValue("--text-primary"),
26+
textTertiary: getCssVariableValue("--text-tertiary"),
27+
lineCta: getCssVariableValue("--line-cta"),
28+
lineStructure: getCssVariableValue("--line-structure"),
29+
};
3230
}
3331

34-
export function Mermaid({ chart }: MermaidProps) {
35-
try {
36-
const svg = normalizeMermaidSvgFonts(renderMermaidSVG(normalizeMermaidInput(chart), {
37-
bg: "var(--surface-1)",
38-
fg: "var(--text-primary)",
39-
line: "var(--line-cta)",
40-
accent: "var(--line-cta)",
41-
muted: "var(--text-tertiary)",
42-
surface: "var(--surface-2)",
43-
border: "var(--text-tertiary)",
44-
interactive: false,
45-
transparent: true,
46-
}));
32+
function getMermaidConfig(
33+
themeValues: ReturnType<typeof getMermaidThemeValues>,
34+
): MermaidConfig {
35+
const {
36+
surface1,
37+
surface2,
38+
textPrimary,
39+
textTertiary,
40+
lineCta,
41+
lineStructure,
42+
} = themeValues;
4743

48-
return (
49-
<div
50-
className="mermaid-diagram my-4 overflow-x-auto border p-4 not-prose rounded-none"
51-
dangerouslySetInnerHTML={{ __html: svg }}
52-
/>
53-
);
54-
} catch (error) {
55-
console.warn("Failed to render Mermaid diagram", error);
44+
return {
45+
startOnLoad: false,
46+
securityLevel: "strict",
47+
theme: "base",
48+
htmlLabels: true,
49+
fontFamily: "var(--font-sans)",
50+
themeVariables: {
51+
background: "transparent",
52+
mainBkg: surface2,
53+
primaryColor: surface2,
54+
primaryBorderColor: textTertiary,
55+
primaryTextColor: textPrimary,
56+
secondaryColor: surface1,
57+
secondaryBorderColor: textTertiary,
58+
secondaryTextColor: textPrimary,
59+
tertiaryColor: surface1,
60+
tertiaryBorderColor: textTertiary,
61+
tertiaryTextColor: textPrimary,
62+
lineColor: lineCta,
63+
textColor: textPrimary,
64+
edgeLabelBackground: surface1,
65+
clusterBkg: surface1,
66+
clusterBorder: lineStructure,
67+
nodeBorder: textTertiary,
68+
defaultLinkColor: lineCta,
69+
},
70+
};
71+
}
72+
73+
function getMermaidCacheKey(chart: string, themeMode: string): string {
74+
return `${themeMode}\n${chart}`;
75+
}
76+
77+
function enqueueMermaidRender<T>(render: () => Promise<T>): Promise<T> {
78+
const queuedRender = mermaidRenderQueue.then(render, render);
79+
mermaidRenderQueue = queuedRender.then(
80+
() => undefined,
81+
() => undefined,
82+
);
83+
return queuedRender;
84+
}
85+
86+
async function renderMermaidChart(
87+
chart: string,
88+
cacheKey: string,
89+
themeValues: ReturnType<typeof getMermaidThemeValues>,
90+
): Promise<string> {
91+
const cachedSvg = renderedSvgCache.get(cacheKey);
92+
if (cachedSvg) return cachedSvg;
93+
94+
const pendingRender = pendingRenderCache.get(cacheKey);
95+
if (pendingRender) return pendingRender;
96+
97+
const mermaidConfig = getMermaidConfig(themeValues);
98+
const renderPromise = enqueueMermaidRender(() =>
99+
import("mermaid").then(({ default: mermaid }) => {
100+
mermaid.initialize(mermaidConfig);
101+
return mermaid.render(`mermaid-diagram-${nextMermaidRenderId++}`, chart);
102+
}),
103+
)
104+
.then(({ svg }) => {
105+
renderedSvgCache.set(cacheKey, svg);
106+
pendingRenderCache.delete(cacheKey);
107+
return svg;
108+
})
109+
.catch((error: unknown) => {
110+
pendingRenderCache.delete(cacheKey);
111+
throw error;
112+
});
113+
114+
pendingRenderCache.set(cacheKey, renderPromise);
115+
return renderPromise;
116+
}
117+
118+
export function Mermaid({ chart }: { chart: string }) {
119+
const { resolvedTheme } = useTheme();
120+
const [error, setError] = useState<Error | null>(null);
121+
const [renderedSvg, setRenderedSvg] = useState<string | null>(null);
122+
const [renderedCacheKey, setRenderedCacheKey] = useState<string | null>(null);
123+
const [currentTheme, setCurrentTheme] = useState<{
124+
cacheSignature: string;
125+
values: ReturnType<typeof getMermaidThemeValues>;
126+
} | null>(null);
127+
const currentCacheKey =
128+
currentTheme === null
129+
? null
130+
: getMermaidCacheKey(chart, currentTheme.cacheSignature);
131+
const isRendered =
132+
renderedSvg !== null && renderedCacheKey === currentCacheKey;
133+
const themeModeCacheKey =
134+
resolvedTheme === undefined
135+
? null
136+
: getMermaidCacheKey(chart, resolvedTheme);
137+
const visibleSvg =
138+
isRendered || themeModeCacheKey === null
139+
? renderedSvg
140+
: (renderedSvgByThemeModeCache.get(themeModeCacheKey) ?? null);
56141

142+
useEffect(() => {
143+
setError(null);
144+
setCurrentTheme(null);
145+
146+
if (resolvedTheme === undefined) return;
147+
148+
const frame = requestAnimationFrame(() => {
149+
const values = getMermaidThemeValues();
150+
setCurrentTheme({
151+
cacheSignature: [
152+
resolvedTheme,
153+
values.surface1,
154+
values.surface2,
155+
values.textPrimary,
156+
values.textTertiary,
157+
values.lineCta,
158+
values.lineStructure,
159+
].join("\n"),
160+
values,
161+
});
162+
});
163+
164+
return () => {
165+
cancelAnimationFrame(frame);
166+
};
167+
}, [resolvedTheme]);
168+
169+
useEffect(() => {
170+
let isCurrent = true;
171+
172+
if (currentTheme === null) return;
173+
174+
const cacheKey = getMermaidCacheKey(chart, currentTheme.cacheSignature);
175+
setError(null);
176+
177+
const cachedSvg = renderedSvgCache.get(cacheKey);
178+
if (cachedSvg) {
179+
if (themeModeCacheKey !== null) {
180+
renderedSvgByThemeModeCache.set(themeModeCacheKey, cachedSvg);
181+
}
182+
setRenderedSvg(cachedSvg);
183+
setRenderedCacheKey(cacheKey);
184+
return;
185+
}
186+
187+
setRenderedSvg(null);
188+
setRenderedCacheKey(null);
189+
190+
renderMermaidChart(chart, cacheKey, currentTheme.values)
191+
.then((svg) => {
192+
if (!isCurrent) return;
193+
194+
if (themeModeCacheKey !== null) {
195+
renderedSvgByThemeModeCache.set(themeModeCacheKey, svg);
196+
}
197+
setRenderedSvg(svg);
198+
setRenderedCacheKey(cacheKey);
199+
})
200+
.catch((renderError: unknown) => {
201+
if (!isCurrent) return;
202+
203+
console.warn("Failed to render Mermaid diagram", renderError);
204+
setError(
205+
renderError instanceof Error
206+
? renderError
207+
: new Error(String(renderError)),
208+
);
209+
});
210+
211+
return () => {
212+
isCurrent = false;
213+
};
214+
}, [chart, currentTheme, themeModeCacheKey]);
215+
216+
if (error) {
57217
return (
58-
<CodeBlock title="Mermaid">
59-
<Pre>{normalizeMermaidInput(chart)}</Pre>
60-
</CodeBlock>
218+
<pre className="my-4 overflow-x-auto border border-line-structure bg-surface-1 p-4 text-left text-xs text-text-primary not-prose">
219+
{chart}
220+
</pre>
61221
);
62222
}
223+
224+
return (
225+
<div className="mermaid-diagram my-4 overflow-x-auto border p-4 not-prose rounded-none">
226+
{visibleSvg === null && (
227+
<div
228+
aria-hidden="true"
229+
className="flex items-center justify-center py-6"
230+
>
231+
<Loader2 className="size-4 animate-spin text-text-tertiary" />
232+
</div>
233+
)}
234+
<div
235+
key={renderedCacheKey ?? themeModeCacheKey}
236+
className={visibleSvg === null ? "hidden" : ""}
237+
dangerouslySetInnerHTML={
238+
visibleSvg === null ? undefined : { __html: visibleSvg }
239+
}
240+
/>
241+
</div>
242+
);
63243
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@
7474
"@vercel/og": "^0.6.8",
7575
"@vidstack/react": "^1.12.13",
7676
"ai": "^6.0.153",
77-
"beautiful-mermaid": "^1.1.3",
7877
"class-variance-authority": "^0.7.1",
7978
"clsx": "^2.1.1",
8079
"embla-carousel-react": "^8.6.0",
@@ -94,6 +93,7 @@
9493
"lucide-react": "^0.577.0",
9594
"marked": "^16.3.0",
9695
"mdast-util-mdx-jsx": "^3.2.0",
96+
"mermaid": "^11.15.0",
9797
"nanoid": "^5.1.5",
9898
"next": "^16.2.6",
9999
"next-sitemap": "^4.2.3",

0 commit comments

Comments
 (0)