@topmindspace/tms-skills 0.1.0
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 +26 -0
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/bin/tms-skills.js +160 -0
- package/package.json +48 -0
- package/top-ppt-html/README.md +254 -0
- package/top-ppt-html/SKILL.md +111 -0
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +3926 -0
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.model.json +168 -0
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.html +3926 -0
- package/top-ppt-html/assets/examples/2026-09-09-architecture-spectrum.model.json +168 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.html +4325 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-apple-mono.model.json +321 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.html +4325 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-brand-red.model.json +321 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +4325 -0
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.model.json +321 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.html +5527 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-deep-teal.model.json +914 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.html +5527 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-indigo-violet.model.json +914 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +5527 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.model.json +914 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.html +5527 -0
- package/top-ppt-html/assets/examples/2026-09-09-research-warm-sand.model.json +914 -0
- package/top-ppt-html/assets/pptx-export.js +1944 -0
- package/top-ppt-html/assets/style-gallery.html +589 -0
- package/top-ppt-html/assets/templates/architecture.html +3728 -0
- package/top-ppt-html/assets/templates/engine.css +840 -0
- package/top-ppt-html/assets/templates/presentation.html +3738 -0
- package/top-ppt-html/assets/templates/research.html +4017 -0
- package/top-ppt-html/assets/templates/ui.js +520 -0
- 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 -0
- package/top-ppt-html/evals/rubric.schema.json +25 -0
- package/top-ppt-html/evals/run_evals.py +220 -0
- package/top-ppt-html/evals/trace.example.json +16 -0
- package/top-ppt-html/package.json +35 -0
- package/top-ppt-html/references/charts-basic.md +624 -0
- package/top-ppt-html/references/charts-discipline.md +108 -0
- package/top-ppt-html/references/charts-extended.md +482 -0
- package/top-ppt-html/references/charts.md +28 -0
- package/top-ppt-html/references/components-atoms.md +624 -0
- package/top-ppt-html/references/components.md +30 -0
- package/top-ppt-html/references/content-rules.md +490 -0
- package/top-ppt-html/references/design-system-engine.md +235 -0
- package/top-ppt-html/references/design-system.md +478 -0
- package/top-ppt-html/references/failure-modes.md +214 -0
- package/top-ppt-html/references/high-fidelity.md +127 -0
- package/top-ppt-html/references/icons.md +397 -0
- package/top-ppt-html/references/industry-benchmark.md +105 -0
- package/top-ppt-html/references/infographics-stats.md +308 -0
- package/top-ppt-html/references/infographics-structure.md +226 -0
- package/top-ppt-html/references/infographics.md +43 -0
- package/top-ppt-html/references/layout-grammar.md +315 -0
- package/top-ppt-html/references/layouts-architecture.md +108 -0
- package/top-ppt-html/references/layouts-combo.md +600 -0
- package/top-ppt-html/references/layouts-research.md +160 -0
- package/top-ppt-html/references/modes.md +254 -0
- package/top-ppt-html/references/outline-design.md +275 -0
- package/top-ppt-html/references/playbook.md +266 -0
- package/top-ppt-html/references/pptx-export.md +209 -0
- package/top-ppt-html/references/reform-plan.md +252 -0
- package/top-ppt-html/references/styles.md +370 -0
- package/top-ppt-html/references/tech-design.md +138 -0
- package/top-ppt-html/scripts/audit_css.py +109 -0
- package/top-ppt-html/scripts/audit_docs.py +176 -0
- package/top-ppt-html/scripts/audit_skill.py +220 -0
- package/top-ppt-html/scripts/audit_styles.py +351 -0
- package/top-ppt-html/scripts/build_examples.py +2276 -0
- package/top-ppt-html/scripts/build_pptx.js +2380 -0
- package/top-ppt-html/scripts/capture_theme_overview.js +78 -0
- package/top-ppt-html/scripts/checks_html.py +127 -0
- package/top-ppt-html/scripts/cross_verify.py +294 -0
- package/top-ppt-html/scripts/env_probe.py +158 -0
- package/top-ppt-html/scripts/extract_model.py +210 -0
- package/top-ppt-html/scripts/extract_snippet.py +374 -0
- package/top-ppt-html/scripts/gen_channel_a.js +214 -0
- package/top-ppt-html/scripts/layout-constants.json +3377 -0
- package/top-ppt-html/scripts/layout_slots.json +830 -0
- package/top-ppt-html/scripts/lib_layout_regions.js +412 -0
- package/top-ppt-html/scripts/measure_height.py +178 -0
- package/top-ppt-html/scripts/model-schema.json +547 -0
- package/top-ppt-html/scripts/negative_tests.py +307 -0
- package/top-ppt-html/scripts/package_skill.py +291 -0
- package/top-ppt-html/scripts/prepare_images.py +341 -0
- package/top-ppt-html/scripts/probe_image_export.py +188 -0
- package/top-ppt-html/scripts/quality_gate.py +301 -0
- package/top-ppt-html/scripts/regression.py +308 -0
- package/top-ppt-html/scripts/render_compare.py +275 -0
- package/top-ppt-html/scripts/render_from_model.py +698 -0
- package/top-ppt-html/scripts/scaffold_report.py +1054 -0
- package/top-ppt-html/scripts/section-file-map.json +105 -0
- package/top-ppt-html/scripts/sync_runtime.py +662 -0
- package/top-ppt-html/scripts/validate_pptx.py +1510 -0
- package/top-ppt-html/scripts/validate_report.py +1456 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
TopPPT HTML · 单源注入器(版式常量 + DSL schema · 三模板公共片段统一注入)
|
|
5
|
+
用法:
|
|
6
|
+
python scripts/sync_runtime.py
|
|
7
|
+
|
|
8
|
+
职责(消除"多处手动同步"债务):
|
|
9
|
+
1. 读 scripts/layout-constants.json(唯一事实源:页面几何 / 12 列网格 / 语义字阶 C0–T14 / 三模式独立比例尺
|
|
10
|
+
/ 页型几何 / 9 风格 token / 图表登记四元组 / 容器内边距 / 锚点容差 / 图片准入门
|
|
11
|
+
/ 校验预算 / 强调色表 / 去AI味词表 / 深度模式配置)
|
|
12
|
+
2. 读 scripts/model-schema.json(DSL schema 单源:页型字段与必填约束 + 图表类型白名单 + 数据表策略)
|
|
13
|
+
→ 注入 assets/pptx-export.js 的 __TOPPPT_SCHEMA__ 标记块(浏览器端 validateModel 消费)
|
|
14
|
+
3. 重新生成 assets/pptx-export.js 与 assets/style-gallery.html 的 __TOPPPT_CONSTANTS__ 标记块
|
|
15
|
+
(含 GRID/TYPO/CONT/REG/DEEP、MTS 三模式比例尺、PRESETS/PRESETS_DARK——画廊配色即由此派生,不再手抄色值)
|
|
16
|
+
4. 对 assets/templates/ 三份模式模板注入公共片段(避免三份漂移):
|
|
17
|
+
__TOPPPT_ENGINE__ ← assets/templates/engine.css(公共样式引擎)
|
|
18
|
+
__TOPPPT_UI__ ← assets/templates/ui.js(公共 UI 脚本:主题/工具组/翻页/动效/预览模态)
|
|
19
|
+
__TOPPPT_RUNTIME__ ← assets/pptx-export.js(PPTX 预览运行时内联副本)
|
|
20
|
+
5. 校验双端单源引用:scripts/build_pptx.js 引用 layout-constants.json(Node require);
|
|
21
|
+
scripts/extract_model.py 引用 model-schema.json(本地校验同一份 schema)
|
|
22
|
+
6. 校验「页型四件套」完整性:schema 每个页型都要在 layout-constants.json 的 pageTypeGeometry 有几何映射
|
|
23
|
+
7. 校验「图表登记四元组」完整性:charts.types ↔ charts.registry 双向一致、pptx 通道合法、
|
|
24
|
+
非原生图表 dataTable 不得为 off、schema.chartTypes 必须是 registry 子集
|
|
25
|
+
7b. 校验「图表类型可达性」不变量:每个 registry 类型必须可作 chart.type(schema.chartTypes)
|
|
26
|
+
或可作 sections[].type(信息图专属页型)——否则模型无法表达、双引擎实现沦为死代码
|
|
27
|
+
7c. 校验「页型实现可达性」冒烟:schema 每个页型都应出现在 build_pptx.js 与 pptx-export.js 中
|
|
28
|
+
(子串级检查,拦「schema 加了页型、引擎没实现」的静默退化)
|
|
29
|
+
7d. 校验「骨架尺寸单源」不变量:charts.scaffold 键 ⊆ registry,且默认值 ≥ charts.minSize
|
|
30
|
+
同口径阈值(拦「scaffold 硬编码第二源漂移」;信息图页型清单亦单源于此)
|
|
31
|
+
8. 校验语义字阶:每个 level 的 role 必须在 modeTypeScale 中存在
|
|
32
|
+
9. 打印常量摘要哈希,供各方核对(pptx-export.js / 三模板 / 画廊 / build_pptx.js)
|
|
33
|
+
|
|
34
|
+
模式模板(生成时确定形态,非运行时切换):
|
|
35
|
+
templates/presentation.html · templates/research.html · templates/architecture.html
|
|
36
|
+
各模板自带:模式密度层 + 模式专属组件 + 模式骨架章节 + REPORT_MODEL(mode 已锁定)
|
|
37
|
+
"""
|
|
38
|
+
import json
|
|
39
|
+
import re
|
|
40
|
+
import hashlib
|
|
41
|
+
import sys
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
# Windows GBK 控制台兜底:强制 UTF-8 输出
|
|
45
|
+
try:
|
|
46
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
47
|
+
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
52
|
+
LC = ROOT / 'scripts' / 'layout-constants.json'
|
|
53
|
+
MS = ROOT / 'scripts' / 'model-schema.json'
|
|
54
|
+
PE = ROOT / 'assets' / 'pptx-export.js'
|
|
55
|
+
GALLERY = ROOT / 'assets' / 'style-gallery.html'
|
|
56
|
+
TPL_DIR = ROOT / 'assets' / 'templates'
|
|
57
|
+
ENGINE = TPL_DIR / 'engine.css'
|
|
58
|
+
UI = TPL_DIR / 'ui.js'
|
|
59
|
+
TEMPLATES = [TPL_DIR / 'presentation.html',
|
|
60
|
+
TPL_DIR / 'research.html',
|
|
61
|
+
TPL_DIR / 'architecture.html']
|
|
62
|
+
BJ = ROOT / 'scripts' / 'build_pptx.js'
|
|
63
|
+
EM = ROOT / 'scripts' / 'extract_model.py'
|
|
64
|
+
|
|
65
|
+
CONST_PAT = re.compile(r'/\* __TOPPPT_CONSTANTS_START__ \*/[\s\S]*?/\* __TOPPPT_CONSTANTS_END__ \*/')
|
|
66
|
+
SCHEMA_PAT = re.compile(r'/\* __TOPPPT_SCHEMA_START__ \*/[\s\S]*?/\* __TOPPPT_SCHEMA_END__ \*/')
|
|
67
|
+
ENGINE_PAT = re.compile(r'/\* __TOPPPT_ENGINE_START__ \*/[\s\S]*?/\* __TOPPPT_ENGINE_END__ \*/')
|
|
68
|
+
UI_PAT = re.compile(r'/\* __TOPPPT_UI_START__ \*/[\s\S]*?/\* __TOPPPT_UI_END__ \*/')
|
|
69
|
+
RUNTIME_PAT = re.compile(r'/\* __TOPPPT_RUNTIME_START__ \*/[\s\S]*?/\* __TOPPPT_RUNTIME_END__ \*/')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _strip_comments(obj):
|
|
73
|
+
"""递归剔除 JSON 对象里的 $comment 键(注入 JS 常量用)。"""
|
|
74
|
+
if isinstance(obj, dict):
|
|
75
|
+
return {k: _strip_comments(v) for k, v in obj.items() if not k.startswith('$')}
|
|
76
|
+
if isinstance(obj, list):
|
|
77
|
+
return [_strip_comments(v) for v in obj]
|
|
78
|
+
return obj
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_const_block(lc: dict) -> str:
|
|
82
|
+
page, pt = lc['page'], lc['pageTypes']
|
|
83
|
+
styles = _strip_comments(lc['styles'])
|
|
84
|
+
styles_dark = _strip_comments(lc.get('stylesDark') or {})
|
|
85
|
+
ts = _strip_comments(lc['typeScale'])
|
|
86
|
+
mts = _strip_comments(lc['modeTypeScale'])
|
|
87
|
+
def _presets_js(var_name, styles_dict):
|
|
88
|
+
if not styles_dict:
|
|
89
|
+
return 'var %s = {};' % var_name
|
|
90
|
+
return 'var %s = {\n' % var_name + ',\n'.join(
|
|
91
|
+
" '%s': { accent:'%s', ink:'%s', body:'%s', faint:'%s', bg:'%s', surface:'%s', line:'%s', soft:'%s', onAccent:'%s', font:'%s', fontDisplay:'%s' }" % (
|
|
92
|
+
k, v['accent'], v['ink'], v['body'], v['faint'], v['bg'], v['surface'], v['line'], v['soft'],
|
|
93
|
+
v['onAccent'], v['font'], v['fontDisplay'])
|
|
94
|
+
for k, v in styles_dict.items()) + '\n};'
|
|
95
|
+
presets_js = _presets_js('PRESETS', styles)
|
|
96
|
+
presets_dark_js = _presets_js('PRESETS_DARK', styles_dark)
|
|
97
|
+
ts_js = 'var TS_BASE = ' + json.dumps(ts, separators=(',', ':')) + ';'
|
|
98
|
+
mts_js = 'var MTS = ' + json.dumps(mts, separators=(',', ':')) + ';'
|
|
99
|
+
pt_js = 'var PT = ' + json.dumps(_strip_comments(pt), ensure_ascii=False, separators=(',', ':')) + ';'
|
|
100
|
+
colors_js = 'var STYLE_DATA_COLORS = ' + json.dumps(
|
|
101
|
+
_strip_comments(lc.get('styleDataColors') or {}), separators=(',', ':')) + ';'
|
|
102
|
+
colors_dark_js = 'var STYLE_DATA_COLORS_DARK = ' + json.dumps(
|
|
103
|
+
_strip_comments(lc.get('styleDataColorsDark') or {}), separators=(',', ':')) + ';'
|
|
104
|
+
# 12 列网格 / 语义字阶 / 容器内边距 / 图表登记四元组 / 深度模式配置
|
|
105
|
+
grid_js = 'var GRID = ' + json.dumps(_strip_comments(lc['grid']), separators=(',', ':')) + ';'
|
|
106
|
+
typo_js = 'var TYPO = ' + json.dumps(_strip_comments(lc['typography']), ensure_ascii=False,
|
|
107
|
+
separators=(',', ':')) + ';'
|
|
108
|
+
cont_js = 'var CONT = ' + json.dumps(_strip_comments(lc['containers']), ensure_ascii=False,
|
|
109
|
+
separators=(',', ':')) + ';'
|
|
110
|
+
reg_js = 'var REG = ' + json.dumps(_strip_comments((lc.get('charts') or {}).get('registry') or {}),
|
|
111
|
+
ensure_ascii=False, separators=(',', ':')) + ';'
|
|
112
|
+
deep_js = 'var DEEP = ' + json.dumps(_strip_comments(lc['deepMode']), ensure_ascii=False,
|
|
113
|
+
separators=(',', ':')) + ';'
|
|
114
|
+
# 图片规格与配图占位约定(双引擎拼同一占位文本,cross_verify 逐页文本一致)
|
|
115
|
+
img_js = 'var IMG = ' + json.dumps(_strip_comments(lc.get('imageSpec') or {}), ensure_ascii=False,
|
|
116
|
+
separators=(',', ':')) + ';'
|
|
117
|
+
# 页型布局区域 IR(与 scripts/lib_layout_regions.js 同源公式;浏览器端 A 通道消费)
|
|
118
|
+
slots_path = ROOT / 'scripts' / 'layout_slots.json'
|
|
119
|
+
slots = {}
|
|
120
|
+
if slots_path.exists():
|
|
121
|
+
try:
|
|
122
|
+
slots = json.loads(slots_path.read_text(encoding='utf-8'))
|
|
123
|
+
except json.JSONDecodeError:
|
|
124
|
+
slots = {}
|
|
125
|
+
slots_js = 'var LAYOUT_SLOTS = ' + json.dumps(_strip_comments(slots), ensure_ascii=False,
|
|
126
|
+
separators=(',', ':')) + ';'
|
|
127
|
+
region_fn = r'''
|
|
128
|
+
/* regionOf(pageType, slotId, opts) — 页型布局区域 IR(与 scripts/lib_layout_regions.js 同源) */
|
|
129
|
+
function regionOf(pageType, slotId, opts) {
|
|
130
|
+
var type = String(pageType || 'points'), slot = String(slotId || '');
|
|
131
|
+
opts = opts || {};
|
|
132
|
+
var L = PT.layout || {}, C = PT.common || {};
|
|
133
|
+
var topDefault = C.bodyY || 2.5;
|
|
134
|
+
var bottomDefault = opts.withNote ? (L.contentBottomWithNote || 6.4) : (L.contentBottom || 6.9);
|
|
135
|
+
if (slot === 'head' || slot === 'chrome') {
|
|
136
|
+
return { x: MX, y: C.headEyebrowY || 0.55, w: CW,
|
|
137
|
+
h: Math.max(0.4, (C.headRuleY || 1.95) - (C.headEyebrowY || 0.55)),
|
|
138
|
+
titleY: C.headTitleY, ruleY: C.headRuleY, leadY: C.leadY, bodyY: C.bodyY };
|
|
139
|
+
}
|
|
140
|
+
if (slot === 'annotation' || slot === 'soWhat') {
|
|
141
|
+
var sy = (PT.exhibit && PT.exhibit.soWhatY != null) ? PT.exhibit.soWhatY : 6.05;
|
|
142
|
+
return { x: MX, y: sy, w: CW, h: 0.62 };
|
|
143
|
+
}
|
|
144
|
+
if (slot === 'footnote') {
|
|
145
|
+
var fy = (PT.exhibit && PT.exhibit.footnoteY != null) ? PT.exhibit.footnoteY : ((PT.note && PT.note.y) || 6.55);
|
|
146
|
+
return { x: MX, y: fy, w: CW - 1.2, h: (PT.note && PT.note.h) || 0.32 };
|
|
147
|
+
}
|
|
148
|
+
var P = PT[type] || {};
|
|
149
|
+
if (type === 'exhibit' && slot === 'primary') {
|
|
150
|
+
var ey = P.chartY || 2.95;
|
|
151
|
+
return { x: MX, y: ey, w: CW, h: Math.max(1.2, (L.contentBottomWithNote || 6.4) - ey), badgeY: P.badgeY };
|
|
152
|
+
}
|
|
153
|
+
if (type === 'metrics' && slot === 'primary') {
|
|
154
|
+
return { x: MX, y: P.bandY || 3.1, w: CW, h: P.cardH || 2.3, gap: P.gap, maxW: P.maxW,
|
|
155
|
+
valY: P.valY, valH: P.valH, capY: P.capY, capH: P.capH };
|
|
156
|
+
}
|
|
157
|
+
if (type === 'table' && slot === 'primary') {
|
|
158
|
+
var ty = P.y || topDefault;
|
|
159
|
+
return { x: MX, y: ty, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - ty) };
|
|
160
|
+
}
|
|
161
|
+
if (type === 'bar' && slot === 'primary') {
|
|
162
|
+
var by = P.chartY || 2.95;
|
|
163
|
+
var bb = opts.bottom != null ? opts.bottom : bottomDefault;
|
|
164
|
+
return { x: MX + (P.chartX || 0), y: by, w: CW - (P.chartW || 0), h: Math.max(1.2, bb - by) };
|
|
165
|
+
}
|
|
166
|
+
if (type === 'bar' && slot === 'hbar') {
|
|
167
|
+
var y0 = P.hbarY0 || 2.6;
|
|
168
|
+
var y1 = opts.bottom != null ? opts.bottom : (P.hbarY1 || 6.25);
|
|
169
|
+
return { x: MX, y: y0, w: CW, h: Math.max(1.2, y1 - y0) };
|
|
170
|
+
}
|
|
171
|
+
if (type === 'points' && (slot === 'primary' || slot === 'list')) {
|
|
172
|
+
var ly = P.listY || topDefault;
|
|
173
|
+
return { x: MX, y: ly, w: CW, h: Math.max(0.8, (opts.bottom != null ? opts.bottom : bottomDefault) - ly) };
|
|
174
|
+
}
|
|
175
|
+
if (type === 'split' && (slot === 'left' || slot === 'right')) {
|
|
176
|
+
var st = P.chartY || topDefault;
|
|
177
|
+
var sb = opts.bottom != null ? opts.bottom : bottomDefault;
|
|
178
|
+
if (slot === 'left') return { x: MX, y: st, w: CW * (P.leftW || 0.55), h: Math.max(1.2, sb - st) };
|
|
179
|
+
return { x: MX + CW * (P.rightX != null ? P.rightX : 0.585), y: st, w: CW * (P.rightW || 0.415), h: Math.max(1.2, sb - st) };
|
|
180
|
+
}
|
|
181
|
+
if (type === 'twocol' && (slot === 'primary' || slot === 'left' || slot === 'right')) {
|
|
182
|
+
var gap = P.colGap || 0.5, colW = (CW - gap) / 2;
|
|
183
|
+
var tt = opts.top != null ? opts.top : topDefault;
|
|
184
|
+
var tb = opts.bottom != null ? opts.bottom : bottomDefault;
|
|
185
|
+
if (slot === 'right') return { x: MX + colW + gap, y: tt, w: colW, h: Math.max(1, tb - tt) };
|
|
186
|
+
return { x: MX, y: tt, w: colW, h: Math.max(1, tb - tt) };
|
|
187
|
+
}
|
|
188
|
+
if ((type === 'diagram' || type === 'lane') && slot === 'primary') {
|
|
189
|
+
var arch = PT.arch || {};
|
|
190
|
+
var dy = (opts.mode === 'architecture' && arch.fullStartY != null) ? arch.fullStartY : (P.bodyStartY || 2.9);
|
|
191
|
+
var db = (opts.mode === 'architecture' && arch.fullEndY != null) ? arch.fullEndY
|
|
192
|
+
: (opts.bottom != null ? opts.bottom : bottomDefault);
|
|
193
|
+
return { x: MX, y: dy, w: CW, h: Math.max(1.2, db - dy) };
|
|
194
|
+
}
|
|
195
|
+
if (type === 'kpi' && (slot === 'hero' || slot === 'primary')) {
|
|
196
|
+
return { x: MX, y: P.heroY || 2.9, w: CW * (P.heroW || 0.52), h: P.heroH || 1.6,
|
|
197
|
+
dividerX: P.dividerX, metricY0: P.metricY0, metricRowH: P.metricRowH };
|
|
198
|
+
}
|
|
199
|
+
if (type === 'comparison' && (slot === 'left' || slot === 'primary' || slot === 'right')) {
|
|
200
|
+
var cg = P.panelGap || 0.4, pw = (CW - cg) / 2;
|
|
201
|
+
if (slot === 'right') return { x: MX + pw + cg, y: P.panelY || 2.55, w: pw, h: P.panelH || 3.4 };
|
|
202
|
+
return { x: MX, y: P.panelY || 2.55, w: pw, h: P.panelH || 3.4 };
|
|
203
|
+
}
|
|
204
|
+
if (type === 'quote' && slot === 'primary') {
|
|
205
|
+
return { x: MX + 1.05, y: P.textY || 2.7, w: CW - 2.1, h: P.textH || 2.3 };
|
|
206
|
+
}
|
|
207
|
+
if (type === 'heatmap' && slot === 'primary') {
|
|
208
|
+
var hy = P.y || 2.6;
|
|
209
|
+
return { x: MX, y: hy, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - hy) };
|
|
210
|
+
}
|
|
211
|
+
if (type === 'bullet' && slot === 'primary') {
|
|
212
|
+
var uy = P.y || 2.6;
|
|
213
|
+
return { x: MX, y: uy, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - uy) };
|
|
214
|
+
}
|
|
215
|
+
if (type === 'pyramid' && slot === 'primary') {
|
|
216
|
+
var py2 = P.y || 2.35;
|
|
217
|
+
return { x: MX, y: py2, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - py2) };
|
|
218
|
+
}
|
|
219
|
+
if (type === 'steps' && slot === 'primary') {
|
|
220
|
+
return { x: MX, y: P.y || 3.05, w: CW, h: P.barH || 1.35 };
|
|
221
|
+
}
|
|
222
|
+
if (type === 'cards' && slot === 'primary') {
|
|
223
|
+
var cy2 = P.startY || 3.0;
|
|
224
|
+
return { x: MX, y: cy2, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - cy2) };
|
|
225
|
+
}
|
|
226
|
+
if (type === 'timeline' && slot === 'primary') {
|
|
227
|
+
return { x: MX, y: P.labelY || 2.95, w: CW, h: Math.max(1, bottomDefault - (P.labelY || 2.95)) };
|
|
228
|
+
}
|
|
229
|
+
if (type === 'image' && slot === 'primary') {
|
|
230
|
+
var iy2 = Math.max(P.y || 2.3, opts.top || 0);
|
|
231
|
+
return { x: MX, y: iy2, w: CW, h: Math.max(1.2, Math.min(P.h || 4.2, (opts.bottom != null ? opts.bottom : bottomDefault) - iy2)) };
|
|
232
|
+
}
|
|
233
|
+
if ((type === 'halftable') && (slot === 'left' || slot === 'primary')) {
|
|
234
|
+
var rs = PT.research || {};
|
|
235
|
+
return { x: MX, y: topDefault, w: CW * (rs.halfTableW || 0.52), h: Math.max(1, bottomDefault - topDefault) };
|
|
236
|
+
}
|
|
237
|
+
if ((type === 'halftable') && (slot === 'right' || slot === 'secondary')) {
|
|
238
|
+
var rs2 = PT.research || {};
|
|
239
|
+
return { x: MX + CW * (rs2.halfChartX || 0.57), y: topDefault, w: CW * (rs2.halfChartW || 0.43), h: Math.max(1, bottomDefault - topDefault) };
|
|
240
|
+
}
|
|
241
|
+
if (type === 'matrix' && slot === 'primary') {
|
|
242
|
+
var rm = PT.research || {};
|
|
243
|
+
var my = rm.matrixY || 2.3;
|
|
244
|
+
return { x: MX, y: my, w: CW, h: Math.max(1, (opts.bottom != null ? opts.bottom : bottomDefault) - my) };
|
|
245
|
+
}
|
|
246
|
+
var INFO = { sankey:1, treemap:1, boxplot:1, network:1, marimekko:1, streamgraph:1 };
|
|
247
|
+
if (INFO[type] && slot === 'primary') {
|
|
248
|
+
var iy3 = P.y || topDefault;
|
|
249
|
+
var ih3 = Math.max(1.2, Math.min(P.h || 4.1, (opts.bottom != null ? opts.bottom : bottomDefault) - iy3));
|
|
250
|
+
return { x: MX, y: iy3, w: CW, h: ih3 };
|
|
251
|
+
}
|
|
252
|
+
return { x: MX, y: topDefault, w: CW, h: Math.max(0.8, bottomDefault - topDefault) };
|
|
253
|
+
}
|
|
254
|
+
'''
|
|
255
|
+
return '\n'.join([
|
|
256
|
+
'/* __TOPPPT_CONSTANTS_START__ */',
|
|
257
|
+
'/* ── 由 scripts/sync_runtime.py 从 scripts/layout-constants.json 注入 · 禁止手改 ── */',
|
|
258
|
+
'/* MTS = 三模式独立排版比例尺(按模式取 pt;基准 TS_BASE = presentation 档) */',
|
|
259
|
+
'/* PRESETS_DARK = 风格 dark token(REPORT_MODEL.theme="dark" 时导出深色版)*/',
|
|
260
|
+
'/* GRID = 12 列网格;TYPO = 语义字阶 C0–T14;CONT = 容器内边距;REG = 图表登记四元组;DEEP = 深度模式配置 */',
|
|
261
|
+
'/* IMG = 素材图片规格与配图占位约定(版式/裁切/建议尺寸/占位标签,双引擎同源) */',
|
|
262
|
+
'/* STYLE_DATA_COLORS(_DARK) = 9 套风格各自的编码色板 c1–c5(数据系列;结构色纪律不变) */',
|
|
263
|
+
'/* LAYOUT_SLOTS + regionOf = 页型布局区域 IR(与 scripts/lib_layout_regions.js 同源) */',
|
|
264
|
+
'var PW = %s, PH = %s, MX = %s, CW = PW - 2 * MX, EMU = %s;'
|
|
265
|
+
% (page['pw'], page['ph'], page['mx'], page['emu']),
|
|
266
|
+
ts_js, mts_js, pt_js, presets_js, presets_dark_js, colors_js, colors_dark_js,
|
|
267
|
+
grid_js, typo_js, cont_js, reg_js, deep_js, img_js, slots_js, region_fn,
|
|
268
|
+
'/* __TOPPPT_CONSTANTS_END__ */'])
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def build_schema_block(ms: dict) -> str:
|
|
272
|
+
return '\n'.join([
|
|
273
|
+
'/* __TOPPPT_SCHEMA_START__ */',
|
|
274
|
+
'/* ── 由 scripts/sync_runtime.py 从 scripts/model-schema.json 注入 · 禁止手改 ── */',
|
|
275
|
+
'/* DSL schema 单源:浏览器端 validateModel 与 scripts/extract_model.py 消费同一份定义 */',
|
|
276
|
+
'var MODEL_SCHEMA = ' + json.dumps(_strip_comments(ms), ensure_ascii=False, separators=(',', ':')) + ';',
|
|
277
|
+
'/* __TOPPPT_SCHEMA_END__ */'])
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def inject(path: Path, pat: re.Pattern, repl: str, label: str) -> bool:
|
|
281
|
+
src = path.read_text(encoding='utf-8')
|
|
282
|
+
if not pat.search(src):
|
|
283
|
+
print(f'错误: {path.name} 缺少 {label} 标记块')
|
|
284
|
+
return False
|
|
285
|
+
path.write_text(pat.sub(lambda m: repl, src, count=1), encoding='utf-8')
|
|
286
|
+
return True
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def main() -> int:
|
|
290
|
+
if not LC.exists():
|
|
291
|
+
print(f'错误: 找不到 {LC}')
|
|
292
|
+
return 2
|
|
293
|
+
lc = json.loads(LC.read_text(encoding='utf-8'))
|
|
294
|
+
if not MS.exists():
|
|
295
|
+
print(f'错误: 找不到 {MS}(DSL schema 单源)')
|
|
296
|
+
return 2
|
|
297
|
+
ms = json.loads(MS.read_text(encoding='utf-8'))
|
|
298
|
+
ok = True
|
|
299
|
+
|
|
300
|
+
# ① 重新生成 pptx-export.js 常量块 + schema 块
|
|
301
|
+
src = PE.read_text(encoding='utf-8')
|
|
302
|
+
if not CONST_PAT.search(src):
|
|
303
|
+
print('错误: assets/pptx-export.js 缺少 __TOPPPT_CONSTANTS__ 标记块')
|
|
304
|
+
return 1
|
|
305
|
+
if not SCHEMA_PAT.search(src):
|
|
306
|
+
print('错误: assets/pptx-export.js 缺少 __TOPPPT_SCHEMA__ 标记块')
|
|
307
|
+
return 1
|
|
308
|
+
block = build_const_block(lc)
|
|
309
|
+
PE.write_text(CONST_PAT.sub(lambda m: block, src), encoding='utf-8')
|
|
310
|
+
src = PE.read_text(encoding='utf-8')
|
|
311
|
+
schema_block = build_schema_block(ms)
|
|
312
|
+
PE.write_text(SCHEMA_PAT.sub(lambda m: schema_block, src), encoding='utf-8')
|
|
313
|
+
|
|
314
|
+
# ①-b 风格画廊:同一份常量块注入(画廊配色改为从 PRESETS/PRESETS_DARK 派生,消除第四处手抄色值)
|
|
315
|
+
if GALLERY.exists():
|
|
316
|
+
gsrc = GALLERY.read_text(encoding='utf-8')
|
|
317
|
+
if CONST_PAT.search(gsrc):
|
|
318
|
+
GALLERY.write_text(CONST_PAT.sub(lambda m: block, gsrc), encoding='utf-8')
|
|
319
|
+
else:
|
|
320
|
+
print('警告: assets/style-gallery.html 缺少 __TOPPPT_CONSTANTS__ 标记块(画廊配色无法单源化)')
|
|
321
|
+
ok = False
|
|
322
|
+
|
|
323
|
+
# ② 公共片段源
|
|
324
|
+
if not ENGINE.exists() or not UI.exists():
|
|
325
|
+
print(f'错误: 缺少公共片段 {ENGINE} / {UI}')
|
|
326
|
+
return 1
|
|
327
|
+
engine_css = ENGINE.read_text(encoding='utf-8').rstrip('\n')
|
|
328
|
+
ui_js = UI.read_text(encoding='utf-8').rstrip('\n')
|
|
329
|
+
runtime = PE.read_text(encoding='utf-8').rstrip('\n')
|
|
330
|
+
|
|
331
|
+
engine_repl = ('/* __TOPPPT_ENGINE_START__ */\n'
|
|
332
|
+
'/* ══ TopPPT HTML 公共样式引擎(assets/templates/engine.css 的内联副本 · 由 scripts/sync_runtime.py 注入,禁止手改) ══ */\n'
|
|
333
|
+
+ engine_css + '\n/* __TOPPPT_ENGINE_END__ */')
|
|
334
|
+
ui_repl = ('/* __TOPPPT_UI_START__ */\n'
|
|
335
|
+
'/* ══ TopPPT HTML 公共 UI 脚本(assets/templates/ui.js 的内联副本 · 由 scripts/sync_runtime.py 注入,禁止手改) ══ */\n'
|
|
336
|
+
+ ui_js + '\n/* __TOPPPT_UI_END__ */')
|
|
337
|
+
runtime_repl = ('/* __TOPPPT_RUNTIME_START__ */\n'
|
|
338
|
+
'/* ══ PPTX 导出运行时(assets/pptx-export.js 的内联副本 · 由 scripts/sync_runtime.py 注入,禁止手改) ══ */\n'
|
|
339
|
+
+ runtime + '\n/* __TOPPPT_RUNTIME_END__ */')
|
|
340
|
+
|
|
341
|
+
# ③ 注入三份模式模板
|
|
342
|
+
for tpl in TEMPLATES:
|
|
343
|
+
if not tpl.exists():
|
|
344
|
+
print(f'警告: 模板不存在 {tpl}')
|
|
345
|
+
ok = False
|
|
346
|
+
continue
|
|
347
|
+
ok = inject(tpl, ENGINE_PAT, engine_repl, '__TOPPPT_ENGINE__') and ok
|
|
348
|
+
ok = inject(tpl, UI_PAT, ui_repl, '__TOPPPT_UI__') and ok
|
|
349
|
+
ok = inject(tpl, RUNTIME_PAT, runtime_repl, '__TOPPPT_RUNTIME__') and ok
|
|
350
|
+
|
|
351
|
+
# ④ 校验双端单源引用(build_pptx.js → layout-constants.json;extract_model.py → model-schema.json)
|
|
352
|
+
if BJ.exists():
|
|
353
|
+
bj = BJ.read_text(encoding='utf-8')
|
|
354
|
+
if 'layout-constants.json' not in bj:
|
|
355
|
+
print('警告: scripts/build_pptx.js 未引用 layout-constants.json(常量可能漂移)')
|
|
356
|
+
ok = False
|
|
357
|
+
else:
|
|
358
|
+
print('警告: scripts/build_pptx.js 不存在')
|
|
359
|
+
ok = False
|
|
360
|
+
if EM.exists():
|
|
361
|
+
em = EM.read_text(encoding='utf-8')
|
|
362
|
+
if 'model-schema.json' not in em:
|
|
363
|
+
print('警告: scripts/extract_model.py 未引用 model-schema.json(schema 可能漂移)')
|
|
364
|
+
ok = False
|
|
365
|
+
else:
|
|
366
|
+
print('警告: scripts/extract_model.py 不存在')
|
|
367
|
+
ok = False
|
|
368
|
+
|
|
369
|
+
# ⑤ 页型四件套完整性:schema 每个页型都要有几何映射,且映射指向存在的几何组
|
|
370
|
+
pt = lc.get('pageTypes') or {}
|
|
371
|
+
p2g = lc.get('pageTypeGeometry') or {}
|
|
372
|
+
geom_keys = {k for k in pt if not k.startswith('$')}
|
|
373
|
+
schema_types = [k for k in (ms.get('pageTypes') or {}) if not k.startswith('$')]
|
|
374
|
+
for t in schema_types:
|
|
375
|
+
g = p2g.get(t)
|
|
376
|
+
if not g:
|
|
377
|
+
print(f'警告: 页型 {t!r} 在 model-schema.json 中,但 layout-constants.json 的 pageTypeGeometry 缺映射')
|
|
378
|
+
ok = False
|
|
379
|
+
elif g not in geom_keys:
|
|
380
|
+
print(f'警告: 页型 {t!r} 映射到不存在的几何组 {g!r}')
|
|
381
|
+
ok = False
|
|
382
|
+
orphan = [t for t in p2g if not t.startswith('$') and t not in schema_types]
|
|
383
|
+
if orphan:
|
|
384
|
+
print(f'警告: pageTypeGeometry 含未登记页型 {orphan}(schema 与几何不同步)')
|
|
385
|
+
ok = False
|
|
386
|
+
|
|
387
|
+
# ⑥ 图表登记四元组完整性:charts.types 与 charts.registry 双向一致,且 registry 条目字段齐全
|
|
388
|
+
chart_types = [t for t in ((lc.get('charts') or {}).get('types') or [])]
|
|
389
|
+
registry = (lc.get('charts') or {}).get('registry') or {}
|
|
390
|
+
for t in chart_types:
|
|
391
|
+
if t not in registry:
|
|
392
|
+
print(f'警告: 图表类型 {t!r} 在 charts.types 中,但 charts.registry 缺登记')
|
|
393
|
+
ok = False
|
|
394
|
+
for t, spec in registry.items():
|
|
395
|
+
if t.startswith('$'):
|
|
396
|
+
continue
|
|
397
|
+
if t not in chart_types:
|
|
398
|
+
print(f'警告: charts.registry 含未登记类型 {t!r}(types 与 registry 不同步)')
|
|
399
|
+
ok = False
|
|
400
|
+
if spec.get('pptx') not in ('native', 'shape'):
|
|
401
|
+
print(f'警告: 图表 {t!r} 的 pptx 通道非法(应为 native / shape)')
|
|
402
|
+
ok = False
|
|
403
|
+
if not spec.get('dataTable'):
|
|
404
|
+
print(f'警告: 图表 {t!r} 缺 dataTable 策略')
|
|
405
|
+
ok = False
|
|
406
|
+
if spec.get('pptx') == 'shape' and spec.get('dataTable') == 'off' and t != 'sparkline':
|
|
407
|
+
print(f'警告: 非原生图表 {t!r} 的 dataTable 不得为 off(数据可追溯铁律)')
|
|
408
|
+
ok = False
|
|
409
|
+
|
|
410
|
+
# ⑦ 图表类型白名单 ↔ 登记表一致(schema.chartTypes 必须是 registry 的子集)
|
|
411
|
+
schema_charts = [c for c in (ms.get('chartTypes') or [])]
|
|
412
|
+
unknown_charts = [c for c in schema_charts if c not in registry]
|
|
413
|
+
if unknown_charts:
|
|
414
|
+
print(f'警告: model-schema.json chartTypes 含 registry 未登记类型 {unknown_charts}')
|
|
415
|
+
ok = False
|
|
416
|
+
|
|
417
|
+
# ⑦-b 可达性不变量:每个 registry 类型都必须「模型可表达」,否则双引擎实现是死代码。
|
|
418
|
+
# 可达 = 可作 chart.type(在 schema.chartTypes)∪ 可作 sections[].type(信息图专属页型)。
|
|
419
|
+
# 信息图页型清单单源 = charts.scaffold.infoTypes(scaffold/extract_snippet 同源消费)。
|
|
420
|
+
scaff = (lc.get('charts') or {}).get('scaffold') or {}
|
|
421
|
+
info_page_types = set(scaff.get('infoTypes') or {})
|
|
422
|
+
unreachable = [t for t in chart_types if t not in schema_charts and t not in info_page_types]
|
|
423
|
+
if unreachable:
|
|
424
|
+
print(f'警告: 图表类型 {unreachable} 既不在 schema.chartTypes 也不是信息图专属页型 —— '
|
|
425
|
+
'模型无法表达,双引擎实现将成为死代码')
|
|
426
|
+
ok = False
|
|
427
|
+
ghost_pages = sorted(t for t in info_page_types if t not in schema_types)
|
|
428
|
+
if ghost_pages:
|
|
429
|
+
print(f'警告: 信息图页型 {ghost_pages} 未登记在 schema.pageTypes(可达性口径失效)')
|
|
430
|
+
ok = False
|
|
431
|
+
for t in info_page_types:
|
|
432
|
+
if t in schema_charts:
|
|
433
|
+
print(f'警告: 信息图页型 {t!r} 同时出现在 chartTypes —— 二者载荷不同,'
|
|
434
|
+
'会造成 chart 对象字段歧义')
|
|
435
|
+
ok = False
|
|
436
|
+
|
|
437
|
+
# ⑦-d 骨架尺寸单源不变量:charts.scaffold 是 scaffold_report.py 骨架尺寸的唯一事实源——
|
|
438
|
+
# ① 键必须 ⊆ registry(防孤儿尺寸)② 默认值 ≥ charts.minSize 同口径阈值(防骨架天生不达标)
|
|
439
|
+
if not scaff:
|
|
440
|
+
print('警告: layout-constants.json 缺 charts.scaffold(骨架尺寸退回脚本硬编码第二源)')
|
|
441
|
+
ok = False
|
|
442
|
+
else:
|
|
443
|
+
min_size = (lc.get('charts') or {}).get('minSize') or {}
|
|
444
|
+
legal = set(chart_types)
|
|
445
|
+
for caliber, key in (('viewBoxHeight', 'vbHeightMin'), ('pxWidth', 'pxWidthMin')):
|
|
446
|
+
for t, v in (scaff.get(caliber) or {}).items():
|
|
447
|
+
if t not in legal:
|
|
448
|
+
print(f'警告: charts.scaffold.{caliber} 含 registry 未登记类型 {t!r}')
|
|
449
|
+
ok = False
|
|
450
|
+
floor = (min_size.get(t) or {}).get(key)
|
|
451
|
+
if floor is not None and v < floor:
|
|
452
|
+
print(f'警告: charts.scaffold.{caliber}[{t!r}]={v} < minSize.{key}={floor}'
|
|
453
|
+
'(骨架默认值天生不达标)')
|
|
454
|
+
ok = False
|
|
455
|
+
orphan_info = sorted(t for t in info_page_types if t not in legal)
|
|
456
|
+
if orphan_info:
|
|
457
|
+
print(f'警告: charts.scaffold.infoTypes 含 registry 未登记类型 {orphan_info}')
|
|
458
|
+
ok = False
|
|
459
|
+
|
|
460
|
+
# ⑦-c 页型实现可达性(冒烟):schema 的每个页型都应在双引擎源码中出现,
|
|
461
|
+
# 否则「schema 加了页型、引擎没实现」会静默退化为默认版式。
|
|
462
|
+
# 注:这是子串级冒烟检查(不解析 AST),只能拦住「完全没提」的漏实现。
|
|
463
|
+
for eng_name, eng_path in (('scripts/build_pptx.js', ROOT / 'scripts' / 'build_pptx.js'),
|
|
464
|
+
('assets/pptx-export.js', ROOT / 'assets' / 'pptx-export.js')):
|
|
465
|
+
try:
|
|
466
|
+
eng_txt = eng_path.read_text(encoding='utf-8')
|
|
467
|
+
except OSError:
|
|
468
|
+
print(f'警告: 无法读取 {eng_name},跳过页型实现核查')
|
|
469
|
+
ok = False
|
|
470
|
+
continue
|
|
471
|
+
eng_missing = [t for t in schema_types if t not in eng_txt]
|
|
472
|
+
if eng_missing:
|
|
473
|
+
print(f'警告: {eng_name} 未见页型 {eng_missing}(schema 已登记但引擎未提及)')
|
|
474
|
+
ok = False
|
|
475
|
+
|
|
476
|
+
# ⑧ 语义字阶完整性:每个 level 的 role 必须在 modeTypeScale 的角色集合中存在
|
|
477
|
+
mts = lc['modeTypeScale']
|
|
478
|
+
mts_roles = {k for k in (mts.get('presentation') or {})}
|
|
479
|
+
for lv in (lc.get('typography') or {}).get('levels') or []:
|
|
480
|
+
if lv.get('role') not in mts_roles:
|
|
481
|
+
print(f'警告: 语义字阶 {lv.get("id")} 指向不存在的比例尺角色 {lv.get("role")!r}')
|
|
482
|
+
ok = False
|
|
483
|
+
|
|
484
|
+
# ⑨ 图片系统完整性:imageSpec 版式在双引擎可表达 + 占位标签三要素齐全 +
|
|
485
|
+
# image 页型三选一必填口径(src/items/placeholder)。拦「schema 放宽了、引擎没实现」
|
|
486
|
+
# 与「占位文本拼不出来」两类静默退化。
|
|
487
|
+
img = lc.get('imageSpec') or {}
|
|
488
|
+
img_layouts = [x for x in (img.get('layouts') or [])]
|
|
489
|
+
if not img_layouts:
|
|
490
|
+
print('警告: layout-constants.json 缺 imageSpec.layouts(素材图片版式无单源)')
|
|
491
|
+
ok = False
|
|
492
|
+
for key in ('placeholderLabel', 'placeholderHintPrefix', 'placeholderHintSuffix', 'recommendedSizePx'):
|
|
493
|
+
if not img.get(key):
|
|
494
|
+
print(f'警告: imageSpec 缺 {key}(配图占位文本无法由双引擎同源拼接)')
|
|
495
|
+
ok = False
|
|
496
|
+
for lay in img_layouts:
|
|
497
|
+
if lay not in (img.get('recommendedSizePx') or {}):
|
|
498
|
+
print(f'警告: imageSpec.recommendedSizePx 缺版式 {lay!r} 的建议尺寸')
|
|
499
|
+
ok = False
|
|
500
|
+
# 版式比例:每个版式都要有 ratioDefault(双引擎据此算高度)+ ratioCssClass(HTML 锁比例类)
|
|
501
|
+
ratio_def = img.get('ratioDefault') or {}
|
|
502
|
+
ratio_css = img.get('ratioCssClass') or {}
|
|
503
|
+
engine_css = (TPL_DIR / 'engine.css').read_text(encoding='utf-8') if (TPL_DIR / 'engine.css').exists() else ''
|
|
504
|
+
for lay in img_layouts:
|
|
505
|
+
if not ratio_def.get(lay):
|
|
506
|
+
print(f'警告: imageSpec.ratioDefault 缺版式 {lay!r} 的比例(HTML/PPTX 版式会不一致)')
|
|
507
|
+
ok = False
|
|
508
|
+
cls = ratio_css.get(lay)
|
|
509
|
+
if not cls:
|
|
510
|
+
print(f'警告: imageSpec.ratioCssClass 缺版式 {lay!r} 的锁定类')
|
|
511
|
+
ok = False
|
|
512
|
+
elif engine_css and ('.' + cls) not in engine_css:
|
|
513
|
+
print(f'警告: engine.css 缺图片比例锁定类 .{cls}(HTML 侧无法锁定 {lay} 版式比例)')
|
|
514
|
+
ok = False
|
|
515
|
+
img_def = (ms.get('pageTypes') or {}).get('image') or {}
|
|
516
|
+
if not any(str(s).startswith('anyof:') for s in (img_def.get('required') or [])):
|
|
517
|
+
print('警告: model-schema.json 的 image 页型未用 anyof 三选一(src/items/placeholder)')
|
|
518
|
+
ok = False
|
|
519
|
+
for eng_name, eng_path in (('scripts/build_pptx.js', ROOT / 'scripts' / 'build_pptx.js'),
|
|
520
|
+
('assets/pptx-export.js', ROOT / 'assets' / 'pptx-export.js')):
|
|
521
|
+
try:
|
|
522
|
+
eng_txt = eng_path.read_text(encoding='utf-8')
|
|
523
|
+
except OSError:
|
|
524
|
+
continue
|
|
525
|
+
eng_missing_lay = [lay for lay in img_layouts
|
|
526
|
+
if lay not in ('full', 'half') and ("'%s'" % lay) not in eng_txt]
|
|
527
|
+
if eng_missing_lay:
|
|
528
|
+
print(f'警告: {eng_name} 未见图片版式 {eng_missing_lay}(imageSpec 已登记但引擎未提及)')
|
|
529
|
+
ok = False
|
|
530
|
+
|
|
531
|
+
# ⑩ 页型布局 IR(layout_slots.json):高频页型槽位完整性
|
|
532
|
+
slots_path = ROOT / 'scripts' / 'layout_slots.json'
|
|
533
|
+
n_slots = 0
|
|
534
|
+
if not slots_path.exists():
|
|
535
|
+
print('警告: 缺 scripts/layout_slots.json(页型布局 IR 单源)')
|
|
536
|
+
ok = False
|
|
537
|
+
else:
|
|
538
|
+
try:
|
|
539
|
+
slots_doc = json.loads(slots_path.read_text(encoding='utf-8'))
|
|
540
|
+
slots_pages = slots_doc.get('pageTypes') or {}
|
|
541
|
+
for pt_name, pt_def in slots_pages.items():
|
|
542
|
+
if pt_name not in schema_types:
|
|
543
|
+
print(f'警告: layout_slots 页型 {pt_name!r} 不在 schema.pageTypes')
|
|
544
|
+
ok = False
|
|
545
|
+
slot_list = pt_def.get('slots') or []
|
|
546
|
+
if not slot_list:
|
|
547
|
+
print(f'警告: layout_slots 页型 {pt_name!r} 无槽位定义')
|
|
548
|
+
ok = False
|
|
549
|
+
continue
|
|
550
|
+
ids = [s.get('id') for s in slot_list]
|
|
551
|
+
if 'head' not in ids or not any(s.get('role') == 'primary' for s in slot_list):
|
|
552
|
+
print(f'警告: layout_slots 页型 {pt_name!r} 缺 head 或 primary 槽位')
|
|
553
|
+
ok = False
|
|
554
|
+
n_slots = len(slots_pages)
|
|
555
|
+
# 布局 IR 模块存在性(B 通道 require)
|
|
556
|
+
ir_mod = ROOT / 'scripts' / 'lib_layout_regions.js'
|
|
557
|
+
if not ir_mod.exists():
|
|
558
|
+
print('警告: 缺 scripts/lib_layout_regions.js(布局区域 IR 模块)')
|
|
559
|
+
ok = False
|
|
560
|
+
else:
|
|
561
|
+
ir_txt = ir_mod.read_text(encoding='utf-8')
|
|
562
|
+
for key in ('regionOf', 'layout_slots.json', 'layout-constants.json'):
|
|
563
|
+
if key not in ir_txt:
|
|
564
|
+
print(f'警告: lib_layout_regions.js 缺少 {key!r} 引用')
|
|
565
|
+
ok = False
|
|
566
|
+
# 布局 IR 烟测:regionOf 与 pageTypes 常量一致(防公式漂移)
|
|
567
|
+
try:
|
|
568
|
+
import subprocess as _sp
|
|
569
|
+
smoke_js = r'''
|
|
570
|
+
const R = require("./scripts/lib_layout_regions.js");
|
|
571
|
+
const LC = R.LC;
|
|
572
|
+
const cases = [
|
|
573
|
+
["exhibit","primary", LC.pageTypes.exhibit.chartY],
|
|
574
|
+
["metrics","primary", LC.pageTypes.metrics.bandY],
|
|
575
|
+
["head","head", LC.pageTypes.common.headEyebrowY],
|
|
576
|
+
["bar","primary", LC.pageTypes.bar.chartY],
|
|
577
|
+
["table","primary", LC.pageTypes.table.y],
|
|
578
|
+
];
|
|
579
|
+
let bad = 0;
|
|
580
|
+
for (const [t,s,expectY] of cases) {
|
|
581
|
+
const r = R.regionOf(t, s, {});
|
|
582
|
+
if (!r || Math.abs(r.y - expectY) > 0.001) {
|
|
583
|
+
console.error("IR drift", t, s, r && r.y, expectY);
|
|
584
|
+
bad++;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
process.exit(bad ? 1 : 0);
|
|
588
|
+
'''
|
|
589
|
+
node = None
|
|
590
|
+
try:
|
|
591
|
+
sys.path.insert(0, str(ROOT / 'scripts'))
|
|
592
|
+
from env_probe import find_node # noqa: E402
|
|
593
|
+
node = find_node()
|
|
594
|
+
except Exception:
|
|
595
|
+
node = None
|
|
596
|
+
if node:
|
|
597
|
+
rsm = _sp.run([node, '-e', smoke_js], cwd=str(ROOT),
|
|
598
|
+
capture_output=True, text=True)
|
|
599
|
+
if rsm.returncode != 0:
|
|
600
|
+
print('警告: lib_layout_regions regionOf 烟测失败(与 pageTypes 不一致)')
|
|
601
|
+
print((rsm.stderr or rsm.stdout or '')[:400])
|
|
602
|
+
ok = False
|
|
603
|
+
except Exception as exc:
|
|
604
|
+
print(f'警告: 布局 IR 烟测未执行: {exc}')
|
|
605
|
+
except json.JSONDecodeError as exc:
|
|
606
|
+
print(f'警告: layout_slots.json 解析失败: {exc}')
|
|
607
|
+
ok = False
|
|
608
|
+
n_slots = 0
|
|
609
|
+
|
|
610
|
+
# ⑪ 版本单源一致性:layout-constants.json `version` 是唯一事实源;
|
|
611
|
+
# model-schema / layout_slots / package.json(npm 语义化 x.y.z)漂移即 WARN
|
|
612
|
+
# (v8.3 教训:SKILL.md frontmatter 移除 version 后,五处版本一度只有一处升级)
|
|
613
|
+
ver = lc.get('version')
|
|
614
|
+
if not ver:
|
|
615
|
+
print('警告: layout-constants.json 缺 version(版本唯一事实源)')
|
|
616
|
+
ok = False
|
|
617
|
+
else:
|
|
618
|
+
if ms.get('version') != ver:
|
|
619
|
+
print(f'警告: model-schema.json version={ms.get("version")!r} ≠ layout-constants.json {ver!r}')
|
|
620
|
+
ok = False
|
|
621
|
+
try:
|
|
622
|
+
slots_ver = (json.loads((ROOT / 'scripts' / 'layout_slots.json').read_text(encoding='utf-8'))
|
|
623
|
+
.get('version'))
|
|
624
|
+
if slots_ver != ver:
|
|
625
|
+
print(f'警告: layout_slots.json version={slots_ver!r} ≠ {ver!r}')
|
|
626
|
+
ok = False
|
|
627
|
+
except (OSError, json.JSONDecodeError):
|
|
628
|
+
pass # ⑩ 已告警
|
|
629
|
+
try:
|
|
630
|
+
pj_ver = json.loads((ROOT / 'package.json').read_text(encoding='utf-8')).get('version')
|
|
631
|
+
if not str(pj_ver or '').startswith(f'{ver}.'):
|
|
632
|
+
print(f'警告: package.json version={pj_ver!r} 未对齐 {ver}.x(npm 语义化)')
|
|
633
|
+
ok = False
|
|
634
|
+
except (OSError, json.JSONDecodeError):
|
|
635
|
+
pass
|
|
636
|
+
|
|
637
|
+
# ⑤ 摘要哈希(各方核对用)
|
|
638
|
+
raw = LC.read_text(encoding='utf-8')
|
|
639
|
+
digest = hashlib.sha256(raw.encode('utf-8')).hexdigest()[:16]
|
|
640
|
+
ms_raw = MS.read_text(encoding='utf-8')
|
|
641
|
+
ms_digest = hashlib.sha256(ms_raw.encode('utf-8')).hexdigest()[:16]
|
|
642
|
+
n_styles = len(lc['styles'])
|
|
643
|
+
n_pt = len([k for k in lc['pageTypes'] if not k.startswith('$')])
|
|
644
|
+
n_schema_pt = len([k for k in ms['pageTypes'] if not k.startswith('$')])
|
|
645
|
+
n_charts = len((lc.get('charts') or {}).get('types') or [])
|
|
646
|
+
reg = (lc.get('charts') or {}).get('registry') or {}
|
|
647
|
+
n_native = len([t for t, s in reg.items() if not t.startswith('$') and s.get('pptx') == 'native'])
|
|
648
|
+
n_shape = len([t for t, s in reg.items() if not t.startswith('$') and s.get('pptx') == 'shape'])
|
|
649
|
+
n_typo = len((lc.get('typography') or {}).get('levels') or [])
|
|
650
|
+
print('单源注入完成:')
|
|
651
|
+
print(f' layout-constants.json sha256[:16] = {digest}')
|
|
652
|
+
print(f' model-schema.json sha256[:16] = {ms_digest} · 页型 {n_schema_pt} 种')
|
|
653
|
+
print(f' 风格 token {n_styles} 套 · 三模式独立比例尺 {len([k for k in lc["modeTypeScale"] if not k.startswith("$")])} 套 · 页型几何 {n_pt} 组')
|
|
654
|
+
print(f' 图表登记 {n_charts} 种(原生 {n_native} / 形状 {n_shape}) · 语义字阶 {n_typo} 级 · 12 列网格 {lc["grid"]["columns"]} 列 · 布局 IR {n_slots} 页型')
|
|
655
|
+
print(' 已刷新: assets/pptx-export.js(常量块 + schema 块) · assets/style-gallery.html(常量块) · templates/{presentation,research,architecture}.html(引擎/UI/运行时内联副本)')
|
|
656
|
+
print(f' 双端单源引用校验: {"PASS" if ok else "WARN(build_pptx 应 require layout-constants.json;extract_model 应读 model-schema.json;页型四件套与图表登记须完整)"}')
|
|
657
|
+
print(f' 说明: 页型几何 {n_pt} 组(多页型共享几何组) · schema 页型 {n_schema_pt} 种')
|
|
658
|
+
return 0
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
if __name__ == '__main__':
|
|
662
|
+
sys.exit(main())
|