OPMERKING: Dit is onderdeel van mijn experimenten met AI / een manier om $1000 Calude Code Web credits uit te geven. Ik heb dit een BUNCH van papieren, mijn begrip, vragen die ik moest genereren van dit artikel. Het is leuk en vult een gat dat ik niet ergens anders heb gezien gevuld.
Dit artikel bouwt voort op eerdere artikelen: Als je dat nog niet gedaan hebt, kijk dan. Meermin.js toevoegen met htmx, Thema's voor Mermaid wisselen, en Meerminnendiagrammen verbeteren met Pan/Zoom en Export. Deze diepe duik verklaart de binnenkant achter die implementaties.
Mermaid.js is echt briljant. Schrijf eenvoudige tekst, krijg mooie diagrammen. Geen gerommel meer met Visio of draw.io, verlies bronbestanden, of behoud van aparte beeldbestanden. Alles leeft in markdown, versiegestuurd naast uw code.
Maar ik wilde het weten. hoe Het werkt eigenlijk onder de motorkap.
graph LR
A[Text] --> B[Magic?]
B --> C[Beautiful Diagram]
...worden een echte SVG? En nog belangrijker, hoe kun je haak in het toe te voegen functies zoals de pan/zoom, thema switching, en export functionaliteit die ik voor deze site (nu beschikbaar als @mostlylucid/mermaid-enhancements)?
Na veel graven door Mermaid's broncode en het bouwen van echte extensies, hier is alles wat ik heb geleerd over hoe Mermaid intern werkt en hoe om het goed uit te breiden.
Mermaid verandert tekstdefinities in diagrammen. Denk aan "Markdown for diagrammen."
De oude manier:
De Zeemeermin manier:
Updaten? Gewoon de tekst bewerken. Versie controle? Het is gewoon tekst! Werkt in markdown? Yep!
Mermaid ondersteunt een belachelijk aantal diagram types:
graph LR
A[Flowcharts] --> B[Sequence Diagrams]
B --> C[Class Diagrams]
C --> D[State Diagrams]
D --> E[ER Diagrams]
E --> F[Gantt Charts]
F --> G[Pie Charts]
G --> H[Git Graphs]
H --> I[User Journeys]
I --> J[And many more...]
Zie de Zeemeermin docs voor de volledige lijst.
Dit is wat er gebeurt als Mermaid een diagram geeft:
graph TD
A[Text Definition] --> B[Lexer/Tokenizer]
B --> C[Parser]
C --> D[AST Built]
D --> E[Diagram Type Detected]
E --> F[Type-Specific Renderer]
F --> G[SVG Generated]
G --> H[Inserted into DOM]
H --> I[Your Enhancements Run]
style A stroke:#059669,stroke-width:3px,color:#10b981
style D stroke:#2563eb,stroke-width:3px,color:#3b82f6
style G stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
style I stroke:#d97706,stroke-width:3px,color:#f59e0b
Laten we elke stap afbreken.
Alles begint met tekst. Je schrijft diagrammen in Mermaid's DSL (domeinspecifieke taal):
// Flowchart
const diagram = `
graph TD
A[Start] --> B{Is it working?}
B -->|Yes| C[Great!]
B -->|No| D[Debug time]
`;
De lexer breekt tekst in tokens. Bijvoorbeeld deze regel:
A[Start] --> B{Decision}
Wordt tokens als:
[
{ type: 'NODE_ID', value: 'A' },
{ type: 'NODE_TEXT', value: 'Start' },
{ type: 'ARROW', value: '-->' },
{ type: 'NODE_ID', value: 'B' },
{ type: 'NODE_TEXT', value: 'Decision' },
{ type: 'NODE_SHAPE', value: 'diamond' } // from { }
]
De parser verbruikt tokens en bouwt een Abstract Syntax Tree (AST):
// Simplified AST structure
{
type: 'flowchart',
direction: 'TD',
nodes: [
{ id: 'A', text: 'Start', shape: 'rect' },
{ id: 'B', text: 'Decision', shape: 'diamond' }
],
edges: [
{ from: 'A', to: 'B', type: 'arrow' }
]
}
Mermaid gebruikt verschillende parsers voor elk diagram type. Deze worden vaak gegenereerd uit grammatica bestanden met behulp van Jison (zoals Yacc/Bison voor JavaScript).
Mermaid detecteert diagram type vanaf de eerste regel:
// Simplified detection logic
if (text.match(/^\s*graph/)) return 'flowchart';
if (text.match(/^\s*sequenceDiagram/)) return 'sequence';
if (text.match(/^\s*classDiagram/)) return 'class';
// ... etc
Elk diagramtype heeft zijn eigen renderer. De renderer neemt de AST en genereert SVG elementen.
Voor stroomschema's gebruikt Mermaid de Dagre bibliotheek voor grafische indeling. Voor anderen maakt het gebruik van aangepaste algoritmen of bibliotheken zoals Cytoscape.
// Simplified flowchart renderer
export const draw = function (text, id, version, diagObj) {
const graph = diagObj.db; // The AST
const svg = d3.select(`#${id}`);
// Render nodes
graph.getVertices().forEach(vertex => {
drawNode(svg, vertex);
});
// Render edges
graph.getEdges().forEach(edge => {
drawEdge(svg, edge);
});
// Apply layout algorithm
dagre.layout(graph);
};
De renderer produceert SVG markup:
<svg xmlns="http://www.w3.org/2000/svg">
<g class="node">
<rect x="0" y="0" width="100" height="50"/>
<text x="50" y="25">Start</text>
</g>
<g class="edge">
<path d="M 100 25 L 200 25" stroke="#333"/>
</g>
</svg>
Zeemeermin vindt alles .mermaid elementen en vervangt ze door weergegeven SVG:
// From mermaid.ts
export const init = async function (config, nodes) {
const nodesToProcess = nodes || document.querySelectorAll('.mermaid');
for (const node of nodesToProcess) {
const id = `mermaid-${Date.now()}-${Math.random()}`;
const txt = node.textContent;
const { svg } = await render(id, txt);
node.innerHTML = svg;
}
};
Nadat Mermaid de SVG invoegt, kunt u het verbeteren. Dit is waar al mijn verbeteringen haak in:
Meer hierover hieronder.
Nu we weten hoe Mermaid werkt, laten we onderzoeken hoe het uit te breiden.
De meest elementaire uitbreiding is configuratie:
import mermaid from 'mermaid';
mermaid.initialize({
startOnLoad: true,
theme: 'dark',
securityLevel: 'loose',
flowchart: {
curve: 'basis',
padding: 15
}
});
Ik heb dit uitgebreid behandeld in Thema's voor Mermaid wisselen, maar hier is de belangrijkste implementatie:
Mermaid moet worden geïnitialiseerd met een thema, en je kunt het niet veranderen na. MAAR als je wilt her-render diagrammen met een nieuw thema, moet je de originele diagram bron ..die Mermaid slaat niet op in de DOM.
Bewaar de oorspronkelijke inhoud voor het renderen, herstel en re-render bij het schakelen van thema's:
// From my theme-switcher implementation
const originalData = new Map();
// Save original content before first render
const saveOriginalData = async () => {
const elements = document.querySelectorAll('.mermaid');
elements.forEach(element => {
const id = element.id || `mermaid-${Date.now()}`;
element.id = id;
// Store the original diagram source
if (!originalData.has(id)) {
originalData.set(id, element.textContent?.trim());
}
});
};
// When theme changes, restore and re-render
const loadMermaid = async (theme) => {
mermaid.initialize({
startOnLoad: false,
theme: theme
});
const elements = document.querySelectorAll('.mermaid');
for (const element of elements) {
const source = originalData.get(element.id);
if (source) {
element.innerHTML = ''; // Clear
element.removeAttribute('data-processed');
const { svg } = await mermaid.render(
`mermaid-svg-${element.id}`,
source
);
element.innerHTML = svg;
}
}
};
Meerdere detectiemethoden voor thema's (sites behandelen thema's anders):
function detectTheme() {
// Check various sources
if (typeof window.__themeState !== 'undefined') {
return window.__themeState;
}
if (localStorage.theme) {
return localStorage.theme;
}
if (document.documentElement.classList.contains('dark')) {
return 'dark';
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
Zie de volledige thema switcher code voor details.
Dit is waar de echte magie gebeurt. Na Mermaid renders, kunt u interactieve functies toevoegen.
Ik heb dit uitgebreid behandeld in Meerminnendiagrammen verbeteren met Pan/Zoom en Export, dus ik zal de belangrijkste technieken hier markeren.
Maak een wikkelcontainer aan voor bedieningselementen:
function wrapDiagram(element) {
if (element.closest('.mermaid-wrapper')) {
return element.closest('.mermaid-wrapper');
}
const wrapper = document.createElement('div');
wrapper.className = 'mermaid-wrapper';
wrapper.id = `wrapper-${element.id}`;
element.parentNode.insertBefore(wrapper, element);
wrapper.appendChild(element);
return wrapper;
}
Gebruik svg-pan-zoom:
import svgPanZoom from 'svg-pan-zoom';
const panZoomInstances = new Map();
function initPanZoom(svgElement, diagramId) {
// Clean up existing instance
if (panZoomInstances.has(diagramId)) {
panZoomInstances.get(diagramId).destroy();
panZoomInstances.delete(diagramId);
}
const instance = svgPanZoom(svgElement, {
zoomEnabled: true,
controlIconsEnabled: false,
fit: true,
center: true,
minZoom: 0.1,
maxZoom: 10
});
panZoomInstances.set(diagramId, instance);
return instance;
}
Zwevend bedieningspaneel aanmaken:
function createControlButtons(container, diagramId) {
const controlsDiv = document.createElement('div');
controlsDiv.className = 'mermaid-controls';
const buttons = [
{ icon: 'bx-fullscreen', title: 'Fullscreen', action: 'fullscreen' },
{ icon: 'bx-zoom-in', title: 'Zoom In', action: 'zoomIn' },
{ icon: 'bx-zoom-out', title: 'Zoom Out', action: 'zoomOut' },
{ icon: 'bx-reset', title: 'Reset', action: 'reset' },
{ icon: 'bx-move', title: 'Pan', action: 'pan' },
{ icon: 'bx-image', title: 'Export PNG', action: 'exportPng' },
{ icon: 'bx-code-alt', title: 'Export SVG', action: 'exportSvg' }
];
buttons.forEach(btn => {
const button = document.createElement('button');
button.className = `mermaid-control-btn bx ${btn.icon}`;
button.setAttribute('data-action', btn.action);
button.setAttribute('data-diagram-id', diagramId);
controlsDiv.appendChild(button);
});
container.appendChild(controlsDiv);
}
Verbind geen luisteraars aan elke knop.
document.addEventListener('click', (e) => {
const target = e.target;
if (!target.classList.contains('mermaid-control-btn')) return;
const action = target.getAttribute('data-action');
const diagramId = target.getAttribute('data-diagram-id');
const panZoom = panZoomInstances.get(diagramId);
switch (action) {
case 'zoomIn': panZoom?.zoomIn(); break;
case 'zoomOut': panZoom?.zoomOut(); break;
case 'reset': panZoom?.reset(); break;
// ... etc
}
});
De uitdaging: SVG elementen hebben dynamische grootte, pan/zoom transformeert, en geërfde stijlen. Om goed te exporteren, moet je:
Gebruik html-to-image:
import { toPng, toSvg } from 'html-to-image';
async function exportDiagram(container, format, diagramId) {
const svgElement = container.querySelector('svg');
if (!svgElement) return;
// Clone to avoid modifying original
const clonedSvg = svgElement.cloneNode(true);
// Get or calculate viewBox
let viewBox = clonedSvg.getAttribute('viewBox');
if (!viewBox) {
const bbox = svgElement.getBBox();
viewBox = `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`;
clonedSvg.setAttribute('viewBox', viewBox);
}
// Set explicit dimensions
const [, , width, height] = viewBox.split(' ').map(Number);
clonedSvg.setAttribute('width', width);
clonedSvg.setAttribute('height', height);
// Remove pan-zoom transforms
clonedSvg.removeAttribute('style');
// Create off-screen container
const temp = document.createElement('div');
temp.style.position = 'absolute';
temp.style.left = '-9999px';
temp.appendChild(clonedSvg);
document.body.appendChild(temp);
// Export
const dataUrl = format === 'png'
? await toPng(clonedSvg, { pixelRatio: 2 })
: await toSvg(clonedSvg);
downloadFile(dataUrl, `diagram-${Date.now()}.${format}`);
// Cleanup
document.body.removeChild(temp);
}
Kritische details:
Zie de volledige uitvoercode voor meer details.
Ik verpakte al deze verbeteringen als @mostlylucid/mermaid-enhancements. Zie Publishing Mermaid Enhancements as an npm Package voor alle details.
Dit is hoe het allemaal bij elkaar past:
graph TB
A[User Initializes] --> B[init Function]
B --> C[initMermaid]
B --> D[enhanceMermaidDiagrams]
C --> E[Theme Detection]
C --> F[Event Listeners]
C --> G[Mermaid Rendering]
E --> E1[Global State]
E --> E2[LocalStorage]
E --> E3[DOM Class]
E --> E4[OS Preference]
F --> F1[Custom Events]
F --> F2[Media Query]
G --> H[Apply Enhancements]
D --> H
H --> I[Wrap Diagrams]
H --> J[Init Pan/Zoom]
H --> K[Add Controls]
I --> L[Interactive Diagram]
J --> L
K --> L
L --> M[User Interactions]
M --> M1[Zoom In/Out]
M --> M2[Pan]
M --> M3[Fullscreen]
M --> M4[Export PNG/SVG]
style A stroke:#059669,stroke-width:3px,color:#10b981
style L stroke:#2563eb,stroke-width:3px,color:#3b82f6
style M stroke:#7c3aed,stroke-width:3px,color:#8b5cf6
npm install @mostlylucid/mermaid-enhancements
import { init } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';
await init();
Je zeemeermin diagrammen hebben nu:
Als ik bedekt in Meermin.js toevoegen met htmx, moet je Mermaid opnieuw initialiseren na HTMX content swaps:
// On page load
document.addEventListener('DOMContentLoaded', function () {
mermaid.initialize({ startOnLoad: true });
});
// After HTMX swaps content
document.body.addEventListener('htmx:afterSwap', function(evt) {
mermaid.run();
});
Met het uitbreidingspakket:
import { init, enhanceMermaidDiagrams } from '@mostlylucid/mermaid-enhancements';
// Initial load
await init();
// After HTMX swap
document.body.addEventListener('htmx:afterSwap', async function() {
await init(); // Re-init Mermaid with current theme
enhanceMermaidDiagrams(); // Re-apply enhancements
});
Na het bouwen van dit spul en het debuggen van rare rand gevallen, hier is wat werkt:
Mermaid bewaart de originele diagrambron niet na het renderen. U MOET het zelf opslaan:
const originalData = new Map();
// Before first render
element.setAttribute('data-original-code', element.textContent);
originalData.set(element.id, element.textContent);
// When re-rendering
element.innerHTML = originalData.get(element.id);
Geheugenlekken zijn echt. Vernietig instanties voordat er nieuwe worden gemaakt:
if (panZoomInstances.has(id)) {
try {
panZoomInstances.get(id).destroy();
} catch (e) {
console.warn('Failed to destroy:', e);
}
panZoomInstances.delete(id);
}
Koppel geen luisteraars aan individuele knoppen:
// ❌ Don't do this
buttons.forEach(btn => {
btn.addEventListener('click', handler);
});
// ✅ Do this
document.addEventListener('click', (e) => {
if (e.target.matches('.mermaid-control-btn')) {
handleClick(e.target);
}
});
Raketlader vertraagt JavaScript-uitvoering. Wacht op afhankelijkheden:
function waitForDependencies(maxAttempts = 50) {
return new Promise((resolve) => {
let attempts = 0;
const check = () => {
if (window.mermaid && window.htmx && window.Alpine) {
resolve();
} else if (attempts >= maxAttempts) {
resolve(); // Give up
} else {
attempts++;
setTimeout(check, Math.min(50 * Math.pow(1.2, attempts), 500));
}
};
check();
});
}
En sluit je hoofdscript uit van Rocket Loader:
<script src="main.js" data-cfasync="false"></script>
Gebruik requestAnimationFrame voor een betere timing dan willekeurig setTimeout:
// After Mermaid renders
await mermaid.run();
// Wait for paint before enhancing
await new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
enhanceMermaidDiagrams();
resolve();
});
});
});
SVG's kunnen raar zijn. Controleer altijd:
const svgElement = container.querySelector('svg');
if (!svgElement) {
console.warn('No SVG found');
return;
}
// Clone before modifying
const cloned = svgElement.cloneNode(true);
// Ensure viewBox exists
let viewBox = cloned.getAttribute('viewBox');
if (!viewBox) {
const bbox = svgElement.getBBox();
viewBox = `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`;
}
Met de juiste initialisatie, moet je zien:
Saving original data
Loading mermaid with theme: dark
Mermaid initialized
Enhanced 3 diagrams
Na de implementatie van verbeteringen:
Diagram niet weergeven:
window.mermaid)Pan/zoom werkt niet:
pointer-events: none)Export vangt alleen hoek:
Thema niet schakelen:
data-processed niet opnieuw ingesteldLaad geen verbeteringen totdat nodig:
let enhancementsLoaded = false;
async function loadEnhancements() {
if (enhancementsLoaded) return;
const { enhanceMermaidDiagrams } = await import('./enhancements.js');
enhanceMermaidDiagrams();
enhancementsLoaded = true;
}
// Load on first interaction
document.addEventListener('click', (e) => {
if (e.target.closest('.mermaid')) {
loadEnhancements();
}
}, { once: true });
Schema's alleen renderen als ze zichtbaar zijn:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
renderDiagram(entry.target);
observer.unobserve(entry.target);
}
});
}, { rootMargin: '100px' });
document.querySelectorAll('.mermaid').forEach(el => {
observer.observe(el);
});
Bij verandering van grootte of thema:
let timeout;
window.addEventListener('resize', () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
panZoomInstances.forEach(instance => {
instance.resize();
instance.fit();
});
}, 250);
});
Mermaid.js is fantastisch uit de doos, maar begrijpen hoe het intern werkt laat je een aantal echt coole verbeteringen bouwen. De belangrijkste inzichten:
requestAnimationFrameAlle technieken die ik hier heb behandeld worden gebruikt in de productie op deze site en verpakt in @mostlylucid/mermaid-enhancements. De volledige bron is beschikbaar op meestal lucidweb/meestal lucid-meermin.
Probeer de knoppen op de diagrammen hierboven!
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.