@chengzhao-dev/dsh-quarto-book 0.6.3 → 0.6.4
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.
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/lib/index.js +49 -7
- package/lib/tools/pass-report.js +12 -12
- package/lib/tools/render-check.js +230 -61
- package/lib/workspace.js +1 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +9 -6
- package/skills/designing-theme/assets/theme/css/content.css +15 -0
- package/skills/designing-theme/scripts/checking-layout.py +11 -3
- package/skills/designing-theme/scripts/measuring-pages.mjs +2 -2
- package/skills/writing-quarto/SKILL.md +7 -0
- package/skills/writing-quarto/examples/03-qmd-elements.md +29 -2
- package/skills/writing-quarto/quarto/authoring.md +3 -2
- package/skills/writing-quarto/quarto/basics.md +1 -0
- package/skills/writing-quarto/quarto/coords.md +2 -1
- package/skills/writing-quarto/quarto/figures.md +49 -0
- package/skills/writing-quarto/quarto/mermaid.md +1 -1
- package/skills/writing-quarto/quarto/refactor.md +1 -1
- package/skills/writing-quarto/references/book-journey.md +38 -0
- package/skills/writing-quarto/references/token-budget.md +26 -0
- package/skills/writing-quarto/scripts/checking-callouts.py +2 -2
- package/skills/writing-quarto/scripts/checking-dom.py +12 -6
- package/skills/writing-quarto/scripts/checking-links.py +29 -13
- package/skills/writing-quarto/scripts/checking-names.py +3 -1
- package/skills/writing-quarto/scripts/checking-output.py +3 -3
- package/skills/writing-quarto/scripts/checking-prose.py +11 -7
- package/skills/writing-quarto/scripts/testing-content.py +68 -1
- package/skills/writing-quarto/scripts/verifying-content.py +166 -36
- package/skills/writing-quarto/steps/03-blocks.md +4 -3
- package/skills/writing-quarto/steps/06-wording.md +17 -0
package/lib/workspace.js
CHANGED
|
@@ -13,6 +13,7 @@ const BOOK_SOURCE_EXTENSIONS = new Set(['.qmd', '.md']);
|
|
|
13
13
|
const NON_CHAPTER_FILE_RE = /^(?:readme|changelog|license|licence|contributing|code[_-]of[_-]conduct|security|support|authors|contributors|agents|claude)(?:[.-][a-z0-9_-]+)*\.(?:md|markdown|qmd)$/i;
|
|
14
14
|
const IGNORED_DIRECTORIES = new Set([
|
|
15
15
|
'.git',
|
|
16
|
+
'.agents',
|
|
16
17
|
'.quarto',
|
|
17
18
|
'.cache',
|
|
18
19
|
'.venv',
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Quarto Book Writing & Theme",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "Write, check, and publish a Chinese Quarto Book: nine-step chapter writing, structure/link/figure checks, a consistent theme, and render acceptance."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chengzhao-dev/dsh-quarto-book",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.4",
|
|
4
|
+
"description": "把一份 Quarto Book 写成可读、可查、可发布的中文技术书:九步章节写作、结构/链接/图表检查、统一主题与渲染验收。 · Write, check, and publish a Chinese Quarto Book: nine-step chapter writing, structure/link/figure checks, a consistent theme, and render acceptance.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"catalog": {
|
|
36
36
|
"category": "skill",
|
|
37
37
|
"summary": {
|
|
38
|
-
"zh": "
|
|
39
|
-
"en": "
|
|
38
|
+
"zh": "把一份 Quarto Book 写成可读、可查、可发布的中文技术书:九步章节写作、结构/链接/图表检查、统一主题与渲染验收。",
|
|
39
|
+
"en": "Write, check, and publish a Chinese Quarto Book: a nine-step chapter workflow, structure/link/figure checks, a consistent theme, and render acceptance."
|
|
40
40
|
},
|
|
41
41
|
"capabilities": [
|
|
42
42
|
"skills",
|
|
@@ -47,8 +47,11 @@
|
|
|
47
47
|
"icon": "./icon.svg",
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsc -p tsconfig.json",
|
|
50
|
-
"test": "npm run build && node --test tests
|
|
51
|
-
"check": "tsc --noEmit -p tsconfig.json && node scripts/checking-skill-links.mjs",
|
|
50
|
+
"test": "npm run build && node --test \"tests/**/*.test.mjs\"",
|
|
51
|
+
"check": "tsc --noEmit -p tsconfig.json && node scripts/checking-skill-links.mjs && node scripts/checking-skill-budget.mjs && node scripts/checking-package.mjs",
|
|
52
|
+
"check:lib": "node scripts/checking-lib.mjs",
|
|
53
|
+
"prepack": "npm run build",
|
|
54
|
+
"prepublishOnly": "npm run check && npm run check:lib",
|
|
52
55
|
"pack": "node -e \"require('fs').mkdirSync('temp',{recursive:true})\" && npm pack --pack-destination temp",
|
|
53
56
|
"pack:check": "npm pack --dry-run"
|
|
54
57
|
},
|
|
@@ -199,6 +199,21 @@ mjx-container[display="true"] {
|
|
|
199
199
|
box-sizing: border-box;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
/* 原生 HTML/SVG 图:图与图注作为一个整体居中;图注在图下,沿用上面的 figure 图注样式。 */
|
|
203
|
+
#quarto-document-content .book-figure {
|
|
204
|
+
margin: 1.25rem auto;
|
|
205
|
+
text-align: center;
|
|
206
|
+
}
|
|
207
|
+
#quarto-document-content .book-figure > svg {
|
|
208
|
+
display: block;
|
|
209
|
+
margin-inline: auto;
|
|
210
|
+
max-width: 100%;
|
|
211
|
+
height: auto;
|
|
212
|
+
}
|
|
213
|
+
#quarto-document-content .book-figure > figcaption {
|
|
214
|
+
margin-top: 0.5rem;
|
|
215
|
+
}
|
|
216
|
+
|
|
202
217
|
/* jupyter 单元的图必须用 IPython.display.HTML 输出保持 SVG 内联:image/svg+xml 输出会被
|
|
203
218
|
Quarto 外置成 <img>,无法继承站点 data-bs-theme 明暗与文档字体。text/html 输出不被
|
|
204
219
|
Quarto 识别为图像,fig-cap 退化为图后一个裸 <p>,这里按 figcaption 同款图注样式居中。 */
|
|
@@ -7,7 +7,7 @@ Node、Playwright 与 Edge 可用时,measuring-pages.mjs 验证真实几何。
|
|
|
7
7
|
规范出处:本技能 `references/theme-system.md`。
|
|
8
8
|
|
|
9
9
|
用法:python checking-layout.py [--root .] [--book-dir _book] [--browser]
|
|
10
|
-
退出码:0 = 关键令牌全部存在,1 =
|
|
10
|
+
退出码:0 = 关键令牌全部存在,1 = 有缺失,2 = 渲染产物目录缺失,3 = 请求了 --browser 但运行时不可用。
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import argparse
|
|
@@ -348,7 +348,12 @@ def resolve_node():
|
|
|
348
348
|
|
|
349
349
|
|
|
350
350
|
def resolve_playwright_root():
|
|
351
|
-
candidates = [
|
|
351
|
+
candidates = [
|
|
352
|
+
os.environ.get("BOARD_GAMES_NODE_PATH"),
|
|
353
|
+
os.environ.get("NODE_PATH"),
|
|
354
|
+
str(THEME_DIR.parents[1] / "node_modules"),
|
|
355
|
+
str(ROOT / "node_modules"),
|
|
356
|
+
]
|
|
352
357
|
cache = Path.home() / ".cache" / "codex-runtimes"
|
|
353
358
|
if cache.is_dir():
|
|
354
359
|
candidates.extend(
|
|
@@ -838,7 +843,7 @@ def main():
|
|
|
838
843
|
book_dir = ROOT / book_dir
|
|
839
844
|
if not book_dir.is_dir():
|
|
840
845
|
print(f"BookDir not found: {book_dir}. Run 'quarto render' first.")
|
|
841
|
-
return
|
|
846
|
+
return 2
|
|
842
847
|
|
|
843
848
|
css_texts = [
|
|
844
849
|
p.read_text(encoding="utf-8", errors="ignore")
|
|
@@ -1024,6 +1029,9 @@ def main():
|
|
|
1024
1029
|
|
|
1025
1030
|
print()
|
|
1026
1031
|
if fail == 0:
|
|
1032
|
+
if args.browser and browser_note:
|
|
1033
|
+
print(f"SKIP browser-layout {browser_note}")
|
|
1034
|
+
return 3
|
|
1027
1035
|
if browser_note:
|
|
1028
1036
|
print(f"PASS theme-static;SKIP browser-layout {browser_note}")
|
|
1029
1037
|
else:
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
|
-
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
9
|
+
import { delimiter, dirname, join, relative, resolve, sep } from "node:path";
|
|
10
10
|
import { pathToFileURL } from "node:url";
|
|
11
11
|
|
|
12
12
|
const argv = process.argv.slice(2);
|
|
@@ -79,7 +79,7 @@ function loadPlaywright() {
|
|
|
79
79
|
try {
|
|
80
80
|
return require("playwright");
|
|
81
81
|
} catch (error) {
|
|
82
|
-
for (const directory of (process.env.NODE_PATH || "").split(
|
|
82
|
+
for (const directory of (process.env.NODE_PATH || "").split(delimiter).filter(Boolean)) {
|
|
83
83
|
const candidate = join(directory, "playwright", "index.js");
|
|
84
84
|
if (existsSync(candidate)) {
|
|
85
85
|
return require(candidate);
|
|
@@ -16,6 +16,12 @@ metadata:
|
|
|
16
16
|
- 项目带有 `.agents/skills/quarto-book-project/SKILL.md` 时,读完本技能再读它,并只按它指向的 `writing/` 文件读章节对照;没有就跳过,不要凭猜测补书名、章节名和术语。书内 `context/` 下的旧通用副本不再打开,写法以本技能为准。
|
|
17
17
|
- 书仓库根目录的 `README.md` 只走第 5–8 步:它是仓库说明页,不是章节;不套第 1–4 步的任务与结构骨架,不写「常见错误」「自测问题」,不调 `quarto_render_check`。第 8 步发现问题只退回第 5、6、7 步。`README.en.md` 同样处理;`CHANGELOG.md`、`LICENSE`、`AGENTS.md` 不在写作范围。这条只在书仓库里适用(技能本身由 Book 门禁触发)。
|
|
18
18
|
|
|
19
|
+
## 整书走法
|
|
20
|
+
|
|
21
|
+
改多章或排目录时,先读 `references/book-journey.md`:先定全书主线与章节次序,再逐章走九步,最后做全书一致性回读。只润色一章时不读它。
|
|
22
|
+
|
|
23
|
+
单章与整书共用同一套九步,不另造一套章节规则;项目有章节表时以项目表为准。技能文件体量受 `references/token-budget.md` 约束,新增整书规则只写进 `book-journey.md`。
|
|
24
|
+
|
|
19
25
|
## 两种走法
|
|
20
26
|
|
|
21
27
|
增量润色沿九步走,只改判为不合格的地方,其余保持原样。全章重构仍是这九步,但第 1–2 步先定删留、不动句子,第 3–4 步只拆块和段,第 5–7 步才逐句改。
|
|
@@ -29,6 +35,7 @@ metadata:
|
|
|
29
35
|
1. 定任务:写出读者做完能完成的那件事,再列这一章要覆盖的任务。规则在 `steps/01-task.md`。
|
|
30
36
|
2. 定结构:排页面骨架、二级标题和篇幅预算,只定删留、标题与章题对齐。规则在 `steps/02-structure.md`。
|
|
31
37
|
3. 排列内容块:导语、围栏、收束与 Callout 的次序,一个围栏只放正在讲的那一块。规则在 `steps/03-blocks.md`。
|
|
38
|
+
图注写在图下、图前只写引出关系;三类图的统一写法见 `quarto/figures.md`。
|
|
32
39
|
4. 定段落:一段一个对象,段间衔接落在话题切换处。规则在 `steps/04-paragraphs.md`。
|
|
33
40
|
5. 定句子:主语选执行者,对象先行,一句一动作。规则在 `steps/05-sentences.md`。
|
|
34
41
|
6. 定用词:统一术语、主动语态和语气。规则在 `steps/06-wording.md`。
|
|
@@ -48,8 +48,8 @@ constexpr bool IsInRange(Position position) { … }
|
|
|
48
48
|
不佳(图前图例句复述图注的方位信息,一段三句三个主题):
|
|
49
49
|
`Position` 把坐标交给 `Board`,负数和行列为 `3` 的位置都会被拒绝。下面的图标出可用的 `(row, col)`。左上角是 `(0, 0)`,行向下增加,列向右增加。
|
|
50
50
|
|
|
51
|
-
改写(方位与图例由 fig-cap
|
|
52
|
-
调用方把 `Position` 交给 `Board`,行列出现负数或 `3`
|
|
51
|
+
改写(方位与图例由 fig-cap 独自说明,正文只留行为句与桥接句):
|
|
52
|
+
调用方把 `Position` 交给 `Board`,行列出现负数或 `3` 都会被拒绝。图列出可用的 `(row, col)`,桥接句不再交代图面方位。
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
判定信号:两个围栏之间只有空行;图后直接进入与图无关的下一块;块前引语同时推进多个对象(如 `Position` 与 `IsInRange` 各讲一句);图注已写明的方位与图例信息,桥接段或图后正文再讲一遍。
|
|
@@ -78,6 +78,33 @@ constexpr bool IsInRange(Position position) { … }
|
|
|
78
78
|
|
|
79
79
|
判定信号:图后句删掉任一分句中心仍成立;图注或桥接句已写明的信息在图后再讲一遍。图后正文只给图注讲不完的新结论,图后对照与句段侧的冒号分号堆叠见 `examples/04-paragraph-opening.md`「冒号后堆叠分号解释」。
|
|
80
80
|
|
|
81
|
+
## 原生 SVG 图注对照
|
|
82
|
+
|
|
83
|
+
`{=html}` 围栏里的 SVG 不会被 Quarto 包成 figure;图注必须自己用 `<figure>` 和 `<figcaption>` 放在图下,不要在图前留一句“下图标出……”。
|
|
84
|
+
|
|
85
|
+
````text
|
|
86
|
+
不佳(图注占图前正文,SVG 裸放):
|
|
87
|
+
下图标出这块棋盘上可用的 (row, col)。
|
|
88
|
+
|
|
89
|
+
```{=html}
|
|
90
|
+
<svg class="coords-grid" role="img" aria-label="棋盘坐标。">…</svg>
|
|
91
|
+
```
|
|
92
|
+
````
|
|
93
|
+
|
|
94
|
+
````text
|
|
95
|
+
改写(图注在图下,正文只留引出关系的一句):
|
|
96
|
+
代码给出行列上限和坐标字段,图把行列编号落到棋盘格子上。
|
|
97
|
+
|
|
98
|
+
```{=html}
|
|
99
|
+
<figure class="book-figure">
|
|
100
|
+
<svg class="coords-grid" role="img" aria-label="棋盘坐标。">…</svg>
|
|
101
|
+
<figcaption>棋盘坐标图:3 行 3 列,每格标出 (row, col);行向下、列向右,左上角是 (0, 0)。</figcaption>
|
|
102
|
+
</figure>
|
|
103
|
+
```
|
|
104
|
+
````
|
|
105
|
+
|
|
106
|
+
判定信号:`<svg>` 是否在 `<figure>` 内、`<figcaption>` 是否在 `svg` 之后;图前是否还有只交代图面的句子;图注与图后正文是否逐句重复。
|
|
107
|
+
|
|
81
108
|
## 块后解释先于图对照
|
|
82
109
|
|
|
83
110
|
同一小节既有代码块项目解释又有图时,解释紧跟代码块,桥接句与图在后;图后不再回头讲代码项。
|
|
@@ -76,8 +76,9 @@ README 遵循 GitHub Markdown。根目录 `README.md` 是在线阅读链接的
|
|
|
76
76
|
|
|
77
77
|
项目解释不得插队到图之后:禁止「导语 → 代码 → 图 → 再解释代码项」,图后正文不再回头讲代码块内的对象。代码块与图之间只有空行、或图后不接结论直接进入与图无关的下一块,都视为堆叠;结论已并入桥接段的小节收尾图除外。反例与改写对照见 `steps/03-blocks.md` 与 `quarto/coords.md`。
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
图的放置另有四条边界:
|
|
80
80
|
|
|
81
|
+
- **图注与正文分工**:图注写图面内容与读法,图前句写这张图核对哪条关系;图前不写“下图标出……”这类只交代图面的句子,图注落在图下。三类图的图注写法见 `quarto/figures.md`。
|
|
81
82
|
- **一图一事**:一张图只核对一条已讲清的关系。图需要同时表达两条关系时,拆成多张图,或按关系拆成多个 `###`,每节一图。
|
|
82
83
|
- **同主题相邻**:代码块与图必须是同一对象的两种呈现。下一块与图主题不同时,先用一句正文收束本对象,再另起 `###`;禁止「代码 A → 一句拐弯 → 图讲 B → 再贴代码 C」的同节混排。
|
|
83
84
|
- **图不承载正文未写的新信息**:图中的每个框、箭头或标记都能在图前正文找到对应对象与关系;读者需要新概念才能读懂的图,先在正文补出该概念,再放图。
|
|
@@ -128,4 +129,4 @@ Callout 个数见 `steps/02-structure.md`,本段不写数字;`warning` 只
|
|
|
128
129
|
|
|
129
130
|
交叉引用使用真实标题生成的锚点,引用前确认目标页面存在。本项目不手写英文 `{#id}`,标题锚点由中文标题派生;跨章节引用用章节标题链接,不用「第 N 章」。
|
|
130
131
|
|
|
131
|
-
需要渲染与链接依据时,读 `quarto/render.md` 与 `quarto/mermaid.md`。
|
|
132
|
+
需要渲染与链接依据时,读 `quarto/render.md` 与 `quarto/mermaid.md`;图注、原生 SVG 与图前句的分工读 `quarto/figures.md`。
|
|
@@ -12,12 +12,13 @@
|
|
|
12
12
|
|
|
13
13
|
- 页面 frontmatter 开 `engine: jupyter`,只给含图页面开启,避免全书每次渲染都起 kernel。
|
|
14
14
|
- 坐标图单元用 ```` ```{python} ```` 围栏,块首写 `#| echo: false`、`#| warning: false`,再写 `#| fig-cap: "图 N:…"` 与 `#| fig-align: center`;图注文字概括图意,不与图后正文逐句重复,也不用 `>` 引用块另起一套图注。
|
|
15
|
+
- 没有执行环境或页面保持静态时,允许直接写原生 HTML/SVG;图必须用 `<figure class="book-figure">` 包住,`<figcaption>` 在图后。三类图的统一图注约定见 `quarto/figures.md`,不要把图注留成图前的独立段落。
|
|
15
16
|
- 单元内从当前目录向上定位含 `_quarto.yml` 的书根,按路径加载本技能 `scripts/diagrams/rendering-coords.py`,绑定名仍是 `coords_grid`,用 `IPython.display.HTML` 输出保持 SVG 内联。不要换成 `IPython.display.SVG`:image/svg+xml 输出会被 Quarto 外置成 `<img>` 文件引用,无法继承站点 `data-bs-theme` 明暗与文档字体(书内字体退回系统字体)。
|
|
16
17
|
- 图注讲不完的读图要点写在图后正文(一两句),不复述图注已写明的信息。
|
|
17
18
|
|
|
18
19
|
## 上游代码与图的桥接
|
|
19
20
|
|
|
20
|
-
|
|
21
|
+
坐标图不孤立出现:引出图的代码块与图之间必须有桥接句,说明图核对代码的哪部分契约(如「图核对 `IsValid` 接受的范围」);图后先给从图带走的结论,再进入下一块;小节以图收尾时,从图带走的结论可并入桥接段(如 示例章先给 `row` 为 `3` 或 `col` 为 `-1` 这类反例再上图),不在图后孤悬一句。代码块与图之间只有空行视为堆叠,固定序列见 `writing-quarto` 技能 `quarto/authoring.md`「代码与图的桥接」与 `steps/03-blocks.md`;图注与图前句的分工见 `quarto/figures.md`。
|
|
21
22
|
|
|
22
23
|
坐标图单元计入盒子密度(口径见 `steps/03-blocks.md`):同一 `###` 内坐标图与代码块相邻出现时,桥接句就是盒子之间的承接正文;`verifying-content.py` 对两围栏之间无正文给出 WARN 提示,人审与对照案例仍是主检查。
|
|
23
24
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# 图与图注
|
|
2
|
+
|
|
3
|
+
图由图像、`{mermaid}`、`{python}` 坐标图和原生 HTML/SVG 共同遵守这一份约定。图注负责读图信息,正文负责图与上下文的关系;两者不互相复述。
|
|
4
|
+
|
|
5
|
+
## 位置与归属
|
|
6
|
+
|
|
7
|
+
- 图注默认在图下,图整体居中。Quarto 的 `fig-cap` 默认就是图下题注;不要把图注写成图前独立段落。
|
|
8
|
+
- `{mermaid}` 用 `%%| fig-cap`,`{python}` 坐标图用 `#| fig-cap`,原生 HTML/SVG 用 `<figure>` 内的 `<figcaption>`。三者都落在图下。
|
|
9
|
+
- 图前只写引出关系的一句:为什么这里出现这张图、它核对哪条已讲清的契约。图前不写“下图标出……”这类只交代图面内容的句子;这种句子属于图注。
|
|
10
|
+
- 图后只写从图带走的结论或图外新信息。图注已写明的坐标、方位、图例、成员取舍不再复述。
|
|
11
|
+
|
|
12
|
+
## 图注写法
|
|
13
|
+
|
|
14
|
+
图注是一句完整的读者句,至少包含“这张图画的是什么”和“读图要看的那条关系”。不手写“图 1:”,Quarto 按 `lang: zh` 自动编号。
|
|
15
|
+
|
|
16
|
+
- 不佳:下图标出这块棋盘上可用的 `(row, col)`。
|
|
17
|
+
- 改写(图下):`棋盘坐标图:3 行 3 列,每格标出 (row, col);行向下、列向右,左上角是 (0, 0)。`
|
|
18
|
+
- 原句只是图面说明,移入 `<figcaption>`,不再占据图前正文。
|
|
19
|
+
- 不佳:总览图为什么不列成员?
|
|
20
|
+
- 改写(删除;图注写“总览只画类型与关系”即可)。
|
|
21
|
+
|
|
22
|
+
图注只解释图本身,不承担章节过渡,也不替读者总结整节。
|
|
23
|
+
|
|
24
|
+
## 原生 HTML/SVG
|
|
25
|
+
|
|
26
|
+
原生 HTML/SVG 图用 `<figure>` 包住,图注用 `<figcaption>`,图注必须是 `figure` 的最后一个子元素:
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<figure class="book-figure">
|
|
30
|
+
<svg class="coords-grid" role="img" aria-label="棋盘坐标:3 行 3 列,左上角为 (0, 0)。">…</svg>
|
|
31
|
+
<figcaption>棋盘坐标图:3 行 3 列,每格标出 (row, col);行向下、列向右,左上角是 (0, 0)。</figcaption>
|
|
32
|
+
</figure>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `role="img"` 与 `aria-label` 给读屏软件一个短名称;`figcaption` 给视觉读者完整图注。
|
|
36
|
+
- 主题用 `.book-figure` 统一居中、限制最大宽度,并让 `figcaption` 落在图下;不要另写行内样式。
|
|
37
|
+
- 图注内需要行内代码时写 `<code>`,不要写 Markdown 反引号——`{=html}` 围栏按原样输出。
|
|
38
|
+
|
|
39
|
+
## 与其他图类的关系
|
|
40
|
+
|
|
41
|
+
- Mermaid 的方向、节点预算与线型约定见 `quarto/mermaid.md`;它的 `fig-cap` 同样遵守本文件。
|
|
42
|
+
- 坐标图优先用 `quarto/coords.md` 的 `{python}` 助手;没有执行环境时,允许静态 HTML/SVG,但必须按本文件包 `<figure>` 与 `<figcaption>`,不能把图注留在图前。
|
|
43
|
+
- 代码到图的固定序列见 `quarto/authoring.md`「代码与图的桥接」与 `steps/03-blocks.md`;桥接句讲关系,图注解图面,两者分工不同。
|
|
44
|
+
|
|
45
|
+
## 自检
|
|
46
|
+
|
|
47
|
+
- 图前是否还有“下图 / 图中 / 本图”这类只交代图面的句子?有就移入图注或删除。
|
|
48
|
+
- 图注是否重复了图后正文?重复就删正文或删图注,只留一处。
|
|
49
|
+
- 原生 SVG 是否在 `figure` 内,`figcaption` 是否在 `svg` 之后、`</figure>` 之前?
|
|
@@ -64,4 +64,4 @@ mermaid 用 init 指令中的字体测量标签宽度,页面 CSS 按 `--ui-fon
|
|
|
64
64
|
|
|
65
65
|
每个 `{mermaid}` 图块在围栏开头、init 之前写一行 `%%| fig-cap: "程序结构总览。实线箭头表示调用或依赖,……"`。`fig-cap` 只写图意和读图约定,不手写“图 1:”这类编号;Quarto 按 `lang: zh` 自动在正文中排成“图 1:”。图注紧跟图块开头、落在图下。线型、箭头方向、读图范围这类说明只写进该图的图注,图后正文不再复述;图后只补图外的新信息或从图带走的结论,不点名被省略的成员,也不写“其余成员见某章”。`fig-cap` 是 Quarto 对 Mermaid 的单元选项语法,`%%|` 必须直接写在围栏开头、init 之前(Quarto 只解析块首选项;写在 init 之后会被当成 Mermaid 注释原样渲染),不能改成 `#|` 或图后引用块。
|
|
66
66
|
|
|
67
|
-
图注文字概括图意,不与图后正文逐句重复;需要交叉引用时同时写 `label: fig-...`,正文用 `@fig-...`
|
|
67
|
+
图注文字概括图意,不与图后正文逐句重复;需要交叉引用时同时写 `label: fig-...`,正文用 `@fig-...` 引用。图注与图前句的统一分工、原生 HTML/SVG 的图注写法见 `quarto/figures.md`。
|
|
@@ -14,6 +14,6 @@
|
|
|
14
14
|
|
|
15
15
|
## 收尾
|
|
16
16
|
|
|
17
|
-
全仓重构后,核对导航与路由表、标题层级、章节链接、源码摘录、命令和输出。先运行增量验证定位问题,再对所有 QMD 执行全量 `verifying-content.py
|
|
17
|
+
全仓重构后,核对导航与路由表、标题层级、章节链接、源码摘录、命令和输出。先运行增量验证定位问题,再对所有 QMD 执行全量 `verifying-content.py`,最后在仓库根目录渲染 Book。按章推进时,每一章的出口是 `verifying-content.py --root <书根> --paths <该章>`;全部章节通过之后才 `render`。涉及知识库时同步索引、健康检查和评测;涉及代码时用实际构建、CTest 或示例验证核对文档。
|
|
18
18
|
|
|
19
19
|
## 本项目边界
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 整本书的写作次序
|
|
2
|
+
|
|
3
|
+
单章润色看一章;整书重构先看这本书要带读者完成什么,再决定章节次序。本文件只定整书层的判断,单章规则仍在 `steps/`。
|
|
4
|
+
|
|
5
|
+
## 先定一条主线
|
|
6
|
+
|
|
7
|
+
把目录写成一句可验收的话:读者从什么基础出发,按顺序做完哪些事,最后能独立交付什么。主线要落到一个可运行、可检查的产物;章节是这条主线上的阶段,不是互不相干的 API 清单。
|
|
8
|
+
|
|
9
|
+
写目录前先删掉三类章:同一对象拆成多章、只罗列类型而没有阶段结果、前后章可以任意交换。保留下来的章,每一章只回答一个关键问题,并在开头接住上一章的结果、结尾交出下一章需要的输入。
|
|
10
|
+
|
|
11
|
+
## 三遍走
|
|
12
|
+
|
|
13
|
+
1. **骨架遍**:先排章节次序与每章关键,确认读者按顺序能完成主线;此时不写句子,不补代码。
|
|
14
|
+
2. **章节遍**:每章按 `steps/01-task.md` 到 `steps/09-check.md` 走九步,只改这一章;章与章的接口在章首、章末对齐。
|
|
15
|
+
3. **全书遍**:从头回读,检查术语、项目名、图注、链接与前后承诺;只改跨章不一致,不在这一遍重写单章。
|
|
16
|
+
|
|
17
|
+
骨架遍改次序,章节遍改正文,全书遍改一致性;三遍不混做。
|
|
18
|
+
|
|
19
|
+
## 以井字棋书为例
|
|
20
|
+
|
|
21
|
+
| 阶段 | 章节 | 交给下一章的结果 |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| 规则 | 对局规则 | 一局怎样算结束 |
|
|
24
|
+
| 结构 | 工程结构、程序结构 | 文件与类的整体形状 |
|
|
25
|
+
| 数据 | 棋盘、对局 | 格子、坐标、落子后的状态 |
|
|
26
|
+
| 决策 | 电脑落子 | 电脑怎样选一格 |
|
|
27
|
+
| 交互 | 终端程序、程序入口 | 人怎样下一局 |
|
|
28
|
+
| 质量 | 调试、测试、构建与运行 | 改动后怎样确认没坏 |
|
|
29
|
+
| 收束 | 局面快照 | 一局怎样保存与还原 |
|
|
30
|
+
|
|
31
|
+
这张表只是示例。项目有自己的章节表时,以 `.agents/skills/quarto-book-project/writing/chapter-map.md` 为准;没有就按上面的阶段顺序推,不猜项目事实。
|
|
32
|
+
|
|
33
|
+
## 插件层怎么配合
|
|
34
|
+
|
|
35
|
+
- 整书规划先读本文件与项目章节表,再逐章加载 `steps/`;不要一次读完九步。
|
|
36
|
+
- 单章判不准时才开 `examples/`;排图、查渲染或全仓重构才开 `quarto/`。
|
|
37
|
+
- 全仓检查用 `scripts/`,渲染验收用 `quarto_render_check`;两件事都放在最后一遍,不在骨架遍执行。
|
|
38
|
+
- 文件体量受 `references/token-budget.md` 约束;新增整书规则只写进本文件,不复制到各步。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# 技能文件的 token 预算
|
|
2
|
+
|
|
3
|
+
本文件约束技能 Markdown 的体量。它只管加载成本,不替代 `docs/writing-quarto-layout.md` 的分层规则。
|
|
4
|
+
|
|
5
|
+
## 预算
|
|
6
|
+
|
|
7
|
+
估算口径:一个 CJK 字符按 1 token,其余字符按 4 字符 1 token 计。`scripts/checking-skill-budget.mjs` 用同一口径检查,`npm run check` 会执行。
|
|
8
|
+
|
|
9
|
+
| 目录或文件 | 单文件上限 |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `writing-quarto/SKILL.md`、`designing-theme/SKILL.md` | 2000 |
|
|
12
|
+
| `steps/02-structure.md`、`steps/03-blocks.md`、`steps/05-sentences.md` | 12000 |
|
|
13
|
+
| 其余 `steps/*.md` | 8000 |
|
|
14
|
+
| `examples/*.md` | 5000 |
|
|
15
|
+
| `quarto/*.md` | 5000 |
|
|
16
|
+
| `designing-theme/references/theme-system.md` | 8000 |
|
|
17
|
+
| 其余 `references/*.md`、`knowledge/*.md` | 6000 |
|
|
18
|
+
| `assets/**/meta.md` | 2000 |
|
|
19
|
+
| 全部技能 Markdown 合计 | 140000 |
|
|
20
|
+
|
|
21
|
+
## 怎么用
|
|
22
|
+
|
|
23
|
+
- 新增规则先判断属于哪一层,再写进已有文件;同一判定只成文一处,其他位置写引用。
|
|
24
|
+
- 文件将超限时,先删重复、合并同源条目,再考虑拆成同目录的新文件;拆文件后同步入口和 `scripts/checking-skill-links.mjs`。
|
|
25
|
+
- 单章润色只加载 `SKILL.md` 与当前一步;不要为了省 token 把整本书的规则塞进 `SKILL.md`。
|
|
26
|
+
- 预算不是目标:没必要的条文直接删,不靠缩写把句子压成暗语。
|
|
@@ -6,7 +6,7 @@ Quarto 只认内置 5 类 callout(note/tip/warning/important/caution)。未
|
|
|
6
6
|
<section> 没有提示框样式。它还混进右侧目录。本脚本在 _book/**/*.html 里抓这一回归。
|
|
7
7
|
|
|
8
8
|
用法:python checking-callouts.py [--root .] [--book-dir _book]
|
|
9
|
-
退出码:0 = 全部正常,1 = 发现退化的 callout
|
|
9
|
+
退出码:0 = 全部正常,1 = 发现退化的 callout,2 = 渲染产物目录缺失。
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import argparse
|
|
@@ -71,7 +71,7 @@ def main():
|
|
|
71
71
|
book_dir = root / book_dir
|
|
72
72
|
if not book_dir.is_dir():
|
|
73
73
|
print(f"未找到渲染产物目录:{book_dir};请先执行 quarto render。")
|
|
74
|
-
return
|
|
74
|
+
return 2
|
|
75
75
|
|
|
76
76
|
html_files = sorted(book_dir.rglob("*.html"))
|
|
77
77
|
bad_pages = 0
|
|
@@ -240,7 +240,8 @@ def check_contracts(book_dir, htmls, css_pairs, root=None, require_favicon=True)
|
|
|
240
240
|
))
|
|
241
241
|
|
|
242
242
|
# ---------- C1 复制按钮可见性 ----------
|
|
243
|
-
|
|
243
|
+
checked = 0
|
|
244
|
+
bad = 0
|
|
244
245
|
scaffold_pages = 0
|
|
245
246
|
for path, tree, _rels in htmls:
|
|
246
247
|
scaffolds = [n for n in walk(tree) if SCAFFOLD in n.classes]
|
|
@@ -249,20 +250,23 @@ def check_contracts(book_dir, htmls, css_pairs, root=None, require_favicon=True)
|
|
|
249
250
|
scaffold_pages += 1
|
|
250
251
|
for sc in scaffolds:
|
|
251
252
|
code = [c for c in sc.children if c.tag == "div" and SRCODE in c.classes]
|
|
253
|
+
if not code:
|
|
254
|
+
continue
|
|
255
|
+
checked += 1
|
|
252
256
|
btns = [c for c in sc.children if c.tag == "button" and COPY_BTN in c.classes]
|
|
253
|
-
if
|
|
254
|
-
|
|
257
|
+
if not btns or any(is_descendant(b, code[0]) for b in btns):
|
|
258
|
+
bad += 1
|
|
255
259
|
hover_rule = any(
|
|
256
260
|
".%s:hover .%s" % (SCAFFOLD, COPY_BTN) in css for _, css in css_pairs
|
|
257
261
|
)
|
|
258
262
|
stale_rule = any(
|
|
259
263
|
"div.%s:hover .%s" % (SRCODE, COPY_BTN) in css for _, css in css_pairs
|
|
260
264
|
)
|
|
261
|
-
c1 =
|
|
265
|
+
c1 = checked > 0 and bad == 0 and hover_rule and not stale_rule
|
|
262
266
|
results.append((
|
|
263
267
|
"C1", "复制按钮 hover 可见(兄弟 DOM + scaffold 选择器)", c1,
|
|
264
|
-
"pages=%d
|
|
265
|
-
% (scaffold_pages,
|
|
268
|
+
"pages=%d checked=%d bad=%d hover-rule=%s%s"
|
|
269
|
+
% (scaffold_pages, checked, bad, hover_rule, " stale-rule!" if stale_rule else ""),
|
|
266
270
|
[
|
|
267
271
|
"产物中 button.%s 必须与 div.%s 同为 .%s 的直接子元素" % (COPY_BTN, SRCODE, SCAFFOLD),
|
|
268
272
|
"CSS 必须含 .%s:hover .%s(不能停留在 div.%s:hover 的后代写法)"
|
|
@@ -535,6 +539,8 @@ def main():
|
|
|
535
539
|
args = parser.parse_args()
|
|
536
540
|
|
|
537
541
|
book_dir = Path(args.book_dir)
|
|
542
|
+
if not book_dir.is_absolute():
|
|
543
|
+
book_dir = Path(args.root) / book_dir
|
|
538
544
|
if not book_dir.is_dir():
|
|
539
545
|
print("MISS 产物目录不存在:%s(请先运行 quarto render)" % book_dir)
|
|
540
546
|
return 2
|
|
@@ -21,7 +21,9 @@ from pathlib import Path
|
|
|
21
21
|
|
|
22
22
|
LINK_RE = re.compile(r"\[[^\]]*\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)")
|
|
23
23
|
HTML_LINK_RE = re.compile(r"<(?:a|img)\b[^>]*\b(?:href|src)=\"([^\"]+)\"", re.IGNORECASE)
|
|
24
|
+
FENCE_RE = re.compile(r"^\s*(```+|~~~+)")
|
|
24
25
|
SKIP_PREFIXES = ("http://", "https://", "mailto:", "tel:", "data:", "#", "//")
|
|
26
|
+
SKIP_DIRS = {".git", ".cache", ".quarto", "_book", "_freeze", "_site", "build", "node_modules", "temp"}
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def collect_sources(root: Path, paths: list[str]) -> list[Path]:
|
|
@@ -41,17 +43,24 @@ def collect_sources(root: Path, paths: list[str]) -> list[Path]:
|
|
|
41
43
|
return sorted(
|
|
42
44
|
path for path in found
|
|
43
45
|
if not any(
|
|
44
|
-
part in
|
|
45
|
-
for part in path.parts
|
|
46
|
+
part in SKIP_DIRS
|
|
47
|
+
for part in path.relative_to(root).parts
|
|
46
48
|
)
|
|
47
49
|
)
|
|
48
50
|
|
|
49
51
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
def iter_links(text: str):
|
|
53
|
+
in_fence = False
|
|
54
|
+
for number, line in enumerate(text.splitlines(), start=1):
|
|
55
|
+
if FENCE_RE.match(line):
|
|
56
|
+
in_fence = not in_fence
|
|
57
|
+
continue
|
|
58
|
+
if in_fence:
|
|
59
|
+
continue
|
|
60
|
+
for match in LINK_RE.finditer(line):
|
|
61
|
+
yield number, match.group(1)
|
|
62
|
+
for match in HTML_LINK_RE.finditer(line):
|
|
63
|
+
yield number, match.group(1)
|
|
55
64
|
|
|
56
65
|
|
|
57
66
|
def main() -> int:
|
|
@@ -72,13 +81,20 @@ def main() -> int:
|
|
|
72
81
|
return 2
|
|
73
82
|
|
|
74
83
|
sources = collect_sources(root, args.paths)
|
|
75
|
-
|
|
84
|
+
if not sources:
|
|
85
|
+
print("失败 没有可检查的 Markdown/Quarto 源文件", file=sys.stderr)
|
|
86
|
+
return 2
|
|
87
|
+
broken: list[tuple[str, int, str, str]] = []
|
|
76
88
|
checked = 0
|
|
77
89
|
|
|
78
90
|
for source in sources:
|
|
79
|
-
|
|
91
|
+
try:
|
|
92
|
+
text = source.read_text(encoding="utf-8")
|
|
93
|
+
except UnicodeDecodeError as exc:
|
|
94
|
+
print(f"失败 {source} 不是合法 UTF-8:{exc}", file=sys.stderr)
|
|
95
|
+
return 1
|
|
80
96
|
rel = source.relative_to(root).as_posix()
|
|
81
|
-
for raw in
|
|
97
|
+
for number, raw in iter_links(text):
|
|
82
98
|
target = raw.strip()
|
|
83
99
|
if not target or target.startswith(SKIP_PREFIXES):
|
|
84
100
|
continue
|
|
@@ -91,7 +107,7 @@ def main() -> int:
|
|
|
91
107
|
candidates = [source.parent / clean, root / clean]
|
|
92
108
|
if any(candidate.exists() for candidate in candidates):
|
|
93
109
|
continue
|
|
94
|
-
broken.append((rel, target, "目标不存在"))
|
|
110
|
+
broken.append((rel, number, target, "目标不存在"))
|
|
95
111
|
|
|
96
112
|
print(f"扫描 {len(sources)} 个文件,检查 {checked} 条本地链接。")
|
|
97
113
|
if not broken:
|
|
@@ -99,8 +115,8 @@ def main() -> int:
|
|
|
99
115
|
return 0
|
|
100
116
|
|
|
101
117
|
print(f"发现 {len(broken)} 条断链:")
|
|
102
|
-
for rel, target, reason in broken[:80]:
|
|
103
|
-
print(f" {rel} -> {target}({reason})")
|
|
118
|
+
for rel, number, target, reason in broken[:80]:
|
|
119
|
+
print(f" {rel}:{number} -> {target}({reason})")
|
|
104
120
|
return 1
|
|
105
121
|
|
|
106
122
|
|
|
@@ -33,7 +33,7 @@ EXCLUDE_DIR_SUFFIXES = ("_files",)
|
|
|
33
33
|
|
|
34
34
|
TOOL_FIXED_NAMES = {
|
|
35
35
|
"CMakeLists.txt", "_quarto.yml", "_quarto.yaml", "SKILL.md", "AGENTS.md",
|
|
36
|
-
"README.md", "README.en.md", "LICENSE", "CONTRIBUTING.md",
|
|
36
|
+
"README.md", "README.en.md", "LICENSE", "OFL.txt", "CONTRIBUTING.md",
|
|
37
37
|
".editorconfig", ".gitignore", ".gitattributes", "openai.yaml",
|
|
38
38
|
"package.json", "package-lock.json", "pnpm-lock.yaml", "tsconfig.json",
|
|
39
39
|
}
|
|
@@ -56,6 +56,8 @@ def name_form_error(name: str) -> str | None:
|
|
|
56
56
|
if " " in name:
|
|
57
57
|
return "含空格"
|
|
58
58
|
if "-" not in name:
|
|
59
|
+
if not all(ch.islower() or ch.isdigit() or ch == "." for ch in name):
|
|
60
|
+
return "含非小写字符"
|
|
59
61
|
return None
|
|
60
62
|
parts = name.split("-")
|
|
61
63
|
if any(part == "" for part in parts):
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
不在本检查范围内。
|
|
8
8
|
|
|
9
9
|
用法:python checking-output.py [--book-dir _book] [--root .] [--require-sitemap]
|
|
10
|
-
退出码:0 = 通过;1 =
|
|
10
|
+
退出码:0 = 通过;1 = 存在阻塞问题;2 = 渲染产物目录或首页缺失。
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import argparse
|
|
@@ -37,12 +37,12 @@ def main() -> int:
|
|
|
37
37
|
index = book / "index.html"
|
|
38
38
|
if not index.is_file():
|
|
39
39
|
print(f"缺少产物首页 {book.as_posix()}/index.html:确认在仓库根目录渲染")
|
|
40
|
-
return
|
|
40
|
+
return 2
|
|
41
41
|
|
|
42
42
|
html_files = sorted(book.rglob("*.html"))
|
|
43
43
|
if not html_files:
|
|
44
44
|
print(f"{book.as_posix()} 下没有任何 HTML 文件")
|
|
45
|
-
return
|
|
45
|
+
return 2
|
|
46
46
|
|
|
47
47
|
for path in html_files:
|
|
48
48
|
rel = path.relative_to(book).as_posix()
|
|
@@ -42,13 +42,13 @@ class Issue:
|
|
|
42
42
|
message: str
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def
|
|
45
|
+
def split_frontmatter(text: str) -> tuple[int, str]:
|
|
46
46
|
lines = text.splitlines()
|
|
47
47
|
if lines and lines[0].strip() == "---":
|
|
48
48
|
for index in range(1, len(lines)):
|
|
49
49
|
if lines[index].strip() == "---":
|
|
50
|
-
return "\n".join(lines[index + 1 :])
|
|
51
|
-
return text
|
|
50
|
+
return index + 1, "\n".join(lines[index + 1 :])
|
|
51
|
+
return 0, text
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def mask_links_and_tags(line: str) -> str:
|
|
@@ -98,11 +98,11 @@ def check_line(path: str, number: int, line: str) -> list[Issue]:
|
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
def scan_file(root: Path, path: Path) -> list[Issue]:
|
|
101
|
-
text = path.read_text(encoding="utf-8"
|
|
102
|
-
|
|
101
|
+
text = path.read_text(encoding="utf-8")
|
|
102
|
+
offset, body = split_frontmatter(text)
|
|
103
103
|
issues: list[Issue] = []
|
|
104
104
|
in_fence = False
|
|
105
|
-
for number, line in enumerate(
|
|
105
|
+
for number, line in enumerate(body.splitlines(), start=offset + 1):
|
|
106
106
|
if FENCE.match(line):
|
|
107
107
|
in_fence = not in_fence
|
|
108
108
|
continue
|
|
@@ -135,7 +135,11 @@ def main() -> int:
|
|
|
135
135
|
|
|
136
136
|
issues: list[Issue] = []
|
|
137
137
|
for path in collect_files(root):
|
|
138
|
-
|
|
138
|
+
try:
|
|
139
|
+
issues.extend(scan_file(root, path))
|
|
140
|
+
except UnicodeDecodeError as exc:
|
|
141
|
+
print(f"失败 {path} 不是合法 UTF-8:{exc}", file=sys.stderr)
|
|
142
|
+
return 1
|
|
139
143
|
|
|
140
144
|
if args.json:
|
|
141
145
|
print(
|