diff --git a/.kiro/specs/mermaid-codeblock-magic/design.md b/.kiro/specs/mermaid-codeblock-magic/design.md index 8029b94..dc9fade 100644 --- a/.kiro/specs/mermaid-codeblock-magic/design.md +++ b/.kiro/specs/mermaid-codeblock-magic/design.md @@ -1,821 +1,790 @@ -# Mermaid 代码块魔改支持 - 设计文档 +# Design Document: Mermaid 代码块渲染修复 -## 1. 设计概述 +## Overview -### 1.1 设计目标 -实现标准 Markdown 代码块 (` ```mermaid `) 的 Mermaid 图表渲染,通过在代码高亮之前拦截并转换代码块,完全绕过 WordPress 和代码高亮的干扰。 +本设计旨在修复 Argon WordPress 主题中 Mermaid 图表渲染的关键问题。当前实现存在以下核心问题: -### 1.2 核心设计理念 -**提前拦截,转换容器**:在代码高亮处理之前,将 `
` 转换为 `
`,使其不被代码高亮处理,同时能被 Mermaid 渲染系统识别。 +1. **PJAX 加载后显示原始文本**:页面切换后 Mermaid 不渲染 +2. **语法解析错误**:flowchart、erDiagram 等语法解析失败 +3. **功能缺失**:缺少导出、全屏查看等功能 +4. **交互体验差**:工具栏遮挡内容、缩放不流畅 -### 1.3 设计参考 -参考主题中数学公式的实现方式: -- 数学公式使用特殊分隔符(`$...$`),不会被代码高亮处理 -- Mermaid 代码块通过提前转换,达到类似效果 -- 两者都在 PJAX 加载后重新处理 +设计方案采用模块化架构,确保代码质量和可维护性。 -### 1.4 技术栈 -- **JavaScript**:原生 JavaScript + jQuery(主题现有技术栈) -- **Mermaid.js**:主题已集成的图表渲染库 -- **执行时机**:代码高亮之前(`highlightJsRender()` 函数开始处) +## Architecture -## 2. 架构设计 +### 核心架构原则 -### 2.1 整体流程 +1. **模块化设计**:将功能拆分为独立模块,易于维护和扩展 +2. **错误隔离**:每个模块独立的错误处理,互不影响 +3. **性能优先**:使用批量渲染、延迟加载等优化技术 +4. **用户体验**:提供流畅的交互和友好的错误提示 -```mermaid -flowchart TD - A[页面加载/PJAX切换] --> B[highlightJsRender 调用] - B --> C[convertMermaidCodeblocks 执行] - C --> D[查找 mermaid 代码块] - D --> E{找到代码块?} - E -->|是| F[提取纯文本代码] - E -->|否| G[继续代码高亮] - F --> H[创建 mermaid-from-codeblock 容器] - H --> I[替换原始代码块] - I --> J[标记已处理] - J --> G - G --> K[代码高亮处理其他代码块] - K --> L[detectMermaidBlocks 检测] - L --> M[提取 Mermaid 代码] - M --> N[mermaid.init 渲染] -``` - -### 2.2 模块设计 - -#### 模块 1:代码块转换器 (convertMermaidCodeblocks) -**职责**:在代码高亮前拦截并转换 mermaid 代码块 - -**输入**:DOM 树(包含未处理的代码块) -**输出**:转换后的 DOM 树(mermaid 代码块已替换为容器) - -**核心逻辑**: -1. 使用多个选择器查找代码块 -2. 提取纯文本代码 -3. 创建新容器 -4. 替换原始元素 - -#### 模块 2:代码提取器 (extractMermaidCode) -**职责**:从不同格式的容器中提取 Mermaid 代码 - -**输入**:DOM 元素(可能是 div、pre、code) -**输出**:纯文本 Mermaid 代码 - -**支持格式**: -- `
`(新增) -- `
` -- `
` -- `
`(降级)
-
-#### 模块 3:Mermaid 检测器 (detectMermaidBlocks)
-**职责**:检测页面中所有需要渲染的 Mermaid 容器
-
-**输入**:DOM 树
-**输出**:需要渲染的元素列表
-
-**检测优先级**:
-1. `div.mermaid-shortcode`(Shortcode 格式)
-2. `div.mermaid-from-codeblock`(代码块魔改格式)
-3. `div.mermaid`(标准格式)
-4. `pre code.language-mermaid`(降级格式)
-
-### 2.3 数据流设计
+### 架构图
 
 ```
