feat: 实现 Mermaid 库加载失败的降级处理机制

- 添加多 CDN 备选方案(jsdelivr、unpkg、本地镜像)
- 实现递归加载逻辑,主 CDN 失败时自动尝试备用 CDN
- 添加 onerror 事件处理,捕获库加载失败
- 所有 CDN 失败时显示友好的错误提示
- 在错误提示中保留原始代码供用户查看
- 添加详细的控制台日志输出
- 创建 PHP 和 HTML 测试文件验证功能
- 暴露 MermaidRenderer 到全局作用域供降级处理使用

Requirements: 1.4, 2.3, 7.1, 7.2, 7.3, 7.4, 7.5
This commit is contained in:
2026-01-23 23:12:05 +08:00
parent 43b695bd66
commit 1d5899ce7e
8 changed files with 1962 additions and 3 deletions

View File

@@ -0,0 +1,977 @@
# Design Document: Mermaid 图表支持
## Overview
本设计文档描述了在 Argon WordPress 主题中集成 Mermaid 图表支持的技术方案。由于 WP-Markdown 编辑器的特殊渲染方式(将 Mermaid 代码块保存为单行,缺少真正的换行符),直接集成 Mermaid 存在技术障碍。
本设计采用**插件兼容方案**,通过支持主流 Mermaid WordPress 插件(如 WP Githuber MD、Markdown Block 等)来实现功能,同时提供主题级别的样式优化、夜间模式适配和性能优化。
### 核心设计原则
1. **插件优先**:依赖成熟的 Mermaid 插件处理代码解析和渲染
2. **主题增强**:提供样式优化、主题适配和性能优化
3. **灵活配置**:支持 CDN/本地加载、多种主题、调试模式
4. **优雅降级**:加载失败时提供备用方案和友好提示
5. **性能优先**:按需加载、异步加载、缓存优化
## Architecture
### 系统架构图
```
┌─────────────────────────────────────────────────────────────┐
│ WordPress 前端页面 │
└─────────────────────────────────────────────────────────────┘
├─ 包含 Mermaid 代码块?
┌─────────┴─────────┐
│ │
是 否
│ │
▼ ▼
┌──────────────────┐ ┌──────────┐
│ 加载 Mermaid 库 │ │ 不加载 │
└──────────────────┘ └──────────┘
├─ CDN 模式 / 本地模式
┌──────────────────┐
│ Mermaid.js 库 │
└──────────────────┘
┌──────────────────┐
│ 代码块检测器 │
│ - class="mermaid"│
│ - language="mermaid"│
│ - data-lang="mermaid"│
└──────────────────┘
┌──────────────────┐
│ 渲染引擎 │
│ - 初始化配置 │
│ - 主题适配 │
│ - 错误处理 │
└──────────────────┘
┌──────────────────┐
│ 样式增强器 │
│ - 容器样式 │
│ - 响应式适配 │
│ - 夜间模式 │
└──────────────────┘
┌──────────────────┐
│ 渲染后的 SVG │
└──────────────────┘
```
### 组件交互流程
```
用户访问页面
WordPress 渲染页面
主题检测页面内容
├─ 是否包含 Mermaid 代码块?
▼ (是)
加载 Mermaid 库 (CDN/本地)
DOMContentLoaded 事件触发
初始化 Mermaid 配置
├─ 设置主题 (日间/夜间)
├─ 设置安全级别
└─ 设置错误处理
检测所有 Mermaid 代码块
批量渲染图表
├─ 成功 → 应用样式增强
└─ 失败 → 显示错误提示
监听主题切换事件
重新渲染图表 (如需要)
```
## Components and Interfaces
### 1. 配置管理组件 (Configuration Manager)
**职责**:管理 Mermaid 相关的所有配置选项
**接口**
```php
// 获取配置选项
function argon_get_mermaid_option($option_name, $default = null)
// 保存配置选项
function argon_update_mermaid_option($option_name, $value)
// 验证 CDN 地址格式
function argon_validate_mermaid_cdn_url($url)
// 获取当前主题模式对应的 Mermaid 主题
function argon_get_mermaid_theme()
```
**配置选项**
- `argon_enable_mermaid`: 启用/禁用 Mermaid 支持 (true/false)
- `argon_mermaid_cdn_source`: CDN 来源 (jsdelivr/unpkg/custom/local)
- `argon_mermaid_cdn_custom_url`: 自定义 CDN 地址
- `argon_mermaid_theme`: 图表主题 (default/dark/forest/neutral/auto)
- `argon_mermaid_use_local`: 使用本地镜像 (true/false)
- `argon_mermaid_debug_mode`: 调试模式 (true/false)
### 2. 库加载器 (Library Loader)
**职责**:负责检测页面内容并按需加载 Mermaid 库
**接口**
```php
// 检测页面是否包含 Mermaid 代码块
function argon_has_mermaid_content($content)
// 加载 Mermaid 库
function argon_enqueue_mermaid_scripts()
// 获取 Mermaid 库 URL
function argon_get_mermaid_library_url()
```
**实现逻辑**
1.`wp_enqueue_scripts` 钩子中检查当前页面内容
2. 使用正则表达式检测 Mermaid 代码块标记
3. 如果检测到,根据配置加载对应的库文件
4. 添加 async 或 defer 属性实现异步加载
**CDN 地址映射**
```php
$cdn_urls = [
'jsdelivr' => 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js',
'unpkg' => 'https://unpkg.com/mermaid@10/dist/mermaid.min.js',
'local' => get_template_directory_uri() . '/assets/vendor/mermaid/mermaid.min.js'
];
```
### 3. 渲染引擎初始化器 (Render Engine Initializer)
**职责**:初始化 Mermaid 配置并启动渲染
**接口**
```javascript
// 初始化 Mermaid 配置
function initMermaidConfig()
// 获取当前主题对应的 Mermaid 主题
function getMermaidTheme()
// 渲染所有 Mermaid 图表
function renderAllMermaidCharts()
// 重新渲染图表(主题切换时)
function reRenderMermaidCharts()
```
**Mermaid 配置对象**
```javascript
{
startOnLoad: false, // 手动控制渲染时机
theme: 'default', // 根据页面主题动态设置
securityLevel: 'loose', // 允许 HTML 标签
logLevel: 'error', // 生产环境使用 error调试模式使用 debug
flowchart: {
useMaxWidth: true,
htmlLabels: true
}
}
```
### 4. 代码块检测器 (Code Block Detector)
**职责**:识别页面中的 Mermaid 代码块
**接口**
```javascript
// 检测所有 Mermaid 代码块
function detectMermaidBlocks()
// 检查元素是否为 Mermaid 代码块
function isMermaidBlock(element)
// 提取代码块内容
function extractMermaidCode(element)
```
**检测规则**(优先级从高到低):
1. `<div class="mermaid">` - 标准格式
2. `<pre><code class="language-mermaid">` - Markdown 格式
3. `<pre data-lang="mermaid">` - 自定义属性格式
4. `<code class="mermaid">` - 简化格式
**特殊处理**
- 忽略 HTML 注释中的代码块
- 处理 WP-Markdown 生成的 `<script>document.write()</script>` 格式
- 解码转义字符(`\n`, `\"`, `\'`
### 5. 样式增强器 (Style Enhancer)
**职责**:为渲染后的图表添加主题样式
**接口**
```javascript
// 应用容器样式
function applyMermaidContainerStyles(container)
// 应用响应式样式
function applyResponsiveStyles(container)
// 应用夜间模式样式
function applyDarkModeStyles(container, isDarkMode)
```
**CSS 样式类**
```css
.mermaid-container {
background: var(--card-background);
border-radius: 8px;
padding: 20px;
margin: 20px 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow-x: auto;
max-width: 100%;
}
.mermaid-container svg {
max-width: 100%;
height: auto;
}
html.darkmode .mermaid-container {
background: var(--card-background-dark);
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
}
```
### 6. 错误处理器 (Error Handler)
**职责**:处理渲染错误并显示友好提示
**接口**
```javascript
// 处理渲染错误
function handleMermaidError(error, element)
// 显示错误提示
function showErrorMessage(element, errorInfo)
// 记录调试信息
function logDebugInfo(message, data)
```
**错误提示格式**
```html
<div class="mermaid-error">
<div class="error-icon">⚠️</div>
<div class="error-title">Mermaid 图表渲染失败</div>
<div class="error-message">错误类型: 语法错误</div>
<div class="error-details">行号: 3</div>
<details>
<summary>查看原始代码</summary>
<pre><code>...</code></pre>
</details>
</div>
```
### 7. 主题切换监听器 (Theme Switch Listener)
**职责**:监听主题模式切换并重新渲染图表
**接口**
```javascript
// 监听主题切换事件
function listenThemeSwitch()
// 主题切换回调
function onThemeSwitched(isDarkMode)
// 批量重新渲染
function batchReRender(elements)
```
**实现方式**
- 监听 Argon 主题的 `argon:theme-switched` 自定义事件
- 监听 `html` 元素的 `darkmode` class 变化MutationObserver
- 使用防抖避免频繁重新渲染
### 8. 插件兼容层 (Plugin Compatibility Layer)
**职责**:检测并兼容主流 Mermaid 插件
**接口**
```php
// 检测已安装的 Mermaid 插件
function argon_detect_mermaid_plugins()
// 检查是否已加载 Mermaid 库
function argon_is_mermaid_loaded()
// 避免重复加载
function argon_prevent_duplicate_loading()
```
**支持的插件**
1. **WP Githuber MD** - 检测 `wp-githuber-md` 插件
2. **Markdown Block** - 检测 Gutenberg Mermaid 块
3. **Code Syntax Block** - 检测代码高亮插件的 Mermaid 支持
**兼容策略**
- 如果插件已加载 Mermaid 库,主题不再重复加载
- 主题只提供样式增强和主题适配
- 通过 `window.mermaid` 对象检测库是否已加载
## Data Models
### 1. Mermaid 配置对象
```javascript
interface MermaidConfig {
enabled: boolean; // 是否启用
cdnSource: string; // CDN 来源: 'jsdelivr' | 'unpkg' | 'custom' | 'local'
customCdnUrl: string; // 自定义 CDN 地址
theme: string; // 图表主题: 'default' | 'dark' | 'forest' | 'neutral' | 'auto'
useLocal: boolean; // 是否使用本地镜像
debugMode: boolean; // 调试模式
autoThemeSwitch: boolean; // 自动切换主题
}
```
### 2. 代码块元素对象
```javascript
interface MermaidBlock {
element: HTMLElement; // DOM 元素
code: string; // Mermaid 代码
type: string; // 代码块类型: 'div' | 'pre-code' | 'custom'
rendered: boolean; // 是否已渲染
error: Error | null; // 渲染错误
}
```
### 3. 渲染结果对象
```javascript
interface RenderResult {
success: boolean; // 是否成功
svg: string; // 渲染后的 SVG
error: {
type: string; // 错误类型
message: string; // 错误信息
line: number; // 错误行号
} | null;
}
```
### 4. 插件检测结果
```php
interface PluginDetectionResult {
'wp-githuber-md': boolean; // WP Githuber MD
'markdown-block': boolean; // Markdown Block
'code-syntax-block': boolean; // Code Syntax Block
'mermaid-loaded': boolean; // Mermaid 库是否已加载
}
```
### 5. 错误信息对象
```javascript
interface ErrorInfo {
type: string; // 错误类型: 'syntax' | 'render' | 'load'
message: string; // 错误信息
line: number | null; // 错误行号
code: string; // 原始代码
timestamp: number; // 时间戳
}
```
## Correctness Properties
*属性是一种特征或行为,应该在系统的所有有效执行中保持为真——本质上是关于系统应该做什么的正式陈述。属性作为人类可读规范和机器可验证正确性保证之间的桥梁。*
### Property 1: 按需加载库
*对于任意* WordPress 页面,当且仅当页面内容包含 Mermaid 代码块时,主题应该加载 Mermaid JavaScript 库。
**Validates: Requirements 1.1, 1.5, 8.1**
### Property 2: CDN 地址正确性
*对于任意* CDN 配置选项jsdelivr、unpkg、custom、local生成的脚本 URL 应该与配置选项对应的 CDN 地址匹配。
**Validates: Requirements 1.2, 1.3**
### Property 3: 代码块识别完整性
*对于任意* 包含 Mermaid 标记的 HTML 元素class="mermaid"、language="mermaid"、data-lang="mermaid"),检测器应该能够识别并提取其中的 Mermaid 代码。
**Validates: Requirements 10.1, 10.2, 10.3**
### Property 4: 渲染成功后替换内容
*对于任意* 成功渲染的 Mermaid 图表,原始代码块文本应该被移除,并替换为渲染后的 SVG 图表。
**Validates: Requirements 2.4**
### Property 5: 错误时保留原始代码
*对于任意* 渲染失败的 Mermaid 代码块,系统应该显示错误提示信息,并保留原始代码块以便用户修正。
**Validates: Requirements 7.1, 7.4**
### Property 6: 主题模式自动切换
*对于任意* 页面主题模式切换(日间↔夜间),当配置为自动切换时,所有 Mermaid 图表应该重新渲染并使用对应的图表主题(浅色↔深色)。
**Validates: Requirements 4.1, 4.2, 4.3**
### Property 7: 自定义主题优先级
*对于任意* 图表,当管理员设置了自定义图表主题时,应该使用自定义主题而不是根据页面主题自动切换。
**Validates: Requirements 4.5**
### Property 8: 响应式容器宽度
*对于任意* 渲染后的 Mermaid 图表容器,其最大宽度应该设置为 100%,并且当图表宽度超过容器时应该启用横向滚动。
**Validates: Requirements 3.1, 3.3**
### Property 9: 移动端自适应
*对于任意* 屏幕宽度小于 768px 的设备Mermaid 图表应该自动调整大小以适应屏幕宽度,不应该出现横向溢出。
**Validates: Requirements 3.2**
### Property 10: 夜间模式样式适配
*对于任意* Mermaid 图表容器,在夜间模式下应该应用深色背景和边框样式,与页面整体风格保持一致。
**Validates: Requirements 6.5**
### Property 11: 卡片内边距
*对于任意* 在卡片中显示的 Mermaid 图表容器应该添加适当的内边距padding确保图表与卡片边缘有足够的间距。
**Validates: Requirements 6.3**
### Property 12: CDN 地址验证
*对于任意* 用户输入的自定义 CDN 地址,保存前应该验证其格式是否为有效的 URL并且以 `.js` 结尾。
**Validates: Requirements 5.5**
### Property 13: 避免重复加载
*对于任意* 页面,当检测到已有 Mermaid 插件加载了 Mermaid 库时,主题不应该重复加载该库。
**Validates: Requirements 9.4**
### Property 14: 错误信息完整性
*对于任意* Mermaid 解析错误,错误提示信息应该包含错误类型和详细的错误描述,帮助用户定位问题。
**Validates: Requirements 7.3**
### Property 15: 批量渲染性能
*对于任意* 包含多个 Mermaid 图表的页面,所有图表应该在一次 DOM 遍历中批量收集,然后批量渲染,而不是逐个渲染。
**Validates: Requirements 8.4**
### Property 16: 渲染缓存
*对于任意* 已成功渲染的 Mermaid 图表,在页面生命周期内不应该重复渲染,除非主题模式发生切换。
**Validates: Requirements 8.3**
### Property 17: 代码块优先级
*对于任意* 同时包含多个 Mermaid 标记的元素(如同时有 class="mermaid" 和 data-lang="mermaid"),应该优先使用 class 属性进行识别。
**Validates: Requirements 10.4**
### Property 18: 忽略注释代码块
*对于任意* 被 HTML 注释包裹的 Mermaid 代码块,检测器应该忽略它们,不进行渲染。
**Validates: Requirements 10.5**
## Error Handling
### 1. 库加载失败
**场景**CDN 不可用或网络问题导致 Mermaid 库加载失败
**处理策略**
1. 监听脚本 `onerror` 事件
2. 在控制台输出详细错误信息
3. 尝试降级到备用 CDNjsdelivr → unpkg → local
4. 如果所有 CDN 都失败,显示全局提示信息
**实现**
```javascript
function loadMermaidWithFallback(urls, index = 0) {
if (index >= urls.length) {
console.error('[Argon Mermaid] 所有 CDN 加载失败');
showGlobalError('Mermaid 库加载失败,请检查网络连接');
return;
}
const script = document.createElement('script');
script.src = urls[index];
script.async = true;
script.onerror = () => {
console.warn(`[Argon Mermaid] CDN ${urls[index]} 加载失败,尝试备用 CDN`);
loadMermaidWithFallback(urls, index + 1);
};
script.onload = () => {
console.log(`[Argon Mermaid] 成功从 ${urls[index]} 加载库`);
initMermaid();
};
document.head.appendChild(script);
}
```
### 2. 代码解析错误
**场景**Mermaid 代码语法错误导致解析失败
**处理策略**
1. 捕获 Mermaid 渲染异常
2. 提取错误类型和行号信息
3. 在原代码块位置显示友好的错误提示
4. 保留原始代码供用户查看和修正
5. 在调试模式下输出详细堆栈信息
**错误提示 UI**
```html
<div class="mermaid-error-container">
<div class="error-header">
<span class="error-icon">⚠️</span>
<span class="error-title">图表渲染失败</span>
</div>
<div class="error-body">
<p class="error-type">错误类型: 语法错误</p>
<p class="error-message">Expecting 'NEWLINE', 'SPACE', got 'GRAPH'</p>
<p class="error-line">位置: 第 3 行</p>
</div>
<details class="error-code">
<summary>查看原始代码</summary>
<pre><code class="language-mermaid">...</code></pre>
</details>
</div>
```
### 3. 配置验证错误
**场景**:管理员输入无效的配置选项
**处理策略**
1. 在保存前验证所有配置项
2. CDN URL 格式验证(必须是有效 URL 且以 .js 结尾)
3. 主题名称验证(必须是预定义的主题之一)
4. 显示具体的验证错误信息
5. 阻止保存无效配置
**验证函数**
```php
function argon_validate_mermaid_settings($settings) {
$errors = [];
// 验证 CDN URL
if ($settings['cdn_source'] === 'custom') {
$url = $settings['custom_cdn_url'];
if (!filter_var($url, FILTER_VALIDATE_URL)) {
$errors[] = 'CDN 地址格式无效';
} elseif (!preg_match('/\.js$/', $url)) {
$errors[] = 'CDN 地址必须以 .js 结尾';
}
}
// 验证主题名称
$valid_themes = ['default', 'dark', 'forest', 'neutral', 'auto'];
if (!in_array($settings['theme'], $valid_themes)) {
$errors[] = '无效的图表主题';
}
return $errors;
}
```
### 4. 插件冲突
**场景**:多个插件同时加载 Mermaid 库导致冲突
**处理策略**
1. 在加载前检测 `window.mermaid` 是否已存在
2. 如果已存在,跳过库加载,只应用样式增强
3. 记录检测结果到控制台
4. 在设置页显示插件兼容性状态
**检测逻辑**
```javascript
function checkMermaidLoaded() {
if (typeof window.mermaid !== 'undefined') {
console.log('[Argon Mermaid] 检测到 Mermaid 库已由其他插件加载');
return true;
}
return false;
}
```
### 5. 主题切换异常
**场景**:主题切换时重新渲染失败
**处理策略**
1. 使用 try-catch 包裹重新渲染逻辑
2. 如果重新渲染失败,保留原有图表
3. 在控制台输出警告信息
4. 不影响页面其他功能
**实现**
```javascript
function reRenderOnThemeSwitch() {
const charts = document.querySelectorAll('.mermaid-rendered');
charts.forEach(chart => {
try {
const code = chart.dataset.mermaidCode;
const newTheme = getMermaidTheme();
mermaid.initialize({ theme: newTheme });
mermaid.render('mermaid-' + Date.now(), code, (svg) => {
chart.innerHTML = svg;
});
} catch (error) {
console.warn('[Argon Mermaid] 重新渲染失败,保留原图表', error);
}
});
}
```
## Testing Strategy
### 测试方法概述
本功能采用**双重测试策略**
- **单元测试**:验证具体示例、边缘情况和错误条件
- **属性测试**:验证跨所有输入的通用属性
两者互补且都是全面覆盖所需的:
- 单元测试捕获具体的 bug
- 属性测试验证一般正确性
### 单元测试策略
单元测试应专注于:
- **具体示例**:演示正确行为的特定案例
- **集成点**:组件之间的交互
- **边缘情况和错误条件**:特殊场景处理
**测试框架**:使用 PHPUnitPHP 部分)和 JestJavaScript 部分)
**PHP 单元测试示例**
```php
// 测试 CDN URL 验证
public function test_validate_cdn_url_with_valid_url() {
$url = 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js';
$this->assertTrue(argon_validate_mermaid_cdn_url($url));
}
public function test_validate_cdn_url_with_invalid_url() {
$url = 'not-a-valid-url';
$this->assertFalse(argon_validate_mermaid_cdn_url($url));
}
// 测试代码块检测
public function test_has_mermaid_content_with_div_class() {
$content = '<div class="mermaid">flowchart TD</div>';
$this->assertTrue(argon_has_mermaid_content($content));
}
public function test_has_mermaid_content_without_mermaid() {
$content = '<p>Regular paragraph</p>';
$this->assertFalse(argon_has_mermaid_content($content));
}
```
**JavaScript 单元测试示例**
```javascript
// 测试主题获取
test('getMermaidTheme returns dark theme in dark mode', () => {
document.documentElement.classList.add('darkmode');
expect(getMermaidTheme()).toBe('dark');
});
test('getMermaidTheme returns default theme in light mode', () => {
document.documentElement.classList.remove('darkmode');
expect(getMermaidTheme()).toBe('default');
});
// 测试代码块检测
test('isMermaidBlock detects div with mermaid class', () => {
const div = document.createElement('div');
div.className = 'mermaid';
expect(isMermaidBlock(div)).toBe(true);
});
test('isMermaidBlock ignores regular div', () => {
const div = document.createElement('div');
expect(isMermaidBlock(div)).toBe(false);
});
```
### 属性测试策略
**测试框架**:使用 fast-checkJavaScript进行属性测试
**配置要求**
- 每个属性测试最少运行 100 次迭代
- 每个测试必须引用设计文档中的属性
- 标签格式:`Feature: mermaid-support, Property {number}: {property_text}`
**属性测试示例**
```javascript
// Feature: mermaid-support, Property 1: 按需加载库
// 对于任意 WordPress 页面,当且仅当页面内容包含 Mermaid 代码块时,主题应该加载 Mermaid JavaScript 库
fc.assert(
fc.property(
fc.string(), // 生成随机页面内容
fc.boolean(), // 是否包含 Mermaid 代码块
(content, hasMermaid) => {
const pageContent = hasMermaid
? content + '<div class="mermaid">flowchart TD</div>'
: content;
const shouldLoad = argon_has_mermaid_content(pageContent);
expect(shouldLoad).toBe(hasMermaid);
}
),
{ numRuns: 100 }
);
// Feature: mermaid-support, Property 2: CDN 地址正确性
// 对于任意 CDN 配置选项,生成的脚本 URL 应该与配置选项对应的 CDN 地址匹配
fc.assert(
fc.property(
fc.constantFrom('jsdelivr', 'unpkg', 'local'),
(cdnSource) => {
const expectedUrls = {
'jsdelivr': 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js',
'unpkg': 'https://unpkg.com/mermaid@10/dist/mermaid.min.js',
'local': '/wp-content/themes/argon/assets/vendor/mermaid/mermaid.min.js'
};
const actualUrl = argon_get_mermaid_library_url(cdnSource);
expect(actualUrl).toContain(expectedUrls[cdnSource]);
}
),
{ numRuns: 100 }
);
// Feature: mermaid-support, Property 3: 代码块识别完整性
// 对于任意包含 Mermaid 标记的 HTML 元素,检测器应该能够识别并提取其中的 Mermaid 代码
fc.assert(
fc.property(
fc.constantFrom('class', 'language', 'data-lang'),
fc.string({ minLength: 10 }),
(markType, code) => {
let element;
switch (markType) {
case 'class':
element = document.createElement('div');
element.className = 'mermaid';
element.textContent = code;
break;
case 'language':
element = document.createElement('pre');
const codeEl = document.createElement('code');
codeEl.className = 'language-mermaid';
codeEl.textContent = code;
element.appendChild(codeEl);
break;
case 'data-lang':
element = document.createElement('pre');
element.setAttribute('data-lang', 'mermaid');
element.textContent = code;
break;
}
expect(isMermaidBlock(element)).toBe(true);
expect(extractMermaidCode(element)).toBe(code);
}
),
{ numRuns: 100 }
);
// Feature: mermaid-support, Property 6: 主题模式自动切换
// 对于任意页面主题模式切换,当配置为自动切换时,所有 Mermaid 图表应该重新渲染并使用对应的图表主题
fc.assert(
fc.property(
fc.boolean(), // 初始主题模式
fc.integer({ min: 1, max: 10 }), // 图表数量
(initialDarkMode, chartCount) => {
// 设置初始主题
if (initialDarkMode) {
document.documentElement.classList.add('darkmode');
} else {
document.documentElement.classList.remove('darkmode');
}
// 创建多个图表
const charts = [];
for (let i = 0; i < chartCount; i++) {
const chart = document.createElement('div');
chart.className = 'mermaid-rendered';
chart.dataset.mermaidCode = 'flowchart TD\nA-->B';
document.body.appendChild(chart);
charts.push(chart);
}
// 切换主题
const newDarkMode = !initialDarkMode;
if (newDarkMode) {
document.documentElement.classList.add('darkmode');
} else {
document.documentElement.classList.remove('darkmode');
}
// 触发重新渲染
reRenderOnThemeSwitch();
// 验证所有图表都使用了新主题
const expectedTheme = newDarkMode ? 'dark' : 'default';
charts.forEach(chart => {
expect(chart.dataset.currentTheme).toBe(expectedTheme);
});
// 清理
charts.forEach(chart => chart.remove());
}
),
{ numRuns: 100 }
);
// Feature: mermaid-support, Property 12: CDN 地址验证
// 对于任意用户输入的自定义 CDN 地址,保存前应该验证其格式是否为有效的 URL并且以 .js 结尾
fc.assert(
fc.property(
fc.webUrl(), // 生成随机 URL
fc.constantFrom('.js', '.css', '.json', ''), // 不同的文件扩展名
(baseUrl, extension) => {
const url = baseUrl + extension;
const isValid = argon_validate_mermaid_cdn_url(url);
// 只有以 .js 结尾的有效 URL 才应该通过验证
const shouldBeValid = extension === '.js';
expect(isValid).toBe(shouldBeValid);
}
),
{ numRuns: 100 }
);
// Feature: mermaid-support, Property 15: 批量渲染性能
// 对于任意包含多个 Mermaid 图表的页面,所有图表应该在一次 DOM 遍历中批量收集,然后批量渲染
fc.assert(
fc.property(
fc.integer({ min: 1, max: 20 }), // 图表数量
(chartCount) => {
// 创建多个图表
for (let i = 0; i < chartCount; i++) {
const div = document.createElement('div');
div.className = 'mermaid';
div.textContent = `flowchart TD\nA${i}-->B${i}`;
document.body.appendChild(div);
}
// 记录 DOM 查询次数
let queryCount = 0;
const originalQuerySelectorAll = document.querySelectorAll;
document.querySelectorAll = function(...args) {
queryCount++;
return originalQuerySelectorAll.apply(this, args);
};
// 执行批量渲染
renderAllMermaidCharts();
// 恢复原始方法
document.querySelectorAll = originalQuerySelectorAll;
// 验证只进行了一次 DOM 查询
expect(queryCount).toBe(1);
// 清理
document.querySelectorAll('.mermaid').forEach(el => el.remove());
}
),
{ numRuns: 100 }
);
```
### 集成测试
**测试场景**
1. **完整渲染流程**:从页面加载到图表显示的完整流程
2. **插件兼容性**:与 WP Githuber MD、Markdown Block 等插件的集成
3. **主题切换**:日间/夜间模式切换时的图表重新渲染
4. **错误恢复**CDN 加载失败时的降级处理
**测试工具**:使用 Playwright 或 Puppeteer 进行端到端测试
### 手动测试清单
由于某些需求难以自动化测试,需要进行手动验证:
- [ ] 图表颜色与页面背景色有足够的对比度Requirements 4.4
- [ ] 图表容器样式与主题整体风格一致Requirements 6.2
- [ ] 所有 Mermaid 官方图表类型都能正确渲染Requirements 2.2
- [ ] 移动端显示效果良好Requirements 3.2
- [ ] 错误提示信息友好易懂Requirements 7.1
- [ ] 设置页预览功能正常工作Requirements 5.6
### 测试覆盖率目标
- **PHP 代码覆盖率**:≥ 80%
- **JavaScript 代码覆盖率**:≥ 85%
- **属性测试覆盖**:所有 18 个正确性属性
- **单元测试覆盖**:所有核心函数和边缘情况

