@topmindspace/tms-skills 0.1.19 → 0.2.0

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.en.md +5 -2
  3. package/README.md +4 -1
  4. package/package.json +5 -2
  5. package/topmind-cover/README.md +40 -0
  6. package/topmind-cover/SKILL.md +66 -0
  7. package/topmind-cover/package-lock.json +16 -0
  8. package/topmind-cover/package.json +16 -0
  9. package/topmind-cover/references/cover-styles.md +40 -0
  10. package/topmind-cover/scripts/audit_css.py +39 -0
  11. package/topmind-cover/scripts/audit_docs.py +58 -0
  12. package/topmind-cover/scripts/audit_skill.py +60 -0
  13. package/topmind-cover/scripts/audit_styles.py +54 -0
  14. package/topmind-cover/scripts/crop-cover.py +69 -0
  15. package/topmind-cover/scripts/negative_tests.py +71 -0
  16. package/topmind-cover/scripts/package_skill.py +188 -0
  17. package/topmind-wechat-post/README.md +49 -0
  18. package/topmind-wechat-post/SKILL.md +208 -0
  19. package/topmind-wechat-post/assets/themes/amber-review.json +35 -0
  20. package/topmind-wechat-post/assets/themes/graphite.json +35 -0
  21. package/topmind-wechat-post/assets/themes/minimal-ink.json +35 -0
  22. package/topmind-wechat-post/assets/themes/newsprint.json +35 -0
  23. package/topmind-wechat-post/assets/themes/tech-blue.json +35 -0
  24. package/topmind-wechat-post/package-lock.json +16 -0
  25. package/topmind-wechat-post/package.json +16 -0
  26. package/topmind-wechat-post/references/design.md +50 -0
  27. package/topmind-wechat-post/references/element-spec.md +162 -0
  28. package/topmind-wechat-post/references/known-pits.md +86 -0
  29. package/topmind-wechat-post/references/theme-map.md +139 -0
  30. package/topmind-wechat-post/references/typography-rules.md +62 -0
  31. package/topmind-wechat-post/references/wechat-constraints.md +106 -0
  32. package/topmind-wechat-post/references/workflow.md +102 -0
  33. package/topmind-wechat-post/references/writing-quality.md +129 -0
  34. package/topmind-wechat-post/scripts/audit_css.py +39 -0
  35. package/topmind-wechat-post/scripts/audit_docs.py +58 -0
  36. package/topmind-wechat-post/scripts/audit_skill.py +60 -0
  37. package/topmind-wechat-post/scripts/audit_styles.py +54 -0
  38. package/topmind-wechat-post/scripts/lint-wechat.py +446 -0
  39. package/topmind-wechat-post/scripts/md2wechat.py +1602 -0
  40. package/topmind-wechat-post/scripts/negative_tests.py +65 -0
  41. package/topmind-wechat-post/scripts/new-article.py +183 -0
  42. package/topmind-wechat-post/scripts/package_skill.py +188 -0
  43. package/topmind-wechat-post/scripts/push-to-topstream.py +226 -0
  44. package/topmind-wechat-post/scripts/scan_ai_flavor.py +464 -0
  45. package/topmind-wechat-post/scripts/sync-mapping.py +220 -0
  46. package/topmind-wechat-post/scripts/sync-status.py +187 -0
  47. package/topmind-x-article/README.md +40 -0
  48. package/topmind-x-article/SKILL.md +62 -0
  49. package/topmind-x-article/package-lock.json +16 -0
  50. package/topmind-x-article/package.json +16 -0
  51. package/topmind-x-article/references/publish-checklist.md +24 -0
  52. package/topmind-x-article/references/x-format.md +24 -0
  53. package/topmind-x-article/scripts/audit_css.py +39 -0
  54. package/topmind-x-article/scripts/audit_docs.py +58 -0
  55. package/topmind-x-article/scripts/audit_skill.py +60 -0
  56. package/topmind-x-article/scripts/audit_styles.py +54 -0
  57. package/topmind-x-article/scripts/md2x.py +121 -0
  58. package/topmind-x-article/scripts/negative_tests.py +74 -0
  59. package/topmind-x-article/scripts/package_skill.py +188 -0
