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

19 KiB
Raw Blame 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 代码块

接口

/**
 * 检测页面中的 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 图表

接口

/**
 * 渲染所有 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 (交互模块)

职责:管理图表交互功能

接口

/**
 * 创建 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 (导出模块)

职责:导出图表为图片文件

接口

/**
 * 显示导出菜单
 * @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 的生命周期

接口

/**
 * 清理 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 (错误处理模块)

职责:处理渲染错误

接口

/**
 * 显示 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 (图表数据模型)

/**
 * 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 (配置模型)

/**
 * 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. 语法错误:显示语法错误位置和错误信息

错误处理实现

/**
 * 安全执行函数
 * @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 实现缩放和拖拽

性能优化实现

/**
 * 批量渲染图表
 * @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. 性能测试:测试渲染性能和内存占用

测试用例

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