@topmindspace/tms-skills 2.0.0 → 2.0.2
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 +139 -123
- package/README.md +126 -103
- package/bin/tms-skills.js +160 -160
- package/package.json +48 -48
- package/top-ppt-html/README.md +254 -253
- package/top-ppt-html/SKILL.md +111 -111
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3873 -3926
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +167 -167
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +3873 -3926
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +167 -167
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +4272 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +320 -320
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +4272 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +320 -320
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4272 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +320 -320
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +5474 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +913 -913
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +5474 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +913 -913
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5474 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +913 -913
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +5474 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +913 -913
- package/top-ppt-html/assets/pptx-export.js +1944 -1944
- package/top-ppt-html/assets/style-gallery.html +559 -589
- package/top-ppt-html/assets/templates/architecture.html +3675 -3728
- package/top-ppt-html/assets/templates/engine.css +787 -840
- package/top-ppt-html/assets/templates/presentation.html +3685 -3738
- package/top-ppt-html/assets/templates/research.html +3964 -4017
- package/top-ppt-html/assets/templates/ui.js +520 -520
- package/top-ppt-html/assets/theme-overview-architecture.png +0 -0
- package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
- package/top-ppt-html/assets/theme-overview-research.png +0 -0
- package/top-ppt-html/assets/theme-overview.png +0 -0
- package/top-ppt-html/evals/prompts.csv +15 -15
- package/top-ppt-html/evals/rubric.schema.json +25 -25
- package/top-ppt-html/evals/run_evals.py +220 -220
- package/top-ppt-html/evals/trace.example.json +16 -16
- package/top-ppt-html/package-lock.json +175 -0
- package/top-ppt-html/package.json +30 -35
- package/top-ppt-html/references/charts-basic.md +624 -624
- package/top-ppt-html/references/charts-discipline.md +108 -108
- package/top-ppt-html/references/charts-extended.md +482 -482
- package/top-ppt-html/references/charts.md +28 -28
- package/top-ppt-html/references/components-atoms.md +624 -624
- package/top-ppt-html/references/components.md +30 -30
- package/top-ppt-html/references/content-rules.md +490 -490
- package/top-ppt-html/references/design-system-engine.md +235 -235
- package/top-ppt-html/references/design-system.md +471 -478
- package/top-ppt-html/references/failure-modes.md +214 -214
- package/top-ppt-html/references/high-fidelity.md +127 -127
- package/top-ppt-html/references/icons.md +397 -397
- package/top-ppt-html/references/industry-benchmark.md +105 -105
- package/top-ppt-html/references/infographics-stats.md +308 -308
- package/top-ppt-html/references/infographics-structure.md +226 -226
- package/top-ppt-html/references/infographics.md +43 -43
- package/top-ppt-html/references/layout-grammar.md +315 -315
- package/top-ppt-html/references/layouts-architecture.md +108 -108
- package/top-ppt-html/references/layouts-combo.md +600 -600
- package/top-ppt-html/references/layouts-research.md +160 -160
- package/top-ppt-html/references/modes.md +254 -254
- package/top-ppt-html/references/outline-design.md +275 -275
- package/top-ppt-html/references/playbook.md +266 -266
- package/top-ppt-html/references/pptx-export.md +209 -209
- package/top-ppt-html/references/reform-plan.md +252 -252
- package/top-ppt-html/references/styles.md +336 -370
- package/top-ppt-html/references/tech-design.md +138 -138
- package/top-ppt-html/scripts/audit_css.py +109 -109
- package/top-ppt-html/scripts/audit_docs.py +176 -176
- package/top-ppt-html/scripts/audit_skill.py +220 -220
- package/top-ppt-html/scripts/audit_styles.py +293 -351
- package/top-ppt-html/scripts/build_examples.py +2276 -2276
- package/top-ppt-html/scripts/build_pptx.js +2380 -2380
- package/top-ppt-html/scripts/capture_theme_overview.js +78 -78
- package/top-ppt-html/scripts/checks_html.py +127 -127
- package/top-ppt-html/scripts/cross_verify.py +294 -294
- package/top-ppt-html/scripts/env_probe.py +158 -158
- package/top-ppt-html/scripts/extract_model.py +210 -210
- package/top-ppt-html/scripts/extract_snippet.py +374 -374
- package/top-ppt-html/scripts/gen_channel_a.js +214 -214
- package/top-ppt-html/scripts/layout-constants.json +3309 -3377
- package/top-ppt-html/scripts/layout_slots.json +830 -830
- package/top-ppt-html/scripts/lib_layout_regions.js +412 -412
- package/top-ppt-html/scripts/measure_height.py +178 -178
- package/top-ppt-html/scripts/model-schema.json +547 -547
- package/top-ppt-html/scripts/negative_tests.py +307 -307
- package/top-ppt-html/scripts/package_skill.py +291 -291
- package/top-ppt-html/scripts/prepare_images.py +341 -341
- package/top-ppt-html/scripts/probe_image_export.py +188 -188
- package/top-ppt-html/scripts/quality_gate.py +301 -301
- package/top-ppt-html/scripts/regression.py +307 -308
- package/top-ppt-html/scripts/render_compare.py +275 -275
- package/top-ppt-html/scripts/render_from_model.py +698 -698
- package/top-ppt-html/scripts/scaffold_report.py +1054 -1054
- package/top-ppt-html/scripts/section-file-map.json +104 -104
- package/top-ppt-html/scripts/sync_runtime.py +662 -662
- package/top-ppt-html/scripts/validate_pptx.py +1510 -1510
- package/top-ppt-html/scripts/validate_report.py +1456 -1456
|
@@ -1,351 +1,293 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""TopPPT HTML · 风格与单源质量审计工具:
|
|
4
|
-
① WCAG 对比度:9 风格 × light/dark 的关键 token 配对(MD3/WCAG 阈值)
|
|
5
|
-
② 双源一致性:engine.css(HTML 侧 token)vs layout-constants.json(PPTX 侧 token)
|
|
6
|
-
③ 单源完整性:styleAccents 覆盖各风格 light/dark 强调色;charts.minSize ⊆ charts.types;
|
|
7
|
-
图表最小尺寸键名合法(pxWidthMin / vbHeightMin)
|
|
8
|
-
用法:python scripts/audit_styles.py(改任何风格色值或常量后必跑;退出码 0 = 全部通过)
|
|
9
|
-
"""
|
|
10
|
-
import json
|
|
11
|
-
import re
|
|
12
|
-
import sys
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
16
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
17
|
-
CSS = (ROOT / 'assets' / 'templates' / 'engine.css').read_text(encoding='utf-8')
|
|
18
|
-
LC = json.loads((ROOT / 'scripts' / 'layout-constants.json').read_text(encoding='utf-8'))
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
def parse_block(block):
|
|
22
|
-
toks = {}
|
|
23
|
-
for m in re.finditer(r'--([\w-]+)\s*:\s*(#[0-9a-fA-F]{3,8})', block):
|
|
24
|
-
toks[m.group(1)] = m.group(2).lower()
|
|
25
|
-
return toks
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def parse_css():
|
|
29
|
-
"""返回 {style: {'light': toks, 'dark': toks}}。
|
|
30
|
-
9 套风格均有显式 html[data-style="X"]{...}(light)与
|
|
31
|
-
html[data-style="X"][data-theme="dark"]{...}(dark)两块;:root 仅作无 data-style 时的默认值。"""
|
|
32
|
-
styles = {}
|
|
33
|
-
pat = re.compile(r'html\[data-style="([\w-]+)"\](?:\[data-theme="dark"\])?\{([\s\S]*?)\}')
|
|
34
|
-
for m in pat.finditer(CSS):
|
|
35
|
-
sid, body = m.group(1), m.group(2)
|
|
36
|
-
sel = m.group(0).split('{', 1)[0]
|
|
37
|
-
side = 'dark' if 'data-theme="dark"' in sel else 'light'
|
|
38
|
-
# 同一 (风格, 主题) 可能由多个块叠加(基础 token 块 + 编码色板块)→ 合并而非覆盖
|
|
39
|
-
styles.setdefault(sid, {}).setdefault(side, {}).update(parse_block(body))
|
|
40
|
-
return styles
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def rel_lum(hexc):
|
|
44
|
-
h = hexc.lstrip('#')
|
|
45
|
-
if len(h) == 3:
|
|
46
|
-
h = ''.join(c * 2 for c in h)
|
|
47
|
-
r, g, b = (int(h[i:i + 2], 16) / 255 for i in (0, 2, 4))
|
|
48
|
-
def f(c):
|
|
49
|
-
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
|
50
|
-
r, g, b = f(r), f(g), f(b)
|
|
51
|
-
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def contrast(a, b):
|
|
55
|
-
la, lb = rel_lum(a), rel_lum(b)
|
|
56
|
-
hi, lo = max(la, lb), min(la, lb)
|
|
57
|
-
return (hi + 0.05) / (lo + 0.05)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def hex2rgb(h):
|
|
61
|
-
h = h.lstrip('#')
|
|
62
|
-
if len(h) == 3:
|
|
63
|
-
h = ''.join(c * 2 for c in h)
|
|
64
|
-
return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def rgb2hls(r, g, b):
|
|
68
|
-
"""返回 (色相°, 饱和, 明度) —— 用于编码色板两两可区分性判定。"""
|
|
69
|
-
r, g, b = r / 255, g / 255, b / 255
|
|
70
|
-
mx, mn = max(r, g, b), min(r, g, b)
|
|
71
|
-
l = (mx + mn) / 2
|
|
72
|
-
if mx == mn:
|
|
73
|
-
return 0.0, 0.0, l
|
|
74
|
-
d = mx - mn
|
|
75
|
-
s = d / (2 - mx - mn) if l > 0.5 else d / (mx + mn)
|
|
76
|
-
if mx == r:
|
|
77
|
-
h = ((g - b) / d) % 6
|
|
78
|
-
elif mx == g:
|
|
79
|
-
h = (b - r) / d + 2
|
|
80
|
-
else:
|
|
81
|
-
h = (r - g) / d + 4
|
|
82
|
-
return h * 60, s, l
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
CHECKS = [
|
|
86
|
-
# (名称, token对, 阈值) 阈值依据:WCAG AA 正文 4.5 / 大字与图形 3.0
|
|
87
|
-
('正文 text / bg', ('text', 'bg'), 4.5),
|
|
88
|
-
('次级 text-2 / bg', ('text-2', 'bg'), 4.5),
|
|
89
|
-
('弱化 text-3 / bg(注解)', ('text-3', 'bg'), 3.0),
|
|
90
|
-
('强调文字 accent-text / bg', ('accent-text', 'bg'), 4.5),
|
|
91
|
-
('强调色 accent / bg(图形)', ('accent', 'bg'), 3.0),
|
|
92
|
-
('按钮文字 accent-on / accent', ('accent-on', 'accent'), 4.5),
|
|
93
|
-
('正文 text / surface-1', ('text', 'surface-1'), 4.5),
|
|
94
|
-
('次级 text-2 / surface-1', ('text-2', 'surface-1'), 4.5),
|
|
95
|
-
]
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def audit_contrast(styles):
|
|
99
|
-
fails = []
|
|
100
|
-
for sid in sorted(styles):
|
|
101
|
-
for theme in ('light', 'dark'):
|
|
102
|
-
t = styles[sid].get(theme)
|
|
103
|
-
if not t or 'bg' not in t:
|
|
104
|
-
continue
|
|
105
|
-
for name, (fg, bg), th in CHECKS:
|
|
106
|
-
if fg not in t or bg not in t:
|
|
107
|
-
continue
|
|
108
|
-
c = contrast(t[fg], t[bg])
|
|
109
|
-
if c < th:
|
|
110
|
-
fails.append(f'{sid}.{theme}: {name} = {c:.2f} < {th} ({t[fg]} on {t[bg]})')
|
|
111
|
-
return fails
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def audit_consistency(styles, lc_styles, side='light', lc_key='styles'):
|
|
115
|
-
"""PPTX 常量(layout-constants.json)与 CSS token 的映射对照(归一化 # 前缀与 3 位缩写)。
|
|
116
|
-
side='dark' 时对照 stylesDark ↔ engine.css 深色块(PPTX 深色导出与 HTML 深色主题同源)。"""
|
|
117
|
-
def norm(v):
|
|
118
|
-
v = (v or '').lower().lstrip('#')
|
|
119
|
-
if len(v) == 3:
|
|
120
|
-
v = ''.join(c * 2 for c in v)
|
|
121
|
-
return v
|
|
122
|
-
MAP = [('ink', 'text'), ('body', 'text-2'), ('faint', 'text-3'),
|
|
123
|
-
('bg', 'bg'), ('surface', 'surface-1'), ('line', 'border'),
|
|
124
|
-
('accent', 'accent'), ('onAccent', 'accent-on'), ('soft', 'accent-soft')]
|
|
125
|
-
diffs = []
|
|
126
|
-
for sid, pt in lc_styles.items():
|
|
127
|
-
css = styles.get(sid, {}).get(side, {})
|
|
128
|
-
for jk, ck in MAP:
|
|
129
|
-
jv, cv = norm(pt.get(jk)), norm(css.get(ck))
|
|
130
|
-
if jv and cv and jv != cv:
|
|
131
|
-
diffs.append(f'{sid}.{side}: JSON.{jk}={jv} != CSS.{ck}={cv}')
|
|
132
|
-
elif jv and not cv:
|
|
133
|
-
diffs.append(f'{sid}.{side}: CSS 缺 {ck}')
|
|
134
|
-
if not lc_styles:
|
|
135
|
-
diffs.append(f'layout-constants.json 缺 {lc_key}(起 PPTX 深色导出依赖它)')
|
|
136
|
-
return diffs
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def audit_data_palette(styles):
|
|
140
|
-
"""编码色板审计:9 套风格 × light/dark,各 5 色。
|
|
141
|
-
① 覆盖完整(styleDataColors / styleDataColorsDark 与 styles / stylesDark 键集一致)
|
|
142
|
-
② 每套 5 色、与 engine.css 覆盖块逐值一致(双源)
|
|
143
|
-
③ 每个色对所在主题 bg 的对比度 ≥ 3.0(图形元素可辨下限)
|
|
144
|
-
④ 同套内两两可区分(色相差 ≥25° 或明度差 ≥0.12)"""
|
|
145
|
-
diffs = []
|
|
146
|
-
for key, theme in (('styleDataColors', 'light'), ('styleDataColorsDark', 'dark')):
|
|
147
|
-
table = {k: v for k, v in (LC.get(key) or {}).items() if not k.startswith('$')}
|
|
148
|
-
base = {k: v for k, v in (LC.get('styles' if theme == 'light' else 'stylesDark') or {}).items()
|
|
149
|
-
if not k.startswith('$')}
|
|
150
|
-
for sid in base:
|
|
151
|
-
if sid not in table:
|
|
152
|
-
diffs.append(f'{key} 缺风格 {sid}')
|
|
153
|
-
for sid, cols in table.items():
|
|
154
|
-
if len(cols) != 5:
|
|
155
|
-
diffs.append(f'{key}.{sid} 色数 {len(cols)} != 5')
|
|
156
|
-
continue
|
|
157
|
-
css = styles.get(sid, {}).get(theme, {})
|
|
158
|
-
for i, c in enumerate(cols, 1):
|
|
159
|
-
if c.startswith('#'):
|
|
160
|
-
diffs.append(f'{key}.{sid}.c{i}={c} 不得带 # 前缀(会原样流入 OOXML srgbClr val)')
|
|
161
|
-
continue
|
|
162
|
-
cv = (css.get(f'c{i}') or '').lower().lstrip('#')
|
|
163
|
-
if not cv:
|
|
164
|
-
diffs.append(f'{key}.{sid}.c{i} 未在 engine.css {theme} 块中定义')
|
|
165
|
-
elif cv != c.lower().lstrip('#'):
|
|
166
|
-
diffs.append(f'{key}.{sid}.c{i}={c} != engine.css {cv}')
|
|
167
|
-
bg = (base.get(sid) or {}).get('bg') or ('#ffffff' if theme == 'light' else '#000000')
|
|
168
|
-
if not bg.startswith('#'):
|
|
169
|
-
bg = '#' + bg
|
|
170
|
-
for c in cols:
|
|
171
|
-
if contrast(c if c.startswith('#') else '#' + c, bg) < 3.0:
|
|
172
|
-
diffs.append(f'{key}.{sid}: {c} 对 bg {bg} 对比 {contrast(c, bg):.2f} < 3.0')
|
|
173
|
-
hls = [rgb2hls(*hex2rgb(c)) for c in cols]
|
|
174
|
-
for i in range(5):
|
|
175
|
-
for j in range(i + 1, 5):
|
|
176
|
-
dh = abs(hls[i][0] - hls[j][0]) % 360
|
|
177
|
-
dh = min(dh, 360 - dh)
|
|
178
|
-
if dh < 25 and abs(hls[i][2] - hls[j][2]) < 0.12:
|
|
179
|
-
diffs.append(f'{key}.{sid}: c{i + 1}/c{j + 1} 不可区分(Δh={dh:.0f}°)')
|
|
180
|
-
return diffs
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
def
|
|
184
|
-
"""
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
for
|
|
225
|
-
if
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
for
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
for
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
print(f'
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if c_fails:
|
|
295
|
-
for f in c_fails:
|
|
296
|
-
print(' [FAIL]', f)
|
|
297
|
-
else:
|
|
298
|
-
print(f' 全部通过({n_styles} 风格 × light/dark × {len(CHECKS)} 组配对 ≥ WCAG 阈值)')
|
|
299
|
-
|
|
300
|
-
d_fails = audit_consistency(css_styles, LC['styles'], 'light', 'styles')
|
|
301
|
-
print('\n── 双源 token 一致性 · 浅色(engine.css ↔ layout-constants.json styles)──')
|
|
302
|
-
if d_fails:
|
|
303
|
-
for f in d_fails:
|
|
304
|
-
print(' [DIFF]', f)
|
|
305
|
-
else:
|
|
306
|
-
print(f' 全部一致({n_styles} 风格 × 9 字段)')
|
|
307
|
-
|
|
308
|
-
dd_fails = audit_consistency(css_styles, LC.get('stylesDark') or {}, 'dark', 'stylesDark')
|
|
309
|
-
print('\n── 双源 token 一致性 · 深色(engine.css dark 块 ↔ layout-constants.json stylesDark)──')
|
|
310
|
-
if dd_fails:
|
|
311
|
-
for f in dd_fails:
|
|
312
|
-
print(' [DIFF]', f)
|
|
313
|
-
else:
|
|
314
|
-
print(f' 全部一致({n_styles} 风格 × 9 字段 · PPTX 深色导出与 HTML 深色主题同源)')
|
|
315
|
-
|
|
316
|
-
s_fails = audit_single_source()
|
|
317
|
-
print('\n── 单源完整性(styleAccents / charts 登记表 / 校验预算键)──')
|
|
318
|
-
if s_fails:
|
|
319
|
-
for f in s_fails:
|
|
320
|
-
print(' [DIFF]', f)
|
|
321
|
-
else:
|
|
322
|
-
n_charts = len((LC.get('charts') or {}).get('types') or [])
|
|
323
|
-
n_acc = len([k for k in (LC.get('styleAccents') or {}) if not k.startswith('$')])
|
|
324
|
-
print(f' 全部一致(强调色表 {n_acc} 套 · 图表登记 {n_charts} 种 · 校验预算 {len([k for k in (LC.get("checkBudgets") or {}) if not k.startswith("$")])} 模式)')
|
|
325
|
-
|
|
326
|
-
dp_fails = audit_data_palette(css_styles)
|
|
327
|
-
print('\n── 编码色板(9 风格 × light/dark × c1–c5 · 双源 + 对比度 + 可区分性)──')
|
|
328
|
-
if dp_fails:
|
|
329
|
-
for f in dp_fails:
|
|
330
|
-
print(' [DIFF]', f)
|
|
331
|
-
else:
|
|
332
|
-
print(f' 全部通过({len([k for k in (LC.get("styleDataColors") or {}) if not k.startswith("$")])} 套 × 2 主题 × 5 色)')
|
|
333
|
-
|
|
334
|
-
u_fails = audit_ui_palette()
|
|
335
|
-
print('\n── ui.js 风格下拉色板 ↔ layout-constants.json accent ──')
|
|
336
|
-
if u_fails:
|
|
337
|
-
for f in u_fails:
|
|
338
|
-
print(' [DIFF]', f)
|
|
339
|
-
else:
|
|
340
|
-
print(f' 全部一致({n_styles} 套色板 = {n_styles} 套 accent)')
|
|
341
|
-
|
|
342
|
-
id_fails = audit_style_identity(css_styles)
|
|
343
|
-
print('\n── 风格语汇(font / radius / typeBoost / serifDisplay / link · engine.css ↔ styleIdentity)──')
|
|
344
|
-
if id_fails:
|
|
345
|
-
for f in id_fails:
|
|
346
|
-
print(' [DIFF]', f)
|
|
347
|
-
else:
|
|
348
|
-
n_id = len([k for k in (LC.get('styleIdentity') or {}) if not k.startswith('$')])
|
|
349
|
-
print(f' 全部落地({n_id} 套风格语汇与 engine.css 覆盖块一致)')
|
|
350
|
-
|
|
351
|
-
sys.exit(1 if (c_fails or d_fails or dd_fails or s_fails or dp_fails or u_fails or id_fails) else 0)
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML · 风格与单源质量审计工具:
|
|
4
|
+
① WCAG 对比度:9 风格 × light/dark 的关键 token 配对(MD3/WCAG 阈值)
|
|
5
|
+
② 双源一致性:engine.css(HTML 侧 token)vs layout-constants.json(PPTX 侧 token)
|
|
6
|
+
③ 单源完整性:styleAccents 覆盖各风格 light/dark 强调色;charts.minSize ⊆ charts.types;
|
|
7
|
+
图表最小尺寸键名合法(pxWidthMin / vbHeightMin)
|
|
8
|
+
用法:python scripts/audit_styles.py(改任何风格色值或常量后必跑;退出码 0 = 全部通过)
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
16
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
17
|
+
CSS = (ROOT / 'assets' / 'templates' / 'engine.css').read_text(encoding='utf-8')
|
|
18
|
+
LC = json.loads((ROOT / 'scripts' / 'layout-constants.json').read_text(encoding='utf-8'))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_block(block):
|
|
22
|
+
toks = {}
|
|
23
|
+
for m in re.finditer(r'--([\w-]+)\s*:\s*(#[0-9a-fA-F]{3,8})', block):
|
|
24
|
+
toks[m.group(1)] = m.group(2).lower()
|
|
25
|
+
return toks
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_css():
|
|
29
|
+
"""返回 {style: {'light': toks, 'dark': toks}}。
|
|
30
|
+
9 套风格均有显式 html[data-style="X"]{...}(light)与
|
|
31
|
+
html[data-style="X"][data-theme="dark"]{...}(dark)两块;:root 仅作无 data-style 时的默认值。"""
|
|
32
|
+
styles = {}
|
|
33
|
+
pat = re.compile(r'html\[data-style="([\w-]+)"\](?:\[data-theme="dark"\])?\{([\s\S]*?)\}')
|
|
34
|
+
for m in pat.finditer(CSS):
|
|
35
|
+
sid, body = m.group(1), m.group(2)
|
|
36
|
+
sel = m.group(0).split('{', 1)[0]
|
|
37
|
+
side = 'dark' if 'data-theme="dark"' in sel else 'light'
|
|
38
|
+
# 同一 (风格, 主题) 可能由多个块叠加(基础 token 块 + 编码色板块)→ 合并而非覆盖
|
|
39
|
+
styles.setdefault(sid, {}).setdefault(side, {}).update(parse_block(body))
|
|
40
|
+
return styles
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def rel_lum(hexc):
|
|
44
|
+
h = hexc.lstrip('#')
|
|
45
|
+
if len(h) == 3:
|
|
46
|
+
h = ''.join(c * 2 for c in h)
|
|
47
|
+
r, g, b = (int(h[i:i + 2], 16) / 255 for i in (0, 2, 4))
|
|
48
|
+
def f(c):
|
|
49
|
+
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
|
|
50
|
+
r, g, b = f(r), f(g), f(b)
|
|
51
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def contrast(a, b):
|
|
55
|
+
la, lb = rel_lum(a), rel_lum(b)
|
|
56
|
+
hi, lo = max(la, lb), min(la, lb)
|
|
57
|
+
return (hi + 0.05) / (lo + 0.05)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def hex2rgb(h):
|
|
61
|
+
h = h.lstrip('#')
|
|
62
|
+
if len(h) == 3:
|
|
63
|
+
h = ''.join(c * 2 for c in h)
|
|
64
|
+
return tuple(int(h[i:i + 2], 16) for i in (0, 2, 4))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def rgb2hls(r, g, b):
|
|
68
|
+
"""返回 (色相°, 饱和, 明度) —— 用于编码色板两两可区分性判定。"""
|
|
69
|
+
r, g, b = r / 255, g / 255, b / 255
|
|
70
|
+
mx, mn = max(r, g, b), min(r, g, b)
|
|
71
|
+
l = (mx + mn) / 2
|
|
72
|
+
if mx == mn:
|
|
73
|
+
return 0.0, 0.0, l
|
|
74
|
+
d = mx - mn
|
|
75
|
+
s = d / (2 - mx - mn) if l > 0.5 else d / (mx + mn)
|
|
76
|
+
if mx == r:
|
|
77
|
+
h = ((g - b) / d) % 6
|
|
78
|
+
elif mx == g:
|
|
79
|
+
h = (b - r) / d + 2
|
|
80
|
+
else:
|
|
81
|
+
h = (r - g) / d + 4
|
|
82
|
+
return h * 60, s, l
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
CHECKS = [
|
|
86
|
+
# (名称, token对, 阈值) 阈值依据:WCAG AA 正文 4.5 / 大字与图形 3.0
|
|
87
|
+
('正文 text / bg', ('text', 'bg'), 4.5),
|
|
88
|
+
('次级 text-2 / bg', ('text-2', 'bg'), 4.5),
|
|
89
|
+
('弱化 text-3 / bg(注解)', ('text-3', 'bg'), 3.0),
|
|
90
|
+
('强调文字 accent-text / bg', ('accent-text', 'bg'), 4.5),
|
|
91
|
+
('强调色 accent / bg(图形)', ('accent', 'bg'), 3.0),
|
|
92
|
+
('按钮文字 accent-on / accent', ('accent-on', 'accent'), 4.5),
|
|
93
|
+
('正文 text / surface-1', ('text', 'surface-1'), 4.5),
|
|
94
|
+
('次级 text-2 / surface-1', ('text-2', 'surface-1'), 4.5),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def audit_contrast(styles):
|
|
99
|
+
fails = []
|
|
100
|
+
for sid in sorted(styles):
|
|
101
|
+
for theme in ('light', 'dark'):
|
|
102
|
+
t = styles[sid].get(theme)
|
|
103
|
+
if not t or 'bg' not in t:
|
|
104
|
+
continue
|
|
105
|
+
for name, (fg, bg), th in CHECKS:
|
|
106
|
+
if fg not in t or bg not in t:
|
|
107
|
+
continue
|
|
108
|
+
c = contrast(t[fg], t[bg])
|
|
109
|
+
if c < th:
|
|
110
|
+
fails.append(f'{sid}.{theme}: {name} = {c:.2f} < {th} ({t[fg]} on {t[bg]})')
|
|
111
|
+
return fails
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def audit_consistency(styles, lc_styles, side='light', lc_key='styles'):
|
|
115
|
+
"""PPTX 常量(layout-constants.json)与 CSS token 的映射对照(归一化 # 前缀与 3 位缩写)。
|
|
116
|
+
side='dark' 时对照 stylesDark ↔ engine.css 深色块(PPTX 深色导出与 HTML 深色主题同源)。"""
|
|
117
|
+
def norm(v):
|
|
118
|
+
v = (v or '').lower().lstrip('#')
|
|
119
|
+
if len(v) == 3:
|
|
120
|
+
v = ''.join(c * 2 for c in v)
|
|
121
|
+
return v
|
|
122
|
+
MAP = [('ink', 'text'), ('body', 'text-2'), ('faint', 'text-3'),
|
|
123
|
+
('bg', 'bg'), ('surface', 'surface-1'), ('line', 'border'),
|
|
124
|
+
('accent', 'accent'), ('onAccent', 'accent-on'), ('soft', 'accent-soft')]
|
|
125
|
+
diffs = []
|
|
126
|
+
for sid, pt in lc_styles.items():
|
|
127
|
+
css = styles.get(sid, {}).get(side, {})
|
|
128
|
+
for jk, ck in MAP:
|
|
129
|
+
jv, cv = norm(pt.get(jk)), norm(css.get(ck))
|
|
130
|
+
if jv and cv and jv != cv:
|
|
131
|
+
diffs.append(f'{sid}.{side}: JSON.{jk}={jv} != CSS.{ck}={cv}')
|
|
132
|
+
elif jv and not cv:
|
|
133
|
+
diffs.append(f'{sid}.{side}: CSS 缺 {ck}')
|
|
134
|
+
if not lc_styles:
|
|
135
|
+
diffs.append(f'layout-constants.json 缺 {lc_key}(起 PPTX 深色导出依赖它)')
|
|
136
|
+
return diffs
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def audit_data_palette(styles):
|
|
140
|
+
"""编码色板审计:9 套风格 × light/dark,各 5 色。
|
|
141
|
+
① 覆盖完整(styleDataColors / styleDataColorsDark 与 styles / stylesDark 键集一致)
|
|
142
|
+
② 每套 5 色、与 engine.css 覆盖块逐值一致(双源)
|
|
143
|
+
③ 每个色对所在主题 bg 的对比度 ≥ 3.0(图形元素可辨下限)
|
|
144
|
+
④ 同套内两两可区分(色相差 ≥25° 或明度差 ≥0.12)"""
|
|
145
|
+
diffs = []
|
|
146
|
+
for key, theme in (('styleDataColors', 'light'), ('styleDataColorsDark', 'dark')):
|
|
147
|
+
table = {k: v for k, v in (LC.get(key) or {}).items() if not k.startswith('$')}
|
|
148
|
+
base = {k: v for k, v in (LC.get('styles' if theme == 'light' else 'stylesDark') or {}).items()
|
|
149
|
+
if not k.startswith('$')}
|
|
150
|
+
for sid in base:
|
|
151
|
+
if sid not in table:
|
|
152
|
+
diffs.append(f'{key} 缺风格 {sid}')
|
|
153
|
+
for sid, cols in table.items():
|
|
154
|
+
if len(cols) != 5:
|
|
155
|
+
diffs.append(f'{key}.{sid} 色数 {len(cols)} != 5')
|
|
156
|
+
continue
|
|
157
|
+
css = styles.get(sid, {}).get(theme, {})
|
|
158
|
+
for i, c in enumerate(cols, 1):
|
|
159
|
+
if c.startswith('#'):
|
|
160
|
+
diffs.append(f'{key}.{sid}.c{i}={c} 不得带 # 前缀(会原样流入 OOXML srgbClr val)')
|
|
161
|
+
continue
|
|
162
|
+
cv = (css.get(f'c{i}') or '').lower().lstrip('#')
|
|
163
|
+
if not cv:
|
|
164
|
+
diffs.append(f'{key}.{sid}.c{i} 未在 engine.css {theme} 块中定义')
|
|
165
|
+
elif cv != c.lower().lstrip('#'):
|
|
166
|
+
diffs.append(f'{key}.{sid}.c{i}={c} != engine.css {cv}')
|
|
167
|
+
bg = (base.get(sid) or {}).get('bg') or ('#ffffff' if theme == 'light' else '#000000')
|
|
168
|
+
if not bg.startswith('#'):
|
|
169
|
+
bg = '#' + bg
|
|
170
|
+
for c in cols:
|
|
171
|
+
if contrast(c if c.startswith('#') else '#' + c, bg) < 3.0:
|
|
172
|
+
diffs.append(f'{key}.{sid}: {c} 对 bg {bg} 对比 {contrast(c, bg):.2f} < 3.0')
|
|
173
|
+
hls = [rgb2hls(*hex2rgb(c)) for c in cols]
|
|
174
|
+
for i in range(5):
|
|
175
|
+
for j in range(i + 1, 5):
|
|
176
|
+
dh = abs(hls[i][0] - hls[j][0]) % 360
|
|
177
|
+
dh = min(dh, 360 - dh)
|
|
178
|
+
if dh < 25 and abs(hls[i][2] - hls[j][2]) < 0.12:
|
|
179
|
+
diffs.append(f'{key}.{sid}: c{i + 1}/c{j + 1} 不可区分(Δh={dh:.0f}°)')
|
|
180
|
+
return diffs
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def audit_ui_palette():
|
|
184
|
+
"""ui.js 的 header 风格下拉色板必须与 layout-constants.json 的 accent 一致
|
|
185
|
+
(ui.js 在运行时块之前执行,无法直接读 PRESETS,故用审计兜住这最后一处手写色值)。"""
|
|
186
|
+
ui = ROOT / 'assets' / 'templates' / 'ui.js'
|
|
187
|
+
if not ui.exists():
|
|
188
|
+
return ['assets/templates/ui.js 不存在']
|
|
189
|
+
txt = ui.read_text(encoding='utf-8')
|
|
190
|
+
m = re.search(r'var STYLES=\[([\s\S]*?)\];', txt)
|
|
191
|
+
if not m:
|
|
192
|
+
return ['ui.js 未找到 var STYLES 色板数组']
|
|
193
|
+
diffs = []
|
|
194
|
+
pairs = re.findall(r"\[\s*'([\w-]+)'\s*,\s*'[^']*'\s*,\s*'(#[0-9a-fA-F]{3,8})'\s*\]", m.group(1))
|
|
195
|
+
if not pairs:
|
|
196
|
+
return ['ui.js STYLES 色板解析失败(格式变化?)']
|
|
197
|
+
for sid, hexv in pairs:
|
|
198
|
+
acc = str(((LC.get('styles') or {}).get(sid) or {}).get('accent') or '')
|
|
199
|
+
if not acc:
|
|
200
|
+
diffs.append(f'ui.js 色板含未知风格 {sid}')
|
|
201
|
+
elif acc.lower().lstrip('#') != hexv.lower().lstrip('#'):
|
|
202
|
+
diffs.append(f'ui.js 色板 {sid} = {hexv} != layout-constants accent #{acc}')
|
|
203
|
+
missing = [s for s in (LC.get('styles') or {}) if s not in {p[0] for p in pairs}]
|
|
204
|
+
for s in missing:
|
|
205
|
+
diffs.append(f'ui.js 色板缺风格 {s}')
|
|
206
|
+
return diffs
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def audit_single_source():
|
|
210
|
+
"""单源完整性:styleAccents 覆盖 / charts 登记表自洽 / 图表最小尺寸键名合法。"""
|
|
211
|
+
diffs = []
|
|
212
|
+
accents = {k: v for k, v in (LC.get('styleAccents') or {}).items() if not k.startswith('$')}
|
|
213
|
+
for key, side in (('styles', 'light'), ('stylesDark', 'dark')):
|
|
214
|
+
for sid, toks in (LC.get(key) or {}).items():
|
|
215
|
+
acc = str(toks.get('accent') or '').lower().lstrip('#')
|
|
216
|
+
have = {str(x).lower().lstrip('#') for x in (accents.get(sid) or [])}
|
|
217
|
+
if acc and acc not in have:
|
|
218
|
+
diffs.append(f'styleAccents.{sid} 未收录 {side} 强调色 #{acc}(单一强调色检查会漏判)')
|
|
219
|
+
missing = [s for s in (LC.get('styles') or {}) if s not in accents]
|
|
220
|
+
for s in missing:
|
|
221
|
+
diffs.append(f'styleAccents 缺风格 {s}')
|
|
222
|
+
charts = LC.get('charts') or {}
|
|
223
|
+
types = set(charts.get('types') or [])
|
|
224
|
+
for t, rule in (charts.get('minSize') or {}).items():
|
|
225
|
+
if types and t not in types:
|
|
226
|
+
diffs.append(f'charts.minSize.{t} 未登记在 charts.types')
|
|
227
|
+
for k in (rule or {}):
|
|
228
|
+
if k not in ('pxWidthMin', 'vbHeightMin'):
|
|
229
|
+
diffs.append(f'charts.minSize.{t}.{k} 键名非法(应为 pxWidthMin / vbHeightMin)')
|
|
230
|
+
for t in types:
|
|
231
|
+
if t not in (charts.get('minSize') or {}):
|
|
232
|
+
diffs.append(f'charts.types.{t} 缺最小尺寸登记(校验器将跳过该类型)')
|
|
233
|
+
for key in ('checkBudgets', 'styleAccents', 'aiFlavor', 'pageTypeGeometry',
|
|
234
|
+
'styleDataColors', 'styleDataColorsDark'):
|
|
235
|
+
if key not in LC:
|
|
236
|
+
diffs.append(f'layout-constants.json 缺 {key}(validate_report.py 依赖它做单源校验)')
|
|
237
|
+
return diffs
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
css_styles = parse_css()
|
|
241
|
+
n_styles = len(css_styles)
|
|
242
|
+
print(f'解析到 {n_styles} 套风格(CSS 侧)')
|
|
243
|
+
c_fails = audit_contrast(css_styles)
|
|
244
|
+
print('\n── WCAG 对比度审计 ──')
|
|
245
|
+
if c_fails:
|
|
246
|
+
for f in c_fails:
|
|
247
|
+
print(' [FAIL]', f)
|
|
248
|
+
else:
|
|
249
|
+
print(f' 全部通过({n_styles} 风格 × light/dark × {len(CHECKS)} 组配对 ≥ WCAG 阈值)')
|
|
250
|
+
|
|
251
|
+
d_fails = audit_consistency(css_styles, LC['styles'], 'light', 'styles')
|
|
252
|
+
print('\n── 双源 token 一致性 · 浅色(engine.css ↔ layout-constants.json styles)──')
|
|
253
|
+
if d_fails:
|
|
254
|
+
for f in d_fails:
|
|
255
|
+
print(' [DIFF]', f)
|
|
256
|
+
else:
|
|
257
|
+
print(f' 全部一致({n_styles} 风格 × 9 字段)')
|
|
258
|
+
|
|
259
|
+
dd_fails = audit_consistency(css_styles, LC.get('stylesDark') or {}, 'dark', 'stylesDark')
|
|
260
|
+
print('\n── 双源 token 一致性 · 深色(engine.css dark 块 ↔ layout-constants.json stylesDark)──')
|
|
261
|
+
if dd_fails:
|
|
262
|
+
for f in dd_fails:
|
|
263
|
+
print(' [DIFF]', f)
|
|
264
|
+
else:
|
|
265
|
+
print(f' 全部一致({n_styles} 风格 × 9 字段 · PPTX 深色导出与 HTML 深色主题同源)')
|
|
266
|
+
|
|
267
|
+
s_fails = audit_single_source()
|
|
268
|
+
print('\n── 单源完整性(styleAccents / charts 登记表 / 校验预算键)──')
|
|
269
|
+
if s_fails:
|
|
270
|
+
for f in s_fails:
|
|
271
|
+
print(' [DIFF]', f)
|
|
272
|
+
else:
|
|
273
|
+
n_charts = len((LC.get('charts') or {}).get('types') or [])
|
|
274
|
+
n_acc = len([k for k in (LC.get('styleAccents') or {}) if not k.startswith('$')])
|
|
275
|
+
print(f' 全部一致(强调色表 {n_acc} 套 · 图表登记 {n_charts} 种 · 校验预算 {len([k for k in (LC.get("checkBudgets") or {}) if not k.startswith("$")])} 模式)')
|
|
276
|
+
|
|
277
|
+
dp_fails = audit_data_palette(css_styles)
|
|
278
|
+
print('\n── 编码色板(9 风格 × light/dark × c1–c5 · 双源 + 对比度 + 可区分性)──')
|
|
279
|
+
if dp_fails:
|
|
280
|
+
for f in dp_fails:
|
|
281
|
+
print(' [DIFF]', f)
|
|
282
|
+
else:
|
|
283
|
+
print(f' 全部通过({len([k for k in (LC.get("styleDataColors") or {}) if not k.startswith("$")])} 套 × 2 主题 × 5 色)')
|
|
284
|
+
|
|
285
|
+
u_fails = audit_ui_palette()
|
|
286
|
+
print('\n── ui.js 风格下拉色板 ↔ layout-constants.json accent ──')
|
|
287
|
+
if u_fails:
|
|
288
|
+
for f in u_fails:
|
|
289
|
+
print(' [DIFF]', f)
|
|
290
|
+
else:
|
|
291
|
+
print(f' 全部一致({n_styles} 套色板 = {n_styles} 套 accent)')
|
|
292
|
+
|
|
293
|
+
sys.exit(1 if (c_fails or d_fails or dd_fails or s_fails or dp_fails or u_fails) else 0)
|