@topmindspace/tms-skills 0.1.0 → 0.1.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 +76 -0
- package/README.md +9 -3
- package/bin/tms-skills.js +200 -160
- package/package.json +12 -9
- package/top-ppt-html/README.md +162 -254
- package/top-ppt-html/SKILL.md +126 -111
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3926 -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-presentation-business-blue.html +4325 -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-mckinsey.html +5527 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +913 -913
- package/top-ppt-html/assets/pptx-export.js +1944 -1944
- package/top-ppt-html/assets/style-gallery.html +589 -589
- package/top-ppt-html/assets/templates/architecture.html +3728 -3728
- package/top-ppt-html/assets/templates/engine.css +840 -840
- package/top-ppt-html/assets/templates/presentation.html +3738 -3738
- package/top-ppt-html/assets/templates/research.html +4017 -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-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 +186 -0
- package/top-ppt-html/package.json +11 -11
- package/top-ppt-html/references/charts-basic.md +624 -624
- package/top-ppt-html/references/charts-discipline.md +110 -108
- package/top-ppt-html/references/charts-extended.md +482 -482
- package/top-ppt-html/references/charts.md +37 -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 +510 -490
- package/top-ppt-html/references/default-surface.md +61 -0
- package/top-ppt-html/references/design-system.md +478 -478
- package/top-ppt-html/references/failure-modes.md +227 -214
- package/top-ppt-html/references/high-fidelity.md +127 -127
- package/top-ppt-html/references/icons.md +176 -397
- 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 +325 -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 +259 -254
- package/top-ppt-html/references/outline-design.md +275 -275
- package/top-ppt-html/references/playbook.md +284 -266
- package/top-ppt-html/references/pptx-export.md +227 -209
- package/top-ppt-html/references/presentation-craft.md +69 -0
- package/top-ppt-html/references/styles.md +370 -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 +231 -220
- package/top-ppt-html/scripts/audit_styles.py +351 -351
- package/top-ppt-html/scripts/build_examples.py +55 -2276
- package/top-ppt-html/scripts/build_pptx.js +2386 -2380
- package/top-ppt-html/scripts/capture_theme_overview.js +79 -78
- package/top-ppt-html/scripts/checks_html.py +166 -127
- package/top-ppt-html/scripts/cross_verify.py +298 -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 +111 -18
- package/top-ppt-html/scripts/lib_layout_regions.js +410 -412
- package/top-ppt-html/scripts/measure_height.py +178 -178
- package/top-ppt-html/scripts/negative_tests.py +433 -307
- package/top-ppt-html/scripts/package_skill.py +294 -291
- package/top-ppt-html/scripts/prepare_images.py +341 -341
- package/top-ppt-html/scripts/probe_image_export.py +187 -188
- package/top-ppt-html/scripts/quality_gate.py +309 -301
- package/top-ppt-html/scripts/recommend_layout.py +384 -0
- package/top-ppt-html/scripts/regression.py +308 -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/smoke_pptx.sh +30 -0
- package/top-ppt-html/scripts/sync_runtime.py +659 -662
- package/top-ppt-html/scripts/validate_pptx.py +1579 -1510
- package/top-ppt-html/scripts/validate_report.py +1755 -1456
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +0 -3926
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +0 -168
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +0 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +0 -321
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +0 -4325
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +0 -321
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +0 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +0 -914
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +0 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +0 -914
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +0 -5527
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +0 -914
- package/top-ppt-html/assets/theme-overview-presentation.png +0 -0
- package/top-ppt-html/references/design-system-engine.md +0 -235
- package/top-ppt-html/references/industry-benchmark.md +0 -105
- package/top-ppt-html/references/reform-plan.md +0 -252
- package/top-ppt-html/scripts/layout_slots.json +0 -830
|
@@ -1,78 +1,79 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/* TopPPT HTML · 主题参考图生成器(维护工具,非交付依赖、非交付硬门禁)
|
|
3
|
-
*
|
|
4
|
-
* 用法:
|
|
5
|
-
* npm install playwright # 或在任意位置安装 playwright 后设置 NODE_PATH
|
|
6
|
-
* npx playwright install chromium # 首次需要下载浏览器
|
|
7
|
-
* node scripts/capture_theme_overview.js [--channel=msedge|chrome|chromium] [--scale=1.25]
|
|
8
|
-
*
|
|
9
|
-
* --scale = deviceScaleFactor(默认 1.25)。参考图只用于"让用户看见风格与主题",
|
|
10
|
-
* 1.25 在 1480px 视口下已足够清晰,同时把单张 PNG 控制在 ~1MB 内(便于在 IDE 里快速打开)。
|
|
11
|
-
*
|
|
12
|
-
* 浏览器通道自动探测:优先命令行 --channel=,其次环境变量 TOP_PPT_BROWSER_CHANNEL,
|
|
13
|
-
* 最后回落到 Playwright 自带的 chromium(无本机浏览器依赖)。
|
|
14
|
-
*
|
|
15
|
-
* 产物(assets/,覆盖旧图):
|
|
16
|
-
* theme-overview.png —— 默认(演示 Tab
|
|
17
|
-
* theme-overview-
|
|
18
|
-
* theme-overview-
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* 每次截图前断言当前 Tab 文案,防止 Tab 切换未生效导致产出雷同图。 */
|
|
22
|
-
const path = require('path');
|
|
23
|
-
let chromium;
|
|
24
|
-
try {
|
|
25
|
-
({ chromium } = require('playwright'));
|
|
26
|
-
} catch (e) {
|
|
27
|
-
console.error('未找到 playwright。请先安装:npm install playwright && npx playwright install chromium');
|
|
28
|
-
console.error('(本脚本仅用于刷新 assets/theme-overview*.png 参考图,非技能交付依赖。)');
|
|
29
|
-
process.exit(2);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const TAB_TEXT = { presentation: '演示汇报', research: '研究报告', architecture: '信息架构图' };
|
|
33
|
-
const channelArg = (process.argv.find((a) => a.startsWith('--channel=')) || '').split('=')[1];
|
|
34
|
-
const scaleArg = parseFloat((process.argv.find((a) => a.startsWith('--scale=')) || '').split('=')[1]);
|
|
35
|
-
const SCALE = Number.isFinite(scaleArg) && scaleArg > 0 ? scaleArg : 1.25;
|
|
36
|
-
const CHANNEL = channelArg || process.env.TOP_PPT_BROWSER_CHANNEL
|
|
37
|
-
|| process.env.TOP_PPT_BROWSER_CHANNEL || '';
|
|
38
|
-
|
|
39
|
-
(async () => {
|
|
40
|
-
const gallery = 'file:///' + path.resolve(__dirname, '../assets/style-gallery.html').replace(/\\/g, '/');
|
|
41
|
-
const outDir = path.resolve(__dirname, '../assets');
|
|
42
|
-
/* channel 为空 → 用 Playwright 自带 chromium(不依赖本机已装浏览器) */
|
|
43
|
-
const browser = await chromium.launch(CHANNEL ? { channel: CHANNEL } : {});
|
|
44
|
-
const page = await browser.newPage({ viewport: { width: 1480, height: 1200 }, deviceScaleFactor: SCALE });
|
|
45
|
-
await page.goto(gallery);
|
|
46
|
-
await page.waitForSelector('.preset');
|
|
47
|
-
|
|
48
|
-
const modes = ['presentation', 'research', 'architecture'];
|
|
49
|
-
for (const m of modes) {
|
|
50
|
-
const idx = modes.indexOf(m);
|
|
51
|
-
await page.evaluate((i) => document.querySelectorAll('.mtab')[i].click(), idx);
|
|
52
|
-
await page.waitForTimeout(500);
|
|
53
|
-
const on = await page.evaluate(() =>
|
|
54
|
-
(document.querySelector('.mtab.on .mtab__t') || {}).textContent || '');
|
|
55
|
-
if (on !== TAB_TEXT[m]) {
|
|
56
|
-
throw new Error(`Tab 切换断言失败:期望「${TAB_TEXT[m]}」实际「${on}」(防止雷同图回归)`);
|
|
57
|
-
}
|
|
58
|
-
/* 卡片数必须等于单源风格数(PRESETS 由 sync_runtime.py 从 layout-constants.json 注入),
|
|
59
|
-
* 防止「扩了风格但画廊/参考图没跟上」的静默漂移(旧断言只查 n < 9,太弱)。 */
|
|
60
|
-
const n = await page.evaluate(() => document.querySelectorAll('.preset').length);
|
|
61
|
-
const expected = await page.evaluate(() =>
|
|
62
|
-
(typeof PRESETS === 'object' && PRESETS) ? Object.keys(PRESETS).length : 0);
|
|
63
|
-
if (expected && n !== expected) {
|
|
64
|
-
throw new Error(`画廊卡片数 ${n} ≠ 单源风格数 ${expected}(画廊与 layout-constants.json 漂移)`);
|
|
65
|
-
}
|
|
66
|
-
if (n < 9) throw new Error(`卡片数异常:${n}`);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* TopPPT HTML · 主题参考图生成器(维护工具,非交付依赖、非交付硬门禁)
|
|
3
|
+
*
|
|
4
|
+
* 用法:
|
|
5
|
+
* npm install playwright # 或在任意位置安装 playwright 后设置 NODE_PATH
|
|
6
|
+
* npx playwright install chromium # 首次需要下载浏览器
|
|
7
|
+
* node scripts/capture_theme_overview.js [--channel=msedge|chrome|chromium] [--scale=1.25]
|
|
8
|
+
*
|
|
9
|
+
* --scale = deviceScaleFactor(默认 1.25)。参考图只用于"让用户看见风格与主题",
|
|
10
|
+
* 1.25 在 1480px 视口下已足够清晰,同时把单张 PNG 控制在 ~1MB 内(便于在 IDE 里快速打开)。
|
|
11
|
+
*
|
|
12
|
+
* 浏览器通道自动探测:优先命令行 --channel=,其次环境变量 TOP_PPT_BROWSER_CHANNEL,
|
|
13
|
+
* 最后回落到 Playwright 自带的 chromium(无本机浏览器依赖)。
|
|
14
|
+
*
|
|
15
|
+
* 产物(assets/,覆盖旧图):
|
|
16
|
+
* theme-overview.png —— 默认(演示 Tab)整体参考图(= 演示态)
|
|
17
|
+
* theme-overview-research.png —— 研究模式 Tab(咨询密排形态)
|
|
18
|
+
* theme-overview-architecture.png —— 架构模式 Tab(图为王形态)
|
|
19
|
+
*
|
|
20
|
+
* 演示态与整体图内容相同,不再单独写 theme-overview-presentation.png。
|
|
21
|
+
* 每次截图前断言当前 Tab 文案,防止 Tab 切换未生效导致产出雷同图。 */
|
|
22
|
+
const path = require('path');
|
|
23
|
+
let chromium;
|
|
24
|
+
try {
|
|
25
|
+
({ chromium } = require('playwright'));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error('未找到 playwright。请先安装:npm install playwright && npx playwright install chromium');
|
|
28
|
+
console.error('(本脚本仅用于刷新 assets/theme-overview*.png 参考图,非技能交付依赖。)');
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const TAB_TEXT = { presentation: '演示汇报', research: '研究报告', architecture: '信息架构图' };
|
|
33
|
+
const channelArg = (process.argv.find((a) => a.startsWith('--channel=')) || '').split('=')[1];
|
|
34
|
+
const scaleArg = parseFloat((process.argv.find((a) => a.startsWith('--scale=')) || '').split('=')[1]);
|
|
35
|
+
const SCALE = Number.isFinite(scaleArg) && scaleArg > 0 ? scaleArg : 1.25;
|
|
36
|
+
const CHANNEL = channelArg || process.env.TOP_PPT_BROWSER_CHANNEL
|
|
37
|
+
|| process.env.TOP_PPT_BROWSER_CHANNEL || '';
|
|
38
|
+
|
|
39
|
+
(async () => {
|
|
40
|
+
const gallery = 'file:///' + path.resolve(__dirname, '../assets/style-gallery.html').replace(/\\/g, '/');
|
|
41
|
+
const outDir = path.resolve(__dirname, '../assets');
|
|
42
|
+
/* channel 为空 → 用 Playwright 自带 chromium(不依赖本机已装浏览器) */
|
|
43
|
+
const browser = await chromium.launch(CHANNEL ? { channel: CHANNEL } : {});
|
|
44
|
+
const page = await browser.newPage({ viewport: { width: 1480, height: 1200 }, deviceScaleFactor: SCALE });
|
|
45
|
+
await page.goto(gallery);
|
|
46
|
+
await page.waitForSelector('.preset');
|
|
47
|
+
|
|
48
|
+
const modes = ['presentation', 'research', 'architecture'];
|
|
49
|
+
for (const m of modes) {
|
|
50
|
+
const idx = modes.indexOf(m);
|
|
51
|
+
await page.evaluate((i) => document.querySelectorAll('.mtab')[i].click(), idx);
|
|
52
|
+
await page.waitForTimeout(500);
|
|
53
|
+
const on = await page.evaluate(() =>
|
|
54
|
+
(document.querySelector('.mtab.on .mtab__t') || {}).textContent || '');
|
|
55
|
+
if (on !== TAB_TEXT[m]) {
|
|
56
|
+
throw new Error(`Tab 切换断言失败:期望「${TAB_TEXT[m]}」实际「${on}」(防止雷同图回归)`);
|
|
57
|
+
}
|
|
58
|
+
/* 卡片数必须等于单源风格数(PRESETS 由 sync_runtime.py 从 layout-constants.json 注入),
|
|
59
|
+
* 防止「扩了风格但画廊/参考图没跟上」的静默漂移(旧断言只查 n < 9,太弱)。 */
|
|
60
|
+
const n = await page.evaluate(() => document.querySelectorAll('.preset').length);
|
|
61
|
+
const expected = await page.evaluate(() =>
|
|
62
|
+
(typeof PRESETS === 'object' && PRESETS) ? Object.keys(PRESETS).length : 0);
|
|
63
|
+
if (expected && n !== expected) {
|
|
64
|
+
throw new Error(`画廊卡片数 ${n} ≠ 单源风格数 ${expected}(画廊与 layout-constants.json 漂移)`);
|
|
65
|
+
}
|
|
66
|
+
if (n < 9) throw new Error(`卡片数异常:${n}`);
|
|
67
|
+
// presentation tab → theme-overview.png only (no duplicate *-presentation.png)
|
|
68
|
+
if (m === 'presentation') {
|
|
69
|
+
await page.screenshot({ path: path.join(outDir, 'theme-overview.png'), fullPage: true });
|
|
70
|
+
console.log(`captured: theme-overview.png (tab=${on}, cards=${n}; presentation = overview)`);
|
|
71
|
+
} else {
|
|
72
|
+
await page.screenshot({ path: path.join(outDir, `theme-overview-${m}.png`), fullPage: true });
|
|
73
|
+
console.log(`captured: theme-overview-${m}.png (tab=${on}, cards=${n})`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await browser.close();
|
|
78
|
+
console.log(`done: 3 张主题参考图已刷新(deviceScaleFactor=${SCALE})`);
|
|
79
|
+
})().catch((e) => { console.error(e); process.exit(1); });
|
|
@@ -1,127 +1,166 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""TopPPT HTML · HTML 检查公共库(单源:承载正则 / 图表通道 / 多样性下限 / 组合版式)
|
|
4
|
-
|
|
5
|
-
消费方:validate_report.py · quality_gate.py · evals/run_evals.py
|
|
6
|
-
禁止在消费方再手抄 CARRIERS / 多样性公式 / 组合版式判定。
|
|
7
|
-
阈值仍读 layout-constants.json(charts.variety / charts.registry)。
|
|
8
|
-
"""
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import re
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
ROOT = Path(__file__).resolve().parent.parent
|
|
16
|
-
_LC_PATH = ROOT / 'scripts' / 'layout-constants.json'
|
|
17
|
-
|
|
18
|
-
# 承载类型正则(组合版式:一页是否含 ≥2 种承载)
|
|
19
|
-
CARRIERS = [
|
|
20
|
-
r'<svg\b[^>]*data-chart=',
|
|
21
|
-
r'class="tbl-wrap',
|
|
22
|
-
r'class="card[\s"]',
|
|
23
|
-
r'class="metric[\s"]',
|
|
24
|
-
r'class="media[\s"]',
|
|
25
|
-
r'class="arch[\s"]',
|
|
26
|
-
r'class="lane[\s"]',
|
|
27
|
-
r'class="matrix[\s"]',
|
|
28
|
-
r'class="heat[\s"]',
|
|
29
|
-
r'class="bul[\s"]',
|
|
30
|
-
r'class="pyr[\s"]',
|
|
31
|
-
r'class="steps[\s"]',
|
|
32
|
-
r'class="exhibit[\s"]',
|
|
33
|
-
r'class="cols-2',
|
|
34
|
-
r'class="cols-3',
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
SKIP_BAND_IDS = ('agenda', 'cover', 'refs', 'appendix', 'next')
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def load_lc() -> dict:
|
|
41
|
-
return json.loads(_LC_PATH.read_text(encoding='utf-8'))
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def chart_registry(lc: dict | None = None) -> dict:
|
|
45
|
-
lc = lc or load_lc()
|
|
46
|
-
return {k: v for k, v in ((lc.get('charts') or {}).get('registry') or {}).items()
|
|
47
|
-
if isinstance(v, dict)}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def chart_variety(lc: dict | None = None) -> dict:
|
|
51
|
-
lc = lc or load_lc()
|
|
52
|
-
return (lc.get('charts') or {}).get('variety') or {}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def bands(html: str) -> list[str]:
|
|
56
|
-
return re.split(r'(?=<section class="band)', html)[1:]
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def content_bands(html: str, skip_ids: tuple[str, ...] = SKIP_BAND_IDS) -> list[str]:
|
|
60
|
-
out = []
|
|
61
|
-
for b in bands(html):
|
|
62
|
-
head = b[:240]
|
|
63
|
-
if any(f'id="{i}"' in head for i in skip_ids):
|
|
64
|
-
continue
|
|
65
|
-
out.append(b)
|
|
66
|
-
return out
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def chart_types_per_page(html: str) -> list[list[str]]:
|
|
70
|
-
return [re.findall(r'data-chart="([^"]+)"', b) for b in bands(html)]
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def distinct_chart_types(html: str) -> list[str]:
|
|
74
|
-
used = [t for ts in chart_types_per_page(html) for t in ts]
|
|
75
|
-
return sorted(set(used))
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def variety_floor(mode: str, n_chart_pages: int, variety: dict | None = None) -> int:
|
|
79
|
-
"""类型数下限 = min(模式上限, max(1, ⌈图表页数 × ratio⌉))"""
|
|
80
|
-
v = variety if variety is not None else chart_variety()
|
|
81
|
-
floor = int((v.get('minTypes') or {}).get(mode, 4) or 4)
|
|
82
|
-
ratio = float(v.get('minTypesRatio') or 0.6)
|
|
83
|
-
return min(floor, max(1, int(-(-n_chart_pages * ratio // 1))))
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML · HTML 检查公共库(单源:承载正则 / 图表通道 / 多样性下限 / 组合版式)
|
|
4
|
+
|
|
5
|
+
消费方:validate_report.py · quality_gate.py · evals/run_evals.py
|
|
6
|
+
禁止在消费方再手抄 CARRIERS / 多样性公式 / 组合版式判定。
|
|
7
|
+
阈值仍读 layout-constants.json(charts.variety / charts.registry)。
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
16
|
+
_LC_PATH = ROOT / 'scripts' / 'layout-constants.json'
|
|
17
|
+
|
|
18
|
+
# 承载类型正则(组合版式:一页是否含 ≥2 种承载)
|
|
19
|
+
CARRIERS = [
|
|
20
|
+
r'<svg\b[^>]*data-chart=',
|
|
21
|
+
r'class="tbl-wrap',
|
|
22
|
+
r'class="card[\s"]',
|
|
23
|
+
r'class="metric[\s"]',
|
|
24
|
+
r'class="media[\s"]',
|
|
25
|
+
r'class="arch[\s"]',
|
|
26
|
+
r'class="lane[\s"]',
|
|
27
|
+
r'class="matrix[\s"]',
|
|
28
|
+
r'class="heat[\s"]',
|
|
29
|
+
r'class="bul[\s"]',
|
|
30
|
+
r'class="pyr[\s"]',
|
|
31
|
+
r'class="steps[\s"]',
|
|
32
|
+
r'class="exhibit[\s"]',
|
|
33
|
+
r'class="cols-2',
|
|
34
|
+
r'class="cols-3',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
SKIP_BAND_IDS = ('agenda', 'cover', 'refs', 'appendix', 'next')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_lc() -> dict:
|
|
41
|
+
return json.loads(_LC_PATH.read_text(encoding='utf-8'))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def chart_registry(lc: dict | None = None) -> dict:
|
|
45
|
+
lc = lc or load_lc()
|
|
46
|
+
return {k: v for k, v in ((lc.get('charts') or {}).get('registry') or {}).items()
|
|
47
|
+
if isinstance(v, dict)}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def chart_variety(lc: dict | None = None) -> dict:
|
|
51
|
+
lc = lc or load_lc()
|
|
52
|
+
return (lc.get('charts') or {}).get('variety') or {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def bands(html: str) -> list[str]:
|
|
56
|
+
return re.split(r'(?=<section class="band)', html)[1:]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def content_bands(html: str, skip_ids: tuple[str, ...] = SKIP_BAND_IDS) -> list[str]:
|
|
60
|
+
out = []
|
|
61
|
+
for b in bands(html):
|
|
62
|
+
head = b[:240]
|
|
63
|
+
if any(f'id="{i}"' in head for i in skip_ids):
|
|
64
|
+
continue
|
|
65
|
+
out.append(b)
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def chart_types_per_page(html: str) -> list[list[str]]:
|
|
70
|
+
return [re.findall(r'data-chart="([^"]+)"', b) for b in bands(html)]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def distinct_chart_types(html: str) -> list[str]:
|
|
74
|
+
used = [t for ts in chart_types_per_page(html) for t in ts]
|
|
75
|
+
return sorted(set(used))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def variety_floor(mode: str, n_chart_pages: int, variety: dict | None = None) -> int:
|
|
79
|
+
"""类型数下限 = min(模式上限, max(1, ⌈图表页数 × ratio⌉))"""
|
|
80
|
+
v = variety if variety is not None else chart_variety()
|
|
81
|
+
floor = int((v.get('minTypes') or {}).get(mode, 4) or 4)
|
|
82
|
+
ratio = float(v.get('minTypesRatio') or 0.6)
|
|
83
|
+
return min(floor, max(1, int(-(-n_chart_pages * ratio // 1))))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def core_charts(lc: dict | None = None) -> list[str]:
|
|
88
|
+
lc = lc or load_lc()
|
|
89
|
+
ls = lc.get('layoutSystem') or {}
|
|
90
|
+
return list(ls.get('defaultCharts') or
|
|
91
|
+
['bar', 'hbar', 'line', 'donut', 'progress', 'area', 'stack', 'dualline'])
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def advanced_charts(lc: dict | None = None) -> set[str]:
|
|
95
|
+
lc = lc or load_lc()
|
|
96
|
+
ls = lc.get('layoutSystem') or {}
|
|
97
|
+
return set(ls.get('advancedCharts') or [])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def variety_core_preference(used: list[str], variety: dict | None = None,
|
|
101
|
+
lc: dict | None = None) -> str | None:
|
|
102
|
+
"""preferCoreFirst:advanced 已用但核图多样性不足 → WARN 文案。
|
|
103
|
+
|
|
104
|
+
advanced 仍计入 minTypes(不罚);仅提示勿用冷门图凑下限。
|
|
105
|
+
"""
|
|
106
|
+
v = variety if variety is not None else chart_variety(lc)
|
|
107
|
+
if not v.get('preferCoreFirst'):
|
|
108
|
+
return None
|
|
109
|
+
core = set(core_charts(lc))
|
|
110
|
+
adv = advanced_charts(lc)
|
|
111
|
+
distinct = set(used)
|
|
112
|
+
core_used = distinct & core
|
|
113
|
+
adv_used = distinct & adv if adv else (distinct - core)
|
|
114
|
+
if not adv_used:
|
|
115
|
+
return None
|
|
116
|
+
floor_hint = int(v.get('corePreferMin') or 3)
|
|
117
|
+
if len(core_used) >= min(floor_hint, len(core)):
|
|
118
|
+
return None
|
|
119
|
+
return (
|
|
120
|
+
f"核图仅 {sorted(core_used) or '∅'}(建议先 ≥{floor_hint} 种核图拉开多样性);"
|
|
121
|
+
f"已用 advanced {sorted(adv_used)}——允许计入 minTypes,但勿为凑下限而选用;"
|
|
122
|
+
f"仅内容意图命中时用 advanced"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def adjacent_same_type(pages: list[list[str]]) -> list[str]:
|
|
126
|
+
"""相邻图表页同型告警文案列表。"""
|
|
127
|
+
adj, prev, prev_i = [], None, 0
|
|
128
|
+
for i, ts in enumerate(pages):
|
|
129
|
+
if not ts:
|
|
130
|
+
continue
|
|
131
|
+
if prev and set(ts) & set(prev):
|
|
132
|
+
adj.append(f'第 {prev_i + 1}/{i + 1} 页同型 {sorted(set(ts) & set(prev))}')
|
|
133
|
+
prev, prev_i = ts, i
|
|
134
|
+
return adj
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def carrier_count(band: str) -> int:
|
|
138
|
+
return sum(1 for pat in CARRIERS if re.search(pat, band))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def composite_pages(html: str) -> tuple[int, int]:
|
|
142
|
+
"""返回 (含 ≥2 种承载的内容页数, 内容页总数)。"""
|
|
143
|
+
content = content_bands(html)
|
|
144
|
+
multi = sum(1 for b in content if carrier_count(b) >= 2)
|
|
145
|
+
return multi, len(content)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def composite_required(mode: str, variety: dict | None = None) -> float:
|
|
149
|
+
"""组合版式比例下限;非 compositeModes 返回 0。"""
|
|
150
|
+
v = variety if variety is not None else chart_variety()
|
|
151
|
+
if mode not in (v.get('compositeModes') or ['research']):
|
|
152
|
+
return 0.0
|
|
153
|
+
return float(v.get('compositeMinRatio') or 0)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def chart_channel(chart_type: str, reg: dict | None = None) -> str:
|
|
157
|
+
reg = reg if reg is not None else chart_registry()
|
|
158
|
+
spec = reg.get(str(chart_type or 'bar').lower()) or {}
|
|
159
|
+
return str(spec.get('pptx') or 'native')
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def chart_datatable_mode(chart: dict, reg: dict | None = None) -> str:
|
|
163
|
+
reg = reg if reg is not None else chart_registry()
|
|
164
|
+
spec = reg.get(str(chart.get('type') or 'bar').lower()) or {}
|
|
165
|
+
mode = str(chart.get('dataTable') or spec.get('dataTable') or 'notes').lower()
|
|
166
|
+
return 'inline' if mode == 'appendix' else mode
|