@chengzhao-dev/dsh-quarto-book 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.en.md +60 -31
  2. package/README.md +53 -24
  3. package/lib/tools/render-check.js +1 -1
  4. package/package.json +5 -2
  5. package/skills/designing-theme/assets/theme/assets/fonts/lxgw-bright-code-common.woff2 +0 -0
  6. package/skills/designing-theme/assets/theme/assets/fonts/lxgw-bright-medium-common.woff2 +0 -0
  7. package/skills/designing-theme/assets/theme/css/mermaid.css +5 -6
  8. package/skills/designing-theme/knowledge/reading-surface.md +1 -1
  9. package/skills/designing-theme/references/theme-system.md +5 -5
  10. package/skills/designing-theme/scripts/checking-layout.py +49 -9
  11. package/skills/writing-quarto/examples/02-structure-shape.md +26 -12
  12. package/skills/writing-quarto/examples/03-block-after.md +14 -0
  13. package/skills/writing-quarto/examples/03-qmd-elements.md +16 -0
  14. package/skills/writing-quarto/examples/04-paragraph-cases.md +14 -0
  15. package/skills/writing-quarto/examples/04-paragraph-opening.md +2 -2
  16. package/skills/writing-quarto/examples/05-sentence-frame.md +4 -4
  17. package/skills/writing-quarto/examples/06-wording-cases.md +23 -0
  18. package/skills/writing-quarto/examples/07-layout-list.md +3 -3
  19. package/skills/writing-quarto/examples/08-appendix.md +1 -1
  20. package/skills/writing-quarto/quarto/authoring.md +4 -4
  21. package/skills/writing-quarto/quarto/basics.md +16 -16
  22. package/skills/writing-quarto/quarto/mermaid.md +41 -5
  23. package/skills/writing-quarto/quarto/prose.md +4 -4
  24. package/skills/writing-quarto/quarto/terminal.md +1 -1
  25. package/skills/writing-quarto/scripts/testing-content.py +197 -1
  26. package/skills/writing-quarto/scripts/verifying-content.py +276 -0
  27. package/skills/writing-quarto/steps/03-blocks.md +17 -6
  28. package/skills/writing-quarto/steps/04-paragraphs.md +19 -2
  29. package/skills/writing-quarto/steps/06-wording.md +36 -0
  30. package/skills/writing-quarto/steps/07-layout.md +1 -1
  31. package/skills/writing-quarto/steps/08-reread.md +1 -0
  32. package/skills/writing-quarto/steps/09-check.md +3 -3
  33. package/templates/quarto-book-project/SKILL.md +1 -1
  34. package/templates/quarto-book-project/checks.json +5 -0
@@ -210,3 +210,19 @@ CTest(CMake 自带的测试运行工具)… 随后跟上术语卡又把同
210
210
  ```
211
211
 
212
212
  判定信号:术语卡的定义句与行内括号逐字同义(升级无收益);Callout 内出现删掉后读者无法走通主线的必经动作。
213
+
214
+ ## 节末旁注改为 Callout 收尾
215
+
216
+ 不佳(`##` 最后一块是裸句,读者分不清它是结论还是旁注):
217
+
218
+ 六个目录各管一件事(列表略)。`build/` 里没有源码,删掉或换一个目录后重新配置、编译就会按源码重新生成。
219
+
220
+ 改写(末句独立成 Callout,正文不再保留):
221
+
222
+ 列表略。
223
+
224
+ ::: {.callout-note title="构建目录不含源码"}
225
+ 构建目录 `build/` 里只有生成物,没有源码。整个目录可以删掉,也可以换一个路径;下一次配置和编译会按源码重新生成。
226
+ :::
227
+
228
+ 判定信号:`##` 最后一块是一句可跳过的补充,前面是列表、表格或围栏;Callout 能独立读出对象与条件,且不与正文任何一句逐字相同。末句是本节结论、下一章入口或读者必须照做的步骤时,不升级。
@@ -85,3 +85,17 @@
85
85
  ```
86
86
 
87
87
  判定信号:读者复述不出这段要记住什么;删掉整段后导语与围栏仍闭合;段内另一件内容在后文小节已有专讲。
88
+
89
+ ## 章节核心漂移
90
+
91
+ 标题是棋盘,章首却把格子的用途列成一段,主语从 `Board` 滑到「格子」:
92
+
93
+ ```text
94
+ 不佳(标题是棋盘,首句却在数格子能推出哪些判断):
95
+ 棋盘是 3 行 3 列,共九个格子。每次落子先检查坐标;满盘、三连和赢家都从这些格子判断。
96
+
97
+ 改写(首句主语回到 Board 的保存职责,判断留给后文小节):
98
+ 棋盘是 3 行 3 列,共九个格子,Board 保存这些格子,并提供写入与读出的接口。
99
+ ```
100
+
101
+ 判定信号:章首或 `##` 首句的主语是下游对象(格子、对局、终端),而不是标题对象;把主语换成标题对象后句子仍成立,说明原句已经漂移。
@@ -44,7 +44,7 @@ Game::GetResult 这三个成员。Game::ReadCellState 与 Game::GetResult 声明
44
44
 
