@topmindspace/tms-skills 0.3.0 → 0.3.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/README.en.md +10 -70
  3. package/README.md +11 -69
  4. package/package.json +1 -1
  5. package/top-ppt-html/README.en.md +19 -20
  6. package/top-ppt-html/README.md +36 -90
  7. package/top-ppt-html/SKILL.md +65 -60
  8. package/top-ppt-html/references/charts.md +0 -15
  9. package/top-ppt-html/references/components.md +0 -18
  10. package/top-ppt-html/references/design-system.md +2 -2
  11. package/top-ppt-html/references/icons.md +2 -2
  12. package/top-ppt-html/references/infographics.md +1 -5
  13. package/top-ppt-html/references/modes.md +4 -4
  14. package/top-ppt-html/references/tech-design.md +3 -3
  15. package/top-ppt-html/scripts/build_examples.py +4 -4
  16. package/top-ppt-html/scripts/build_pptx.js +240 -59
  17. package/top-ppt-html/scripts/check_triggers.py +10 -2
  18. package/top-ppt-html/scripts/extract_model.py +16 -5
  19. package/top-ppt-html/scripts/gen_channel_a.js +55 -10
  20. package/top-ppt-html/scripts/hydrate_charts.py +68 -13
  21. package/top-ppt-html/scripts/negative_tests.py +1 -1
  22. package/top-ppt-html/scripts/prepare_images.py +1 -1
  23. package/top-ppt-html/scripts/recommend_layout.py +26 -3
  24. package/top-ppt-html/scripts/render_from_model.py +78 -13
  25. package/top-ppt-html/scripts/scaffold_report.py +43 -3
  26. package/top-ppt-html/scripts/sync_runtime.py +32 -4
  27. package/top-ppt-html/scripts/test_feedback_gates.py +24 -0
  28. package/top-ppt-html/scripts/validate_pptx.py +10 -1
  29. package/top-ppt-html/scripts/validate_report.py +43 -7
  30. package/topmind-cover/README.en.md +12 -8
  31. package/topmind-cover/README.md +13 -9
  32. package/topmind-cover/SKILL.md +33 -21
  33. package/topmind-cover/assets/examples/README.md +16 -19
  34. package/topmind-cover/assets/examples/big-type-wechat.png +0 -0
  35. package/topmind-cover/assets/examples/big-type.png +0 -0
  36. package/topmind-cover/assets/examples/brand-launch-wechat.png +0 -0
  37. package/topmind-cover/assets/examples/brand-launch.png +0 -0
  38. package/topmind-cover/assets/examples/gan-huo-wechat.png +0 -0
  39. package/topmind-cover/assets/examples/gan-huo.png +0 -0
  40. package/topmind-cover/assets/examples/ip-fun-wechat.png +0 -0
  41. package/topmind-cover/assets/examples/ip-fun.png +0 -0
  42. package/topmind-cover/assets/examples/magazine-wechat.png +0 -0
  43. package/topmind-cover/assets/examples/magazine.png +0 -0
  44. package/topmind-cover/assets/examples/minimal-wechat.png +0 -0
  45. package/topmind-cover/assets/examples/minimal.png +0 -0
  46. package/topmind-cover/assets/examples/news-flash-wechat.png +0 -0
  47. package/topmind-cover/assets/examples/news-flash.png +0 -0
  48. package/topmind-cover/assets/examples/overview.png +0 -0
  49. package/topmind-cover/assets/examples/tutorial-steps-wechat.png +0 -0
  50. package/topmind-cover/assets/examples/tutorial-steps.png +0 -0
  51. package/topmind-cover/references/cover-styles.md +322 -143
  52. package/topmind-wechat-post/README.en.md +49 -1
  53. package/topmind-wechat-post/README.md +48 -2
  54. package/topmind-wechat-post/SKILL.md +35 -37
  55. package/topmind-wechat-post/references/element-spec.md +1 -1
  56. package/topmind-wechat-post/references/known-pits.md +2 -3
  57. package/topmind-wechat-post/references/typography-rules.md +1 -1
  58. package/topmind-wechat-post/references/workflow.md +3 -3
  59. package/topmind-wechat-post/references/writing-quality.md +8 -32
  60. package/topmind-wechat-post/scripts/md2wechat.py +125 -42
  61. package/topmind-wechat-post/scripts/negative_tests.py +32 -0
  62. package/topmind-x-article/README.en.md +40 -1
  63. package/topmind-x-article/README.md +42 -4
  64. package/topmind-x-article/SKILL.md +11 -5
  65. package/topmind-x-article/references/x-format.md +13 -10
  66. package/topmind-x-article/scripts/md2x.py +220 -40
  67. package/topmind-cover/assets/examples/big-poster-wechat.png +0 -0
  68. package/topmind-cover/assets/examples/big-poster.png +0 -0
  69. package/topmind-cover/assets/examples/cyber-glitch-wechat.png +0 -0
  70. package/topmind-cover/assets/examples/cyber-glitch.png +0 -0
  71. package/topmind-cover/assets/examples/guochao-wechat.png +0 -0
  72. package/topmind-cover/assets/examples/guochao.png +0 -0
  73. package/topmind-cover/assets/examples/tech-future-wechat.png +0 -0
  74. package/topmind-cover/assets/examples/tech-future.png +0 -0
