@chengzhao-dev/dsh-quarto-book 0.6.3 → 0.6.5
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 +5 -3
- package/lib/index.js +49 -7
- package/lib/tools/pass-report.js +12 -12
- package/lib/tools/render-check.js +253 -65
- 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/SKILL.md +1 -1
- package/skills/designing-theme/assets/theme/css/content.css +31 -8
- package/skills/designing-theme/references/theme-spine.md +1 -1
- 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 +11 -2
- package/skills/writing-quarto/examples/03-block-after.md +29 -0
- package/skills/writing-quarto/examples/03-excerpt-lead.md +10 -0
- package/skills/writing-quarto/examples/03-qmd-elements.md +29 -2
- package/skills/writing-quarto/examples/04-paragraph-cases.md +13 -0
- package/skills/writing-quarto/examples/07-layout-list.md +34 -0
- package/skills/writing-quarto/quarto/authoring.md +5 -4
- 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 +2 -2
- package/skills/writing-quarto/quarto/refactor.md +1 -1
- package/skills/writing-quarto/quarto/render.md +4 -4
- package/skills/writing-quarto/quarto/tables.md +79 -0
- 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 +70 -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 +187 -35
- package/skills/writing-quarto/steps/02-structure.md +1 -1
- package/skills/writing-quarto/steps/03-blocks.md +19 -6
- package/skills/writing-quarto/steps/04-paragraphs.md +1 -0
- package/skills/writing-quarto/steps/06-wording.md +17 -0
- package/skills/writing-quarto/steps/07-layout.md +5 -1
- package/skills/writing-quarto/steps/08-reread.md +1 -1
|
@@ -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
|
|
@@ -20,8 +20,13 @@ import sys
|
|
|
20
20
|
from pathlib import Path
|
|
21
21
|
|
|
22
22
|
LINK_RE = re.compile(r"\[[^\]]*\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)")
|
|
23
|
+
REF_USE_RE = re.compile(r"(!?)\[([^\]]*)\]\[([^\]]*)\]")
|
|
24
|
+
REF_DEF_RE = re.compile(r"^ {0,3}\[([^\]]+)\]:\s*(?:<([^>\s]+)>|(\S+))")
|
|
25
|
+
INLINE_CODE_RE = re.compile(r"`+[^`]*`+")
|
|
23
26
|
HTML_LINK_RE = re.compile(r"<(?:a|img)\b[^>]*\b(?:href|src)=\"([^\"]+)\"", re.IGNORECASE)
|
|
27
|
+
FENCE_RE = re.compile(r"^\s*(```+|~~~+)")
|
|
24
28
|
SKIP_PREFIXES = ("http://", "https://", "mailto:", "tel:", "data:", "#", "//")
|
|
29
|
+
SKIP_DIRS = {".git", ".cache", ".quarto", "_book", "_freeze", "_site", "build", "node_modules", "temp"}
|
|
25
30
|
|
|
26
31
|
|
|
27
32
|
def collect_sources(root: Path, paths: list[str]) -> list[Path]:
|
|
@@ -41,17 +46,57 @@ def collect_sources(root: Path, paths: list[str]) -> list[Path]:
|
|
|
41
46
|
return sorted(
|
|
42
47
|
path for path in found
|
|
43
48
|
if not any(
|
|
44
|
-
part in
|
|
45
|
-
for part in path.parts
|
|
49
|
+
part in SKIP_DIRS
|
|
50
|
+
for part in path.relative_to(root).parts
|
|
46
51
|
)
|
|
47
52
|
)
|
|
48
53
|
|
|
49
54
|
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
def normalize_label(label: str) -> str:
|
|
56
|
+
return re.sub(r"\s+", " ", label.strip()).casefold()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def collect_definitions(text: str) -> dict[str, tuple[int, str]]:
|
|
60
|
+
"""收集引用式链接定义 `[label]: target`;围栏内的定义不算。"""
|
|
61
|
+
definitions: dict[str, tuple[int, str]] = {}
|
|
62
|
+
in_fence = False
|
|
63
|
+
for number, line in enumerate(text.splitlines(), start=1):
|
|
64
|
+
if FENCE_RE.match(line):
|
|
65
|
+
in_fence = not in_fence
|
|
66
|
+
continue
|
|
67
|
+
if in_fence:
|
|
68
|
+
continue
|
|
69
|
+
match = REF_DEF_RE.match(line)
|
|
70
|
+
if not match:
|
|
71
|
+
continue
|
|
72
|
+
label = normalize_label(match.group(1))
|
|
73
|
+
target = match.group(2) or match.group(3) or ""
|
|
74
|
+
if label and label not in definitions:
|
|
75
|
+
definitions[label] = (number, target)
|
|
76
|
+
return definitions
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def iter_links(text: str, definitions: dict[str, tuple[int, str]]):
|
|
80
|
+
in_fence = False
|
|
81
|
+
for number, line in enumerate(text.splitlines(), start=1):
|
|
82
|
+
if FENCE_RE.match(line):
|
|
83
|
+
in_fence = not in_fence
|
|
84
|
+
continue
|
|
85
|
+
if in_fence or REF_DEF_RE.match(line):
|
|
86
|
+
continue
|
|
87
|
+
# 行内代码里的方括号不是链接;先抹掉,避免把示例当引用。
|
|
88
|
+
visible = INLINE_CODE_RE.sub(lambda match: " " * len(match.group(0)), line)
|
|
89
|
+
for match in LINK_RE.finditer(visible):
|
|
90
|
+
yield number, match.group(1), None
|
|
91
|
+
for match in REF_USE_RE.finditer(visible):
|
|
92
|
+
label = match.group(3) or match.group(2)
|
|
93
|
+
key = normalize_label(label)
|
|
94
|
+
if not key:
|
|
95
|
+
continue
|
|
96
|
+
definition = definitions.get(key)
|
|
97
|
+
yield number, definition[1] if definition else None, label
|
|
98
|
+
for match in HTML_LINK_RE.finditer(visible):
|
|
99
|
+
yield number, match.group(1), None
|
|
55
100
|
|
|
56
101
|
|
|
57
102
|
def main() -> int:
|
|
@@ -72,13 +117,25 @@ def main() -> int:
|
|
|
72
117
|
return 2
|
|
73
118
|
|
|
74
119
|
sources = collect_sources(root, args.paths)
|
|
75
|
-
|
|
120
|
+
if not sources:
|
|
121
|
+
print("失败 没有可检查的 Markdown/Quarto 源文件", file=sys.stderr)
|
|
122
|
+
return 2
|
|
123
|
+
broken: list[tuple[str, int, str, str]] = []
|
|
76
124
|
checked = 0
|
|
77
125
|
|
|
78
126
|
for source in sources:
|
|
79
|
-
|
|
127
|
+
try:
|
|
128
|
+
text = source.read_text(encoding="utf-8")
|
|
129
|
+
except UnicodeDecodeError as exc:
|
|
130
|
+
print(f"失败 {source} 不是合法 UTF-8:{exc}", file=sys.stderr)
|
|
131
|
+
return 1
|
|
80
132
|
rel = source.relative_to(root).as_posix()
|
|
81
|
-
|
|
133
|
+
definitions = collect_definitions(text)
|
|
134
|
+
for number, raw, label in iter_links(text, definitions):
|
|
135
|
+
if raw is None:
|
|
136
|
+
checked += 1
|
|
137
|
+
broken.append((rel, number, f"[{label}]", "引用标签未定义"))
|
|
138
|
+
continue
|
|
82
139
|
target = raw.strip()
|
|
83
140
|
if not target or target.startswith(SKIP_PREFIXES):
|
|
84
141
|
continue
|
|
@@ -91,7 +148,7 @@ def main() -> int:
|
|
|
91
148
|
candidates = [source.parent / clean, root / clean]
|
|
92
149
|
if any(candidate.exists() for candidate in candidates):
|
|
93
150
|
continue
|
|
94
|
-
broken.append((rel, target, "目标不存在"))
|
|
151
|
+
broken.append((rel, number, target, "目标不存在"))
|
|
95
152
|
|
|
96
153
|
print(f"扫描 {len(sources)} 个文件,检查 {checked} 条本地链接。")
|
|
97
154
|
if not broken:
|
|
@@ -99,8 +156,8 @@ def main() -> int:
|
|
|
99
156
|
return 0
|
|
100
157
|
|
|
101
158
|
print(f"发现 {len(broken)} 条断链:")
|
|
102
|
-
for rel, target, reason in broken[:80]:
|
|
103
|
-
print(f" {rel} -> {target}({reason})")
|
|
159
|
+
for rel, number, target, reason in broken[:80]:
|
|
160
|
+
print(f" {rel}:{number} -> {target}({reason})")
|
|
104
161
|
return 1
|
|
105
162
|
|
|
106
163
|
|
|
@@ -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(
|
|
@@ -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
|
|