45
45
  ```text
46
46
  不佳(一段压三问):
47
- 配置由 CMake 完成:读取 CMakeLists.txt,在 build/ 下生成构建计划。构建由 Ninja 完成:按计划把源码编译成可执行文件。所以改了 CMakeLists.txt 要重跑配置,而只改 main.cpp 时重新构建就够了。
47
+ 配置由 CMake 完成:读取 CMakeLists.txt,在 build/ 下生成构建计划。构建由 Ninja 完成:按计划把源码编译成可执行文件。所以改了 CMakeLists.txt 要重新运行配置,而只改 main.cpp 时重新构建就够了。
48
48
 
49
49
  改写(引言 → 引导词列表 → 收束):
50
50
  配置和构建是两件不同的事,由不同工具完成。
@@ -52,7 +52,7 @@ Game::GetResult 这三个成员。Game::ReadCellState 与 Game::GetResult 声明
52
52
  - **配置**:由 CMake 完成,读取 CMakeLists.txt,在 build/ 下生成构建计划。
53
53
  - **构建**:由 Ninja 完成,按计划把源码编译成可执行文件。
54
54
 
55
- 改了 CMakeLists.txt 要重跑配置;只改 main.cpp 时重新构建即可。
55
+ 改了 CMakeLists.txt 要重新运行配置;只改 main.cpp 时重新构建即可。
56
56
  ```
57
57
 
58
58
  ## 问答对象错位
@@ -41,7 +41,7 @@
41
41
 
42
42
  ```text
43
43
  不佳(「各说一层」把 PlaceResult 与 Result 的分工配进一句,删掉任一配对另一照常成立):
44
- 操作结果和流程结果各说一层:`PlaceResult` 回答这一次操作写进状态面板没有,`Result` 回答整局流程进行到了哪一步。
44
+ 操作结果和流程结果各说一层:`PlaceResult` 表示这一次操作有没有写进状态面板,`Result` 表示整局流程走到哪一步。
45
45
 
46
46
  改写(两个类型已在前文各配导语和围栏,分工不再复述,直接进入下一块):
47
47
  (整段删除。下一块改为一句桥接:这些类型由 `Game` 的四个数据成员使用,都声明在 `state.h` 的 `private:` 下。)
@@ -107,15 +107,15 @@
107
107
 
108
108
  ## 定句子:两次说话拆成一次对话
109
109
 
110
- 条文见 `steps/05-sentences.md`「一次对话」。总述叠了几件内容:有没有写进状态面板、整局走到哪、它们是两个问题、用两个枚举分开答,下一句又改口「接着看」「第一个枚举」。改写发生在定句子:每件内容一次对话,紧挨自己的围栏。
110
+ 条文见 `steps/05-sentences.md`「一次对话」。总述叠了几件内容:有没有写进状态面板、整局走到哪、它们是两个问题、用两个枚举分开表示,下一句又改口「接着看」「第一个枚举」。改写发生在定句子:每件内容一次对话,紧挨自己的围栏。
111
111
 
112
112
  **定句子(示例章两个枚举的总述):**
113
113
 
114
114
  ```text
115
115
  不佳(几件内容叠进一次说话,读者要拿开头的问题去猜「第一个」是哪个;「走向」又与开头的「进行到了哪一步」对不上):
116
- 至于操作有没有写进状态面板、整局进行到了哪一步,是两个不同的问题,`Game` 用两个枚举分开回答。接着看操作可能写入状态面板、也可能被拒绝的处理结果,用第一个枚举 `PlaceResult` 表示:
116
+ 至于操作有没有写进状态面板、整局进行到了哪一步,是两个不同的问题,`Game` 用两个枚举分别表示。接着看操作可能写入状态面板、也可能被拒绝的处理结果,用第一个枚举 `PlaceResult` 表示:
117
117
 
118
- 第二个枚举回答流程整体的走向,用 `Result` 表示:
118
+ 第二个枚举表示流程整体的走向,用 `Result` 表示:
119
119
 
120
120
  改写(每件内容一次对话,说法从开头保持到围栏,中间不编号):
121
121
  这次操作有没有写进状态面板,用 `PlaceResult` 表示:
@@ -86,6 +86,7 @@ state.h 里 `private:` 下的 ToFlatIndex 只写函数名与参数表,函数
86
86
  | 错误被返回 | API 返回错误 |
87
87
  | 刻意保持最小 / 刻意…… | 直接陈述程序做了什么、不包含什么 |
88
88
  | 只可能来自 A,不可能来自 B | 写成可核对的排查范围:工程不含 B → 失败时先查 A |
89
+ | 只保存 / 只把 / 只返回 / 只有(范围断言) | 直接写动作与对象;需要限定范围时写出尺寸、条件、文件名或入口。「只读」「只写」作固定术语保留 |
89
90
  | 由 N 段命令定义:A、B 和 C | 直接点名对象:这段规则定义可执行目标 app,规定产物目录、源文件和 C++20。 |
90
91
  | 规则库 | 项目动态库(读者正文与代码注释、CMake 注释统一此定名) |
91
92
  | 即胜着 / 更好的着 / 随机着法 | 马上能赢的一步 / 对当前用户更有利的一步 / 随机操作 |
