@chengzhao-dev/dsh-quarto-book 0.6.2 → 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 +60 -31
- package/README.md +53 -24
- package/lib/index.js +49 -7
- package/lib/tools/pass-report.js +12 -12
- package/lib/tools/render-check.js +231 -62
- package/lib/workspace.js +1 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +13 -7
- package/skills/designing-theme/assets/theme/assets/fonts/lxgw-bright-code-common.woff2 +0 -0
- package/skills/designing-theme/assets/theme/assets/fonts/lxgw-bright-medium-common.woff2 +0 -0
- package/skills/designing-theme/assets/theme/css/content.css +15 -0
- package/skills/designing-theme/assets/theme/css/mermaid.css +5 -6
- package/skills/designing-theme/knowledge/reading-surface.md +1 -1
- package/skills/designing-theme/references/theme-system.md +5 -5
- package/skills/designing-theme/scripts/checking-layout.py +60 -12
- package/skills/designing-theme/scripts/measuring-pages.mjs +2 -2
- package/skills/writing-quarto/SKILL.md +7 -0
- package/skills/writing-quarto/examples/02-structure-shape.md +26 -12
- package/skills/writing-quarto/examples/03-block-after.md +14 -0
- package/skills/writing-quarto/examples/03-qmd-elements.md +45 -2
- package/skills/writing-quarto/examples/04-paragraph-cases.md +14 -0
- package/skills/writing-quarto/examples/04-paragraph-opening.md +2 -2
- package/skills/writing-quarto/examples/05-sentence-frame.md +4 -4
- package/skills/writing-quarto/examples/06-wording-cases.md +23 -0
- package/skills/writing-quarto/examples/07-layout-list.md +3 -3
- package/skills/writing-quarto/examples/08-appendix.md +1 -1
- package/skills/writing-quarto/quarto/authoring.md +7 -6
- package/skills/writing-quarto/quarto/basics.md +17 -16
- 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 +41 -5
- package/skills/writing-quarto/quarto/prose.md +4 -4
- package/skills/writing-quarto/quarto/refactor.md +1 -1
- package/skills/writing-quarto/quarto/terminal.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 +264 -1
- package/skills/writing-quarto/scripts/verifying-content.py +442 -36
- package/skills/writing-quarto/steps/03-blocks.md +20 -8
- package/skills/writing-quarto/steps/04-paragraphs.md +19 -2
- package/skills/writing-quarto/steps/06-wording.md +53 -0
- package/skills/writing-quarto/steps/07-layout.md +1 -1
- package/skills/writing-quarto/steps/08-reread.md +1 -0
- package/skills/writing-quarto/steps/09-check.md +3 -3
- package/templates/quarto-book-project/SKILL.md +1 -1
- package/templates/quarto-book-project/checks.json +5 -0
|
@@ -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>` 之前?
|
|
@@ -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
|
-
|
|
30
|
-
|
|
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-...` 引用。图注与图前句的统一分工、原生 HTML/SVG 的图注写法见 `quarto/figures.md`。
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
- 冒号前必须是总说或提示语;冒号后的内容离开前文能读成完整一句。
|
|
37
37
|
- 同一列表的末项标点统一:都用句号,或都不带句号。
|
|
38
38
|
|
|
39
|
-
反例:`Board`
|
|
39
|
+
反例:`Board` 只保存九个棋盘格子。落子、满盘、成线与赢家都从这些格子得出。写入前先检查坐标。声明在工程目录的 board.h。实现放在 board.cpp。
|
|
40
40
|
|
|
41
|
-
改写:`Board`
|
|
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`,棋盘不变。
|
|
@@ -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
|
## 本项目边界
|
|
@@ -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
|
|
|
@@ -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(
|