|
2 | 2 | import {useEffect, useState} from 'react'; |
3 | 3 | import {useTheme} from 'next-themes'; |
4 | 4 |
|
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 | | - */ |
11 | 5 | export default function Mermaid() { |
12 | 6 | const [isDoneRendering, setDoneRendering] = useState(false); |
13 | 7 | const {resolvedTheme: theme} = useTheme(); |
| 8 | + |
14 | 9 | useEffect(() => { |
15 | 10 | 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 | 11 | const mermaidBlocks = |
29 | 12 | 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 |
| 13 | + if (mermaidBlocks.length === 0) return; |
| 14 | + |
| 15 | + const escapeHTML = (str: string) => |
| 16 | + str.replace( |
| 17 | + /[&<>"']/g, |
| 18 | + match => |
| 19 | + ({ |
| 20 | + '&': '&', |
| 21 | + '<': '<', |
| 22 | + '>': '>', |
| 23 | + '"': '"', |
| 24 | + "'": ''', |
| 25 | + })[match] || match |
| 26 | + ); |
| 27 | + |
34 | 28 | 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)); |
| 29 | + const svgPanZoom = (await import('svg-pan-zoom')).default; |
| 30 | + |
| 31 | + // Create light and dark versions |
| 32 | + mermaidBlocks.forEach(block => { |
| 33 | + const code = block.textContent ?? ''; |
| 34 | + block.innerHTML = escapeHTML(code); |
| 35 | + block.style.backgroundColor = 'transparent'; |
| 36 | + block.classList.add('light'); |
52 | 37 |
|
53 | | - const darkModeBlock = lightModeblock.cloneNode(true) as HTMLPreElement; |
54 | | - darkModeBlock.classList.add('dark'); |
55 | | - darkModeBlock.classList.remove('light'); |
56 | | - parentCodeTabs?.appendChild(darkModeBlock); |
| 38 | + const parentCodeTabs = block.closest('.code-tabs-wrapper'); |
| 39 | + const darkBlock = block.cloneNode(true) as HTMLDivElement; |
| 40 | + darkBlock.classList.replace('light', 'dark'); |
| 41 | + |
| 42 | + if (parentCodeTabs) { |
| 43 | + parentCodeTabs.innerHTML = ''; |
| 44 | + parentCodeTabs.append(block, darkBlock); |
| 45 | + } else { |
| 46 | + const wrapper = document.createElement('div'); |
| 47 | + wrapper.className = 'mermaid-theme-wrapper'; |
| 48 | + block.parentNode?.insertBefore(wrapper, block); |
| 49 | + wrapper.append(block, darkBlock); |
| 50 | + } |
57 | 51 | }); |
| 52 | + |
| 53 | + // Render both themes |
| 54 | + mermaid.initialize({startOnLoad: false, theme: 'default'}); |
58 | 55 | await mermaid.run({nodes: document.querySelectorAll('.language-mermaid.light')}); |
59 | 56 |
|
60 | 57 | mermaid.initialize({startOnLoad: false, theme: 'dark'}); |
61 | | - await mermaid |
62 | | - .run({nodes: document.querySelectorAll('.language-mermaid.dark')}) |
63 | | - .then(() => setDoneRendering(true)); |
| 58 | + await mermaid.run({nodes: document.querySelectorAll('.language-mermaid.dark')}); |
| 59 | + |
| 60 | + // Initialize pan/zoom for all SVGs (including hidden ones) |
| 61 | + document.querySelectorAll('.language-mermaid svg').forEach(svg => { |
| 62 | + const svgElement = svg as SVGSVGElement; |
| 63 | + const rect = svgElement.getBoundingClientRect(); |
| 64 | + |
| 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 | + }); |
| 81 | + |
| 82 | + setDoneRendering(true); |
64 | 83 | }; |
| 84 | + |
65 | 85 | renderMermaid(); |
66 | 86 | }, []); |
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 |
| 87 | + |
70 | 88 | 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 | | - } |
| 89 | + <style> |
| 90 | + {` |
| 91 | + .language-mermaid.light { display: ${theme === 'dark' ? 'none' : 'block'}; } |
| 92 | + .language-mermaid.dark { display: ${theme === 'dark' ? 'block' : 'none'}; } |
| 93 | + .dark .language-mermaid.light { display: none; } |
| 94 | + .dark .language-mermaid.dark { display: block; } |
91 | 95 | `} |
92 | | - </style> |
93 | | - ) |
| 96 | + </style> |
94 | 97 | ) : null; |
95 | 98 | } |
0 commit comments