@@ -177,3 +178,25 @@ CMakeLists.txt 把编译和测试串起来:先 cmake --build build,再跑 ct
177
178
  ```
178
179
 
179
180
  判定信号:删掉「下面我们来看」「至关重要」「无缝」「赋能」后,读者取走的信息不变;同一动作在一段里换了三个说法;正文出现对读者的招呼或「如有疑问请联系」。
181
+
182
+ ## 无信息提问与自造关系词
183
+
184
+ 正文不向读者发问,也不把关系另造成一个新名词:
185
+
186
+ ```text
187
+ 不佳(读者被问一个与当前任务无关的问题):
188
+ 总览图为什么不列成员?
189
+
190
+ 改写(删除;图注已经说明总览的范围):
191
+ (删除)
192
+ ```
193
+
194
+ ```text
195
+ 不佳(把组成关系说成「组成边」,再让读者解释它):
196
+ Game 与 Board 之间的组成边表示什么?
197
+
198
+ 改写(用通行关系词直接陈述):
199
+ Game 持有一个 Board 作为组成部分。
200
+ ```
201
+
202
+ 判定信号:正文句以「为什么 / 为何 / 表示什么」收尾并带问号;出现「×边」式自造关系词;把图的取舍或成员多少写成读者需要回答的问题。
@@ -114,10 +114,10 @@ $$
114
114
 
115
115
  ```text
116
116
  不佳(五句各用句号,顿号串在分句之间):
117
- `Board` 只保存九个棋盘格子。落子、满盘、成线与赢家都由它回答。写入前先检查坐标。声明在工程目录的 board.h。实现放在 board.cpp。
117
+ `Board` 只保存九个棋盘格子。落子、满盘、成线与赢家都从这些格子得出。写入前先检查坐标。声明在工程目录的 board.h。实现放在 board.cpp。
118
118
 
119
- 改写(共享主语并成总说,四类问题用冒号列举,声明与实现并成一句):
120
- `Board` 只保存九个棋盘格子。它回答四类问题:坐标是否越界、棋盘是否已满、是否成线以及赢家是谁。写入前先检查坐标。接口声明在工程目录的 board.h,实现放在 board.cpp。
119
+ 改写(共享主语并成总说,四类结果用冒号列举,声明与实现并成一句):
120
+ `Board` 从九个棋盘格子得出四类结果:坐标是否越界、棋盘是否已满、是否连成三个、赢家是谁。写入前先检查坐标;接口声明在工程目录的 board.h,实现放在 board.cpp。
121
121
  ```
122
122
 
123
123
  判定信号:同一段连续 4 个以上句号,且多数句子短于 15 字;顿号用在分句之间;「声明在…的…」把位置拆成两句。
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 操作节删除离题辩白
6
6
 
7
- 以 示例章运行一键脚本节为对照。脚本节标题是「怎么跑、看到什么」,嵌在块后的「幂等 / 跳过 / 配置漂移」长论与标题无关,新人缺语境;删除后主线(执行脚本 → 核对输出)仍闭合,「必须重跑配置」已由「修改 CMakeLists 后行为没变」排查节与脚本注释覆盖:
7
+ 以 示例章运行一键脚本节为对照。脚本节标题是「怎么跑、看到什么」,嵌在块后的「幂等 / 跳过 / 配置漂移」长论与标题无关,新人缺语境;删除后主线(执行脚本 → 核对输出)仍闭合,「必须重新运行配置」已由「修改 CMakeLists 后行为没变」排查节与脚本注释覆盖:
8
8
 
9
9
  ```text
10
10
  不佳(操作节内的工程取舍长论):
@@ -2,13 +2,13 @@
2
2
 
3
3
  ## 文件关系先于单页润色
4
4
 
5
- 多个页面一起修改时,先确定入口、索引和正文的职责。根首页只回答「这是什么、从哪一部分开始」,part 落地页只安排顺序,正文负责一个可完成的任务。前置页面不提前展开后续命令,正文不重复落地页给出的路线。
5
+ 多个页面一起修改时,先确定入口、索引和正文的职责。根首页只说明「这是什么、从哪一部分开始」,part 落地页只安排顺序,正文负责一个可完成的任务。前置页面不提前展开后续命令,正文不重复落地页给出的路线。
6
6
 
7
7
  包含实际源码和构建结构的章节,应在第一次展示源码前加入目录树,展示前核对真实磁盘。目录树细则见 `steps/03-blocks.md`。
8
8
 
9
9
  ## 页面增长与拆页
10
10
 
11
- 教学页达到 `steps/02-structure.md` 的 token 上限(整章 5000 token)后,按读者任务拆页或上调本书阈值,不做「上篇/中篇/下篇」式机械切分。每页只改变一个主要工程变量或回答一个可验证问题。每个新页面都登记到 `_quarto.yml` 与所属 part 的落地页,并拥有独立的代码路径。
11
+ 教学页达到 `steps/02-structure.md` 的 token 上限(整章 5000 token)后,按读者任务拆页或上调本书阈值,不做「上篇/中篇/下篇」式机械切分。每页只改变一个主要工程变量或解决一个可验证问题。每个新页面都登记到 `_quarto.yml` 与所属 part 的落地页,并拥有独立的代码路径。
12
12
 
