Files
argon-theme/.kiro/specs/mermaid-codeblock-magic/design.md
nanhaoluo 07cd43e2bd docs: 完成任务 2.3 - 添加语法错误处理和友好提示
- 验证错误捕获机制完整(同步和异步)
- 验证友好错误提示已实现
- 验证原始代码查看功能
- 验证错误类型识别和行号提取
- 验证完整的 CSS 样式(日间/夜间模式)
- 创建测试文档和总结文档
- 更新任务状态为已完成
- 满足需求 2.5, 7.1-7.4
2026-01-25 13:18:12 +08:00

791 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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