-原始 HTML:
-

-flowchart TD
-    A --> B
-
- -↓ convertMermaidCodeblocks() - -转换后 HTML: -
-flowchart TD - A --> B -
- -↓ detectMermaidBlocks() - -检测到的代码: -"flowchart TD\n A --> B" - -↓ mermaid.init() - -渲染后 HTML: -
- ... -
+Mermaid 渲染系统 +├── 检测模块 (MermaidDetector) +│ ├── 扫描代码块 +│ ├── 识别 Mermaid 语法 +│ └── 过滤已渲染的块 +├── 渲染模块 (MermaidRenderer) +│ ├── 初始化 Mermaid +│ ├── 批量渲染 +│ ├── 错误处理 +│ └── 降级方案 +├── 交互模块 (MermaidInteraction) +│ ├── 工具栏管理 +│ ├── 缩放控制 +│ ├── 拖拽控制 +│ └── 全屏查看 +├── 导出模块 (MermaidExporter) +│ ├── PNG 导出 +│ ├── SVG 导出 +│ └── 错误处理 +└── 生命周期模块 (MermaidLifecycle) + ├── PJAX 集成 + ├── 资源清理 + └── 主题同步 ``` -## 3. 详细设计 +## Components and Interfaces -### 3.1 代码块转换函数 +### 1. Mermaid Detector (检测模块) -#### 函数签名 -```javascript -function convertMermaidCodeblocks() -``` +**职责**:检测页面中的 Mermaid 代码块 -#### 实现位置 -`argontheme.js` 第 3942 行之前(`highlightJsRender()` 函数开始处) - -#### 选择器设计 - -```javascript -const selectors = [ - 'pre > code.language-mermaid', // 标准 Markdown 格式(最常见) - 'pre > code.mermaid', // 简化格式 - 'code.language-mermaid', // 无 pre 包裹 - 'pre[data-lang="mermaid"]' // 自定义属性格式 -]; -``` - -**设计理由**: -- 支持多种插件生成的 HTML 结构 -- 优先匹配最常见的格式 -- 提供降级支持 - -#### 重复处理防护 -```javascript -if (element.dataset.mermaidProcessed) { - return; // 跳过已处理的元素 -} -``` - -**设计理由**: -- 避免 PJAX 切换时重复处理 -- 防止多次调用导致的错误 -- 使用 data 属性标记状态 - -#### 代码提取逻辑 -```javascript -let code = element.textContent.trim(); -``` - -**设计理由**: -- `textContent` 获取纯文本,避免 HTML 实体 -- `trim()` 移除前后空白 -- 不进行任何字符转换,保持原始内容 - -#### 容器创建逻辑 -```javascript -const container = document.createElement('div'); -container.className = 'mermaid-from-codeblock'; -container.textContent = code; -container.dataset.processed = 'true'; -``` - -**设计理由**: -- 使用 `textContent` 而非 `innerHTML`,避免 XSS -- 添加特定类名,便于识别来源 -- 标记已处理状态 - -#### 元素替换逻辑 -```javascript -const targetElement = element.closest('pre') || element; -targetElement.parentNode.replaceChild(container, targetElement); -``` - -**设计理由**: -- 优先替换整个 `
` 元素
-- 如果没有 `
` 包裹,替换 `` 元素
-- 保留原始位置和上下文
-
-### 3.2 集成点设计
-
-#### 集成点 1:highlightJsRender() 函数
-**位置**:`argontheme.js` 第 3942 行
-
-**修改前**:
-```javascript
-function highlightJsRender(){
-	if (typeof(hljs) == "undefined"){
-		return;
-	}
-	// ... 代码高亮逻辑
-}
-```
-
-**修改后**:
-```javascript
-function highlightJsRender(){
-	// 在代码高亮之前,先处理 Mermaid 代码块
-	convertMermaidCodeblocks();
-	
-	if (typeof(hljs) == "undefined"){
-		return;
-	}
-	// ... 代码高亮逻辑
-}
-```
-
-**设计理由**:
-- 在代码高亮之前执行,确保 mermaid 代码块不被处理
-- 不影响其他代码块的高亮
-- 执行顺序:转换 → 高亮 → 渲染
-
-#### 集成点 2:detectMermaidBlocks() 函数
-**位置**:`argontheme.js` 第 4430 行
-
-**修改前**:
-```javascript
-const selectors = [
-	'div.mermaid-shortcode',
-	'div.mermaid',
-	'pre code.language-mermaid',
-	// ...
-];
-```
-
-**修改后**:
-```javascript
-const selectors = [
-	'div.mermaid-shortcode',         // Shortcode 格式
-	'div.mermaid-from-codeblock',    // 代码块魔改格式(新增)
-	'div.mermaid',                   // 标准格式
-	'pre code.language-mermaid',     // Markdown 格式(降级)
-	'pre[data-lang="mermaid"]',      // 自定义属性格式
-	'code.mermaid'                   // 简化格式
-];
-```
-
-**设计理由**:
-- 添加新的容器类型到检测列表
-- 优先级高于标准 `mermaid` 类
-- 保留降级选择器,确保兼容性
-
-#### 集成点 3:extractMermaidCode() 函数
-**位置**:`argontheme.js` 第 4650 行
-
-**修改前**:
-```javascript
-// 处理 Shortcode 格式
-if (element.classList.contains('mermaid-shortcode')) {
-	code = element.textContent;
-	this.logDebug('从 Shortcode 格式提取代码');
-}
-```
-
-**修改后**:
-```javascript
-// 处理 Shortcode 格式
-if (element.classList.contains('mermaid-shortcode')) {
-	code = element.textContent;
-	this.logDebug('从 Shortcode 格式提取代码');
-}
-// 处理代码块魔改格式
-else if (element.classList.contains('mermaid-from-codeblock')) {
-	code = element.textContent;
-	this.logDebug('从代码块魔改格式提取代码');
-}
-```
-
-**设计理由**:
-- 与 Shortcode 格式使用相同的提取方式
-- 添加调试日志,便于追踪
-- 保持代码一致性
-
-### 3.3 PJAX 兼容设计
-
-#### 执行时机
-PJAX 加载完成后的回调链(`argontheme.js` 第 2862-2890 行):
-```javascript
-$(document).on('pjax:complete', function() {
-	// ... 其他初始化
-	try { highlightJsRender(); } catch (err) { ... }  // 包含代码块转换
-	// ... 其他初始化
-});
-```
-
-**设计理由**:
-- `highlightJsRender()` 已在 PJAX 回调中调用
-- 代码块转换自动在每次 PJAX 切换后执行
-- 无需额外修改 PJAX 逻辑
-
-#### 重复处理防护
-使用 `data-processed` 属性标记已处理的元素:
-```javascript
-if (element.dataset.mermaidProcessed) {
-	return;
-}
-// ... 处理逻辑
-element.dataset.mermaidProcessed = 'true';
-```
-
-**设计理由**:
-- 避免同一元素被多次转换
-- 支持 PJAX 页面切换
-- 轻量级标记,不影响性能
-
-### 3.4 错误处理设计
-
-#### 空代码检查
-```javascript
-let code = element.textContent.trim();
-if (!code) {
-	return; // 跳过空代码块
-}
-```
-
-**设计理由**:
-- 避免创建空容器
-- 减少不必要的 DOM 操作
-- 提高性能
-
-#### Try-Catch 包裹
-```javascript
-try {
-	convertMermaidCodeblocks();
-} catch (err) {
-	console.error('Mermaid 代码块转换失败:', err);
-}
-```
-
-**设计理由**:
-- 捕获异常,不中断其他代码块的处理
-- 记录错误日志,便于调试
-- 提供降级方案(代码块仍可通过降级选择器检测)
-
-#### 降级支持
-如果代码块转换失败,仍可通过降级选择器检测:
-```javascript
-'pre code.language-mermaid'  // 降级选择器
-```
-
-**设计理由**:
-- 确保即使转换失败,仍能渲染
-- 提供多层保障
-- 增强系统健壮性
-
-## 4. 接口设计
-
-### 4.1 公共函数
-
-#### convertMermaidCodeblocks()
+**接口**:
 ```javascript
 /**
- * 在代码高亮之前转换 Mermaid 代码块
- * 将 
 转换为 
- * + * 检测页面中的 Mermaid 代码块 + * @returns {HTMLElement[]} Mermaid 代码块数组 + */ +function detectMermaidBlocks() { + const blocks = []; + + // 检测

+	document.querySelectorAll('pre code.language-mermaid').forEach(code => {
+		if (!isRendered(code)) {
+			blocks.push(code);
+		}
+	});
+	
+	// 检测 

+	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}
+ */
+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} 是否加载成功
+ */
+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}
+ */
+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 = `
+		
+		
+		
+		
+		
+	`;
+	return toolbar;
+}
+
+/**
+ * 绑定交互事件
+ * @param {HTMLElement} container - 容器元素
  * @returns {void}
  */
