@chengzhao-dev/dsh-quarto-book 0.6.12 → 0.6.14
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 +9 -1
- package/README.md +9 -1
- package/lib/tools/render-check.js +3 -2
- package/package.json +3 -1
- package/skills/designing-theme/assets/theme/css/nav.css +24 -3
- package/skills/designing-theme/references/theme-spine.md +1 -1
- package/skills/designing-theme/references/theme-system.md +1 -1
- package/skills/designing-theme/scripts/checking-layout.py +31 -0
- package/skills/designing-theme/scripts/measuring-pages.mjs +34 -1
- package/skills/writing-quarto/SKILL.md +7 -5
- package/skills/writing-quarto/quarto/readability.md +1 -1
- package/skills/writing-quarto/references/fence-semantics.md +68 -0
- package/skills/writing-quarto/references/rules-cards.md +164 -0
- package/skills/writing-quarto/scripts/checking-names.py +15 -1
- package/skills/writing-quarto/scripts/verifying-content.py +111 -2
- package/skills/writing-quarto/steps/03-blocks.md +4 -0
- package/skills/writing-quarto/steps/08-reread.md +3 -0
- package/skills/writing-quarto/steps/09-check.md +3 -1
package/README.en.md
CHANGED
|
@@ -29,7 +29,7 @@ Both skills are discovered from the package's `skills/` directory and loaded on
|
|
|
29
29
|
2. Open a session in the book repository. `_quarto.yml` needs a standalone `type: book` line, and the book needs at least one `.qmd` or `.md` chapter source; only then do both skill names appear.
|
|
30
30
|
3. Call a skill: `writing-quarto` for prose, `designing-theme` for the theme.
|
|
31
31
|
|
|
32
|
-
Replace `--profile` with the profile you actually run. The desktop app always runs the `desktop` profile, and that profile is owned by the app, so `dsh --profile desktop` is rejected; install or upgrade with `dsh plugin --profile desktop add <package>`, and use the app's Settings → Plugins → Add plugin only if the app refuses that write. To pin a version, write `npm:@chengzhao-dev/dsh-quarto-book@0.6.
|
|
32
|
+
Replace `--profile` with the profile you actually run. The desktop app always runs the `desktop` profile, and that profile is owned by the app, so `dsh --profile desktop` is rejected; install or upgrade with `dsh plugin --profile desktop add <package>`, and use the app's Settings → Plugins → Add plugin only if the app refuses that write. To pin a version, write `npm:@chengzhao-dev/dsh-quarto-book@0.6.14`; to follow a GitHub tag, write `github:chengzhao-dev/dsh-quarto-book#v0.6.14`.
|
|
33
33
|
|
|
34
34
|
Upgrading from the old unscoped package `dsh-quarto-book` (removed from npm with 0.3.4, so this only affects profiles that installed it earlier): remove it from the same profile first with `dsh plugin --profile <profile> remove dsh-quarto-book`, then add the scoped package. Keeping both installed registers `writing-quarto` and `designing-theme` twice at the same rank 550.
|
|
35
35
|
|
|
@@ -106,6 +106,14 @@ npm run pack
|
|
|
106
106
|
|
|
107
107
|
`npm test` compiles and runs the tool tests; `npm run check` type-checks and validates skill paths; `npm run pack` writes the tarball into `temp/`, which is already in `.gitignore`.
|
|
108
108
|
|
|
109
|
+
Replay the CI sequence locally with one command before pushing:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm run ci
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
It runs `npm ci`, `npm test`, `npm run check:lib`, `npm run check`, and `npm run pack:check` — the same steps as `.github/workflows/ci.yml`, plus the publish-time package gate.
|
|
116
|
+
|
|
109
117
|
Development install:
|
|
110
118
|
|
|
111
119
|
```bash
|
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
2. 在书仓库里打开会话。`_quarto.yml` 要有独立一行的 `type: book`,书根至少有一个 `.qmd` 或 `.md` 章节源文件;两个技能名这时才会出现。
|
|
30
30
|
3. 调用技能:写正文用 `writing-quarto`,改主题用 `designing-theme`。
|
|
31
31
|
|
|
32
|
-
`--profile` 换成你实际运行的那个 profile。桌面应用固定跑 `desktop` profile,且该 profile 由应用独占管理,`dsh --profile desktop` 直接启动会被拒绝;安装或升级用 `dsh plugin --profile desktop add <包>`,应用拒绝写入时再走「设置 → 插件 → 添加插件」。需要固定版本时写 `npm:@chengzhao-dev/dsh-quarto-book@0.6.
|
|
32
|
+
`--profile` 换成你实际运行的那个 profile。桌面应用固定跑 `desktop` profile,且该 profile 由应用独占管理,`dsh --profile desktop` 直接启动会被拒绝;安装或升级用 `dsh plugin --profile desktop add <包>`,应用拒绝写入时再走「设置 → 插件 → 添加插件」。需要固定版本时写 `npm:@chengzhao-dev/dsh-quarto-book@0.6.14`,跟 GitHub 标签时写 `github:chengzhao-dev/dsh-quarto-book#v0.6.14`。
|
|
33
33
|
|
|
34
34
|
从旧的无作用域包 `dsh-quarto-book` 升级(该包已随 0.3.4 从 npm 删除,只影响之前装过它的 profile):先在同一个 profile 里 `dsh plugin --profile <profile> remove dsh-quarto-book`,再装新包。两个包同时留着会以相同的 rank 550 各注册一遍 `writing-quarto` 和 `designing-theme`。
|
|
35
35
|
|
|
@@ -108,6 +108,14 @@ npm run pack
|
|
|
108
108
|
|
|
109
109
|
`npm test` 会先编译再跑工具测试;`npm run check` 做类型检查与技能路径自检;`npm run pack` 把 tarball 只写进 `temp/`,那个目录已在 `.gitignore` 里。
|
|
110
110
|
|
|
111
|
+
推送前用一条命令复现 CI 的检查序列:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npm run ci
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`npm run ci` 依次执行 `npm ci`、`npm test`、`npm run check:lib`、`npm run check` 与 `npm run pack:check`,覆盖 `.github/workflows/ci.yml` 的全部步骤,并在打包前挡住发布内容的问题。
|
|
118
|
+
|
|
111
119
|
开发安装:
|
|
112
120
|
|
|
113
121
|
```bash
|
|
@@ -774,11 +774,12 @@ export function registerRenderCheckTool(tools) {
|
|
|
774
774
|
if (typeof cwd !== 'string' || cwd.trim() === '') {
|
|
775
775
|
return refusedResult('当前会话没有 cwd;quarto_render_check 只在 Quarto Book 项目的会话 cwd 内工作。');
|
|
776
776
|
}
|
|
777
|
-
const
|
|
777
|
+
const qmdPath = path.resolve(cwd, args.qmd_path);
|
|
778
|
+
const root = (await findBookRoot(cwd)) ?? (await findBookRoot(path.dirname(qmdPath)));
|
|
778
779
|
if (!root) {
|
|
779
780
|
return refusedResult('当前会话不在 Quarto Book 项目内:需要 git checkout、_quarto.yml 的 type: book,以及至少一个 .qmd 或 .md 源文件。');
|
|
780
781
|
}
|
|
781
|
-
return runRenderCheck({ root, qmdPath
|
|
782
|
+
return runRenderCheck({ root, qmdPath, dryRun: args.dry_run, signal: exec?.signal });
|
|
782
783
|
},
|
|
783
784
|
presentCall(args) {
|
|
784
785
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chengzhao-dev/dsh-quarto-book",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.14",
|
|
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",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsc -p tsconfig.json",
|
|
50
50
|
"test": "npm run build && node scripts/checking-github-workflows.mjs && node --test \"tests/**/*.test.mjs\"",
|
|
51
|
+
"ci": "npm ci && npm test && npm run check:lib && npm run check && npm run pack:check",
|
|
51
52
|
"check": "tsc --noEmit -p tsconfig.json && node scripts/checking-skill-links.mjs && node scripts/checking-skill-budget.mjs && node scripts/checking-package.mjs && node scripts/checking-github-workflows.mjs",
|
|
52
53
|
"check:lib": "node scripts/checking-lib.mjs",
|
|
53
54
|
"check:workflows": "node scripts/checking-github-workflows.mjs",
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"dsh",
|
|
61
62
|
"deepseek-harness",
|
|
62
63
|
"dsh-plugin",
|
|
64
|
+
"cordis",
|
|
63
65
|
"quarto",
|
|
64
66
|
"quarto-book",
|
|
65
67
|
"technical-writing",
|
|
@@ -570,13 +570,29 @@ html[data-palette="solarized"] .navbar-brand::before {
|
|
|
570
570
|
width: 6rem;
|
|
571
571
|
}
|
|
572
572
|
|
|
573
|
+
/* 只剩书标:书标放到 1.375rem 补回丢掉标题后的视觉重量,并撤掉桌面端那条
|
|
574
|
+
0.2rem 抬升——它原本用来对齐汉字基线,没有标题时反而把图标顶高。 */
|
|
575
|
+
.navbar-brand {
|
|
576
|
+
position: relative;
|
|
577
|
+
}
|
|
573
578
|
.navbar-brand::before {
|
|
574
|
-
width: 1.
|
|
575
|
-
height: 1.
|
|
579
|
+
width: 1.375rem;
|
|
580
|
+
height: 1.375rem;
|
|
581
|
+
margin-bottom: 0;
|
|
576
582
|
}
|
|
577
583
|
|
|
584
|
+
/* 书名改成视觉隐藏而不是 display:none:这枚链接仍是「回到首页」,
|
|
585
|
+
屏幕阅读器要能读出书名,否则书标就是一个没有名字的链接。 */
|
|
578
586
|
.navbar-title {
|
|
579
|
-
|
|
587
|
+
position: absolute;
|
|
588
|
+
width: 1px;
|
|
589
|
+
height: 1px;
|
|
590
|
+
margin: -1px;
|
|
591
|
+
padding: 0;
|
|
592
|
+
border: 0;
|
|
593
|
+
overflow: hidden;
|
|
594
|
+
clip-path: inset(50%);
|
|
595
|
+
white-space: nowrap;
|
|
580
596
|
}
|
|
581
597
|
}
|
|
582
598
|
/* 主题切换:可点区域 44px,图标仍是 16px */
|
|
@@ -689,6 +705,11 @@ nav.quarto-secondary-nav .container-fluid.d-flex {
|
|
|
689
705
|
nav.quarto-secondary-nav .quarto-search-button {
|
|
690
706
|
display: none;
|
|
691
707
|
}
|
|
708
|
+
/* 首页与部首页在次级导航里只有一项(书名或部名),紧挨顶栏品牌又说一遍;
|
|
709
|
+
它也说不出“书内位置”,故只留侧栏开关,让次级导航只承担章节轨迹。 */
|
|
710
|
+
nav.quarto-secondary-nav .quarto-page-breadcrumbs ol.breadcrumb:has(> li:only-child) {
|
|
711
|
+
display: none;
|
|
712
|
+
}
|
|
692
713
|
/* 宽屏只隐藏次级导航里的面包屑(仅留汉堡);标题块面包屑桌面端显示、与标题同栏左对齐,
|
|
693
714
|
窄屏由 Quarto 生成的 d-none 自动隐藏(改显次级导航实例) */
|
|
694
715
|
@media (min-width: 992px) {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
| 3 定尺度 | 字号台阶、行长与栏宽;视口不够时不改字号,只让代码块横向滚动 | `references/theme-system.md`「字号、行长与页面尺度(定尺度)」+ `assets/theme/css/tokens.css` |
|
|
14
14
|
| 4 定颜色 | 正文、强调、链接、次要文字、代码标题在 `body` 与 `body.quarto-dark` 各写一套;Solarized 覆盖层写 `html[data-palette="solarized"]`,山光覆盖层写 `html[data-palette="hill-light"][data-mode="…"] body`;代码语法色默认改 `palettes/flexoki/highlight-light.theme` / `highlight-dark.theme` 两份主题文件,Solarized 与山光的语法色写各自 `palettes/<name>/syntax.css`;切换后核对加粗、链接、链接里的加粗、列表小标题和代码文件名没有串色 | `references/theme-system.md`「令牌与色板(定颜色)」+ `assets/theme/palettes/flexoki/tokens.css` + `assets/theme/palettes/flexoki/highlight-*.theme` + `assets/theme/palettes/solarized/syntax.css` |
|
|
15
15
|
| 5 定组件 | 只打开要改的那一个 `assets/theme/css/*.css`;导航字重、答案折叠、行内代码、复制按钮、打印换行属于这一步,不在定颜色里顺手改 | `references/theme-system.md`「组件约定(定组件)」对应条目 |
|
|
16
|
-
| 6 通过检查 | `scripts/checking-layout.py`,然后整本 `render
|
|
16
|
+
| 6 通过检查 | `scripts/checking-layout.py`,然后整本 `render`;需要无障碍交叉验证时在 `format.html` 打开 `axe: true`(Quarto 内置 axe-core);只检查,不在这一步发明新的字号或颜色 | `scripts/checking-layout.py`;检查项回到本步骤表与 `theme-system.md`「组件约定」 |
|
|
17
17
|
|
|
18
18
|
## 增量链(改规则)
|
|
19
19
|
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
- **任务列表**:`input[type=checkbox]` 用 `accent-color: var(--action-color)`,不用系统蓝。
|
|
87
87
|
- **图片**:正文图与坐标图圆角一律 `--radius-sm`(3px),不用 6px 大圆角;图注选择器同时覆盖 `figure figcaption`、`.quarto-float-caption`、`.figure-caption`(`lang: zh` 后 Quarto 生成「图 3.1: …」,`lang` 必须写在 `_quarto.yml`,否则图/表题回落英文 Figure/Table)。
|
|
88
88
|
- **图片灯箱**:`format.html.lightbox` 打开后 GLightbox 默认 0.7 黑遮罩,页面图注会从图后透出;主题把遮罩加深到 0.86、关闭/翻页按钮做成 32% 黑的圆钮白描边。透明底 SVG 必须垫白卡(`img[src$=".svg"]`),否则深色遮罩上会整幅消失;位图不加白边。
|
|
89
|
-
- **顶栏品牌标**:`palettes/<name>/brand-mark.svg` 作 `mask`、以 `--navbar-fg` 上色;外形是同一枚「打开的书」,16px 仍可辨。标签页用同目录 `favicon-light.svg` / `favicon-dark.svg`,`book.favicon` 指 Flexoki light,其余五份随 `resources` 发布,`palette.html` 按色板与明暗换 href。图标高 1.25rem、与标题间距 0.5rem,`margin-bottom: 0.2rem`
|
|
89
|
+
- **顶栏品牌标**:`palettes/<name>/brand-mark.svg` 作 `mask`、以 `--navbar-fg` 上色;外形是同一枚「打开的书」,16px 仍可辨。标签页用同目录 `favicon-light.svg` / `favicon-dark.svg`,`book.favicon` 指 Flexoki light,其余五份随 `resources` 发布,`palette.html` 按色板与明暗换 href。图标高 1.25rem、与标题间距 0.5rem,`margin-bottom: 0.2rem` 对齐汉字基线;窄屏先压搜索再省标题:`≤420px` 只留书标,图标长到 1.375rem 并撤掉那条抬升,书名改成视觉隐藏(`clip-path: inset(50%)`)而不是 `display:none`——这枚链接仍是「回到首页」,读屏必须能读出书名。次级导航的面包屑在首页与部首页只有一项(书名或部名),紧挨品牌重复一遍,用 `:has(> li:only-child)` 整条收起,只留侧栏开关;两项以上的章节轨迹才显示。
|
|
90
90
|
- **顶栏搜索**:Quarto 的 Algolia detached button 自带 `svg.aa-SubmitIcon`,主题不再给 `#quarto-search` 画伪元素放大镜;按钮 32×32、图标 18px、用 `--navbar-muted` 上色,hover 用 `--navbar-hover-bg`。浮层容器、表单和输入框都用纸面/正文令牌,输入框需用与 Quarto 同等长的选择器(`.aa-DetachedContainer .aa-DetachedFormContainer .aa-Form .aa-InputWrapper .aa-Input`)压过编译期暗色值;占位文字用 `--text-secondary`,取消按钮 `white-space: nowrap`,结果选中项用 `--surface-raised`。`≤575.98px` 时浮层收成贴顶通栏,左右无圆角,避免 Quarto 的桌面 `top:100px` 把搜索条压在正文中部。
|
|
91
91
|
- **色板切换**:`.palette-picker` 是 select-only combobox——按钮 `role="combobox"` + `aria-haspopup="listbox"`,面板 `.palette-menu-list` 为 `role="listbox"`,选项 `role="option"`;焦点留在按钮上,用 `aria-activedescendant` 指当前项。闭合态是 2rem 高胶囊:色点 + 英文名(Flexoki / Solarized / Hill Light)+ 方向箭头;展开后三行「色点 + 名称」,当前项右侧打勾。键位照 APG:↑/↓/Home/End 移动、首字母跳转、Enter 或 Space 提交、Esc 无提交关闭、Tab 关闭并走开。名称用英文,与 `book-palette` 的取值和色板目录同名,不另起中文译名。选择仍写 `book-palette`,首屏恢复逻辑不变,样式在 `css/nav.css`。不用原生 `<select>`(暗色下会保持系统浅色下拉)。
|
|
92
92
|
- **明暗圆钮**:Quarto 自带的是 16px「开关」位图(`bi-toggle-off/on` 的 data-URI `background-image`,填充色写死在 SVG 里),紧挨色板胶囊时两枚圆角疙瘩容易看混。主题把 `::before` 改成只做 `mask`、颜色吃 `currentColor` 的对比圆盘(描边 1.2、半径 6.2,实心半圆让到描边内缘),`.alternate`(夜间)旋转 180°,实心半圆换边;可点区域 44px、图标 16px。
|
|
@@ -518,6 +518,37 @@ def browser_layout(book_dir, verbose):
|
|
|
518
518
|
f"{label}: 次级导航底边 "
|
|
519
519
|
f"{header_rule['secondaryBorderBottomWidth']}px,会和顶栏底边叠成双线"
|
|
520
520
|
)
|
|
521
|
+
# 品牌区:窄屏(≤420)只留书标,链接不能因此变成无名链接——
|
|
522
|
+
# 书名必须留在可访问树里(视觉隐藏即可),图标补回一点尺寸。
|
|
523
|
+
brand = data.get("navBrand") or {}
|
|
524
|
+
if brand:
|
|
525
|
+
if not brand.get("label"):
|
|
526
|
+
problems.append(
|
|
527
|
+
f"{label}: 顶栏品牌链接没有可读名称,屏幕阅读器读不出书名"
|
|
528
|
+
)
|
|
529
|
+
if brand.get("titleDisplay") == "none":
|
|
530
|
+
problems.append(
|
|
531
|
+
f"{label}: 书名被 display:none 摘出可访问树,窄屏书标成了无名链接"
|
|
532
|
+
)
|
|
533
|
+
expected_icon = 22 if width <= 420 else 20
|
|
534
|
+
if abs(brand.get("iconWidth", 0) - expected_icon) > 0.6:
|
|
535
|
+
problems.append(
|
|
536
|
+
f"{label}: 书标 {brand.get('iconWidth')}px,"
|
|
537
|
+
f"应为 {expected_icon}px"
|
|
538
|
+
)
|
|
539
|
+
# 次级导航只有一项面包屑(首页/部首页的书名)时应整条收起:
|
|
540
|
+
# 它紧挨顶栏品牌重复一遍,也说不出「书内位置」。
|
|
541
|
+
crumb = data.get("secondaryBreadcrumb") or {}
|
|
542
|
+
if (
|
|
543
|
+
crumb
|
|
544
|
+
and crumb.get("navDisplay") != "none"
|
|
545
|
+
and crumb.get("listDisplay") != "none"
|
|
546
|
+
and crumb.get("items", 0) < 2
|
|
547
|
+
):
|
|
548
|
+
problems.append(
|
|
549
|
+
f"{label}: 次级导航只有 {crumb.get('items', 0)} 项面包屑"
|
|
550
|
+
"(与顶栏书名重复),应整条隐藏"
|
|
551
|
+
)
|
|
521
552
|
h2_after = data.get("h2Rules") or []
|
|
522
553
|
if h2_after:
|
|
523
554
|
first = h2_after[0]
|
|
@@ -151,7 +151,9 @@ async function collect(page, pageId, scheme) {
|
|
|
151
151
|
}
|
|
152
152
|
for (const element of scope.querySelectorAll("*")) {
|
|
153
153
|
if (
|
|
154
|
-
|
|
154
|
+
// `.visually-hidden` 是 Bootstrap 的屏幕阅读器工具类(clip-path 藏在 1px 盒子里),
|
|
155
|
+
// 与下面 `svg` 同理:它本来就要超出自身 1px 盒宽,不能当版式溢出上报。
|
|
156
|
+
element.closest(".screen-reader-only, .sr-only, .visually-hidden, [aria-hidden='true']")
|
|
155
157
|
// 内联 SVG(棋盘快照等)没有 CSS 盒模型,<text> 的 clientWidth 恒为 0、
|
|
156
158
|
// scrollWidth 按字形量出,天然「溢出」,不能当版式问题。
|
|
157
159
|
|| element.closest("svg")
|
|
@@ -232,6 +234,35 @@ async function collect(page, pageId, scheme) {
|
|
|
232
234
|
};
|
|
233
235
|
})
|
|
234
236
|
: null;
|
|
237
|
+
// 品牌区:窄屏收起书名只留书标时,链接仍要有可读名称(屏幕阅读器读不到书名
|
|
238
|
+
// 就等于失去「回到首页」的语义),所以书名只能视觉隐藏、不能 display:none。
|
|
239
|
+
// 书标随书名一起收掉后需要补一点视觉重量,图标尺寸由断点决定。
|
|
240
|
+
const brand = document.querySelector(".navbar-brand");
|
|
241
|
+
const brandTitle = brand ? brand.querySelector(".navbar-title") : null;
|
|
242
|
+
const navBrand = brand
|
|
243
|
+
? {
|
|
244
|
+
label: (
|
|
245
|
+
brand.getAttribute("aria-label")
|
|
246
|
+
|| (brandTitle ? brandTitle.textContent : "")
|
|
247
|
+
|| ""
|
|
248
|
+
).trim(),
|
|
249
|
+
titleDisplay: brandTitle ? style(brandTitle).display : null,
|
|
250
|
+
width: Math.round(brand.getBoundingClientRect().width),
|
|
251
|
+
iconWidth: px(getComputedStyle(brand, "::before").width),
|
|
252
|
+
}
|
|
253
|
+
: null;
|
|
254
|
+
// 次级导航的面包屑在首页与部首页只有一项(书名或部名),紧挨顶栏品牌重复一遍,
|
|
255
|
+
// 也说不出「书内位置」,应整条收起;两项及以上(章节页)才显示。
|
|
256
|
+
const crumbList = document.querySelector(
|
|
257
|
+
"nav.quarto-secondary-nav .quarto-page-breadcrumbs ol.breadcrumb"
|
|
258
|
+
);
|
|
259
|
+
const secondaryBreadcrumb = crumbList
|
|
260
|
+
? {
|
|
261
|
+
items: crumbList.querySelectorAll(":scope > li").length,
|
|
262
|
+
listDisplay: style(crumbList).display,
|
|
263
|
+
navDisplay: style(crumbList.closest(".quarto-page-breadcrumbs")).display,
|
|
264
|
+
}
|
|
265
|
+
: null;
|
|
235
266
|
const rootStyle = style(root);
|
|
236
267
|
return {
|
|
237
268
|
pageId,
|
|
@@ -259,6 +290,8 @@ async function collect(page, pageId, scheme) {
|
|
|
259
290
|
copyButtonOpacities,
|
|
260
291
|
sidebarOverlay,
|
|
261
292
|
sidebarSections,
|
|
293
|
+
navBrand,
|
|
294
|
+
secondaryBreadcrumb,
|
|
262
295
|
headerRule,
|
|
263
296
|
h2Rules,
|
|
264
297
|
contentWidth: px(rootStyle.width),
|
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# Skill: writing-quarto
|
|
9
9
|
|
|
10
|
-
承载 `.qmd`
|
|
10
|
+
承载 `.qmd` 写法与 Book 结构约定。正文九步:增量任务只开对应一步;全章重构才走九步。判不准才开步骤点名的 `examples/` 案例,排图与查渲染开 `quarto/`;研究目录 `docs/research/` 只供开发。
|
|
11
11
|
|
|
12
12
|
## 适用场景
|
|
13
13
|
|
|
@@ -33,19 +33,17 @@ metadata:
|
|
|
33
33
|
|
|
34
34
|
## 两种走法
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
增量任务只走对应步骤,只改判为不合格的地方;全章重构仍走九步,但第 1–2 步先定删留、不动句子,第 3–4 步只拆块和段,第 5–7 步才逐句改。
|
|
37
37
|
|
|
38
38
|
两条走法共守同一组不变量,写在这里,不重复进各步:不新增原文没有的事实、数字、命令、性能结论、引文和出处;不把「可能」「通常」「在本书里」这类留有余地的说法改成确定说法;保留否定、条件、范围、时间和归属;围栏里的代码、命令、路径、URL 与 YAML 不改。增量润色只删除和换词,不扩写、不补论证;全章重构可删可换序,但不能发明事实,也不能把限定说成确定。
|
|
39
39
|
|
|
40
|
-
后一步发现前一步没做完,退回那一步改一次;同一文件第二次仍失败就停在检查输出上,不重走整段。
|
|
41
|
-
|
|
42
40
|
## 九步
|
|
43
41
|
|
|
44
42
|
九步按四个写作域分:写作 1–2,布局 3–4 与 7,润色 5–6 与 8,排版归主题技能;第 9 步是检查与渲染。跨域改动按「写作 → 布局 → 润色」的顺序推进,前一步落定再进下一步。
|
|
45
43
|
|
|
46
44
|
1. 定任务:写出读者做完能完成的那件事,再列这一章要覆盖的任务。规则在 `steps/01-task.md`。
|
|
47
45
|
2. 定结构:排页面骨架、二级标题和篇幅预算,只定删留、标题与章题对齐。规则在 `steps/02-structure.md`。
|
|
48
|
-
3. 排列内容块:导语、围栏、收束与 Callout 的次序,一个围栏只放正在讲的那一块。规则在 `steps/03-blocks.md`,表格写法见 `quarto/tables.md
|
|
46
|
+
3. 排列内容块:导语、围栏、收束与 Callout 的次序,一个围栏只放正在讲的那一块。规则在 `steps/03-blocks.md`,表格写法见 `quarto/tables.md`;围栏给声明还是给定义,按 `references/fence-semantics.md` 判。
|
|
49
47
|
图注写在图下、图前只写引出关系;三类图的统一写法见 `quarto/figures.md`。
|
|
50
48
|
4. 定段落:一段一个关键,下一句接住上一句的新信息;段间衔接落在话题切换处。规则在 `steps/04-paragraphs.md`。
|
|
51
49
|
5. 定句子:主语选执行者,对象先行,一句一动作。规则在 `steps/05-sentences.md`。
|
|
@@ -54,6 +52,10 @@ metadata:
|
|
|
54
52
|
8. 回读并退回:按现象判断属于哪一步,先按 `references/priority.md` 判冲突,再退回那一步改。规则在 `steps/08-reread.md`。
|
|
55
53
|
9. 检查与渲染:先核对内容与链接,再调 `quarto_render_check` 渲染并检查 HTML。规则在 `steps/09-check.md`。
|
|
56
54
|
|
|
55
|
+
## 规则卡
|
|
56
|
+
|
|
57
|
+
改句子、改块、改标题时,按需打开 `references/rules-cards.md` 的一张卡(C01–C30)查正例与反例,不整份读。卡是结论的紧凑副本,步骤文件仍是权威;两者冲突时以步骤为准。
|
|
58
|
+
|
|
57
59
|
## 完成判据
|
|
58
60
|
|
|
59
61
|
- [ ] 本轮点名的文件走完它对应的步骤,改动落在正文里;不要求九步全走。
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
## 混排自查
|
|
45
45
|
|
|
46
|
-
`python <skill>/scripts/checking-prose.py --root <book>`
|
|
46
|
+
`python <skill>/scripts/checking-prose.py --root <book>` 检查中文与拉丁字母、数字、行内代码之间的空格,以及中文句里的半角标点。脚本只报高置信项,不替代人工回读。环境里只有 `python3` 时,把命令里的 `python` 换成 `python3`。
|
|
47
47
|
|
|
48
48
|
## 依据
|
|
49
49
|
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# 围栏与正文的配对
|
|
2
|
+
|
|
3
|
+
本文件只回答一个问题:代码围栏里给读者看的是声明还是定义,正文就只讲哪种话。
|
|
4
|
+
|
|
5
|
+
## 两种素材,两种话
|
|
6
|
+
|
|
7
|
+
| 素材 | 围栏里是什么 | 正文可以讲什么 |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| 声明围栏 | `.h` 里的签名、成员、类定义 | 契约:参数、返回值、异常、边界、副作用、与其它接口的分工 |
|
|
10
|
+
| 定义围栏 | `.cpp`(或头文件里的函数体)里的实现 | 机制:用了哪个函数、什么表达式、是否复制、扫描顺序、为什么这样写 |
|
|
11
|
+
|
|
12
|
+
判据只有一句:**删掉定义文件,这句话还成立吗?** 成立是契约,不成立是实现。
|
|
13
|
+
|
|
14
|
+
## 三种合法配对
|
|
15
|
+
|
|
16
|
+
| 配对 | 围栏 | 正文 | 用在 |
|
|
17
|
+
| --- | --- | --- | --- |
|
|
18
|
+
| 契约型 | 声明 | 契约文字 | 定义平凡:单行 `return`、一跳转发、两行赋值 |
|
|
19
|
+
| 机制型 | 声明 + 定义 | 实现文字 | 定义非平凡,需要讲分支、算法或取舍 |
|
|
20
|
+
| 纯定义型 | 定义 | 实现文字 | 定义就在头文件(`constexpr`、模板),或声明已在本章前文出现过 |
|
|
21
|
+
|
|
22
|
+
补充一条**组合转发例外**:同一小节里几个一跳转发的定义可以合并成一个短围栏,正文讲它们如何组合;合并后仍遵守「正文只讲围栏里看得到的代码」。
|
|
23
|
+
|
|
24
|
+
## 不允许的写法
|
|
25
|
+
|
|
26
|
+
1. 声明围栏 + 实现文字(本节讲机制,读者手上却只有接口)。
|
|
27
|
+
2. 声明围栏 + 「完整实现见源文件」这类指路句:读者在书里看不到,等于没给。
|
|
28
|
+
3. 正文出现围栏里不存在的函数名或表达式(正文讲 `GetWinner`,围栏只有 `IsWin` 的声明)。
|
|
29
|
+
4. 声明与定义之间隔了整段机制解释,读者翻到围栏时讲解已经结束。
|
|
30
|
+
5. 定义不平凡却只写契约,再在「常见错误」或 Callout 里补一段机制。
|
|
31
|
+
|
|
32
|
+
## 平凡定义的判定
|
|
33
|
+
|
|
34
|
+
- 平凡:单次比较、单个 `return`、两行赋值、纯委托(`return Other();`)、只做一次边界检查后返回。
|
|
35
|
+
- 不平凡:含循环或分支组合、更新状态、与别的类型协作、需要解释「为什么不用另一种写法」。
|
|
36
|
+
|
|
37
|
+
平凡判例:`IsInRange` 的 `return row >= 0 && row < kRows && col >= 0 && col < kCols;`、`IsWin` 的 `return GetWinner().has_value();`。
|
|
38
|
+
|
|
39
|
+
## 正反例(棋盘章的真实片段)
|
|
40
|
+
|
|
41
|
+
改前——声明围栏,正文讲定义:
|
|
42
|
+
|
|
43
|
+
```markdown
|
|
44
|
+
```{.cpp filename="board.h"}
|
|
45
|
+
/// @return 是否有任意一枚棋子连成一条线。
|
|
46
|
+
[[nodiscard]] bool IsWin() const;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`IsWin` 复用 `GetWinner` 的三连判定,不再单独扫描棋盘。
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
改后——组合围栏,正文对着代码讲组合:
|
|
53
|
+
|
|
54
|
+
```markdown
|
|
55
|
+
```{.cpp filename="board.cpp"}
|
|
56
|
+
bool Board::IsGameOver() const { return IsWin() || IsDraw(); }
|
|
57
|
+
bool Board::IsWin() const { return GetWinner().has_value(); }
|
|
58
|
+
bool Board::IsDraw() const { return IsFull() && !IsWin(); }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`IsGameOver` 把两个判断用 `||` 连起来;`IsWin` 与 `IsDraw` 各自只回答一个问题,真正的扫描都在别处。
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 与其它规则的关系
|
|
65
|
+
|
|
66
|
+
- **代码块门槛**(`steps/03-blocks.md`):平凡实现不出围栏;本文件给的是「已经决定要讲」时该怎么配围栏。
|
|
67
|
+
- **正文与注释分工**:契约与边界写进 `///`,正文不转述;实现文字只补围栏里没有的上下文(为什么这样组合、和谁配合)。
|
|
68
|
+
- **检查器**:`scripts/verifying-content.py` 按 `###` 小节扫描,只有 `.h` 围栏而正文出现实现词时报 `decl-without-def`;「常见错误」「自测问题」「本章回顾」与表格行豁免。
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# 规则卡
|
|
2
|
+
|
|
3
|
+
研究结论的压缩版。每张卡一行规则、一个正例、一个反例和所属步骤;写正文时按需打开一张,不要整份读完。
|
|
4
|
+
来源编号见 `docs/research/synthesis.md` 的表格;卡号写进步骤文件末尾,便于追溯。
|
|
5
|
+
|
|
6
|
+
## 句子
|
|
7
|
+
|
|
8
|
+
**C01 段首给结论。** 第一句写这段要读者记住的事,背景放后面。
|
|
9
|
+
- 正:`棋盘用固定大小的二维数组保存,行和列都是 0 到 2。`
|
|
10
|
+
- 反:`在开始介绍棋盘之前,我们先来了解一下背景。`
|
|
11
|
+
- 步骤:`steps/04-paragraphs.md`
|
|
12
|
+
|
|
13
|
+
**C02 一段只推进一个动作。** 说明、举证、划界、收口四选一;出现第二个动作就分段。
|
|
14
|
+
- 正:一段说清 `PlaceCell` 的职责,下一段再说它拒绝哪些输入。
|
|
15
|
+
- 反:一段里既讲职责、又讲实现细节、又讲历史原因。
|
|
16
|
+
- 步骤:`steps/04-paragraphs.md`
|
|
17
|
+
|
|
18
|
+
**C03 句子有预算。** 中文单句不超过 40 字,英文不超过 25 词;超了先拆。
|
|
19
|
+
- 正:`落子前先检查坐标是否越界。越界时返回 false,不修改棋盘。`
|
|
20
|
+
- 反:`落子前先检查坐标是否越界并在越界时返回 false 且不修改棋盘以及不记录日志。`
|
|
21
|
+
- 步骤:`steps/05-sentences.md`
|
|
22
|
+
|
|
23
|
+
**C04 术语首次出现给中英。** 中文在前,英文括注一次,之后只用中文。
|
|
24
|
+
- 正:`棋盘格子(cell)只有三种状态。`
|
|
25
|
+
- 反:`cell 有三种状态,后面我们有时也叫它格子。`
|
|
26
|
+
- 步骤:`steps/06-wording.md`
|
|
27
|
+
|
|
28
|
+
**C05 删复述句。** 代码已经说明的,不要再写一遍。
|
|
29
|
+
- 正:`PlaceCell 返回 false 有两种原因:越界,或格子已被占用。`
|
|
30
|
+
- 反:`上面的代码中,if 判断 position.row 是否越界,如果越界就 return false。`
|
|
31
|
+
- 步骤:`steps/08-reread.md`
|
|
32
|
+
|
|
33
|
+
**C06 删元叙述。** 不写「本节将」「下面我们来看」「值得注意的是」。
|
|
34
|
+
- 正:`判断胜负要扫描四条线。`
|
|
35
|
+
- 反:`下面我们来看一下如何判断胜负。`
|
|
36
|
+
- 步骤:`steps/08-reread.md`
|
|
37
|
+
|
|
38
|
+
**C07 示例先于术语。** 先给能跑的最小例子,再解释机制。
|
|
39
|
+
- 正:先给 `Board b; b.PlaceCell({0,0}, CellType::kX);` 与输出,再讲存储布局。
|
|
40
|
+
- 反:先讲「采用行优先的连续存储」,再给例子。
|
|
41
|
+
- 步骤:`steps/03-blocks.md`
|
|
42
|
+
|
|
43
|
+
**C08 反例紧随正例。** 错误写法放在正例之后一段,不埋进注里。
|
|
44
|
+
- 正:正例后一段写「如果漏掉越界检查,会……」,附代码。
|
|
45
|
+
- 反:把反例塞进 `::: {.callout-note}` 的折叠块。
|
|
46
|
+
- 步骤:`steps/04-paragraphs.md`
|
|
47
|
+
|
|
48
|
+
## 结构
|
|
49
|
+
|
|
50
|
+
**C09 标题用名词短语。** 不用问句,不用「关于……」。
|
|
51
|
+
- 正:`胜负判定`
|
|
52
|
+
- 反:`我们如何判断胜负?`
|
|
53
|
+
- 步骤:`steps/02-structure.md`
|
|
54
|
+
|
|
55
|
+
**C10 章节按五步走。** 现象 → 最小例子 → 机制 → 边界 → 自测。
|
|
56
|
+
- 正:`03-board.qmd` 依次讲空棋盘、落子、越界、判胜、自测。
|
|
57
|
+
- 反:先给完整类声明,再逐行解释。
|
|
58
|
+
- 步骤:`steps/02-structure.md`
|
|
59
|
+
|
|
60
|
+
**C11 表格只放同构信息。** 叙述内容仍写自然段。
|
|
61
|
+
- 正:三种格子类型一行一个,列固定。
|
|
62
|
+
- 反:把一段因果解释塞进表格的「说明」列。
|
|
63
|
+
- 步骤:`steps/03-blocks.md`
|
|
64
|
+
|
|
65
|
+
**C12 一节一个关键。** 一章只解决一个问题,多出来的挪到下一章。
|
|
66
|
+
- 正:03 章只讲棋盘;游戏流程留给 04 章。
|
|
67
|
+
- 反:03 章顺手把 `Game` 的状态机也讲了。
|
|
68
|
+
- 步骤:`steps/02-structure.md`
|
|
69
|
+
|
|
70
|
+
**C13 章首导语 60–146 字。** 一段说清这章解决什么、读者读完能做什么。
|
|
71
|
+
- 正:`这一章把棋盘写成类……读完后你能判断任意坐标是否合法。`
|
|
72
|
+
- 反:三段历史回顾加一张架构图。
|
|
73
|
+
- 步骤:`steps/01-task.md`
|
|
74
|
+
|
|
75
|
+
## 代码块与图
|
|
76
|
+
|
|
77
|
+
**C14 代码块前一句导语,后一段解说。** 导语说这块代码要证明什么,解说只讲没写在代码里的。
|
|
78
|
+
- 正:`下面这段是判胜的核心循环:` + 代码 + `两层 any_of 在第一次命中时短路。`
|
|
79
|
+
- 反:导语写「代码如下」,解说把代码逐行翻译。
|
|
80
|
+
- 步骤:`steps/03-blocks.md`
|
|
81
|
+
|
|
82
|
+
**C15 围栏属性只写必要项。** 需要文件名才写 `filename`,需要行号才写 `start-from`。
|
|
83
|
+
- 正:` ```{.cpp filename="board.h"} `
|
|
84
|
+
- 反:每个代码块都堆 `filename`、`code-link`、`start-from`。
|
|
85
|
+
- 步骤:`steps/03-blocks.md`
|
|
86
|
+
|
|
87
|
+
**C16 图示有替代文本。** 图必须有 `alt` 或 `aria-label`,说明图在讲什么,不写「示意图」。
|
|
88
|
+
- 正:`aria-label="三行三列的棋盘,行列都从 0 编号"`
|
|
89
|
+
- 反:`alt="图 1"`
|
|
90
|
+
- 步骤:`steps/07-layout.md`
|
|
91
|
+
|
|
92
|
+
**C17 交叉引用用锚点。** 引用其他章节写 `@fig-`、`@tbl-` 或 `[](){#...}`,不写「见上文」。
|
|
93
|
+
- 正:`判胜规则见 @fig-board-lines。`
|
|
94
|
+
- 反:`判胜规则见上面的图。`
|
|
95
|
+
- 步骤:`steps/07-layout.md`
|
|
96
|
+
|
|
97
|
+
## 用词与排版
|
|
98
|
+
|
|
99
|
+
**C18 称呼读者用「你」。** 不写「我们」「大家」「读者」。
|
|
100
|
+
- 正:`你可以先用空棋盘试一次落子。`
|
|
101
|
+
- 反:`我们接下来让大家试着落子。`
|
|
102
|
+
- 步骤:`steps/06-wording.md`
|
|
103
|
+
|
|
104
|
+
**C19 中英混排加空格。** 中文与英文、数字之间留一个空格;全角标点只用于中文语境。
|
|
105
|
+
- 正:`C++23 的 std::array 固定长度。`
|
|
106
|
+
- 反:`C++23的std::array固定长度。`
|
|
107
|
+
- 步骤:`steps/07-layout.md`
|
|
108
|
+
|
|
109
|
+
**C20 数字与单位写全。** 行列编号写「第 0 行」,范围写「0 到 2」,不写「0~2」。
|
|
110
|
+
- 正:`行和列的取值都是 0 到 2。`
|
|
111
|
+
- 反:`行列取值 0~2。`
|
|
112
|
+
- 步骤:`steps/07-layout.md`
|
|
113
|
+
|
|
114
|
+
**C21 链接文字要自述。** 链接文字说明目标是什么,不写「这里」「点击」。
|
|
115
|
+
- 正:`参见 [Google C++ 风格指南](https://google.github.io/styleguide/cppguide.html)。`
|
|
116
|
+
- 反:`参见[这里](https://google.github.io/styleguide/cppguide.html)。`
|
|
117
|
+
- 步骤:`steps/09-check.md`
|
|
118
|
+
|
|
119
|
+
**C22 列表只列同构项。** 列表项长度相近;出现完整句子就改自然段。
|
|
120
|
+
- 正:三条各一行的「越界、占用、终局」检查项。
|
|
121
|
+
- 反:列表第一项一句话,第二项三行解释。
|
|
122
|
+
- 步骤:`steps/07-layout.md`
|
|
123
|
+
|
|
124
|
+
## 节奏与收尾
|
|
125
|
+
|
|
126
|
+
**C23 长文分小节。** 一节 60–90 行,超过就拆;拆出来的小节标题写名词短语。
|
|
127
|
+
- 正:`棋盘存储`、`落子`、`判胜` 三个小节。
|
|
128
|
+
- 反:一个 300 行的「实现」小节。
|
|
129
|
+
- 步骤:`steps/02-structure.md`
|
|
130
|
+
|
|
131
|
+
**C24 排障写成可跟做步骤。** 失败现象、检查项、修法各一行,命令可直接复制。
|
|
132
|
+
- 正:`报错 EADDRINUSE:先运行 lsof -i :3000,再结束占用进程。`
|
|
133
|
+
- 反:`如果端口被占用,可能需要检查一下相关进程。`
|
|
134
|
+
- 步骤:`steps/09-check.md`
|
|
135
|
+
|
|
136
|
+
**C25 每章末尾给三句回顾。** 只回顾结论,不引入新概念。
|
|
137
|
+
- 正:`棋盘用二维数组保存;落子前检查越界与占用;判胜扫描四条线。`
|
|
138
|
+
- 反:回顾里补一个前面没讲过的 `std::optional` 用法。
|
|
139
|
+
- 步骤:`steps/08-reread.md`
|
|
140
|
+
|
|
141
|
+
**C26 自测问题不引入新概念。** 问题只用本章讲过的内容,答案放折叠块。
|
|
142
|
+
- 正:`如果省略越界检查,传入 {-1, 0} 会发生什么?`
|
|
143
|
+
- 反:`请用 std::variant 重写棋盘。`
|
|
144
|
+
- 步骤:`steps/08-reread.md`
|
|
145
|
+
|
|
146
|
+
**C27 术语表只收本书自造词。** 通用 C++ 术语不收;收「棋盘格子」「落子」这类本书固定叫法。
|
|
147
|
+
- 正:`棋盘格子:棋盘上的一个位置,取值见 CellType。`
|
|
148
|
+
- 反:把 `std::array` 也收进术语表。
|
|
149
|
+
- 步骤:`steps/06-wording.md`
|
|
150
|
+
|
|
151
|
+
**C28 代码注释不复述正文。** 正文讲契约,注释讲代码里看不出的取舍;同一句不写两遍。
|
|
152
|
+
- 正:注释写「不用编号求和,和相等不代表同色」,正文讲判胜算法。
|
|
153
|
+
- 反:注释把正文的判胜步骤抄一遍。
|
|
154
|
+
- 步骤:`steps/03-blocks.md`
|
|
155
|
+
|
|
156
|
+
**C29 渲染后朗读一遍。** 出声读,卡住的地方就是句子太长或指代不清。
|
|
157
|
+
- 正:读不顺的句子拆成两句。
|
|
158
|
+
- 反:只看渲染截图就交付。
|
|
159
|
+
- 步骤:`steps/09-check.md`
|
|
160
|
+
|
|
161
|
+
**C30 改完就校验。** 每改完一节跑一次 `verifying-content.py`,不攒到最后。
|
|
162
|
+
- 正:`python scripts/verifying-content.py --root . --strict`
|
|
163
|
+
- 反:整章改完再跑,报错定位到整章。
|
|
164
|
+
- 步骤:`steps/09-check.md`
|
|
@@ -30,6 +30,10 @@ EXCLUDE_DIRS = {
|
|
|
30
30
|
"dist", "_freeze", "temp", "__pycache__", ".venv", "venv",
|
|
31
31
|
}
|
|
32
32
|
EXCLUDE_DIR_SUFFIXES = ("_files",)
|
|
33
|
+
# 构建目录的变体:build-debug / build-release / _build 都是输出目录,
|
|
34
|
+
# 里面是 CMake 自己生成的文件名(CompilerIdCXX.cpp、cmake_install.cmake),
|
|
35
|
+
# 不该参与命名检查,否则每次构建后都报一堆假阳性。
|
|
36
|
+
EXCLUDE_DIR_PREFIXES = ("build-", "_build")
|
|
33
37
|
|
|
34
38
|
TOOL_FIXED_NAMES = {
|
|
35
39
|
"CMakeLists.txt", "_quarto.yml", "_quarto.yaml", "SKILL.md", "AGENTS.md",
|
|
@@ -49,6 +53,13 @@ def first_non_ascii(name: str) -> str | None:
|
|
|
49
53
|
def name_form_error(name: str) -> str | None:
|
|
50
54
|
if name in TOOL_FIXED_NAMES:
|
|
51
55
|
return None
|
|
56
|
+
# CMake 模块名按社区习惯写 CamelCase(CompileSettings.cmake),由
|
|
57
|
+
# include() / find_package() 按名查找,改名会直接断掉构建,故与
|
|
58
|
+
# CMakeLists.txt 同类放行;只看大小写规则,不放过下划线或空格。
|
|
59
|
+
if name.endswith(".cmake"):
|
|
60
|
+
if "_" in name or " " in name:
|
|
61
|
+
return "含下划线" if "_" in name else "含空格"
|
|
62
|
+
return None
|
|
52
63
|
if any(a.islower() and b.isupper() for a, b in zip(name, name[1:])):
|
|
53
64
|
return "含驼峰"
|
|
54
65
|
if "_" in name:
|
|
@@ -86,6 +97,7 @@ def main() -> int:
|
|
|
86
97
|
|
|
87
98
|
print(f"扫描:{repo_root}")
|
|
88
99
|
print(f"排除目录:{', '.join(sorted(EXCLUDE_DIRS))}")
|
|
100
|
+
print(f"排除目录前缀:{', '.join(EXCLUDE_DIR_PREFIXES)}")
|
|
89
101
|
print()
|
|
90
102
|
|
|
91
103
|
bad: list[tuple[str, str]] = []
|
|
@@ -93,7 +105,9 @@ def main() -> int:
|
|
|
93
105
|
for dirpath, dirnames, filenames in os.walk(repo_root):
|
|
94
106
|
dirnames[:] = [
|
|
95
107
|
d for d in dirnames
|
|
96
|
-
if d not in EXCLUDE_DIRS
|
|
108
|
+
if d not in EXCLUDE_DIRS
|
|
109
|
+
and not d.endswith(EXCLUDE_DIR_SUFFIXES)
|
|
110
|
+
and not d.startswith(EXCLUDE_DIR_PREFIXES)
|
|
97
111
|
]
|
|
98
112
|
for name in dirnames + filenames:
|
|
99
113
|
count += 1
|
|
@@ -56,6 +56,13 @@ CARRIER_RE = re.compile("承载")
|
|
|
56
56
|
NEXT_SECTION_RE = re.compile(r"(?:下一节|上一节)")
|
|
57
57
|
CALLOUT_OPEN_RE = re.compile(r"^\s*:::\s*\{?\s*\.callout-")
|
|
58
58
|
OPENING_CLICHE_RE = re.compile(r"^(?:本章|本节|这一章|这一节)(?:将|会|要)|^下面(?:介绍|讲)")
|
|
59
|
+
# 元叙述与清点句:读者不需要作者交代「接下来讲什么」;一句里排到两个序列词,
|
|
60
|
+
# 说明这句话在报清单而不是在讲因果。规则卡见 references/rules-cards.md 的 C06、C02。
|
|
61
|
+
META_NARRATION_RE = re.compile(
|
|
62
|
+
r"本节将|本章将|这一节将|接下来(?:我们|将|会|介绍|讲)|下面(?:我们)?(?:先|来|看)|"
|
|
63
|
+
r"值得我们|值得注意的是|需要说明的是|如上所述|如前所述"
|
|
64
|
+
)
|
|
65
|
+
ENUMERATION_RE = re.compile(r"首先|其次|再次|然后|最后|其一|其二|其三|第[一二三四五]是|第[一二三四五],")
|
|
59
66
|
LADDER_RE = re.compile(r"(?:分别交给|各说一层)")
|
|
60
67
|
SKILL_TONE_RE = re.compile(r"这里不定义|测试也不必|不必为[^。;\n]{0,20}再记|不写双层循环")
|
|
61
68
|
INLINE_NAME_RE = re.compile(r"`([A-Za-z_][A-Za-z0-9_]*)`")
|
|
@@ -68,6 +75,14 @@ FUNCTION_NAME_RE = re.compile(
|
|
|
68
75
|
TERMINAL_RE = re.compile(r"终端|打印|显示|输入|读入|命令行")
|
|
69
76
|
TEST_RE = re.compile(r"测试|断言|用例|gtest|CTest")
|
|
70
77
|
|
|
78
|
+
# 围栏与正文的配对:声明围栏只配契约文字。正文出现实现词而小节里没有定义围栏时
|
|
79
|
+
# 报 decl-without-def,规则与正反例见 references/fence-semantics.md。
|
|
80
|
+
FENCE_FILENAME_RE = re.compile(r'filename="([^"]+)"')
|
|
81
|
+
IMPL_MARKER_RE = re.compile(r"复用|委托给|扫描|遍历|对每个|短路|表达式|状态位|缓存|不再单独|递归")
|
|
82
|
+
FENCE_SKIP_SECTION_RE = re.compile(r"常见错误|自测|回顾|练习|小结")
|
|
83
|
+
DECL_SUFFIXES = (".h", ".hpp", ".hh", ".hxx")
|
|
84
|
+
DEF_SUFFIXES = (".cpp", ".cc", ".cxx", ".inl")
|
|
85
|
+
|
|
71
86
|
TREE_CATEGORY_COMMENTS = frozenset({
|
|
72
87
|
"头文件", "源文件", "源码", "实现", "测试", "配置文件",
|
|
73
88
|
"构建脚本", "构建规则", "动态库", "可执行文件",
|
|
@@ -175,12 +190,24 @@ def parse_chapters(quarto_yml: Path) -> list[str]:
|
|
|
175
190
|
return chapters
|
|
176
191
|
|
|
177
192
|
|
|
193
|
+
EXCLUDED_DIRS = {
|
|
194
|
+
"node_modules", "_book", ".quarto", ".git", "_freeze", "_site",
|
|
195
|
+
"temp", "build", "dist", "_build",
|
|
196
|
+
}
|
|
197
|
+
EXCLUDED_DIR_PREFIXES = ("build-",)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def is_generated_dir(part: str) -> bool:
|
|
201
|
+
# 生成物与缓存目录整棵跳过:临时试验稿、构建产物里的 .qmd 不属于书稿源,
|
|
202
|
+
# 整仓扫描时报出它们的问题会让作者去改根本没有交付的文件。
|
|
203
|
+
return part in EXCLUDED_DIRS or part.startswith(".") or part.startswith(EXCLUDED_DIR_PREFIXES)
|
|
204
|
+
|
|
205
|
+
|
|
178
206
|
def collect_all_qmd(root: Path) -> list[Path]:
|
|
179
|
-
excluded = {"node_modules", "_book", ".quarto", ".git", "_freeze", "_site"}
|
|
180
207
|
found: list[Path] = []
|
|
181
208
|
for candidate in sorted(root.rglob("*.qmd")):
|
|
182
209
|
rel_parts = candidate.relative_to(root).parts
|
|
183
|
-
if any(part
|
|
210
|
+
if any(is_generated_dir(part) for part in rel_parts):
|
|
184
211
|
continue
|
|
185
212
|
found.append(candidate)
|
|
186
213
|
return found
|
|
@@ -226,6 +253,20 @@ def check_prose_block(
|
|
|
226
253
|
issues.append(Issue(path, start_line, "第 N 章式编号指称(用章节标题或链接)"))
|
|
227
254
|
if SKILL_TONE_RE.search(compact):
|
|
228
255
|
issues.append(Issue(path, start_line, "技能腔:只约束作者的选择,整句删掉"))
|
|
256
|
+
if META_NARRATION_RE.search(compact):
|
|
257
|
+
issues.append(Issue(
|
|
258
|
+
path, start_line,
|
|
259
|
+
"元叙述:删掉「本节将 / 下面我们看」这类句子,直接从内容写起",
|
|
260
|
+
))
|
|
261
|
+
if CJK_RE.search(compact):
|
|
262
|
+
for sentence in re.split(r"(?<=[。!?])", compact):
|
|
263
|
+
markers = ENUMERATION_RE.findall(sentence)
|
|
264
|
+
if len(markers) >= 2:
|
|
265
|
+
issues.append(Issue(
|
|
266
|
+
path, start_line,
|
|
267
|
+
f"清点句:一句里出现 {len(markers)} 个序列词,改成列表或自然衔接",
|
|
268
|
+
))
|
|
269
|
+
break
|
|
229
270
|
if not is_list and CJK_RE.search(compact):
|
|
230
271
|
names = {
|
|
231
272
|
match.group(1)
|
|
@@ -546,6 +587,72 @@ def check_lead(
|
|
|
546
587
|
))
|
|
547
588
|
|
|
548
589
|
|
|
590
|
+
def check_fence_semantics(path: Path, lines: list[str]) -> list[Issue]:
|
|
591
|
+
"""声明围栏只能配契约文字:小节内没有定义围栏却讲实现时报错。"""
|
|
592
|
+
issues: list[Issue] = []
|
|
593
|
+
section_title: str | None = None
|
|
594
|
+
section_line = 0
|
|
595
|
+
fences: list[str] = []
|
|
596
|
+
prose: list[str] = []
|
|
597
|
+
in_fence = False
|
|
598
|
+
in_section = False
|
|
599
|
+
|
|
600
|
+
def flush() -> None:
|
|
601
|
+
nonlocal fences, prose
|
|
602
|
+
if section_title is None:
|
|
603
|
+
return
|
|
604
|
+
if not FENCE_SKIP_SECTION_RE.search(section_title):
|
|
605
|
+
has_decl = any(name.endswith(DECL_SUFFIXES) for name in fences if name)
|
|
606
|
+
has_def = any(name.endswith(DEF_SUFFIXES) for name in fences if name)
|
|
607
|
+
if has_decl and not has_def and prose:
|
|
608
|
+
text = " ".join(prose)
|
|
609
|
+
marker = IMPL_MARKER_RE.search(text)
|
|
610
|
+
if marker:
|
|
611
|
+
issues.append(Issue(
|
|
612
|
+
path, section_line,
|
|
613
|
+
f"小节「{section_title}」只有声明围栏,正文却讲实现({marker.group(0)});"
|
|
614
|
+
"按 references/fence-semantics.md 改成契约文字,或补定义围栏",
|
|
615
|
+
))
|
|
616
|
+
fences = []
|
|
617
|
+
prose = []
|
|
618
|
+
|
|
619
|
+
section_title = None
|
|
620
|
+
fences = []
|
|
621
|
+
prose = []
|
|
622
|
+
in_fence = False
|
|
623
|
+
in_section = False
|
|
624
|
+
for number, raw in enumerate(lines, start=1):
|
|
625
|
+
if FENCE_RE.match(raw):
|
|
626
|
+
if not in_fence:
|
|
627
|
+
match = FENCE_FILENAME_RE.search(raw)
|
|
628
|
+
fences.append(match.group(1) if match else "")
|
|
629
|
+
in_fence = not in_fence
|
|
630
|
+
continue
|
|
631
|
+
if in_fence:
|
|
632
|
+
continue
|
|
633
|
+
heading = HEADING_RE.match(raw)
|
|
634
|
+
if heading:
|
|
635
|
+
level = len(heading.group(1))
|
|
636
|
+
if level in (2, 3):
|
|
637
|
+
flush()
|
|
638
|
+
section_title = heading.group(2)
|
|
639
|
+
section_line = number
|
|
640
|
+
in_section = True
|
|
641
|
+
elif level == 1:
|
|
642
|
+
flush()
|
|
643
|
+
section_title = None
|
|
644
|
+
in_section = False
|
|
645
|
+
continue
|
|
646
|
+
if not in_section or not raw.strip():
|
|
647
|
+
continue
|
|
648
|
+
stripped = raw.strip()
|
|
649
|
+
if stripped.startswith(("|", ">", ":::", "-", "*", "+")) or re.match(r"^\d+\.\s", stripped):
|
|
650
|
+
continue
|
|
651
|
+
prose.append(stripped)
|
|
652
|
+
flush()
|
|
653
|
+
return issues
|
|
654
|
+
|
|
655
|
+
|
|
549
656
|
def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> list[Issue]:
|
|
550
657
|
issues: list[Issue] = []
|
|
551
658
|
text = read_text(path)
|
|
@@ -672,6 +779,8 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
|
|
|
672
779
|
if in_fence:
|
|
673
780
|
issues.append(Issue(path, fence_start, "代码围栏未闭合"))
|
|
674
781
|
|
|
782
|
+
issues.extend(check_fence_semantics(path, lines))
|
|
783
|
+
|
|
675
784
|
for block in fence_blocks:
|
|
676
785
|
issues.extend(check_tree_block(path, block))
|
|
677
786
|
|
|
@@ -74,6 +74,10 @@ int main() {
|
|
|
74
74
|
- 仅为证明「源码有此函数」而贴出的实现。
|
|
75
75
|
- 打断小节中心的硬插块。
|
|
76
76
|
|
|
77
|
+
### 声明围栏与定义围栏
|
|
78
|
+
|
|
79
|
+
围栏里是声明,正文就只能讲契约;正文要讲实现(复用哪个函数、怎么组合、扫不扫描),围栏里必须给出对应的定义。三种合法配对、平凡定义的判定与正反例见 `references/fence-semantics.md`;`verifying-content.py` 对「只有 `.h` 围栏却讲实现」的小节报 `decl-without-def`。
|
|
80
|
+
|
|
77
81
|
### 类与多函数摘录的垂直空白
|
|
78
82
|
|
|
79
83
|
摘录 `class` 或多函数片段时**保留**源码中函数之间的空行,禁止为省行数坍缩成函数注释贴着下一声明;省略部分成员时,被保留的相邻函数之间仍空 1 行;同一 access 段内最后一组数据成员与函数之间空 1 行,数据成员组内不空行。与「逐字摘录」冲突时以源码空白为准:教学缩略可删成员,不可删「函数间距」惯例。
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
| 定段落 | 一段内两个独立中心 | 按「拆核四步」拆段或改列表 | `steps/04-paragraphs.md`「拆核四步(多核混段的拆段方法)」、`examples/04-paragraph-cases.md`「自然段多中心未拆」 |
|
|
22
22
|
| 定结构 | 章引言顿号串 ≥3 个交付物 | 下沉为列表或正文 `##` | `steps/02-structure.md`「六段块序列」、`examples/02-structure-shape.md`「目录式堆砌」 |
|
|
23
23
|
| 排列内容块 | 过简实现围栏(单次比较或直接 `return`) | 删围栏,改入口列表 + 失败策略列表 | `steps/03-blocks.md` |
|
|
24
|
+
| 排列内容块 | 声明围栏 + 实现文字(正文讲复用、委托、扫描,围栏只有 `.h`) | 按 `references/fence-semantics.md` 三选一:改契约文字、补组合或定义围栏、删实现句 | `steps/03-blocks.md`「声明围栏与定义围栏」、`references/fence-semantics.md` |
|
|
24
25
|
| 排列内容块 | 块间闭环缺失(标题下无引导、段→列表无 lead-in、代码→图只有空行、表前无关系句、Callout 复述块内) | 按块链检查清单补桥接 | `steps/03-blocks.md`「块链检查清单」、`steps/03-blocks.md` |
|
|
25
26
|
| 排列内容块 | 节末旁注裸句(`##` 最后一块是一句可跳过的补充,前面是列表、表格或围栏) | 改写为独立可读的 Callout 收尾,正文删同句;超过两句改小节 | `steps/03-blocks.md`「节末旁注与 Callout 收尾」、`examples/03-qmd-elements.md`「节末旁注改为 Callout 收尾」 |
|
|
26
27
|
| 定段落 | 段内同义堆叠(两句换说法讲同一机制) | 删辅句或合并;公式递进与简洁边界豁免 | `steps/04-paragraphs.md`「段内同义堆叠禁令」 |
|
|
@@ -116,3 +117,5 @@
|
|
|
116
117
|
## 正反例
|
|
117
118
|
|
|
118
119
|
通用情况判得准就不打开。判不准时打开 `examples/08-appendix.md`。
|
|
120
|
+
|
|
121
|
+
定稿前对照 `references/rules-cards.md` 的 C05、C06、C25、C26:删复述句与元叙述,补三句回顾,自测题只用本章内容。
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
### 渲染与 HTML
|
|
16
16
|
|
|
17
|
-
内容核对通过后,调 `quarto_render_check
|
|
17
|
+
内容核对通过后,调 `quarto_render_check`。工具先按会话工作目录找书根;会话不在书内时,从 `qmd_path` 所在目录向上找 `_quarto.yml`,两处都找不到才拒绝并返回「当前会话不在 Quarto Book 项目内」。被拒绝时把工作目录切到书根再调用;仍不行就停住并说明,不改用手跑 `quarto render` 顶替。参数只有两个:
|
|
18
18
|
|
|
19
19
|
- `qmd_path`:正在改的 `.qmd` 路径。
|
|
20
20
|
- `dry_run`:`true` 只做输出目录安全检查,`false` 才启动渲染。
|
|
@@ -76,3 +76,5 @@
|
|
|
76
76
|
## 正反例
|
|
77
77
|
|
|
78
78
|
第 9 步不设例子文件。检查结果本身就是判据。
|
|
79
|
+
|
|
80
|
+
自查时按 `references/rules-cards.md` 逐条对照,只打开当前段落涉及的卡片;卡片编号写在动手记录里,便于回溯。
|