REMARQUE: Ceci fait partie de mes expériences avec l'IA / un moyen de dépenser 1000 $ Code Calude crédits Web. J'ai alimenté ceci un BUNCH de papiers, ma compréhension, questions que j'ai dû générer cet article. C'est amusant et comble un vide que je n'ai pas vu comblé nulle part ailleurs.
Ce post s'appuie sur les articles précédents: Si tu ne l'as pas encore fait, regarde. Ajouter sirmaid.js avec htmx, Thèmes de commutation pour sirène, et Améliorer les diagrammes de sirène avec Pan/Zoom et Export. Cette plongée profonde explique les internes derrière ces implémentations.
Sirmaid.js est vraiment brillant. Ecrivez du texte simple, obtenez de beaux diagrammes. Plus de trébuchement avec Visio ou draw.io, perdre des fichiers sources, ou maintenir des fichiers d'image séparés. Tout vit en balisage, contrôlé en version à côté de votre code.
Mais je voulais savoir. Comment En fait, ça marche sous le capot.
graph LR
A[Text] --> B[Magic?]
B --> C[Beautiful Diagram]
...devenir un SVG réel? Et plus important, comment pouvez-vous l'accrocher pour ajouter des fonctionnalités comme le pan/zoom, le changement de thème, et la fonctionnalité d'exportation que j'ai construit pour ce site (maintenant disponible comme @mostlylucide/mermaid-enhancements)?
Après beaucoup de recherches dans le code source de Sirmaid et de construction de vraies extensions, voici tout ce que j'ai appris sur le fonctionnement interne de Sirmaid et comment l'étendre correctement.
Sirène transforme les définitions de texte en diagrammes. Pensez "Marquage pour les diagrammes."
L'ancienne façon :
La manière de la sirène :
Mettre à jour? Il suffit de modifier le texte. Contrôle de la version? C'est juste du texte! Fonctionne en balisage? Oui!
La sirène supporte un nombre ridicule de types de diagrammes :
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...]
Voir les documents de sirène pour la liste complète.
Voici ce qui se passe quand Sirmaid rend un diagramme :
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
Découpons chaque pas.
Tout commence par du texte. Vous écrivez des diagrammes dans le DSL de Sirmaid (langue spécifique au domaine):
// Flowchart
const diagram = `
graph TD
A[Start] --> B{Is it working?}
B -->|Yes| C[Great!]
B -->|No| D[Debug time]
`;
Le lexer brise le texte en jetons. Par exemple, cette ligne :
A[Start] --> B{Decision}
Devient des jetons comme:
[
{ 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 { }
]
L'analyseur consomme des jetons et construit un 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' }
]
}
Sirène utilise différents parseurs pour chaque type de diagramme. Ceux-ci sont souvent générés à partir de fichiers de grammaire en utilisant Jison (comme Yacc/Bison pour JavaScript).
Sirène détecte le type de diagramme à partir de la première ligne:
// 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
Chaque type de diagramme a son propre rendeur. Le rendeur prend l'AST et génère des éléments SVG.
Pour les diagrammes de flux, Mermaid utilise M. Dagre bibliothèque pour la mise en page graphique. Pour d'autres, il utilise des algorithmes personnalisés ou des bibliothèques comme 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);
};
Le rendu produit le balisage 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 sirène trouve tout .mermaid les éléments et les remplace par SVG rendu:
// 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;
}
};
Après la sirène insère le SVG, vous pouvez l'améliorer. C'est là que toutes mes améliorations s'accrochent:
Plus sur ceci ci-dessous.
Maintenant que nous savons comment fonctionne la sirène, nous allons explorer comment l'étendre.
L'extension la plus basique est la configuration:
import mermaid from 'mermaid';
mermaid.initialize({
startOnLoad: true,
theme: 'dark',
securityLevel: 'loose',
flowchart: {
curve: 'basis',
padding: 15
}
});
J'ai largement couvert cette question en Thèmes de commutation pour sirène, mais voici la mise en œuvre clé:
La sirène a besoin d'être initialisée avec un thème, et vous ne pouvez pas le changer après. MAIS si vous voulez rendre des diagrammes avec un nouveau thème, vous avez besoin de la source du diagramme original — qui Mermaid ne stocke pas dans les DOM.
Conservez le contenu original avant le rendu, puis restaurez et renvoyez lors de la commutation des thèmes :
// 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;
}
}
};
Méthodes de détection de thèmes multiples (les sites traitent les thèmes différemment):
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';
}
Voir le code du commutateur de thème complet pour plus de détails.
C'est là que se produit la vraie magie. Après les rendus de Sirmaid, vous pouvez ajouter des fonctionnalités interactives.
J'ai largement couvert cette question en Améliorer les diagrammes de sirène avec Pan/Zoom et Export, donc je vais mettre en évidence les techniques clés ici.
Créer un conteneur d'emballage pour les commandes :
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;
}
Utilisation 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;
}
Créer un panneau de commande flottant :
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);
}
N'attachez pas les auditeurs à chaque bouton. Utilisez la délégation d'événement:
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
}
});
Le défi : Les éléments SVG ont un dimensionnement dynamique, des transformations pan/zoom et des styles hérités. Pour exporter correctement, vous devez :
Utilisation html-à-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);
}
Détails critiques:
Voir le code d'exportation complet pour plus de détails.
J'ai emballé toutes ces améliorations comme @mostlylucide/mermaid-enhancementsVoir Édition Sirène Améliorations en tant que paquet npm pour plus de détails.
Voici comment tout s'harmonise :
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();
Vos diagrammes de Sirène ont maintenant :
Comme je l'ai couvert Ajouter sirmaid.js avec htmx, vous devez réinitialiser la sirène après les swaps de contenu 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();
});
Avec le paquet d'améliorations:
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
});
Après avoir construit ce truc et débogé des cas bizarres de bord, voici ce qui fonctionne:
Sirène ne conserve pas la source du diagramme d'origine après le rendu. Vous devez le stocker vous-même:
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);
Les fuites de mémoire sont réelles. Détruisez les instances avant de créer de nouvelles:
if (panZoomInstances.has(id)) {
try {
panZoomInstances.get(id).destroy();
} catch (e) {
console.warn('Failed to destroy:', e);
}
panZoomInstances.delete(id);
}
N'attachez pas d'auditeurs à des boutons individuels :
// ❌ 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 retarde l'exécution JavaScript. Attendez les dépendances :
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();
});
}
Et exclure votre script principal de Rocket Loader:
<script src="main.js" data-cfasync="false"></script>
Utilisation requestAnimationFrame pour un meilleur timing qu'arbitraire setTimeout:
// After Mermaid renders
await mermaid.run();
// Wait for paint before enhancing
await new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
enhanceMermaidDiagrams();
resolve();
});
});
});
Les SVG peuvent être bizarres.
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}`;
}
Avec l'initialisation appropriée, vous devriez voir:
Saving original data
Loading mermaid with theme: dark
Mermaid initialized
Enhanced 3 diagrams
Après la mise en œuvre des améliorations:
Diagramme non rendu:
window.mermaid)Pan/zoom ne fonctionne pas:
pointer-events: none)Captures à l'exportation seulement dans le coin :
Thème non commuté :
data-processed ne pas réinitialiserNe chargez pas les améliorations jusqu'à ce qu'elles soient nécessaires :
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 });
Diagrammes de rendu uniquement lorsqu'ils sont visibles:
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);
});
Sur redimensionnement ou changement de thème :
let timeout;
window.addEventListener('resize', () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
panZoomInstances.forEach(instance => {
instance.resize();
instance.fit();
});
}, 250);
});
Sirmaid.js est fantastique hors de la boîte, mais comprendre comment il fonctionne en interne vous permet de construire des améliorations vraiment cool.
requestAnimationFrameToutes les techniques que j'ai couvertes ici sont utilisées en production sur ce site et emballées dans @mostlylucide/mermaid-enhancements. La source complète est disponible à l'adresse suivante: principalementlucideweb/mermaid le pluslylucide.
Essayez les contrôles sur les diagrammes ci-dessus!
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.