@@ -14,26 +14,202 @@ import re
14
14
  import sys
15
15
  from pathlib import Path
16
16
 
17
+ # --- 行内规则 ---------------------------------------------------------------
18
+
19
+ # markdown 转义:反斜杠 + ASCII 标点 → 标点本身(先占位保护,避免被 emphasis 误配对)
20
+ _UNESCAPE_RE = re.compile(r"\\([!\"#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~])")
21
+ _PUA0, _PUA1 = "\ue000", "\ue001"
22
+
23
+ HR_RE = re.compile(r"^(\*(\s*\*){2,}|-(\s*-){2,}|_(\s*_){2,})\s*$")
24
+ HEADING_RE = re.compile(r"^(#{1,6})\s+(.*)$")
25
+ SETEXT_RE = re.compile(r"^=+\s*$")
26
+ TABLE_SEP_RE = re.compile(r"^:?-+:?$")
27
+ # 引用式定义:[label]: url "可选 title"(行首至多 3 空格)
28
+ REFDEF_RE = re.compile(r"^\s{0,3}\[([^\]]+)\]:\s*(\S+)(?:\s+[\"'(].*[\"')])?\s*$")
29
+
30
+
31
+ def _protect_escapes(s):
32
+ """把反斜杠转义换成私有用区占位符,返回 (新串, 还原表)。"""
33
+ store = []
34
+
35
+ def repl(m):
36
+ store.append(m.group(1))
37
+ return f"{_PUA0}{len(store) - 1}{_PUA1}"
38
+
39
+ return _UNESCAPE_RE.sub(repl, s), store
40
+
41
+
42
+ def _restore_escapes(s, store):
43
+ def repl(m):
44
+ return store[int(m.group(1))]
45
+
46
+ return re.sub(f"{_PUA0}(\\d+){_PUA1}", repl, s)
47
+
48
+
49
+ def _parse_paren(s, j):
50
+ """s[j] == '(' 时,找与之匹配的 ')'(括号可嵌套)。
51
+
52
+ 返回 (括号内文本, 右括号之后的位置);括号不平衡返回 None。
53
+ """
54
+ depth = 0
55
+ k, n = j, len(s)
56
+ while k < n:
57
+ c = s[k]
58
+ if c == "(":
59
+ depth += 1
60
+ elif c == ")":
61
+ depth -= 1
62
+ if depth == 0:
63
+ return s[j + 1:k], k + 1
64
+ k += 1
65
+ return None
66
+
67
+
68
+ def _split_url_title(inner):
69
+ """把 `(url "title")` 括号内文本拆出 url(title 丢弃)。"""
70
+ inner = inner.strip()
71
+ m = re.match(r"^(\S+?)(?:\s+(?:\"[^\"]*\"|'[^']*'|\([^)]*\)))?$", inner)
72
+ return m.group(1) if m else inner
73
+
74
+
75
+ def _replace_images(s, images, defs):
76
+ """![alt](url) / ![alt][ref] → [图N](alt 记入 images)。"""
77
+ res = []
78
+ i, n = 0, len(s)
79
+ while i < n:
80
+ if s.startswith("![", i):
81
+ k = s.find("]", i + 2)
82
+ if k == -1 or "]" in s[i + 2:k]:
83
+ res.append(s[i])
84
+ i += 1
85
+ continue
86
+ alt = s[i + 2:k]
87
+ j = k + 1
88
+ if j < n and s[j] == "(":
89
+ parsed = _parse_paren(s, j)
90
+ if not parsed:
91
+ res.append(s[i])
92
+ i += 1
93
+ continue
94
+ j = parsed[1] # url 允许为空;图片占位不依赖 url
95
+ elif j < n and s[j] == "[":
96
+ k2 = s.find("]", j + 1)
97
+ if k2 == -1:
98
+ res.append(s[i])
99
+ i += 1
100
+ continue
101
+ label = s[j + 1:k2] or alt # [alt][] 省略式:label 取 alt
102
+ if label.strip().lower() not in defs:
103
+ res.append(s[i])
104
+ i += 1
105
+ continue
106
+ j = k2 + 1
107
+ else:
108
+ res.append(s[i])
109
+ i += 1
110
+ continue
111
+ images.append(alt.strip())
112
+ res.append(f"[图{len(images)}]")
113
+ i = j
114
+ else:
115
+ res.append(s[i])
116
+ i += 1
117
+ return "".join(res)
118
+
119
+
120
+ def _replace_links(s, defs):
121
+ """[文字](url) / [文字][ref] / [文字][] / [文字] → 文字(url)。"""
122
+ # 1) 行内式:url 括号可嵌套
123
+ res = []
124
+ i, n = 0, len(s)
125
+ while i < n:
126
+ if s[i] == "[" and not (i > 0 and s[i - 1] == "!"):
127
+ k = s.find("](", i + 1)
128
+ if k != -1 and "]" not in s[i + 1:k]:
129
+ parsed = _parse_paren(s, k + 1)
130
+ if parsed:
131
+ url = _split_url_title(parsed[0])
132
+ if url:
133
+ res.append(f"{s[i + 1:k]}({url})")
134
+ i = parsed[1]
135
+ continue
136
+ res.append(s[i])
137
+ i += 1
138
+ s = "".join(res)
139
+
140
+ # 2) 引用式 [文字][label] / [文字][]((?<!\!) 避免误伤残留的坏图片写法)
141
+ def _ref(m):
142
+ label = (m.group(2) or m.group(1)).strip().lower()
143
+ url = defs.get(label)
144
+ return f"{m.group(1)}({url})" if url else m.group(0)
145
+
146
+ s = _INLINE_REF2_RE.sub(_ref, s)
147
+
148
+ # 3) 快捷引用 [文字](仅当 label 有定义时才转,避免误伤普通方括号)
149
+ def _shortcut(m):
150
+ url = defs.get(m.group(1).strip().lower())
151
+ return f"{m.group(1)}({url})" if url else m.group(0)
152
+
153
+ return _INLINE_REF1_RE.sub(_shortcut, s)
154
+
155
+
156
+ # _inline() 的行内模式:预编译在模块级,避免每行重复走 re 缓存查找
157
+ _INLINE_BI_RE = re.compile(r"\*\*\*(.+?)\*\*\*") # 粗斜体
158
+ _INLINE_B_RE = re.compile(r"\*\*(.+?)\*\*") # 加粗
159
+ _INLINE_BU_RE = re.compile(r"__(.+?)__")
160
+ _INLINE_I_RE = re.compile(r"\*(.+?)\*") # 斜体
161
+ _INLINE_CODE_RE = re.compile(r"`(.+?)`") # 行内代码
162
+ _INLINE_DEL_RE = re.compile(r"~~(.+?)~~") # 删除线
163
+ _INLINE_TASK0_RE = re.compile(r"^(\s*)- \[ \]\s+")
164
+ _INLINE_TASK1_RE = re.compile(r"^(\s*)- \[x\]\s+", re.I)
165
+ _INLINE_REF2_RE = re.compile(r"(?<!\!)\[([^\]]+)\]\[([^\]]*)\]")
166
+ _INLINE_REF1_RE = re.compile(r"(?<!\!)\[([^\]]+)\]")
167
+
168
+
169
+ def _inline(s, defs):
170
+ """行内标记剥离(用于正文行、标题文本、表格单元格)。"""
171
+ s = _INLINE_BI_RE.sub(r"\1", s) # 粗斜体
172
+ s = _INLINE_B_RE.sub(r"\1", s) # 加粗
173
+ s = _INLINE_BU_RE.sub(r"\1", s)
174
+ s = _INLINE_I_RE.sub(r"\1", s) # 斜体
175
+ s = _INLINE_CODE_RE.sub(r"\1", s) # 行内代码
176
+ s = _INLINE_DEL_RE.sub(r"\1", s) # 删除线
177
+ s = _replace_links(s, defs) # 链接(行内式/引用式)
178
+ s = _INLINE_TASK0_RE.sub(r"\1- ", s) # 任务列表
179
+ s = _INLINE_TASK1_RE.sub(r"\1- ", s)
180
+ return s
181
+
182
+
183
+ def _strip_frontmatter(md):
184
+ """去掉文首 frontmatter 块。块内非空行必须都含 ':',否则视为正文(如文首分割线)。"""
185
+ m = re.match(r"^---[ \t]*\r?\n(.*?)\r?\n---[ \t]*(\r?\n|$)", md, flags=re.S)
186
+ if m and all(not ln.strip() or ":" in ln for ln in m.group(1).splitlines()):
187
+ return md[m.end():]
188
+ return md
189
+
17
190
 
