Mermaid.js Buceo profundo: Cómo funciona realmente y cómo extenderlo (Español (Spanish))

Mermaid.js Buceo profundo: Cómo funciona realmente y cómo extenderlo

Sunday, 09 November 2025

//

17 minute read

Introducción

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.

¿Qué es Mermaid.js?

Sirena convierte las definiciones de texto en diagramas. Piensa en "Marcado para diagramas".

A la vieja manera:

  1. Abrir herramienta de diagramación
  2. Crear diagrama
  3. Exportar como PNG
  4. Incrustado en documentos
  5. ¿Necesita actualizar? Encuentre el archivo fuente, edite, reexporte, reemplace la imagen...

El camino de la sirena:

  1. Escribir diagrama en texto
  2. Hecho

Actualízalo? Sólo edita el texto. Control de la versión? Es sólo texto! Funciona en marcado? Sí!

Tipos de diagramas

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.

Cómo funciona realmente la sirena: El oleoducto

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.

Paso 1: Definición de texto

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

Paso 2: Análisis léxico

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

Paso 3: Análisis y generación AST

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

Paso 4: Detección de diagramas

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

Paso 5: Renderización específica de tipo

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

Paso 6: Generación SVG

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>

Paso 7: Inserción de DOM

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

Paso 8: Post-Procesamiento (donde usted entra)

Después de Sirena inserta el SVG, se puede mejorar. Aquí es donde todas mis mejoras gancho en:

  • Funcionalidad de pan/zoom
  • Botones de control
  • Capacidades de exportación
  • Cambio de tema

Más sobre esto abajo.

Extender Sirena: Los puntos de extensión

Ahora que sabemos cómo trabaja Sirena, exploremos cómo extenderla.

1. Configuración

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

2. Personalización del tema

He cubierto esto extensamente en Cambiar temas por Sirena, pero aquí está la implementación clave:

El problema con el cambio de tema

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.

La solución

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.

3. Mejoras posteriores a los resultados

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

Diagramas de envoltorio

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

Añadiendo 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;
}

Botones de control

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

Delegación de eventos (¡Performance!)

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

Funcionalidad de exportación

El desafío: Los elementos SVG tienen dimensionamiento dinámico, transforma pan/zoom y estilos heredados. Para exportar correctamente, necesita:

  1. Clonar el SVG
  2. Preservar las dimensiones
  3. Eliminar transforms
  4. Convertir en 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);
}

Detalles críticos:

  • Preservar vistaBox - Captura todo el diagrama, no sólo la porción visible
  • Renderizado fuera de pantalla - Evite afectar el diagrama mostrado
  • pixelRatio: 2 - High-DPI para las exportaciones de PNG crujientes

Ver el código de exportación completo para más detalles.

El paquete completo de mejora

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

Uso

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:

  • • Pan/zoom interactivo
  • • Caja de luz de pantalla completa
  • • Exportación de PNG/SVG
  • • Conmutación automática del tema
  • Diseño responsivo

Integración con HTMX

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

Mejores prácticas que aprendí

Después de construir este material y depurar casos de bordes extraños, esto es lo que funciona:

1. Almacenar siempre el contenido original

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

2. Limpiar las instancias

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

3. Usar delegación de eventos

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

4. Manipule el cargador de cohetes Cloudflare

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>

5. El tiempo es todo

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

6. Manipulación defensiva de SVG

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

Consejos de depuración

Salida de la consola

Con la inicialización adecuada, usted debe ver:

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

Lista de comprobación de pruebas

Después de implementar mejoras:

  • Diagramas renderizados en la carga de la página
  • Controles de pan/zoom funcionan
  • Pantalla completa se abre/se cierra (X, haga clic fuera, ESC)
  • Exportación de PNG captura diagrama completo
  • [ Exportación SVG preserva vectores
  • Funciona después del intercambio de contenido HTMX
  • El tema de conmutación re-renders correctamente
  • [ # Móvil sensible #
  • [ Estilo de modo oscuro
  • Accesibilidad del teclado

Cuestiones comunes

Diagrama no renderizado:

  • Comprobar si hay errores en la consola del navegador
  • Verificar sirena cargada (window.mermaid)
  • Comprobar la sintaxis del diagrama

Sin trabajar en pan ni en zoom:

  • Verificar inicializado svg-pan-zoom
  • Comprobar si CSS está en conflicto (pointer-events: none)
  • Inspeccionar mapa de instancia

Exportar capturas sólo esquina:

  • Falta la preservación de la vistaBox
  • Transformar no eliminado
  • Comprobar la lógica de clonación

Tema que no cambia:

  • Datos originales no guardados
  • data-processed no restablecer
  • Los oyentes de eventos no están registrados

Consideraciones sobre el desempeño

Inicialización perezosa

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

Observador de Intersección

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

Debounce Re-renders

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

Conclusión

Mermaid.js es fantástico fuera de la caja, pero entender cómo funciona internamente le permite construir algunas mejoras realmente geniales.

  1. Pipeline de renderización: Texto → Lexer → Parser → AST → Renderer → SVG
  2. Puntos de extensión: Config, temas, mejoras post-render
  3. Almacenar datos originalesLa sirena no lo hace por ti.
  4. Limpiar los recursos: Las fugas de memoria son reales
  5. Delegación del evento: Mejor rendimiento
  6. Fuentes temáticas múltiples: Los sitios manejan los temas de manera diferente
  7. Cuestiones relativas al calendario: Usar requestAnimationFrame

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

Puestos relacionados

Recursos

¡Prueba los controles en los diagramas de arriba!

Finding related posts...
logo

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