mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
fix: restore mermaid labels in markdown preview (#659)
This commit is contained in:
parent
cfe49ff404
commit
8c016c91b2
6 changed files with 65 additions and 11 deletions
|
|
@ -280,10 +280,14 @@ export default function MarkdownPreview({
|
|||
},
|
||||
// Why: Intercept code elements to detect mermaid fenced blocks. rehype-highlight
|
||||
// sets className="language-mermaid" on the <code> inside <pre> for ```mermaid blocks.
|
||||
// We render those as SVG diagrams instead of highlighted source.
|
||||
// We render those as SVG diagrams instead of highlighted source. Markdown preview
|
||||
// opts out of Mermaid HTML labels because this path sanitizes the SVG before
|
||||
// injection, and sanitized foreignObject labels disappear on some platforms.
|
||||
code: ({ className, children, ...props }) => {
|
||||
if (/language-mermaid/.test(className || '')) {
|
||||
return <MermaidBlock content={String(children).trimEnd()} isDark={isDark} />
|
||||
return (
|
||||
<MermaidBlock content={String(children).trimEnd()} isDark={isDark} htmlLabels={false} />
|
||||
)
|
||||
}
|
||||
return (
|
||||
<code className={className} {...props}>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import React, { useEffect, useId, useRef, useState } from 'react'
|
||||
import mermaid from 'mermaid'
|
||||
import DOMPurify from 'dompurify'
|
||||
import { getMermaidConfig } from './mermaid-config'
|
||||
|
||||
type MermaidBlockProps = {
|
||||
content: string
|
||||
isDark: boolean
|
||||
htmlLabels?: boolean
|
||||
}
|
||||
|
||||
// Why: mermaid.render() manipulates global DOM state (element IDs, internal
|
||||
|
|
@ -17,21 +19,26 @@ let renderQueue: Promise<void> = Promise.resolve()
|
|||
* Renders a mermaid diagram string as SVG. Falls back to raw source with an
|
||||
* error banner if the syntax is invalid — never breaks the rest of the preview.
|
||||
*/
|
||||
export default function MermaidBlock({ content, isDark }: MermaidBlockProps): React.JSX.Element {
|
||||
export default function MermaidBlock({
|
||||
content,
|
||||
isDark,
|
||||
htmlLabels = true
|
||||
}: MermaidBlockProps): React.JSX.Element {
|
||||
const id = useId().replace(/:/g, '_')
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const theme = isDark ? 'dark' : 'default'
|
||||
// Re-initialize on every effect so the theme stays in sync with the
|
||||
// current appearance. mermaid.initialize() is cheap and idempotent.
|
||||
mermaid.initialize({ startOnLoad: false, theme })
|
||||
|
||||
let cancelled = false
|
||||
|
||||
const render = async (): Promise<void> => {
|
||||
try {
|
||||
// Why: Mermaid stores initialize() config in global module state. Apply
|
||||
// the config inside the same serialized render task so another
|
||||
// MermaidBlock cannot overwrite htmlLabels/theme between initialize()
|
||||
// and render(), which would make markdown preview fall back to the
|
||||
// broken foreignObject label path again.
|
||||
mermaid.initialize(getMermaidConfig(isDark, htmlLabels))
|
||||
const { svg } = await mermaid.render(`mermaid-${id}`, content)
|
||||
if (!cancelled && containerRef.current) {
|
||||
// Why: although mermaid uses DOMPurify internally, we add an explicit
|
||||
|
|
@ -58,7 +65,7 @@ export default function MermaidBlock({ content, isDark }: MermaidBlockProps): Re
|
|||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [content, isDark, id])
|
||||
}, [content, htmlLabels, isDark, id])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -113,10 +113,12 @@ export function RichMarkdownCodeBlock({
|
|||
<NodeViewContent<'pre'> as="pre" />
|
||||
{/* Why: mermaid diagrams render as a live SVG preview below the editable
|
||||
source so users can see the result while editing. The code block stays
|
||||
editable — the diagram is read-only output. */}
|
||||
editable — the diagram is read-only output. This preview also goes
|
||||
through MermaidBlock's sanitized SVG path, so it must opt out of
|
||||
Mermaid HTML labels just like markdown preview to keep labels visible. */}
|
||||
{isMermaid && node.textContent.trim() && (
|
||||
<div contentEditable={false} className="mermaid-preview">
|
||||
<MermaidBlock content={node.textContent.trim()} isDark={isDark} />
|
||||
<MermaidBlock content={node.textContent.trim()} isDark={isDark} htmlLabels={false} />
|
||||
</div>
|
||||
)}
|
||||
</NodeViewWrapper>
|
||||
|
|
|
|||
28
src/renderer/src/components/editor/mermaid-config.test.ts
Normal file
28
src/renderer/src/components/editor/mermaid-config.test.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getMermaidConfig } from './mermaid-config'
|
||||
|
||||
describe('getMermaidConfig', () => {
|
||||
it('keeps Mermaid HTML labels enabled by default', () => {
|
||||
expect(getMermaidConfig(false)).toMatchObject({
|
||||
startOnLoad: false,
|
||||
theme: 'default',
|
||||
htmlLabels: true
|
||||
})
|
||||
})
|
||||
|
||||
it('can disable HTML labels for sanitized preview paths', () => {
|
||||
expect(getMermaidConfig(false, false)).toMatchObject({
|
||||
startOnLoad: false,
|
||||
theme: 'default',
|
||||
htmlLabels: false
|
||||
})
|
||||
})
|
||||
|
||||
it('switches to the dark mermaid theme when the preview is dark', () => {
|
||||
expect(getMermaidConfig(true)).toMatchObject({
|
||||
theme: 'dark',
|
||||
htmlLabels: true
|
||||
})
|
||||
})
|
||||
})
|
||||
12
src/renderer/src/components/editor/mermaid-config.ts
Normal file
12
src/renderer/src/components/editor/mermaid-config.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import type mermaid from 'mermaid'
|
||||
|
||||
export function getMermaidConfig(
|
||||
isDark: boolean,
|
||||
htmlLabels = true
|
||||
): Parameters<typeof mermaid.initialize>[0] {
|
||||
return {
|
||||
startOnLoad: false,
|
||||
theme: isDark ? 'dark' : 'default',
|
||||
htmlLabels
|
||||
}
|
||||
}
|
||||
1
src/renderer/src/mermaid.d.ts
vendored
1
src/renderer/src/mermaid.d.ts
vendored
|
|
@ -4,6 +4,7 @@ declare module 'mermaid' {
|
|||
type MermaidInitializeOptions = {
|
||||
startOnLoad?: boolean
|
||||
theme?: MermaidTheme
|
||||
htmlLabels?: boolean
|
||||
}
|
||||
|
||||
type MermaidRenderResult = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue