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
使用 Pan/#_ @ om & 导出加强美人鱼图
Friday, 07 November 2025
一. 导言导言 导言 导言 导言
npm 软件包可用@: @% 现已在 美人鱼 -增强音量 @ -} a production % 1 check npm profession * MPK2 见 作为 npm 软件包的美人鱼增强 有关如何在您的项目中使用它的细节@. @%
美人鱼是用文字创建图表的绝佳工具 @ , @ , 但默认翻譯可能限制复杂的图表@ MS K1 @ 用户可以 '} 很容易放大以查看大图周围的详细内容 @ I, 或导出用于文档@.{ 。 在此文章中 @ MOSK6 I' 会显示我如何加强这个网站上的美人魚圖与交互式 pançQK8 @ zoom control@ MPK9_ 全屏光箱查看 < k10\ > 和导出功能 @( both PNG 和 SVG 格式@).}
这是生产 - ready , 处理暗色模式的转换 优雅地@,\ 并且抗云雾火箭装载器干扰\ MS K3\ 适应性强
看起来像什么
因此,我们接下来要做的是这个“.”页面上的一个不错的插页 “(”和Popout)美人鱼.js展示了像GitHub'这样的''s sorta 更好“.”意指图多纳什 ' T 使用SCREENS, 但仍然在easuu读@.}

