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.
La sirena trasforma le definizioni di testo in diagrammi. Pensate "Markdown per diagrammi."
Alla vecchia maniera:
La via della Sirenetta:
Aggiornarlo? Basta modificare il testo. Controllo versione? E 'solo testo! Funziona in markdown? Sì!
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.
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.
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]
`;
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 { }
]
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).
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
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);
};
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>
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;
}
};
Dopo che la Sirenetta inserisce il SVG, è possibile migliorarlo. Questo è dove tutti i miei miglioramenti gancio in:
Di più su questo qui sotto.
Ora che sappiamo come funziona la Sirenetta, esploriamo come estenderla.
L'estensione più fondamentale è la configurazione:
import mermaid from 'mermaid';
mermaid.initialize({
startOnLoad: true,
theme: 'dark',
securityLevel: 'loose',
flowchart: {
curve: 'basis',
padding: 15
}
});
L'ho trattato molto bene. Cambiare i temi per la sirena, ma ecco l'implementazione chiave:
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.
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.
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.
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;
}
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;
}
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);
}
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
}
});
La sfida: Gli elementi SVG hanno dimensioni dinamiche, trasformazioni pan/zoom e stili ereditati. Per esportare correttamente, è necessario:
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:
Vedi il codice completo dell'esportazione per ulteriori dettagli.
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
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:
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
});
Dopo aver costruito questa roba e debug strani casi bordo, ecco cosa funziona:
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);
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);
}
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);
}
});
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>
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();
});
});
});
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}`;
}
Con una corretta inizializzazione, si dovrebbe vedere:
Saving original data
Loading mermaid with theme: dark
Mermaid initialized
Enhanced 3 diagrams
Dopo l'implementazione dei miglioramenti:
Diagramma non rendering:
window.mermaid)Pan/zoom non funzionante:
pointer-events: none)Esporta cattura solo angolo:
Tema che non cambia:
data-processed non resettaNon 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 });
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);
});
Su ridimensionare o modificare il tema:
let timeout;
window.addEventListener('resize', () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
panZoomInstances.forEach(instance => {
instance.resize();
instance.fit();
});
}, 250);
});
Mermaid.js è fantastico fuori dalla scatola, ma capire come funziona internamente consente di costruire alcuni miglioramenti davvero cool. Le intuizioni chiave:
requestAnimationFrameTutte 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.
Provare i controlli sui diagrammi di cui sopra!
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.