@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
|
@@ -95,9 +95,18 @@ def kp(k, v) -> str:
|
|
|
95
95
|
return f'<li><strong>{k}。</strong>{v}</li>' if k else f'<li>{v}</li>'
|
|
96
96
|
|
|
97
97
|
|
|
98
|
-
def
|
|
98
|
+
def sec_anchor(i: int, sec: dict) -> str:
|
|
99
|
+
"""页锚点 id:模型 anchor 优先,否则 s{i}(agenda 链接与 open_sec 同源,禁各写一份)。"""
|
|
100
|
+
a = str((sec or {}).get('anchor') or '').strip()
|
|
101
|
+
if re.fullmatch(r'[A-Za-z][A-Za-z0-9_-]*', a):
|
|
102
|
+
return a
|
|
103
|
+
return f's{i}'
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def open_sec(i: int, ptype: str, extra: str = '', sec: dict = None) -> str:
|
|
99
107
|
skel = PAGE_TO_PRESET.get(ptype, 'P4')
|
|
100
|
-
|
|
108
|
+
sid = sec_anchor(i, sec)
|
|
109
|
+
return (f'<section class="band" id="{sid}" data-skel="{skel}" '
|
|
101
110
|
f'data-page-type="{ptype}"{extra}>')
|
|
102
111
|
|
|
103
112
|
|
|
@@ -118,10 +127,17 @@ def sowhat(sec: dict) -> str:
|
|
|
118
127
|
|
|
119
128
|
|
|
120
129
|
def footnote(sec: dict) -> str:
|
|
130
|
+
# footnote 与 note 共用来源行(与 build_pptx 注释带同口径:两者都不丢)
|
|
121
131
|
t = sec.get('footnote')
|
|
122
|
-
|
|
132
|
+
n = sec.get('note')
|
|
133
|
+
parts = []
|
|
134
|
+
if t:
|
|
135
|
+
parts.append(cite(t))
|
|
136
|
+
if n and n != t:
|
|
137
|
+
parts.append(f'注:{cite(n)}')
|
|
138
|
+
if not parts:
|
|
123
139
|
return ''
|
|
124
|
-
return f' <div class="footnote">{
|
|
140
|
+
return f' <div class="footnote">{" ".join(parts)}</div>\n'
|
|
125
141
|
|
|
126
142
|
|
|
127
143
|
def flags_block(sec: dict) -> str:
|
|
@@ -134,55 +150,49 @@ def flags_block(sec: dict) -> str:
|
|
|
134
150
|
|
|
135
151
|
|
|
136
152
|
def wrap(i: int, ptype: str, body: str, sec: dict, extra: str = '') -> str:
|
|
137
|
-
return (f'{open_sec(i, ptype, extra)}\n <div class="wrap">\n'
|
|
153
|
+
return (f'{open_sec(i, ptype, extra, sec)}\n <div class="wrap">\n'
|
|
138
154
|
f'{shead(sec)}\n{body}{sowhat(sec)}{footnote(sec)}{flags_block(sec)}'
|
|
139
155
|
f' </div>\n</section>')
|
|
140
156
|
|
|
141
157
|
|
|
142
|
-
# ──
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
'智能': _SVG_OPEN + '<rect x="4" y="4" width="16" height="16" rx="2"/><path d="M9 9h6v6H9zM9 1v3M15 1v3M9 20v3M15 20v3M1 9h3M1 15h3M20 9h3M20 15h3"/></svg>',
|
|
163
|
-
'洞察': _SVG_OPEN + '<circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>',
|
|
164
|
-
'工具': _SVG_OPEN + '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></svg>',
|
|
165
|
-
'清单': _SVG_OPEN + '<path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>',
|
|
166
|
-
}
|
|
158
|
+
# ── 语义图标库(单源:assets/icons/ + index.json;与 icon_lib.js / PPTX 真导出同源)──
|
|
159
|
+
_ICON_DIR = Path(__file__).resolve().parent.parent / 'assets' / 'icons'
|
|
160
|
+
_ICON_INDEX: dict = {}
|
|
161
|
+
try:
|
|
162
|
+
_ICON_INDEX = json.loads((_ICON_DIR / 'index.json').read_text(encoding='utf-8'))
|
|
163
|
+
except OSError:
|
|
164
|
+
_ICON_INDEX = {}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _load_icons() -> 'dict[str, str]':
|
|
168
|
+
out: dict[str, str] = {}
|
|
169
|
+
for name in (_ICON_INDEX.get('icons') or {}):
|
|
170
|
+
try:
|
|
171
|
+
out[name] = (_ICON_DIR / f'{name}.svg').read_text(encoding='utf-8').strip()
|
|
172
|
+
except OSError:
|
|
173
|
+
continue
|
|
174
|
+
return out
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
ICONS: dict[str, str] = _load_icons()
|
|
167
178
|
_ICONS_ORDER = list(ICONS.keys())
|
|
179
|
+
_ICON_DEFAULTS = [n for n in (_ICON_INDEX.get('defaults') or []) if n in ICONS] or _ICONS_ORDER
|
|
168
180
|
|
|
169
181
|
|
|
170
182
|
def pick_icon(title: str = '', idx: int = 0) -> str:
|
|
171
|
-
"""
|
|
172
|
-
|
|
173
|
-
for key, svg in ICONS.items():
|
|
174
|
-
if key in t:
|
|
175
|
-
return svg
|
|
176
|
-
return ICONS[_ICONS_ORDER[idx % len(_ICONS_ORDER)]]
|
|
183
|
+
"""按标题语义挑图标 SVG;未命中则在默认表内轮换(选不出用默认,不盲目轮换全量)。"""
|
|
184
|
+
return ICONS[pick_icon_name(title, idx)]
|
|
177
185
|
|
|
178
186
|
|
|
179
187
|
def pick_icon_name(title: str = '', idx: int = 0) -> str:
|
|
180
|
-
"""图标名(与 icon_lib.js / PPTX
|
|
188
|
+
"""图标名(与 icon_lib.js / PPTX 真导出同名;关键词来自 index.json)。"""
|
|
181
189
|
t = str(title or '')
|
|
182
|
-
|
|
183
|
-
|
|
190
|
+
meta = _ICON_INDEX.get('icons') or {}
|
|
191
|
+
for key in _ICONS_ORDER:
|
|
192
|
+
kws = (meta.get(key) or {}).get('keywords') or [key]
|
|
193
|
+
if any(kw and kw in t for kw in kws):
|
|
184
194
|
return key
|
|
185
|
-
return
|
|
195
|
+
return _ICON_DEFAULTS[idx % len(_ICON_DEFAULTS)]
|
|
186
196
|
|
|
187
197
|
|
|
188
198
|
def card_head(title: str, idx: int = 0, icon_name: str | None = None) -> str:
|
|
@@ -253,16 +263,23 @@ def r_metrics(i, sec):
|
|
|
253
263
|
|
|
254
264
|
|
|
255
265
|
def r_kpi(i, sec):
|
|
266
|
+
# 大数指标页:hero 大数字 + 支撑指标行(metrics,可选)+ 要点;三者缺一不丢其余
|
|
256
267
|
hero = sec.get('hero') or []
|
|
257
268
|
val = hero[0] if hero else ''
|
|
258
269
|
lab = hero[1] if len(hero) > 1 else ''
|
|
259
270
|
delta = hero[2] if len(hero) > 2 else ''
|
|
271
|
+
left = ''
|
|
272
|
+
if val or lab:
|
|
273
|
+
left = (
|
|
274
|
+
' <div class="stack gap-3"><div class="t-metric" style="color:var(--accent)">'
|
|
275
|
+
f'{esc(val)}</div>'
|
|
276
|
+
f'<div class="t-h3">{esc(lab)}</div>'
|
|
277
|
+
+ (f'<div class="t-sm" style="color:var(--accent);font-weight:600">{esc(delta)}</div>' if delta else '')
|
|
278
|
+
+ '</div>\n')
|
|
279
|
+
mets = metrics_row(sec.get('metrics')) if sec.get('metrics') else ''
|
|
260
280
|
body = (
|
|
261
281
|
' <div class="grid g-hero rv a-c">\n'
|
|
262
|
-
|
|
263
|
-
f'<div class="t-h3">{esc(lab)}</div>'
|
|
264
|
-
+ (f'<div class="t-sm" style="color:var(--accent);font-weight:600">{esc(delta)}</div>' if delta else '')
|
|
265
|
-
+ '</div>\n'
|
|
282
|
+
+ left + mets
|
|
266
283
|
+ ' <div class="grid g-2">\n'
|
|
267
284
|
+ pts_list(sec.get('points') or [['支撑', '一句话。']])
|
|
268
285
|
+ '\n </div>\n </div>\n')
|
|
@@ -339,14 +356,20 @@ def r_exhibit(i, sec):
|
|
|
339
356
|
|
|
340
357
|
def r_twocol(i, sec):
|
|
341
358
|
paras = sec.get('paragraphs') or []
|
|
359
|
+
variant = sec.get('variant') or ''
|
|
342
360
|
cols = []
|
|
343
|
-
for p in paras:
|
|
361
|
+
for pi, p in enumerate(paras):
|
|
344
362
|
if isinstance(p, (list, tuple)) and len(p) >= 2:
|
|
345
363
|
cols.append(f' <p class="t-body"><strong>{esc(p[0])}。</strong>{cite(p[1])}</p>')
|
|
346
364
|
else:
|
|
347
365
|
cols.append(f' <p class="t-body">{cite(p)}</p>')
|
|
366
|
+
# 变体 2-col-feature:首栏为特性栏(复用 .card 语义:加粗标题 + accent 左线)
|
|
367
|
+
extra = ''
|
|
368
|
+
if variant == '2-col-feature' and cols:
|
|
369
|
+
cols[0] = cols[0].replace('class="t-body"', 'class="t-body col--feature"', 1)
|
|
370
|
+
extra = ' data-variant="2-col-feature"'
|
|
348
371
|
body = ' <div class="cols-2 rv">\n' + '\n'.join(cols) + '\n </div>\n'
|
|
349
|
-
return wrap(i, 'twocol', body, sec)
|
|
372
|
+
return wrap(i, 'twocol', body, sec, extra)
|
|
350
373
|
|
|
351
374
|
|
|
352
375
|
def r_threecol(i, sec):
|
|
@@ -365,11 +388,16 @@ def r_threecol(i, sec):
|
|
|
365
388
|
|
|
366
389
|
def r_cards(i, sec):
|
|
367
390
|
cards = sec.get('cards') or []
|
|
391
|
+
variant = sec.get('variant') or ''
|
|
368
392
|
n = int(sec.get('columns') or min(3, max(1, len(cards))))
|
|
369
393
|
cls = {1: 'g-2', 2: 'g-2', 3: 'g-3', 4: 'g-4'}.get(n, 'g-3')
|
|
370
394
|
# 纯卡片栅格强制等高(g-2--equal / g-3--equal),与 CSS / layout-grammar「同行卡片 stretch」一致
|
|
371
395
|
if cls in ('g-2', 'g-3', 'g-4'):
|
|
372
396
|
cls = cls + '--equal'
|
|
397
|
+
extra = ''
|
|
398
|
+
if variant == 'bento-grid':
|
|
399
|
+
# 变体 bento-grid:首卡跨 2 列(.card--lead),复用 .card 组件
|
|
400
|
+
cls, extra = 'g-bento', ' data-variant="bento-grid"'
|
|
373
401
|
blocks = []
|
|
374
402
|
for ci, cd in enumerate(cards):
|
|
375
403
|
if isinstance(cd, dict):
|
|
@@ -380,11 +408,12 @@ def r_cards(i, sec):
|
|
|
380
408
|
title, points, icon_name = cd[0], [['', cd[1]]], None
|
|
381
409
|
else:
|
|
382
410
|
title, points, icon_name = (cd or ''), [], None
|
|
411
|
+
lead_cls = ' card--lead' if (variant == 'bento-grid' and ci == 0) else ''
|
|
383
412
|
blocks.append(
|
|
384
|
-
f' <div class="card">{card_head(str(title), ci, icon_name)}\n'
|
|
413
|
+
f' <div class="card{lead_cls}">{card_head(str(title), ci, icon_name)}\n'
|
|
385
414
|
f' {pts_list(points)}</div>')
|
|
386
415
|
body = f' <div class="grid {cls} rv">\n' + '\n'.join(blocks) + '\n </div>\n'
|
|
387
|
-
return wrap(i, 'cards', body, sec)
|
|
416
|
+
return wrap(i, 'cards', body, sec, extra)
|
|
388
417
|
|
|
389
418
|
|
|
390
419
|
def r_split(i, sec):
|
|
@@ -519,6 +548,9 @@ def r_image(i, sec):
|
|
|
519
548
|
else:
|
|
520
549
|
grid_cls = f'media-grid media-grid--{min(4, max(2, n))}'
|
|
521
550
|
media = f' <div class="{grid_cls} rv">\n' + '\n'.join(cells) + '\n </div>\n'
|
|
551
|
+
# 组 caption(图编号):多图版式同样渲染,与 PPTX img.caption 同口径(此前仅单图分支渲染)
|
|
552
|
+
if cap:
|
|
553
|
+
media += f' <p class="media__src">{cite(cap)}</p>\n'
|
|
522
554
|
else:
|
|
523
555
|
src = im.get('src') or ''
|
|
524
556
|
ph = bool(im.get('placeholder')) or not src
|
|
@@ -589,23 +621,32 @@ def r_lane(i, sec):
|
|
|
589
621
|
|
|
590
622
|
def r_timeline(i, sec):
|
|
591
623
|
items = []
|
|
624
|
+
variant = sec.get('variant') or ''
|
|
592
625
|
for ph in (sec.get('phases') or []):
|
|
593
626
|
if isinstance(ph, (list, tuple)):
|
|
594
627
|
lab, name, desc = (list(ph) + ['', '', ''])[:3]
|
|
595
628
|
state = ph[3] if len(ph) > 3 else ''
|
|
629
|
+
icon_name = ph[4] if len(ph) > 4 else ''
|
|
596
630
|
else:
|
|
597
631
|
lab, name, desc, state = ph.get('label', ''), ph.get('name', ''), ph.get('d', ''), ph.get('s', '')
|
|
632
|
+
icon_name = ph.get('icon', '')
|
|
598
633
|
cls = 'tl__i'
|
|
599
634
|
if state == 'done':
|
|
600
635
|
cls += ' tl__i--done'
|
|
601
636
|
elif state == 'now':
|
|
602
637
|
cls += ' tl__i--now'
|
|
638
|
+
# 变体 timeline:节点图标(icon:<name>,无图标回落圆点)
|
|
639
|
+
dot = '<div class="tl__d"></div>'
|
|
640
|
+
if variant == 'timeline' and icon_name and icon_name in ICONS:
|
|
641
|
+
dot = (f'<div class="tl__ico" data-icon="{esc(icon_name)}">'
|
|
642
|
+
f'{ICONS[icon_name]}</div>')
|
|
603
643
|
items.append(
|
|
604
|
-
f' <div class="{cls}"
|
|
644
|
+
f' <div class="{cls}">{dot}'
|
|
605
645
|
f'<div class="tl__l">{esc(lab)}</div><div class="tl__t">{esc(name)}</div>'
|
|
606
646
|
f'<p class="t-body">{cite(desc)}</p></div>')
|
|
607
647
|
body = ' <div class="fig rv"><div class="tl">\n' + '\n'.join(items) + '\n </div></div>\n'
|
|
608
|
-
|
|
648
|
+
extra = ' data-variant="timeline"' if variant == 'timeline' else ''
|
|
649
|
+
return wrap(i, 'timeline', body, sec, extra)
|
|
609
650
|
|
|
610
651
|
|
|
611
652
|
def r_steps(i, sec):
|
|
@@ -675,18 +716,34 @@ def r_pyramid(i, sec):
|
|
|
675
716
|
|
|
676
717
|
|
|
677
718
|
def r_halftable(i, sec):
|
|
678
|
-
"""半表半图:g-half 左表右图(校验 TYPE_FEATURE.halftable)。
|
|
719
|
+
"""半表半图:g-half 左表右图(校验 TYPE_FEATURE.halftable)。
|
|
720
|
+
research 模式带 exhibitNo 时:右图按 Exhibit 规范加编号徽标与来源行,表侧不丢
|
|
721
|
+
(此前 research 统一走 r_exhibit 会静默丢弃 table——内容覆盖率门禁捕获)。"""
|
|
679
722
|
tbl = sec.get('table') or {}
|
|
680
723
|
ch = sec.get('chart') or {}
|
|
681
724
|
head = tbl.get('head') or []
|
|
682
725
|
rows = tbl.get('rows') or []
|
|
683
726
|
th = ''.join(f'<th>{esc(h)}</th>' for h in head)
|
|
684
727
|
trs = ''.join('<tr>' + ''.join(f'<td>{cite(c)}</td>' for c in row) + '</tr>' for row in rows)
|
|
728
|
+
no = sec.get('exhibitNo')
|
|
729
|
+
if no:
|
|
730
|
+
fig = (
|
|
731
|
+
' <div class="fig"><div class="exhibit rv">\n'
|
|
732
|
+
f' <div class="exhibit__hd"><span class="exhibit__no">Exhibit {esc(no)}</span>'
|
|
733
|
+
f'<span class="exhibit__t">{esc(sec.get("title") or "")}</span></div>\n'
|
|
734
|
+
f'{chart_svg(ch, i)}'
|
|
735
|
+
f' <div class="exhibit__src">{cite(sec.get("footnote") or "来源:待补")}</div>\n'
|
|
736
|
+
' </div></div>\n')
|
|
737
|
+
sec = dict(sec)
|
|
738
|
+
sec.pop('footnote', None) # 已进来源行,避免 wrap 再渲染一次
|
|
739
|
+
else:
|
|
740
|
+
fig = (
|
|
741
|
+
f' <div class="fig"><div class="fig__cap">{esc(ch.get("cap") or "互证图")}</div>\n'
|
|
742
|
+
f'{chart_svg(ch, i)} </div>\n')
|
|
685
743
|
body = (
|
|
686
744
|
' <div class="grid g-half g-2 rv a-start">\n'
|
|
687
745
|
f' <div class="tbl-wrap"><table><thead><tr>{th}</tr></thead><tbody>{trs}</tbody></table></div>\n'
|
|
688
|
-
f'
|
|
689
|
-
f'{chart_svg(ch, i)} </div>\n'
|
|
746
|
+
f'{fig}'
|
|
690
747
|
' </div>\n')
|
|
691
748
|
return wrap(i, 'halftable', body, sec)
|
|
692
749
|
|
|
@@ -800,8 +857,10 @@ def render_body(model: dict) -> str:
|
|
|
800
857
|
title = ag[1] if isinstance(ag, (list, tuple)) and len(ag) > 1 else ''
|
|
801
858
|
desc = ag[2] if isinstance(ag, (list, tuple)) and len(ag) > 2 else ''
|
|
802
859
|
href_i = chapter_first.get(str(num).zfill(2)) or chapter_first.get(str(num)) or gi
|
|
860
|
+
# 锚点与 open_sec 同源:该页有 anchor 则链到命名锚点
|
|
861
|
+
href_id = sec_anchor(href_i, secs[href_i - 1] if 0 < href_i <= len(secs) else {})
|
|
803
862
|
lis.append(
|
|
804
|
-
f' <li class="agenda__i"><a class="agenda__a" href="#
|
|
863
|
+
f' <li class="agenda__i"><a class="agenda__a" href="#{href_id}">'
|
|
805
864
|
f'<span class="agenda__n">{esc(num)}</span>'
|
|
806
865
|
f'<span><span class="agenda__t">{esc(title)}</span>'
|
|
807
866
|
f'<div class="agenda__d">{esc(desc)}</div></span></a></li>')
|
|
@@ -822,7 +881,9 @@ def render_body(model: dict) -> str:
|
|
|
822
881
|
sec = dict(sec)
|
|
823
882
|
sec['exhibitNo'] = ex_n
|
|
824
883
|
fn = RENDERERS.get(t, r_points)
|
|
825
|
-
if is_research_exhibit:
|
|
884
|
+
if is_research_exhibit and t != 'halftable':
|
|
885
|
+
# halftable 在 research 下仍走 r_halftable(表+图),仅借 exhibitNo 加 Exhibit 徽标;
|
|
886
|
+
# 统一走 r_exhibit 会静默丢弃 table(内容覆盖率门禁捕获,0.2.0 修)。
|
|
826
887
|
fn = r_exhibit
|
|
827
888
|
try:
|
|
828
889
|
parts.append(fn(i, sec))
|
|
@@ -113,7 +113,7 @@ CHART_CYCLE = CHART_CORE # 轮换默认序列(骨架示范「图要选对」
|
|
|
113
113
|
_SCAFFOLD = (LC.get('charts') or {}).get('scaffold') or {}
|
|
114
114
|
if not _SCAFFOLD.get('viewBoxHeight') or not _SCAFFOLD.get('infoTypes'):
|
|
115
115
|
# 启动自检:单源缺键时立刻失败,防「静默退回默认值 180」的窗口期
|
|
116
|
-
print('
|
|
116
|
+
print('错误:layout-constants.json 缺 charts.scaffold.viewBoxHeight/infoTypes'
|
|
117
117
|
'(骨架尺寸单源不完整;补单源后先跑 sync_runtime.py)', file=sys.stderr)
|
|
118
118
|
sys.exit(2)
|
|
119
119
|
CHART_VIEWBOX = dict(_SCAFFOLD.get('viewBoxHeight') or {})
|
|
@@ -301,10 +301,10 @@ def inject(path: Path, pat: re.Pattern, repl: str, label: str) -> bool:
|
|
|
301
301
|
try:
|
|
302
302
|
src = path.read_text(encoding='utf-8')
|
|
303
303
|
except OSError as e:
|
|
304
|
-
print(f'
|
|
304
|
+
print(f'错误: 无法读取 {path.name}: {e}')
|
|
305
305
|
return False
|
|
306
306
|
if not pat.search(src):
|
|
307
|
-
print(f'
|
|
307
|
+
print(f'错误: {path.name} 缺少 {label} 标记块')
|
|
308
308
|
return False
|
|
309
309
|
path.write_text(pat.sub(lambda m: repl, src, count=1), encoding='utf-8')
|
|
310
310
|
return True
|
|
@@ -312,32 +312,32 @@ def inject(path: Path, pat: re.Pattern, repl: str, label: str) -> bool:
|
|
|
312
312
|
|
|
313
313
|
def main() -> int:
|
|
314
314
|
if not LC.exists():
|
|
315
|
-
print(f'
|
|
315
|
+
print(f'错误: 找不到 {LC}')
|
|
316
316
|
return 2
|
|
317
317
|
try:
|
|
318
318
|
lc = json.loads(LC.read_text(encoding='utf-8'))
|
|
319
319
|
except UnicodeDecodeError as e:
|
|
320
|
-
print(f'
|
|
320
|
+
print(f'错误: {LC.name} 不是合法 UTF-8: {e}')
|
|
321
321
|
return 2
|
|
322
322
|
except json.JSONDecodeError as e:
|
|
323
|
-
print(f'
|
|
323
|
+
print(f'错误: {LC.name} 不是合法 JSON: {e}')
|
|
324
324
|
return 2
|
|
325
325
|
except OSError as e:
|
|
326
|
-
print(f'
|
|
326
|
+
print(f'错误: 无法读取 {LC}: {e}')
|
|
327
327
|
return 2
|
|
328
328
|
if not MS.exists():
|
|
329
|
-
print(f'
|
|
329
|
+
print(f'错误: 找不到 {MS}(DSL schema 单源)')
|
|
330
330
|
return 2
|
|
331
331
|
try:
|
|
332
332
|
ms = json.loads(MS.read_text(encoding='utf-8'))
|
|
333
333
|
except UnicodeDecodeError as e:
|
|
334
|
-
print(f'
|
|
334
|
+
print(f'错误: {MS.name} 不是合法 UTF-8: {e}')
|
|
335
335
|
return 2
|
|
336
336
|
except json.JSONDecodeError as e:
|
|
337
|
-
print(f'
|
|
337
|
+
print(f'错误: {MS.name} 不是合法 JSON: {e}')
|
|
338
338
|
return 2
|
|
339
339
|
except OSError as e:
|
|
340
|
-
print(f'
|
|
340
|
+
print(f'错误: 无法读取 {MS}: {e}')
|
|
341
341
|
return 2
|
|
342
342
|
ok = True
|
|
343
343
|
|
|
@@ -345,13 +345,13 @@ def main() -> int:
|
|
|
345
345
|
try:
|
|
346
346
|
src = PE.read_text(encoding='utf-8')
|
|
347
347
|
except OSError as e:
|
|
348
|
-
print(f'
|
|
348
|
+
print(f'错误: 无法读取 {PE}: {e}')
|
|
349
349
|
return 1
|
|
350
350
|
if not CONST_PAT.search(src):
|
|
351
|
-
print('
|
|
351
|
+
print('错误: assets/pptx-export.js 缺少 __TOPPPT_CONSTANTS__ 标记块')
|
|
352
352
|
return 1
|
|
353
353
|
if not SCHEMA_PAT.search(src):
|
|
354
|
-
print('
|
|
354
|
+
print('错误: assets/pptx-export.js 缺少 __TOPPPT_SCHEMA__ 标记块')
|
|
355
355
|
return 1
|
|
356
356
|
block = build_const_block(lc)
|
|
357
357
|
PE.write_text(CONST_PAT.sub(lambda m: block, src), encoding='utf-8')
|
|
@@ -370,7 +370,7 @@ def main() -> int:
|
|
|
370
370
|
|
|
371
371
|
# ② 公共片段源
|
|
372
372
|
if not ENGINE.exists() or not UI.exists():
|
|
373
|
-
print(f'
|
|
373
|
+
print(f'错误: 缺少公共片段 {ENGINE} / {UI}')
|
|
374
374
|
return 1
|
|
375
375
|
engine_css = ENGINE.read_text(encoding='utf-8').rstrip('\n')
|
|
376
376
|
ui_js = UI.read_text(encoding='utf-8').rstrip('\n')
|
|
@@ -138,6 +138,7 @@ SLIDE_RE = re.compile(r"ppt/slides/slide(\d+)\.xml$")
|
|
|
138
138
|
_PROBE_SKIP_KEYS = {
|
|
139
139
|
"colors", "color", "icon", "href", "id", "type", "exhibitNo", "max", "unit",
|
|
140
140
|
"hint", "src", "alt", "fit", "layout", "placeholder", "ratio", "dataTable",
|
|
141
|
+
"variant", # 版式变体注册值(bento-grid/timeline/2-col-feature)不渲染为文本
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
TopPPT HTML· HTML 报告质量校验(交付闭环)
|
|
5
5
|
用法:
|
|
6
6
|
python validate_report.py <报告.html> [--strict] [--layout-qa] [--json]
|
|
7
|
+
python validate_report.py --help
|
|
7
8
|
|
|
8
9
|
输出每项 PASS/FAIL,结尾给汇总与结论;任一 FAIL 时退出码为 1(--strict 时 WARN 也计失败)。
|
|
9
10
|
--json 以 JSON 输出全部检查结果(供脚本/流水线读取)。
|
|
@@ -30,6 +31,8 @@ import re
|
|
|
30
31
|
import json
|
|
31
32
|
import base64
|
|
32
33
|
import hashlib
|
|
34
|
+
import html as _html
|
|
35
|
+
import unicodedata
|
|
33
36
|
from typing import Optional
|
|
34
37
|
from html.parser import HTMLParser
|
|
35
38
|
from pathlib import Path
|
|
@@ -370,6 +373,155 @@ def _check_bands(txt, chk, mode="presentation"):
|
|
|
370
373
|
f"{bad_grids} 处未对齐" if bad_grids else "", level="WARN")
|
|
371
374
|
|
|
372
375
|
|
|
376
|
+
def _icon_pack_names():
|
|
377
|
+
"""图标包合法名集合(assets/icons/index.json);读不到时返回 None(跳过存在性检查)。"""
|
|
378
|
+
try:
|
|
379
|
+
idx = json.loads((Path(__file__).resolve().parent.parent / 'assets' / 'icons'
|
|
380
|
+
/ 'index.json').read_text(encoding='utf-8'))
|
|
381
|
+
return set((idx.get('icons') or {}).keys())
|
|
382
|
+
except OSError:
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _check_icon_refs(txt, chk):
|
|
387
|
+
"""图标引用存在性:data-icon / icon: 标记引用了包外图标 → WARN(不 FAIL)。"""
|
|
388
|
+
names = _icon_pack_names()
|
|
389
|
+
if names is None:
|
|
390
|
+
return
|
|
391
|
+
# 只扫渲染正文:去掉 script/style/注释与嵌入模型 JSON,避免代码与文档示例误报
|
|
392
|
+
body = re.sub(r'<script[\s\S]*?</script>', '', txt, flags=re.I)
|
|
393
|
+
body = re.sub(r'<style[\s\S]*?</style>', '', body, flags=re.I)
|
|
394
|
+
body = re.sub(r'<!--[\s\S]*?-->', '', body)
|
|
395
|
+
body = re.sub(r'window\.REPORT_MODEL\s*=\s*\{[\s\S]*?\n\};', '', body)
|
|
396
|
+
refs = set(re.findall(r'data-icon="([^"]+)"', body))
|
|
397
|
+
refs |= set(re.findall(r'icon:([\u4e00-\u9fa5A-Za-z0-9_-]+)', body))
|
|
398
|
+
# 排除 hydrate 占位与 CSS 类名噪声:只看真实引用上下文
|
|
399
|
+
bad = sorted(r for r in refs if r not in names and not r.startswith('{{'))
|
|
400
|
+
chk("图标引用存在(assets/icons 包内)", not bad,
|
|
401
|
+
f"未知图标: {', '.join(bad[:6])}{'…' if len(bad) > 6 else ''}({len(bad)} 个)" if bad else "",
|
|
402
|
+
level="WARN")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# ── 内容覆盖率(0.2.0):版式不得以丢文字为代价美化 ──
|
|
406
|
+
# 口径:REPORT_MODEL 可见文本串(去重、归一化)有多少字符能在渲染后 HTML 可见文本中找到;
|
|
407
|
+
# <95% → WARN(只报告,不 FAIL;长串按 8 字窗口部分计分,避免数字重排/截断误杀)。
|
|
408
|
+
_COV_SKIP_KEYS = {
|
|
409
|
+
# 元字段(不渲染为正文)
|
|
410
|
+
"style", "theme", "mode", "type", "variant", "icon", "color", "colors",
|
|
411
|
+
"src", "href", "id", "layout", "datatable", "dataTable", "font", "fontface",
|
|
412
|
+
"exhibitno", "exhibitNo", "columns", "max", "min", "unit", "ratio", "fit",
|
|
413
|
+
"align", "width", "height", "placeholder", "alt", "target", "rel", "dir",
|
|
414
|
+
"lang", "charset", "defer", "async", "media",
|
|
415
|
+
"hint", # 搭建期给智能体的指令(如 image.hint),不是读者正文
|
|
416
|
+
}
|
|
417
|
+
_COV_URL_RE = re.compile(r'^(https?://|www\.|data:|#)', re.I)
|
|
418
|
+
_COV_COLOR_RE = re.compile(r'^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$')
|
|
419
|
+
# 纯小写 ASCII 标识符(无空格/CJK/大写):渲染器语义 flag(如 "focus"),不是正文
|
|
420
|
+
_COV_FLAG_RE = re.compile(r'^[a-z][a-z0-9_-]*$')
|
|
421
|
+
# 信息图页型(r_info 故意留"按 infographics 规格绘制"占位、PPTX 走形状通道):
|
|
422
|
+
# 其 chart 数据子树不计入覆盖率(数据在模型与 PPTX 中完整,仅 HTML 待定制信息图)
|
|
423
|
+
_COV_INFO_TYPES = {'sankey', 'treemap', 'boxplot', 'network', 'marimekko', 'streamgraph'}
|
|
424
|
+
_COV_INFO_DATA_KEYS = {'chart', 'labels', 'values', 'groups', 'nodes', 'links', 'edges', 'data',
|
|
425
|
+
'series', 'dataset', 'cols', 'rows', 'cells'}
|
|
426
|
+
_COV_MIN_RATIO = 0.95 # 覆盖率下限(layout-constants qualityGates.contentCoverage 可覆写)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _norm_cov(s: str) -> str:
|
|
430
|
+
s = unicodedata.normalize('NFKC', s)
|
|
431
|
+
s = re.sub(r'(?<=\d),(?=\d)', '', s) # 千分位 1,000 → 1000(中文逗号保留)
|
|
432
|
+
s = re.sub(r'\s+', '', s)
|
|
433
|
+
# 标点不计入:内容在、标点异(: vs 无、() vs 无)不算丢失;比的是信息不是排印
|
|
434
|
+
s = ''.join(c for c in s if not unicodedata.category(c).startswith('P'))
|
|
435
|
+
return s
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _model_cov_strings(model) -> list:
|
|
439
|
+
"""从 REPORT_MODEL 递归抽取候选正文串(去重、保序)。"""
|
|
440
|
+
out, seen = [], set()
|
|
441
|
+
|
|
442
|
+
def walk(node, skip_sub=False):
|
|
443
|
+
if isinstance(node, dict):
|
|
444
|
+
# 信息图页型:chart 数据子树是故意占位(r_info),不计入
|
|
445
|
+
info_data = isinstance(node.get('type'), str) and node.get('type').lower() in _COV_INFO_TYPES
|
|
446
|
+
for k, v in node.items():
|
|
447
|
+
kl = str(k).lower()
|
|
448
|
+
if kl in _COV_SKIP_KEYS or str(k).startswith('data-'):
|
|
449
|
+
continue
|
|
450
|
+
if info_data and kl in _COV_INFO_DATA_KEYS:
|
|
451
|
+
continue
|
|
452
|
+
walk(v)
|
|
453
|
+
elif isinstance(node, (list, tuple)):
|
|
454
|
+
for v in node:
|
|
455
|
+
walk(v)
|
|
456
|
+
elif isinstance(node, str):
|
|
457
|
+
t = node.strip()
|
|
458
|
+
if len(t) < 4 or _COV_URL_RE.match(t) or _COV_COLOR_RE.match(t):
|
|
459
|
+
return
|
|
460
|
+
if _COV_FLAG_RE.match(t):
|
|
461
|
+
return # 渲染器语义 flag(如 focus),非正文
|
|
462
|
+
if t not in seen:
|
|
463
|
+
seen.add(t)
|
|
464
|
+
out.append(t)
|
|
465
|
+
|
|
466
|
+
walk(model)
|
|
467
|
+
return out
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _visible_cov_text(txt: str) -> str:
|
|
471
|
+
"""渲染后可见文本:剔 script(含 REPORT_MODEL JSON 本体)/style/注释/标签后归一化。"""
|
|
472
|
+
t = re.sub(r'<script[\s\S]*?</script>', '', txt, flags=re.I)
|
|
473
|
+
t = re.sub(r'<style[\s\S]*?</style>', '', t, flags=re.I)
|
|
474
|
+
t = re.sub(r'<!--[\s\S]*?-->', '', t)
|
|
475
|
+
t = re.sub(r'<[^>]+>', '', t)
|
|
476
|
+
return _norm_cov(_html.unescape(t))
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def _cov_credit(cand: str, rendered: str) -> float:
|
|
480
|
+
"""候选串的字符计分:整串命中按全长;否则按 8 字窗口命中率折算。"""
|
|
481
|
+
n = _norm_cov(cand)
|
|
482
|
+
if not n:
|
|
483
|
+
return 0.0
|
|
484
|
+
if n in rendered:
|
|
485
|
+
return float(len(n))
|
|
486
|
+
if len(n) <= 12:
|
|
487
|
+
return 0.0
|
|
488
|
+
win, hits, total = 8, 0, 0
|
|
489
|
+
for i in range(0, len(n), win):
|
|
490
|
+
total += 1
|
|
491
|
+
if n[i:i + win] in rendered:
|
|
492
|
+
hits += 1
|
|
493
|
+
return len(n) * hits / total if total else 0.0
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _check_content_coverage(txt, chk, model) -> None:
|
|
497
|
+
"""模型文本 → 渲染文本覆盖率(只 WARN)。"""
|
|
498
|
+
if not isinstance(model, dict):
|
|
499
|
+
return
|
|
500
|
+
try:
|
|
501
|
+
min_ratio = float((load_constants().get("qualityGates") or {}).get("contentCoverage")
|
|
502
|
+
or _COV_MIN_RATIO)
|
|
503
|
+
except Exception:
|
|
504
|
+
min_ratio = _COV_MIN_RATIO
|
|
505
|
+
cands = _model_cov_strings(model)
|
|
506
|
+
if not cands:
|
|
507
|
+
return
|
|
508
|
+
rendered = _visible_cov_text(txt)
|
|
509
|
+
total = sum(len(_norm_cov(c)) for c in cands)
|
|
510
|
+
if not total:
|
|
511
|
+
return
|
|
512
|
+
miss = []
|
|
513
|
+
got = 0.0
|
|
514
|
+
for c in cands:
|
|
515
|
+
cr = _cov_credit(c, rendered)
|
|
516
|
+
got += cr
|
|
517
|
+
if cr < len(_norm_cov(c)) * 0.5:
|
|
518
|
+
miss.append(c[:24])
|
|
519
|
+
ratio = got / total
|
|
520
|
+
chk(f"内容覆盖率(模型文本→渲染文本 ≥{min_ratio:.0%})", ratio >= min_ratio,
|
|
521
|
+
f"覆盖率 {ratio:.1%}({len(cands)} 串),疑似丢失: {';'.join(miss[:3])}" if ratio < min_ratio else "",
|
|
522
|
+
level="WARN")
|
|
523
|
+
|
|
524
|
+
|
|
373
525
|
def _check_icons(txt, chk):
|
|
374
526
|
"""图标使用:长报告不应全文无图标;含 ≥2 张卡的内容页应有 .card__ico(WARN)。"""
|
|
375
527
|
if len(_bands(txt)) < 6:
|
|
@@ -1630,6 +1782,9 @@ def main():
|
|
|
1630
1782
|
if len(sys.argv) < 2:
|
|
1631
1783
|
print(__doc__)
|
|
1632
1784
|
return 2
|
|
1785
|
+
if sys.argv[1] in ('-h', '--help'):
|
|
1786
|
+
print(__doc__)
|
|
1787
|
+
return 0
|
|
1633
1788
|
path = Path(sys.argv[1])
|
|
1634
1789
|
strict = '--strict' in sys.argv
|
|
1635
1790
|
as_json = '--json' in sys.argv
|
|
@@ -1797,8 +1952,10 @@ def main():
|
|
|
1797
1952
|
_check_mode_layouts(txt, chk, mode)
|
|
1798
1953
|
if mode != 'architecture':
|
|
1799
1954
|
_check_icons(txt, chk)
|
|
1955
|
+
_check_icon_refs(txt, chk)
|
|
1800
1956
|
_check_pptx_export(txt, chk)
|
|
1801
1957
|
model = _check_model_consistency(txt, chk, mode, style)
|
|
1958
|
+
_check_content_coverage(txt, chk, model)
|
|
1802
1959
|
_check_type_features(txt, chk, model)
|
|
1803
1960
|
_check_exhibits(txt, chk, mode)
|
|
1804
1961
|
_check_research_extras(txt, chk, mode)
|
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
A reusable skill for generating cover art for X long-form posts and WeChat articles. Goal: **striking, eye-catching, theme-focused**.
|
|
6
6
|
|
|
7
|
-
- Version: **v0.
|
|
7
|
+
- Version: **v0.2.0** (same tag as `@topmindspace/tms-skills@0.4.1`)
|
|
8
8
|
|
|
9
9
|
## Style gallery
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<img src="https://github.com/topmindspace/tms-skills/raw/main/topmind-cover/assets/examples/overview.png" alt="topmind-cover ·
|
|
12
|
+
<img src="https://github.com/topmindspace/tms-skills/raw/main/topmind-cover/assets/examples/overview.png" alt="topmind-cover · 11 cover-style overview" width="960" />
|
|
13
13
|
</p>
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
11 styles (clean white / background blur / paper collage / news flash / giant-type manifesto / tutorial steps / minimalist / editorial magazine / viral dry-goods / brand launch / fun IP, light-first ordering): use cases, palette stories, abstract design principles, and Chinese+English prompt recipes in [`references/cover-styles.md`](./references/cover-styles.md); full-size per-style shots in [`assets/examples/`](./assets/examples/).
|
|
16
16
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
@@ -22,7 +22,7 @@ npx @topmindspace/tms-skills install topmind-cover
|
|
|
22
22
|
|
|
23
23
|
## Usage
|
|
24
24
|
|
|
25
|
-
0. **Pick a style**: choose 1 of
|
|
25
|
+
0. **Pick a style**: choose 1 of 11 styles from `references/cover-styles.md` by topic (light-first) → check the example image to confirm the visual language.
|
|
26
26
|
1. **Refine the title**: lock the title copy first (usually ≤ 10 chars) using that style's title rules; rewrite until it lands — no drawing yet.
|
|
27
27
|
2. **Composition brief**: one-sentence brief (theme / audience / mood / visual metaphor / palette direction) before composing the prompt.
|
|
28
28
|
3. Input: article title + 3 theme keywords + platform (x / wechat / both, default both).
|