@topmindspace/tms-skills 0.3.6 → 0.4.1
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/CHANGELOG.md +153 -0
- package/README.en.md +12 -9
- package/README.md +12 -9
- package/bin/tms-skills.js +153 -13
- package/package.json +1 -1
- package/top-ppt-html/README.en.md +2 -2
- package/top-ppt-html/README.md +1 -1
- package/top-ppt-html/SKILL.md +8 -7
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +58 -184
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +13 -3
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +58 -337
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +15 -1
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +58 -930
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +15 -1
- package/top-ppt-html/assets/examples/2026-09-26-topmind-tms-skills-showcase.html +141 -31
- package/top-ppt-html/assets/icons/index.json +557 -0
- package/top-ppt-html/assets/icons//344/270/212/344/274/240.png +0 -0
- package/top-ppt-html/assets/icons//344/270/212/344/274/240.svg +1 -0
- package/top-ppt-html/assets/icons//344/270/213/350/275/275.png +0 -0
- package/top-ppt-html/assets/icons//344/270/213/350/275/275.svg +1 -0
- package/top-ppt-html/assets/icons//344/270/213/351/231/215.png +0 -0
- package/top-ppt-html/assets/icons//344/270/213/351/231/215.svg +1 -0
- package/top-ppt-html/assets/icons//344/271/246/347/255/276.png +0 -0
- package/top-ppt-html/assets/icons//344/271/246/347/255/276.svg +1 -0
- package/top-ppt-html/assets/icons//344/272/221.png +0 -0
- package/top-ppt-html/assets/icons//344/272/221.svg +1 -0
- package/top-ppt-html/assets/icons//344/275/215/347/275/256.png +0 -0
- package/top-ppt-html/assets/icons//344/275/215/347/275/256.svg +1 -0
- package/top-ppt-html/assets/icons//344/277/241/346/201/257.png +0 -0
- package/top-ppt-html/assets/icons//344/277/241/346/201/257.svg +1 -0
- package/top-ppt-html/assets/icons//345/210/206/344/272/253.png +0 -0
- package/top-ppt-html/assets/icons//345/210/206/344/272/253.svg +1 -0
- package/top-ppt-html/assets/icons//345/213/276/351/200/211.png +0 -0
- package/top-ppt-html/assets/icons//345/213/276/351/200/211.svg +1 -0
- package/top-ppt-html/assets/icons//345/215/240/346/257/224.png +0 -0
- package/top-ppt-html/assets/icons//345/215/240/346/257/224.svg +1 -0
- package/top-ppt-html/assets/icons//345/233/242/351/230/237.png +0 -0
- package/top-ppt-html/assets/icons//345/233/242/351/230/237.svg +1 -0
- package/top-ppt-html/assets/icons//345/233/276/350/241/250.png +0 -0
- package/top-ppt-html/assets/icons//345/233/276/350/241/250.svg +1 -0
- package/top-ppt-html/assets/icons//345/242/236/351/225/277.png +0 -0
- package/top-ppt-html/assets/icons//345/242/236/351/225/277.svg +1 -0
- package/top-ppt-html/assets/icons//345/256/211/345/205/250.png +0 -0
- package/top-ppt-html/assets/icons//345/256/211/345/205/250.svg +1 -0
- package/top-ppt-html/assets/icons//345/257/271/350/257/235.png +0 -0
- package/top-ppt-html/assets/icons//345/257/271/350/257/235.svg +1 -0
- package/top-ppt-html/assets/icons//345/267/245/345/205/267.png +0 -0
- package/top-ppt-html/assets/icons//345/267/245/345/205/267.svg +1 -0
- package/top-ppt-html/assets/icons//345/276/252/347/216/257.png +0 -0
- package/top-ppt-html/assets/icons//345/276/252/347/216/257.svg +1 -0
- package/top-ppt-html/assets/icons//346/210/220/346/236/234.png +0 -0
- package/top-ppt-html/assets/icons//346/210/220/346/236/234.svg +1 -0
- package/top-ppt-html/assets/icons//346/211/213/346/234/272.png +0 -0
- package/top-ppt-html/assets/icons//346/211/213/346/234/272.svg +1 -0
- package/top-ppt-html/assets/icons//346/225/210/347/216/207.png +0 -0
- package/top-ppt-html/assets/icons//346/225/210/347/216/207.svg +1 -0
- package/top-ppt-html/assets/icons//346/225/260/346/215/256.png +0 -0
- package/top-ppt-html/assets/icons//346/225/260/346/215/256.svg +1 -0
- package/top-ppt-html/assets/icons//346/226/207/344/273/266/345/244/271.png +0 -0
- package/top-ppt-html/assets/icons//346/226/207/344/273/266/345/244/271.svg +1 -0
- package/top-ppt-html/assets/icons//346/226/207/346/241/243.png +0 -0
- package/top-ppt-html/assets/icons//346/226/207/346/241/243.svg +1 -0
- package/top-ppt-html/assets/icons//346/227/227/345/270/234.png +0 -0
- package/top-ppt-html/assets/icons//346/227/227/345/270/234.svg +1 -0
- package/top-ppt-html/assets/icons//346/227/266/351/227/264.png +0 -0
- package/top-ppt-html/assets/icons//346/227/266/351/227/264.svg +1 -0
- package/top-ppt-html/assets/icons//346/230/237/346/240/207.png +0 -0
- package/top-ppt-html/assets/icons//346/230/237/346/240/207.svg +1 -0
- package/top-ppt-html/assets/icons//346/231/272/350/203/275.png +0 -0
- package/top-ppt-html/assets/icons//346/231/272/350/203/275.svg +1 -0
- package/top-ppt-html/assets/icons//346/235/203/351/231/220.png +0 -0
- package/top-ppt-html/assets/icons//346/235/203/351/231/220.svg +1 -0
- package/top-ppt-html/assets/icons//346/243/200/346/237/245.png +0 -0
- package/top-ppt-html/assets/icons//346/243/200/346/237/245.svg +1 -0
- package/top-ppt-html/assets/icons//346/264/236/345/257/237.png +0 -0
- package/top-ppt-html/assets/icons//346/264/236/345/257/237.svg +1 -0
- package/top-ppt-html/assets/icons//346/265/201/347/250/213.png +0 -0
- package/top-ppt-html/assets/icons//346/265/201/347/250/213.svg +1 -0
- package/top-ppt-html/assets/icons//346/270/205/345/215/225.png +0 -0
- package/top-ppt-html/assets/icons//346/270/205/345/215/225.svg +1 -0
- package/top-ppt-html/assets/icons//347/201/253/347/256/255.png +0 -0
- package/top-ppt-html/assets/icons//347/201/253/347/256/255.svg +1 -0
- package/top-ppt-html/assets/icons//347/201/257/346/263/241.png +0 -0
- package/top-ppt-html/assets/icons//347/201/257/346/263/241.svg +1 -0
- package/top-ppt-html/assets/icons//347/224/250/346/210/267.png +0 -0
- package/top-ppt-html/assets/icons//347/224/250/346/210/267.svg +1 -0
- package/top-ppt-html/assets/icons//347/233/256/346/240/207.png +0 -0
- package/top-ppt-html/assets/icons//347/233/256/346/240/207.svg +1 -0
- package/top-ppt-html/assets/icons//347/256/255/345/244/264.png +0 -0
- package/top-ppt-html/assets/icons//347/256/255/345/244/264.svg +1 -0
- package/top-ppt-html/assets/icons//347/273/204/347/273/207.png +0 -0
- package/top-ppt-html/assets/icons//347/273/204/347/273/207.svg +1 -0
- package/top-ppt-html/assets/icons//347/275/227/347/233/230.png +0 -0
- package/top-ppt-html/assets/icons//347/275/227/347/233/230.svg +1 -0
- package/top-ppt-html/assets/icons//350/241/250/346/240/274.png +0 -0
- package/top-ppt-html/assets/icons//350/241/250/346/240/274.svg +1 -0
- package/top-ppt-html/assets/icons//350/256/241/345/210/222.png +0 -0
- package/top-ppt-html/assets/icons//350/256/241/345/210/222.svg +1 -0
- package/top-ppt-html/assets/icons//350/256/276/347/275/256.png +0 -0
- package/top-ppt-html/assets/icons//350/256/276/347/275/256.svg +1 -0
- package/top-ppt-html/assets/icons//350/266/213/345/212/277.png +0 -0
- package/top-ppt-html/assets/icons//350/266/213/345/212/277.svg +1 -0
- package/top-ppt-html/assets/icons//351/202/256/344/273/266.png +0 -0
- package/top-ppt-html/assets/icons//351/202/256/344/273/266.svg +1 -0
- package/top-ppt-html/assets/icons//351/207/221/351/222/261.png +0 -0
- package/top-ppt-html/assets/icons//351/207/221/351/222/261.svg +1 -0
- package/top-ppt-html/assets/icons//351/223/276/346/216/245.png +0 -0
- package/top-ppt-html/assets/icons//351/223/276/346/216/245.svg +1 -0
- package/top-ppt-html/assets/icons//351/227/252/347/224/265.png +0 -0
- package/top-ppt-html/assets/icons//351/227/252/347/224/265.svg +1 -0
- package/top-ppt-html/assets/icons//351/243/216/351/231/251.png +0 -0
- package/top-ppt-html/assets/icons//351/243/216/351/231/251.svg +1 -0
- package/top-ppt-html/assets/pptx-export.js +44 -15
- package/top-ppt-html/assets/style-gallery.html +2 -2
- package/top-ppt-html/assets/templates/architecture.html +59 -17
- package/top-ppt-html/assets/templates/engine.css +12 -0
- package/top-ppt-html/assets/templates/presentation.html +59 -17
- package/top-ppt-html/assets/templates/research.html +59 -17
- package/top-ppt-html/package-lock.json +2 -2
- package/top-ppt-html/package.json +2 -2
- package/top-ppt-html/references/icons.md +22 -19
- package/top-ppt-html/references/layout-variants.md +69 -0
- package/top-ppt-html/references/style-pack.md +40 -0
- package/top-ppt-html/scripts/audit_css.py +1 -1
- package/top-ppt-html/scripts/build_pptx.js +228 -50
- package/top-ppt-html/scripts/check_icons.py +119 -0
- package/top-ppt-html/scripts/hydrate_charts.py +60 -5
- package/top-ppt-html/scripts/icon_lib.js +66 -37
- package/top-ppt-html/scripts/layout-constants.json +92 -5
- package/top-ppt-html/scripts/make_icon_pack.py +172 -0
- package/top-ppt-html/scripts/model-schema.json +4 -1
- package/top-ppt-html/scripts/regression.py +3 -3
- package/top-ppt-html/scripts/render_compare.py +1 -1
- package/top-ppt-html/scripts/render_from_model.py +116 -55
- package/top-ppt-html/scripts/scaffold_report.py +1 -1
- package/top-ppt-html/scripts/sync_runtime.py +14 -14
- package/top-ppt-html/scripts/validate_pptx.py +1 -0
- package/top-ppt-html/scripts/validate_report.py +157 -0
- package/topmind-cover/README.en.md +4 -4
- package/topmind-cover/README.md +4 -4
- package/topmind-cover/SKILL.md +36 -16
- package/topmind-cover/assets/examples/README.md +13 -7
- package/topmind-cover/assets/examples/bg-blur-alt.png +0 -0
- package/topmind-cover/assets/examples/bg-blur-wechat.png +0 -0
- package/topmind-cover/assets/examples/bg-blur.png +0 -0
- package/topmind-cover/assets/examples/big-type-wechat.png +0 -0
- package/topmind-cover/assets/examples/big-type.png +0 -0
- package/topmind-cover/assets/examples/brand-launch-wechat.png +0 -0
- package/topmind-cover/assets/examples/brand-launch.png +0 -0
- package/topmind-cover/assets/examples/gan-huo-wechat.png +0 -0
- package/topmind-cover/assets/examples/gan-huo.png +0 -0
- package/topmind-cover/assets/examples/ip-fun-wechat.png +0 -0
- package/topmind-cover/assets/examples/ip-fun.png +0 -0
- package/topmind-cover/assets/examples/magazine-wechat.png +0 -0
- package/topmind-cover/assets/examples/magazine.png +0 -0
- package/topmind-cover/assets/examples/minimal-wechat.png +0 -0
- package/topmind-cover/assets/examples/minimal.png +0 -0
- package/topmind-cover/assets/examples/news-flash-wechat.png +0 -0
- package/topmind-cover/assets/examples/news-flash.png +0 -0
- package/topmind-cover/assets/examples/overview.png +0 -0
- package/topmind-cover/assets/examples/paper-collage-alt.png +0 -0
- package/topmind-cover/assets/examples/paper-collage-wechat.png +0 -0
- package/topmind-cover/assets/examples/paper-collage.png +0 -0
- package/topmind-cover/assets/examples/tutorial-steps-wechat.png +0 -0
- package/topmind-cover/assets/examples/tutorial-steps.png +0 -0
- package/topmind-cover/assets/examples/white-clean-alt-wechat.png +0 -0
- package/topmind-cover/assets/examples/white-clean-alt.png +0 -0
- package/topmind-cover/assets/examples/white-clean-wechat.png +0 -0
- package/topmind-cover/assets/examples/white-clean.png +0 -0
- package/topmind-cover/package.json +2 -2
- package/topmind-cover/references/cover-styles.md +210 -35
- package/topmind-cover/references/impact-language.md +43 -2
- package/topmind-cover/references/ref-white-clean-1.png +0 -0
- package/topmind-cover/references/ref-white-clean-2.png +0 -0
- package/topmind-cover/scripts/crop-cover.py +19 -13
- package/topmind-wechat-post/README.en.md +2 -2
- package/topmind-wechat-post/README.md +2 -2
- package/topmind-wechat-post/SKILL.md +1 -1
- package/topmind-wechat-post/scripts/md2wechat.py +142 -30
- package/topmind-wechat-post/scripts/negative_tests.py +74 -0
- package/topmind-x-article/README.en.md +1 -1
- package/topmind-x-article/README.md +1 -1
- package/topmind-x-article/scripts/md2x.py +25 -5
- package/topmind-x-article/scripts/negative_tests.py +17 -0
|
@@ -31,7 +31,10 @@ def main() -> None:
|
|
|
31
31
|
|
|
32
32
|
src = Path(args.src)
|
|
33
33
|
out_dir = Path(args.out_dir)
|
|
34
|
-
|
|
34
|
+
try:
|
|
35
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
except OSError as e:
|
|
37
|
+
sys.exit(f"错误:无法创建输出目录 {out_dir}({e})")
|
|
35
38
|
|
|
36
39
|
if not src.is_file():
|
|
37
40
|
sys.exit(f"错误:找不到输入图片 {src}")
|
|
@@ -50,18 +53,21 @@ def main() -> None:
|
|
|
50
53
|
x0, y0 = (w - cw) // 2, (h - ch) // 2
|
|
51
54
|
base = img.crop((x0, y0, x0 + cw, y0 + ch))
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
try:
|
|
57
|
+
for name, (tw, th) in TARGETS.items():
|
|
58
|
+
ratio = tw / th
|
|
59
|
+
bw, bh = base.size
|
|
60
|
+
if bw / bh > ratio:
|
|
61
|
+
cw2, ch2 = int(bh * ratio), bh
|
|
62
|
+
else:
|
|
63
|
+
cw2, ch2 = bw, int(bw / ratio)
|
|
64
|
+
x1, y1 = (bw - cw2) // 2, (bh - ch2) // 2
|
|
65
|
+
out = base.crop((x1, y1, x1 + cw2, y1 + ch2)).resize((tw, th), Image.LANCZOS)
|
|
66
|
+
dest = out_dir / name
|
|
67
|
+
out.save(dest)
|
|
68
|
+
print(f"OK {dest} ({tw}x{th})")
|
|
69
|
+
except OSError as e:
|
|
70
|
+
sys.exit(f"错误:写入封面图失败({e})")
|
|
65
71
|
|
|
66
72
|
|
|
67
73
|
if __name__ == "__main__":
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Full lifecycle for WeChat articles: delivery package, review & rewrite, three quality gates, status sync, WeChat inline typography, and publish checklist.
|
|
6
6
|
|
|
7
|
-
- Version: **v0.1.0** (same tag as `@topmindspace/tms-skills@0.
|
|
7
|
+
- Version: **v0.1.0** (same tag as `@topmindspace/tms-skills@0.4.1`)
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -60,7 +60,7 @@ Output `demo/demo-公众号版.html` (open in a browser → click "复制正文"
|
|
|
60
60
|
|
|
61
61
|
```html
|
|
62
62
|
<!-- h2 auto-numbered; closing chapters like "总结" get ∞ -->
|
|
63
|
-
<h2 ...><span ...>
|
|
63
|
+
<h2 ...><span ...>1</span>Chapter One</h2>
|
|
64
64
|
<!-- images inlined as base64, pasted along with the text -->
|
|
65
65
|
<img src="data:image/png;base64,iVBORw0KGgo..." ...>
|
|
66
66
|
```
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
公众号文章全生命周期:交付包、审校改写、质量三关、状态同步、微信内联排版与发布清单。
|
|
6
6
|
|
|
7
|
-
- 版本:**v0.1.0**(与 `@topmindspace/tms-skills@0.
|
|
7
|
+
- 版本:**v0.1.0**(与 `@topmindspace/tms-skills@0.4.1` 同 tag)
|
|
8
8
|
|
|
9
9
|
## 安装
|
|
10
10
|
|
|
@@ -59,7 +59,7 @@ python3 scripts/md2wechat.py --input demo.md --out-dir demo --slug demo --embed-
|
|
|
59
59
|
|
|
60
60
|
```html
|
|
61
61
|
<!-- 二级标题自动编号;"总结"类末章编号为 ∞ -->
|
|
62
|
-
<h2 ...><span ...>
|
|
62
|
+
<h2 ...><span ...>1</span>第一章</h2>
|
|
63
63
|
<!-- 图片 base64 内嵌,粘贴即带图 -->
|
|
64
64
|
<img src="data:image/png;base64,iVBORw0KGgo..." ...>
|
|
65
65
|
```
|
|
@@ -50,7 +50,7 @@ updated: 2026-09-29
|
|
|
50
50
|
| `scripts/scan_ai_flavor.py` | 中文去 AI 味扫描(与 `qu-aiwei-zh` 同源) |
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
# 路径解析:CLI --base
|
|
53
|
+
# 路径解析:CLI --base → env TOPMIND_WECHAT_BASE / TOPMIND_WORKSPACE / TOPSTREAM_ROOT → 惯例
|
|
54
54
|
export TOPMIND_WORKSPACE=/path/to/workspace # 推荐
|
|
55
55
|
python3 scripts/new-article.py --slug demo --title "标题" --direction reverse
|
|
56
56
|
python3 scripts/lint-wechat.py --input <包>/公众号稿.md --fix
|
|
@@ -143,7 +143,7 @@ LANG_RULES = {
|
|
|
143
143
|
],
|
|
144
144
|
"yaml": [
|
|
145
145
|
("comment", r"#[^\n]*"),
|
|
146
|
-
("key", r"
|
|
146
|
+
("key", r"^\s*[\w.\-/]+(?=\s*:)"),
|
|
147
147
|
("string", r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\''),
|
|
148
148
|
("keyword", r"\b(?:true|false|null|yes|no|on|off)\b"),
|
|
149
149
|
("number", r"\b\d+\.?\d*\b"),
|
|
@@ -157,9 +157,9 @@ LANG_RULES = {
|
|
|
157
157
|
("op", r"[=<>!*/+\-]+"),
|
|
158
158
|
],
|
|
159
159
|
"diff": [
|
|
160
|
-
("meta", r"
|
|
161
|
-
("add", r"
|
|
162
|
-
("del", r"
|
|
160
|
+
("meta", r"^@@[^\n]*|^---[^\n]*|^\+\+\+[^\n]*"),
|
|
161
|
+
("add", r"^\+[^\n]*"),
|
|
162
|
+
("del", r"^-[^\n]*"),
|
|
163
163
|
],
|
|
164
164
|
"html": [
|
|
165
165
|
("comment", r"<!--[\s\S]*?-->"),
|
|
@@ -219,7 +219,10 @@ def highlight(code, lang, scheme=None):
|
|
|
219
219
|
palette = scheme or CTX.get("hl_scheme") or HIGHLIGHT
|
|
220
220
|
pattern = _HL_PATTERN_CACHE.get(key)
|
|
221
221
|
if pattern is None:
|
|
222
|
-
|
|
222
|
+
# (?m) 不能写进单个分支:拼成 alternation 后它就不在表达式开头了,
|
|
223
|
+
# Python 会报 "global flags not at the start"(历史 crash:```yaml / ```diff
|
|
224
|
+
# 代码块直接 Traceback)。MULTILINE 在编译期统一加,分支里只留 ^。
|
|
225
|
+
pattern = re.compile("|".join("(?P<%s>%s)" % (name, pat) for name, pat in rules), re.S | re.M)
|
|
223
226
|
_HL_PATTERN_CACHE[key] = pattern
|
|
224
227
|
out = []
|
|
225
228
|
pos = 0
|
|
@@ -294,8 +297,12 @@ def load_theme(spec):
|
|
|
294
297
|
path = resolve_theme(spec)
|
|
295
298
|
if not path:
|
|
296
299
|
return base
|
|
297
|
-
|
|
298
|
-
|
|
300
|
+
try:
|
|
301
|
+
with open(path, encoding="utf-8") as fh:
|
|
302
|
+
data = json.load(fh)
|
|
303
|
+
except (OSError, ValueError) as e:
|
|
304
|
+
# ValueError 覆盖 json.JSONDecodeError:坏掉的主题文件给干净报错,不抛 Traceback
|
|
305
|
+
sys.exit("错误:主题文件无法解析 %s(%s)" % (path, e))
|
|
299
306
|
accent = data.get("accent", base["accent"])
|
|
300
307
|
# 主色派生字段必须按「本主题」的主色重算,不能继承默认主题的值:
|
|
301
308
|
# 默认主题的 accent_light / mark_bg 是砖红系,继承过来会让蓝色或灰色主题里
|
|
@@ -341,8 +348,67 @@ PANGU_B = re.compile(r"([A-Za-z0-9])([%s])" % HAN)
|
|
|
341
348
|
# render_inline() 的行内模式:预编译在模块级,避免每行重复走 re 缓存查找
|
|
342
349
|
# (666k 行级调用 × 10 个模式时,re.sub 的字符串模式查找是可观开销)。
|
|
343
350
|
INLINE_CODE_RE = re.compile(r"`([^`]+)`")
|
|
344
|
-
|
|
345
|
-
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _parse_paren(s, j):
|
|
354
|
+
"""s[j] == '(' 时找与之匹配的 ')'(括号可嵌套:URL 里允许出现平衡括号,如维基链接)。
|
|
355
|
+
|
|
356
|
+
返回 (括号内文本, 右括号之后的位置);括号不平衡返回 None。
|
|
357
|
+
"""
|
|
358
|
+
depth = 0
|
|
359
|
+
k, n = j, len(s)
|
|
360
|
+
while k < n:
|
|
361
|
+
c = s[k]
|
|
362
|
+
if c == "(":
|
|
363
|
+
depth += 1
|
|
364
|
+
elif c == ")":
|
|
365
|
+
depth -= 1
|
|
366
|
+
if depth == 0:
|
|
367
|
+
return s[j + 1:k], k + 1
|
|
368
|
+
k += 1
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# `(url)` / `(url "title")` / `(url 'title')` / `(url (title))` → url(title 丢弃)。
|
|
373
|
+
_URL_TITLE_RE = re.compile(r'^(\S+?)(?:\s+(?:"[^"]*"|\'[^\']*\'|\([^()]*\)))?\s*$')
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _split_url_title(inner):
|
|
377
|
+
"""把链接/图片括号内的 `url "title"` 拆出 url(title 丢弃)。
|
|
378
|
+
|
|
379
|
+
裸 url 不得含空白:含裸空白时返回 ""(调用方保留原文,
|
|
380
|
+
与旧正则 `[^)\\s]+` 的行为一致,不静默吞掉)。
|
|
381
|
+
"""
|
|
382
|
+
m = _URL_TITLE_RE.match(inner.strip())
|
|
383
|
+
return m.group(1) if m else ""
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _scan_md_links(text, img, repl):
|
|
387
|
+
"""扫描替换行内图片/链接,url 括号支持嵌套、可带 "title"。
|
|
388
|
+
|
|
389
|
+
img=True 扫 ,False 扫 [label](url);repl(文本, url) -> 替换串。
|
|
390
|
+
括号不平衡、url 为空、url 含裸空白时保留原文(旧正则同样认不出这些写法,
|
|
391
|
+
保留原文比截断 URL 更安全——历史 bug:`.png)` 被截成 `img(1`,
|
|
392
|
+
文件明明存在却被记成缺失;`[维基](…/猫_(动物))` 的 URL 被截断、正文多个 `)`)。
|
|
393
|
+
"""
|
|
394
|
+
open_tok = "![" if img else "["
|
|
395
|
+
skip = len(open_tok)
|
|
396
|
+
res = []
|
|
397
|
+
i, n = 0, len(text)
|
|
398
|
+
while i < n:
|
|
399
|
+
if text.startswith(open_tok, i):
|
|
400
|
+
k = text.find("]", i + skip)
|
|
401
|
+
if k != -1 and k + 1 < n and text[k + 1] == "(":
|
|
402
|
+
parsed = _parse_paren(text, k + 1)
|
|
403
|
+
if parsed:
|
|
404
|
+
url = _split_url_title(parsed[0])
|
|
405
|
+
if url:
|
|
406
|
+
res.append(repl(text[i + skip:k], url))
|
|
407
|
+
i = parsed[1]
|
|
408
|
+
continue
|
|
409
|
+
res.append(text[i])
|
|
410
|
+
i += 1
|
|
411
|
+
return "".join(res)
|
|
346
412
|
INLINE_BADGE_RE = re.compile(r"\[!([^\]]+)\]")
|
|
347
413
|
INLINE_HL_RE = re.compile(r"==([^=]+)==")
|
|
348
414
|
INLINE_UNDER_RE = re.compile(r"\+\+([^+]+)\+\+")
|
|
@@ -390,9 +456,8 @@ def render_inline(raw):
|
|
|
390
456
|
strong = 'font-weight:600;color:%s;' % theme["text_strong"]
|
|
391
457
|
em = 'font-style:normal;color:%s;' % theme["text_muted"]
|
|
392
458
|
|
|
393
|
-
# images
|
|
394
|
-
def img_repl(
|
|
395
|
-
alt, src = m.group(1), m.group(2)
|
|
459
|
+
# images(行内图与独立成行图共用同一括号语义)
|
|
460
|
+
def img_repl(alt, src):
|
|
396
461
|
# 行内图片同样走图片管线:本地文件解析 → 复制到 images/ → 登记清单,
|
|
397
462
|
# 否则 --embed-images 找不到文件、清单漏项、计数为 0(历史 bug:
|
|
398
463
|
# 段落里的  被原样保留相对路径,粘贴到公众号后整图消失)。
|
|
@@ -403,11 +468,13 @@ def render_inline(raw):
|
|
|
403
468
|
return ('<img src="%s"%s style="max-width:100%%;height:auto;'
|
|
404
469
|
'border-radius:3px;vertical-align:middle;">') % (src, caption)
|
|
405
470
|
|
|
406
|
-
text =
|
|
471
|
+
text = _scan_md_links(text, True, img_repl)
|
|
407
472
|
|
|
408
473
|
# links
|
|
409
|
-
def link_repl(
|
|
410
|
-
|
|
474
|
+
def link_repl(label, url):
|
|
475
|
+
# 入口处已 esc():先还原,插入点与脚注登记处再按需转义。
|
|
476
|
+
# 否则脚注列表里 URL 的 & 会被 esc 两次、显示成 &(历史 bug)。
|
|
477
|
+
label, url = html.unescape(label), html.unescape(url)
|
|
411
478
|
if link_mode == "footnote" and footnotes is not None:
|
|
412
479
|
# url→序号字典:原写法每次链接都扫全表两次(O(n²)),长文链接多时明显变慢
|
|
413
480
|
fidx = CTX.get("footnote_idx")
|
|
@@ -423,14 +490,14 @@ def render_inline(raw):
|
|
|
423
490
|
idx = [f["url"] for f in footnotes].index(url) + 1
|
|
424
491
|
return ('%s<sup style="font-size:11px;color:%s;'
|
|
425
492
|
'padding-left:1px;">[%d]</sup>'
|
|
426
|
-
% (label, theme["accent"], idx))
|
|
493
|
+
% (esc(label), theme["accent"], idx))
|
|
427
494
|
if link_mode == "inline":
|
|
428
495
|
return ('%s<span style="font-size:13px;color:%s;">(%s)</span>'
|
|
429
|
-
% (label, theme["text_light"], url))
|
|
496
|
+
% (esc(label), theme["text_light"], esc(url)))
|
|
430
497
|
return ('<span style="font-size:13px;color:%s;word-break:break-all;">'
|
|
431
|
-
'%s(%s)</span>' % (theme["text_light"], label, url))
|
|
498
|
+
'%s(%s)</span>' % (theme["text_light"], esc(label), esc(url)))
|
|
432
499
|
|
|
433
|
-
text =
|
|
500
|
+
text = _scan_md_links(text, False, link_repl)
|
|
434
501
|
|
|
435
502
|
# badges: [!文本]
|
|
436
503
|
# 不用 display:inline-block:公众号对非常规 display 值支持不稳定,
|
|
@@ -483,9 +550,33 @@ def render_inline(raw):
|
|
|
483
550
|
# ---------------------------------------------------------------- blocks
|
|
484
551
|
|
|
485
552
|
HEADING_RE = re.compile(r"^(#{1,6})\s+(.*)$")
|
|
486
|
-
|
|
553
|
+
# 围栏反引号数 >= 3:```` 开栏只能由 >= 4 反引号闭合(CommonMark),
|
|
554
|
+
# 否则 markdown 示例代码里的内层 ``` 会把外层栏提前"闭合",造成静默错排。
|
|
555
|
+
FENCE_RE = re.compile(r"^(`{3,})(\w*)\s*$")
|
|
487
556
|
HR_RE = re.compile(r"^(-{3,}|\*{3,}|_{3,})\s*$")
|
|
488
|
-
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def match_img_only(line):
|
|
560
|
+
"""独立成行图片: 独占一行(src 括号可嵌套、可带 title)。
|
|
561
|
+
|
|
562
|
+
返回 (alt, src),不是独立成行图片返回 None。
|
|
563
|
+
与行内图片共用同一括号语义(_parse_paren + _split_url_title)。
|
|
564
|
+
"""
|
|
565
|
+
if not line.startswith("!["):
|
|
566
|
+
return None
|
|
567
|
+
k = line.find("]", 2)
|
|
568
|
+
if k == -1:
|
|
569
|
+
return None
|
|
570
|
+
j = k + 1
|
|
571
|
+
if j >= len(line) or line[j] != "(":
|
|
572
|
+
return None
|
|
573
|
+
parsed = _parse_paren(line, j)
|
|
574
|
+
if not parsed:
|
|
575
|
+
return None
|
|
576
|
+
src = _split_url_title(parsed[0])
|
|
577
|
+
if not src or line[parsed[1]:].strip():
|
|
578
|
+
return None
|
|
579
|
+
return line[2:k], src
|
|
489
580
|
CAPTION_RE = re.compile(r"^\*(.+)\*\s*$")
|
|
490
581
|
TABLE_SEP_RE = re.compile(r"^\|?[\s:|-]+\|[\s:|-]*$")
|
|
491
582
|
CONTAINER_RE = re.compile(r"^:::+\s*(\w+)?\s*(.*)$")
|
|
@@ -920,7 +1011,10 @@ def resolve_image(src, md_path, out_dir, assets, asset_root):
|
|
|
920
1011
|
"missing": True})
|
|
921
1012
|
return src
|
|
922
1013
|
img_dir = os.path.join(out_dir, "images")
|
|
923
|
-
|
|
1014
|
+
try:
|
|
1015
|
+
os.makedirs(img_dir, exist_ok=True)
|
|
1016
|
+
except OSError as e:
|
|
1017
|
+
sys.exit("错误:无法创建图片目录 %s(%s)" % (img_dir, e))
|
|
924
1018
|
base = os.path.basename(abs_path)
|
|
925
1019
|
target = os.path.join(img_dir, base)
|
|
926
1020
|
n = 1
|
|
@@ -929,7 +1023,10 @@ def resolve_image(src, md_path, out_dir, assets, asset_root):
|
|
|
929
1023
|
target = os.path.join(img_dir, "%s-%d%s" % (stem, n, ext))
|
|
930
1024
|
n += 1
|
|
931
1025
|
if not os.path.exists(target):
|
|
932
|
-
|
|
1026
|
+
try:
|
|
1027
|
+
shutil.copy2(abs_path, target)
|
|
1028
|
+
except OSError as e:
|
|
1029
|
+
sys.exit("错误:复制图片失败 %s → %s(%s)" % (abs_path, target, e))
|
|
933
1030
|
rel = os.path.relpath(target, out_dir)
|
|
934
1031
|
assets.append({"src": src, "local": rel, "remote": False,
|
|
935
1032
|
"basename": os.path.basename(target),
|
|
@@ -1002,10 +1099,20 @@ def convert_blocks(lines, md_path, out_dir, theme, ctx):
|
|
|
1002
1099
|
|
|
1003
1100
|
m = FENCE_RE.match(stripped)
|
|
1004
1101
|
if m:
|
|
1005
|
-
|
|
1102
|
+
open_len = len(m.group(1))
|
|
1103
|
+
lang = m.group(2)
|
|
1006
1104
|
i += 1
|
|
1007
1105
|
buf = []
|
|
1008
|
-
while i < n
|
|
1106
|
+
while i < n:
|
|
1107
|
+
s2 = lines[i].strip()
|
|
1108
|
+
k = 0
|
|
1109
|
+
while k < len(s2) and s2[k] == "`":
|
|
1110
|
+
k += 1
|
|
1111
|
+
# 闭合栏:反引号数 >= 开栏数且后面无其它字符(闭合栏可比开栏长)。
|
|
1112
|
+
# 带信息串的 ```python 不是闭合栏,是栏内代码——旧写法按
|
|
1113
|
+
# startswith("```") 判闭合,会把栏提前关掉。
|
|
1114
|
+
if k >= open_len and not s2[k:].strip():
|
|
1115
|
+
break
|
|
1009
1116
|
buf.append(lines[i])
|
|
1010
1117
|
i += 1
|
|
1011
1118
|
i += 1
|
|
@@ -1080,7 +1187,7 @@ def convert_blocks(lines, md_path, out_dir, theme, ctx):
|
|
|
1080
1187
|
out.append(block_html)
|
|
1081
1188
|
continue
|
|
1082
1189
|
|
|
1083
|
-
m =
|
|
1190
|
+
m = match_img_only(stripped)
|
|
1084
1191
|
if m:
|
|
1085
1192
|
caption = None
|
|
1086
1193
|
if i + 1 < n:
|
|
@@ -1088,9 +1195,9 @@ def convert_blocks(lines, md_path, out_dir, theme, ctx):
|
|
|
1088
1195
|
if cm:
|
|
1089
1196
|
caption = cm.group(1)
|
|
1090
1197
|
i += 1
|
|
1091
|
-
src = resolve_image(m
|
|
1198
|
+
src = resolve_image(m[1], md_path, out_dir,
|
|
1092
1199
|
ctx["assets"], ctx["asset_root"])
|
|
1093
|
-
out.append(render_figure(src, caption, theme, alt=m
|
|
1200
|
+
out.append(render_figure(src, caption, theme, alt=m[0]))
|
|
1094
1201
|
i += 1
|
|
1095
1202
|
continue
|
|
1096
1203
|
|
|
@@ -1101,7 +1208,7 @@ def convert_blocks(lines, md_path, out_dir, theme, ctx):
|
|
|
1101
1208
|
if (not cur or HEADING_RE.match(cur) or HR_RE.match(cur)
|
|
1102
1209
|
or CONTAINER_RE.match(cur) or cur.startswith(">")
|
|
1103
1210
|
or cur.startswith("|") or is_list_item(lines[i])
|
|
1104
|
-
or
|
|
1211
|
+
or match_img_only(cur) or FENCE_RE.match(cur)):
|
|
1105
1212
|
break
|
|
1106
1213
|
buf.append(cur)
|
|
1107
1214
|
i += 1
|
|
@@ -1597,6 +1704,8 @@ def main():
|
|
|
1597
1704
|
if missing:
|
|
1598
1705
|
sys.exit("缺少必填参数:%s" % "、".join(missing))
|
|
1599
1706
|
|
|
1707
|
+
if os.path.isdir(args.input):
|
|
1708
|
+
sys.exit("错误:输入是目录不是文件 %s" % args.input)
|
|
1600
1709
|
if not os.path.isfile(args.input):
|
|
1601
1710
|
sys.exit("错误:找不到输入文件 %s" % args.input)
|
|
1602
1711
|
|
|
@@ -1609,7 +1718,10 @@ def main():
|
|
|
1609
1718
|
except OSError as e:
|
|
1610
1719
|
sys.exit("错误:无法读取输入文件 %s(%s)" % (args.input, e))
|
|
1611
1720
|
|
|
1612
|
-
|
|
1721
|
+
try:
|
|
1722
|
+
os.makedirs(args.out_dir, exist_ok=True)
|
|
1723
|
+
except OSError as e:
|
|
1724
|
+
sys.exit("错误:无法创建输出目录 %s(%s)" % (args.out_dir, e))
|
|
1613
1725
|
body, assets, diagrams, footnotes = convert(
|
|
1614
1726
|
md_text, os.path.abspath(args.input), args.out_dir, theme,
|
|
1615
1727
|
args.link_mode, args.render_mermaid, args.asset_root or None,
|
|
@@ -87,6 +87,80 @@ def main() -> None:
|
|
|
87
87
|
check("md2wechat 分隔线无 inline-block 告警",
|
|
88
88
|
"inline-block" not in r.stdout, r.stdout[:200])
|
|
89
89
|
|
|
90
|
+
# 9. 括号 URL 不被截断(链接与图片;历史 bug:`.png)` 被截成
|
|
91
|
+
# `img(1` 导致真实文件记成缺失;`[维基](…/猫_(动物))` 的 URL 被截断、
|
|
92
|
+
# 正文多个悬挂 `)`;`[t](url "title")` 被原样保留)
|
|
93
|
+
(td / "img(1).png").write_bytes(_b64.b64decode(png))
|
|
94
|
+
(td / "paren.md").write_text(
|
|
95
|
+
"# t\n\n.png)\n\n"
|
|
96
|
+
"[维基](https://zh.wikipedia.org/wiki/猫_(动物)) 与 "
|
|
97
|
+
"[带title](https://example.com/a \"标题文字\")。\n",
|
|
98
|
+
encoding="utf-8")
|
|
99
|
+
r = run("md2wechat.py", ["--input", str(td / "paren.md"),
|
|
100
|
+
"--out-dir", str(td / "o9"), "--slug", "t9",
|
|
101
|
+
"--no-toc", "--signature", "off"])
|
|
102
|
+
html = (td / "o9" / "t9-公众号版.html").read_text(encoding="utf-8")
|
|
103
|
+
check("md2wechat 括号图片不截断",
|
|
104
|
+
'src="images/img(1).png"' in html and (td / "o9" / "images" / "img(1).png").exists(),
|
|
105
|
+
r.stdout[:200])
|
|
106
|
+
check("md2wechat 括号链接不截断",
|
|
107
|
+
"猫_(动物)" in html and ")</sup>" not in html,
|
|
108
|
+
r.stdout[:200])
|
|
109
|
+
check("md2wechat title 链接被转换",
|
|
110
|
+
"https://example.com/a" in html and '[带title](' not in html,
|
|
111
|
+
r.stdout[:200])
|
|
112
|
+
|
|
113
|
+
# 10. 脚注 URL 的 & 不双重转义(历史 bug:显示成 &)
|
|
114
|
+
(td / "amp.md").write_text(
|
|
115
|
+
"# t\n\n[和号](https://x.com/?a=1&b=2)\n", encoding="utf-8")
|
|
116
|
+
r = run("md2wechat.py", ["--input", str(td / "amp.md"),
|
|
117
|
+
"--out-dir", str(td / "o10"), "--slug", "t10",
|
|
118
|
+
"--no-toc", "--signature", "off"])
|
|
119
|
+
html = (td / "o10" / "t10-公众号版.html").read_text(encoding="utf-8")
|
|
120
|
+
check("md2wechat 脚注 URL 不双重转义",
|
|
121
|
+
"a=1&b=2" in html and "&amp;" not in html)
|
|
122
|
+
|
|
123
|
+
# 11. 坏主题 JSON 干净报错(历史 crash:json Traceback)
|
|
124
|
+
(td / "bad-theme.json").write_text("{bad json", encoding="utf-8")
|
|
125
|
+
(td / "t11.md").write_text("# t\n\n正文。\n", encoding="utf-8")
|
|
126
|
+
r = run("md2wechat.py", ["--input", str(td / "t11.md"),
|
|
127
|
+
"--out-dir", str(td / "o11"), "--slug", "t11",
|
|
128
|
+
"--theme", str(td / "bad-theme.json")])
|
|
129
|
+
check("md2wechat 坏主题 JSON 干净报错",
|
|
130
|
+
r.returncode != 0 and "Traceback" not in r.stderr, r.stderr[:100])
|
|
131
|
+
|
|
132
|
+
# 12. 目录当输入:文案明确(与 md2x 一致),非零干净退出
|
|
133
|
+
r = run("md2wechat.py", ["--input", str(td),
|
|
134
|
+
"--out-dir", str(td / "o12"), "--slug", "t12"])
|
|
135
|
+
check("md2wechat 目录输入文案正确",
|
|
136
|
+
r.returncode != 0 and "输入是目录不是文件" in r.stderr
|
|
137
|
+
and "Traceback" not in r.stderr, r.stderr[:100])
|
|
138
|
+
|
|
139
|
+
# 13. yaml/diff 代码块不高亮 crash(历史 crash:(?m) 拼进 alternation
|
|
140
|
+
# 报 "global flags not at the start",任何 ```yaml 块都 Traceback)
|
|
141
|
+
(td / "hl.md").write_text(
|
|
142
|
+
"# t\n\n```yaml\nkey: value\n```\n\n```diff\n+add\n-del\n```\n",
|
|
143
|
+
encoding="utf-8")
|
|
144
|
+
r = run("md2wechat.py", ["--input", str(td / "hl.md"),
|
|
145
|
+
"--out-dir", str(td / "o13"), "--slug", "t13"])
|
|
146
|
+
html = (td / "o13" / "t13-公众号版.html").read_text(encoding="utf-8")
|
|
147
|
+
check("md2wechat yaml/diff 代码块不崩溃",
|
|
148
|
+
r.returncode == 0 and "Traceback" not in r.stderr
|
|
149
|
+
and "key" in html, r.stderr[:100])
|
|
150
|
+
|
|
151
|
+
# 14. 嵌套围栏:```` 开栏不被内层 ``` 提前闭合
|
|
152
|
+
(td / "nest.md").write_text(
|
|
153
|
+
"# t\n\n````markdown\n外层\n```python\nprint(1)\n```\n结束\n````\n",
|
|
154
|
+
encoding="utf-8")
|
|
155
|
+
r = run("md2wechat.py", ["--input", str(td / "nest.md"),
|
|
156
|
+
"--out-dir", str(td / "o14"), "--slug", "t14",
|
|
157
|
+
"--no-toc", "--signature", "off"])
|
|
158
|
+
html = (td / "o14" / "t14-公众号版.html").read_text(encoding="utf-8")
|
|
159
|
+
body = html.split('<article id="wx-body"')[1]
|
|
160
|
+
check("md2wechat 嵌套围栏不提前闭合",
|
|
161
|
+
r.returncode == 0 and "````markdown" not in body
|
|
162
|
+
and body.count("MARKDOWN</section>") == 1, r.stderr[:100])
|
|
163
|
+
|
|
90
164
|
if fails:
|
|
91
165
|
print(f"\n[negative_tests] {len(fails)} 项失败")
|
|
92
166
|
sys.exit(1)
|
|
@@ -193,6 +193,7 @@ def convert(md: str):
|
|
|
193
193
|
defs = {}
|
|
194
194
|
out_lines = []
|
|
195
195
|
in_code = False
|
|
196
|
+
fence_len = 0
|
|
196
197
|
|
|
197
198
|
md, esc_store = _protect_escapes(md)
|
|
198
199
|
md = _strip_frontmatter(md)
|
|
@@ -209,9 +210,23 @@ def convert(md: str):
|
|
|
209
210
|
for raw in body:
|
|
210
211
|
line = raw.rstrip()
|
|
211
212
|
|
|
212
|
-
#
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
# 代码块:先处理围栏,块内行原样缩进(分割线规则不进代码块)。
|
|
214
|
+
# 围栏反引号数 >= 3:```` 开栏只能由 >= 4 反引号闭合(CommonMark),
|
|
215
|
+
# 否则 markdown 示例里的内层 ``` 会把外层栏提前"闭合",造成静默错排。
|
|
216
|
+
stripped = line.strip()
|
|
217
|
+
if stripped.startswith("```"):
|
|
218
|
+
k = 0
|
|
219
|
+
while k < len(stripped) and stripped[k] == "`":
|
|
220
|
+
k += 1
|
|
221
|
+
if not in_code:
|
|
222
|
+
in_code = True
|
|
223
|
+
fence_len = k
|
|
224
|
+
elif k >= fence_len and not stripped[k:].strip():
|
|
225
|
+
in_code = False
|
|
226
|
+
fence_len = 0
|
|
227
|
+
else:
|
|
228
|
+
# 栏内误写的短栏 / 带信息串的栏是代码内容,不是闭合栏
|
|
229
|
+
out_lines.append(" " + line)
|
|
215
230
|
continue
|
|
216
231
|
if in_code:
|
|
217
232
|
out_lines.append(" " + line)
|
|
@@ -281,6 +296,8 @@ def main() -> None:
|
|
|
281
296
|
|
|
282
297
|
inp = Path(args.input)
|
|
283
298
|
if not inp.is_file():
|
|
299
|
+
if inp.is_dir():
|
|
300
|
+
sys.exit(f"错误:输入是目录不是文件 {inp}")
|
|
284
301
|
sys.exit(f"错误:找不到输入文件 {inp}")
|
|
285
302
|
try:
|
|
286
303
|
md = inp.read_text(encoding="utf-8-sig")
|
|
@@ -291,8 +308,11 @@ def main() -> None:
|
|
|
291
308
|
text, images = convert(md)
|
|
292
309
|
|
|
293
310
|
out = Path(args.out)
|
|
294
|
-
|
|
295
|
-
|
|
311
|
+
try:
|
|
312
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
313
|
+
out.write_text(text, encoding="utf-8")
|
|
314
|
+
except OSError as e:
|
|
315
|
+
sys.exit(f"错误:无法写入输出文件 {out}({e})")
|
|
296
316
|
|
|
297
317
|
chars = len(text.replace("\n", ""))
|
|
298
318
|
print(f"OK {out}")
|
|
@@ -64,6 +64,23 @@ def main() -> None:
|
|
|
64
64
|
out = (td / "o6.txt").read_text(encoding="utf-8") if (td / "o6.txt").exists() else ""
|
|
65
65
|
check("正常输入含配图清单", r.returncode == 0 and "[图1]" in out)
|
|
66
66
|
|
|
67
|
+
# 7. 嵌套围栏:```` 开栏不被内层 ``` 提前闭合(内层栏是代码内容)
|
|
68
|
+
(td / "nest.md").write_text(
|
|
69
|
+
"# t\n\n````markdown\n外层\n```python\nprint(1)\n```\n结束\n````\n",
|
|
70
|
+
encoding="utf-8")
|
|
71
|
+
r = run([str(td / "nest.md"), "--out", str(td / "o7.txt")])
|
|
72
|
+
out = (td / "o7.txt").read_text(encoding="utf-8") if (td / "o7.txt").exists() else ""
|
|
73
|
+
check("嵌套围栏不提前闭合",
|
|
74
|
+
r.returncode == 0 and " ```python" in out and "print(1)" in out)
|
|
75
|
+
|
|
76
|
+
# 8. 括号 URL 链接不断裂(与 md2wechat 对齐的守卫)
|
|
77
|
+
(td / "paren.md").write_text(
|
|
78
|
+
"# t\n\n[维基](https://zh.wikipedia.org/wiki/猫_(动物))\n", encoding="utf-8")
|
|
79
|
+
r = run([str(td / "paren.md"), "--out", str(td / "o8.txt")])
|
|
80
|
+
out = (td / "o8.txt").read_text(encoding="utf-8") if (td / "o8.txt").exists() else ""
|
|
81
|
+
check("括号 URL 不截断",
|
|
82
|
+
r.returncode == 0 and "猫_(动物)" in out)
|
|
83
|
+
|
|
67
84
|
if fails:
|
|
68
85
|
print(f"\n[negative_tests] {len(fails)} 项失败")
|
|
69
86
|
sys.exit(1)
|