13
13
  超限先判断内容是否必要:凑字数句删除,必要讲解保留并按读者任务拆页。长代码不得以内联围栏绕过预算;先下沉为项目源码(例如 `src/` 或 `src/`,以项目实际目录为准),再在页面中摘录关键片段。
14
14
 
@@ -94,7 +94,7 @@ README 遵循 GitHub Markdown。根目录 `README.md` 是在线阅读链接的
94
94
  | 深入机制 | `important` | 读者可跳过但不影响成功 |
95
95
  | 严重且不可逆 | `caution` | 数据丢失、需要停手确认 |
96
96
 
97
- 带标题的 Callout 使用 `::: {.callout-note title="中文标题"}`,标题由块属性提供,块内不再写 `##` 标题。Callout 放在它所补充的段落或小节内容的最后面,前后各留一个空行;术语卡是唯一就近放置的例外,紧跟术语参与的那段解释之后。必经步骤不进 callout。结果型 Callout 的边界见 `steps/03-blocks.md`。
97
+ 带标题的 Callout 使用 `::: {.callout-note title="中文标题"}`,标题由块属性提供,块内不再写 `##` 标题。Callout 放在它所补充的段落或小节内容的最后面,前后各留一个空行;术语卡是唯一就近放置的例外,紧跟术语参与的那段解释之后。必经步骤不进 callout。结果型 Callout 的边界见 `steps/03-blocks.md`。节末旁注的升级与位置见 `steps/03-blocks.md`「节末旁注与 Callout 收尾」;正文与 Callout 不复述同一句。
98
98
 
99
99
  参数解释默认使用短无序列表。Callout 不承载必经命令或构建命令,也不把唯一成功判据写成脱离正文的脚注。
100
100
 
@@ -102,7 +102,7 @@ README 遵循 GitHub Markdown。根目录 `README.md` 是在线阅读链接的
102
102
 
103
103
  何时升级为 Callout、何时留行内括号,只会遇到两种情形:
104
104
 
105
- - **升级**:说明需要的文字超过一句同位短语,且确有读者会回看的价值——术语卡(业界叫法 + 通过条件,就近放置)或尾置补充(正文与围栏注释之外确有独立价值的一点,如风险、版本差异、可选替代)。
105
+ - **升级**:说明需要的文字超过一句同位短语,且确有读者会回看的价值——术语卡(业界叫法 + 通过条件,就近放置)或尾置补充(正文与围栏注释之外确有独立价值的一点,如风险、版本差异、可选替代)。节末旁注按尾置补充处理,位置与改写要求见 `steps/03-blocks.md`「节末旁注与 Callout 收尾」。
106
106
  - **不升级**:一句同位短语能说清的(如「CTest(CMake 自带的测试运行工具)」)一律留行内括号;升级后卡片里没有正文未写的新信息时,删卡片。
107
107
 
108
108
  Callout 个数见 `steps/02-structure.md`,本段不写数字;`warning` 只进「常见错误」节里对应的环境差异条目。必经步骤与成功判据永不进 Callout。位置与类型选择的细则见 `steps/03-blocks.md`「元素边界」。
@@ -36,9 +36,9 @@ format: html
36
36
  ## 常用命令
37
37
 
38
38
  ```bash
39
- quarto render 文档.qmd # 渲染单个文档
40
- quarto preview 文档.qmd # 本地预览(实时刷新)
41
- quarto render # 渲染当前项目全部内容
39
+ quarto render 文档.qmd # 渲染单个文档
40
+ quarto preview 文档.qmd # 本地预览(实时刷新)
41
+ quarto render # 渲染当前项目全部内容
42
42
  ```
43
43
 
44
44
  `quarto render` 必须在仓库根目录执行,在其他目录执行会静默不渲染。书里若有 `{python}` 可执行单元,先按项目约定设置 `QUARTO_PYTHON`,再运行 `quarto render`;不要依赖系统 PATH 里的 `python3`(Windows 上可能是 Store 占位符)。
@@ -49,17 +49,17 @@ quarto render # 渲染当前项目全部内容
49
49
 
50
50
  ```text
51
51
  book/
52
- ├── _quarto.yml # 书配置:chapters、appendices 与输出
53
- ├── index.qmd # 根首页:只导航到各 part 落地页
54
- ├── content #/ 正文与附录
55
- │ ├── <part> #/ 一个部分
56
- │ │ ├── index.qmd # part 落地页:本部分章节卡片
57
- │ │ ├── 01-intro.qmd
58
- │ │ └── 02-topic.qmd
59
- │ └── appendix #/ 附录
60
- │ └── references.qmd
61
- ├── theme #/ 书内主题副本
62
- └── filters #/ 书内过滤器
52
+ ├── _quarto.yml # 章序与输出配置
53
+ ├── index.qmd # 首页:导航到各 part
54
+ ├── content/ #/ 正文与附录
55
+ │ ├── <part>/ #/ 一个部分
56
+ │ │ ├── index.qmd # part 落地页:本部分章节卡片
57
+ │ │ ├── 01-intro.qmd # 第一章的正文
58
+ │ │ └── 02-topic.qmd # 第二章的正文
59
+ │ └── appendix/ #/ 附录
60
+ │ └── references.qmd # 参考文献与术语出处
61
+ ├── theme/ #/ 书内主题副本
62
+ └── filters/ #/ 书内过滤器
63
63
  ```
