@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.
- package/CHANGELOG.md +186 -0
- package/README.en.md +10 -70
- package/README.md +11 -69
- package/package.json +1 -1
- package/top-ppt-html/README.en.md +19 -20
- package/top-ppt-html/README.md +36 -90
- package/top-ppt-html/SKILL.md +65 -60
- package/top-ppt-html/references/charts.md +0 -15
- package/top-ppt-html/references/components.md +0 -18
- package/top-ppt-html/references/design-system.md +2 -2
- package/top-ppt-html/references/icons.md +2 -2
- package/top-ppt-html/references/infographics.md +1 -5
- package/top-ppt-html/references/modes.md +4 -4
- package/top-ppt-html/references/tech-design.md +3 -3
- package/top-ppt-html/scripts/build_examples.py +4 -4
- package/top-ppt-html/scripts/build_pptx.js +240 -59
- package/top-ppt-html/scripts/check_triggers.py +10 -2
- package/top-ppt-html/scripts/extract_model.py +16 -5
- package/top-ppt-html/scripts/gen_channel_a.js +55 -10
- package/top-ppt-html/scripts/hydrate_charts.py +68 -13
- package/top-ppt-html/scripts/negative_tests.py +1 -1
- package/top-ppt-html/scripts/prepare_images.py +1 -1
- package/top-ppt-html/scripts/recommend_layout.py +26 -3
- package/top-ppt-html/scripts/render_from_model.py +78 -13
- package/top-ppt-html/scripts/scaffold_report.py +43 -3
- package/top-ppt-html/scripts/sync_runtime.py +32 -4
- package/top-ppt-html/scripts/test_feedback_gates.py +24 -0
- package/top-ppt-html/scripts/validate_pptx.py +10 -1
- package/top-ppt-html/scripts/validate_report.py +43 -7
- package/topmind-cover/README.en.md +12 -8
- package/topmind-cover/README.md +13 -9
- package/topmind-cover/SKILL.md +33 -21
- package/topmind-cover/assets/examples/README.md +16 -19
- 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/tutorial-steps-wechat.png +0 -0
- package/topmind-cover/assets/examples/tutorial-steps.png +0 -0
- package/topmind-cover/references/cover-styles.md +322 -143
- package/topmind-wechat-post/README.en.md +49 -1
- package/topmind-wechat-post/README.md +48 -2
- package/topmind-wechat-post/SKILL.md +35 -37
- package/topmind-wechat-post/references/element-spec.md +1 -1
- package/topmind-wechat-post/references/known-pits.md +2 -3
- package/topmind-wechat-post/references/typography-rules.md +1 -1
- package/topmind-wechat-post/references/workflow.md +3 -3
- package/topmind-wechat-post/references/writing-quality.md +8 -32
- package/topmind-wechat-post/scripts/md2wechat.py +125 -42
- package/topmind-wechat-post/scripts/negative_tests.py +32 -0
- package/topmind-x-article/README.en.md +40 -1
- package/topmind-x-article/README.md +42 -4
- package/topmind-x-article/SKILL.md +11 -5
- package/topmind-x-article/references/x-format.md +13 -10
- package/topmind-x-article/scripts/md2x.py +220 -40
- package/topmind-cover/assets/examples/big-poster-wechat.png +0 -0
- package/topmind-cover/assets/examples/big-poster.png +0 -0
- package/topmind-cover/assets/examples/cyber-glitch-wechat.png +0 -0
- package/topmind-cover/assets/examples/cyber-glitch.png +0 -0
- package/topmind-cover/assets/examples/guochao-wechat.png +0 -0
- package/topmind-cover/assets/examples/guochao.png +0 -0
- package/topmind-cover/assets/examples/tech-future-wechat.png +0 -0
- 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][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
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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 =
|
|
220
|
+
# 图片占位(行内式/引用式)
|
|
221
|
+
line = _replace_images(line, images, defs)
|
|
222
|
+
|
|
223
|
+
# 引用:剥掉所有层级的 >(放标题识别之前,引用内标题也能转)
|
|
224
|
+
line = re.sub(r"^(>\s?)+", "", line)
|
|
46
225
|
|
|
47
|
-
# 标题 →
|
|
48
|
-
m =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
66
|
-
if all(
|
|
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).
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|