diff --git a/components/Sidebar/MermaidDiagram.tsx b/components/Sidebar/MermaidDiagram.tsx new file mode 100644 index 00000000..785ae005 --- /dev/null +++ b/components/Sidebar/MermaidDiagram.tsx @@ -0,0 +1,97 @@ +import React, { useEffect, useRef, useState } from 'react' +import { Box } from '@chakra-ui/react' + +let mermaidId = 0 +let mermaidLoaded: Promise | null = null + +function loadMermaid(): Promise { + 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('') + const [error, setError] = useState('') + 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 ( + + Mermaid error: {error} + + ) + } + + if (!svg) { + return ( + + Rendering diagram... + + ) + } + + return ( + + ) +} diff --git a/util/processOrg.tsx b/util/processOrg.tsx index 2abc256a..2520b129 100644 --- a/util/processOrg.tsx +++ b/util/processOrg.tsx @@ -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' @@ -168,6 +169,40 @@ export const ProcessedOrg = (props: ProcessedOrgProps) => { img: ({ src }) => { return }, + pre: ({ children, className }) => { + // Detect mermaid code blocks: uniorg emits
+              // and remark emits 

+              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: 
+              if (classStr.includes('src-mermaid')) {
+                const code = extractText(children).trim()
+                if (code) return 
+              }
+
+              // Check for markdown: 

+              const childArray = children ? React.Children.toArray(children as ReactNode) : []
+              if (childArray.length === 1 && React.isValidElement(childArray[0])) {
+                const codeEl = childArray[0] as React.ReactElement
+                const codeClass = String(codeEl.props?.className || '')
+                if (codeClass.includes('language-mermaid')) {
+                  const code = extractText(codeEl).trim()
+                  if (code) return 
+                }
+              }
+
+              return 
{children as ReactNode}
+ }, section: ({ children, className }) => { if (className && (className as string).slice(-1) === `${previewNode.level}`) { return {(children as React.ReactElement[]).slice(1)}