diff --git a/src/components/mermaid.tsx b/src/components/mermaid.tsx index dfaabe8599129..174d44fef8b1e 100644 --- a/src/components/mermaid.tsx +++ b/src/components/mermaid.tsx @@ -1,9 +1,16 @@ 'use client'; -import {useEffect} from 'react'; +import {useEffect, useState} from 'react'; import {useTheme} from 'next-themes'; +/** + * we target ```mermaid``` code blocks after they have been highlighted (not ideal), + * then we strip the code from the html elements used for highlighting + * then we render the mermaid chart both in light and dark modes + * CSS takes care of showing the right one depending on the theme + */ export default function Mermaid() { - const theme = useTheme(); + const [isDoneRendering, setDoneRendering] = useState(false); + const {resolvedTheme: theme} = useTheme(); useEffect(() => { const renderMermaid = async () => { const escapeHTML = (str: string) => { @@ -24,24 +31,64 @@ export default function Mermaid() { return; } const {default: mermaid} = await import('mermaid'); - mermaid.initialize({ - startOnLoad: false, - theme: theme.resolvedTheme === 'light' ? 'default' : 'dark', - }); - mermaidBlocks.forEach(block => { + mermaid.initialize({startOnLoad: false}); + mermaidBlocks.forEach(lightModeblock => { // get rid of code highlighting - const code = block.textContent ?? ''; - block.innerHTML = escapeHTML(code); + const code = lightModeblock.textContent ?? ''; + lightModeblock.innerHTML = escapeHTML(code); // force transparent background - block.style.backgroundColor = 'transparent'; - const parentCodeTabs = block.closest('.code-tabs-wrapper'); - if (parentCodeTabs) { - parentCodeTabs.innerHTML = block.outerHTML; + lightModeblock.style.backgroundColor = 'transparent'; + lightModeblock.classList.add('light'); + const parentCodeTabs = lightModeblock.closest('.code-tabs-wrapper'); + if (!parentCodeTabs) { + // eslint-disable-next-line no-console + console.error('Mermaid code block was not wrapped in a code tab'); + return; } + // empty the container + parentCodeTabs.innerHTML = ''; + parentCodeTabs.appendChild(lightModeblock.cloneNode(true)); + + const darkModeBlock = lightModeblock.cloneNode(true) as HTMLPreElement; + darkModeBlock.classList.add('dark'); + darkModeBlock.classList.remove('light'); + parentCodeTabs?.appendChild(darkModeBlock); }); - await mermaid.run({nodes: document.querySelectorAll('.language-mermaid')}); + await mermaid.run({nodes: document.querySelectorAll('.language-mermaid.light')}); + + mermaid.initialize({startOnLoad: false, theme: 'dark'}); + await mermaid + .run({nodes: document.querySelectorAll('.language-mermaid.dark')}) + .then(() => setDoneRendering(true)); }; renderMermaid(); - }, [theme]); - return null; + }, []); + // we have to wait for mermaid.js to finish rendering both light and dark charts + // before we hide one of them depending on the theme + // this is necessary because mermaid.js relies on the DOM for calculations + return isDoneRendering ? ( + theme === 'dark' ? ( + + ) : ( + + ) + ) : null; }