Files
argon-theme/.kiro/specs/mermaid-codeblock-magic/design.md

791 lines
19 KiB
Markdown
Raw Normal View History

# Design Document: Mermaid 代码块渲染修复
## Overview
本设计旨在修复 Argon WordPress 主题中 Mermaid 图表渲染的关键问题。当前实现存在以下核心问题:
1. **PJAX 加载后显示原始文本**:页面切换后 Mermaid 不渲染
2. **语法解析错误**flowchart、erDiagram 等语法解析失败
3. **功能缺失**:缺少导出、全屏查看等功能
4. **交互体验差**:工具栏遮挡内容、缩放不流畅
设计方案采用模块化架构,确保代码质量和可维护性。
## Architecture
### 核心架构原则
1. **模块化设计**:将功能拆分为独立模块,易于维护和扩展
2. **错误隔离**:每个模块独立的错误处理,互不影响
3. **性能优先**:使用批量渲染、延迟加载等优化技术
4. **用户体验**:提供流畅的交互和友好的错误提示
### 架构图
```
Mermaid 渲染系统
├── 检测模块 (MermaidDetector)
│ ├── 扫描代码块
│ ├── 识别 Mermaid 语法
│ └── 过滤已渲染的块
├── 渲染模块 (MermaidRenderer)
│ ├── 初始化 Mermaid
│ ├── 批量渲染
│ ├── 错误处理
│ └── 降级方案
├── 交互模块 (MermaidInteraction)
│ ├── 工具栏管理
│ ├── 缩放控制
│ ├── 拖拽控制
│ └── 全屏查看
├── 导出模块 (MermaidExporter)
│ ├── PNG 导出
│ ├── SVG 导出
│ └── 错误处理
└── 生命周期模块 (MermaidLifecycle)
├── PJAX 集成
├── 资源清理
└── 主题同步
```
## Components and Interfaces
### 1. Mermaid Detector (检测模块)
**职责**:检测页面中的 Mermaid 代码块
**接口**
```javascript
/**
* 检测页面中的 Mermaid 代码块
* @returns {HTMLElement[]} Mermaid 代码块数组
*/
function detectMermaidBlocks() {
const blocks = [];
// 检测 <pre><code class="language-mermaid">
document.querySelectorAll('pre code.language-mermaid').forEach(code => {
if (!isRendered(code)) {
blocks.push(code);
}
});
// 检测 <pre><code class="mermaid">
document.querySelectorAll('pre code.mermaid').forEach(code => {
if (!isRendered(code)) {
blocks.push(code);
}
});
return blocks;
}
/**
* 检查代码块是否已渲染
* @param {HTMLElement} element - 代码块元素
* @returns {boolean} 是否已渲染
*/
function isRendered(element) {
return element.hasAttribute('data-mermaid-rendered') ||
element.closest('.mermaid-container') !== null;
}
```
### 2. Mermaid Renderer (渲染模块)
**职责**:渲染 Mermaid 图表
**接口**
```javascript
/**
* 渲染所有 Mermaid 图表
* @returns {Promise<void>}
*/
async function renderAllMermaidCharts() {
const blocks = detectMermaidBlocks();
if (blocks.length === 0) return;
// 等待 Mermaid 库加载
if (!await waitForMermaid()) {
ArgonDebug.error('[Argon Mermaid] Mermaid 库加载失败');
return;
}
// 初始化 Mermaid
if (!initMermaid()) {
ArgonDebug.error('[Argon Mermaid] Mermaid 初始化失败');
return;
}
// 批量渲染
for (let i = 0; i < blocks.length; i++) {
await renderMermaidChart(blocks[i], i);
}
}
/**
* 等待 Mermaid 库加载
* @param {number} timeout - 超时时间(毫秒)
* @returns {Promise<boolean>} 是否加载成功
*/
function waitForMermaid(timeout = 5000) {
return new Promise((resolve) => {
if (typeof window.mermaid !== 'undefined') {
resolve(true);
return;
}
const startTime = Date.now();
const checkInterval = setInterval(() => {
if (typeof window.mermaid !== 'undefined') {
clearInterval(checkInterval);
resolve(true);
} else if (Date.now() - startTime > timeout) {
clearInterval(checkInterval);
resolve(false);
}
}, 100);
});
}
/**
* 初始化 Mermaid
* @returns {boolean} 是否初始化成功
*/
function initMermaid() {
if (typeof window.mermaid === 'undefined') {
return false;
}
try {
const theme = getMermaidTheme();
window.mermaid.initialize({
startOnLoad: false,
theme: theme,
securityLevel: 'loose',
flowchart: {
useMaxWidth: true,
htmlLabels: true
}
});
return true;
} catch (e) {
ArgonDebug.error('[Argon Mermaid] 初始化失败:', e);
return false;
}
}
/**
* 渲染单个图表
* @param {HTMLElement} element - 代码块元素
* @param {number} index - 图表索引
* @returns {Promise<void>}
*/
async function renderMermaidChart(element, index) {
const chartId = `mermaid-chart-${Date.now()}-${index}`;
const code = element.textContent.trim();
ArgonDebug.log(`[Argon Mermaid] 开始渲染: ${chartId}`);
try {
// 使用 mermaid.render API
const result = await window.mermaid.render(`mermaid-svg-${chartId}`, code);
// 创建容器
const container = createMermaidContainer(chartId, result.svg, code);
// 替换原始代码块
element.closest('pre').replaceWith(container);
// 标记已渲染
element.setAttribute('data-mermaid-rendered', 'true');
ArgonDebug.log(`[Argon Mermaid] 渲染成功: ${chartId}`);
} catch (error) {
ArgonDebug.error(`[Argon Mermaid] 渲染失败: ${chartId}`, error);
// 显示错误
showMermaidError(element, error, code);
}
}
```
### 3. Mermaid Interaction (交互模块)
**职责**:管理图表交互功能
**接口**
```javascript
/**
* 创建 Mermaid 容器
* @param {string} chartId - 图表 ID
* @param {string} svg - SVG 代码
* @param {string} code - 原始代码
* @returns {HTMLElement} 容器元素
*/
function createMermaidContainer(chartId, svg, code) {
const container = document.createElement('div');
container.className = 'mermaid-container';
container.setAttribute('data-chart-id', chartId);
container.setAttribute('data-original-code', code);
// 创建图表包装器
const wrapper = document.createElement('div');
wrapper.className = 'mermaid-wrapper';
wrapper.innerHTML = svg;
// 创建工具栏
const toolbar = createMermaidToolbar(chartId);
container.appendChild(toolbar);
container.appendChild(wrapper);
// 绑定交互事件
bindMermaidInteraction(container);
return container;
}
/**
* 创建工具栏
* @param {string} chartId - 图表 ID
* @returns {HTMLElement} 工具栏元素
*/
function createMermaidToolbar(chartId) {
const toolbar = document.createElement('div');
toolbar.className = 'mermaid-toolbar';
toolbar.innerHTML = `
<button class="mermaid-btn mermaid-zoom-in" title="放大">
<i class="fa fa-search-plus"></i>
</button>
<button class="mermaid-btn mermaid-zoom-out" title="缩小">
<i class="fa fa-search-minus"></i>
</button>
<button class="mermaid-btn mermaid-reset" title="重置">
<i class="fa fa-refresh"></i>
</button>
<button class="mermaid-btn mermaid-fullscreen" title="全屏">
<i class="fa fa-expand"></i>
</button>
<button class="mermaid-btn mermaid-export" title="导出">
<i class="fa fa-download"></i>
</button>
`;
return toolbar;
}
/**
* 绑定交互事件
* @param {HTMLElement} container - 容器元素
* @returns {void}
*/
function bindMermaidInteraction(container) {
const wrapper = container.querySelector('.mermaid-wrapper');
const svg = wrapper.querySelector('svg');
let scale = 1;
let translateX = 0;
let translateY = 0;
let isDragging = false;
let startX = 0;
let startY = 0;
// 缩放功能
container.querySelector('.mermaid-zoom-in').addEventListener('click', () => {
scale = Math.min(scale * 1.2, 5);
updateTransform();
});
container.querySelector('.mermaid-zoom-out').addEventListener('click', () => {
scale = Math.max(scale / 1.2, 0.5);
updateTransform();
});
container.querySelector('.mermaid-reset').addEventListener('click', () => {
scale = 1;
translateX = 0;
translateY = 0;
updateTransform();
});
// 鼠标滚轮缩放
wrapper.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.9 : 1.1;
scale = Math.max(0.5, Math.min(5, scale * delta));
updateTransform();
}, { passive: false });
// 拖拽功能
wrapper.addEventListener('mousedown', (e) => {
if (scale > 1) {
isDragging = true;
startX = e.clientX - translateX;
startY = e.clientY - translateY;
wrapper.style.cursor = 'grabbing';
}
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
translateX = e.clientX - startX;
translateY = e.clientY - startY;
updateTransform();
}
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
wrapper.style.cursor = scale > 1 ? 'grab' : 'default';
}
});
// 更新变换
function updateTransform() {
svg.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
svg.style.transformOrigin = 'center';
svg.style.transition = 'transform 0.2s ease';
wrapper.style.cursor = scale > 1 ? 'grab' : 'default';
}
// 全屏查看
container.querySelector('.mermaid-fullscreen').addEventListener('click', () => {
openMermaidFullscreen(container);
});
// 导出功能
container.querySelector('.mermaid-export').addEventListener('click', () => {
showExportMenu(container);
});
}
```
### 4. Mermaid Exporter (导出模块)
**职责**:导出图表为图片文件
**接口**
```javascript
/**
* 显示导出菜单
* @param {HTMLElement} container - 容器元素
* @returns {void}
*/
function showExportMenu(container) {
const menu = document.createElement('div');
menu.className = 'mermaid-export-menu';
menu.innerHTML = `
<button class="export-png">导出 PNG</button>
<button class="export-svg">导出 SVG</button>
`;
menu.querySelector('.export-png').addEventListener('click', () => {
exportMermaidAsPNG(container);
menu.remove();
});
menu.querySelector('.export-svg').addEventListener('click', () => {
exportMermaidAsSVG(container);
menu.remove();
});
container.appendChild(menu);
// 点击外部关闭菜单
setTimeout(() => {
document.addEventListener('click', function closeMenu(e) {
if (!menu.contains(e.target)) {
menu.remove();
document.removeEventListener('click', closeMenu);
}
});
}, 0);
}
/**
* 导出为 PNG
* @param {HTMLElement} container - 容器元素
* @returns {void}
*/
function exportMermaidAsPNG(container) {
const svg = container.querySelector('svg');
const chartId = container.getAttribute('data-chart-id');
try {
// 创建 canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 获取 SVG 尺寸
const bbox = svg.getBBox();
canvas.width = bbox.width;
canvas.height = bbox.height;
// 将 SVG 转换为图片
const svgData = new XMLSerializer().serializeToString(svg);
const img = new Image();
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
img.onload = () => {
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(url);
// 下载
canvas.toBlob((blob) => {
const link = document.createElement('a');
link.download = `${chartId}.png`;
link.href = URL.createObjectURL(blob);
link.click();
URL.revokeObjectURL(link.href);
});
};
img.src = url;
} catch (error) {
ArgonDebug.error('[Argon Mermaid] PNG 导出失败:', error);
alert('导出失败,请重试');
}
}
/**
* 导出为 SVG
* @param {HTMLElement} container - 容器元素
* @returns {void}
*/
function exportMermaidAsSVG(container) {
const svg = container.querySelector('svg');
const chartId = container.getAttribute('data-chart-id');
try {
const svgData = new XMLSerializer().serializeToString(svg);
const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = `${chartId}.svg`;
link.href = url;
link.click();
URL.revokeObjectURL(url);
} catch (error) {
ArgonDebug.error('[Argon Mermaid] SVG 导出失败:', error);
alert('导出失败,请重试');
}
}
```
### 5. Mermaid Lifecycle (生命周期模块)
**职责**:管理 Mermaid 的生命周期
**接口**
```javascript
/**
* 清理 Mermaid 实例
* @returns {void}
*/
function cleanupMermaidInstances() {
// 移除所有容器
document.querySelectorAll('.mermaid-container').forEach(container => {
// 移除事件监听器
const toolbar = container.querySelector('.mermaid-toolbar');
if (toolbar) {
toolbar.querySelectorAll('button').forEach(btn => {
btn.replaceWith(btn.cloneNode(true));
});
}
// 移除容器
container.remove();
});
ArgonDebug.log('[Argon Mermaid] 实例已清理');
}
/**
* PJAX 集成
* @returns {void}
*/
function initMermaidPjaxIntegration() {
// PJAX 开始前清理
$(document).on('pjax:beforeReplace', function() {
cleanupMermaidInstances();
});
// PJAX 完成后渲染
$(document).on('pjax:complete', function() {
renderAllMermaidCharts();
});
}
/**
* 主题同步
* @returns {void}
*/
function syncMermaidTheme() {
const isDark = document.documentElement.classList.contains('darkmode');
const theme = isDark ? 'dark' : 'default';
if (typeof window.mermaid !== 'undefined') {
window.mermaid.initialize({
theme: theme
});
// 重新渲染所有图表
renderAllMermaidCharts();
}
}
/**
* 获取 Mermaid 主题
* @returns {string} 主题名称
*/
function getMermaidTheme() {
const isDark = document.documentElement.classList.contains('darkmode');
return isDark ? 'dark' : 'default';
}
```
### 6. Error Handler (错误处理模块)
**职责**:处理渲染错误
**接口**
```javascript
/**
* 显示 Mermaid 错误
* @param {HTMLElement} element - 代码块元素
* @param {Error} error - 错误对象
* @param {string} code - 原始代码
* @returns {void}
*/
function showMermaidError(element, error, code) {
const errorContainer = document.createElement('div');
errorContainer.className = 'mermaid-error';
errorContainer.innerHTML = `
<div class="mermaid-error-header">
<i class="fa fa-exclamation-triangle"></i>
<span>Mermaid 图表渲染失败</span>
</div>
<div class="mermaid-error-message">
${error.message || '未知错误'}
</div>
<details class="mermaid-error-details">
<summary>查看原始代码</summary>
<pre><code>${escapeHtml(code)}</code></pre>
</details>
`;
element.closest('pre').replaceWith(errorContainer);
}
/**
* 转义 HTML
* @param {string} html - HTML 字符串
* @returns {string} 转义后的字符串
*/
function escapeHtml(html) {
const div = document.createElement('div');
div.textContent = html;
return div.innerHTML;
}
```
## Data Models
### MermaidChart (图表数据模型)
```javascript
/**
* Mermaid 图表数据模型
*/
class MermaidChart {
constructor(id, code, element) {
this.id = id; // 图表 ID
this.code = code; // 原始代码
this.element = element; // DOM 元素
this.rendered = false; // 是否已渲染
this.scale = 1; // 缩放级别
this.translateX = 0; // X 轴偏移
this.translateY = 0; // Y 轴偏移
this.error = null; // 错误信息
}
/**
* 标记为已渲染
*/
markRendered() {
this.rendered = true;
this.element.setAttribute('data-mermaid-rendered', 'true');
}
/**
* 设置错误
*/
setError(error) {
this.error = error;
}
/**
* 重置变换
*/
resetTransform() {
this.scale = 1;
this.translateX = 0;
this.translateY = 0;
}
}
```
### MermaidConfig (配置模型)
```javascript
/**
* Mermaid 配置模型
*/
class MermaidConfig {
constructor() {
this.theme = 'default'; // 主题
this.startOnLoad = false; // 自动加载
this.securityLevel = 'loose'; // 安全级别
this.flowchart = {
useMaxWidth: true,
htmlLabels: true
};
}
/**
* 从全局配置加载
*/
loadFromGlobal() {
this.theme = getMermaidTheme();
}
/**
* 转换为 Mermaid 配置对象
*/
toMermaidConfig() {
return {
theme: this.theme,
startOnLoad: this.startOnLoad,
securityLevel: this.securityLevel,
flowchart: this.flowchart
};
}
}
```
## Error Handling
### 错误处理策略
1. **渲染错误**:显示友好的错误提示,保留原始代码
2. **库加载失败**:显示加载失败提示,提供重试选项
3. **导出失败**:显示导出失败提示,记录错误日志
4. **语法错误**:显示语法错误位置和错误信息
### 错误处理实现
```javascript
/**
* 安全执行函数
* @param {Function} fn - 要执行的函数
* @param {string} moduleName - 模块名称
* @returns {boolean} 是否执行成功
*/
function safeExecute(fn, moduleName) {
try {
fn();
ArgonDebug.log(`[Argon Mermaid] ${moduleName} 执行成功`);
return true;
} catch (error) {
ArgonDebug.error(`[Argon Mermaid] ${moduleName} 执行失败:`, error);
return false;
}
}
```
## Performance Optimization
### 优化策略
1. **批量渲染**:使用 requestAnimationFrame 批量渲染图表
2. **延迟加载**:优先渲染可见图表,延迟渲染视口外的图表
3. **缓存优化**:缓存已渲染的图表,避免重复渲染
4. **硬件加速**:使用 CSS transform 实现缩放和拖拽
### 性能优化实现
```javascript
/**
* 批量渲染图表
* @param {HTMLElement[]} blocks - 代码块数组
* @returns {Promise<void>}
*/
async function batchRenderCharts(blocks) {
for (let i = 0; i < blocks.length; i++) {
await new Promise(resolve => {
requestAnimationFrame(async () => {
await renderMermaidChart(blocks[i], i);
resolve();
});
});
}
}
```
## Testing Strategy
### 测试方法
1. **单元测试**:测试各个模块的功能
2. **集成测试**:测试模块之间的集成
3. **端到端测试**:测试完整的渲染流程
4. **性能测试**:测试渲染性能和内存占用
### 测试用例
```javascript
describe('Mermaid Renderer', () => {
it('should detect mermaid blocks', () => {
const blocks = detectMermaidBlocks();
expect(blocks.length).toBeGreaterThan(0);
});
it('should render mermaid chart', async () => {
const block = document.querySelector('code.language-mermaid');
await renderMermaidChart(block, 0);
expect(block.hasAttribute('data-mermaid-rendered')).toBe(true);
});
it('should handle render error', async () => {
const block = createMockBlock('invalid syntax');
await renderMermaidChart(block, 0);
expect(document.querySelector('.mermaid-error')).not.toBeNull();
});
});
```
## Code Quality Standards
### 代码规范
1. **命名规范**:使用驼峰命名法,函数名动词开头
2. **注释规范**:使用 JSDoc 注释,说明参数和返回值
3. **错误处理**:所有异步操作都要有错误处理
4. **性能优化**:避免频繁的 DOM 操作,使用批量处理
### 代码审查清单
- [ ] 所有函数都有 JSDoc 注释
- [ ] 所有异步操作都有错误处理
- [ ] 所有事件监听器都在清理时移除
- [ ] 使用 let/const 而不是 var
- [ ] 遵循项目代码规范Tab 缩进、单引号等)
- [ ] 没有 console.log使用 ArgonDebug
- [ ] 性能优化(批量 DOM 操作、requestAnimationFrame