64
64
 
65
65
  Quarto Book 的核心配置在书根 `_quarto.yml`:
@@ -78,7 +78,7 @@ book:
78
78
  chapters:
79
79
  - content/<part>/01-intro.qmd
80
80
  - content/<part>/02-topic.qmd
81
- - content/<part>/99-bibliography.qmd # 书目页:#refs 留在 chapters
81
+ - content/<part>/99-bibliography.qmd # 书目页:#refs 留在 chapters
82
82
  appendices:
83
83
  - content/appendix/references.qmd
84
84
  ```
@@ -109,7 +109,7 @@ title: "章节标题"
109
109
 
110
110
  开篇说明写正文普通段落(1-3 句);章节与 part 落地页不写 `description`,定位信息只保留在开篇说明。
111
111
 
112
- ## 第一个小节
112
+ # # 第一个小节
113
113
  正文。
114
114
  ```
115
115
 
@@ -15,17 +15,53 @@ Quarto 只执行 ` ```{mermaid} ` 属性围栏;plain 的 ` ```mermaid ` 围栏
15
15
  每个 `{mermaid}` 块的第一行写:
16
16
 
17
17
  ```text
18
- %%{init: {"fontFamily": "LXGW Bright Medium, system-ui, sans-serif"}}%%
18
+ %%{init: {"fontFamily": "LXGW Bright Medium, system-ui, sans-serif", "class": {"hideEmptyMembersBox": true}}}%%
19
19
  ```
20
20
 
21
21
  mermaid 用 init 指令中的字体测量标签宽度,页面 CSS 按 `--ui-font` 渲染文字;两者使用同一字体栈,标签测量与最终渲染一致,避免文字溢出或留白。`mermaid.css` 的排版守卫依赖这一约定。
22
22
 
23
+ `class.hideEmptyMembersBox` 只影响类图:没有成员的类只画类名和注解,不画空成员栏。接口、派生类和函数节点都靠它收紧;`verifying-content.py` 要求每个类图块首出现这一项。
24
+
25
+ ## 类图:先定内容预算
26
+
27
+ 正文栏宽随视口变化:默认 1280 宽的 Edge 窗口下约 644 px,1440 宽下约 804 px。类图按自然宽度绘制,预算按最窄的 644 px 定,目标宽度不超过 600 px。把图缩到栏宽会让 15 px 的字降到读不清,横向滚动又会把图切成几段;两种结果都不合格。类图先按下面的预算删内容,再考虑拆图:
28
+
29
+ | 项 | 上限 | 说明 |
30
+ | --- | --- | --- |
31
+ | 类型节点 | 6 个 | 包括空类、接口、枚举与函数节点 |
32
+ | 成员行合计 | 14 行 | `<<enum>>` 这类注解不计 |
33
+ | 单个类的成员行 | 5 行 | 只留本章连边或正文要用的成员 |
34
+ | 单行成员显示列 | 40 列 | 汉字按 2 列计;参数省略类型名 |
35
+ | 估算宽度 | 600 px | 渲染后按 `svg` 的 `max-width` 核对;超过就删内容或改方向 |
36
+ | 方向 | 有成员用 `TB`,无成员可用 `LR` | 基类在上,派生类在下 |
37
+
38
+ 超预算时先删节点和成员,再按前后半拆图;不缩字号、不缩图、不等横向滚动兜底。
39
+
40
+ 总览图只画类型和边,不列成员:成员会把每个类框撑宽,让关系退到视口外。成员放进分块明细或对应章节,总览只画“谁和谁相连”。
41
+
42
+ ## 类图成员写法
43
+
44
+ - 成员行用 `+`/`-` 标出可见性,类型写在名字前,参数只保留名字:`+bool CompletesLine(position, cell_type)`;
45
+ - 一行只写一个成员,不把多个方法压进同一行;
46
+ - 只列本章用到的成员;完整声明留在代码块和章节正文里,图内不补空成员,也不在图后写“其余成员见某章”;
47
+ - 空类、接口和派生类只写类名与 `<<interface>>` 一类注解,不补空成员占位;
48
+ - 关系边标签只写动词或名词短语,不写方法签名清单。
49
+
50
+ ## 流程图与类图的方向
51
+
52
+ - 分层结构用 `TB`;单向推进链节点数 ≥ 5 时默认 `TB` 单列,一屏过高时拆成两张图,禁止长 `LR` 挤爆视口;节点数 ≤ 4 的短链可用 `LR`;
53
+ - 类图有成员时用 `TB`,基类在上、派生类在下;无成员的纯类型关系图才用 `LR`;
54
+ - 单图节点不超过 6 个,一图一事不变,超出优先拆图而不是压缩标签。
55
+
23
56
  ## 节点与边标签写法
24
57
 
25
58
  - 节点标签一律放在双引号内,例如 `A["标签文字"]`,不依赖裸文本解析;
26
59
  - 节点标签只画抽象层名,不把职责明细塞进节点括号;逐项职责用图后列表交代;
