Mermaid Highlight ━━━━━━━━━━━━━━━━━ Mermaid gives us a way to style nodes through the use of css, but rather than using normal css selectors we need to use . This also applies to subgraphs, and... Date: March 7, 2022 Mermaid gives us a way to style nodes through the use of css, but rather than using normal css selectors we need to use style . This also applies to subgraphs, and we can use the name of the subgraph in place of the nodeid. ``` graph TD; a --> A A --> B B --> C style A fill:#f9f,stroke:#333,stroke-width:4px style B fill:#f9f,stroke:#333,stroke-width:4px subgraph one a end style one fill:#BADA55 ``` produces the following graph graph TD; a --> A A --> B B --> C style A fill:#f9f,stroke:#333,stroke-width:4px style B fill:#f9f,stroke:#333,stroke-width:4px subgraph one a end style one fill:#BADA55 import mermaid from '/assets/vendor/mermaid/mermaid.esm.min.mjs'; const rootStyle = getComputedStyle(document.documentElement); const css = (name, fallback) => (rootStyle.getPropertyValue(name) || fallback).trim(); const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches || document.documentElement.dataset.theme === 'dark'; const accent = css('--color-primary', '#ffcd11'); const flowchart = { nodeSpacing: 60, rankSpacing: 90, padding: 12, }; const themeCSS = ` .label foreignObject > div { padding: 14px 14px 10px; line-height: 1.2; } .nodeLabel { padding: 14px 14px 10px; line-height: 1.2; } * { cursor: pointer; } `; const themeVariables = { background: css('--color-background', '#ffffff'), primaryColor: css('--color-code-bg', '#0a0a0a'), primaryTextColor: css('--color-text', '#1f2937'), primaryBorderColor: accent, lineColor: accent, textColor: css('--color-text', '#1f2937'), nodeBkg: css('--color-code-bg', '#0a0a0a'), nodeBorder: accent, nodeTextColor: css('--color-text', '#1f2937'), fontSize: '16px', nodePadding: 20, nodeTextMargin: 14, clusterBkg: isDark ? css('--color-background', '#0f0f0f') : css('--color-surface', '#f9fafb'), clusterBorder: accent, clusterTextColor: css('--color-text', '#1f2937'), titleColor: css('--color-text', '#1f2937'), edgeLabelBackground: css('--color-code-bg', '#0a0a0a'), }; const SVG_PAN_ZOOM_CDN = 'https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.2/dist/svg-pan-zoom.min.js'; let mermaidLightbox = null; let activePanZoom = null; // Inject lightbox styles once const injectLightboxStyles = () => { if (document.getElementById('mermaid-lightbox-css')) return; const style = document.createElement('style'); style.id = 'mermaid-lightbox-css'; style.textContent = ` /* Container fills the GLightbox slide */ .mermaid-lightbox-wrap { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: transparent; position: relative; } .mermaid-lightbox-wrap svg { width: 100% !important; height: 100% !important; max-width: 100%; max-height: 100%; } /* Hide GLightbox prev/next arrows (single-slide lightbox) */ .glightbox-container .gprev, .glightbox-container .gnext { display: none !important; } /* Hide description area that renders as a white box */ .glightbox-container .gslide-description, .glightbox-container .gslide-title, .glightbox-container .gdesc-inner, .glightbox-container .gslide-desc { display: none !important; } /* Remove white background from inline slide content */ .glightbox-container .gslide-inline { background: transparent !important; } /* Make the inline content area fill the slide */ .glightbox-container .ginlined-content { max-width: none !important; max-height: none !important; width: 100%; height: 100%; padding: 0 !important; } /* Remove box-shadow from the media container */ .glightbox-container .gslide-media { box-shadow: none !important; } /* Toolbar styling */ .mermaid-lightbox-toolbar { position: absolute; top: 8px; right: 8px; z-index: 10; display: flex; gap: 4px; } .mermaid-pz-btn { background: rgba(0,0,0,0.6); color: #fff; border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; padding: 4px 10px; cursor: pointer; font-size: 14px; line-height: 1; } .mermaid-pz-btn:hover { background: rgba(0,0,0,0.8); border-color: rgba(255,255,255,0.6); } `; document.head.appendChild(style); }; // Lazy-load svg-pan-zoom from CDN, returns a promise const loadSvgPanZoom = () => { if (typeof svgPanZoom !== 'undefined') return Promise.resolve(); return new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = SVG_PAN_ZOOM_CDN; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); }; // Initialize svg-pan-zoom on the SVG inside the lightbox. // Retries until the lightbox container has settled dimensions. let _pzRetries = 0; const initPanZoom = () => { if (activePanZoom) return; const container = document.querySelector('.glightbox-container .gslide.current .mermaid-lightbox-wrap'); if (!container) return; const svgEl = container.querySelector('svg'); if (!svgEl) return; // Ensure the container has layout dimensions before initializing. const cRect = container.getBoundingClientRect(); if (cRect.width < 10 || cRect.height < 10) { if (_pzRetries < 20) { _pzRetries++; setTimeout(initPanZoom, 50); } return; } // svg-pan-zoom needs a viewBox. Pre-rendered SVGs from mermaid // usually have one; browser-rendered ones may not. if (!svgEl.getAttribute('viewBox')) { let w = parseFloat(svgEl.getAttribute('width')); let h = parseFloat(svgEl.getAttribute('height')); if (!w && svgEl.style.maxWidth) w = parseFloat(svgEl.style.maxWidth); if (!w || !h) { const r = svgEl.getBoundingClientRect(); if (!w) w = r.width; if (!h) h = r.height; } if (w > 0 && h > 0) { svgEl.setAttribute('viewBox', '0 0 ' + w + ' ' + h); } else if (_pzRetries < 20) { _pzRetries++; setTimeout(initPanZoom, 50); return; } } _pzRetries = 0; // Clear inline dimensions so SVG can be sized by the container // and svg-pan-zoom can manage transforms. svgEl.removeAttribute('width'); svgEl.removeAttribute('height'); svgEl.style.cssText = 'width:100%;height:100%;'; try { activePanZoom = svgPanZoom(svgEl, { zoomEnabled: true, panEnabled: true, controlIconsEnabled: false, fit: true, center: true, contain: false, minZoom: 0.3, maxZoom: 10, zoomScaleSensitivity: 0.3, mouseWheelZoomEnabled: true, preventMouseEventsDefault: true, }); // Double-check fit after a frame in case dimensions shifted requestAnimationFrame(() => { if (!activePanZoom) return; activePanZoom.resize(); activePanZoom.fit(); activePanZoom.center(); }); } catch (_) { activePanZoom = null; } // Add reset/fit buttons let toolbar = container.querySelector('.mermaid-lightbox-toolbar'); if (!toolbar) { toolbar = document.createElement('div'); toolbar.className = 'mermaid-lightbox-toolbar'; toolbar.innerHTML = '' + '' + ''; toolbar.addEventListener('click', (ev) => { const btn = ev.target.closest('[data-action]'); if (!btn || !activePanZoom) return; ev.preventDefault(); ev.stopPropagation(); const action = btn.dataset.action; if (action === 'fit') { activePanZoom.resize(); activePanZoom.fit(); activePanZoom.center(); } else if (action === 'zoomin') { activePanZoom.zoomIn(); } else if (action === 'zoomout') { activePanZoom.zoomOut(); } }); container.prepend(toolbar); } }; // Destroy pan-zoom on lightbox close const destroyPanZoom = () => { if (activePanZoom) { try { activePanZoom.destroy(); } catch (_) { /* no-op */ } activePanZoom = null; } }; let _lbRetries = 0; const ensureMermaidLightbox = () => { const diagrams = document.querySelectorAll('.mermaid svg'); if (!diagrams.length) { // Mermaid ESM may still be rendering -- retry up to 2s if (_lbRetries < 20) { _lbRetries++; setTimeout(ensureMermaidLightbox, 100); } return; } _lbRetries = 0; injectLightboxStyles(); diagrams.forEach((svg) => { if (svg.dataset.lightboxBound) return; svg.dataset.lightboxBound = 'true'; svg.style.cursor = 'pointer'; svg.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const svgHtml = svg.outerHTML; const openLightbox = () => { if (!mermaidLightbox) { mermaidLightbox = GLightbox({ selector: false, openEffect: 'fade', closeEffect: 'fade', zoomable: false, draggable: false, skin: 'clean', }); mermaidLightbox.on('slide_after_load', () => { destroyPanZoom(); _pzRetries = 0; loadSvgPanZoom().then(() => initPanZoom()); }); mermaidLightbox.on('close', destroyPanZoom); } mermaidLightbox.setElements([{ content: '
' + svgHtml + '
', width: '90vw', height: '90vh' }]); mermaidLightbox.open(); loadSvgPanZoom(); }; if (typeof GLightbox !== 'undefined') { openLightbox(); } else if (window.initGLightbox) { window.initGLightbox(); openLightbox(); } else { window.addEventListener('glightbox-ready', () => { openLightbox(); }, { once: true }); } }); }); }; mermaid.initialize({ startOnLoad: false, theme: 'base', themeVariables, flowchart, themeCSS }); window.initMermaid = async () => { try { await mermaid.run(); } catch (e) { console.error('mermaid.run failed:', e); } ensureMermaidLightbox(); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => window.initMermaid()); } else { window.initMermaid(); }