@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
|
@@ -68,6 +68,36 @@ title: 入门
|
|
|
68
68
|
"""
|
|
69
69
|
|
|
70
70
|
|
|
71
|
+
HTML_FIGURE_BAD = """---
|
|
72
|
+
title: 入门
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 目标
|
|
76
|
+
|
|
77
|
+
这一章说明一个可以重复验证的目标。
|
|
78
|
+
|
|
79
|
+
```{=html}
|
|
80
|
+
<svg class="coords-grid" role="img" aria-label="棋盘坐标。"><rect width="10" height="10"/></svg>
|
|
81
|
+
```
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
HTML_FIGURE_GOOD = """---
|
|
85
|
+
title: 入门
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 目标
|
|
89
|
+
|
|
90
|
+
这一章说明一个可以重复验证的目标。
|
|
91
|
+
|
|
92
|
+
```{=html}
|
|
93
|
+
<figure class="book-figure">
|
|
94
|
+
<svg class="coords-grid" role="img" aria-label="棋盘坐标。"><rect width="10" height="10"/></svg>
|
|
95
|
+
<figcaption>棋盘坐标图:3 行 3 列。</figcaption>
|
|
96
|
+
</figure>
|
|
97
|
+
```
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
|
|
71
101
|
TREE_GAP_CHAPTER = """---
|
|
72
102
|
title: 入门
|
|
73
103
|
---
|
|
@@ -139,8 +169,11 @@ def main() -> int:
|
|
|
139
169
|
pass
|
|
140
170
|
|
|
141
171
|
failures: list[str] = []
|
|
172
|
+
passed_cases = 0
|
|
142
173
|
|
|
143
174
|
def expect(name: str, chapter: str, code: int, needle: str, project: bool = False) -> None:
|
|
175
|
+
nonlocal passed_cases
|
|
176
|
+
passed_cases += 1
|
|
144
177
|
with tempfile.TemporaryDirectory() as raw:
|
|
145
178
|
root = Path(raw)
|
|
146
179
|
make_book(root, chapter)
|
|
@@ -177,6 +210,31 @@ def main() -> int:
|
|
|
177
210
|
VALID_CHAPTER + "\n`Game` 与 `Board` 之间的组成边表示什么?\n",
|
|
178
211
|
1, "自造关系词",
|
|
179
212
|
)
|
|
213
|
+
expect(
|
|
214
|
+
"类型承载",
|
|
215
|
+
VALID_CHAPTER + "\n坐标由 `CellPosition` 承载,取值由 `CellType` 承载。\n",
|
|
216
|
+
1, "承载",
|
|
217
|
+
)
|
|
218
|
+
expect(
|
|
219
|
+
"小节导航",
|
|
220
|
+
VALID_CHAPTER + "\n下一节说明落子顺序。\n",
|
|
221
|
+
1, "小节导航",
|
|
222
|
+
)
|
|
223
|
+
expect(
|
|
224
|
+
"拉链句",
|
|
225
|
+
VALID_CHAPTER + "\n两个类型分别交给坐标与取值。\n",
|
|
226
|
+
1, "拉链句",
|
|
227
|
+
)
|
|
228
|
+
duplicate_link = (
|
|
229
|
+
"https://github.com/chengzhao-dev/cpp-board-games/blob/main/"
|
|
230
|
+
"games/tictactoe/tictactoe/include/board.h"
|
|
231
|
+
)
|
|
232
|
+
expect(
|
|
233
|
+
"同章重复文件链接",
|
|
234
|
+
VALID_CHAPTER
|
|
235
|
+
+ f"\n[board.h]({duplicate_link})\n\n[board.h]({duplicate_link})\n",
|
|
236
|
+
1, "同章重复文件链接",
|
|
237
|
+
)
|
|
180
238
|
expect("标题口语化", VALID_CHAPTER.replace("## 目标", "## 自己走一局"), 1, "标题带人称")
|
|
181
239
|
expect("标题带行内代码", VALID_CHAPTER.replace("## 目标", "## 运行 `app`"), 1, "标题带行内代码")
|
|
182
240
|
expect("正文 H1", VALID_CHAPTER.replace("## 目标", "# 目标"), 1, "正文出现 H1")
|
|
@@ -196,6 +254,12 @@ def main() -> int:
|
|
|
196
254
|
"---\ntitle: 入门\n---\n\n## 目标\n\n```\nprint(1)\n```\n",
|
|
197
255
|
1, "未声明语言",
|
|
198
256
|
)
|
|
257
|
+
expect(
|
|
258
|
+
"相邻围栏缺少空行",
|
|
259
|
+
"---\ntitle: 入门\n---\n\n## 目标\n\n"
|
|
260
|
+
"```text\na\n```\n```text\nb\n```\n",
|
|
261
|
+
1, "DOC-E11",
|
|
262
|
+
)
|
|
199
263
|
expect(
|
|
200
264
|
"项目禁用词生效",
|
|
201
265
|
"---\ntitle: 入门\n---\n\n## 目标\n\n棋盘格子按行列编号,程序读取当前状态。\n",
|
|
@@ -209,6 +273,8 @@ def main() -> int:
|
|
|
209
273
|
)
|
|
210
274
|
|
|
211
275
|
expect("目录树合格", TREE_CHAPTER, 0, "OK")
|
|
276
|
+
expect("原生 SVG 缺图注", HTML_FIGURE_BAD, 1, "figcaption")
|
|
277
|
+
expect("原生 SVG 图注合格", HTML_FIGURE_GOOD, 0, "OK")
|
|
212
278
|
expect(
|
|
213
279
|
"标记后多空格",
|
|
214
280
|
TREE_CHAPTER.replace("# 读入坐标", "# 读入坐标"),
|
|
@@ -325,6 +391,7 @@ def main() -> int:
|
|
|
325
391
|
)
|
|
326
392
|
|
|
327
393
|
with tempfile.TemporaryDirectory() as raw:
|
|
394
|
+
passed_cases += 1
|
|
328
395
|
root = Path(raw)
|
|
329
396
|
make_book(root)
|
|
330
397
|
(root / "content" / "part" / "01-intro.qmd").unlink()
|
|
@@ -339,7 +406,7 @@ def main() -> int:
|
|
|
339
406
|
print(f" FAIL {line}")
|
|
340
407
|
return 1
|
|
341
408
|
|
|
342
|
-
print("PASS:
|
|
409
|
+
print(f"PASS:{passed_cases} 个用例全部通过")
|
|
343
410
|
return 0
|
|
344
411
|
|
|
345
412
|
|
|
@@ -51,6 +51,9 @@ TRAILING_SPACE_RE = re.compile(r"[ ]+$")
|
|
|
51
51
|
QA_PROSE_RE = re.compile(r"(?:它|只|都|分别|各自)回答|回答(?:四类|整局|这个|哪些|什么)")
|
|
52
52
|
CODE_META_RE = re.compile(r"对应的代码")
|
|
53
53
|
COINED_RELATION_RE = re.compile(r"(?:组成|依赖|继承|调用)边")
|
|
54
|
+
CARRIER_RE = re.compile("承载")
|
|
55
|
+
NEXT_SECTION_RE = re.compile(r"(?:下一节|上一节)")
|
|
56
|
+
LADDER_RE = re.compile(r"(?:分别交给|各说一层)")
|
|
54
57
|
|
|
55
58
|
TREE_CATEGORY_COMMENTS = frozenset({
|
|
56
59
|
"头文件", "源文件", "源码", "实现", "测试", "配置文件",
|
|
@@ -89,7 +92,8 @@ class Issue:
|
|
|
89
92
|
|
|
90
93
|
|
|
91
94
|
def read_text(path: Path) -> str:
|
|
92
|
-
|
|
95
|
+
# utf-8-sig 兼容带 BOM 的合法 UTF-8 文件;BOM 不进入行号与正文。
|
|
96
|
+
return path.read_text(encoding="utf-8-sig")
|
|
93
97
|
|
|
94
98
|
|
|
95
99
|
def strip_frontmatter(text: str) -> tuple[str, str]:
|
|
@@ -139,9 +143,9 @@ def parse_chapters(quarto_yml: Path) -> list[str]:
|
|
|
139
143
|
if stripped and not raw.startswith((" ", "\t", "-")):
|
|
140
144
|
in_chapters = False
|
|
141
145
|
continue
|
|
142
|
-
if not stripped
|
|
146
|
+
if not stripped:
|
|
143
147
|
continue
|
|
144
|
-
item = stripped[1:].strip()
|
|
148
|
+
item = stripped[1:].strip() if stripped.startswith("-") else stripped
|
|
145
149
|
key, sep, value = item.partition(":")
|
|
146
150
|
if sep and re.fullmatch(r"[a-z_]+", key.strip()):
|
|
147
151
|
# 只接受会落到页面的键:href 与 part 的文件形式。
|
|
@@ -153,6 +157,17 @@ def parse_chapters(quarto_yml: Path) -> list[str]:
|
|
|
153
157
|
return chapters
|
|
154
158
|
|
|
155
159
|
|
|
160
|
+
def collect_all_qmd(root: Path) -> list[Path]:
|
|
161
|
+
excluded = {"node_modules", "_book", ".quarto", ".git", "_freeze", "_site"}
|
|
162
|
+
found: list[Path] = []
|
|
163
|
+
for candidate in sorted(root.rglob("*.qmd")):
|
|
164
|
+
rel_parts = candidate.relative_to(root).parts
|
|
165
|
+
if any(part in excluded or part.startswith(".") for part in rel_parts):
|
|
166
|
+
continue
|
|
167
|
+
found.append(candidate)
|
|
168
|
+
return found
|
|
169
|
+
|
|
170
|
+
|
|
156
171
|
def collect_chapter_files(root: Path, chapters: list[str]) -> list[Path]:
|
|
157
172
|
found: list[Path] = []
|
|
158
173
|
for rel in chapters:
|
|
@@ -164,15 +179,8 @@ def collect_chapter_files(root: Path, chapters: list[str]) -> list[Path]:
|
|
|
164
179
|
found.append(index)
|
|
165
180
|
if chapters:
|
|
166
181
|
return found
|
|
167
|
-
# No chapter table: audit every
|
|
168
|
-
|
|
169
|
-
for candidate in sorted(root.rglob("*.qmd")):
|
|
170
|
-
rel_parts = candidate.relative_to(root).parts
|
|
171
|
-
if any(part in excluded or part.startswith(".") for part in rel_parts):
|
|
172
|
-
continue
|
|
173
|
-
if candidate not in found:
|
|
174
|
-
found.append(candidate)
|
|
175
|
-
return found
|
|
182
|
+
# No chapter table: audit every .qmd outside generated/vendor dirs.
|
|
183
|
+
return collect_all_qmd(root)
|
|
176
184
|
|
|
177
185
|
|
|
178
186
|
def check_heading(path: Path, line: int, level: int, text: str, issues: list[Issue]) -> None:
|
|
@@ -425,6 +433,38 @@ def check_tree_block(path: Path, block: list[tuple[int, str]]) -> list[Issue]:
|
|
|
425
433
|
return issues
|
|
426
434
|
|
|
427
435
|
|
|
436
|
+
def check_html_figure_block(path: Path, block: list[tuple[int, str]]) -> list[Issue]:
|
|
437
|
+
"""原生 HTML/SVG 图要用 <figure> 包住,<figcaption> 放在图后。"""
|
|
438
|
+
issues: list[Issue] = []
|
|
439
|
+
svg_line: int | None = None
|
|
440
|
+
figure_line: int | None = None
|
|
441
|
+
caption_line: int | None = None
|
|
442
|
+
for number, raw in block:
|
|
443
|
+
if "<figure" in raw and figure_line is None:
|
|
444
|
+
figure_line = number
|
|
445
|
+
if "<svg" in raw and svg_line is None:
|
|
446
|
+
svg_line = number
|
|
447
|
+
if figure_line is None:
|
|
448
|
+
issues.append(Issue(
|
|
449
|
+
path, number,
|
|
450
|
+
"原生 SVG 图要用 <figure> 包住,并在图后写 <figcaption>",
|
|
451
|
+
))
|
|
452
|
+
if "<figcaption" in raw and caption_line is None:
|
|
453
|
+
caption_line = number
|
|
454
|
+
if svg_line is not None and figure_line is not None:
|
|
455
|
+
if caption_line is None:
|
|
456
|
+
issues.append(Issue(
|
|
457
|
+
path, svg_line,
|
|
458
|
+
"原生 SVG 图缺 <figcaption>(图注要落在图下)",
|
|
459
|
+
))
|
|
460
|
+
elif caption_line < svg_line:
|
|
461
|
+
issues.append(Issue(
|
|
462
|
+
path, caption_line,
|
|
463
|
+
"<figcaption> 要放在图后,不能放在图前",
|
|
464
|
+
))
|
|
465
|
+
return issues
|
|
466
|
+
|
|
467
|
+
|
|
428
468
|
def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> list[Issue]:
|
|
429
469
|
issues: list[Issue] = []
|
|
430
470
|
text = read_text(path)
|
|
@@ -436,6 +476,10 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
|
|
|
436
476
|
elif front and not parse_frontmatter_title(front):
|
|
437
477
|
issues.append(Issue(path, 1, "frontmatter 缺少 title"))
|
|
438
478
|
|
|
479
|
+
# 保留 frontmatter 的行数,后续行号才是文件里的真实行号。
|
|
480
|
+
leading = text[: len(text) - len(body)] if body else ""
|
|
481
|
+
if leading:
|
|
482
|
+
body = "\n" * leading.count("\n") + body
|
|
439
483
|
lines = body.splitlines()
|
|
440
484
|
in_fence = False
|
|
441
485
|
fence_lang = ""
|
|
@@ -443,8 +487,11 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
|
|
|
443
487
|
fence_lines: list[tuple[int, str]] = []
|
|
444
488
|
fence_blocks: list[list[tuple[int, str]]] = []
|
|
445
489
|
mermaid_blocks: list[list[tuple[int, str]]] = []
|
|
490
|
+
html_blocks: list[list[tuple[int, str]]] = []
|
|
491
|
+
fence_is_html = False
|
|
446
492
|
paragraph: list[str] = []
|
|
447
493
|
paragraph_start = 0
|
|
494
|
+
last_fence_end: int | None = None
|
|
448
495
|
|
|
449
496
|
def flush_paragraph() -> None:
|
|
450
497
|
nonlocal paragraph
|
|
@@ -459,24 +506,35 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
|
|
|
459
506
|
if not in_fence:
|
|
460
507
|
in_fence = True
|
|
461
508
|
fence_start = number
|
|
509
|
+
if last_fence_end is not None and number == last_fence_end + 1:
|
|
510
|
+
issues.append(Issue(
|
|
511
|
+
path, number,
|
|
512
|
+
"相邻围栏之间至少留一个空行(DOC-E11);空行不等于桥接正文",
|
|
513
|
+
))
|
|
462
514
|
fence_lang = (fence.group(2) or "").strip()
|
|
463
515
|
fence_lines = []
|
|
516
|
+
fence_is_html = False
|
|
464
517
|
if not fence_lang:
|
|
465
518
|
issues.append(Issue(path, number, "代码围栏未声明语言"))
|
|
466
519
|
elif fence_lang == "mermaid":
|
|
467
520
|
issues.append(Issue(path, number, "plain mermaid 围栏(改用 ```{mermaid})"))
|
|
468
521
|
elif fence_lang.startswith("{"):
|
|
469
522
|
inner = fence_lang.strip("{}").strip()
|
|
523
|
+
fence_is_html = inner == "=html"
|
|
470
524
|
if inner and not inner.startswith((".", "=")) and "mermaid" not in inner:
|
|
471
525
|
# ```{python} style attribute fences are valid.
|
|
472
526
|
pass
|
|
473
527
|
else:
|
|
474
528
|
in_fence = False
|
|
529
|
+
last_fence_end = number
|
|
475
530
|
if fence_lines:
|
|
476
531
|
fence_blocks.append(fence_lines)
|
|
477
532
|
if "mermaid" in fence_lang:
|
|
478
533
|
mermaid_blocks.append(fence_lines)
|
|
534
|
+
if fence_is_html:
|
|
535
|
+
html_blocks.append(fence_lines)
|
|
479
536
|
fence_lines = []
|
|
537
|
+
fence_is_html = False
|
|
480
538
|
continue
|
|
481
539
|
|
|
482
540
|
if in_fence:
|
|
@@ -504,7 +562,12 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
|
|
|
504
562
|
flush_paragraph()
|
|
505
563
|
continue
|
|
506
564
|
|
|
507
|
-
if raw.lstrip().startswith(
|
|
565
|
+
if raw.lstrip().startswith("|"):
|
|
566
|
+
flush_paragraph()
|
|
567
|
+
# 表格行按列组织,不套散文句长阈值;列数与表头一致性由表格检查负责。
|
|
568
|
+
continue
|
|
569
|
+
|
|
570
|
+
if raw.lstrip().startswith((">", "-", "*", "+", "1.")):
|
|
508
571
|
flush_paragraph()
|
|
509
572
|
check_prose_block(path, number, re.sub(r"^\s*(?:[>|]|[-*+]|\d+\.)\s*", "", raw), issues)
|
|
510
573
|
continue
|
|
@@ -515,6 +578,9 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
|
|
|
515
578
|
|
|
516
579
|
flush_paragraph()
|
|
517
580
|
|
|
581
|
+
if in_fence:
|
|
582
|
+
issues.append(Issue(path, fence_start, "代码围栏未闭合"))
|
|
583
|
+
|
|
518
584
|
for block in fence_blocks:
|
|
519
585
|
issues.extend(check_tree_block(path, block))
|
|
520
586
|
|
|
@@ -522,21 +588,41 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
|
|
|
522
588
|
issues.extend(check_mermaid_block(path, block))
|
|
523
589
|
issues.extend(check_mermaid_wrapper(path, lines, block))
|
|
524
590
|
|
|
591
|
+
for block in html_blocks:
|
|
592
|
+
issues.extend(check_html_figure_block(path, block))
|
|
593
|
+
|
|
594
|
+
fenced_lines: set[int] = set()
|
|
595
|
+
inside_fence = False
|
|
596
|
+
for number, raw in enumerate(lines, start=1):
|
|
597
|
+
if FENCE_RE.match(raw):
|
|
598
|
+
fenced_lines.add(number)
|
|
599
|
+
inside_fence = not inside_fence
|
|
600
|
+
elif inside_fence:
|
|
601
|
+
fenced_lines.add(number)
|
|
602
|
+
|
|
603
|
+
blob_links: dict[str, list[int]] = {}
|
|
604
|
+
|
|
525
605
|
for index, raw in enumerate(lines, start=1):
|
|
526
|
-
|
|
527
|
-
if not match:
|
|
606
|
+
if index in fenced_lines:
|
|
528
607
|
continue
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
608
|
+
for match in LINK_RE.finditer(raw):
|
|
609
|
+
target = match.group(1).strip()
|
|
610
|
+
if target.startswith(("http://", "https://", "mailto:", "#", "data:", "//")):
|
|
611
|
+
if target.startswith("https://github.com/") and "/blob/" in target:
|
|
612
|
+
blob_links.setdefault(target, []).append(index)
|
|
613
|
+
continue
|
|
614
|
+
clean = target.split("#", 1)[0].split("?", 1)[0]
|
|
615
|
+
if not clean or "<" in clean:
|
|
616
|
+
continue
|
|
617
|
+
if not (path.parent / clean).exists() and not (root / clean).exists():
|
|
618
|
+
issues.append(Issue(path, index, f"本地链接不可达:{target}"))
|
|
619
|
+
|
|
620
|
+
for target, numbers in blob_links.items():
|
|
621
|
+
if len(numbers) > 1:
|
|
622
|
+
issues.append(Issue(
|
|
623
|
+
path, numbers[1],
|
|
624
|
+
f"同章重复文件链接:{target}(首次在第 {numbers[0]} 行);每章每条仓库路径至多一条",
|
|
625
|
+
))
|
|
540
626
|
|
|
541
627
|
for term in GENERIC_FORBIDDEN_TERMS:
|
|
542
628
|
for index, raw in enumerate(lines, start=1):
|
|
@@ -571,6 +657,24 @@ def check_file(path: Path, root: Path, project: dict, chapters: list[str]) -> li
|
|
|
571
657
|
path, index,
|
|
572
658
|
f"自造关系词「{match.group(0)}」;用组成、调用、依赖、继承等通行词",
|
|
573
659
|
))
|
|
660
|
+
match = CARRIER_RE.search(raw)
|
|
661
|
+
if match:
|
|
662
|
+
issues.append(Issue(
|
|
663
|
+
path, index,
|
|
664
|
+
"类型或字段不用「承载」描述;改用表示、保存、记录或对应",
|
|
665
|
+
))
|
|
666
|
+
match = NEXT_SECTION_RE.search(raw)
|
|
667
|
+
if match:
|
|
668
|
+
issues.append(Issue(
|
|
669
|
+
path, index,
|
|
670
|
+
f"小节导航「{match.group(0)}」;删除后用新节首句承接,章末预告保留",
|
|
671
|
+
))
|
|
672
|
+
match = LADDER_RE.search(raw)
|
|
673
|
+
if match:
|
|
674
|
+
issues.append(Issue(
|
|
675
|
+
path, index,
|
|
676
|
+
f"拉链句「{match.group(0)}」;拆句或改成一项一行",
|
|
677
|
+
))
|
|
574
678
|
|
|
575
679
|
required = project.get("chapter_titles", {}).get(rel)
|
|
576
680
|
if required:
|
|
@@ -605,6 +709,7 @@ def load_project(root: Path) -> tuple[bool, dict]:
|
|
|
605
709
|
def main() -> int:
|
|
606
710
|
parser = argparse.ArgumentParser(description="检查 Quarto Book 章节源码")
|
|
607
711
|
parser.add_argument("--root", default=".", help="书根目录(默认当前目录)")
|
|
712
|
+
parser.add_argument("--paths", nargs="+", help="只检查这些 qmd 路径(相对 --root)")
|
|
608
713
|
parser.add_argument("--strict", action="store_true", help="把未列入目录的文件也视为错误")
|
|
609
714
|
parser.add_argument("--quiet", action="store_true", help="只输出汇总")
|
|
610
715
|
args = parser.parse_args()
|
|
@@ -621,27 +726,52 @@ def main() -> int:
|
|
|
621
726
|
return 2
|
|
622
727
|
|
|
623
728
|
quarto_yml = root / "_quarto.yml"
|
|
624
|
-
|
|
729
|
+
try:
|
|
730
|
+
chapters = parse_chapters(quarto_yml)
|
|
731
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
732
|
+
print(f"失败 读取目录表失败: {exc}")
|
|
733
|
+
return 1
|
|
625
734
|
files = collect_chapter_files(root, chapters)
|
|
735
|
+
issues: list[Issue] = []
|
|
736
|
+
if args.paths:
|
|
737
|
+
requested: list[Path] = []
|
|
738
|
+
for raw in args.paths:
|
|
739
|
+
candidate = (root / raw).resolve()
|
|
740
|
+
if root not in candidate.parents and candidate != root:
|
|
741
|
+
issues.append(Issue(quarto_yml, 1, f"--paths 落在书根之外:{raw}"))
|
|
742
|
+
elif not candidate.is_file():
|
|
743
|
+
issues.append(Issue(candidate, 1, f"--paths 指定的文件不存在:{raw}"))
|
|
744
|
+
else:
|
|
745
|
+
requested.append(candidate)
|
|
746
|
+
files = requested
|
|
626
747
|
if not files:
|
|
627
748
|
print(f"失败 {root} 下没有 .qmd 章节(确认 --root 指向书根目录)")
|
|
628
749
|
return 1
|
|
629
750
|
|
|
630
|
-
|
|
751
|
+
try:
|
|
752
|
+
has_project, project = load_project(root)
|
|
753
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
754
|
+
print(f"失败 读取项目规则失败: {exc}")
|
|
755
|
+
return 1
|
|
631
756
|
|
|
632
757
|
missing = [rel for rel in chapters if not (root / rel).is_file()]
|
|
633
|
-
issues
|
|
758
|
+
issues.extend([
|
|
634
759
|
Issue(quarto_yml, 1, f"目录中的章节不存在:{rel}") for rel in missing
|
|
635
|
-
]
|
|
760
|
+
])
|
|
761
|
+
|
|
762
|
+
if args.strict:
|
|
763
|
+
extras = [item for item in collect_all_qmd(root) if item not in files]
|
|
764
|
+
for item in extras:
|
|
765
|
+
issues.append(Issue(item, 1, "未列入 _quarto.yml(strict 模式)"))
|
|
766
|
+
files.extend(extras)
|
|
636
767
|
|
|
637
768
|
for path in files:
|
|
638
|
-
|
|
769
|
+
try:
|
|
770
|
+
issues.extend(check_file(path, root, project, chapters))
|
|
771
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
772
|
+
issues.append(Issue(path, 1, f"无法按 UTF-8 读取:{exc}"))
|
|
639
773
|
|
|
640
|
-
if args.strict:
|
|
641
|
-
for issue in list(issues):
|
|
642
|
-
if "未列入 _quarto.yml" in issue.message:
|
|
643
|
-
issue.message = issue.message.replace("(目录会漏章)", "")
|
|
644
|
-
else:
|
|
774
|
+
if not args.strict:
|
|
645
775
|
issues = [issue for issue in issues if "未列入 _quarto.yml" not in issue.message]
|
|
646
776
|
|
|
647
777
|
if not args.quiet:
|
|
@@ -100,11 +100,12 @@ int main() {
|
|
|
100
100
|
|
|
101
101
|
### 代码与图的桥接
|
|
102
102
|
|
|
103
|
-
代码块引出 `{mermaid}` 图或 `{python}` 坐标图时按固定序列组织:代码块 → 桥接句 → 图(含 `fig-cap`)→ 图后新结论。固定序列的完整写法见 `writing-quarto` 技能 `quarto/authoring.md
|
|
103
|
+
代码块引出 `{mermaid}` 图或 `{python}` 坐标图时按固定序列组织:代码块 → 桥接句 → 图(含 `fig-cap`)→ 图后新结论。固定序列的完整写法见 `writing-quarto` 技能 `quarto/authoring.md`「代码与图的桥接」,边界如下:
|
|
104
104
|
|
|
105
105
|
- 代码块与图之间必须有桥接句,说明图要核对代码已讲清的哪条关系;中间只有空行视为堆叠(`verifying-content.py` 的 DOC-E11 只要求相邻围栏留空行,空行不等于桥接正文)。
|
|
106
106
|
- 图后先给「从图带走的结论」或图外新信息,再进入下一块;小节以图收尾时,结论可并入桥接段,不在图后孤悬一句。图注已写明的信息不复述。坐标图计入盒子密度,口径见 `quarto/coords.md` 与本文件。
|
|
107
|
-
-
|
|
107
|
+
- 图注与图前句分工:图注写图面内容与读法,落在图下;图前只写引出关系的一句,不写“下图标出……”这类图面说明。三类图(Mermaid、坐标图、原生 HTML/SVG)的写法见 `quarto/figures.md`。
|
|
108
|
+
- 一图一事、同主题相邻、图不承载正文未写的新信息:一张图只核对一条已讲清的关系;代码与图主题不同时收束本对象再拆 `###`,禁止「代码 A → 短句拐弯 → 图讲 B → 再贴代码 C」的同节混排(上述边界的写法见 `quarto/authoring.md`「代码与图的桥接」)。
|
|
108
109
|
- 图前段只讲本页可见的对象,且单一主题:描述版式的句子其对象必须同页可见,引用前章画面时复述读者需要的关键事实(如行列取值 0 到 2),不指称前章画面的版式(「画面上方是列号」式会让读者翻回前章);调用方行为等第二主题移出图前段(示例章定义坐标范围节两段并一段的改稿对照)。
|
|
109
110
|
|
|
110
111
|
图不能替代结论句:桥接句与图带走的结论缺任何一处,读者只有图没有判断依据。正反对照见 `examples/03-qmd-elements.md`「代码与图桥接对照」。
|
|
@@ -225,7 +226,7 @@ int main() {
|
|
|
225
226
|
|
|
226
227
|
### 情况:图是 Mermaid 或坐标轴图
|
|
227
228
|
|
|
228
|
-
要画流程图、类图或时序图才打开 `quarto/mermaid.md`(类图先按内容预算删节点和成员),坐标轴网格图打开 `quarto/coords.md`。不排图不打开。
|
|
229
|
+
要画流程图、类图或时序图才打开 `quarto/mermaid.md`(类图先按内容预算删节点和成员),坐标轴网格图打开 `quarto/coords.md`,图注与原生 HTML/SVG 图打开 `quarto/figures.md`。不排图不打开。
|
|
229
230
|
|
|
230
231
|
### 情况:Callout
|
|
231
232
|
|
|
@@ -31,6 +31,22 @@
|
|
|
31
31
|
|
|
32
32
|
「只读」「只写」是读写性质的固定术语,保留;`const` 成员函数首次称「只读」时,写明它不改动对象。安全、协议或验收条件确实需要排他时,写成可核对的条件(「`PlaceCell` 在目标格子非空时返回 `false`」),不靠「只」承担论证。
|
|
33
33
|
|
|
34
|
+
### 表示、保存、记录,不用「承载」
|
|
35
|
+
|
|
36
|
+
读者正文不把类型或字段写成「承载」某个值。「承载」是抽象能力或文档职责的说法,套到 `struct`、`enum`、字段上会让读者分不清它表示什么、运行时保存什么。
|
|
37
|
+
|
|
38
|
+
按对象选动词:
|
|
39
|
+
|
|
40
|
+
- 类型表达概念:`CellPosition` **表示**一对行列,`CellType` **表示**格子里的取值。
|
|
41
|
+
- 对象保存数据:`Board` 的 `cells_` **保存**九个 `CellType`。
|
|
42
|
+
- 文件或协议留下记录:快照 **记录**落子次数、当前玩家和结果。
|
|
43
|
+
- 名称与值对应:`kX` **对应** X 标记。
|
|
44
|
+
|
|
45
|
+
- 不佳:坐标由 `CellPosition` 承载,取值由 `CellType` 承载。
|
|
46
|
+
- 改写:`CellPosition` 表示一对行列,`CellType` 表示格子里的取值。
|
|
47
|
+
- 不佳:这份配置承载三个字段。
|
|
48
|
+
- 改写:这份配置保存三个字段。
|
|
49
|
+
|
|
34
50
|
### 读者正文禁无信息提问与自造关系词
|
|
35
51
|
|
|
36
52
|
读者正文不向读者发问。没有任务指向的提问(「总览图为什么不列成员?」「这条边表示什么?」)只是把作者自检提纲搬进正文,删掉后读者仍能拿到同样信息;需要读者判断的内容改写为陈述句,或放进自测。
|
|
@@ -100,6 +116,7 @@
|
|
|
100
116
|
- 动词选用与主动语态见「目标语气」;「由 X 完成」在这一步改成主动句,改完露出第二个核就退回定段落。
|
|
101
117
|
- 「同一概念只使用一个称呼」的判定见「七个质量维度」;英文标识保持大小写。
|
|
102
118
|
- 「游戏」指作品或玩法整体,「对局」指具体一场:定义「井字棋是…游戏」用「游戏」,规则、落子与结果语境用「对局」;同一页不把两者当同义词换用(对照见 `examples/06-wording-cases.md`「游戏与对局」)。
|
|
119
|
+
- 项目名在同一页首次写全(如「井字棋棋盘」),后文改用对象名(「棋盘」「动态库」「对局」);不逐段重复项目名,也不在同一页反复解释已经说过的事。
|
|
103
120
|
- 无信息修饰的删除判定见「七个质量维度」;禁用词以 `scripts/verifying-content.py` 词表为准。
|
|
104
121
|
- 压缩动词写全:`重建`、`重编`、`重跑` 这类两字词,读者可能读成「重新创建」;写「重新生成」「重新编译」「重新运行」,同一页后续也不回退到压缩词。
|
|
105
122
|
- 连续出现三个「的」时的改写判定见「七个质量维度」第 5 条。
|