27
60
  - 标签中不使用全角冒号与直角引号「」承载分隔语义;并列用顿号,补充说明用全角括号;
28
- - 边标签写在 `-- 文字 -->` 中,保持简短中文;
29
- - 图表方向以窄屏可读为底线:分层结构用 `TB`;单向推进链节点数 ≥ 5 时默认 `TB` 单列,一屏过高时拆成两张图(如按前后半各一张),禁止长 `LR` 挤爆视口;节点数 ≤ 4 的短链可用 `LR`;
30
- - 单图节点建议 ≤ 6,一图一事不变,超出优先拆图而不是压缩标签。类图方向用 TB:基类在上,派生类在下。
31
- - 图后不设 `>` 引用块图注:图注内容并入图后列表的引言句或正文句,图表之后直接跟职责列表,不让图独立承载全部信息。
61
+ - 边标签写在 `-- 文字 -->` 中,保持简短中文。
62
+
63
+ ## 图注
64
+
65
+ 每个 `{mermaid}` 图块在围栏开头、init 之前写一行 `%%| fig-cap: "程序结构总览。实线箭头表示调用或依赖,……"`。`fig-cap` 只写图意和读图约定,不手写“图 1:”这类编号;Quarto 按 `lang: zh` 自动在正文中排成“图 1:”。图注紧跟图块开头、落在图下。线型、箭头方向、读图范围这类说明只写进该图的图注,图后正文不再复述;图后只补图外的新信息或从图带走的结论,不点名被省略的成员,也不写“其余成员见某章”。`fig-cap` 是 Quarto 对 Mermaid 的单元选项语法,`%%|` 必须直接写在围栏开头、init 之前(Quarto 只解析块首选项;写在 init 之后会被当成 Mermaid 注释原样渲染),不能改成 `#|` 或图后引用块。
66
+
67
+ 图注文字概括图意,不与图后正文逐句重复;需要交叉引用时同时写 `label: fig-...`,正文用 `@fig-...` 引用。
@@ -36,9 +36,9 @@
36
36
  - 冒号前必须是总说或提示语;冒号后的内容离开前文能读成完整一句。
37
37
  - 同一列表的末项标点统一:都用句号,或都不带句号。
38
38
 
39
- 反例:`Board` 只保存九个棋盘格子。落子、满盘、成线与赢家都由它回答。写入前先检查坐标。声明在工程目录的 board.h。实现放在 board.cpp。
39
+ 反例:`Board` 只保存九个棋盘格子。落子、满盘、成线与赢家都从这些格子得出。写入前先检查坐标。声明在工程目录的 board.h。实现放在 board.cpp。
40
40
 
41
- 改写:`Board` 只保存九个棋盘格子。它回答四类问题:坐标是否越界、棋盘是否已满、是否成线以及赢家是谁。写入前先检查坐标。接口声明在工程目录的 board.h,实现放在 board.cpp。
41
+ 改写:`Board` 从九个棋盘格子得出四类结果:坐标是否越界、棋盘是否已满、是否连成三个、赢家是谁。写入前先检查坐标;接口声明在工程目录的 board.h,实现放在 board.cpp。
42
42
 
43
43
  判定信号:同一段连续 4 个以上句号且多数句子短于 15 字;顿号串在分句之间;声明与实现各占一句,只剩位置碎片。
44
44
 
@@ -85,9 +85,9 @@
85
85
 
86
86
  润色要的「灵性」不靠形容词,靠三件事:主语是具体对象,动词是具体动作,读完能看见结果。
87
87
 
88
- - 删掉章节行程,直接给对象和它回答的问题。
88
+ - 删掉章节行程,直接给对象和它产生的结果。
89
89
  - 原句:这一章介绍棋盘类的设计,首先看成员,然后看落子,最后看胜负判定。
90
- - 改写:棋盘只保存九个格子。落子、满盘、成线与赢家都由它回答。
90
+ - 改写:棋盘只保存九个格子。落子、满盘、成线与赢家都从这些格子得出。
91
91
  - 把「进行校验」「负责计算」换成调用方看得见的动作与返回值。
92
92
  - 原句:程序会对输入进行校验,如果坐标不合法就会返回错误。
93
93
  - 改写:坐标越界、格子被占用或对局已结束时,`TryPlace` 返回 `kRejected`,棋盘不变。
@@ -75,7 +75,7 @@
75
75
  3. 如何恢复:能否重新生成,或恢复需要哪些前置。
76
76
  4. 替代方案:先给出可逆写法,把不可逆写法放在确认新状态正常之后。
77
77
 
78
- `rm -rf` 只有在没有安全替代、且恢复路径明确时才写,并放在 warning callout 或独立段落里,不与成功路径混排。删除 `build/` 属于可重建产物,恢复方式是重跑配置。
78
+ `rm -rf` 只有在没有安全替代、且恢复路径明确时才写,并放在 warning callout 或独立段落里,不与成功路径混排。删除 `build/` 属于可重新生成的产物,恢复方式是重新运行配置。
79
79
 
80
80
  ## 常见错误条目形态与三步诊断流
81
81
 