@@ -0,0 +1,446 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """lint-wechat v2 — 公众号排版体检。
4
+
5
+ Checks a Markdown draft against WeChat MP readability constraints and
6
+ Chinese typography conventions. Zero dependency.
7
+
8
+ Usage:
9
+ python3 lint-wechat.py --input 稿.md --asset-root /path/to/repo
10
+ python3 lint-wechat.py --input 稿.md --fix # auto-fix CJK spacing
11
+ """
12
+
13
+ import argparse
14
+ import os
15
+ import re
16
+ import sys
17
+
18
+ FENCE_RE = re.compile(r"^```(\w*)\s*$")
19
+ HEADING_RE = re.compile(r"^(#{1,6})\s+(.*)$")
20
+ IMG_RE = re.compile(r"!\[([^\]]*)\]\(([^)\s]+)")
21
+ LINK_RE = re.compile(r"(?<!!)\[([^\]]+)\]\(([^)\s]+)\)")
22
+ CONTAINER_RE = re.compile(r"^:::+\s*(\w+)?")
23
+ CJK = re.compile(r"[\u4e00-\u9fff]")
24
+
25
+ # 与 md2wechat.py 的 CONTAINER_KINDS 保持一致,外加 toc/sign 两个结构容器。
26
+ # 拼错容器名会被静默降级成 note,这里显式报出来。
27
+ KNOWN_CONTAINERS = {
28
+ "note", "info", "memo", "tip", "success", "ok",
29
+ "warn", "warning", "danger", "error",
30
+ "pull", "quote", "golden", "dialogue", "chat", "talk",
31
+ "stat", "data", "metric",
32
+ "toc", "目录", "导读", "sign", "signature", "签名",
33
+ }
34
+ HAN = r"\u4e00-\u9fff\u3400-\u4dbf"
35
+ PANGU_A = re.compile(r"([%s])([A-Za-z0-9])" % HAN)
36
+ PANGU_B = re.compile(r"([A-Za-z0-9])([%s])" % HAN)
37
+
38
+ # AI 腔 / 八股表达
39
+ AI_TONE = [
40
+ (r"综上所述|总而言之|值得(注意|一提|记录|关注|单独)|不难看出|众所周知|显而易见", "八股过渡词"),
41
+ (r"随着[^,。]{0,8}的发展|在当今[^,。]{0,6}(时代|社会)", "时代背景式开场"),
42
+ (r"不仅仅?是[^,。]{0,12},?更是", "「不仅是…更是」排比"),
43
+ (r"不是[^,。]{1,12},不是[^,。]{1,12},而是", "三段式排比"),
44
+ # 注意:不收「口径」(数据校验语境的官方口径/社区口径)与「对齐」(alignment),属专业用法
45
+ (r"赋能|抓手|打法|颗粒度|底层逻辑|组合拳|护城河|生态位|闭环|沉淀(?![物池])|赛道|倒逼|复盘|卡位|生态化反", "互联网黑话"),
46
+ (r"本质上|事实上|意味着|由此可见|换句话说|换言之|殊途同归|更进一步说", "AI 高频连接词"),
47
+ (r"^(#|##|###)?\s*\*\*[^\*]{8,60}\*\*[::]", "标题式加粗金句"),
48
+ (r"(极其|非常|大大|十分|特别)(轻巧|优秀|出色|明显|强大)", "副词堆叠"),
49
+ ]
50
+
51
+ # 段末加粗金句:整段被加粗且以句号结尾(AI 体典型收尾)
52
+ AI_BOLD_PUNCHLINE = re.compile(r"[。!?]\s*\*\*[^\n\*]{10,120}\*\*[。!?]?\s*$")
53
+
54
+ MAX_IMG_MB = 2.0
55
+ MAX_IMG_HARD_MB = 5.0
56
+
57
+
58
+ MD_LINK_RE = re.compile(r"!?\[([^\]]*)\]\([^)\s]*\)")
59
+ BARE_URL_RE = re.compile(r"https?://[^\s)\]|>]+")
60
+
61
+
62
+ def visual_len(text):
63
+ """读者实际看到的视觉长度。
64
+
65
+ 关键点:URL 不算字数。链接只留显示文字——否则一个 60 字符的 URL 会把
66
+ 一个 40 字的清单项撑成「100 字超标」,链接速查章节会整片误报。
67
+ """
68
+ plain = MD_LINK_RE.sub(r"\1", text)
69
+ plain = BARE_URL_RE.sub("", plain)
70
+ plain = re.sub(r"[*_`>#|]", "", plain)
71
+ cjk = len(CJK.findall(plain))
72
+ latin = len(plain) - cjk
73
+ return int(cjk + latin * 0.6)
74
+
75
+
76
+ def strip_frontmatter(lines):
77
+ if lines and lines[0].strip() == "---":
78
+ for idx in range(1, len(lines)):
79
+ if lines[idx].strip() == "---":
80
+ return lines[idx + 1:], idx + 1
81
+ return lines, 0
82
+
83
+
84
+ # ------------------------------------------------------------------ fix
85
+
86
+ PROTECT = [
87
+ (re.compile(r"`[^`]+`"), "\x01"), # inline code
88
+ (re.compile(r"\]\([^)\s]+\)"), "\x02"), # link/image targets
89
+ (re.compile(r"https?://[^\s)\]]+"), "\x03"), # bare urls
90
+ ]
91
+
92
+
93
+ def pangu_line(line):
94
+ slots = []
95
+
96
+ def stash(m):
97
+ slots.append(m.group(0))
98
+ return "\x00%d\x00" % (len(slots) - 1)
99
+
100
+ for pattern, _ in PROTECT:
101
+ line = pattern.sub(stash, line)
102
+ line = PANGU_A.sub(r"\1 \2", line)
103
+ line = PANGU_B.sub(r"\1 \2", line)
104
+ for idx, text in enumerate(slots):
105
+ line = line.replace("\x00%d\x00" % idx, text)
106
+ return line
107
+
108
+
109
+ def fix_cjk(path, dry_run=False):
110
+ with open(path, encoding="utf-8") as fh:
111
+ lines = fh.read().split("\n")
112
+ _, fm_end = strip_frontmatter(lines)
113
+
114
+ in_fence = False
115
+ changed = []
116
+ for idx, line in enumerate(lines):
117
+ if FENCE_RE.match(line.strip()):
118
+ in_fence = not in_fence
119
+ continue
120
+ if in_fence or idx <= fm_end:
121
+ continue
122
+ new = pangu_line(line)
123
+ if new != line:
124
+ changed.append((idx + 1, line, new))
125
+ lines[idx] = new
126
+
127
+ if not dry_run and changed:
128
+ with open(path, "w", encoding="utf-8") as fh:
129
+ fh.write("\n".join(lines))
130
+ return changed
131
+
132
+
133
+ # ------------------------------------------------------------------ lint
134
+
135
+ def lint(path, max_para, max_item, asset_root=None):
136
+ with open(path, encoding="utf-8") as fh:
137
+ raw = fh.read()
138
+ # fm_off:frontmatter 占掉的行数。所有报出的行号必须加回它,
139
+ # 否则行号比编辑器里看到的小,无法与 qu-aiwei-zh 等工具的定位对照。
140
+ lines, fm_off = strip_frontmatter(raw.split("\n"))
141
+
142
+ errors, warns, info = [], [], []
143
+ base = os.path.dirname(os.path.abspath(path))
144
+
145
+ in_fence = False
146
+ para_buf, para_start = [], 0
147
+ prev_level = 0
148
+ consecutive = 0
149
+ total_chars = 0
150
+ strong_chars = 0
151
+ h1_count = 0
152
+ cjk_missing = 0
153
+ headings = []
154
+ # 视觉两层级分开统计:锚点层(**加粗**,全文 ≤5 处)与标记层
155
+ # (++下划线++ / <u> / ~~荧光笔~~ / ==高亮==,每段 1–3 处)。
156
+ # 合成一个数会掩盖「加粗掉到 0、下划线没补上」这种塌层。
157
+ body_paras = 0 # 正文段落数(真正的段落,不含标题/列表/引用/表格/代码)
158
+ marker_paras = 0 # 其中带标记层的段落数
159
+ anchor_count = 0 # 锚点层加粗处数
160
+ marker_count = 0 # 标记层处数
161
+
162
+ # 「全部链接速查」这类章节是纯链接清单,条目长是天然的,不参与长度检查
163
+ LINK_DUMP_RE = re.compile(r"(全部链接|链接速查|参考链接|参考资料|延伸阅读)")
164
+ # 标记层:真正承担「每段落点」的那一层
165
+ MARKER_RE = re.compile(r"\+\+[^+]+\+\+|~~[^~]+~~|==[^=]+==|<u>.*?</u>")
166
+ in_link_dump = False
167
+ in_container = False
168
+
169
+ def flush_para(end_line):
170
+ nonlocal para_buf, para_start, body_paras, marker_paras
171
+ if not para_buf:
172
+ return
173
+ if in_link_dump:
174
+ para_buf = []
175
+ return
176
+ text = " ".join(para_buf).strip()
177
+ body_paras += 1
178
+ if MARKER_RE.search(text):
179
+ marker_paras += 1
180
+ length = visual_len(text)
181
+ if length > max_para * 1.8:
182
+ errors.append("L%d 段落过长(%d 字,约 %d 行手机屏):%s…"
183
+ % (para_start + 1 + fm_off, length,
184
+ round(length / 28, 1), text[:28]))
185
+ elif length > max_para:
186
+ warns.append("L%d 段落偏长(%d 字),建议拆成两段:%s…"
187
+ % (para_start + 1 + fm_off, length, text[:28]))
188
+ para_buf = []
189
+
190
+ for idx, line in enumerate(lines):
191
+ stripped = line.strip()
192
+
193
+ if FENCE_RE.match(stripped):
194
+ in_fence = not in_fence
195
+ flush_para(idx)
196
+ continue
197
+ if in_fence:
198
+ continue
199
+
200
+ cm = CONTAINER_RE.match(stripped)
201
+ if cm:
202
+ flush_para(idx)
203
+ kind = (cm.group(1) or "").strip().lower()
204
+ if kind:
205
+ if kind not in KNOWN_CONTAINERS:
206
+ warns.append("L%d 未知容器 `::: %s`(会被当成 note 渲染):"
207
+ "可用 %s"
208
+ % (idx + 1 + fm_off, kind,
209
+ "、".join(sorted(c for c in KNOWN_CONTAINERS if c.isascii()))))
210
+ in_container = True
211
+ else:
212
+ in_container = False
213
+ continue
214
+
215
+ # 容器体内的数据行(如 ::: stat 的三行)是组件内容,不是正文段落。
216
+ # 不排除的话 /段 的分母会虚高,覆盖率判据跟着失真。
217
+ if in_container:
218
+ total_chars += visual_len(stripped)
219
+ continue
220
+
221
+ # 分隔线同样不是正文段落:一份 8 条 --- 的稿子会平白多出 8 个分母
222
+ if re.match(r"^(-{3,}|\*{3,}|_{3,})\s*$", stripped):
223
+ flush_para(idx)
224
+ continue
225
+
226
+ m = HEADING_RE.match(stripped)
227
+ if m:
228
+ flush_para(idx)
229
+ level = len(m.group(1))
230
+ title = m.group(2).strip()
231
+ if level == 1:
232
+ h1_count += 1
233
+ else:
234
+ headings.append((level, title))
235
+ if level <= 2:
236
+ in_link_dump = bool(LINK_DUMP_RE.search(title))
237
+ if prev_level and level > prev_level + 1:
238
+ warns.append("L%d 标题跳级 h%d → h%d:%s"
239
+ % (idx + 1 + fm_off, prev_level, level, stripped[:30]))
240
+ prev_level = level
241
+ consecutive += 1
242
+ if consecutive > 2:
243
+ warns.append("L%d 连续 %d 个标题无正文,建议合并或补过渡段"
244
+ % (idx + 1 + fm_off, consecutive))
245
+ continue
246
+ consecutive = 0
247
+
248
+ total_chars += visual_len(stripped)
249
+ # 强调总量 = 加粗 + 下划线 + 荧光笔 + 高亮 + <u>。
250
+ # 只数 **加粗** 会漏掉「标记层」——历史上正是它让加粗掉到 0 也没人发现。
251
+ bold = re.findall(r"\*\*([^*]+)\*\*", stripped)
252
+ marked = (re.findall(r"\+\+([^+]+)\+\+", stripped)
253
+ + re.findall(r"~~([^~]+)~~", stripped)
254
+ + re.findall(r"==([^=]+)==", stripped)
255
+ + re.findall(r"<u>(.*?)</u>", stripped))
256
+ anchor_count += len(bold)
257
+ marker_count += len(marked)
258
+ strong_chars += sum(len(re.findall(r"[*\u4e00-\u9fffA-Za-z0-9]", s))
259
+ for s in bold + marked)
260
+
261
+ if PANGU_A.search(stripped) or PANGU_B.search(stripped):
262
+ cjk_missing += 1
263
+
264
+ if not stripped:
265
+ flush_para(idx)
266
+ continue
267
+
268
+ if re.match(r"^\s*([-*+]|\d+[.)])\s+", line) and not line[:1].isspace():
269
+ flush_para(idx)
270
+ item = re.sub(r"^\s*([-*+]|\d+[.)])\s+", "", line)
271
+ length = visual_len(item)
272
+ j = idx + 1
273
+ while j < len(lines) and lines[j].strip() and lines[j][:1].isspace():
274
+ length += visual_len(lines[j].strip())
275
+ j += 1
276
+ if length > max_item and not in_link_dump:
277
+ warns.append("L%d 列表项偏长(%d 字,含续行),建议拆成多项或改成段落:%s…"
278
+ % (idx + 1 + fm_off, length, item[:26]))
279
+ continue
280
+
281
+ if stripped.startswith(">") or stripped.startswith("|"):
282
+ flush_para(idx)
283
+ continue
284
+
285
+ if re.match(r"^!\[[^\]]*\]\([^)]+\)\s*$", stripped):
286
+ flush_para(idx)
287
+ continue
288
+
289
+ if not para_buf:
290
+ para_start = idx
291
+ para_buf.append(stripped)
292
+
293
+ flush_para(len(lines))
294
+
295
+ if h1_count > 1:
296
+ warns.append("正文有 %d 个 H1,公众号标题在后台单独填写,正文应只保留 1 个主标题"
297
+ % h1_count)
298
+
299
+ if cjk_missing:
300
+ warns.append("中英文/数字之间缺空格(盘古之白)约 %d 处,用 --fix 自动修复"
301
+ % cjk_missing)
302
+
303
+ # 覆盖率是主动动作,占比是被动结果:35 段里标 5 段、每段标 20 字也能凑出
304
+ # 10% 的占比,但整段划线等于没标。所以主判据用覆盖率。
305
+ cov = marker_paras / max(1, body_paras)
306
+ if body_paras >= 5 and cov < 0.6:
307
+ warns.append("标记覆盖率 %.0f%%(正文 %d 段,仅 %d 段有标记,建议 ≥60%%):"
308
+ "写每段时自问「这段只让读者记住 6 个字,是哪 6 个」,"
309
+ "用 ++关键词++ 标出来" % (cov * 100, body_paras, marker_paras))
310
+
311
+ # 锚点层是另一层:加粗是全文最强锚点,滥用等于没有重点
312
+ if anchor_count > 5:
313
+ warns.append("锚点层加粗 %d 处(建议 ≤5):加粗留给产品名/核心金句/CTA,"
314
+ "关键词标记请改用 ++下划线++,不要用加粗补" % anchor_count)
315
+
316
+ density = strong_chars / max(1, total_chars)
317
+ if density > 0.2:
318
+ warns.append("强调占比 %.0f%%(建议 < 20%%),重点过多会被稀释" % (density * 100))
319
+
320
+ punch = 0
321
+ for para in re.split(r"\n\s*\n", raw):
322
+ if AI_BOLD_PUNCHLINE.search(para.strip()):
323
+ punch += 1
324
+ if punch:
325
+ warns.append("段末加粗金句 %d 处,建议降调为普通陈述句" % punch)
326
+
327
+ for pattern, label in AI_TONE:
328
+ locs = []
329
+ for m in re.finditer(pattern, raw, re.M):
330
+ locs.append(raw.count("\n", 0, m.start()) + 1)
331
+ if len(locs) >= 2:
332
+ shown = "、".join("L%d" % n for n in locs[:6])
333
+ warns.append("AI 腔 / 八股表达「%s」%d 处(%s%s),建议改写"
334
+ % (label, len(locs), shown,
335
+ "…" if len(locs) > 6 else ""))
336
+
337
+ text_all = "\n".join(lines)
338
+ for alt, src in IMG_RE.findall(text_all):
339
+ if re.match(r"^https?://", src):
340
+ info.append("远程图片,粘贴时微信通常会自动抓取,失败需手动上传:%s" % src)
341
+ else:
342
+ found = os.path.exists(os.path.join(base, src))
343
+ if not found and asset_root:
344
+ found = os.path.exists(
345
+ os.path.join(asset_root, re.sub(r"^(\.\./)+", "", src)))
346
+ if not found:
347
+ errors.append("本地图片不存在(可用 --asset-root 指定源仓库):%s" % src)
348
+ else:
349
+ p = src if os.path.isabs(src) else os.path.join(base, src)
350
+ if not os.path.exists(p) and asset_root:
351
+ p = os.path.join(asset_root, re.sub(r"^(\.\./)+", "", src))
352
+ mb = os.path.getsize(p) / 1024 / 1024
353
+ if mb > MAX_IMG_HARD_MB:
354
+ errors.append("图片过大 %.1f MB(公众号建议 < %.0f MB):%s"
355
+ % (mb, MAX_IMG_MB, os.path.basename(src)))
356
+ elif mb > MAX_IMG_MB:
357
+ warns.append("图片 %.1f MB 偏大,建议压到 %.0f MB 内:%s"
358
+ % (mb, MAX_IMG_MB, os.path.basename(src)))
359
+
360
+ links = LINK_RE.findall(text_all)
361
+ if links:
362
+ info.append("外链 %d 处,默认转文末「参考链接」脚注(公众号正文不支持可点外链)"
363
+ % len(links))
364
+
365
+ if "```mermaid" in text_all:
366
+ info.append("含 mermaid 流程图,公众号不支持,需渲染成图片后手动插入")
367
+ if re.search(r"\$\$.+?\$\$|\\\((?:.+?)\\\)|\\\[(?:.+?)\\\]|\\[a-zA-Z]{2,}\{", text_all):
368
+ warns.append("疑似 LaTeX 公式,公众号不支持,需转成图片")
369
+
370
+ return (errors, warns, info, total_chars, strong_chars, headings,
371
+ body_paras, marker_paras, anchor_count)
372
+
373
+
374
+ def main():
375
+ ap = argparse.ArgumentParser()
376
+ ap.add_argument("--input", required=True)
377
+ ap.add_argument("--max-para", type=int, default=110)
378
+ ap.add_argument("--max-item", type=int, default=70)
379
+ ap.add_argument("--asset-root", default="")
380
+ ap.add_argument("--fix", action="store_true", help="自动修复中英文之间的空格")
381
+ ap.add_argument("--warn-only", action="store_true")
382
+ args = ap.parse_args()
383
+
384
+ if args.fix:
385
+ changed = fix_cjk(args.input)
386
+ if changed:
387
+ print("已修复中英文间距 %d 行:" % len(changed))
388
+ for num, old, new in changed[:8]:
389
+ print(" L%d %s" % (num, old.strip()[:52]))
390
+ print(" → %s" % new.strip()[:52])
391
+ if len(changed) > 8:
392
+ print(" … 其余 %d 行" % (len(changed) - 8))
393
+ else:
394
+ print("中英文间距无需修复")
395
+ print("")
396
+
397
+ (errors, warns, info, total, strong, headings,
398
+ body_paras, marker_paras, anchor_count) = lint(
399
+ args.input, args.max_para, args.max_item, args.asset_root or None)
400
+
401
+ first_para = ""
402
+ with open(args.input, encoding="utf-8") as fh:
403
+ body, _ = strip_frontmatter(fh.read().split("\n"))
404
+ for line in body:
405
+ s = line.strip()
406
+ if s and not s.startswith(("#", ">", "|", "-", "*", "!", ":")) and not s.startswith("---"):
407
+ first_para = re.sub(r"[*_`]", "", s)
408
+ break
409
+
410
+ print("体检:%s" % os.path.basename(args.input))
411
+ print(" 字数 %d · 标记覆盖 %.0f%%(%d/%d 段) · 锚点加粗 %d 处 · 预计阅读 %d 分钟"
412
+ % (total, marker_paras / max(1, body_paras) * 100,
413
+ marker_paras, body_paras, anchor_count,
414
+ max(1, round(total / 400))))
415
+ print("")
416
+ if errors:
417
+ print("错误 %d:" % len(errors))
418
+ for e in errors:
419
+ print(" ✗ %s" % e)
420
+ print("")
421
+ if warns:
422
+ print("警告 %d:" % len(warns))
423
+ for w in warns:
424
+ print(" ! %s" % w)
425
+ print("")
426
+ if info:
427
+ print("提示 %d:" % len(info))
428
+ for i in info:
429
+ print(" · %s" % i)
430
+ print("")
431
+ if not errors and not warns:
432
+ print(" ✓ 未发现排版问题")
433
+
434
+ print("发布信息(公众号后台手动填):")
435
+ if first_para:
436
+ print(" 摘要候选:%s…" % first_para[:54])
437
+ if headings:
438
+ print(" 章节:%s" % " / ".join(h[1][:12] for h in headings if h[0] == 2))
439
+
440
+ if errors and not args.warn_only:
441
+ return 1
442
+ return 0
443
+
444
+
445
+ if __name__ == "__main__":
446
+ sys.exit(main())