18
191
  def convert(md: str):
19
192
  images = []
193
+ defs = {}
20
194
  out_lines = []
21
195
  in_code = False
22
196
 
23
- def img_repl(m):
24
- alt = m.group(1).strip()
25
- images.append(alt)
26
- return f"[图{len(images)}]"
197
+ md, esc_store = _protect_escapes(md)
198
+ md = _strip_frontmatter(md)
27
199
 
200
+ # 先收集引用式定义([label]: url),定义行不进正文
201
+ body = []
28
202
  for raw in md.splitlines():
29
- line = raw.rstrip()
203
+ m = REFDEF_RE.match(raw)
204
+ if m:
205
+ defs[m.group(1).strip().lower()] = m.group(2)
206
+ else:
207
+ body.append(raw)
30
208
 
31
- # 分割线 → 空行(frontmatter 已在 main() 去除,这里只剩正文分隔线)
32
- if re.match(r"^(\*{3,}|-{3,}|_{3,})\s*$", line):
33
- out_lines.append("")
34
- continue
209
+ for raw in body:
210
+ line = raw.rstrip()
35
211
 
36
- # 代码块:去围栏,内容缩进保留
212
+ # 代码块:先处理围栏,块内行原样缩进(分割线规则不进代码块)
37
213
  if line.strip().startswith("```"):