View File

@@ -0,0 +1,143 @@
# Requirements Document
## Introduction
本文档定义了在 Argon WordPress 主题中实现 Mermaid 图表支持功能的需求。Mermaid 是一个基于 JavaScript 的图表和流程图生成工具,允许用户通过文本描述创建各种类型的可视化图表。
由于 WP-Markdown 编辑器的特殊渲染方式(将整个 Mermaid 代码块保存为单行,没有真正的换行符),直接集成 Mermaid 存在技术障碍。本需求文档采用插件集成方案,通过支持 Mermaid 的 WordPress 插件来实现功能。
## Glossary
- **Mermaid**: 基于 JavaScript 的图表生成库,支持流程图、时序图、类图等多种图表类型
- **WP-Markdown**: WordPress Markdown 编辑器,用于在 WordPress 中编辑 Markdown 格式的内容
- **Theme**: Argon WordPress 主题系统
- **Admin**: WordPress 后台管理员
- **CDN**: 内容分发网络,用于加载外部 JavaScript 库
- **Render_Engine**: Mermaid 图表渲染引擎
- **Code_Block**: Markdown 代码块,用于包含 Mermaid 图表定义
- **Theme_Mode**: 主题模式,包括日间模式和夜间模式
- **Settings_Page**: Argon 主题设置页面
## Requirements
### Requirement 1: Mermaid 库加载
**User Story:** 作为开发者,我希望主题能够正确加载 Mermaid 库,以便在文章中渲染图表。
#### Acceptance Criteria
1. WHEN Admin 在设置页启用 Mermaid 支持 THEN THE Theme SHALL 在前端页面加载 Mermaid JavaScript 库
2. WHERE CDN 模式被选择 WHEN 页面加载时 THEN THE Theme SHALL 从指定的 CDN 地址加载 Mermaid 库
3. WHERE 本地镜像模式被选择 WHEN 页面加载时 THEN THE Theme SHALL 从主题目录加载本地 Mermaid 库文件
4. WHEN Mermaid 库加载失败 THEN THE Theme SHALL 在控制台输出错误信息并降级到备用 CDN
5. THE Theme SHALL 仅在包含 Mermaid 代码块的页面加载 Mermaid 库
### Requirement 2: 图表渲染
**User Story:** 作为内容创作者,我希望在文章中使用 Mermaid 语法创建图表,并在前端正确显示。
#### Acceptance Criteria
1. WHEN 文章包含 Mermaid 代码块 THEN THE Render_Engine SHALL 将代码块渲染为可视化图表
2. THE Render_Engine SHALL 支持所有 Mermaid 官方图表类型流程图、时序图、类图、状态图、甘特图、饼图、Git 图等)
3. WHEN Mermaid 代码语法错误 THEN THE Render_Engine SHALL 显示友好的错误提示信息
4. WHEN 图表渲染成功 THEN THE Theme SHALL 移除原始代码块文本并显示渲染后的 SVG 图表
5. THE Render_Engine SHALL 在 DOM 加载完成后初始化并渲染所有图表
### Requirement 3: 响应式设计
**User Story:** 作为用户,我希望图表能够在不同设备上正确显示,以便在移动端也能查看。
#### Acceptance Criteria
1. WHEN 图表宽度超过容器宽度 THEN THE Theme SHALL 启用横向滚动功能
2. WHEN 用户在移动设备上查看 THEN THE Theme SHALL 自动调整图表大小以适应屏幕宽度
3. THE Theme SHALL 为图表容器设置最大宽度为 100%
4. WHEN 图表高度超过视口高度 THEN THE Theme SHALL 保持图表完整性不进行裁剪
5. THE Theme SHALL 在图表容器上应用响应式 CSS 样式
### Requirement 4: 主题模式适配
**User Story:** 作为用户,我希望图表能够适配夜间模式,以便在不同主题下都有良好的视觉效果。
#### Acceptance Criteria
1. WHEN 用户切换到夜间模式 THEN THE Theme SHALL 自动切换 Mermaid 图表主题为深色主题
2. WHEN 用户切换到日间模式 THEN THE Theme SHALL 自动切换 Mermaid 图表主题为浅色主题
3. THE Theme SHALL 在主题切换时重新渲染所有 Mermaid 图表
4. THE Theme SHALL 确保图表颜色与页面背景色有足够的对比度
5. WHERE Admin 设置了自定义图表主题 THEN THE Theme SHALL 使用自定义主题而不是自动切换
### Requirement 5: 后台设置
**User Story:** 作为管理员,我希望能够在后台配置 Mermaid 功能,以便根据需求调整行为。
#### Acceptance Criteria
1. THE Settings_Page SHALL 提供启用或禁用 Mermaid 支持的开关选项
2. THE Settings_Page SHALL 提供 CDN 地址选择选项jsDelivr、unpkg、自定义
3. THE Settings_Page SHALL 提供图表主题选择选项default、dark、forest、neutral
4. THE Settings_Page SHALL 提供本地镜像开关选项
5. WHEN Admin 保存设置 THEN THE Theme SHALL 验证 CDN 地址格式的有效性
6. THE Settings_Page SHALL 提供 Mermaid 配置预览功能
### Requirement 6: 样式优化
**User Story:** 作为内容创作者,我希望图表有美观的样式,以便提升文章的视觉质量。
#### Acceptance Criteria
1. THE Theme SHALL 为图表容器添加背景色、圆角和阴影样式
2. THE Theme SHALL 确保图表容器样式与主题整体风格一致
3. WHEN 图表在卡片中显示 THEN THE Theme SHALL 添加适当的内边距
4. THE Theme SHALL 为图表添加淡入动画效果
5. THE Theme SHALL 在夜间模式下调整图表容器的背景色和边框色
### Requirement 7: 错误处理
**User Story:** 作为开发者,我希望系统能够优雅地处理错误,以便快速定位和解决问题。
#### Acceptance Criteria
1. WHEN Mermaid 代码解析失败 THEN THE Theme SHALL 在图表位置显示错误提示信息
2. WHEN Mermaid 库加载失败 THEN THE Theme SHALL 在控制台输出详细的错误日志
3. THE Theme SHALL 在错误提示中包含错误类型和行号信息
4. WHEN 发生错误 THEN THE Theme SHALL 保留原始代码块以便用户修正
5. THE Theme SHALL 提供调试模式选项以输出详细的渲染过程信息
### Requirement 8: 性能优化
**User Story:** 作为用户,我希望图表加载不影响页面性能,以便获得流畅的浏览体验。
#### Acceptance Criteria
1. THE Theme SHALL 仅在包含 Mermaid 代码块的页面加载 Mermaid 库
2. THE Theme SHALL 使用异步方式加载 Mermaid 库
3. THE Theme SHALL 缓存已渲染的图表以避免重复渲染
4. WHEN 页面包含多个图表 THEN THE Theme SHALL 批量渲染以提高性能
5. THE Theme SHALL 使用 CDN 加速 Mermaid 库的加载速度
### Requirement 9: 插件兼容性
**User Story:** 作为管理员,我希望 Mermaid 功能能够与常用的 WordPress 插件兼容,以便灵活选择编辑器。
#### Acceptance Criteria
1. THE Theme SHALL 支持通过 WP Githuber MD 插件创建的 Mermaid 图表
2. THE Theme SHALL 支持通过 Markdown Block 插件创建的 Mermaid 图表
3. THE Theme SHALL 支持通过 Code Syntax Block 插件创建的 Mermaid 图表
4. WHEN 检测到多个 Mermaid 插件 THEN THE Theme SHALL 避免重复加载 Mermaid 库
5. THE Theme SHALL 提供插件兼容性检测功能
### Requirement 10: 代码块识别
**User Story:** 作为开发者,我希望系统能够准确识别 Mermaid 代码块,以便正确渲染图表。
#### Acceptance Criteria
1. THE Theme SHALL 识别 class 属性为 "mermaid" 的 div 元素作为 Mermaid 代码块
2. THE Theme SHALL 识别 language 属性为 "mermaid" 的 pre 或 code 元素作为 Mermaid 代码块
3. THE Theme SHALL 识别 data-lang 属性为 "mermaid" 的元素作为 Mermaid 代码块
4. WHEN 代码块同时包含多个识别标记 THEN THE Theme SHALL 优先使用 class 属性
5. THE Theme SHALL 忽略被注释掉的 Mermaid 代码块