-function convertMermaidCodeblocks() {
-	// 实现逻辑
+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.2 数据结构
 
-#### 容器元素结构
-```html
-
- flowchart TD - A --> B -
-``` +### 4. Mermaid Exporter (导出模块) -**属性说明**: -- `class="mermaid-from-codeblock"`:标识来源于代码块 -- `data-processed="true"`:标记已处理 -- 内容:纯文本 Mermaid 代码 +**职责**:导出图表为图片文件 -### 4.3 选择器优先级 - -| 优先级 | 选择器 | 用途 | -|--------|--------|------| -| 1 | `pre > code.language-mermaid` | 标准 Markdown 格式 | -| 2 | `pre > code.mermaid` | 简化格式 | -| 3 | `code.language-mermaid` | 无 pre 包裹 | -| 4 | `pre[data-lang="mermaid"]` | 自定义属性格式 | - -## 5. 性能设计 - -### 5.1 性能目标 -- 单个代码块处理时间 < 10ms -- 不影响页面加载速度 -- 不增加额外的 HTTP 请求 - -### 5.2 性能优化策略 - -#### 优化 1:使用原生 JavaScript +**接口**: ```javascript -document.querySelectorAll(selector) // 而非 $(selector) -``` +/** + * 显示导出菜单 + * @param {HTMLElement} container - 容器元素 + * @returns {void} + */ +function showExportMenu(container) { + const menu = document.createElement('div'); + menu.className = 'mermaid-export-menu'; + menu.innerHTML = ` + + + `; + + 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); +} -**理由**:原生方法性能更好,减少 jQuery 开销 +/** + * 导出为 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('导出失败,请重试'); + } +} -#### 优化 2:提前返回 -```javascript -if (element.dataset.mermaidProcessed) { - return; // 提前返回,避免不必要的处理 +/** + * 导出为 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 (生命周期模块) -#### 优化 3:批量处理 +**职责**:管理 Mermaid 的生命周期 + +**接口**: ```javascript -selectors.forEach(selector => { - document.querySelectorAll(selector).forEach(element => { - // 处理逻辑 +/** + * 清理 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 = ` +
+ + Mermaid 图表渲染失败 +
+
+ ${error.message || '未知错误'} +
+
+ 查看原始代码 +
${escapeHtml(code)}
+
+ `; + + 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} + */ +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(); }); }); ``` -**理由**:一次性查找所有元素,减少 DOM 查询次数 +## Code Quality Standards -#### 优化 4:最小化 DOM 操作 -```javascript -const container = document.createElement('div'); -container.className = 'mermaid-from-codeblock'; -container.textContent = code; -container.dataset.processed = 'true'; -targetElement.parentNode.replaceChild(container, targetElement); -``` +### 代码规范 -**理由**:一次性创建和替换,减少重排和重绘 +1. **命名规范**:使用驼峰命名法,函数名动词开头 +2. **注释规范**:使用 JSDoc 注释,说明参数和返回值 +3. **错误处理**:所有异步操作都要有错误处理 +4. **性能优化**:避免频繁的 DOM 操作,使用批量处理 -### 5.3 性能监控 +### 代码审查清单 -#### 调试日志 -```javascript -this.logDebug('处理了 ' + count + ' 个 Mermaid 代码块'); -this.logDebug('代码内容(前100字符): ' + code.substring(0, 100)); -``` - -**理由**:便于追踪性能问题,不影响生产环境 - -## 6. 安全设计 - -### 6.1 XSS 防护 - -#### 使用 textContent 而非 innerHTML -```javascript -container.textContent = code; // 安全 -// container.innerHTML = code; // 不安全 -``` - -**理由**:`textContent` 会自动转义 HTML,防止 XSS 攻击 - -#### 代码来源验证 -```javascript -let code = element.textContent.trim(); -if (!code) { - return; // 拒绝空代码 -} -``` - -**理由**:避免处理恶意或无效的代码块 - -### 6.2 DOM 操作安全 - -#### 安全的元素替换 -```javascript -const targetElement = element.closest('pre') || element; -if (targetElement.parentNode) { - targetElement.parentNode.replaceChild(container, targetElement); -} -``` - -**理由**:检查父节点存在,避免 null 引用错误 - -## 7. 测试设计 - -### 7.1 单元测试 - -#### 测试用例 1:标准代码块转换 -```javascript -// 输入 -

-flowchart TD
-    A --> B
-
- -// 预期输出 -
-flowchart TD - A --> B -
-``` - -#### 测试用例 2:空代码块处理 -```javascript -// 输入 -
- -// 预期输出 -
// 不转换 -``` - -#### 测试用例 3:重复处理防护 -```javascript -// 第一次处理 -convertMermaidCodeblocks(); // 转换成功 - -// 第二次处理 -convertMermaidCodeblocks(); // 跳过已处理的元素 -``` - -#### 测试用例 4:特殊字符保留 -```javascript -// 输入 -

-A --> B
-C -- text --> D
-
- -// 预期输出 -代码中的 --> 和 -- 保持不变 -``` - -### 7.2 集成测试 - -#### 测试场景 1:与代码高亮集成 -1. 页面包含 mermaid 代码块和其他代码块 -2. 调用 `highlightJsRender()` -3. 验证 mermaid 代码块被转换,其他代码块被高亮 - -#### 测试场景 2:与 Mermaid 渲染集成 -1. 页面包含转换后的容器 -2. 调用 `detectMermaidBlocks()` -3. 验证容器被检测到 -4. 调用 `mermaid.init()` -5. 验证图表正确渲染 - -#### 测试场景 3:PJAX 兼容性 -1. 初始页面加载,代码块正确转换和渲染 -2. PJAX 切换到新页面 -3. 验证新页面的代码块正确转换和渲染 -4. 验证已转换的代码块不被重复处理 - -### 7.3 浏览器测试 - -#### 测试矩阵 -| 浏览器 | 版本 | 测试项 | -|--------|------|--------| -| Chrome | 最新版 | 全部功能 | -| Firefox | 最新版 | 全部功能 | -| Safari | 最新版 | 全部功能 | -| Edge | 最新版 | 全部功能 | - -#### 测试项 -- 代码块转换 -- 图表渲染 -- PJAX 切换 -- 性能表现 - -### 7.4 测试文件 - -创建 `tests/test-codeblock-magic.html`: -```html - - - - Mermaid 代码块魔改测试 - - - -

-	flowchart TD
-	    A --> B
-	
- - -

-	graph LR
-	    C --> D
-	
- - -

-	flowchart TD
-	    A -- text --> B
-	    C ==> D
-	
- - -
- - -``` - -## 8. 部署设计 - -### 8.1 部署步骤 - -#### 步骤 1:修改 argontheme.js -1. 在 `highlightJsRender()` 函数开始处添加 `convertMermaidCodeblocks()` 调用 -2. 实现 `convertMermaidCodeblocks()` 函数 -3. 修改 `detectMermaidBlocks()` 函数,添加新选择器 -4. 修改 `extractMermaidCode()` 函数,支持新容器类型 - -#### 步骤 2:测试验证 -1. 创建测试文件 `tests/test-codeblock-magic.html` -2. 在本地环境测试所有用例 -3. 验证 PJAX 兼容性 -4. 检查浏览器控制台无错误 - -#### 步骤 3:文档更新 -1. 更新 `docs/mermaid-usage-guide.md` -2. 更新 `docs/mermaid-developer-guide.md` -3. 更新 `docs/mermaid-faq.md` - -#### 步骤 4:发布 -1. 提交代码到 Git -2. 更新版本号 -3. 发布更新 - -### 8.2 回滚方案 - -如果出现问题,可以快速回滚: -1. 移除 `convertMermaidCodeblocks()` 调用 -2. 移除新添加的选择器 -3. 恢复原始代码 - -**降级方案**: -- 用户仍可使用 Shortcode 格式 -- 用户仍可使用容器语法 -- 不影响现有功能 - -### 8.3 兼容性保证 - -#### 向后兼容 -- 不影响现有的 Shortcode 格式 -- 不影响现有的容器语法 -- 不影响其他代码块的高亮 - -#### 向前兼容 -- 预留扩展接口 -- 支持未来的新格式 -- 易于维护和升级 - -## 9. 监控与维护 - -### 9.1 日志设计 - -#### 调试日志 -```javascript -this.logDebug('开始转换 Mermaid 代码块'); -this.logDebug('找到 ' + elements.length + ' 个代码块'); -this.logDebug('代码内容: ' + code.substring(0, 100)); -this.logDebug('转换完成'); -``` - -#### 错误日志 -```javascript -console.error('Mermaid 代码块转换失败:', err); -console.error('元素:', element); -console.error('代码:', code); -``` - -### 9.2 性能监控 - -#### 性能指标 -- 代码块转换时间 -- 页面加载时间 -- 内存使用情况 - -#### 监控方法 -```javascript -const startTime = performance.now(); -convertMermaidCodeblocks(); -const endTime = performance.now(); -console.log('转换耗时:', endTime - startTime, 'ms'); -``` - -### 9.3 维护计划 - -#### 定期检查 -- 每月检查浏览器兼容性 -- 每季度检查性能表现 -- 每半年检查代码质量 - -#### 更新策略 -- 跟进 Mermaid.js 版本更新 -- 跟进 WordPress 版本更新 -- 跟进浏览器标准更新 - -## 10. 风险与缓解 - -### 10.1 技术风险 - -#### 风险 1:与其他插件冲突 -**影响**:中等 -**概率**:低 -**缓解措施**: -- 使用唯一的类名 `mermaid-from-codeblock` -- 添加命名空间,避免冲突 -- 提供配置选项,允许禁用 - -#### 风险 2:性能问题 -**影响**:低 -**概率**:低 -**缓解措施**: -- 优化选择器,减少 DOM 查询 -- 使用缓存,避免重复处理 -- 添加性能监控 - -#### 风险 3:浏览器兼容性 -**影响**:中等 -**概率**:低 -**缓解措施**: -- 使用标准 API -- 添加 polyfill -- 提供降级方案 - -### 10.2 用户体验风险 - -#### 风险 1:误转换其他代码块 -**影响**:高 -**概率**:极低 -**缓解措施**: -- 使用精确的选择器 -- 添加类名检查 -- 提供白名单机制 - -#### 风险 2:特殊字符丢失 -**影响**:高 -**概率**:极低 -**缓解措施**: -- 使用 `textContent` 保留原始内容 -- 不进行任何字符转换 -- 添加测试用例验证 - -## 11. 未来扩展 - -### 11.1 短期扩展(1-2 周) - -#### 配置选项 -添加主题设置选项: -- 是否启用代码块魔改 -- 选择器优先级配置 -- 调试模式开关 - -#### 性能优化 -- 添加缓存机制 -- 批量处理优化 -- 延迟加载支持 - -### 11.2 中期扩展(1-2 月) - -#### 编辑器支持 -- 添加编辑器预览功能 -- 支持实时渲染 -- 提供语法提示 - -#### 移动端优化 -- 优化移动端显示 -- 支持触摸交互 -- 响应式布局 - -### 11.3 长期扩展(3-6 月) - -#### 多图表库支持 -- 支持 PlantUML -- 支持 GraphViz -- 支持 D3.js - -#### 高级功能 -- 图表编辑器 -- 图表导出(PNG、SVG) -- 图表分享 - -## 12. 总结 - -### 12.1 设计优势 - -1. **简单高效**:只需添加一个函数,修改三个位置 -2. **兼容性好**:不影响现有功能,支持多种格式 -3. **性能优秀**:使用原生 JavaScript,优化 DOM 操作 -4. **易于维护**:代码清晰,注释详细,易于理解 -5. **安全可靠**:防止 XSS,处理异常,提供降级 - -### 12.2 关键决策 - -| 决策 | 理由 | -|------|------| -| 在代码高亮前拦截 | 避免代码高亮干扰 | -| 使用 textContent | 保留原始内容,防止 XSS | -| 创建新容器类型 | 便于识别来源,支持多种格式 | -| 添加重复处理防护 | 支持 PJAX,避免重复转换 | -| 提供降级支持 | 确保即使转换失败仍能渲染 | - -### 12.3 实施建议 - -1. **分步实施**:先实现核心功能,再添加优化 -2. **充分测试**:创建完整的测试用例,覆盖所有场景 -3. **文档完善**:更新用户文档和开发者文档 -4. **监控反馈**:收集用户反馈,持续优化 - -### 12.4 成功标准 - -- ✅ 可以使用 ` ```mermaid ` 代码块编写图表 -- ✅ 代码块不会被代码高亮处理 -- ✅ 图表正确渲染 -- ✅ 特殊字符不被转换 -- ✅ 换行符正确保留 -- ✅ PJAX 切换正常工作 -- ✅ 性能无明显影响 -- ✅ 兼容所有主流浏览器 +- [ ] 所有函数都有 JSDoc 注释 +- [ ] 所有异步操作都有错误处理 +- [ ] 所有事件监听器都在清理时移除 +- [ ] 使用 let/const 而不是 var +- [ ] 遵循项目代码规范(Tab 缩进、单引号等) +- [ ] 没有 console.log,使用 ArgonDebug +- [ ] 性能优化(批量 DOM 操作、requestAnimationFrame) diff --git a/.kiro/specs/mermaid-codeblock-magic/requirements.md b/.kiro/specs/mermaid-codeblock-magic/requirements.md index a576fdf..eb1e238 100644 --- a/.kiro/specs/mermaid-codeblock-magic/requirements.md +++ b/.kiro/specs/mermaid-codeblock-magic/requirements.md @@ -1,501 +1,257 @@ -# Mermaid 代码块魔改支持 - 需求文档 - -## 1. 项目概述 - -### 1.1 背景 -当前 Argon 主题支持 Mermaid 图表渲染,但存在多个标记方式的兼容性问题: -- **标准 Markdown 代码块** (` ```mermaid `):被 WP-Markdown 插件和代码高亮干扰 -- **容器语法** (`::: mermaid ... :::`):空行导致内容截断 -- **Shortcode** (`[mermaid]...[/mermaid]`):可用但不符合 Markdown 标准 - -用户希望使用标准 Markdown 语法 ` ```mermaid `,但需要绕过所有干扰。 - -### 1.2 核心问题 -1. WP-Markdown 插件会将 ` ```mermaid ` 代码块用 `document.write()` 包裹 -2. WordPress 的 `wptexturize()` 会自动转换特殊字符(`--` → `–`) -3. 主题的代码高亮会处理 mermaid 代码块,添加行号和控制按钮 -4. 三方冲突导致 Mermaid 代码无法正确渲染 - -### 1.3 解决方案 -**魔改代码块显示**:在代码高亮之前拦截 mermaid 代码块,将其转换为 Mermaid 渲染容器,完全绕过代码高亮和 WordPress 格式化。 - -### 1.4 参考实现 -主题中数学公式的实现方式可以作为参考: -- **MathJax/KaTeX**:使用特定分隔符(`$...$`、`\(...\)`)标记数学公式 -- **渲染时机**:在 PJAX 加载完成后调用 `MathJax.typeset()` 或 `renderMathInElement()` -- **不干扰代码高亮**:数学公式使用特殊标记,不会被代码高亮处理 -- **WordPress 兼容**:数学公式分隔符不会被 WordPress 自动转换 - -**关键差异**: -- 数学公式使用**内联标记**(`$...$`),不需要代码块 -- Mermaid 需要使用**代码块**(` ```mermaid `),需要在代码高亮前拦截 -- 数学公式库自动扫描页面,Mermaid 需要手动检测和渲染 - -## 2. 用户故事 - -### 2.1 作为博客作者 -**我想要**:使用标准 Markdown 语法 ` ```mermaid ` 编写流程图 -**以便**:在原生编辑器中清晰可见,符合 Markdown 标准,无需学习特殊语法 - -**验收标准**: -- 可以使用 ` ```mermaid ` 代码块编写 Mermaid 图表 -- 代码块不会被代码高亮处理(无行号、无控制按钮) -- 代码块会被正确转换为 Mermaid 图表 -- 支持所有 Mermaid 语法(flowchart, sequence, class, state 等) - -### 2.2 作为博客作者 -**我想要**:Mermaid 代码中的特殊字符不被 WordPress 转换 -**以便**:箭头符号 `-->` 不会变成 `–>`,图表能正确渲染 - -**验收标准**: -- 箭头符号 `-->` 保持不变 -- 双横线 `--` 保持不变 -- 其他特殊字符(`==`, `~~`, `::` 等)保持不变 -- 换行符正确保留 - -### 2.3 作为博客作者 -**我想要**:Mermaid 代码块在编辑器中显示为代码块 -**以便**:编辑时能清晰看到代码结构,方便修改 - -**验收标准**: -- 在 WordPress 原生编辑器中显示为代码块 -- 在 WP-Markdown 编辑器中显示为代码块 -- 代码块有语法高亮(编辑器层面) -- 保存后前端正确渲染为图表 - -### 2.4 作为开发者 -**我想要**:拦截逻辑在代码高亮之前执行 -**以便**:避免代码高亮干扰 Mermaid 渲染 - -**验收标准**: -- 在 `highlightJsRender()` 函数开始处添加预处理 -- 查找所有 `pre > code.language-mermaid` 元素 -- 提取纯文本代码(不经过任何处理) -- 创建 Mermaid 渲染容器 -- 替换原始代码块元素 - -### 2.5 作为开发者 -**我想要**:支持多种 Mermaid 代码块格式 -**以便**:兼容不同插件和编辑器生成的 HTML 结构 - -**验收标准**: -- 支持 `
` 格式
-- 支持 `
` 格式
-- 支持 `` 格式(无 pre 包裹)
-- 支持 `
` 格式
-
-## 3. 功能需求
-
-### 3.1 代码块拦截(核心功能)
-
-**需求描述**:在代码高亮之前拦截 mermaid 代码块
-
-**实现位置**:`argontheme.js` 的 `highlightJsRender()` 函数开始处(第 3942 行)
-
-**参考实现**:类似数学公式在 PJAX 加载后的处理方式(第 2862-2880 行)
-
-**处理流程**:
-1. 在 `highlightJsRender()` 函数开始处添加预处理
-2. 查找所有 mermaid 代码块(多种选择器)
-3. 遍历每个代码块
-4. 提取纯文本代码
-5. 创建 Mermaid 渲染容器
-6. 替换原始代码块元素
-7. 标记已处理(避免重复处理)
-
-**选择器优先级**:
-```javascript
-const selectors = [
-	'pre > code.language-mermaid',  // 标准格式(最常见)
-	'pre > code.mermaid',           // 简化格式
-	'code.language-mermaid',        // 无 pre 包裹
-	'pre[data-lang="mermaid"]'      // 自定义属性格式
-];
-```
-
-**实现示例**:
-```javascript
-function highlightJsRender(){
-	// 在代码高亮之前,先处理 Mermaid 代码块
-	convertMermaidCodeblocks();
-	
-	// 原有的代码高亮逻辑
-	if (typeof(hljs) == "undefined"){
-		return;
-	}
-	// ...
-}
-
-function convertMermaidCodeblocks(){
-	// 查找所有 mermaid 代码块
-	const selectors = [
-		'pre > code.language-mermaid',
-		'pre > code.mermaid',
-		'code.language-mermaid',
-		'pre[data-lang="mermaid"]'
-	];
-	
-	selectors.forEach(selector => {
-		document.querySelectorAll(selector).forEach(element => {
-			// 避免重复处理
-			if (element.dataset.mermaidProcessed) {
-				return;
-			}
-			
-			// 提取代码
-			let code = element.textContent.trim();
-			if (!code) {
-				return;
-			}
-			
-			// 创建容器
-			const container = document.createElement('div');
-			container.className = 'mermaid-from-codeblock';
-			container.textContent = code;
-			container.dataset.processed = 'true';
-			
-			// 替换元素
-			const targetElement = element.closest('pre') || element;
-			targetElement.parentNode.replaceChild(container, targetElement);
-		});
-	});
-}
-```
-
-### 3.2 代码提取
-
-**需求描述**:从不同格式的代码块中提取纯文本代码
-
-**处理逻辑**:
-- 使用 `textContent` 获取纯文本(避免 HTML 实体)
-- 移除前后空白字符(`trim()`)
-- 不进行任何字符转换(保持原始内容)
-- 检查代码是否为空
-
-**特殊处理**:
-- 如果代码块包含 `