Mermaid.js Deep Dive: Come funziona in realtà e come estenderlo (Italiano (Italian))

Mermaid.js Deep Dive: Come funziona in realtà e come estenderlo

Sunday, 09 November 2025

//

17 minute read

Introduzione

NOTA: Questo fa parte dei miei esperimenti con AI / un modo per spendere $1000 crediti di codice di Calude Web. Ho alimentato questo un BUNCH di carte, la mia comprensione, le domande che ho dovuto generare questo articolo. E 'divertente e riempie un vuoto che non ho visto riempito altrove.

Questo post si basa su articoli precedenti: Se non l'hai già fatto, vattene. Aggiungere sirena.js con htmx, Cambiare i temi per la sirena, e Migliorare i diagrammi delle Sirene con Pan/Zoom ed esportazioneQuesta immersione profonda spiega gli interni dietro queste implementazioni.

Mermaid.js è veramente brillante. Scrivi un testo semplice, ottieni dei bellissimi diagrammi. Niente più fumbling con Visio o draw.io, perdere file sorgente o mantenere file immagine separati. Tutto vive in markdown, versione-controllata accanto al tuo codice.

Ma volevo sapere come Funziona davvero sotto il cofano.

graph LR
    A[Text] --> B[Magic?]
    B --> C[Beautiful Diagram]

...diventare un vero SVG? E, cosa più importante, come si può agganciare in esso per aggiungere funzionalità come il pan / zoom, commutazione tema, e funzionalità di esportazione che ho costruito per questo sito (ora disponibile come @mostlylucid/mermaid-enhancements)?

Dopo un sacco di scavare attraverso il codice sorgente della Sirenetta e la costruzione di estensioni reali, ecco tutto quello che ho imparato su come la Sirenetta funziona internamente e come estenderla correttamente.

Che cos'è Mermaid.js?

La sirena trasforma le definizioni di testo in diagrammi. Pensate "Markdown per diagrammi."

Alla vecchia maniera:

  1. Apri strumento di diagramma
  2. Crea diagramma
  3. Esportazione come PNG
  4. Incorpora nei documenti
  5. È necessario aggiornare? Trovare il file sorgente, modificare, ri-esportare, sostituire l'immagine...

La via della Sirenetta:

  1. Scrivi diagramma nel testo
  2. Fatto

Aggiornarlo? Basta modificare il testo. Controllo versione? E 'solo testo! Funziona in markdown? Sì!

Tipi di diagramma

La sirena supporta un numero ridicolo di tipi di diagramma:

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

Vedi i documenti della Sirenetta per la lista completa.

Come sirena funziona in realtà: Il tubo

Ecco cosa succede quando la Sirenetta rende un diagramma:

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

Dividiamo ogni passo.

Fase 1: Definizione del testo

Tutto inizia con il testo. Scrivi diagrammi in DSL di Mermaid (linguaggio specifico del dominio):

// Flowchart
const diagram = `
graph TD
    A[Start] --> B{Is it working?}
    B -->|Yes| C[Great!]
    B -->|No| D[Debug time]
`;

Fase 2: Analisi lessicale

Il lexer rompe il testo in token. Per esempio, questa riga:

A[Start] --> B{Decision}

Diventa gettoni come:

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

Fase 3: Analisi e generazione AST

Il parser consuma gettoni e costruisce un albero di sintassi astratta (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' }
    ]
}

La sirena utilizza diversi parser per ogni tipo di diagramma. Questi sono spesso generati da file di grammatica utilizzando JisonCity name (optional, probably does not need a translation) (come Yacc/Bison per JavaScript).

Fase 4: Rilevamento del diagramma

La sirena rileva il tipo di diagramma dalla prima riga:

// 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

Passo 5: Rendering specifico per il tipo

Ogni tipo di diagramma ha il proprio render. Il render prende l'AST e genera elementi SVG.

Per i diagrammi di flusso, la sirena utilizza il DagreCity name (optional, probably does not need a translation) libreria per il layout grafico. Per altri, utilizza algoritmi personalizzati o librerie come 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);
};

