99 resolveSrcSet ,
1010 transformBlocks ,
1111} from "@/app/element/markdown-util" ;
12+ import remarkMermaidToTag from "@/app/element/remark-mermaid-to-tag" ;
1213import { boundNumber , useAtomValueSafe } from "@/util/util" ;
1314import clsx from "clsx" ;
1415import { Atom } from "jotai" ;
@@ -25,6 +26,18 @@ import { openLink } from "../store/global";
2526import { IconButton } from "./iconbutton" ;
2627import "./markdown.scss" ;
2728
29+ let mermaidInitialized = false ;
30+ let mermaidInstance : any = null ;
31+
32+ const initializeMermaid = async ( ) => {
33+ if ( ! mermaidInitialized ) {
34+ const mermaid = await import ( "mermaid" ) ;
35+ mermaidInstance = mermaid . default ;
36+ mermaidInstance . initialize ( { startOnLoad : false , theme : "dark" , securityLevel : "strict" } ) ;
37+ mermaidInitialized = true ;
38+ }
39+ } ;
40+
2841const Link = ( {
2942 setFocusedHeading,
3043 props,
@@ -55,7 +68,65 @@ const Heading = ({ props, hnum }: { props: React.HTMLAttributes<HTMLHeadingEleme
5568 ) ;
5669} ;
5770
58- const Code = ( { className, children } : { className : string ; children : React . ReactNode } ) => {
71+ const Mermaid = ( { chart } : { chart : string } ) => {
72+ const ref = useRef < HTMLDivElement > ( null ) ;
73+ const [ isLoading , setIsLoading ] = useState ( true ) ;
74+ const [ error , setError ] = useState < string | null > ( null ) ;
75+
76+ useEffect ( ( ) => {
77+ const renderMermaid = async ( ) => {
78+ try {
79+ setIsLoading ( true ) ;
80+ setError ( null ) ;
81+
82+ await initializeMermaid ( ) ;
83+ if ( ! ref . current || ! mermaidInstance ) {
84+ return ;
85+ }
86+
87+ // Normalize the chart text
88+ let normalizedChart = chart
89+ . replace ( / < b r \s * \/ ? > / gi, "\n" ) // Convert <br/> and <br> to newlines
90+ . replace ( / \r \n ? / g, "\n" ) // Normalize \r \r\n to \n
91+ . replace ( / \n + $ / , "" ) ; // Remove final newline
92+
93+ ref . current . removeAttribute ( "data-processed" ) ;
94+ ref . current . textContent = normalizedChart ;
95+ // console.log("mermaid", normalizedChart);
96+ await mermaidInstance . run ( { nodes : [ ref . current ] } ) ;
97+ setIsLoading ( false ) ;
98+ } catch ( err ) {
99+ console . error ( "Error rendering mermaid diagram:" , err ) ;
100+ setError ( `Failed to render diagram: ${ err . message || err } ` ) ;
101+ setIsLoading ( false ) ;
102+ }
103+ } ;
104+
105+ renderMermaid ( ) ;
106+ } , [ chart ] ) ;
107+
108+ useEffect ( ( ) => {
109+ if ( ! ref . current ) return ;
110+
111+ if ( error ) {
112+ ref . current . textContent = `Error: ${ error } ` ;
113+ ref . current . className = "mermaid error" ;
114+ } else if ( isLoading ) {
115+ ref . current . textContent = "Loading diagram..." ;
116+ ref . current . className = "mermaid" ;
117+ } else {
118+ ref . current . className = "mermaid" ;
119+ }
120+ } , [ isLoading , error ] ) ;
121+
122+ return < div className = "mermaid" ref = { ref } /> ;
123+ } ;
124+
125+ const Code = ( { className = "" , children } : { className ?: string ; children : React . ReactNode } ) => {
126+ if ( / \b l a n g u a g e - m e r m a i d \b / . test ( className ) ) {
127+ const text = Array . isArray ( children ) ? children . join ( "" ) : String ( children ?? "" ) ;
128+ return < Mermaid chart = { text } /> ;
129+ }
59130 return < code className = { className } > { children } </ code > ;
60131} ;
61132
@@ -256,7 +327,7 @@ const Markdown = ({
256327 // Ensure uniqueness of ids between MD preview instances.
257328 const [ idPrefix ] = useState < string > ( crypto . randomUUID ( ) ) ;
258329
259- text = textAtomValue ?? text ;
330+ text = textAtomValue ?? text ?? "" ;
260331 const transformedOutput = transformBlocks ( text ) ;
261332 const transformedText = transformedOutput . content ;
262333 const contentBlocksMap = transformedOutput . blocks ;
@@ -295,6 +366,21 @@ const Markdown = ({
295366 ) ,
296367 } ;
297368 markdownComponents [ "waveblock" ] = ( props : any ) => < WaveBlock { ...props } blockmap = { contentBlocksMap } /> ;
369+ markdownComponents [ "mermaidblock" ] = ( props : any ) => {
370+ const getTextContent = ( children : any ) : string => {
371+ if ( typeof children === "string" ) {
372+ return children ;
373+ } else if ( Array . isArray ( children ) ) {
374+ return children . map ( getTextContent ) . join ( "" ) ;
375+ } else if ( children && typeof children === "object" && children . props && children . props . children ) {
376+ return getTextContent ( children . props . children ) ;
377+ }
378+ return String ( children || "" ) ;
379+ } ;
380+
381+ const chartText = getTextContent ( props . children ) ;
382+ return < Mermaid chart = { chartText } /> ;
383+ } ;
298384
299385 const toc = useMemo ( ( ) => {
300386 if ( showToc && tocRef . current . length > 0 ) {
@@ -335,12 +421,20 @@ const Markdown = ({
335421 ] ,
336422 waveblock : [ [ "blockkey" ] ] ,
337423 } ,
338- tagNames : [ ...( defaultSchema . tagNames || [ ] ) , "span" , "waveblock" , "picture" , "source" ] ,
424+ tagNames : [
425+ ...( defaultSchema . tagNames || [ ] ) ,
426+ "span" ,
427+ "waveblock" ,
428+ "picture" ,
429+ "source" ,
430+ "mermaidblock" ,
431+ ] ,
339432 } ) ,
340433 ( ) => rehypeSlug ( { prefix : idPrefix } ) ,
341434 ] ;
342435 }
343436 const remarkPlugins : any = [
437+ remarkMermaidToTag ,
344438 remarkGfm ,
345439 [ RemarkFlexibleToc , { tocRef : tocRef . current } ] ,
346440 [ createContentBlockPlugin , { blocks : contentBlocksMap } ] ,
0 commit comments