View File

@@ -69,7 +69,7 @@
- **Validates: Requirements 8.4**
- _Requirements: 8.4_
- [~] 5. 实现错误处理机制
- [x] 5. 实现错误处理机制
- 添加库加载失败的降级处理(多个 CDN 备选)
- 实现代码解析错误的捕获和显示
- 创建错误提示 UI 组件

View File

@@ -0,0 +1,51 @@
---
inclusion: manual
---
# Mermaid 功能移除总结
## 移除原因
WP-Markdown 编辑器在保存 Markdown 文件时,会将 Mermaid 代码块保存为一整行(没有真正的换行符),导致 Mermaid 解析器无法正确解析,持续报错:`Parse error on line 1: Expecting 'NEWLINE', 'SPACE', 'GRAPH'`
尝试了多种解决方案JavaScript 解码、PHP 预处理、智能格式化等)均失败。
## 已移除内容
1. **settings.php** - 移除 Mermaid 设置项和选项保存逻辑(约 15 行)
2. **functions.php** - 移除两个 Mermaid 处理函数(约 70 行)
3. **footer.php** - 移除 Mermaid 加载和渲染代码(约 220 行)
4. **style.css** - 移除 Mermaid 图表样式(约 25 行)
5. **本地镜像** - 删除 `assets/vendor/external/mermaid/` 目录
总计移除约 330 行代码和 2 个本地镜像文件。
## 需求文档
已创建 `mermaid-support-requirements.md` 文档,包含:
- 问题详细分析
- 已尝试的解决方案
- 推荐的替代方案(使用支持 Mermaid 的插件)
- 技术实现参考
- 测试用例
## 推荐方案
使用支持 Mermaid 的 WordPress 插件:
- **WP Githuber MD** - 功能强大的 Markdown 编辑器
- **Markdown Block** - Gutenberg 原生 Markdown 块
- **Code Syntax Block** - 支持 Mermaid 的代码块插件
## Git 提交
```
commit 54cbb40
feat: 移除 Mermaid 支持并创建需求文档
- 从 settings.php 移除 Mermaid 设置项和选项保存逻辑
- 从 functions.php 移除 Mermaid 代码块预处理函数
- 从 footer.php 移除 Mermaid 加载和渲染代码
- 从 style.css 移除 Mermaid 图表样式
- 删除本地镜像文件 assets/vendor/external/mermaid/
- 创建 mermaid-support-requirements.md 需求文档
```