38
214
  in_code = not in_code
39
215
  continue
@@ -41,51 +217,52 @@ def convert(md: str):
41
217
  out_lines.append(" " + line)
42
218
  continue
43
219
 
44
- # 图片占位
45
- line = re.sub(r"!\[([^\]]*)\]\([^)]*\)", img_repl, line)
220
+ # 图片占位(行内式/引用式)
221
+ line = _replace_images(line, images, defs)
222
+
223
+ # 引用:剥掉所有层级的 >(放标题识别之前,引用内标题也能转)
224
+ line = re.sub(r"^(>\s?)+", "", line)
46
225
 
47
- # 标题 → 纯文本行
48
- m = re.match(r"^(#{1,6})\s+(.*)$", line)
226
+ # 标题 → 纯文本行(标题内行文标记同样剥离)
227
+ m = HEADING_RE.match(line)
49
228
  if m:
50
229
  out_lines.append("")
51
- out_lines.append(m.group(2).strip())
230
+ out_lines.append(_inline(m.group(2).strip(), defs))
52
231
  out_lines.append("")
53
232
  continue
54
233
 
55
- # 引用 → 纯文本
56
- line = re.sub(r"^>\s?", "", line)
234
+ # Setext 一级标题:上一行是正文则转为标题
235
+ if SETEXT_RE.match(line):
236
+ if out_lines and out_lines[-1].strip():
237
+ text = out_lines.pop()
238
+ out_lines.append("")
239
+ out_lines.append(text)
240
+ out_lines.append("")
241
+ else:
242
+ out_lines.append("")
243
+ continue
57
244
 
