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
Sunday, 09 November 2025
NOTA: Esto es parte de mis experimentos con IA / una manera de gastar $1000 créditos de Código Calude Web. He alimentado esto un montón de documentos, mi comprensión, preguntas que tuve que generar este artículo. Es divertido y llena un vacío que no he visto llenado en ningún otro lugar.
Este post se basa en artículos anteriores: Si aún no lo has hecho, echa un vistazo. Añadir sirena.js con htmx, Cambiar temas por Sirena, y Mejorando los diagramas de sirena con Pan/Zoom y Exportación. Esta inmersión profunda explica los aspectos internos detrás de esas implementaciones.
Mermaid.js es realmente brillante. Escribir texto simple, obtener diagramas hermosos. No más balbucear con Visio o draw.io, perder archivos de origen, o mantener archivos de imagen separados. Todo vive en Markdown, versión controlada junto a su código.
Pero quería saber ¿Cómo? En realidad funciona bajo el capó. ¿Cómo hace esto?
graph LR
A[Text] --> B[Magic?]
B --> C[Beautiful Diagram]
... convertirse en un SVG real? Y lo más importante, ¿cómo se puede conectar en él para añadir características como el pan/zoom, conmutación de temas, y la funcionalidad de exportación que construí para este sitio (ahora disponible como @mostlylucid/mermaid-enhancements)?
Después de investigar mucho a través del código fuente de Mermaid y construir extensiones reales, aquí está todo lo que aprendí acerca de cómo trabaja Mermaid internamente y cómo extenderlo correctamente.
Sirena convierte las definiciones de texto en diagramas. Piensa en "Marcado para diagramas".
A la vieja manera:
El camino de la sirena:
Actualízalo? Sólo edita el texto. Control de la versión? Es sólo texto! Funciona en marcado? Sí!
Sirena soporta un número ridículo de tipos de diagramas:
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...]
Ver los documentos de la sirena para la lista completa.
Esto es lo que sucede cuando Sirena representa un diagrama:
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
Vamos a romper cada paso.
Todo comienza con texto. Usted escribe diagramas en el DSL de Sirena (idioma específico del dominio):
// Flowchart
const diagram = `
graph TD
A[Start] --> B{Is it working?}
B -->|Yes| C[Great!]
B -->|No| D[Debug time]
`;
El lexer rompe texto en tokens. Por ejemplo, esta línea:
A[Start] --> B{Decision}
Se convierte en fichas como:
[
{ 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 { }
]
El analizador consume tokens y construye 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' }
]
}
Sirena utiliza diferentes analizadores para cada tipo de diagrama. Estos se generan a menudo a partir de archivos gramaticales usando Jison (como Yacc/Bison para JavaScript).
Sirena detecta el tipo de diagrama desde la primera línea:
// 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
Cada tipo de diagrama tiene su propio renderizador. El renderizador toma el AST y genera elementos SVG.
Para diagramas de flujo, Sirena utiliza el Dagre biblioteca para el diseño de gráficos. Para otros, utiliza algoritmos personalizados o bibliotecas como 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);
};
El renderizador produce el marcado 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>
Sirena encuentra todo .mermaid elementos y los reemplaza con SVG renderizado:
// 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;
}
};
Después de Sirena inserta el SVG, se puede mejorar. Aquí es donde todas mis mejoras gancho en:
Más sobre esto abajo.
Ahora que sabemos cómo trabaja Sirena, exploremos cómo extenderla.
La extensión más básica es la configuración:
import mermaid from 'mermaid';
mermaid.initialize({
startOnLoad: true,
theme: 'dark',
securityLevel: 'loose',
flowchart: {
curve: 'basis',
padding: 15
}
});
He cubierto esto extensamente en Cambiar temas por Sirena, pero aquí está la implementación clave:
Sirena necesita ser inicializada con un tema, y no puede cambiarlo después. PERO si desea volver a renderizar diagramas con un tema nuevo, necesita la fuente original del diagrama, que Sirena no almacena en el DOM.
Almacene el contenido original antes de renderizar, luego restaure y vuelva a renderizar cuando cambie de tema:
// 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étodos de detección de múltiples temas (sitios manejan temas de manera diferente):
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';
}
Ver el código completo del conmutador del tema para más detalles.
Aquí es donde ocurre la magia real. Después de los renders de Sirena, puedes añadir funciones interactivas.
He cubierto esto extensamente en Mejorando los diagramas de sirena con Pan/Zoom y Exportación, así que voy a destacar las técnicas clave aquí.
Crear un contenedor de envoltura para los controles:
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;
}
Crear panel de control flotante:
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);
}
No adjunte oyentes a cada botón. Utilice la delegación de eventos:
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
}
});
El desafío: Los elementos SVG tienen dimensionamiento dinámico, transforma pan/zoom y estilos heredados. Para exportar correctamente, necesita:
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);
}
Detalles críticos:
Ver el código de exportación completo para más detalles.
He empaquetado todas estas mejoras como @mostlylucid/mermaid-enhancements. Véase Publicación de mejoras de sirena como un paquete npm para más detalles.
Así es como todo encaja:
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();
Tus diagramas de Sirena ahora tienen:
Como cubrí en Añadir sirena.js con htmx, es necesario volver a iniciar Mermaid después de los intercambios de contenido 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 el paquete de mejoras:
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
});
Después de construir este material y depurar casos de bordes extraños, esto es lo que funciona:
La sirena no conserva la fuente original del diagrama después de la renderización.
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);
Las fugas de memoria son reales. Destruya instancias antes de crear nuevas:
if (panZoomInstances.has(id)) {
try {
panZoomInstances.get(id).destroy();
} catch (e) {
console.warn('Failed to destroy:', e);
}
panZoomInstances.delete(id);
}
No adjunte los oyentes a botones individuales:
// ❌ 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 retrasa la ejecución de JavaScript.
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();
});
}
Y excluya su guión principal de Rocket Loader:
<script src="main.js" data-cfasync="false"></script>
Uso requestAnimationFrame para un mejor momento que arbitrario setTimeout:
// After Mermaid renders
await mermaid.run();
// Wait for paint before enhancing
await new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
enhanceMermaidDiagrams();
resolve();
});
});
});
Los SVG pueden ser raros.
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 la inicialización adecuada, usted debe ver:
Saving original data
Loading mermaid with theme: dark
Mermaid initialized
Enhanced 3 diagrams
Después de implementar mejoras:
Diagrama no renderizado:
window.mermaid)Sin trabajar en pan ni en zoom:
pointer-events: none)Exportar capturas sólo esquina:
Tema que no cambia:
data-processed no restablecerNo cargue mejoras hasta que sea necesario:
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 });
Diagramas de renderización sólo cuando se pueden ver:
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);
});
En el cambio de tamaño o tema:
let timeout;
window.addEventListener('resize', () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
panZoomInstances.forEach(instance => {
instance.resize();
instance.fit();
});
}, 250);
});
Mermaid.js es fantástico fuera de la caja, pero entender cómo funciona internamente le permite construir algunas mejoras realmente geniales.
requestAnimationFrameTodas las técnicas que he cubierto aquí se utilizan en la producción en este sitio y empaquetado en @mostlylucid/mermaid-enhancements. La fuente completa está disponible en En su mayoríalucidweb/en su mayoríalucid-mermaid.
¡Prueba los controles en los diagramas de arriba!
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.