View File

@@ -4840,6 +4840,9 @@ void 0;
// ---------- 启动渲染引擎 ----------
// 暴露到全局作用域(用于库加载失败时的降级处理)
window.MermaidRenderer = MermaidRenderer;
// 在 DOM 加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {

View File

@@ -9340,6 +9340,168 @@ function argon_add_mermaid_async_attribute($tag, $handle) {
return $tag;
}
// 添加 async 属性
return str_replace(' src', ' async src', $tag);
// 添加 async 属性和 onerror 事件处理
$tag = str_replace(' src', ' async onerror="argonMermaidLoadFallback()" src', $tag);
return $tag;
}
/**
* 添加 Mermaid 库加载失败的降级处理脚本
*/
function argon_add_mermaid_fallback_script() {
// 只在启用 Mermaid 且页面包含 Mermaid 代码块时添加
if (!argon_get_mermaid_option('enabled', false)) {
return;
}
global $post;
$has_mermaid = false;
if (is_singular() && isset($post->post_content)) {
$has_mermaid = argon_has_mermaid_content($post->post_content);
}
if (!$has_mermaid) {
return;
}
// 获取备用 CDN URL 列表
$fallback_urls = argon_get_mermaid_fallback_urls();
$fallback_urls_json = json_encode($fallback_urls);
// 输出降级处理脚本
?>
<script>
// Mermaid 库加载失败的降级处理
(function() {
'use strict';
// 备用 CDN URL 列表
const fallbackUrls = <?php echo $fallback_urls_json; ?>;
let loadAttempted = false;
/**
* 尝试从备用 CDN 加载 Mermaid 库
*/
window.argonMermaidLoadFallback = function() {
// 避免重复调用
if (loadAttempted) {
return;
}
loadAttempted = true;
console.warn('[Argon Mermaid] 主 CDN 加载失败,尝试备用 CDN');
// 尝试加载备用 CDN
loadMermaidWithFallback(fallbackUrls, 0);
};
/**
* 递归加载备用 CDN
* @param {Array} urls - CDN URL 列表
* @param {number} index - 当前尝试的索引
*/
function loadMermaidWithFallback(urls, index) {
// 如果所有 CDN 都失败了
if (index >= urls.length) {
console.error('[Argon Mermaid] 所有 CDN 加载失败');
showGlobalError();
return;
}
const url = urls[index];
console.log(`[Argon Mermaid] 尝试从备用 CDN 加载: ${url}`);
// 创建 script 标签
const script = document.createElement('script');
script.src = url;
script.async = true;
// 加载失败,尝试下一个 CDN
script.onerror = function() {
console.warn(`[Argon Mermaid] CDN ${url} 加载失败`);
loadMermaidWithFallback(urls, index + 1);
};
// 加载成功,初始化 Mermaid
script.onload = function() {
console.log(`[Argon Mermaid] 成功从备用 CDN 加载: ${url}`);
// 等待 MermaidRenderer 可用
if (typeof window.MermaidRenderer !== 'undefined') {
window.MermaidRenderer.init();
} else {
// 如果 MermaidRenderer 还未定义,等待一下
setTimeout(function() {
if (typeof window.MermaidRenderer !== 'undefined') {
window.MermaidRenderer.init();
}
}, 100);
}
};
// 添加到页面
document.head.appendChild(script);
}
/**
* 显示全局错误提示
*/
function showGlobalError() {
// 查找所有 Mermaid 代码块
const selectors = [
'div.mermaid',
'pre code.language-mermaid',
'pre[data-lang="mermaid"]',
'code.mermaid'
];
let blocks = [];
selectors.forEach(function(selector) {
const elements = document.querySelectorAll(selector);
elements.forEach(function(element) {
if (!blocks.includes(element)) {
blocks.push(element);
}
});
});
// 在每个代码块位置显示错误提示
blocks.forEach(function(block) {
const errorContainer = document.createElement('div');
errorContainer.className = 'mermaid-error-container';
errorContainer.innerHTML = `
<div class="mermaid-error-header">
<span class="mermaid-error-icon">⚠️</span>
<span class="mermaid-error-title">Mermaid 库加载失败</span>
</div>
<div class="mermaid-error-body">
<p class="mermaid-error-type">错误类型: 网络错误</p>
<p class="mermaid-error-message">无法从任何 CDN 加载 Mermaid 库,请检查网络连接或联系管理员。</p>
</div>
<details class="mermaid-error-code">
<summary>查看原始代码</summary>
<pre><code class="language-mermaid">${escapeHtml(block.textContent)}</code></pre>
</details>
`;
block.parentNode.replaceChild(errorContainer, block);
});
}
/**
* HTML 转义
* @param {string} text - 要转义的文本
* @returns {string} 转义后的文本
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
})();
</script>
<?php
}
add_action('wp_head', 'argon_add_mermaid_fallback_script');

View File

@@ -0,0 +1,374 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mermaid 库加载失败降级处理测试</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
.test-section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 {
color: #333;
border-bottom: 2px solid #5e72e4;
padding-bottom: 10px;
}
h2 {
color: #5e72e4;
margin-top: 0;
}
.mermaid {
background: #f8f9fa;
padding: 20px;
border-radius: 4px;
margin: 10px 0;
}
.mermaid-error-container {
background: #fff5f5;
border: 1px solid #fc8181;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.mermaid-error-header {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.mermaid-error-icon {
font-size: 24px;
margin-right: 10px;
}
.mermaid-error-title {
font-size: 18px;
font-weight: bold;
color: #c53030;
}
.mermaid-error-body {
margin-bottom: 15px;
}
.mermaid-error-type {
font-weight: bold;
color: #742a2a;
margin: 5px 0;
}
.mermaid-error-message {
color: #742a2a;
margin: 5px 0;
}
.mermaid-error-code {
margin-top: 10px;
}
.mermaid-error-code summary {
cursor: pointer;
color: #5e72e4;
font-weight: bold;
}
.mermaid-error-code pre {
background: #2d3748;
color: #e2e8f0;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
margin-top: 10px;
}
.test-log {
background: #2d3748;
color: #e2e8f0;
padding: 15px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 14px;
max-height: 300px;
overflow-y: auto;
margin-top: 10px;
}
.test-log div {
margin: 5px 0;
}
.log-info {
color: #63b3ed;
}
.log-warn {
color: #f6ad55;
}
.log-error {
color: #fc8181;
}
.log-success {
color: #68d391;
}
.test-controls {
margin: 20px 0;
}
.test-controls button {
background: #5e72e4;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
font-size: 14px;
}
.test-controls button:hover {
background: #4c63d2;
}
.test-controls button:disabled {
background: #cbd5e0;
cursor: not-allowed;
}
</style>
</head>
<body>
<h1>🧪 Mermaid 库加载失败降级处理测试</h1>
<div class="test-section">
<h2>测试说明</h2>
<p>本测试页面用于验证 Mermaid 库加载失败时的降级处理机制。</p>
<ul>
<li><strong>测试 1</strong>: 主 CDN 加载失败,自动尝试备用 CDN</li>
<li><strong>测试 2</strong>: 所有 CDN 都失败,显示友好的错误提示</li>
<li><strong>测试 3</strong>: 备用 CDN 加载成功,正常渲染图表</li>
</ul>
</div>
<div class="test-section">
<h2>测试控制</h2>
<div class="test-controls">
<button onclick="testFailedMainCDN()">测试 1: 主 CDN 失败</button>
<button onclick="testAllCDNsFailed()">测试 2: 所有 CDN 失败</button>
<button onclick="testSuccessfulFallback()">测试 3: 备用 CDN 成功</button>
<button onclick="clearLog()">清空日志</button>
</div>
<div id="testLog" class="test-log"></div>
</div>
<div class="test-section">
<h2>测试图表</h2>
<div class="mermaid">
flowchart TD
A[开始] --> B{主 CDN 加载}
B -->|成功| C[渲染图表]
B -->|失败| D[尝试备用 CDN 1]
D -->|成功| C
D -->|失败| E[尝试备用 CDN 2]
E -->|成功| C
E -->|失败| F[显示错误提示]
</div>
</div>
<script>
// 模拟降级处理脚本
(function() {
'use strict';
// 备用 CDN URL 列表
const fallbackUrls = [
'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js',
'https://unpkg.com/mermaid@10/dist/mermaid.min.js',
'https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.0.0/mermaid.min.js'
];
let loadAttempted = false;
// 日志函数
function log(message, type = 'info') {
const logDiv = document.getElementById('testLog');
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.className = `log-${type}`;
logEntry.textContent = `[${timestamp}] ${message}`;
logDiv.appendChild(logEntry);
logDiv.scrollTop = logDiv.scrollHeight;
// 同时输出到控制台
console.log(`[Argon Mermaid] ${message}`);
}
/**
* 尝试从备用 CDN 加载 Mermaid 库
*/
window.argonMermaidLoadFallback = function() {
// 避免重复调用
if (loadAttempted) {
return;
}
loadAttempted = true;
log('主 CDN 加载失败,尝试备用 CDN', 'warn');
// 尝试加载备用 CDN
loadMermaidWithFallback(fallbackUrls, 0);
};
/**
* 递归加载备用 CDN
* @param {Array} urls - CDN URL 列表
* @param {number} index - 当前尝试的索引
*/
function loadMermaidWithFallback(urls, index) {
// 如果所有 CDN 都失败了
if (index >= urls.length) {
log('所有 CDN 加载失败', 'error');
showGlobalError();
return;
}
const url = urls[index];
log(`尝试从备用 CDN 加载: ${url}`, 'info');
// 创建 script 标签
const script = document.createElement('script');
script.src = url;
script.async = true;
// 加载失败,尝试下一个 CDN
script.onerror = function() {
log(`CDN ${url} 加载失败`, 'warn');
loadMermaidWithFallback(urls, index + 1);
};
// 加载成功,初始化 Mermaid
script.onload = function() {
log(`成功从备用 CDN 加载: ${url}`, 'success');
// 初始化 Mermaid
if (typeof window.mermaid !== 'undefined') {
window.mermaid.initialize({
startOnLoad: true,
theme: 'default'
});
log('Mermaid 初始化成功', 'success');
}
};
// 添加到页面
document.head.appendChild(script);
}
/**
* 显示全局错误提示
*/
function showGlobalError() {
// 查找所有 Mermaid 代码块
const selectors = [
'div.mermaid',
'pre code.language-mermaid',
'pre[data-lang="mermaid"]',
'code.mermaid'
];
let blocks = [];
selectors.forEach(function(selector) {
const elements = document.querySelectorAll(selector);
elements.forEach(function(element) {
if (!blocks.includes(element)) {
blocks.push(element);
}
});
});
log(`找到 ${blocks.length} 个 Mermaid 代码块,显示错误提示`, 'info');
// 在每个代码块位置显示错误提示
blocks.forEach(function(block) {
const errorContainer = document.createElement('div');
errorContainer.className = 'mermaid-error-container';
errorContainer.innerHTML = `
<div class="mermaid-error-header">
<span class="mermaid-error-icon">⚠️</span>
<span class="mermaid-error-title">Mermaid 库加载失败</span>
</div>
<div class="mermaid-error-body">
<p class="mermaid-error-type">错误类型: 网络错误</p>
<p class="mermaid-error-message">无法从任何 CDN 加载 Mermaid 库,请检查网络连接或联系管理员。</p>
</div>
<details class="mermaid-error-code">
<summary>查看原始代码</summary>
<pre><code class="language-mermaid">${escapeHtml(block.textContent)}</code></pre>
</details>
`;
block.parentNode.replaceChild(errorContainer, block);
});
}
/**
* HTML 转义
* @param {string} text - 要转义的文本
* @returns {string} 转义后的文本
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 测试函数
window.testFailedMainCDN = function() {
log('=== 开始测试 1: 主 CDN 失败 ===', 'info');
loadAttempted = false;
argonMermaidLoadFallback();
};
window.testAllCDNsFailed = function() {
log('=== 开始测试 2: 所有 CDN 失败 ===', 'info');
loadAttempted = false;
// 使用无效的 URL 列表
loadMermaidWithFallback(['https://invalid-cdn-1.com/mermaid.js', 'https://invalid-cdn-2.com/mermaid.js'], 0);
};
window.testSuccessfulFallback = function() {
log('=== 开始测试 3: 备用 CDN 成功 ===', 'info');
loadAttempted = false;
// 直接加载有效的 CDN
loadMermaidWithFallback(fallbackUrls, 0);
};
window.clearLog = function() {
document.getElementById('testLog').innerHTML = '';
log('日志已清空', 'info');
};
// 页面加载完成后的初始化
log('测试页面加载完成', 'success');
log('点击上方按钮开始测试', 'info');
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,249 @@
<?php
/**
* Mermaid 库加载失败降级处理测试
*
* 测试场景:
* 1. 主 CDN 加载失败时,自动尝试备用 CDN
* 2. 所有 CDN 都失败时,显示友好的错误提示
* 3. 备用 CDN 加载成功后,正常初始化渲染引擎
*/
// 加载 WordPress 环境
require_once dirname(__FILE__) . '/../../../wp-load.php';
// 加载主题函数
require_once get_template_directory() . '/functions.php';
/**
* 测试 1: 验证备用 CDN URL 列表
*/
function test_fallback_urls() {
echo "测试 1: 验证备用 CDN URL 列表\n";
echo str_repeat('-', 50) . "\n";
$fallback_urls = argon_get_mermaid_fallback_urls();
// 验证返回的是数组
if (!is_array($fallback_urls)) {
echo "❌ 失败: 返回值不是数组\n";
return false;
}
// 验证至少有 3 个备用 URL
if (count($fallback_urls) < 3) {
echo "❌ 失败: 备用 URL 数量少于 3 个\n";
return false;
}
// 验证每个 URL 都是有效的
foreach ($fallback_urls as $index => $url) {
if (empty($url)) {
echo "❌ 失败: URL #{$index} 为空\n";
return false;
}
// 验证 URL 格式
if (!preg_match('/^https?:\/\/.+\.js$/', $url) && !preg_match('/\/mermaid\.min\.js$/', $url)) {
echo "❌ 失败: URL #{$index} 格式无效: {$url}\n";
return false;
}
echo "✓ URL #{$index}: {$url}\n";
}
echo "✅ 通过: 备用 CDN URL 列表验证成功\n\n";
return true;
}
/**
* 测试 2: 验证降级处理脚本生成
*/
function test_fallback_script_generation() {
echo "测试 2: 验证降级处理脚本生成\n";
echo str_repeat('-', 50) . "\n";
// 启用 Mermaid
update_option('argon_mermaid_enabled', true);
// 创建一个包含 Mermaid 代码块的测试文章
global $post;
$post = (object) [
'ID' => 1,
'post_content' => '<div class="mermaid">flowchart TD\nA-->B</div>'
];
// 捕获输出
ob_start();
argon_add_mermaid_fallback_script();
$output = ob_get_clean();
// 验证输出包含必要的脚本
$checks = [
'argonMermaidLoadFallback' => '降级处理函数',
'loadMermaidWithFallback' => '递归加载函数',
'showGlobalError' => '错误提示函数',
'fallbackUrls' => '备用 URL 列表',
'script.onerror' => '错误处理',
'script.onload' => '加载成功处理'
];
$all_passed = true;
foreach ($checks as $keyword => $description) {
if (strpos($output, $keyword) !== false) {
echo "✓ 包含 {$description}\n";
} else {
echo "❌ 缺少 {$description}\n";
$all_passed = false;
}
}
if ($all_passed) {
echo "✅ 通过: 降级处理脚本生成正确\n\n";
return true;
} else {
echo "❌ 失败: 降级处理脚本不完整\n\n";
return false;
}
}
/**
* 测试 3: 验证 onerror 属性添加
*/
function test_onerror_attribute() {
echo "测试 3: 验证 onerror 属性添加\n";
echo str_repeat('-', 50) . "\n";
// 模拟脚本标签
$original_tag = '<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>';
// 调用函数添加属性
$modified_tag = argon_add_mermaid_async_attribute($original_tag, 'mermaid');
// 验证包含 async 属性
if (strpos($modified_tag, 'async') === false) {
echo "❌ 失败: 缺少 async 属性\n";
return false;
}
echo "✓ 包含 async 属性\n";
// 验证包含 onerror 属性
if (strpos($modified_tag, 'onerror') === false) {
echo "❌ 失败: 缺少 onerror 属性\n";
return false;
}
echo "✓ 包含 onerror 属性\n";
// 验证 onerror 调用正确的函数
if (strpos($modified_tag, 'argonMermaidLoadFallback()') === false) {
echo "❌ 失败: onerror 函数名不正确\n";
return false;
}
echo "✓ onerror 调用正确的函数\n";
echo "修改后的标签: {$modified_tag}\n";
echo "✅ 通过: onerror 属性添加正确\n\n";
return true;
}
/**
* 测试 4: 验证非 Mermaid 脚本不受影响
*/
function test_other_scripts_unaffected() {
echo "测试 4: 验证非 Mermaid 脚本不受影响\n";
echo str_repeat('-', 50) . "\n";
// 模拟其他脚本标签
$original_tag = '<script src="https://example.com/other-script.js"></script>';
// 调用函数
$modified_tag = argon_add_mermaid_async_attribute($original_tag, 'other-script');
// 验证标签未被修改
if ($original_tag === $modified_tag) {
echo "✓ 非 Mermaid 脚本未被修改\n";
echo "✅ 通过: 其他脚本不受影响\n\n";
return true;
} else {
echo "❌ 失败: 非 Mermaid 脚本被错误修改\n";
echo "原始: {$original_tag}\n";
echo "修改: {$modified_tag}\n\n";
return false;
}
}
/**
* 测试 5: 验证 JSON 编码的备用 URL
*/
function test_json_encoded_urls() {
echo "测试 5: 验证 JSON 编码的备用 URL\n";
echo str_repeat('-', 50) . "\n";
$fallback_urls = argon_get_mermaid_fallback_urls();
$json = json_encode($fallback_urls);
// 验证 JSON 编码成功
if ($json === false) {
echo "❌ 失败: JSON 编码失败\n";
return false;
}
echo "✓ JSON 编码成功\n";
// 验证可以解码回数组
$decoded = json_decode($json, true);
if (!is_array($decoded)) {
echo "❌ 失败: JSON 解码失败\n";
return false;
}
echo "✓ JSON 解码成功\n";
// 验证解码后的数组与原数组一致
if ($decoded !== $fallback_urls) {
echo "❌ 失败: 解码后的数组与原数组不一致\n";
return false;
}
echo "✓ 解码后的数组与原数组一致\n";
echo "JSON: {$json}\n";
echo "✅ 通过: JSON 编码验证成功\n\n";
return true;
}
// 运行所有测试
echo "\n";
echo "=================================================\n";
echo "Mermaid 库加载失败降级处理测试\n";
echo "=================================================\n\n";
$tests = [
'test_fallback_urls',
'test_fallback_script_generation',
'test_onerror_attribute',
'test_other_scripts_unaffected',
'test_json_encoded_urls'
];
$passed = 0;
$failed = 0;
foreach ($tests as $test) {
if ($test()) {
$passed++;
} else {
$failed++;
}
}
// 输出测试总结
echo "=================================================\n";
echo "测试总结\n";
echo "=================================================\n";
echo "通过: {$passed} 个测试\n";
echo "失败: {$failed} 个测试\n";
if ($failed === 0) {
echo "\n✅ 所有测试通过!\n";
exit(0);
} else {
echo "\n❌ 部分测试失败,请检查实现。\n";
exit(1);
}