注意:这是我在AI的实验的一部分 / 一种花费1000美元 Clude 代码 Web 信用的方法。 我给这个文件,我的理解,我不得不提出这样的问题。这很有趣,填补了一个我还没有看到的地方填补的空白。
该员额以以前各条为基础: 如果你还没有,你看看 添加美人鱼。 js 与 htmx, 为美人鱼切换主题, 和 使用 Pan/Zoom 和 导出加强美人鱼图这种深度的下潜解释这些执行背后的内在原因。
Mermaid.js 是真正的天才。 写入简单的文字, 获得美丽的图表。 不再用 Visio 或 draw. io 来折腾 Visio 或 draw. io , 丢失源文件, 或者保留单独的图像文件 。 所有的东西都生活在标记下, 版本控制与您的代码并存 。
但我想知道 如何如何 它实际上在引擎盖下工作。
graph LR
A[Text] --> B[Magic?]
B --> C[Beautiful Diagram]
...成为一个真实的SVG?更重要的是,你怎样才能勾上它来添加一些功能,例如Pan/zoom,主题转换, 以及我为这个网站建立的输出功能(现在可用) @ mostlylucid/ permaid- envenancements @ 最优优/ 美人鱼增强)?
经过大量调查 美人鱼的源代码 和构建真正的扩展, 这是我所学到的一切 关于美人鱼的内部工作方式 以及如何适当扩展它。
美人鱼将文本定义转换为图表。 想象一下“ 图表的 Markdown ” 。
旧的方式:
美人鱼的方式:
更新? 编辑文本。 版本控制? 它只是文本! 在标记中有用吗?
美人鱼支持数量荒谬的图表类型:
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...]
见见 美人鱼博士 全部名单。
美人鱼提供图表时会发生什么:
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
让我们打破每一步。
一切从文字开始。 您以美人鱼 DSL (域名语言) 写入图表 :
// Flowchart
const diagram = `
graph TD
A[Start] --> B{Is it working?}
B -->|Yes| C[Great!]
B -->|No| D[Debug time]
`;
名词典将文本拆分为符号。 例如, 此行 :
A[Start] --> B{Decision}
成为像 :
[
{ 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 { }
]
解析器消耗象征物并构建一个抽象的语法树( 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' }
]
}
美人鱼对每一图表类型使用不同的解析器。 这些解析器通常是用语法文件生成的。 Jison 联合 (如JavaScript的Yacc/Bison)。
美人鱼检测第一行的图表类型 :
// 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
每个图表类型都有自己的生成器。 生成器使用 AST 并生成 SVG 元素 。
对于流程图,美人鱼使用 达格 用于图形布局的库。 对于其他的库, 它使用自定义算法或像 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);
};
制造者生产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>
美人鱼找到所有的东西 .mermaid 将其替换为已设定的 SVG :
// 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;
}
};
在美人鱼插入 SVG 后, 您可以加强它。 这是我所有的增强钩 :
下文将进一步阐述这方面的情况。
现在我们知道美人鱼是如何工作的了, 让我们来探索如何扩展它。
最基本的扩展名为配置 :
import mermaid from 'mermaid';
mermaid.initialize({
startOnLoad: true,
theme: 'dark',
securityLevel: 'loose',
flowchart: {
curve: 'basis',
padding: 15
}
});
我广泛报道了 为美人鱼切换主题,但这里是关键执行:
美人鱼需要以主题初始化, 之后无法更改它。 但是, 如果您想要用新主题重新生成图表, 您需要原始的图表源 — — 哪个美人鱼 。 不存储在DOM.
在转换主题前存储原始内容, 然后恢复并重新发送 :
// 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;
}
}
};
多主题检测方法 (网站处理主题不同):
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';
}
见见 整个主题切换码 详细信息。
这就是真正的魔法发生的地方。在美人鱼投产后,你可以添加互动功能。
我广泛报道了 使用 Pan/Zoom 和 导出加强美人鱼图所以我要在此强调关键技术
创建用于控制的包装容器 :
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;
}
使用 svg-pan-zoom (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;
}
创建浮动控制面板 :
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);
}
不要给每个按钮附加听众。使用事件授权 :
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
}
});
挑战: SVG 元素具有动态缩放、泛形/声波变换和继承风格。 要正确导出, 您需要 :
使用 html到图像:
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);
}
关键细节:
见见 完整导出代码 以获取更多细节。
我将所有这些增强功能包装为 @ mostlylucid/ permaid- envenancements @ 最优优/ 美人鱼增强见 作为 npm 软件包的美人鱼增强 用于完整细节。
以下是一切合而为一:
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
npm install @mostlylucid/mermaid-enhancements
import { init } from '@mostlylucid/mermaid-enhancements';
import '@mostlylucid/mermaid-enhancements/styles.css';
await init();
就是这样,你的美人鱼图现在有:
正如我所覆盖的 添加美人鱼。 js 与 htmxHTMX 内容互换后, 您需要重新激活美人鱼 :
// On page load
document.addEventListener('DOMContentLoaded', function () {
mermaid.initialize({ startOnLoad: true });
});
// After HTMX swaps content
document.body.addEventListener('htmx:afterSwap', function(evt) {
mermaid.run();
});
结合强化一揽子计划:
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
});
在建了这个东西 并调试了奇异的边缘案例之后, 这里的原理是:
美人鱼不会保存原始图表来源。
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);
内存泄漏是真实的。 在创建新事件之前销毁实例 :
if (panZoomInstances.has(id)) {
try {
panZoomInstances.get(id).destroy();
} catch (e) {
console.warn('Failed to destroy:', e);
}
panZoomInstances.delete(id);
}
不要将收听者附加在单按钮上 :
// ❌ 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);
}
});
火箭加载器延迟 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();
});
}
将您的主要脚本从火箭加载器中排除 :
<script src="main.js" data-cfasync="false"></script>
使用使用 requestAnimationFrame B. 时间比任意选择更合适 setTimeout:
// After Mermaid renders
await mermaid.run();
// Wait for paint before enhancing
await new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
enhanceMermaidDiagrams();
resolve();
});
});
});
SVGs可能很奇怪,总是检查:
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}`;
}
适当初始化后,您应该看到:
Saving original data
Loading mermaid with theme: dark
Mermaid initialized
Enhanced 3 diagrams
实施增强后:
图表未创建 :
window.mermaid)不起作用的Panzoom:
pointer-events: none)只抓取导出角 :
主题未切换 :
data-processed 未重置需要之前不要加载增强装置 :
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 });
仅在可见时才显示图像图 :
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);
});
更改大小或主题更改时 :
let timeout;
window.addEventListener('resize', () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
panZoomInstances.forEach(instance => {
instance.resize();
instance.fit();
});
}, 250);
});
美人鱼.js是绝妙的盒子外, 但了解内部如何运作, 让你建立一些非常酷的增强。 关键洞察力 :
requestAnimationFrame我在这里所覆盖的所有技术 都用于这个网站的制作和包装 @ mostlylucid/ permaid- envenancements @ 最优优/ 美人鱼增强的完整来源。 多数是网/多数是网/大多数是美人鱼.
尝试上面图表的控件 !
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.