Minä..net-tyyppi nappasi vihdoin rohkeuden upottaa varpaani npm-pakettien maailmaan! Merenneito.js on sen verran hämärä ja outo, että voisin itse asiassa tuoda jotain hyödyllistä! Joten nauti.
Sen jälkeen, kun olen rakentanut todella hyödyllisiä parannuksia Mermaid.js-kaavioihin (interaktiivinen pan/zoom, kokonäytön valolaatikko, vienti PNG/SVG:hen ja automaattinen teemavaihto), päätin, että on aika pakata ne kunnolla ja jakaa ne yhteisön kanssa. Tämä viesti kävelee läpi siitä, miten loin. @mostlylucid/mermaid-enhancements tuotantovalmiina npm-pakettina.
HUOMAUTUS: Työstän edelleen julkaisua. Pysy kanavalla (ensimmäinen npm-pakettini niin ottaa vähän)
Olen käyttänyt näitä parannuksia blogissani jo jonkin aikaa, ja niistä on tullut välttämättömiä monimutkaisten merenneitokaavioiden kanssa työskentelyssä. Ominaisuuksiin kuuluu:
Koska kopioin samaa koodia projektien välillä, oli järkevää luoda kunnon npm-paketti, jota kuka tahansa voisi käyttää.
Perustin ammattimaisen pakettirakenteen, jossa on TypeScript-tuki:
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
Paketti käyttää TypeScriptiä tyyppiturvaan ja parempaan kehittäjäkokemukseen, mutta koostaa JavaScriptiin maksimaalisen yhteensopivuuden.
Näin osat sopivat yhteen:
Joten näet, että se on aika kompakti ja siinä on jotain hyödyllistä toiminnallisuutta paitsi vain staattisia kaavioita. Se aina ärsytti minua, kuinka MASSIVE ne olivat sivulla, joten tämä tuntui järkevältä lähestymistavalta koon muuttamiseen säilyttäen samalla hyödyllisyyden.
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
Ensin määritin kattavat TypeScript-tyypit:
// 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;
};
}
Pääsisäänkäynti on täysin yksinkertainen:
// 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,
};
Parannuslogiikka käärii jokaisen kaavion hallintalaitteilla ja alustaa svg-pan-zoomin:
// 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;
}
}
Ohjauspainikkeet luodaan dynaamisesti:
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);
}
Viennin toteutus kloonaa SVG:n, säilyttää viewBoxin ja käyttää html-to-imagea:
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);
}
}
Teemanvaihtaja käsittelee useita havaintomenetelmiä:
// 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');
}
Paketti.json määrittelee useita hakupisteitä eri käyttötapauksiin:
{
"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"]
}
Yksinkertaisin tapa käyttää pakettia:
import mermaid from 'mermaid';
import { init } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';
await init();
Kohteet, joissa on valo/pimeä tila:
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>
Loin kattavan demosivun, jossa näkyvät kaikki ominaisuudet:
<!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>
Tässä on julkaisutyönkulku:
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
Paketti on nyt saatavilla seuraavilla tavoilla:
npm install @mostlylucid/mermaid-enhancementshttps://unpkg.com/@mostlylucid/mermaid-enhancementshttps://cdn.jsdelivr.net/npm/@mostlylucid/mermaid-enhancementsLisäsin nippukokoa pienentävän pienennysskriptin:
// 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')
);
Tulokset:
Laadin kattavat asiakirjat:
CSS on täysin reagoiva ja tukee tummaa tilaa:
/* 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);
}
Pienellekin kirjastolle TypeScript kiinnitti useita vikoja kehityksen aikana ja tarjoaa erinomaista IDE-tukea käyttäjille.
Kummankin tukeminen import sekä require, sekä minimaalinen versio, tekee paketista monipuolisemman:
"exports": {
".": {
"types": "./src/types.ts",
"import": "./src/index.ts",
"require": "./dist/index.js"
},
"./min": {
"import": "./dist/index.min.js"
}
}
Demosivu auttoi minua nappaamaan vikoja ja toimii elävänä dokumentointina. Käyttäjät näkevät tarkasti, miten se toimii.
Toimita aina muistinhallintaan puhdistustoimintoja:
export function cleanupMermaidEnhancements() {
panZoomInstances.forEach((instance, id) => {
try {
instance.destroy();
} catch (e) {
console.warn(`Failed to destroy pan-zoom instance ${id}:`, e);
}
});
panZoomInstances.clear();
}
Eri sivustot käsittelevät teemoja eri tavalla, joten käytin useita havaintomenetelmiä:
window.__themeState)localStorage.theme)document.documentElement.classList)prefers-color-scheme)Paketti on optimoitu suorituskykyyn:
requestAnimationFrame Sileisiin animaatioihinTestattu ja työn alla:
Tulevien versioiden ideat:
Merenneidon lisälaitteiden pakkaaminen npm-moduuliksi oli hieno oppimiskokemus. Paketti on nyt:
Jos käytät Mermaid.js:ia projekteissasi, kokeile! Vuorovaikutteiset pan/zoom- ja vientiominaisuudet tekevät monimutkaisten kaavioiden kanssa työskentelystä paljon parempaa.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.