# Xaringan 行间公式测试指引

**Solution 1（配对资产）**：`wrap-display-math.R`（knit 时自动反引号包裹）+ `js/mathjax-config.js`（MathJax 2.x 配置）

二者**必须同时使用**方可覆盖 Quarto 式三行 `$$` 行间公式与一行多 `$...$` 行内公式。缺任一组件见负向模块 `mod-negative-*`。

本指引以 [`js/mathjax/test/`](.) 为相对路径根目录。通用验证工作流见 [`docs/harness-asset-validation-workflow.qmd`](../../../docs/harness-asset-validation-workflow.qmd)。

---

## 1. 测试目标与防污染原则

### 1.1 四层验证

| 层级 | 验证内容 |
|------|----------|
| **Knit** | `wrap-display-math.R` 将三行 `$$` 转为 `` `$$...$$` `` |
| **HTML** | `<textarea id="source">` 内为 remark 兼容写法 |
| **浏览器** | MathJax 正确渲染行内/行间公式 |
| **回归** | 边界用例、负向对照、包升级后复测 |

### 1.2 三层测试（L0 / L1 / L2）

| 层级 | 工具 | 说明 |
|------|------|------|
| **L0** | `Rscript Rscript/test-wrap-display-math.R` | 纯函数单元测试，不 render |
| **L1** | `Rscript Rscript/run-module.R <模块名>` | **每模块独立 R 进程**，输出到 `output/modules/<名>/` |
| **L2** | `Rscript Rscript/run-all-modules.R` | L1 全绿后的冒烟；仍分进程 |

### 1.3 防污染规则

| 风险 | 防护 |
|------|------|
| 同 session 多次 render 导致 knit hook 残留 | L1 **必须**用 `run-module.R`（新 R 进程） |
| 正负用例串扰 | 负向仅在 `mod-negative-no-hook.Rmd` / `mod-negative-no-beforeinit.Rmd` |
| 误改 canonical | 迭代只改 `test/js/`、`test/Rscript/` 镜像；L1 全绿后再晋升 `../mathjax-config.js`、`../wrap-display-math.R` |
| 看错 HTML | 产物在 `output/`，已 gitignore |

**反模式**：在同一 RStudio 会话内连续 `render()` 正向与负向 deck（参见 §6.4）。

### 1.4 模块幻灯片格式：`(raw source)` + `(result view)`

`modules/*.Rmd` 中**每个用例占两页幻灯片**，便于测试人员对照：

| 幻灯片标题 | 内容 |
|------------|------|
| **`(raw source) TC-…`** | 用四反引号 ` `````markdown ` 展示**未执行的 Markdown 源码**（与编写 Rmd 时一致） |
| **`(result view) TC-…`** | **实际 remark 解析**的幻灯片正文（与 knit 后进入 `<textarea id="source">` 的路径一致） |

检查顺序建议：先看 **(raw source)** 确认用例写法 → 再看 **(result view)** 在浏览器中是否渲染 → 必要时查 HTML textarea 中 hook 处理后的字符串。

**remark class 用例（TC-I05–I11）**：**(result view)** 须通过 `_setup-chunk.R` 中的 `remark_source()`（`results='asis'`）注入原始 remark 行，避免 knitr 在 class 括号内把 `$...$` 预处理成反引号代码。TC-I04 对照页仍走普通 Markdown 路径。

**浏览器打开路径**：务必打开 **`output/modules/<模块名>/<模块名>.html`**（`run-module.R` 会打包 `libs/`、`js/mathjax-config.js` 与修正后的脚本路径）。勿只复制裸 HTML——否则 `default.css`、`.red` / `.pull-left` 样式与 MathJax 配置均无法加载。

样式见 [`mycss/xaringan-mod-test.css`](mycss/xaringan-mod-test.css)（含 `.red` 与分栏视觉辅助）。

---

## 2. 环境准备

### 2.1 最小 R 依赖

```r
install.packages(c("rmarkdown", "xaringan", "knitr"))
sessionInfo()  # 记录版本，写入本地 TEST-LOG.md
```

记录：R 版本、`knitr` / `rmarkdown` / `xaringan` 版本、OS、浏览器（Chrome / Edge）。

`renv/` 可选，**勿提交** Git。

### 2.2 目录结构（harness test 沙盒）

```text
js/mathjax/test/
├── modules/                    # L1 模块化 Rmd（每用例 raw source + result view 两页）
│   ├── mod-display-core.Rmd
│   ├── mod-display-edge.Rmd
│   ├── mod-inline-core.Rmd
│   ├── mod-inline-remark-classes.Rmd
│   ├── mod-negative-no-hook.Rmd
│   ├── mod-negative-no-beforeinit.Rmd
│   └── _setup-chunk.R
├── js/mathjax-config.js        # test 镜像
├── Rscript/wrap-display-math.R # test 镜像
├── Rscript/run-module.R
├── Rscript/run-all-modules.R
├── Rscript/test-wrap-display-math.R
└── output/modules/             # gitignore
```

Canonical 可交付文件位于上一级 [`js/mathjax/`](../)：`mathjax-config.js`、`wrap-display-math.R`。

### 2.3 配对 YAML 模板（正向模块）

模块 Rmd 位于 `modules/` 时：

```yaml
---
output:
  xaringan::moon_reader:
    lib_dir: ../libs
    nature:
      beforeInit: "../js/mathjax-config.js"