Fase 6: Generazione SVG

Il render produce markup SVG:

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

Passo 7: Inserimento DOM

La sirena trova tutto .mermaid elementi e li sostituisce con reso 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;
    }
};

Passo 8: Post-elaborazione (dove si entra)

Dopo che la Sirenetta inserisce il SVG, è possibile migliorarlo. Questo è dove tutti i miei miglioramenti gancio in:

  • Funzionalità pan/zoom
  • Pulsanti di controllo
  • Capacità di esportazione
  • Commutazione tema

Di più su questo qui sotto.

Estendere la sirena: i punti di estensione

Ora che sappiamo come funziona la Sirenetta, esploriamo come estenderla.

1. Configurazione

L'estensione più fondamentale è la configurazione:

import mermaid from 'mermaid';

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

2. Personalizzazione del tema

L'ho trattato molto bene. Cambiare i temi per la sirena, ma ecco l'implementazione chiave:

Il problema con lo scambio di temi

La sirena ha bisogno di essere inizializzata con un tema, e non si può cambiare dopo. MA se si desidera re-rendere diagrammi con un nuovo tema, è necessario il sorgente del diagramma originale che sirena non memorizza nel DOM.

La soluzione

Memorizzare il contenuto originale prima del rendering, quindi ripristinare e re-render quando si commutano i temi:

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

Metodi di rilevamento di più temi (i siti gestiscono i temi in modo diverso):

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

Vedi il codice completo dello switcher tema per i dettagli.

3. Miglioramenti post-render

Qui è dove avviene la vera magia. Dopo il rendering della Sirenetta, è possibile aggiungere funzioni interattive.

L'ho trattato molto bene. Migliorare i diagrammi delle Sirene con Pan/Zoom ed esportazione, quindi metterò in evidenza le tecniche chiave qui.

Diagrammi di avvolgimento

Crea un contenitore wrapper per i controlli:

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

Aggiunta Pan/Zoom

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

Pulsanti di controllo

Crea pannello di controllo galleggiante:

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

Delegazione dell'evento (Prestazione!)

Non allegare ascoltatori ad ogni pulsante. Usa delega evento:

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

Esporta funzionalità

La sfida: Gli elementi SVG hanno dimensioni dinamiche, trasformazioni pan/zoom e stili ereditati. Per esportare correttamente, è necessario:

  1. Clonare il SVG
  2. Preservare le dimensioni
  3. Rimuovi trasformazioni
  4. Converti in PNG o SVG

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

Dettagli critici:

  • Preserva viewbox - Cattura l'intero diagramma, non solo la porzione visibile
  • Rendering off-screen - Evitare di influenzare il diagramma visualizzato
  • pixelRatio: 2 - Alto DPI per le esportazioni PNG croccanti

Vedi il codice completo dell'esportazione per ulteriori dettagli.

Il pacchetto completo di miglioramento

Ho confezionato tutti questi miglioramenti come @mostlylucid/mermaid-enhancements. Vedi Publishing Mermaid Enhancements come pacchetto npm per i dettagli completi.

Ecco come sta tutto insieme:

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

Uso

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

await init();

I tuoi diagrammi della Sirenetta ora hanno:

  • Pan/zoom interattivo
  • Lightbox a schermo intero
  • Esportazione PNG/SVG
  • Commutazione automatica del tema
  • Disegno responsivo

Integrazione con HTMX

Come ho coperto in Aggiungere sirena.js con htmx, è necessario riinizializzare Mermaid dopo gli swap di contenuti HTMX:

// On page load
document.addEventListener('DOMContentLoaded', function () {
    mermaid.initialize({ startOnLoad: true });
});

// After HTMX swaps content
document.body.addEventListener('htmx:afterSwap', function(evt) {
    mermaid.run();
});

Con il pacchetto miglioramenti:

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

Migliori Pratiche Ho imparato