58
- # 分割线 → 空行
59
- if re.match(r"^(\*{3,}|-{3,}|_{3,})\s*$", line):
245
+ # 分割线(* * * / - - - / _ _ _ 允许空格)→ 空行
246
+ if HR_RE.match(line):
60
247
  out_lines.append("")
61
248
  continue
62
249
 
63
- # 表格行 → "项:值" 列表
250
+ # 表格行 → "项:值" 列表(单元格内行文标记同样剥离)
64
251
  if "|" in line and line.strip().startswith("|"):
65
- cells = [c.strip() for c in line.strip().strip("|").split("|")]
66
- if all(re.match(r"^:?-+:?$", c) for c in cells):
252
+ raw_cells = [c.strip() for c in line.strip().strip("|").split("|")]
253
+ if all(TABLE_SEP_RE.match(c) for c in raw_cells):
67
254
  continue
255
+ cells = [_inline(c, defs) for c in raw_cells]
68
256
  label = cells[0]
69
257
  rest = ",".join(c for c in cells[1:] if c)
70
258
  out_lines.append(f"{label}:{rest}" if rest else label)
71
259
  continue
72
260
 
73
- # 行内标记剥离(先 *** 再 ** 再 *,防残留)
74
- line = re.sub(r"\*\*\*(.+?)\*\*\*", r"\1", line) # 粗斜体
75
- line = re.sub(r"\*\*(.+?)\*\*", r"\1", line) # 加粗
76
- line = re.sub(r"__(.+?)__", r"\1", line)
77
- line = re.sub(r"\*(.+?)\*", r"\1", line) # 斜体
78
- line = re.sub(r"`(.+?)`", r"\1", line) # 行内代码
79
- line = re.sub(r"~~(.+?)~~", r"\1", line) # 删除线
80
- line = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r"\1(\2)", line) # 链接
81
- line = re.sub(r"^(\s*)- \[ \]\s+", r"\1- ", line) # 任务列表
82
- line = re.sub(r"^(\s*)- \[x\]\s+", r"\1- ", line, flags=re.I)
83
-
84
- out_lines.append(line)
261
+ out_lines.append(_inline(line, defs))
85
262
 
86
- # 压缩 3+ 空行为 2 个
263
+ # 压缩 3+ 空行为 2 个;去首尾空行(只 strip 换行,保留首行缩进——文档可能以代码块开头)
87
264
  text = "\n".join(out_lines)
88
- text = re.sub(r"\n{3,}", "\n\n", text).strip() + "\n"
265
+ text = re.sub(r"\n{3,}", "\n\n", text).lstrip("\n").rstrip() + "\n"
89
266
 
90
267
  # 文末附图注清单
91
268
  if images:
@@ -93,7 +270,7 @@ def convert(md: str):
93
270
  for i, alt in enumerate(images, 1):
94
271
  text += f"[图{i}] {alt or '(未写图注)'}\n"
95
272
 
96
- return text, images
273
+ return _restore_escapes(text, esc_store), images
97
274
 
98
275
 
99
276
  def main() -> None:
@@ -105,9 +282,12 @@ def main() -> None:
105
282
  inp = Path(args.input)
106
283
  if not inp.is_file():
107
284
  sys.exit(f"错误:找不到输入文件 {inp}")
108
- md = inp.read_text(encoding="utf-8")
109
- # 去掉首尾 frontmatter 块
110
- md = re.sub(r"^---\n.*?\n---\n", "", md, flags=re.S)
285
+ try:
286
+ md = inp.read_text(encoding="utf-8-sig")
287
+ except UnicodeDecodeError:
288
+ sys.exit(f"错误:输入文件不是有效的 UTF-8 编码 {inp}")
289
+ except OSError as e:
290
+ sys.exit(f"错误:无法读取输入文件 {inp}({e})")
111
291
  text, images = convert(md)
112
292
 
113
293
  out = Path(args.out)