问题
美人鱼图有几种局限性
- 固定规模 - 大图表不是被切断就是缩小以适应
- 无交互活动 -Can'T 放大以查看细节或环绕棋盘
- 无导出 -用户可以 @'}t 为外部用途保存图表
- 流动经验差 - 小屏幕使得复杂的图表无法使用
- 主题转换问题 @ -_ diggrams do'}在光和暗模式之间切换时总是正确重覆@ MS K2 render
解决方案
I've实施了一个全面强化系统, 增加了#:
- 交互式的 Pan/zoom 使用 svg-pan-zoom 库
- 浮动控控按钮 缩放@ / @ off_ MS K1} 重设 < ,} _ span togggle=,}和导出
- 全屏灯框 更好的查看模式
- PNG和SVG出口 功能功能
- Auto - 附加装入 所以整个图表可以通过默认查看可见
- 完整-{width 显示 消除人为尺寸限制
建筑结构概览
解决方案由三个主要部分组成:
graph TB
A[mermaid_theme_switch.js] -->|Initializes| B[Mermaid Diagrams]
B -->|Renders SVG| C[mermaid_enhancements.js]
C -->|Adds| D[Control Buttons]
C -->|Initializes| E[svg-pan-zoom]
D -->|Triggers| F[Pan/Zoom Actions]
D -->|Triggers| G[Export Functions]
D -->|Triggers| H[Fullscreen Lightbox]
执行 执行情况
安装依赖
首次 ,安裝要求的 npm 套件@:
npm install svg-pan-zoom html-to-image
这些图书馆提供#:
svg-pan-zoom用于 SVG 元素的交互式平板和缩放功能html-to-image- 导出 SVG/PNG 功能
核心增强模块
主增强模块 @ (mermaid_enhancements.js@) 处理所有交互式功能@.
创建控制按钮
每个图表都有一个带有所有动作按钮的浮动控制面板@ : @
function createControlButtons(container, diagramId) {
// Check if controls already exist
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);
}
正在初始化 Pan/ @% @ om
svg-pan-zoom 库提供顺畅的@ ,}功能性互动=:
function initPanZoom(svgElement, diagramId) {
// 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, // We use custom controls
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;
}
}
出口功能
出口系统保留图表质量,处理PNG和SVG格式“:”
async function exportDiagram(container, format, diagramId) {
try {
const svgElement = container.querySelector('svg');
if (!svgElement) {
window.showToast && window.showToast('No diagram found to export', 3000, 'error');
return;
}
// Clone the SVG to avoid modifying the original
const clonedSvg = svgElement.cloneNode(true);
// Get the viewBox or calculate from bounding box
let viewBox = clonedSvg.getAttribute('viewBox');
if (!viewBox) {
const bbox = svgElement.getBBox();
viewBox = `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`;
clonedSvg.setAttribute('viewBox', viewBox);
}
// Parse viewBox to get dimensions
const [, , vbWidth, vbHeight] = viewBox.split(' ').map(Number);
// Set explicit dimensions based on viewBox for proper export
clonedSvg.setAttribute('width', vbWidth);
clonedSvg.setAttribute('height', vbHeight);
// Remove inline styles but keep viewBox
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);
let dataUrl;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `mermaid-diagram-${timestamp}`;
if (format === 'png') {
dataUrl = await toPng(clonedSvg, {
backgroundColor: 'white',
pixelRatio: 2 // Higher quality
});
downloadFile(dataUrl, `${filename}.png`);
} else {
dataUrl = await toSvg(clonedSvg, {
backgroundColor: 'transparent'
});
downloadFile(dataUrl, `${filename}.svg`);
}
// Clean up
document.body.removeChild(tempDiv);
window.showToast && window.showToast(`Diagram exported as ${format.toUpperCase()}`, 3000, 'success');
} catch (error) {
console.error('Failed to export diagram:', error);
window.showToast && window.showToast('Failed to export diagram', 3000, 'error');
}
}
关键出口考虑因素 @:
- 保存保护视图框 @- 关键在于捕捉整个图表@,}不只是可见部分
- 计算大小 \ - 明确设置宽度@ MS K1 @ h8, 从视图框中显示, 以一致导出
- 删除变换 - 换成 pan-zoom 变换, 因此导出显示完整的图表
- 背景处理 PNGQ,的白色背景SVG透明
- 高分辨率 - 使用
pixelRatio: 2巴布亚新几内亚出口
全屏灯框
光箱提供沉浸式浏览体验@: @
function openFullscreenLightbox(container, diagramId) {
const svgElement = container.querySelector('svg');
if (!svgElement) return;
// Create lightbox overlay
const lightbox = document.createElement('div');
lightbox.className = 'mermaid-lightbox';
lightbox.innerHTML = `
<div class="mermaid-lightbox-content">
<button class="mermaid-lightbox-close bx bx-x" aria-label="Close"></button>
<div class="mermaid-lightbox-diagram-wrapper">
<div class="mermaid-lightbox-diagram"></div>
</div>
</div>
`;
// Clone and prepare SVG
const clonedSvg = svgElement.cloneNode(true);
clonedSvg.removeAttribute('width');
clonedSvg.removeAttribute('height');
clonedSvg.style.width = '100%';
clonedSvg.style.height = '100%';
const diagramContainer = lightbox.querySelector('.mermaid-lightbox-diagram');
diagramContainer.appendChild(clonedSvg);
// Add controls to lightbox
const wrapper = lightbox.querySelector('.mermaid-lightbox-diagram-wrapper');
const lightboxDiagramId = `${diagramId}-lightbox`;
createControlButtons(wrapper, lightboxDiagramId);
document.body.appendChild(lightbox);
// Initialize pan-zoom after layout completes
setTimeout(() => {
const panZoom = initPanZoom(clonedSvg, lightboxDiagramId);
if (panZoom) {
panZoom.resize();
panZoom.fit();
panZoom.center();
}
}, 100);
// Close handlers
const closeLightbox = () => {
if (panZoomInstances.has(lightboxDiagramId)) {
try {
panZoomInstances.get(lightboxDiagramId).destroy();
} catch (e) {
console.warn('Failed to destroy lightbox pan-zoom:', e);
}
panZoomInstances.delete(lightboxDiagramId);
}
lightbox.remove();
};
lightbox.querySelector('.mermaid-lightbox-close').addEventListener('click', closeLightbox);
lightbox.addEventListener('click', (e) => {
if (e.target === lightbox) closeLightbox();
});
// ESC key to close
const escHandler = (e) => {
if (e.key === 'Escape') {
closeLightbox();
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
}
主增强功能
这把一切连接在一起, 被称作美人鱼完成:
export function enhanceMermaidDiagrams() {
const diagrams = document.querySelectorAll('.mermaid[data-processed="true"]');
diagrams.forEach(diagram => {
const svgElement = diagram.querySelector('svg');
if (!svgElement) return;
// CRITICAL: Remove inline max-width constraint that Mermaid adds
svgElement.style.maxWidth = 'none';
// Wrap diagram with controls
const diagramId = wrapDiagramWithControls(diagram);
// Initialize pan/zoom and auto-fit
const panZoom = initPanZoom(svgElement, diagramId);
if (panZoom) {
// Fit diagram to container by default
setTimeout(() => {
panZoom.resize();
panZoom.fit();
panZoom.center();
}, 100);
}
});
// Set up event delegation for control buttons (only once)
if (!document.body.hasAttribute('data-mermaid-controls-initialized')) {
document.body.addEventListener('click', handleControlClick);
document.body.setAttribute('data-mermaid-controls-initialized', 'true');
}
}
关键修正@: @% 美人鱼适用内线 style="max-width: 1020px" 到 SVG 元素@,, 防止全@ -_width 显示 @. 移除此功能对于正确反应行为来说是不可或缺的 @ MS K3
主题整合
主题切换器确保了在光和暗模式之间切换时的图表重新 - render 正确\ MPK1\
import { enhanceMermaidDiagrams } from './mermaid_enhancements';
const loadMermaid = async (theme) => {
if (!window.mermaid) return;
try {
window.mermaid.initialize({
startOnLoad: false,
theme,
themeVariables: {
background: 'transparent'
}
});
await window.mermaid.run({
querySelector: elementSelector,
});
// Enhance diagrams after rendering completes
// Use requestAnimationFrame for better timing
await new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
enhanceMermaidDiagrams();
resolve();
});
});
});
} catch (err) {
console.error('Mermaid render error:', err);
}
};
使用 requestAnimationFrame 两次确保浏览器完成 SVG 的绘画后, 我们才尝试加强它@. @ @
Cloudflare 火箭装载器兼容性
Cloudflare'火箭装载器可延缓 JavaScript 执行@,破碎初始化 @. 这里='防弹解决方案_:
// Wait for all dependencies to load with exponential backoff
function waitForDependencies(maxAttempts = 50) {
return new Promise((resolve) => {
let attempts = 0;
const checkDependencies = () => {
attempts++;
const depsReady =
typeof window.hljs !== 'undefined' &&
typeof window.mermaid !== 'undefined' &&
typeof window.Alpine !== 'undefined' &&
typeof window.htmx !== 'undefined';
if (depsReady) {
console.log('All dependencies loaded after', attempts, 'attempts');
// Start Alpine.js now that it's loaded
if (window.Alpine && !window.Alpine.version) {
try {
window.Alpine.start();
console.log('Alpine.js started');
} catch (err) {
console.error('Failed to start Alpine:', err);
}
}
resolve();
} else if (attempts >= maxAttempts) {
console.warn('Timeout waiting for dependencies');
resolve(); // Continue anyway
} else {
// Retry with exponential backoff
const delay = Math.min(50 * Math.pow(1.2, attempts), 500);
setTimeout(checkDependencies, delay);
}
};
checkDependencies();
});
}
// Robust initialization
async function safeInitialize() {
try {
await waitForDependencies();
if (document.readyState === 'loading') {
await new Promise(resolve => {
document.addEventListener('DOMContentLoaded', resolve, { once: true });
});
}
await initializePage();
} catch (err) {
console.error('Failed to initialize page:', err);
// Retry once after delay
setTimeout(() => {
initializePage().catch(e => console.error('Retry failed:', e));
}, 1000);
}
}
safeInitialize();
也确保你的主要脚本有 data-cfasync="false" 将它从“火箭加载器”中排除的属性@:
<script src="~/js/dist/main.js" type="module" asp-append-version="true" data-cfasync="false"></script>
与尾风/#DaisyUI的风格
CSS 使用尾风公用事业类和自定义的抛光样式
/* Mermaid diagram wrapper */
.mermaid-wrapper {
@apply relative rounded-lg overflow-hidden w-full;
margin: 1rem 0;
}
.mermaid-wrapper .mermaid {
@apply m-0 w-full;
min-height: 500px;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.mermaid-wrapper .mermaid svg {
width: 100% !important;
height: auto !important;
min-height: 450px;
}
/* Control buttons */
.mermaid-controls {
@apply absolute top-2 right-2 flex gap-1 z-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);
}
.mermaid-control-btn {
@apply p-2 rounded cursor-pointer transition-all duration-200;
background: transparent;
border: none;
color: #4b5563;
font-size: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
}
.mermaid-control-btn:hover {
background: rgba(37, 99, 235, 0.1);
color: #2563eb;
transform: scale(1.1);
}
.dark .mermaid-control-btn {
color: #9ca3af;
}
.dark .mermaid-control-btn:hover {
background: rgba(55, 65, 81, 0.8);
color: #60a5fa;
}
/* Lightbox */
.mermaid-lightbox {
@apply fixed inset-0 z-50 flex items-center justify-center;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(4px);
animation: fadeIn 0.2s ease-out;
}
.dark .mermaid-lightbox {
background: rgba(0, 0, 0, 0.95);
}
.mermaid-lightbox-content {
@apply relative w-11/12 h-5/6 bg-white rounded-lg shadow-2xl;
max-width: 1400px;
}
.dark .mermaid-lightbox-content {
@apply bg-gray-800;
}
.mermaid-lightbox-close {
@apply absolute top-4 right-4 z-10 p-2 rounded-full cursor-pointer transition-all;
background: rgba(0, 0, 0, 0.5);
border: none;
color: white;
font-size: 2rem;
width: 3rem;
height: 3rem;
display: flex;
align-items: center;
justify-content: center;
}
.mermaid-lightbox-close:hover {
background: rgba(220, 38, 38, 0.8);
transform: scale(1.1);
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
测试和调试
这里的' 如何校验一切工作?
控制台输出
使用正确的初始化@ , 您应该看到 @ MS K1
All dependencies loaded after 1 attempts
Alpine.js started
Highlight.js copy plugin registered
Highlight.js initialized on page load
Mermaid initialized on page load
Document is ready - all initializations complete
HTMX event listener registered successfully
HTMX 动态内容
HTMX 互换后 @ , 您应该看到@ MS K1
HTMX afterSettle triggered for: contentcontainer
Highlight.js applied after HTMX swap
Mermaid initialized
Mermaid applied after HTMX swap
HTMX afterSettle complete for: contentcontainer
测试核对清单
- 初始页面负载时提供的图表
- Pan/Zzoom控制工作
- [ @ ]_ 全屏灯光箱打开并关闭@ MS K1}X button *,} 点击外面 @,} ESC 键=)}
- PNG 导出捕捉完整图表 @(}不只是角落@)
- SVG出口保留区矢量
- 在 HTMX 内容互换后起作用
- 主题切换 MS K1 @ renders 图正确
- [ @移动响应@(}控制器保持可见 , 图表缩放 )
- 暗色模式的外观使用正确
- 键盘可访问性 @(}%Tab to control {,} 输入以激活@)}
业绩考量
- 懒惰初始化 - 仅增强页面上的图表
- 清理事件 - 销毁图表被删除时的 pan-çzoom 实例
- 活动代表团 - 单点击收听器处理所有控制按钮
- 动画 与任意设定时间值相比,时机更合适
- 下降的出口出口 @ - @ 防止快速@ MS K1 @ fire 导出点击
浏览器兼容性
测试和研究“:”
- 希罗梅/ 爱奇 90+
- 消防员 @88+#
- 萨法里
- 移动浏览器 @(iOSSafari_,Chrome Moved)
不支持 I11\ 由于现代 JavaScript 特性, @const,}箭头函数 @ , async=/}#await@,}请求動畫Frame @MS K5}
结论结论
此全面增强将静态美人鱼图表转换为互动@,导出可视化 @.。 执行是制作_-,适应边缘情况}{,},并提供了极好的用户体验}.}
密钥取走@: @%
- 删除内嵌的美人鱼@ ' @%s
max-width用于完整@- @width 图的制约 - 导出以捕捉整个图表时保存视图框
- 选择时间,而不是任意延误
- 配有依赖性检查和重试逻辑的Landle Cloudflare 火箭装载器
- 为光箱 @ ( @ button@ MS K1} 提供多重近距离方法, 请在外点击 @ I, ESC @ MPK3}
- 使用包含许多图表的性能授权活动
使用 npm 软件包
您现在可以将此功能安装为 npm 软件包@ :}
npm install @mostlylucid/mermaid-enhancements
import { init } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';
await init();
见见 作为 npm 软件包的美人鱼增强 和高级配置选项@.
完整的源代码也可以在这个博客“ '” 库中找到。 Mostlylucid/src/js/mermaid_enhancements.js 并用作打开@-}源 npm 软件包 美人鱼.
示例图示
这里的'是一个复杂的例子, 显示了这个博客的架构@'}内容系统 @:
graph TB
subgraph Client["Client Browser"]
A[User Request] -->|HTMX| B[Blog Controller]
B -->|Cache Miss| C[Blog Service]
C -->|File Mode| D[Markdown Service]
C -->|DB Mode| E[EF Core Context]
D -->|Parse| F[Markdig Pipeline]
F -->|Render| G[HTML + Mermaid]
E -->|Query| H[PostgreSQL]
H -->|Full-Text Search| I[GIN Index]
G -->|Enhance| J[mermaid_enhancements.js]
J -->|Initialize| K[svg-pan-zoom]
J -->|Add| L[Control Buttons]
L -->|Export| M[html-to-image]
end
subgraph Background["Background Services"]
N[File Watcher] -->|Change Detected| O[Saves to DB]
O -->|Trigger| P[Translation Service]
P -->|Batch| Q[EasyNMT API]
Q -->|12 Languages| R[Translated Files]
end
style A stroke:#22c55e,stroke-width:3px,color:#4ade80
style G stroke:#3b82f6,stroke-width:3px,color:#60a5fa
style J stroke:#f59e0b,stroke-width:3px,color:#f59e0b
style K stroke:#ec4899,stroke-width:3px,color:#ec4899
style M stroke:#8b5cf6,stroke-width:3px,color:#8b5cf6
尝试单击上方图表的控件@! @%