Dopo aver costruito questa roba e debug strani casi bordo, ecco cosa funziona:

1. Memorizza sempre il contenuto originale

La sirena non conserva la sorgente del diagramma originale dopo il rendering. È necessario memorizzarla da sola:

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. Pulire i casi

Le perdite di memoria sono reali. Distruggere le istanze prima di crearne di nuove:

if (panZoomInstances.has(id)) {
    try {
        panZoomInstances.get(id).destroy();
    } catch (e) {
        console.warn('Failed to destroy:', e);
    }
    panZoomInstances.delete(id);
}

3. Utilizzare la delegazione di eventi

Non allegare gli ascoltatori ai singoli pulsanti:

// ❌ 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 Rocket Loader

Rocket Loader ritarda l'esecuzione JavaScript. Attendere le dipendenze:

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

E escludere lo script principale da Rocket Loader:

<script src="main.js" data-cfasync="false"></script>

5. Il tempismo è tutto

Uso requestAnimationFrame per tempi migliori dell'arbitrario setTimeout:

// After Mermaid renders
await mermaid.run();

// Wait for paint before enhancing
await new Promise(resolve => {
    requestAnimationFrame(() => {
        requestAnimationFrame(() => {
            enhanceMermaidDiagrams();
            resolve();
        });
    });
});

6. Manipolazione SVG difensiva

Gli SVG possono essere strani.

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

Punte di debug

Output console

Con una corretta inizializzazione, si dovrebbe vedere:

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

Lista di controllo di prova

Dopo l'implementazione dei miglioramenti:

  • Diagrammi di rendering sul carico di pagina
  • I controlli Pan/Zoom funzionano
  • Schermo intero si apre/chiude (X, fare clic all'esterno, ESC)
  • PNG export cattura il diagramma completo
  • SVG esportazione conserva vettori
  • Funziona dopo lo scambio di contenuti HTMX
  • Switching tema re-renders correttamente
  • Mobile responsive
  • Styling in modalità scura
  • Accessibilità della tastiera

Questioni comuni

Diagramma non rendering:

  • Controllare la console del browser per gli errori
  • Verifica sirena caricata (window.mermaid)
  • Controlla la sintassi del diagramma

Pan/zoom non funzionante:

  • Verifica inizializzata svg-pan-zoom
  • Controllare la presenza di CSS in conflitto (pointer-events: none)
  • Ispezionare la mappa dell'istanza

Esporta cattura solo angolo:

  • Conservazione del riquadro vista mancante
  • Trasforma non rimosso
  • Controlla la logica della clonazione

Tema che non cambia:

  • Dati originali non salvati
  • data-processed non resetta
  • Ascoltatori di eventi non registrati

Considerazioni sulle prestazioni

Inizializzazione pigra

Non caricare i miglioramenti fino a quando necessario:

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

Osservatore di intersezione

Rendere i diagrammi solo quando visibili:

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

Debounce Re-renders

Su ridimensionare o modificare il tema:

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

In conclusione

Mermaid.js è fantastico fuori dalla scatola, ma capire come funziona internamente consente di costruire alcuni miglioramenti davvero cool. Le intuizioni chiave:

  1. Rendering Pipeline: Testo → Lexer → Parser → AST → Renderer → SVG
  2. Punti di estensione: Config, temi, miglioramenti post-render
  3. Memorizza dati originali: La sirena non lo fa per te
  4. Ripulire le risorse: Le perdite di memoria sono reali
  5. Delegazione evento: Migliori prestazioni
  6. Sorgenti tematiche multiple: I siti gestiscono i temi in modo diverso
  7. Questioni temporali: Uso requestAnimationFrame

Tutte le tecniche che ho coperto qui sono utilizzati in produzione su questo sito e confezionati in @mostlylucid/mermaid-enhancements. La fonte completa è disponibile all'indirizzo per lo più weblucid/la maggior partelucid-mermaid.

Post correlati

Risorse

Provare i controlli sui diagrammi di cui sopra!

Finding related posts...
logo

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