Skip to content
Closed
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
5 changes: 1 addition & 4 deletions src/components/CopyPageDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ import {
DropdownContent,
DropdownItem,
} from './Dropdown'
import {
getPackageManager,
PACKAGE_MANAGERS,
} from '~/utils/markdown/installCommand'
import { getPackageManager } from '~/utils/markdown/installCommand'

// Markdown icon component matching the screenshot
function MarkdownIcon({ className }: { className?: string }) {
Expand Down
86 changes: 50 additions & 36 deletions src/components/markdown/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,57 +155,71 @@ export function CodeBlock({
const code = children?.props.children

const [codeElement, setCodeElement] = React.useState(
<pre ref={ref} className={`shiki h-full github-light dark:aurora-x`}>
<pre
ref={ref}
className="h-full p-4 bg-white dark:bg-gray-950 text-gray-800 dark:text-gray-200"
>
<code>{lang === 'mermaid' ? <svg /> : code}</code>
</pre>,
)

React[
typeof document !== 'undefined' ? 'useLayoutEffect' : 'useEffect'
](() => {
let cancelled = false
;(async () => {
const themes = ['github-light', 'aurora-x']
const langStr = lang || 'plaintext'
try {
const themes = ['github-light', 'aurora-x']
const langStr = lang || 'plaintext'

const { highlighter, effectiveLang } = await getHighlighter(langStr)
// Trim trailing newlines to prevent empty lines at end of code block
const trimmedCode = (code || '').trimEnd()
const { highlighter, effectiveLang } = await getHighlighter(langStr)
// Trim trailing newlines to prevent empty lines at end of code block
const trimmedCode = (code || '').trimEnd()

const htmls = await Promise.all(
themes.map(async (theme) => {
const output = highlighter.codeToHtml(trimmedCode, {
lang: effectiveLang,
theme,
transformers: [transformerNotationDiff()],
})
const htmls = await Promise.all(
themes.map(async (theme) => {
const output = highlighter.codeToHtml(trimmedCode, {
lang: effectiveLang,
theme,
transformers: [transformerNotationDiff()],
})

if (lang === 'mermaid') {
const preAttributes = extractPreAttributes(output)
let svgHtml = genSvgMap.get(trimmedCode)
if (!svgHtml) {
const mermaid = await getMermaid()
const { svg } = await mermaid.render('foo', trimmedCode)
genSvgMap.set(trimmedCode, svg)
svgHtml = svg
if (lang === 'mermaid') {
const preAttributes = extractPreAttributes(output)
let svgHtml = genSvgMap.get(trimmedCode)
if (!svgHtml) {
const mermaid = await getMermaid()
const { svg } = await mermaid.render('foo', trimmedCode)
genSvgMap.set(trimmedCode, svg)
Comment on lines +191 to +193
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For Mermaid v11.11.0, what does the first idargument ofmermaid.render(id, text) control, and is it required/recommended to be unique per rendered diagram instance?

💡 Result:

In Mermaid v11.11.0, the first argument id in await mermaid.render(id, text) is used as the DOM id of the generated root <svg> element. Mermaid’s internal renderer literally does .append('svg').attr('id', id). [1]

It’s effectively required (the parameter is not optional) and it’s strongly recommended to be unique per rendered diagram instance in the same document, because Mermaid also derives other temporary element IDs/selectors from it (e.g. 'd' + id, 'i' + id) and will remove any existing elements with the same id before rendering—so reusing an id can overwrite/remove a previously-rendered diagram or otherwise cause collisions. [2]

Sources
[1] https://raw.githubusercontent.com/mermaid-js/mermaid/9c85521/packages/mermaid/src/mermaidAPI.ts
[2] https://raw.githubusercontent.com/mermaid-js/mermaid/9c85521/packages/mermaid/src/mermaidAPI.ts


🏁 Script executed:

cat -n src/components/markdown/CodeBlock.tsx | sed -n '185,200p'

Repository: TanStack/tanstack.com

Length of output: 776


🏁 Script executed:

# Check broader context - how many mermaid blocks might be rendered on a single page
# and if this function is called multiple times
grep -n "lang === 'mermaid'" src/components/markdown/CodeBlock.tsx

Repository: TanStack/tanstack.com

Length of output: 167


Use a unique Mermaid render ID instead of a constant.

The hardcoded 'foo' ID in mermaid.render('foo', trimmedCode) at line 192 will cause duplicate SVG IDs and broken marker/style references when multiple Mermaid blocks are rendered on the same page. According to Mermaid's API, the id parameter becomes the DOM id of the root <svg> element, and reusing the same ID can cause collisions, overwrites, and derived ID conflicts within Mermaid's internal selectors.

Generate a unique ID for each render call:

Suggested fix
 let mermaidInstance: Mermaid | null = null
+let mermaidRenderCounter = 0
 const genSvgMap = new Map<string, string>()

 ...
-                const { svg } = await mermaid.render('foo', trimmedCode)
+                const renderId = `mermaid-${++mermaidRenderCounter}`
+                const { svg } = await mermaid.render(renderId, trimmedCode)
                 genSvgMap.set(trimmedCode, svg)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/markdown/CodeBlock.tsx` around lines 191 - 193, The current
mermaid.render call uses a constant id 'foo', which causes SVG id collisions;
update the CodeBlock.tsx rendering logic to generate a unique id per render (for
example using a short UUID, a timestamp + counter, or the trimmedCode hash) and
pass that id into mermaid.render instead of 'foo', ensure the same generated id
is used if caching via genSvgMap for trimmedCode so lookups remain consistent,
and obtain the mermaid instance via getMermaid() as before.

svgHtml = svg
}
const cls = twMerge(preAttributes.class, 'py-4 bg-neutral-50')
return `<div class='${cls}'>${svgHtml}</div>`
}
return `<div class='${preAttributes.class} py-4 bg-neutral-50'>${svgHtml}</div>`
}

return output
}),
)
return output
}),
)

setCodeElement(
<div
className={twMerge(
isEmbedded ? 'h-full [&>pre]:h-full [&>pre]:rounded-none' : '',
)}
dangerouslySetInnerHTML={{ __html: htmls.join('') }}
ref={ref}
/>,
)
if (!cancelled) {
setCodeElement(
<div
className={twMerge(
isEmbedded ? 'h-full [&>pre]:h-full [&>pre]:rounded-none' : '',
)}
dangerouslySetInnerHTML={{ __html: htmls.join('') }}
ref={ref}
/>,
)
}
} catch (err) {
console.warn('Shiki highlighting failed:', err)
}
Comment on lines +215 to +217
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add a real fallback UI in the error path.

When highlighting/rendering fails, the catch path only logs. For Mermaid blocks, the initial state can stay visually empty (placeholder <svg />), so users still get a blank block on failure.

Suggested fix
       } catch (err) {
         console.warn('Shiki highlighting failed:', err)
+        if (!cancelled) {
+          setCodeElement(
+            <pre
+              ref={ref}
+              className="h-full p-4 bg-white dark:bg-gray-950 text-gray-800 dark:text-gray-200"
+            >
+              <code>{code || ''}</code>
+            </pre>,
+          )
+        }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/markdown/CodeBlock.tsx` around lines 215 - 217, The catch
block in CodeBlock.tsx only logs errors ("Shiki highlighting failed") and leaves
the component blank; update the error path to set a real fallback UI so users
see a sensible placeholder: in the catch of the highlighting routine (the
function that performs Shiki highlighting, e.g., highlightCode or the async
block around shiki), set the component state used to render output (e.g.,
setHighlightedHtml / setRenderState) to a fallback markup for normal code blocks
(plain escaped code wrapped in a <pre><code>...)</code></pre> and for mermaid
blocks call the existing mermaid render path fallback (renderMermaid) or set the
mermaid state to an empty <svg /> placeholder; ensure the fallback path is used
by the render branch that reads the highlighted state so users see the
placeholder instead of nothing.

})()
}, [code, lang])
return () => {
cancelled = true
}
}, [code, lang, isEmbedded])

return (
<div
Expand Down