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
15 changes: 15 additions & 0 deletions src/components/markdown/CodeBlock.server.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<MermaidBlock
className={props.className}
code={code}
isEmbedded={props.isEmbedded}
showTypeCopyButton={props.showTypeCopyButton}
style={props.style}
title={title}
/>
)
}

const rendered = await renderCodeBlockData({
code,
lang,
Expand Down
31 changes: 31 additions & 0 deletions src/components/markdown/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import * as React from 'react'
import { CodeBlockView } from './CodeBlockView'
import { MermaidBlock } from './MermaidBlock'
import {
buildPlainCodeBlockHtml,
extractCodeBlockData,
Expand Down Expand Up @@ -30,6 +31,36 @@ function getRenderPromise(

export function CodeBlock(props: CodeBlockProps) {
const { code, lang, title } = extractCodeBlockData(props)

if (lang === 'mermaid') {
return (
<MermaidBlock
className={props.className}
code={code}
isEmbedded={props.isEmbedded}
showTypeCopyButton={props.showTypeCopyButton}
style={props.style}
title={title}
/>
)
}

return (
<HighlightedCodeBlock code={code} lang={lang} props={props} title={title} />
)
}

function HighlightedCodeBlock({
code,
lang,
props,
title,
}: {
code: string
lang: string
props: CodeBlockProps
title?: string
}) {
const [rendered, setRendered] = React.useState<RenderedCodeBlockData | null>(
null,
)
Expand Down
131 changes: 131 additions & 0 deletions src/components/markdown/MermaidBlock.tsx
Original file line number Diff line number Diff line change
@@ -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<MermaidRenderState>({
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 (
<CodeBlockView
className={className}
copyText={code.trimEnd()}
htmlMarkup={buildPlainCodeBlockHtml(code)}
isEmbedded={isEmbedded}
lang="mermaid"
showTypeCopyButton={showTypeCopyButton}
style={style}
title={title}
/>
)
}
Comment on lines +90 to +117
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error state is indistinguishable from the loading state and silently hides render failures.

On catch, renderState becomes 'error' but the UI renders the exact same CodeBlockView with the raw mermaid source as the 'loading' state does. A malformed diagram will therefore appear as a normal code block with no feedback that rendering failed — both to the user and to anyone debugging. At minimum, log the caught error (mermaid throws informative parse errors) and render a distinct error surface so authors can tell the difference.

🛠️ Suggested change
-      } catch {
+      } catch (error) {
         if (!cancelled) {
+          console.error('Failed to render mermaid diagram', error)
           setRenderState({ status: 'error' })
         }
       }

And branch the render so the error path is visible (e.g., render CodeBlockView with an added title like `${title ?? ''} (mermaid render failed)` or a small inline error message above the fallback).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/markdown/MermaidBlock.tsx` around lines 90 - 117, The catch
block inside the useEffect's renderMermaid flow currently sets renderState to
'error' but neither logs the caught error nor provides a distinct UI; update the
catch to capture the error object, call processLogger/error console (or existing
logging utility) with the error and context (e.g., mermaidId, code), call
setRenderState({ status: 'error', error }) and then change the render branch
that currently returns CodeBlockView when renderState.status !== 'rendered' to
distinguish 'loading' vs 'error' (e.g., when renderState.status === 'error'
render CodeBlockView with an augmented title like `${title ?? ''} (mermaid
render failed)` or display a small inline error message above the fallback) so
parse failures are logged and visibly indicated to authors; reference
functions/identifiers: renderMermaid, setRenderState, renderState,
CodeBlockView, mermaidId, and code.


return (
<div
className={twMerge(
'mermaid-block not-prose w-full max-w-full overflow-x-auto rounded-md border border-gray-500/20 bg-white p-4 dark:bg-gray-950 [&_svg]:mx-auto [&_svg]:max-w-none',
className,
)}
style={style}
role="img"
aria-label={title ?? 'Mermaid diagram'}
dangerouslySetInnerHTML={{ __html: renderState.svg }}
/>
)
}
Loading