@@ -52,6 +52,71 @@ title: 入门
52
52
  先准备输入,再运行检查,最后对照输出。
53
53
  """
54
54
 
55
+ TREE_CHAPTER = """---
56
+ title: 入门
57
+ ---
58
+
59
+ ## 目标
60
+
61
+ 下面是工程目录树,行尾注释说明每个节点在工程里的用途,读者可以对照磁盘检查。
62
+
63
+ ```text
64
+ ├── apps/ #/ 终端程序
65
+ │ └── cli.h # 读入坐标
66
+ └── tests/ #/ 单元测试
67
+ ```
68
+ """
69
+
70
+
71
+ TREE_GAP_CHAPTER = """---
72
+ title: 入门
73
+ ---
74
+
75
+ ## 目标
76
+
77
+ 下面是工程目录树,行尾注释说明每个节点在工程里的用途。
78
+
79
+ ```text
80
+ ├── lib/ #/ 源码
81
+ └── main.c # 入口
82
+ ```
83
+ """
84
+
85
+
86
+ MERMAID_CHAPTER = """---
87
+ title: 入门
88
+ ---
89
+
90
+ ## 目标
91
+
92
+ 下面是一张只画类型和边的类图,读者可以对照箭头理解类型之间的依赖。
93
+
94
+ ::: {.class-diagram}
95
+ ```{mermaid}
96
+ %%| fig-cap: "类型 A 依赖类型 B。实线箭头从使用者指向被使用者。"
97
+ %%{init: {"fontFamily": "LXGW Bright Medium, system-ui, sans-serif", "class": {"hideEmptyMembersBox": true}}}%%
98
+ classDiagram
99
+ direction LR
100
+ class A
101
+ class B
102
+ A --> B
103
+ ```
104
+ :::
105
+ """
106
+
107
+
108
+ def long_tree_chapter(lines: int = 21) -> str:
109
+ rows = []
110
+ for index in range(lines):
111
+ prefix = f"├── file{index}.txt"
112
+ rows.append(prefix + " " * (20 - len(prefix)) + f"# 用途{index}")
113
+ tree = "\n".join(rows)
114
+ return (
115
+ "---\ntitle: 入门\n---\n\n## 目标\n\n"
116
+ "下面是工程目录树,行尾注释说明每个节点在工程里的用途。\n\n"
117
+ f"```text\n{tree}\n```\n"
118
+ )
119
+
55
120
 
56
121
  def run_case(tmp: Path) -> subprocess.CompletedProcess[str]:
57
122
  return subprocess.run(
@@ -97,6 +162,21 @@ def main() -> int:
97
162
  failures.append(f"{name}: 输出缺少「{needle}」\n{output[:600]}")
98
163
 
99
164
  expect("合法样书", VALID_CHAPTER, 0, "OK")
165
+ expect(
166
+ "正文问答式讲解",
167
+ VALID_CHAPTER + "\n`Board` 只回答四类问题:坐标、满盘、三连与赢家。\n",
168
+ 1, "问答式讲解",
169
+ )
170
+ expect(
171
+ "代码字面复述",
172
+ VALID_CHAPTER + "\n对应的代码里,`kBoardRows` 和 `kBoardCols` 都取 3。\n",
173
+ 1, "代码字面复述",
174
+ )
175
+ expect(
176
+ "自造关系词",
177
+ VALID_CHAPTER + "\n`Game` 与 `Board` 之间的组成边表示什么?\n",
178
+ 1, "自造关系词",
179
+ )
100
180
  expect("标题口语化", VALID_CHAPTER.replace("## 目标", "## 自己走一局"), 1, "标题带人称")
101
181
  expect("标题带行内代码", VALID_CHAPTER.replace("## 目标", "## 运行 `app`"), 1, "标题带行内代码")
102
182
  expect("正文 H1", VALID_CHAPTER.replace("## 目标", "# 目标"), 1, "正文出现 H1")
@@ -128,6 +208,122 @@ def main() -> int:
128
208
  0, "OK",
129
209
  )
130
210
 
211
+ expect("目录树合格", TREE_CHAPTER, 0, "OK")
212
+ expect(
213
+ "标记后多空格",
214
+ TREE_CHAPTER.replace("# 读入坐标", "# 读入坐标"),
215
+ 1, "标记后只留一个空格",
216
+ )
217
+ expect(
218
+ "注释文字未对齐",
219
+ TREE_CHAPTER.replace("#/ 终端程序", "#/ 终端程序"),
220
+ 1, "注释文字未对齐",
221
+ )
222
+ expect(
223
+ "树注释错列",
224
+ TREE_CHAPTER.replace("│ └── cli.h #", "│ └── cli.h #"),
225
+ 1, "右移一列",
226
+ )
227
+ expect("节点与注释太近", TREE_GAP_CHAPTER, 1, "至少留四个空格")
228
+ expect(
229
+ "文件 # 未右移",
230
+ TREE_CHAPTER.replace("│ └── cli.h #", "│ └── cli.h #"),
231
+ 1, "右移一列",
232
+ )
233
+ expect("树注释只写类别词", TREE_CHAPTER.replace("# 读入坐标", "# 头文件"), 1, "类别词")
234
+ expect(
235
+ "目录注释带冒号",
236
+ TREE_CHAPTER.replace("#/ 终端程序", "#/ 终端程序:打印棋盘"),
237
+ 1, "目录注释只写这里装什么",
238
+ )
239
+ expect(
240
+ "目录注释过长",
241
+ TREE_CHAPTER.replace("#/ 终端程序", "#/ 终端程序与它的打印读入"),
242
+ 1, "超过 16 列",
243
+ )
244
+ expect("树注释列了三项", TREE_CHAPTER.replace("# 读入坐标", "# 读入、打印与写快照"), 1, "压到 1 到 2 项")
245
+ expect("目录行缺少 #/", TREE_CHAPTER.replace("#/ 终端程序", "# 终端程序"), 1, "目录行的注释以 #/")
246
+ expect("文件行误写 #/", TREE_CHAPTER.replace("# 读入坐标", "#/ 读入坐标"), 1, "文件行的注释以 #")
247
+ expect("树行缺少注释", TREE_CHAPTER.replace("# 读入坐标", ""), 1, "缺少行尾注释")
248
+ expect(
249
+ "树注释过长",
250
+ TREE_CHAPTER.replace("# 读入坐标", "# 读入坐标并打印当前棋盘的全部状态"),
251
+ 1, "超过 28 列",
252
+ )
253
+ expect("树行数过多", long_tree_chapter(), 1, "超过 20 行")
254
+
255
+ expect("类图合格", MERMAID_CHAPTER, 0, "OK")
256
+ expect(
257
+ "类图缺少图注",
258
+ MERMAID_CHAPTER.replace('%%| fig-cap: "类型 A 依赖类型 B。实线箭头从使用者指向被使用者。"\n', ""),
259
+ 1, "fig-cap",
260
+ )
261
+ wrong_order = MERMAID_CHAPTER.replace(
262
+ '%%| fig-cap: "类型 A 依赖类型 B。实线箭头从使用者指向被使用者。"\n%%{init: ',
263
+ "%%{init: ",
264
+ ).replace(
265
+ "classDiagram",
266
+ '%%| fig-cap: "类型 A 依赖类型 B。实线箭头从使用者指向被使用者。"\nclassDiagram',
267
+ 1,
268
+ )
269
+ expect("类图图注写在 init 之后", wrong_order, 1, "init 之前")
270
+ expect(
271
+ "类图缺少页面容器",
272
+ MERMAID_CHAPTER.replace("::: {.class-diagram}\n```{mermaid}", "```{mermaid}").replace(
273
+ "```\n:::", "```"
274
+ ),
275
+ 1, ".class-diagram",
276
+ )
277
+ expect(
278
+ "类图缺少空成员配置",
279
+ MERMAID_CHAPTER.replace(', "class": {"hideEmptyMembersBox": true}', ""),
280
+ 1, "hideEmptyMembersBox",
281
+ )
282
+ expect(
283
+ "类图有成员却用 LR",
284
+ MERMAID_CHAPTER.replace(
285
+ " class B\n",
286
+ " class B {\n int value\n }\n",
287
+ ),
288
+ 1, "有成员却用 LR",
289
+ )
290
+ expect(
291
+ "类图类型过多",
292
+ MERMAID_CHAPTER.replace(
293
+ " class B\n",
294
+ " class B\n class C\n class D\n class E\n class F\n class G\n class H\n",
295
+ ),
296
+ 1, "超过 6 个",
297
+ )
298
+ expect(
299
+ "类图成员行过长",
300
+ MERMAID_CHAPTER.replace(
301
+ " class A\n",
302
+ " class A {\n +bool CompletesLine(CellPosition position, CellType cell_type) const\n }\n",
303
+ ).replace("direction LR", "direction TB"),
304
+ 1, "超过 40 列",
305
+ )
306
+ expect(
307
+ "单个类成员过多",
308
+ MERMAID_CHAPTER.replace(
309
+ " class A\n",
310
+ " class A {\n int a\n int b\n int c\n int d\n int e\n int f\n }\n",
311
+ ).replace("direction LR", "direction TB"),
312
+ 1, "超过 5 行",
313
+ )
314
+ expect(
315
+ "类图成员总数过多",
316
+ MERMAID_CHAPTER.replace(
317
+ " class A\n class B\n",
318
+ " class A {\n int a1\n int a2\n int a3\n }\n"
319
+ " class B {\n int b1\n int b2\n int b3\n }\n"
320
+ " class C {\n int c1\n int c2\n int c3\n }\n"
321
+ " class D {\n int d1\n int d2\n int d3\n }\n"
322
+ " class E {\n int e1\n int e2\n int e3\n }\n",
323
+ ).replace("direction LR", "direction TB"),
324
+ 1, "超过 14 行",
325
+ )
326
+
131
327
  with tempfile.TemporaryDirectory() as raw:
132
328
  root = Path(raw)
133
329
  make_book(root)
@@ -143,7 +339,7 @@ def main() -> int:
143
339
  print(f" FAIL {line}")
144
340
  return 1
145
341
 
146
- print("PASS:11 个用例全部通过")
342
+ print("PASS:36 个用例全部通过")
147
343
  return 0
148
344
 
149
345