From a9566402049db84439cb5520f3ed6322ab35df90 Mon Sep 17 00:00:00 2001 From: Sarah Gerrard Date: Fri, 24 Apr 2026 10:29:30 -0700 Subject: [PATCH] fix mermaid diagrams --- src/components/markdown/CodeBlock.server.tsx | 15 +++ src/components/markdown/CodeBlock.tsx | 31 +++++ src/components/markdown/MermaidBlock.tsx | 131 +++++++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 src/components/markdown/MermaidBlock.tsx diff --git a/src/components/markdown/CodeBlock.server.tsx b/src/components/markdown/CodeBlock.server.tsx index 7bde2bad..5b8a4082 100644 --- a/src/components/markdown/CodeBlock.server.tsx +++ b/src/components/markdown/CodeBlock.server.tsx @@ -1,10 +1,25 @@ import { CodeBlockView } from './CodeBlockView' +import { MermaidBlock } from './MermaidBlock' import type { CodeBlockProps } from './codeBlock.shared' import { extractCodeBlockData } from './codeBlock.shared' import { renderCodeBlockData } from './renderCodeBlock.server' export async function CodeBlock(props: CodeBlockProps) { const { code, lang, title } = extractCodeBlockData(props) + + if (lang === 'mermaid') { + return ( + + ) + } + const rendered = await renderCodeBlockData({ code, lang, diff --git a/src/components/markdown/CodeBlock.tsx b/src/components/markdown/CodeBlock.tsx index d1e5b7ab..39904d0a 100644 --- a/src/components/markdown/CodeBlock.tsx +++ b/src/components/markdown/CodeBlock.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { CodeBlockView } from './CodeBlockView' +import { MermaidBlock } from './MermaidBlock' import { buildPlainCodeBlockHtml, extractCodeBlockData, @@ -30,6 +31,36 @@ function getRenderPromise( export function CodeBlock(props: CodeBlockProps) { const { code, lang, title } = extractCodeBlockData(props) + + if (lang === 'mermaid') { + return ( + + ) + } + + return ( + + ) +} + +function HighlightedCodeBlock({ + code, + lang, + props, + title, +}: { + code: string + lang: string + props: CodeBlockProps + title?: string +}) { const [rendered, setRendered] = React.useState( null, ) diff --git a/src/components/markdown/MermaidBlock.tsx b/src/components/markdown/MermaidBlock.tsx new file mode 100644 index 00000000..8151d4c6 --- /dev/null +++ b/src/components/markdown/MermaidBlock.tsx @@ -0,0 +1,131 @@ +'use client' + +import * as React from 'react' +import { twMerge } from 'tailwind-merge' +import { CodeBlockView } from './CodeBlockView' +import { buildPlainCodeBlockHtml } from './codeBlock.shared' + +type MermaidRenderState = + | { + status: 'loading' + svg?: undefined + } + | { + status: 'rendered' + svg: string + } + | { + status: 'error' + svg?: undefined + } + +function getIsDarkMode() { + return document.documentElement.classList.contains('dark') +} + +function useIsDarkMode() { + const [isDark, setIsDark] = React.useState(false) + + React.useEffect(() => { + const updateIsDark = () => setIsDark(getIsDarkMode()) + updateIsDark() + + const observer = new MutationObserver(updateIsDark) + observer.observe(document.documentElement, { + attributeFilter: ['class'], + attributes: true, + }) + + return () => observer.disconnect() + }, []) + + return isDark +} + +export function MermaidBlock({ + className, + code, + isEmbedded, + showTypeCopyButton, + style, + title, +}: { + className?: string + code: string + isEmbedded?: boolean + showTypeCopyButton?: boolean + style?: React.CSSProperties + title?: string +}) { + const isDark = useIsDarkMode() + const reactId = React.useId() + const mermaidId = React.useMemo( + () => `mermaid-${reactId.replace(/[^a-zA-Z0-9_-]/g, '')}`, + [reactId], + ) + const [renderState, setRenderState] = React.useState({ + status: 'loading', + }) + + React.useEffect(() => { + let cancelled = false + + async function renderMermaid() { + setRenderState({ status: 'loading' }) + + try { + const mermaid = (await import('mermaid')).default + + mermaid.initialize({ + securityLevel: 'strict', + startOnLoad: false, + theme: isDark ? 'dark' : 'default', + }) + + const { svg } = await mermaid.render(mermaidId, code) + + if (!cancelled) { + setRenderState({ status: 'rendered', svg }) + } + } catch { + if (!cancelled) { + setRenderState({ status: 'error' }) + } + } + } + + void renderMermaid() + + return () => { + cancelled = true + } + }, [code, isDark, mermaidId]) + + if (renderState.status !== 'rendered') { + return ( + + ) + } + + return ( +
+ ) +}