Back to "Mermaid.js Deep Dive: hoe het eigenlijk werkt en hoe het uit te breiden"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

Diagrams JavaScript Mermaid SVG

Mermaid.js Deep Dive: hoe het eigenlijk werkt en hoe het uit te breiden

Sunday, 09 November 2025

Inleiding

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.

Wat is Mermaid.js?

Mermaid verandert tekstdefinities in diagrammen. Denk aan "Markdown for diagrammen."

De oude manier:

  1. Open diagrammen tool
  2. diagram aanmaken
  3. Exporteren als PNG
  4. Invoegen in documenten
  5. Moeten updaten? Het bronbestand zoeken, bewerken, opnieuw exporteren, afbeelding vervangen...

De Zeemeermin manier:

  1. diagram in tekst schrijven
  2. Klaar

Updaten? Gewoon de tekst bewerken. Versie controle? Het is gewoon tekst! Werkt in markdown? Yep!

Diagramtypes

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.

Hoe Mermaid werkt eigenlijk: De Pipeline

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.

Stap 1: Tekstdefinitie

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]
`;

Stap 2: Lexical Analysis

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 { }
]

Stap 3: Ontleden en AST-generatie

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).

Stap 4: Diagramdetectie

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

Stap 5: Typespecifieke rendering

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);
};

Stap 6: SVG Generation

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>

Stap 7: DOM-invoeging

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;
    }
};

Stap 8: Post-Processing (Where You Come In)

Nadat Mermaid de SVG invoegt, kunt u het verbeteren. Dit is waar al mijn verbeteringen haak in:

  • Pan/zoomfunctionaliteit
  • Bedieningsknoppen
  • Exportmogelijkheden
  • Thema wisselen

Meer hierover hieronder.

Uitbreiding van Mermaid: De Extension Points

Nu we weten hoe Mermaid werkt, laten we onderzoeken hoe het uit te breiden.

1. Configuratie

De meest elementaire uitbreiding is configuratie:

import mermaid from 'mermaid';

mermaid.initialize({
    startOnLoad: true,
    theme: 'dark',
    securityLevel: 'loose',
    flowchart: {
        curve: 'basis',
        padding: 15
    }
});

2. Themaaanpassing

Ik heb dit uitgebreid behandeld in Thema's voor Mermaid wisselen, maar hier is de belangrijkste implementatie:

Het probleem met thema wisselen

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.

De oplossing

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.

3. Verbeteringen na het renderen

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.

Inpakdiagrammen

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;
}

Pan/Zoom toevoegen

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;
}

Controleknoppen

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);
}

Delegatie van evenementen (Prestatie!)

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
    }
});

Exportfunctionaliteit

De uitdaging: SVG elementen hebben dynamische grootte, pan/zoom transformeert, en geërfde stijlen. Om goed te exporteren, moet je:

  1. Kloon de SVG
  2. Dimensie behouden
  3. Transformaties verwijderen
  4. Converteren naar PNG of SVG

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:

  • Weergavebox behouden - Legt het hele diagram vast, niet alleen zichtbaar gedeelte
  • Off-screen rendering - Vermijd invloed op het weergegeven diagram
  • pixelRatio: 2 - High-DPI voor scherpe PNG export

Zie de volledige uitvoercode voor meer details.

Het volledige enhancementspakket

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

Gebruik

npm install @mostlylucid/mermaid-enhancements
import { init } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';

await init();

Je zeemeermin diagrammen hebben nu:

  • Interactieve pan/zoom
  • Full screen lightbox
  • PNG/SVG-export
  • Automatische thema switch
  • Reagerend design

Integratie met HTMX

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
});

Beste praktijken die ik heb geleerd

Na het bouwen van dit spul en het debuggen van rare rand gevallen, hier is wat werkt:

1. Bewaar altijd originele inhoud

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);

2. Opruimen Instances

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);
}

3. Gebeurtenisdelegatie gebruiken

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);
    }
});

4. Handle Cloudflare Raketlader

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>

5. Timing is alles

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();
        });
    });
});

6. Defensive SVG Handling

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}`;
}

Tips voor debuggen

Console-uitvoer

Met de juiste initialisatie, moet je zien:

Saving original data
Loading mermaid with theme: dark
Mermaid initialized
Enhanced 3 diagrams

Controlelijst testen

Na de implementatie van verbeteringen:

  • Diagramweergave op paginabelasting
  • Werk van pan/zoombesturingen
  • Volledig scherm opent/sluit (X, klik buiten, ESC)
  • PNG export vangt volledig diagram
  • SVG export bewaart vectoren
  • Werkt na HTMX content swap
  • Thema's die correct overschakelen
  • Mobiel responsief
  • Donkere modus styling
  • Bereikbaarheid toetsenbord

Gemeenschappelijke vraagstukken

Diagram niet weergeven:

  • Controleer browserconsole op fouten
  • Controleer of Mermaid geladen is (window.mermaid)
  • diagram syntaxis controleren

Pan/zoom werkt niet:

  • Svg-pan-zoom geïnitialiseerd verifiëren
  • Controle op tegenstrijdige CSS (pointer-events: none)
  • Inspecteer instance map

Export vangt alleen hoek:

  • Ontbrekende viewBox bewaring
  • Niet verwijderd transformeren
  • Kloonlogica controleren

Thema niet schakelen:

  • Originele gegevens niet opgeslagen
  • data-processed niet opnieuw ingesteld
  • Gebeurtenisluisteraars niet geregistreerd

Prestatieoverwegingen

Luie initialisatie

Laad 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 });

Intersectie-waarnemer

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);
});

Renders ongedaan maken

Bij verandering van grootte of thema:

let timeout;
window.addEventListener('resize', () => {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
        panZoomInstances.forEach(instance => {
            instance.resize();
            instance.fit();
        });
    }, 250);
});

Conclusie

Mermaid.js is fantastisch uit de doos, maar begrijpen hoe het intern werkt laat je een aantal echt coole verbeteringen bouwen. De belangrijkste inzichten:

  1. Pijpleiding renderen: Tekst → Lexer → Parser → AST → Renderer → SVG
  2. Extensiepunten: Config, thema's, verbeteringen na render
  3. Originele gegevens opslaanMermaid doet het niet voor jou.
  4. Hulpbronnen opruimen: Geheugenlekken zijn echt
  5. Delegatie van evenementen: Betere prestaties
  6. Meerdere themabronnen: Sites behandelen thema's anders
  7. TimingGebruik requestAnimationFrame

Alle 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.

Gerelateerde berichten

Middelen

Probeer de knoppen op de diagrammen hierboven!

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.