Ik. een .net kerel eindelijk geplukt de moed om mijn tenen te dompelen in de wereld van npm pakketten! Mermaid.js is obscuur genoeg en vreemd genoeg dat ik eigenlijk iets nuttigs kan leveren! Dus geniet ervan.
Na het bouwen van een aantal echt nuttige verbeteringen voor Mermaid.js diagrammen (interactieve pan/zoom, fullscreen lightbox, exporteren naar PNG/SVG, en automatische thema switching), Ik besloot dat het tijd was om ze goed te verpakken en ze te delen met de gemeenschap. Deze post loopt door hoe ik gemaakt @mostlylucid/mermaid-enhancements als een production-ready npm pakket.
OPMERKING: Nog steeds bezig met de release. Blijf kijken (mijn eerste npm pakket dus een beetje nemen)
Ik heb gebruik gemaakt van deze verbeteringen op mijn blog voor een tijdje nu, en ze zijn essentieel geworden voor het werken met complexe Mermaid diagrammen. De functies omvatten:
Aangezien ik dezelfde code kopieerde tussen projecten, was het zinvol om een goed npm-pakket te maken dat iedereen kon gebruiken.
Ik heb een professionele pakketstructuur opgezet met TypeScript ondersteuning:
mostlylucid-mermaid/
├── src/
│ ├── index.ts # Main entry point
│ ├── enhancements.ts # Pan/zoom/export functionality
│ ├── theme-switcher.ts # Theme switching logic
│ ├── types.ts # TypeScript type definitions
│ └── styles.css # Complete styling
├── examples/
│ └── demo.html # Full-featured demo
├── dist/ # Built output (generated)
├── package.json
├── tsconfig.json
├── README.md
├── QUICKSTART.md
├── PUBLISHING.md
└── LICENSE
Het pakket maakt gebruik van TypeScript voor typeveiligheid en betere ontwikkelaarservaring, maar compileert naar JavaScript voor maximale compatibiliteit.
Hier is hoe de componenten bij elkaar passen:
Dus je ziet het is vrij compact en heeft een aantal nuttige functionaliteiten dan alleen statische diagrammen. Het irriteerde me altijd hoe MASSIVE ze waren in de pagina dus dit leek een verstandige benadering van reduceren grootte met behoud van nut.
graph TD
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
Eerst heb ik uitgebreide TypeScript types gedefinieerd:
// src/types.ts
export interface PanZoomInstance {
zoom(scale: number): void;
zoomIn(): void;
zoomOut(): void;
reset(): void;
fit(): void;
center(): void;
resize(): void;
destroy(): void;
isPanEnabled(): boolean;
enablePan(enabled: boolean): void;
}
export type ExportFormat = 'png' | 'svg';
export type Theme = 'dark' | 'default';
export type ControlAction = 'fullscreen' | 'zoomIn' | 'zoomOut' |
'reset' | 'pan' | 'exportPng' | 'exportSvg';
export interface EnhancementConfig {
icons?: IconConfig;
controls?: {
fullscreen?: boolean;
zoom?: boolean;
pan?: boolean;
export?: boolean;
};
}
Het belangrijkste ingangspunt is dood eenvoudig:
// src/index.ts
export {
enhanceMermaidDiagrams,
cleanupMermaidEnhancements
} from './enhancements.js';
export {
initMermaid
} from './theme-switcher.js';
export async function init() {
await initMermaid();
}
export default {
init,
initMermaid,
enhanceMermaidDiagrams,
};
De versterkingslogica wraps elk diagram met controles en initialiseert svg-pan-zoom:
// src/enhancements.ts
import svgPanZoom from 'svg-pan-zoom';
import { toPng, toSvg } from 'html-to-image';
const panZoomInstances = new Map();
function initPanZoom(svgElement: SVGElement, diagramId: string) {
// Clean up existing instance if present
if (panZoomInstances.has(diagramId)) {
try {
panZoomInstances.get(diagramId).destroy();
} catch (e) {
console.warn('Failed to destroy existing pan-zoom instance:', e);
}
panZoomInstances.delete(diagramId);
}
try {
const panZoomInstance = svgPanZoom(svgElement, {
zoomEnabled: true,
controlIconsEnabled: false,
fit: true,
center: true,
minZoom: 0.1,
maxZoom: 10,
zoomScaleSensitivity: 0.3,
dblClickZoomEnabled: true,
mouseWheelZoomEnabled: true,
preventMouseEventsDefault: true,
contain: false
});
panZoomInstances.set(diagramId, panZoomInstance);
return panZoomInstance;
} catch (error) {
console.error('Failed to initialize pan-zoom:', error);
return null;
}
}
De bedieningsknoppen worden dynamisch aangemaakt:
function createControlButtons(container: HTMLElement, diagramId: string) {
if (container.querySelector('.mermaid-controls')) {
return;
}
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 View', action: 'reset' },
{ icon: 'bx-move', title: 'Pan', action: 'pan' },
{ icon: 'bx-image', title: 'Export as PNG', action: 'exportPng' },
{ icon: 'bx-code-alt', title: 'Export as SVG', action: 'exportSvg' }
];
buttons.forEach(btn => {
const button = document.createElement('button');
button.className = `mermaid-control-btn bx ${btn.icon}`;
button.setAttribute('title', btn.title);
button.setAttribute('aria-label', btn.title);
button.setAttribute('data-action', btn.action);
button.setAttribute('data-diagram-id', diagramId);
controlsDiv.appendChild(button);
});
container.appendChild(controlsDiv);
}
De exportimplementatie kloont de SVG, bewaart de viewBox en gebruikt html-to-image:
async function exportDiagram(
container: HTMLElement,
format: ExportFormat,
diagramId: string
) {
try {
const svgElement = container.querySelector('svg');
if (!svgElement) {
console.warn('No diagram found to export');
return;
}
// Clone to avoid modifying the original
const clonedSvg = svgElement.cloneNode(true) as SVGElement;
// 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 for proper export
const [, , vbWidth, vbHeight] = viewBox.split(' ').map(Number);
clonedSvg.setAttribute('width', vbWidth.toString());
clonedSvg.setAttribute('height', vbHeight.toString());
// Remove pan-zoom transforms
clonedSvg.removeAttribute('style');
clonedSvg.style.backgroundColor = 'transparent';
clonedSvg.style.maxWidth = 'none';
// Create temporary container
const tempDiv = document.createElement('div');
tempDiv.style.position = 'absolute';
tempDiv.style.left = '-9999px';
tempDiv.appendChild(clonedSvg);
document.body.appendChild(tempDiv);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `mermaid-diagram-${timestamp}`;
if (format === 'png') {
const dataUrl = await toPng(clonedSvg, {
backgroundColor: 'white',
pixelRatio: 2 // Higher quality
});
downloadFile(dataUrl, `${filename}.png`);
} else {
const dataUrl = await toSvg(clonedSvg, {
backgroundColor: 'transparent'
});
downloadFile(dataUrl, `${filename}.svg`);
}
document.body.removeChild(tempDiv);
console.log(`Diagram exported as ${format.toUpperCase()}`);
} catch (error) {
console.error('Failed to export diagram:', error);
}
}
De themawisselaar verwerkt meerdere detectiemethoden:
// src/theme-switcher.ts
export async function initMermaid() {
// Normalize code fences
normalizeMermaidCodeFences();
const mermaidElements = document.querySelectorAll(elementSelector);
if (mermaidElements.length === 0) return;
await saveOriginalData();
// Set up theme change handlers
const handleDarkThemeSet = async () => {
await resetProcessed();
await loadMermaid('dark');
};
const handleLightThemeSet = async () => {
await resetProcessed();
await loadMermaid('default');
};
// Listen for custom theme events
document.body.addEventListener('dark-theme-set', handleDarkThemeSet);
document.body.addEventListener('light-theme-set', handleLightThemeSet);
// OS theme change listener
if (typeof window.matchMedia === 'function') {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', async (e) => {
await resetProcessed();
await loadMermaid(e.matches ? 'dark' : 'default');
});
}
// Detect current theme with fallbacks
let isDarkMode = false;
if (typeof window.__themeState !== 'undefined') {
isDarkMode = window.__themeState === 'dark';
} else if (localStorage.theme) {
isDarkMode = localStorage.theme === 'dark';
} else if (document.documentElement.classList.contains('dark')) {
isDarkMode = true;
} else if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
isDarkMode = true;
}
await loadMermaid(isDarkMode ? 'dark' : 'default');
}
Het pakket.json definieert meerdere ingangspunten voor verschillende gebruikscases:
{
"name": "@mostlylucid/mermaid-enhancements",
"version": "1.0.0",
"description": "Enhance Mermaid.js diagrams with interactive pan/zoom, fullscreen lightbox, export to PNG/SVG, and automatic theme switching",
"main": "dist/index.js",
"module": "src/index.ts",
"types": "src/types.ts",
"exports": {
".": {
"types": "./src/types.ts",
"import": "./src/index.ts",
"require": "./dist/index.js"
},
"./min": {
"types": "./dist/index.d.ts",
"import": "./dist/index.min.js",
"require": "./dist/index.min.js"
},
"./styles.css": "./src/styles.css"
},
"unpkg": "dist/index.min.js",
"jsdelivr": "dist/index.min.js",
"scripts": {
"build": "tsc",
"minify": "node scripts/minify.js",
"build:all": "npm run build && npm run minify",
"prepublishOnly": "npm run build:all",
"dev": "cd examples && npx http-server -p 3000 -o"
},
"peerDependencies": {
"mermaid": "^10.0.0 || ^11.0.0"
},
"dependencies": {
"html-to-image": "^1.11.11",
"svg-pan-zoom": "^3.6.1"
}
}
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "examples"]
}
De eenvoudigste manier om het pakket te gebruiken:
import mermaid from 'mermaid';
import { init } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';
await init();
Voor sites met licht/donker modus:
import { init } from '@mostlylucid/mermaid-enhancements';
// Initialize
await init();
// When theme changes
function toggleTheme() {
const isDark = document.body.classList.toggle('dark');
document.documentElement.classList.toggle('dark', isDark);
// Notify the enhancements
const event = new Event(isDark ? 'dark-theme-set' : 'light-theme-set');
document.body.dispatchEvent(event);
}
import { useEffect } from 'react';
import { init, cleanupMermaidEnhancements } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';
function MermaidDiagram({ chart }: { chart: string }) {
useEffect(() => {
init();
return () => cleanupMermaidEnhancements();
}, [chart]);
return (
<div className="mermaid">
{chart}
</div>
);
}
<template>
<div class="mermaid">{{ chart }}</div>
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue';
import { init, cleanupMermaidEnhancements } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';
const props = defineProps(['chart']);
onMounted(async () => {
await init();
});
onUnmounted(() => {
cleanupMermaidEnhancements();
});
</script>
Ik heb een uitgebreide demo pagina gemaakt met alle functies:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mermaid Enhancements Demo</title>
<!-- Boxicons for control button icons -->
<link href="https://unpkg.com/[email protected]/css/boxicons.min.css" rel="stylesheet">
<!-- Mermaid Enhancements CSS -->
<link rel="stylesheet" href="../src/styles.css">
</head>
<body>
<!-- Your diagrams -->
<div class="mermaid">
graph TD
A[Start] --> B{Is it working?}
B -->|Yes| C[Great!]
B -->|No| D[Check setup]
C --> E[Zoom & Pan]
D --> F[Read docs]
E --> G[Export to PNG/SVG]
</div>
<!-- Load Mermaid -->
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
window.mermaid = mermaid;
</script>
<!-- Initialize enhancements -->
<script type="module">
import { init } from '../dist/index.js';
await init();
</script>
</body>
</html>
Hier is de publicatie workflow:
sequenceDiagram
participant Dev as Developer
participant Git as Git Repo
participant NPM as npm Registry
participant CDN as unpkg/jsdelivr
participant User as End User
Dev->>Dev: Write code
Dev->>Dev: npm run build:all
Dev->>Dev: Test locally
Dev->>Git: git commit & push
Dev->>Git: Create version tag
Dev->>NPM: npm login
Dev->>NPM: npm publish --access public
NPM-->>CDN: Sync package
User->>NPM: npm install
User->>CDN: Import from CDN
NPM-->>User: Deliver package
CDN-->>User: Serve files
npm run build:all # Compiles TypeScript and minifies
npm run dev # Opens demo at localhost:3000
npm version patch # or minor, or major
npm login
npm publish --access public
Het pakket is nu beschikbaar via:
npm install @mostlylucid/mermaid-enhancementshttps://unpkg.com/@mostlylucid/mermaid-enhancementshttps://cdn.jsdelivr.net/npm/@mostlylucid/mermaid-enhancementsIk heb een minification script toegevoegd om de bundelgrootte te verminderen:
// scripts/minify.js
const { minify } = require('terser');
const fs = require('fs');
const path = require('path');
async function minifyFile(inputPath, outputPath) {
const code = fs.readFileSync(inputPath, 'utf8');
const result = await minify(code, {
compress: {
dead_code: true,
drop_console: false,
drop_debugger: true,
keep_classnames: true,
keep_fnames: true,
},
mangle: {
keep_classnames: true,
keep_fnames: true,
},
format: {
comments: false,
},
});
fs.writeFileSync(outputPath, result.code);
const originalSize = fs.statSync(inputPath).size;
const minifiedSize = fs.statSync(outputPath).size;
const reduction = ((1 - minifiedSize / originalSize) * 100).toFixed(1);
console.log(`✓ ${path.basename(outputPath)}: ${originalSize} → ${minifiedSize} bytes (${reduction}% smaller)`);
}
// Minify main bundle
minifyFile(
path.join(__dirname, '../dist/index.js'),
path.join(__dirname, '../dist/index.min.js')
);
Resultaten:
Ik creëerde uitgebreide documentatie:
De CSS is volledig responsief en ondersteunt de donkere modus:
/* Diagram wrapper */
.mermaid-wrapper {
position: relative;
border-radius: 0.5rem;
overflow: hidden;
width: 100%;
margin: 1rem 0;
}
/* Control buttons */
.mermaid-controls {
position: absolute;
top: 0.5rem;
right: 0.5rem;
display: flex;
gap: 0.25rem;
z-index: 10;
background: rgba(255, 255, 255, 0.9);
border-radius: 0.5rem;
padding: 0.25rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.dark .mermaid-controls {
background: rgba(31, 41, 55, 0.95);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
/* Individual buttons */
.mermaid-control-btn {
padding: 0.5rem;
border-radius: 0.25rem;
cursor: pointer;
transition: all 0.2s;
background: transparent;
border: none;
color: #4b5563;
font-size: 1.25rem;
}
.mermaid-control-btn:hover {
background: rgba(37, 99, 235, 0.1);
color: #2563eb;
transform: scale(1.1);
}
Zelfs voor een kleine bibliotheek, TypeScript gevangen verschillende bugs tijdens de ontwikkeling en biedt uitstekende IDE ondersteuning voor gebruikers.
Het ondersteunen van beide import en require, plus het aanbieden van een verminkte versie, maakt het pakket veelzijdiger:
"exports": {
".": {
"types": "./src/types.ts",
"import": "./src/index.ts",
"require": "./dist/index.js"
},
"./min": {
"import": "./dist/index.min.js"
}
}
De demo pagina hielp me bugs te vangen en dient als levende documentatie. Gebruikers kunnen precies zien hoe het werkt.
Zorg altijd voor opruimfuncties voor geheugenbeheer:
export function cleanupMermaidEnhancements() {
panZoomInstances.forEach((instance, id) => {
try {
instance.destroy();
} catch (e) {
console.warn(`Failed to destroy pan-zoom instance ${id}:`, e);
}
});
panZoomInstances.clear();
}
Verschillende sites behandelen thema's anders, dus ik implementeerde meerdere detectiemethoden:
window.__themeState)localStorage.theme)document.documentElement.classList)prefers-color-scheme)Het pakket is geoptimaliseerd voor prestaties:
requestAnimationFrame voor gladde animatiesGetest en aan het werk:
Ideeën voor toekomstige versies:
Het verpakken van de Mermaid verbeteringen als een npm module was een geweldige leerervaring. Het pakket is nu:
Als je Mermaid.js gebruikt in je projecten, probeer het dan! De interactieve pan/zoom- en exportfuncties maken het werken met complexe diagrammen zoveel beter.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.