Skip to content
Open
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
97 changes: 97 additions & 0 deletions components/Sidebar/MermaidDiagram.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { useEffect, useRef, useState } from 'react'
import { Box } from '@chakra-ui/react'

let mermaidId = 0
let mermaidLoaded: Promise<any> | null = null

function loadMermaid(): Promise<any> {
if (mermaidLoaded) return mermaidLoaded
mermaidLoaded = new Promise((resolve, reject) => {
if (typeof window !== 'undefined' && (window as any).mermaid) {
resolve((window as any).mermaid)
return
}
const script = document.createElement('script')
script.src = 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js'
script.onload = () => {
const m = (window as any).mermaid
m.initialize({ startOnLoad: false, theme: 'dark', securityLevel: 'loose' })
resolve(m)
}
script.onerror = reject
document.head.appendChild(script)
})
return mermaidLoaded
}

export interface MermaidDiagramProps {
code: string
}

export const MermaidDiagram = ({ code }: MermaidDiagramProps) => {
const [svg, setSvg] = useState<string>('')
const [error, setError] = useState<string>('')
const [id] = useState(() => `mermaid-${mermaidId++}`)

useEffect(() => {
let cancelled = false
loadMermaid()
.then(async (mermaid) => {
try {
const { svg: rendered } = await mermaid.render(id, code)
if (!cancelled) {
setSvg(rendered)
setError('')
}
} catch (e: any) {
if (!cancelled) {
setError(e?.message || 'Failed to render diagram')
setSvg('')
}
}
})
.catch((e: any) => {
if (!cancelled) {
setError('Failed to load Mermaid library')
}
})
return () => {
cancelled = true
}
}, [code, id])

if (error) {
return (
<Box
p={3}
my={3}
bg="red.900"
color="red.200"
borderRadius="md"
fontFamily="monospace"
fontSize="sm"
>
Mermaid error: {error}
</Box>
)
}

if (!svg) {
return (
<Box p={3} my={3} color="gray.400" fontSize="sm">
Rendering diagram...
</Box>
)
}

return (
<Box
my={3}
p={2}
borderRadius="md"
overflow="auto"
sx={{ '& svg': { maxWidth: '100%' } }}
dangerouslySetInnerHTML={{ __html: svg }}
/>
)
}
35 changes: 35 additions & 0 deletions util/processOrg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { LinksByNodeId, NodeByCite, NodeById } from '../pages'
import React, { createContext, ReactNode, useMemo } from 'react'
import { OrgImage } from '../components/Sidebar/OrgImage'
import { Section } from '../components/Sidebar/Section'
import { MermaidDiagram } from '../components/Sidebar/MermaidDiagram'
import { NoteContext } from './NoteContext'
import { OrgRoamLink, OrgRoamNode } from '../api'

Expand Down Expand Up @@ -168,6 +169,40 @@ export const ProcessedOrg = (props: ProcessedOrgProps) => {
img: ({ src }) => {
return <OrgImage src={src as string} file={previewNode?.file} />
},
pre: ({ children, className }) => {
// Detect mermaid code blocks: uniorg emits <pre class="src src-mermaid">
// and remark emits <pre><code class="language-mermaid">
const classStr = String(className || '')

// Extract text content from React children recursively
const extractText = (node: any): string => {
if (!node) return ''
if (typeof node === 'string') return node
if (typeof node === 'number') return String(node)
if (Array.isArray(node)) return node.map(extractText).join('')
if (node?.props?.children) return extractText(node.props.children)
return ''
}

// Check for org-mode: <pre class="src src-mermaid">
if (classStr.includes('src-mermaid')) {
const code = extractText(children).trim()
if (code) return <MermaidDiagram code={code} />
}

// Check for markdown: <pre><code class="language-mermaid">
const childArray = children ? React.Children.toArray(children as ReactNode) : []
if (childArray.length === 1 && React.isValidElement(childArray[0])) {
const codeEl = childArray[0] as React.ReactElement<any>
const codeClass = String(codeEl.props?.className || '')
if (codeClass.includes('language-mermaid')) {
const code = extractText(codeEl).trim()
if (code) return <MermaidDiagram code={code} />
}
}

return <pre className={classStr || undefined}>{children as ReactNode}</pre>
},
section: ({ children, className }) => {
if (className && (className as string).slice(-1) === `${previewNode.level}`) {
return <Box>{(children as React.ReactElement[]).slice(1)}</Box>
Expand Down