22import { useEffect , useState } from 'react' ;
33import { useTheme } from 'next-themes' ;
44
5- /**
6- * we target ```mermaid``` code blocks after they have been highlighted (not ideal),
7- * then we strip the code from the html elements used for highlighting
8- * then we render the mermaid chart both in light and dark modes
9- * CSS takes care of showing the right one depending on the theme
10- */
115export default function Mermaid ( ) {
126 const [ isDoneRendering , setDoneRendering ] = useState ( false ) ;
137 const { resolvedTheme : theme } = useTheme ( ) ;
8+
149 useEffect ( ( ) => {
1510 const renderMermaid = async ( ) => {
16- const escapeHTML = ( str : string ) => {
17- return str . replace ( / [ & < > " ' ] / g, function ( match ) {
18- const escapeMap = {
19- '&' : '&' ,
20- '<' : '<' ,
21- '>' : '>' ,
22- '"' : '"' ,
23- "'" : ''' ,
24- } ;
25- return escapeMap [ match ] ;
26- } ) ;
27- } ;
28- const mermaidBlocks =
29- document . querySelectorAll < HTMLDivElement > ( '.language-mermaid' ) ;
30- if ( mermaidBlocks . length === 0 ) {
31- return ;
32- }
33- // we have to dig like this as the nomral import doesn't work
11+ const mermaidBlocks = document . querySelectorAll < HTMLDivElement > ( '.language-mermaid' ) ;
12+ if ( mermaidBlocks . length === 0 ) return ;
13+
14+ const escapeHTML = ( str : string ) =>
15+ str . replace ( / [ & < > " ' ] / g, ( match ) => ( {
16+ '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : '''
17+ } [ match ] || match ) ) ;
18+
3419 const { default : mermaid } = await import ( 'mermaid/dist/mermaid.esm.min.mjs' ) ;
35- mermaid . initialize ( { startOnLoad : false } ) ;
36- mermaidBlocks . forEach ( lightModeblock => {
37- // get rid of code highlighting
38- const code = lightModeblock . textContent ?? '' ;
39- lightModeblock . innerHTML = escapeHTML ( code ) ;
40- // force transparent background
41- lightModeblock . style . backgroundColor = 'transparent' ;
42- lightModeblock . classList . add ( 'light' ) ;
43- const parentCodeTabs = lightModeblock . closest ( '.code-tabs-wrapper' ) ;
44- if ( ! parentCodeTabs ) {
45- // eslint-disable-next-line no-console
46- console . error ( 'Mermaid code block was not wrapped in a code tab' ) ;
47- return ;
48- }
49- // empty the container
50- parentCodeTabs . innerHTML = '' ;
51- parentCodeTabs . appendChild ( lightModeblock . cloneNode ( true ) ) ;
20+
21+ // Create light and dark versions
22+ mermaidBlocks . forEach ( block => {
23+ const code = block . textContent ?? '' ;
24+ block . innerHTML = escapeHTML ( code ) ;
25+ block . style . backgroundColor = 'transparent' ;
26+ block . classList . add ( 'light' ) ;
27+
28+ const parentCodeTabs = block . closest ( '.code-tabs-wrapper' ) ;
29+ const darkBlock = block . cloneNode ( true ) as HTMLDivElement ;
30+ darkBlock . classList . replace ( 'light' , 'dark' ) ;
5231
53- const darkModeBlock = lightModeblock . cloneNode ( true ) as HTMLPreElement ;
54- darkModeBlock . classList . add ( 'dark' ) ;
55- darkModeBlock . classList . remove ( 'light' ) ;
56- parentCodeTabs ?. appendChild ( darkModeBlock ) ;
32+ if ( parentCodeTabs ) {
33+ parentCodeTabs . innerHTML = '' ;
34+ parentCodeTabs . append ( block , darkBlock ) ;
35+ } else {
36+ const wrapper = document . createElement ( 'div' ) ;
37+ wrapper . className = 'mermaid-theme-wrapper' ;
38+ block . parentNode ?. insertBefore ( wrapper , block ) ;
39+ wrapper . append ( block , darkBlock ) ;
40+ }
5741 } ) ;
58- await mermaid . run ( { nodes : document . querySelectorAll ( '.language-mermaid.light' ) } ) ;
5942
43+ // Render both themes
44+ mermaid . initialize ( { startOnLoad : false , theme : 'default' } ) ;
45+ await mermaid . run ( { nodes : document . querySelectorAll ( '.language-mermaid.light' ) } ) ;
46+
6047 mermaid . initialize ( { startOnLoad : false , theme : 'dark' } ) ;
61- await mermaid
62- . run ( { nodes : document . querySelectorAll ( '.language-mermaid.dark' ) } )
63- . then ( ( ) => setDoneRendering ( true ) ) ;
48+ await mermaid . run ( { nodes : document . querySelectorAll ( '.language-mermaid.dark' ) } ) ;
49+
50+ // Initialize pan/zoom
51+ await initializePanZoom ( ) ;
52+ setDoneRendering ( true ) ;
53+ } ;
54+
55+ const initializePanZoom = async ( ) => {
56+ const svgPanZoom = ( await import ( 'svg-pan-zoom' ) ) . default ;
57+
58+ document . querySelectorAll ( '.language-mermaid svg' ) . forEach ( svg => {
59+ const svgElement = svg as SVGSVGElement ;
60+ const container = svgElement . closest ( '.language-mermaid' ) as HTMLElement ;
61+ const isVisible = window . getComputedStyle ( container ) . display !== 'none' ;
62+
63+ if ( isVisible ) {
64+ const rect = svgElement . getBoundingClientRect ( ) ;
65+ if ( rect . width > 0 && rect . height > 0 ) {
66+ svgElement . setAttribute ( 'width' , rect . width . toString ( ) ) ;
67+ svgElement . setAttribute ( 'height' , rect . height . toString ( ) ) ;
68+ }
69+
70+ svgPanZoom ( svgElement , {
71+ zoomEnabled : true ,
72+ panEnabled : true ,
73+ controlIconsEnabled : true ,
74+ fit : true ,
75+ center : true ,
76+ minZoom : 0.1 ,
77+ maxZoom : 10 ,
78+ zoomScaleSensitivity : 0.2 ,
79+ } ) ;
80+ } else {
81+ svgElement . dataset . needsPanZoom = 'true' ;
82+ }
83+ } ) ;
6484 } ;
85+
6586 renderMermaid ( ) ;
6687 } , [ ] ) ;
67- // we have to wait for mermaid.js to finish rendering both light and dark charts
68- // before we hide one of them depending on the theme
69- // this is necessary because mermaid.js relies on the DOM for calculations
88+
89+ // Initialize pan/zoom for newly visible SVGs on theme change
90+ useEffect ( ( ) => {
91+ if ( ! isDoneRendering ) return ;
92+
93+ const initializeDelayedPanZoom = async ( ) => {
94+ const svgPanZoom = ( await import ( 'svg-pan-zoom' ) ) . default ;
95+
96+ document . querySelectorAll ( '.language-mermaid svg[data-needs-pan-zoom="true"]' )
97+ . forEach ( svg => {
98+ const svgElement = svg as SVGSVGElement ;
99+ const container = svgElement . closest ( '.language-mermaid' ) as HTMLElement ;
100+ const isVisible = window . getComputedStyle ( container ) . display !== 'none' ;
101+
102+ if ( isVisible ) {
103+ const rect = svgElement . getBoundingClientRect ( ) ;
104+ if ( rect . width > 0 && rect . height > 0 ) {
105+ svgElement . setAttribute ( 'width' , rect . width . toString ( ) ) ;
106+ svgElement . setAttribute ( 'height' , rect . height . toString ( ) ) ;
107+ }
108+
109+ svgPanZoom ( svgElement , {
110+ zoomEnabled : true ,
111+ panEnabled : true ,
112+ controlIconsEnabled : true ,
113+ fit : true ,
114+ center : true ,
115+ minZoom : 0.1 ,
116+ maxZoom : 10 ,
117+ zoomScaleSensitivity : 0.2 ,
118+ } ) ;
119+
120+ svgElement . removeAttribute ( 'data-needs-pan-zoom' ) ;
121+ }
122+ } ) ;
123+ } ;
124+
125+ setTimeout ( initializeDelayedPanZoom , 50 ) ;
126+ } , [ theme , isDoneRendering ] ) ;
127+
70128 return isDoneRendering ? (
71- theme === 'dark' ? (
72- < style >
73- { `
74- .dark .language-mermaid {
75- display: none;
76- }
77- .dark .language-mermaid.dark {
78- display: block;
79- }
80- ` }
81- </ style >
82- ) : (
83- < style >
84- { `
85- .language-mermaid.light {
86- display: block;
87- }
88- .language-mermaid.dark {
89- display: none;
90- }
129+ < style >
130+ { `
131+ .language-mermaid.light { display: ${ theme === 'dark' ? 'none' : 'block' } ; }
132+ .language-mermaid.dark { display: ${ theme === 'dark' ? 'block' : 'none' } ; }
133+ .dark .language-mermaid.light { display: none; }
134+ .dark .language-mermaid.dark { display: block; }
91135 ` }
92- </ style >
93- )
136+ </ style >
94137 ) : null ;
95- }
138+ }
0 commit comments