J’ai publié un paquet NPM!!! Publication des améliorations Mermaid en tant que paquet npm
Introduction
J’ai enfin pris le courage de plonger mes doigts dans le monde des paquets npm. Mermaid.js est assez obscur et assez étrange pour pouvoir livrer quelque chose utile.
Après avoir construit quelques améliorations vraiment utiles pour Mermaid.js diagrammes (interactive panMSC2zoomM SK3 fullscreen lightboxMST4 exporter à PNG MST5SVGM ST6 et changer automatiquement le thèmeMst7 J’ai décidé qu’il était temps de les emballer correctement et de les partager avec la communautéM st8 Ce post traite de la façon dont j’ai créé @mostlylucid/mermaid-enhancements en tant que production-paquet ready npmM SK1
REMARQUE: Travailler encore sur la version M SK1 Soyez à l'écoute ( mon premier paquet npm prend un peu de temps)
Pourquoi Package It?
J’ai déjà utilisé ces améliorations dans mon blog depuis quelque temps, , et elles sont devenues essentielles pour travailler avec des diagrammes complexes de Mermaid.
- Pan interactif & Zoom - Naviger facilement les grands diagrammes
- Case lumineuse à écran complet - Expérience de vision immersive
- Exporter à PNG/SVG - HighM SK1Décharges de diagrammes de qualité
- Changement de thème automatique - Lampe sans fil/Support en mode noir
- Conception réactive - Fonctionne sur le mobile et le bureau
Puisque je copie le même code entre les projets,, il est logique de créer un paquet npm propre que tout le monde puisse utiliser.
Structure du projet
I set up a professional package structure with TypeScript support:
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
Le paquet utilise TypeScript pour la sécurité de type et une meilleure expérience des développeurs, mais compile à JavaScript pour une compatibilité maximale.
L'architecture
Voici' comment les composants passent ensemble:
C'est ce qu'il ressemble à!
Donc, vous le voyez' est assez compact et a quelques fonctionnalités utiles au-delà des diagrammes statiques seulement . Il m’a toujours annoyé comment MASSIVE ils étaient dans la page de sorte que cela semblait une approche sensible pour réduire la taille tout en maintenant l’utilité
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
Mise en œuvre de base
Définitions du type
Première, J’ai défini des types de typescript complets:
// 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;
};
}
Point d'entrée principal
Le point d’entrée principal est mort simple:
// 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,
};
Mise en œuvre de Pan/Zoom
La logique d'amélioration emballe chaque diagramme avec des contrôles et initialise svg-panM SK1zoom:
// 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;
}
}
Les boutons de commande sont créés dynamiquement:
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);
}
Fonctionnalité d'exportation
La mise en œuvre de l'exportation clone le SVG, préserve la viewBox, et utilise htmlM SK2toMSC3image :
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);
}
}
Changement de thème
Le commutateur thématique gère plusieurs méthodes de détection:
// 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');
}
Configuration du paquet
package.json
Le paquet.json définit des points d'entrée multiples pour différents usages.
{
"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"
}
}
Configuration de TypeScript
{
"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"]
}
Exemples d'utilisation
Usage de base
La façon la plus simple d'utiliser le paquet:
import mermaid from 'mermaid';
import { init } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';
await init();
Avec le changement de thème
Pour les sites lumineux/mode foncé:
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);
}
Intégration réactive
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>
);
}
Intégration Vue
<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>
La page de démonstration
J’ai créé une page de démonstration complète montrant toutes les fonctionnalités:
<!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>
Processus d'édition
Voici le processus de travail d’édition'
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
Step-par-Step
- Construire le paquet:
npm run build:all # Compiles TypeScript and minifies
- Tester localement:
npm run dev # Opens demo at localhost:3000
- Version bump:
npm version patch # or minor, or major
- Publier:
npm login
npm publish --access public
Le paquet est maintenant disponible via:
- npm:
npm install @mostlylucid/mermaid-enhancements - unpkg CDN:
https://unpkg.com/@mostlylucid/mermaid-enhancements - jsdelivr CDN:
https://cdn.jsdelivr.net/npm/@mostlylucid/mermaid-enhancements
Stratégie de minification
J’ai ajouté un script de minification pour réduire la taille du paquet:
// 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')
);
Résultats:
- Normale: M SK1 KB
- Réduction minée: M SK1 Kb (58.3%
Documentation
J’ai créé une documentation complète:
- README.md - Référence complète de l'APIM SK1 exemples, troubleshooting
- ACCÈS RAPIDE.md - 5-Guide du début d'une minute
- PUBLIQUE.md - Instructions de publication npm
- CHANGELOG.md - Historique de la version
Styling
Le CSS est pleinement réceptif et appuie le mode noir:
/* 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);
}
Les leçons apprises
1. TypeScript vaut la peine
Même pour une petite bibliothèque, TypeScript a détecté plusieurs erreurs au cours du développement et offre un excellent support IDE aux utilisateurs.
2. Importance des points d’entrée multiples
Soutenir les deux import et require, plus l'offre d'une version minifiée, rend le paquet plus polyvalentM SK2
"exports": {
".": {
"types": "./src/types.ts",
"import": "./src/index.ts",
"require": "./dist/index.js"
},
"./min": {
"import": "./dist/index.min.js"
}
}
3. La démonstration est essentielle
La page de démonstration m'a aidé à détecter les bugs et sert de documentation vivante. Les utilisateurs peuvent voir exactement comment il fonctionne.
4. Le nettoyage est important
Fournir toujours des fonctions de nettoyage pour la gestion de la mémoire:
export function cleanupMermaidEnhancements() {
panZoomInstances.forEach((instance, id) => {
try {
instance.destroy();
} catch (e) {
console.warn(`Failed to destroy pan-zoom instance ${id}:`, e);
}
});
panZoomInstances.clear();
}
5. La détection du thème nécessite des reculs
Différents sites traitent les thèmes différemment, donc j’ai mis en œuvre plusieurs méthodes de détection:
- État mondial (
window.__themeState) - LocalStorage (
localStorage.theme) - Classe DOM (
document.documentElement.classList) - Préférence OS (
prefers-color-scheme)
Considérations relatives au rendement
Le paquet est optimisé pour la performance:
- Délégation des événements - Lecteur d'événement unique pour tous les boutons de commande
- Initialisation lente - PanM SK1zoom initialisé seulement lorsque nécessaire
- Nettoyage de l'instance - Gestion de la mémoire appropriée
- Re-renders minimaux - Diagrammes seulement re-render lors du changement de thème
- Opérations DOM efficaces - Utilisations
requestAnimationFramepour les animations douces
Soutien du navigateur
Testé et travaillé sur:
- Chrome/Edge M SK1latest)
- Firefox (latest)
- Safari (latestM SK1
- Les navigateurs mobiles (iOS SafariM SK1 Chrome Mobile)
Améliorations à venir
Ideas pour les versions futures:
- Iconnes de boutons de commande configurables (Support pour Font Awesome, Icons du matérielM SK3
- Shortcuts de la claviere (e.gM SK3 CtrlMSC4Plus pour zoomerMST5
- Gestes de touche sur le mobile (pinch to zoom)
- Copier le bouton de code source du diagramme
- Caractéristique de partage du diagramme URL
- Annotations au diagramme
- Formats d'exportation multiples (PDFM SK2 JPEG)
Conclusion
L’emballage des améliorations Mermaid en tant que module npm a été une grande expérience d’apprentissage. Le paquet est maintenant
- Type-safe avec TypeScript
- Well-documenté
- Facile à utiliser
- Production-réaliste
- Cadre-agnostique
- Mise à l'essai
Si vous utilisez Mermaid, .,js dans vos projets et ,, essayez de le faire.
Ressources
- GitHub Repository: Web plus lumineux/plus lumineux
- Package npm: @mostlylucid/mermaidM SK2améliorations (viendra bientôtM SK1
- Démonstration: Démonstration en direct (viendra bientôtM SK1