9
9
resolveSrcSet ,
10
10
transformBlocks ,
11
11
} from "@/app/element/markdown-util" ;
12
+ import remarkMermaidToTag from "@/app/element/remark-mermaid-to-tag" ;
12
13
import { boundNumber , useAtomValueSafe } from "@/util/util" ;
13
14
import clsx from "clsx" ;
14
15
import { Atom } from "jotai" ;
@@ -25,6 +26,18 @@ import { openLink } from "../store/global";
25
26
import { IconButton } from "./iconbutton" ;
26
27
import "./markdown.scss" ;
27
28
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
+
28
41
const Link = ( {
29
42
setFocusedHeading,
30
43
props,
@@ -55,7 +68,65 @@ const Heading = ({ props, hnum }: { props: React.HTMLAttributes<HTMLHeadingEleme
55
68
) ;
56
69
} ;
57
70
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
+ }
59
130
return < code className = { className } > { children } </ code > ;
60
131
} ;
61
132
@@ -256,7 +327,7 @@ const Markdown = ({
256
327
// Ensure uniqueness of ids between MD preview instances.
257
328
const [ idPrefix ] = useState < string > ( crypto . randomUUID ( ) ) ;
258
329
259
- text = textAtomValue ?? text ;
330
+ text = textAtomValue ?? text ?? "" ;
260
331
const transformedOutput = transformBlocks ( text ) ;
261
332
const transformedText = transformedOutput . content ;
262
333
const contentBlocksMap = transformedOutput . blocks ;
@@ -295,6 +366,21 @@ const Markdown = ({
295
366
) ,
296
367
} ;
297
368
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
+ } ;
298
384
299
385
const toc = useMemo ( ( ) => {
300
386
if ( showToc && tocRef . current . length > 0 ) {
@@ -335,12 +421,20 @@ const Markdown = ({
335
421
] ,
336
422
waveblock : [ [ "blockkey" ] ] ,
337
423
} ,
338
- tagNames : [ ...( defaultSchema . tagNames || [ ] ) , "span" , "waveblock" , "picture" , "source" ] ,
424
+ tagNames : [
425
+ ...( defaultSchema . tagNames || [ ] ) ,
426
+ "span" ,
427
+ "waveblock" ,
428
+ "picture" ,
429
+ "source" ,
430
+ "mermaidblock" ,
431
+ ] ,
339
432
} ) ,
340
433
( ) => rehypeSlug ( { prefix : idPrefix } ) ,
341
434
] ;
342
435
}
343
436
const remarkPlugins : any = [
437
+ remarkMermaidToTag ,
344
438
remarkGfm ,
345
439
[ RemarkFlexibleToc , { tocRef : tocRef . current } ] ,
346
440
[ createContentBlockPlugin , { blocks : contentBlocksMap } ] ,
0 commit comments