---
```

```r
```{r setup, include=FALSE}
source(file.path(dirname(knitr::current_input(dir = TRUE)), "_setup-chunk.R"))
```
```

`_setup-chunk.R` 会 `source` 上一级 `Rscript/wrap-display-math.R`。

### 2.4 首次 render 前检查

- [ ] `beforeInit` 指向 `../js/mathjax-config.js`（相对 `modules/*.Rmd`）
- [ ] setup 已 source `wrap-display-math.R`
- [ ] 使用 `run-module.R` 而非交互式重复 render

---

## 3. 执行流程

```mermaid
flowchart LR
  A[改 test 镜像] --> B[L0 单元测试]
  B --> C[L1 run-module 单模块]
  C --> D[inspect-html 可选]
  D --> E[浏览器目视]
  E --> F[更新 TEST-LOG]
  F --> G[L1 全绿后晋升 canonical]
```

### 3.1 三层检查（每个正向模块）

**层 A — HTML 源码**

在 `output/modules/<模块>/<模块>.html` 的 `<textarea id="source">` 中：

| 模式 | 期望 |
|------|------|
| `` `$$Y_{i}=` `` | 行间公式已反引号包裹 |
| 裸 `$$` 单独成行 | 仅出现在**负向**或边界用例幻灯片 |
| `` `\(` `` | 行内公式经 knitr 转为 `\(...\)` |

**层 B — 浏览器**

- 对每个用例：在 **(result view)** 幻灯片检查公式；与 **(raw source)** 幻灯片对照写法
- 行间公式居中；符号完整；标签 `(1)` 可见
- 一行多 `$...$` 均渲染

**层 C — DevTools**

- Console 无 MathJax TeX 致命错误
- Network：`MathJax.js` 从 CDN 加载

---

## 4. 用例矩阵与模块映射

| 模块 | 用例 ID | 说明 |
|------|---------|------|
| `mod-display-core` | TC-D01–D04 | 核心行间公式（必过） |
| `mod-display-edge` | TC-D05–D09 | 边界与 hook 行为 |
| `mod-inline-core` | TC-I01–I03 | 行内公式（必过） |
| `mod-inline-remark-classes` | TC-I04–I11 | **remark 特殊 class 内行内公式**（手动浏览器检查；hook 不覆盖） |
| `mod-negative-no-hook` | TC-X01 | 无 wrap hook，预期失败 |
| `mod-negative-no-beforeinit` | TC-X02 | 无 beforeInit，预期失败 |

### 4.1 TC-D01（`mod-display-core`）

**(raw source)** 幻灯片展示：

````markdown
$$
Y_{i}=\beta_{0}+\beta_{1} X_{i}+\epsilon_{i} \quad \text{(1)}
$$
````

**(result view)** 幻灯片期望：浏览器居中显示 `(1)`；HTML 含 `` `$$Y_{i}=...$$` ``。

### 4.2 TC-I01（`mod-inline-core`）

**(result view)**：`When $a=1$ and $b=2$ we have $c=a+b$.` 中三个行内公式均渲染。

### 4.3 remark.js 结构性问题：特殊 class 内的行内公式（TC-I04–I11）

**`wrap-display-math.R` 与 `mathjax-config.js` 均无法修复**——属于 remark 对 `.footnote[...]`、`.red[...]`、`.pull-left[...]` 等括号内容的解析行为。

| 用例 | 幻灯片写法 | 常见现象 | 对策 |
|------|------------|----------|------|
| TC-I04 | 普通正文 `$...$` | 应正常渲染 | 作对照基线 |
| TC-I05 | `.pull-left[$...$]` | 可能正常或部分失败 | 优先 plain body；必要时拆 slide |
| TC-I06 | `.red[$...$]` | v2 幻灯片常见；**潜在雷区** | 实测后记录；可改用 `\(...\)` 或移出 class |
| TC-I07 | `.red[\(...\)]` | remark **吃掉 `\`**，显示为 inline code | 勿依赖裸 `\(...\)`  inside class |
| TC-I08 | `.footnote[\(...\)]` | 同上；脚注区另需浏览器目视 | 脚注内容避免复杂公式，或 HTML  escape |
| TC-I09 | `.red[`\(...\)`]` | 反引号包裹定界符（workaround） | 在 class 内用 `` `\(...\)` `` 试跑 |
| TC-I10 | `.pull-left[\(...\)]` | 与 TC-I07 同类 | 同 TC-I09 或移出 layout class |
| TC-I11 | R 注入 raw `.red[$...$]` / `\(...\)` | 绕过 knitr 数学预处理，最接近 v2 源码 | 浏览器目视 + 查 textarea 原文 |

**执行（L1，手动层 B 必做）**：

```powershell
Rscript Rscript/run-module.R mod-inline-remark-classes
Rscript Rscript/inspect-remark-classes.R mod-inline-remark-classes
```

打开 `output/modules/mod-inline-remark-classes/mod-inline-remark-classes.html`，逐页对照 **(raw source)** 与 **(result view)**（TC-I04–I11）。**目视检查项**：TC-I05 左右分栏（浅蓝/浅橙背景）、TC-I06 红色文字、TC-I08 页脚脚注区。**本模块不纳入 L2 必过**。

### 4.4 TC-X01 / TC-X02（负向）

- **TC-X01**：有 `beforeInit`、**无** `source(wrap-display-math.R)` → HTML 中裸 `$$`
- **TC-X02**：有 hook、**无** `beforeInit` → MathJax 不工作或行内公式异常

负向模块**单独** `run-module.R` 执行，不与正向模块同 session。

更多边界用例（TC-D05–D09）见 [`modules/mod-display-edge.Rmd`](modules/mod-display-edge.Rmd)。

---

## 5. 自动化

### 5.1 L0 单元测试

```powershell
cd D:\github\harness\js\mathjax\test
Rscript Rscript/test-wrap-display-math.R
```

### 5.2 L1 单模块 render

```powershell
Rscript Rscript/run-module.R mod-display-core
Rscript Rscript/inspect-html.R mod-display-core
```

### 5.3 L2 全模块冒烟

```powershell
Rscript Rscript/run-all-modules.R
```

---

## 6. 特殊场景

### 6.1 打印 / PDF

浏览器打开 HTML → 翻遍幻灯片 → 打印为 PDF → 抽查 TC-D01、TC-I01。

### 6.2 演讲者模式

扩展屏非镜像；核对公式双屏一致。

### 6.3 离线 / 弱网

断网打开 HTML；若 CDN 不可用则 MathJax 失败（课堂场景需自备本地 MathJax）。

### 6.4 同 session 多 deck（反模式）

```r
# 不推荐：hook 可能被覆盖或残留
rmarkdown::render("modules/mod-display-core.Rmd")
rmarkdown::render("modules/mod-negative-no-hook.Rmd")
rmarkdown::render("modules/mod-display-core.Rmd")
```

**应改用** `run-module.R`，每次新 R 进程。

### 6.5 包升级回归

升级 `knitr` / `rmarkdown` / `xaringan` 后重跑 L2 + TC-D01 + TC-I01，并抽样 `mod-inline-remark-classes`（TC-I06、TC-I07）。

### 6.6 remark 特殊 class（结构性限制）

与 §4.4 相同：`.red[...]` + `$...$` 在 v2 幻灯片中仍是潜在雷区；**勿**通过增强 hook 解决。升级后运行 `mod-inline-remark-classes` 并浏览器目视 TC-I04 vs TC-I07。

---

## 7. 测试记录模板（`TEST-LOG.md`，本地 gitignore）

复制 [`TEST-LOG.example.md`](TEST-LOG.example.md) 为 `TEST-LOG.md` 后填写；或使用 §11 一键脚本 `run-check-all.R` 跑完自动化项后再补浏览器列。

```markdown
## 轮次：YYYY-MM-DD | R x.x | knitr x.x | xaringan x.x

| 模块 | L1 HTML | 浏览器 | 备注 |
|------|---------|--------|------|
| mod-display-core | pass | pass | |
| mod-display-edge | pass | pass | |
| mod-inline-core | pass | pass | |
| mod-inline-remark-classes | pass | manual TC-I04–I11 | remark class 限制见 §4.3 |

资产 hash（可选）：
- test/js/mathjax-config.js
- test/Rscript/wrap-display-math.R
```

---

## 8. 必过用例（晋升前）

- L0 单元测试
- L1：`mod-display-core`、`mod-inline-core`
- L2：`run-all-modules.R` 全绿
- 一键自动化：`Rscript Rscript/run-check-all.R`（含 bundle 校验与负向 render）
- 手动：`mod-inline-remark-classes` 浏览器目视 TC-I04–I11（§4.3）

---

## 9. Bug 报告模板

```markdown
### Bug：[简短标题]

**环境：** R / knitr / xaringan / OS / 浏览器  
**用例 ID：** TC-xxx  
**模块：** mod-xxx  
**最小幻灯片源码：**  
**HTML 现象：** 裸 `$$` / 双反引号 / 其他  
**浏览器：** 截图 + Console 完整报错  
**期望 vs 实际：**  
**修复层级：** hook / LaTeX / remark 限制  
**临时规避：**  
```

---

## 10. 晋升与外部分发

L1 必过模块全绿后：

1. 将 `test/js/mathjax-config.js` → [`../mathjax-config.js`](../mathjax-config.js)
2. 将 `test/Rscript/wrap-display-math.R` 逻辑 → [`../wrap-display-math.R`](../wrap-display-math.R)（或 diff 合并）
3. 更新 [`docs/mathjax-xaringan-assets.qmd`](../../../docs/mathjax-xaringan-assets.qmd)
4. Git 提交 **仅** canonical 与 test 规程，不提交 `output/`、`libs/`、`TEST-LOG.md`

外部分发至 course-em-eng 等业务项目时，**两文件一起拷贝**：

- `slide-xaringan/js/mathjax-config.js`
- `slide-xaringan/Rscript/wrap-display-math.R`

并在 Rmd 中成对配置 `beforeInit` + `source(wrap-display-math.R)`。

---

## 11. 首次执行顺序

1. 安装 §2.1 依赖，确认目录 §2.2  
2. `Rscript Rscript/run-check-all.R`（推荐：L0 + L2 + bundle + 负向 render）  
3. 浏览器打开 `output/modules/mod-display-core/mod-display-core.html`，检查 TC-D01 **(result view)**  
4. 浏览器打开 `output/modules/mod-inline-core/mod-inline-core.html`，检查 TC-I01 **(result view)**  
5. 浏览器打开 `output/modules/mod-inline-remark-classes/mod-inline-remark-classes.html`，对照 TC-I04–I11 **(raw source)** vs **(result view)**  
6. 复制 `TEST-LOG.example.md` → `TEST-LOG.md` 并填写 §7  
7. 晋升 §10 canonical 并同步 course-em-eng（成对拷贝两文件）  
