@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,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TopPPT HTML · 页型布局区域 IR(双通道几何收敛起点)
|
|
3
|
+
*
|
|
4
|
+
* 从 layout-constants.json + layout_slots.json 派生「语义槽位 → 英寸矩形」。
|
|
5
|
+
* B 通道(build_pptx.js)直接 require;A 通道消费同一 LC 常量(数字同源),
|
|
6
|
+
* 本模块是把「槽位语义」从散落坐标收拢成可测接口的第一步。
|
|
7
|
+
*
|
|
8
|
+
* 纪律:改几何只改 layout-constants.json;改槽位只改 layout_slots.json。
|
|
9
|
+
* 新增页型先扩 layout_slots.json,再在此补 regionOf 分支,最后写双引擎。
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const ROOT = path.join(__dirname, '..');
|
|
17
|
+
const LC = JSON.parse(fs.readFileSync(path.join(__dirname, 'layout-constants.json'), 'utf8'));
|
|
18
|
+
// 布局 IR:优先 layout-constants.layoutSlots(v9 单源合并);否则回落 layout_slots.json
|
|
19
|
+
let SLOTS = LC.layoutSlots;
|
|
20
|
+
if (!SLOTS || !SLOTS.pageTypes) {
|
|
21
|
+
SLOTS = JSON.parse(fs.readFileSync(path.join(__dirname, 'layout_slots.json'), 'utf8'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const INFO_TYPES = {
|
|
25
|
+
sankey: 1, treemap: 1, boxplot: 1, network: 1, marimekko: 1, streamgraph: 1,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function pageBox() {
|
|
29
|
+
const p = LC.page;
|
|
30
|
+
return { pw: p.pw, ph: p.ph, mx: p.mx, cw: p.pw - 2 * p.mx };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 内容区垂直范围(含注释层预留) */
|
|
34
|
+
function contentBand(opts) {
|
|
35
|
+
const L = LC.pageTypes.layout;
|
|
36
|
+
const C = LC.pageTypes.common;
|
|
37
|
+
const withNote = !!(opts && opts.withNote);
|
|
38
|
+
const top = (opts && opts.top != null) ? opts.top : C.bodyY;
|
|
39
|
+
const bottom = withNote ? L.contentBottomWithNote : L.contentBottom;
|
|
40
|
+
return { top, bottom, h: Math.max(0, bottom - top) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 语义槽位 → {x,y,w,h}(英寸)
|
|
45
|
+
* 未识别页型/槽位时返回 null(调用方回落自身逻辑,不中断)。
|
|
46
|
+
*/
|
|
47
|
+
function regionOf(pageType, slotId, opts) {
|
|
48
|
+
const type = String(pageType || 'points');
|
|
49
|
+
const slot = String(slotId || '');
|
|
50
|
+
const { mx, cw } = pageBox();
|
|
51
|
+
const C = LC.pageTypes.common;
|
|
52
|
+
const L = LC.pageTypes.layout;
|
|
53
|
+
const PT = LC.pageTypes[type] || {};
|
|
54
|
+
|
|
55
|
+
// ── chrome:页头 ──
|
|
56
|
+
if (slot === 'head' || slot === 'chrome') {
|
|
57
|
+
return {
|
|
58
|
+
x: mx, y: C.headEyebrowY, w: cw,
|
|
59
|
+
h: Math.max(0.4, (C.headRuleY || 1.95) - C.headEyebrowY),
|
|
60
|
+
titleY: C.headTitleY, ruleY: C.headRuleY, leadY: C.leadY, bodyY: C.bodyY,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── annotation:so-what / 来源 / note ──
|
|
65
|
+
if (slot === 'annotation' || slot === 'soWhat') {
|
|
66
|
+
const y = (LC.pageTypes.exhibit && LC.pageTypes.exhibit.soWhatY != null)
|
|
67
|
+
? LC.pageTypes.exhibit.soWhatY : 6.05;
|
|
68
|
+
return { x: mx, y, w: cw, h: 0.62 };
|
|
69
|
+
}
|
|
70
|
+
if (slot === 'footnote') {
|
|
71
|
+
const y = (LC.pageTypes.exhibit && LC.pageTypes.exhibit.footnoteY != null)
|
|
72
|
+
? LC.pageTypes.exhibit.footnoteY
|
|
73
|
+
: ((LC.pageTypes.note && LC.pageTypes.note.y) || 6.55);
|
|
74
|
+
return { x: mx, y, w: cw - 1.2, h: (LC.pageTypes.note && LC.pageTypes.note.h) || 0.32 };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── 页型主从区域 ──
|
|
78
|
+
if (type === 'points') {
|
|
79
|
+
const band = contentBand(opts);
|
|
80
|
+
if (slot === 'primary' || slot === 'list') {
|
|
81
|
+
return { x: mx, y: PT.listY || band.top, w: cw, h: Math.max(0.8, band.bottom - (PT.listY || band.top)) };
|
|
82
|
+
}
|
|
83
|
+
if (slot === 'metrics') {
|
|
84
|
+
return { x: mx, y: PT.metricY || 4.7, w: cw, h: Math.max(0.6, L.contentBottom - (PT.metricY || 4.7)) };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (type === 'metrics') {
|
|
89
|
+
if (slot === 'primary') {
|
|
90
|
+
return {
|
|
91
|
+
x: mx, y: PT.bandY || 3.1, w: cw, h: PT.cardH || 2.3,
|
|
92
|
+
valY: PT.valY, valH: PT.valH, capY: PT.capY, capH: PT.capH,
|
|
93
|
+
gap: PT.gap, maxW: PT.maxW,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (type === 'table') {
|
|
99
|
+
if (slot === 'primary') {
|
|
100
|
+
const y = PT.y || L.contentTop;
|
|
101
|
+
return {
|
|
102
|
+
x: mx, y, w: cw,
|
|
103
|
+
h: Math.max(1, ((opts && opts.bottom) || L.contentBottom) - y),
|
|
104
|
+
rowH: PT.rowH, rowHMin: PT.rowHMin, headRowH: PT.headRowH,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (type === 'bar') {
|
|
110
|
+
if (slot === 'primary') {
|
|
111
|
+
const y = PT.chartY || 2.95;
|
|
112
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom
|
|
113
|
+
: ((opts && opts.withNote) ? L.contentBottomWithNote : L.contentBottom);
|
|
114
|
+
return { x: mx + (PT.chartX || 0), y, w: cw - (PT.chartW || 0), h: Math.max(1.2, bottom - y) };
|
|
115
|
+
}
|
|
116
|
+
if (slot === 'hbar') {
|
|
117
|
+
const y0 = PT.hbarY0 || 2.6;
|
|
118
|
+
const y1 = (opts && opts.bottom != null) ? opts.bottom : (PT.hbarY1 || 6.25);
|
|
119
|
+
return { x: mx, y: y0, w: cw, h: Math.max(1.2, y1 - y0) };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (type === 'donut') {
|
|
124
|
+
if (slot === 'primary') {
|
|
125
|
+
const y = PT.chartY || 2.95;
|
|
126
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
|
|
127
|
+
return { x: mx, y, w: cw, h: Math.max(1.2, bottom - y) };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (type === 'exhibit') {
|
|
132
|
+
if (slot === 'primary') {
|
|
133
|
+
const y = PT.chartY || 2.95;
|
|
134
|
+
const h = Math.max(1.2, (L.contentBottomWithNote - y));
|
|
135
|
+
return { x: mx, y, w: cw, h, badgeY: PT.badgeY };
|
|
136
|
+
}
|
|
137
|
+
if (slot === 'secondary') {
|
|
138
|
+
return { x: mx, y: PT.badgeY || L.contentTop, w: cw * 0.32, h: 0.4 };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (type === 'points') {
|
|
143
|
+
const band = contentBand(opts);
|
|
144
|
+
if (slot === 'primary' || slot === 'list') {
|
|
145
|
+
return {
|
|
146
|
+
x: mx, y: PT.listY || band.top, w: cw,
|
|
147
|
+
h: Math.max(0.8, (opts && opts.bottom != null ? opts.bottom : band.bottom) - (PT.listY || band.top)),
|
|
148
|
+
rowH: PT.rowH, rowHMin: PT.rowHMin, markSize: PT.markSize,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (slot === 'metrics') {
|
|
152
|
+
return {
|
|
153
|
+
x: mx, y: PT.metricY || 4.7, w: cw,
|
|
154
|
+
h: Math.max(0.6, L.contentBottom - (PT.metricY || 4.7)),
|
|
155
|
+
metricW: PT.metricW, metricValH: PT.metricValH, metricCapY: PT.metricCapY,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (type === 'split') {
|
|
161
|
+
const SP = PT;
|
|
162
|
+
const top = SP.chartY || L.contentTop;
|
|
163
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
|
|
164
|
+
if (slot === 'left') {
|
|
165
|
+
return {
|
|
166
|
+
x: mx, y: top, w: cw * (SP.leftW || 0.55),
|
|
167
|
+
h: Math.max(1.2, bottom - top),
|
|
168
|
+
tableY: SP.tableY, tableRowH: SP.tableRowH, rowH: SP.rowH, capY: SP.capY,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (slot === 'right') {
|
|
172
|
+
const rx = mx + cw * (SP.rightX != null ? SP.rightX : 0.585);
|
|
173
|
+
return {
|
|
174
|
+
x: rx, y: top, w: cw * (SP.rightW || 0.415),
|
|
175
|
+
h: Math.max(1.2, bottom - top),
|
|
176
|
+
tableY: SP.tableY, tableRowH: SP.tableRowH, rowH: SP.rowH, capY: SP.capY,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (type === 'twocol') {
|
|
182
|
+
const gap = PT.colGap || 0.5;
|
|
183
|
+
const colW = (cw - gap) / 2;
|
|
184
|
+
const top = (opts && opts.top != null) ? opts.top : L.contentTop;
|
|
185
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
|
|
186
|
+
if (slot === 'primary' || slot === 'left') {
|
|
187
|
+
return { x: mx, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW };
|
|
188
|
+
}
|
|
189
|
+
if (slot === 'secondary' || slot === 'right') {
|
|
190
|
+
return { x: mx + colW + gap, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (type === 'diagram' || type === 'lane') {
|
|
195
|
+
if (slot === 'primary') {
|
|
196
|
+
const arch = LC.pageTypes.arch || {};
|
|
197
|
+
const y = (opts && opts.mode === 'architecture' && arch.fullStartY != null)
|
|
198
|
+
? arch.fullStartY
|
|
199
|
+
: (PT.bodyStartY || 2.9);
|
|
200
|
+
const b = (opts && opts.mode === 'architecture' && arch.fullEndY != null)
|
|
201
|
+
? arch.fullEndY
|
|
202
|
+
: ((opts && opts.bottom != null) ? opts.bottom : L.contentBottom);
|
|
203
|
+
return {
|
|
204
|
+
x: mx, y, w: cw, h: Math.max(1.2, b - y),
|
|
205
|
+
layerBarW: PT.layerBarW, layerGap: PT.layerGap, maxLayerH: PT.maxLayerH,
|
|
206
|
+
laneH: arch.laneH, laneHeadW: arch.laneHeadW, laneGap: arch.laneGap,
|
|
207
|
+
stepGap: arch.stepGap, stepMaxW: arch.stepMaxW,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── 剩余高频页型 ──
|
|
213
|
+
if (type === 'kpi') {
|
|
214
|
+
if (slot === 'primary' || slot === 'hero') {
|
|
215
|
+
return {
|
|
216
|
+
x: mx, y: PT.heroY || 2.9, w: cw * (PT.heroW || 0.52), h: PT.heroH || 1.6,
|
|
217
|
+
dividerX: PT.dividerX, metricY0: PT.metricY0, metricRowH: PT.metricRowH,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (slot === 'metrics') {
|
|
221
|
+
const dx = PT.dividerX != null ? PT.dividerX : 0.56;
|
|
222
|
+
return {
|
|
223
|
+
x: mx + cw * dx + 0.35, y: PT.metricY0 || 2.95,
|
|
224
|
+
w: cw * (1 - dx) - 0.5, h: Math.max(0.6, L.contentBottom - (PT.metricY0 || 2.95)),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (type === 'comparison') {
|
|
230
|
+
const gap = PT.panelGap || 0.4;
|
|
231
|
+
const panelW = (cw - gap) / 2;
|
|
232
|
+
const y = PT.panelY || 2.55;
|
|
233
|
+
const h = PT.panelH || 3.4;
|
|
234
|
+
if (slot === 'primary' || slot === 'left') {
|
|
235
|
+
return { x: mx, y, w: panelW, h, titleH: PT.titleH, rowH: PT.rowH };
|
|
236
|
+
}
|
|
237
|
+
if (slot === 'right' || slot === 'secondary') {
|
|
238
|
+
return { x: mx + panelW + gap, y, w: panelW, h, titleH: PT.titleH, rowH: PT.rowH };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (type === 'quote') {
|
|
243
|
+
if (slot === 'primary') {
|
|
244
|
+
return {
|
|
245
|
+
x: mx + 1.05, y: PT.textY || 2.7, w: cw - 2.1, h: PT.textH || 2.3,
|
|
246
|
+
markY: PT.markY, authorY: PT.authorY, contextY: PT.contextY,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (type === 'heatmap') {
|
|
252
|
+
if (slot === 'primary') {
|
|
253
|
+
const y = PT.y || 2.6;
|
|
254
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
|
|
255
|
+
return {
|
|
256
|
+
x: mx, y, w: cw, h: Math.max(1, bottom - y),
|
|
257
|
+
labelW: PT.labelW, headH: PT.headH, cellH: PT.cellH, cellGap: PT.cellGap,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (type === 'bullet') {
|
|
263
|
+
if (slot === 'primary') {
|
|
264
|
+
const y = PT.y || 2.6;
|
|
265
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
|
|
266
|
+
return {
|
|
267
|
+
x: mx, y, w: cw, h: Math.max(1, bottom - y),
|
|
268
|
+
rowH: PT.rowH, labelW: PT.labelW, valW: PT.valW, barH: PT.barH,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (type === 'pyramid') {
|
|
274
|
+
if (slot === 'primary') {
|
|
275
|
+
const y = PT.y || 2.35;
|
|
276
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
|
|
277
|
+
return {
|
|
278
|
+
x: mx, y, w: cw, h: Math.max(1, bottom - y),
|
|
279
|
+
rowH: PT.rowH, gap: PT.gap, minW: PT.minW, labelW: PT.labelW,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (type === 'steps') {
|
|
285
|
+
if (slot === 'primary') {
|
|
286
|
+
const y = PT.y || 3.05;
|
|
287
|
+
return {
|
|
288
|
+
x: mx, y, w: cw, h: PT.barH || 1.35,
|
|
289
|
+
gap: PT.gap, arrowW: PT.arrowW, numH: PT.numH, titleH: PT.titleH,
|
|
290
|
+
pad: PT.pad, maxPerRow: PT.maxPerRow,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (type === 'cards') {
|
|
296
|
+
if (slot === 'primary') {
|
|
297
|
+
const y = PT.startY || 3.0;
|
|
298
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
|
|
299
|
+
return { x: mx, y, w: cw, h: Math.max(1, bottom - y), gap: PT.gap, maxH: PT.maxH, titleH: PT.titleH };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (type === 'timeline') {
|
|
304
|
+
if (slot === 'primary') {
|
|
305
|
+
return {
|
|
306
|
+
x: mx, y: PT.labelY || 2.95, w: cw,
|
|
307
|
+
h: Math.max(1, L.contentBottom - (PT.labelY || 2.95)),
|
|
308
|
+
axisY: PT.axisY, dotY: PT.dotY, dotR: PT.dotR, nameY: PT.nameY,
|
|
309
|
+
descY: PT.descY, descH: PT.descH,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (type === 'image') {
|
|
315
|
+
if (slot === 'primary') {
|
|
316
|
+
const y = Math.max(PT.y || 2.3, (opts && opts.top) || 0);
|
|
317
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
|
|
318
|
+
const capH = (opts && opts.caption) ? 0.32 : 0;
|
|
319
|
+
return {
|
|
320
|
+
x: mx, y, w: cw, h: Math.max(1.2, Math.min(PT.h || 4.2, bottom - y - capH)),
|
|
321
|
+
halfW: PT.halfW, halfGap: PT.halfGap, capY: PT.capY, capH: PT.capH || 0.3,
|
|
322
|
+
radius: PT.radius, gridGap: PT.gridGap, gridCols: PT.gridCols,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (type === 'halftable') {
|
|
328
|
+
if (slot === 'primary' || slot === 'left') {
|
|
329
|
+
const r = LC.pageTypes.research || {};
|
|
330
|
+
const top = (opts && opts.top != null) ? opts.top : L.contentTop;
|
|
331
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
|
|
332
|
+
return { x: mx, y: top, w: cw * (r.halfTableW || 0.52), h: Math.max(1, bottom - top) };
|
|
333
|
+
}
|
|
334
|
+
if (slot === 'secondary' || slot === 'right') {
|
|
335
|
+
const r = LC.pageTypes.research || {};
|
|
336
|
+
const top = (opts && opts.top != null) ? opts.top : L.contentTop;
|
|
337
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
|
|
338
|
+
return {
|
|
339
|
+
x: mx + cw * (r.halfChartX || 0.57), y: top,
|
|
340
|
+
w: cw * (r.halfChartW || 0.43), h: Math.max(1, bottom - top),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (type === 'matrix') {
|
|
346
|
+
if (slot === 'primary') {
|
|
347
|
+
const r = LC.pageTypes.research || {};
|
|
348
|
+
const y = r.matrixY || 2.3;
|
|
349
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
|
|
350
|
+
return {
|
|
351
|
+
x: mx, y, w: cw, h: Math.max(1, bottom - y),
|
|
352
|
+
labelW: r.matrixLabelW, cellH: r.matrixCellH,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (type === 'threecol') {
|
|
358
|
+
const r = LC.pageTypes.research || {};
|
|
359
|
+
const gap = r.col3Gap || 0.38;
|
|
360
|
+
const colW = (cw - 2 * gap) / 3;
|
|
361
|
+
const top = (opts && opts.top != null) ? opts.top : L.contentTop;
|
|
362
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
|
|
363
|
+
if (slot === 'primary' || slot === 'left') {
|
|
364
|
+
return { x: mx, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW, cols: 3 };
|
|
365
|
+
}
|
|
366
|
+
if (slot === 'middle') {
|
|
367
|
+
return { x: mx + colW + gap, y: top, w: colW, h: Math.max(1, bottom - top), gap, colW, cols: 3 };
|
|
368
|
+
}
|
|
369
|
+
if (slot === 'right' || slot === 'secondary') {
|
|
370
|
+
return { x: mx + 2 * (colW + gap), y: top, w: colW, h: Math.max(1, bottom - top), gap, colW, cols: 3 };
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// 信息图页型:内容区主图
|
|
375
|
+
if (INFO_TYPES[type] && (slot === 'primary')) {
|
|
376
|
+
const y = PT.y || L.contentTop;
|
|
377
|
+
const h = PT.h || 4.1;
|
|
378
|
+
const bottom = (opts && opts.bottom != null) ? opts.bottom : L.contentBottom;
|
|
379
|
+
return { x: mx, y, w: cw, h: Math.max(1.2, Math.min(h, bottom - y)) };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// 默认:内容区全宽
|
|
383
|
+
if (slot === 'primary' || slot === 'secondary' || slot === 'left' || slot === 'right') {
|
|
384
|
+
const band = contentBand(opts);
|
|
385
|
+
return { x: mx, y: band.top, w: cw, h: band.h };
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** 槽位是否在 layout_slots.json 中声明为 required */
|
|
391
|
+
function slotRequired(pageType, slotId) {
|
|
392
|
+
const def = (SLOTS.pageTypes || {})[pageType];
|
|
393
|
+
if (!def) return false;
|
|
394
|
+
const s = (def.slots || []).find(x => x.id === slotId);
|
|
395
|
+
return !!(s && s.required);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** 列出页型已声明槽位 id */
|
|
399
|
+
function slotIds(pageType) {
|
|
400
|
+
const def = (SLOTS.pageTypes || {})[pageType];
|
|
401
|
+
return (def && def.slots) ? def.slots.map(s => s.id) : [];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = {
|
|
405
|
+
LC,
|
|
406
|
+
SLOTS,
|
|
407
|
+
pageBox,
|
|
408
|
+
contentBand,
|
|
409
|
+
regionOf,
|
|
410
|
+
slotRequired,
|
|
411
|
+
slotIds,
|
|
412
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML · HTML 页高与容器裁切真值测量(可选 · 需 playwright)
|
|
4
|
+
|
|
5
|
+
静态估算(validate_report 页高溢出)是交付前硬门禁的保守近似,但它看不到
|
|
6
|
+
真实布局:「表格被半幅容器横向裁掉一列」这类缺陷在 strict 全绿时照样发生。
|
|
7
|
+
本脚本用无头浏览器量真实布局高度与容器裁切,供深度交付 / 排版争议时对照。
|
|
8
|
+
|
|
9
|
+
用法:
|
|
10
|
+
python scripts/measure_height.py report.html
|
|
11
|
+
python scripts/measure_height.py report.html --json
|
|
12
|
+
python scripts/measure_height.py report.html --width 1920 --height 1080
|
|
13
|
+
|
|
14
|
+
缺 playwright / chromium 时打印指引并 exit 0(不阻断交付链)。
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> int:
|
|
30
|
+
ap = argparse.ArgumentParser(description='HTML 页高真值测量(Playwright)')
|
|
31
|
+
ap.add_argument('html', help='报告 HTML 路径')
|
|
32
|
+
ap.add_argument('--width', type=int, default=1920)
|
|
33
|
+
ap.add_argument('--height', type=int, default=1080)
|
|
34
|
+
ap.add_argument('--json', action='store_true')
|
|
35
|
+
ap.add_argument('--tolerance-px', type=int, default=8,
|
|
36
|
+
help='band 高度相对视口的容差(超过则记 overflow)')
|
|
37
|
+
args = ap.parse_args()
|
|
38
|
+
|
|
39
|
+
html = Path(args.html)
|
|
40
|
+
if not html.exists():
|
|
41
|
+
print(f'错误:HTML 不存在 {html}')
|
|
42
|
+
return 2
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
from playwright.sync_api import sync_playwright
|
|
46
|
+
except ImportError:
|
|
47
|
+
# 托管 Python 可能未装 playwright;回落到本机已装 playwright 的解释器(不阻断)
|
|
48
|
+
import shutil
|
|
49
|
+
import subprocess
|
|
50
|
+
candidates = []
|
|
51
|
+
for name in ('python', 'python3', 'py'):
|
|
52
|
+
exe = shutil.which(name)
|
|
53
|
+
if exe:
|
|
54
|
+
candidates.append(exe)
|
|
55
|
+
# 常见用户安装路径不写死绝对盘符/用户名;仅用 PATH / py launcher
|
|
56
|
+
for extra in (
|
|
57
|
+
r'py',
|
|
58
|
+
):
|
|
59
|
+
if Path(extra).exists() or shutil.which(extra):
|
|
60
|
+
candidates.append(extra)
|
|
61
|
+
alt = None
|
|
62
|
+
for exe in candidates:
|
|
63
|
+
try:
|
|
64
|
+
probe = subprocess.run(
|
|
65
|
+
[exe, '-c', 'import playwright; print(1)'],
|
|
66
|
+
capture_output=True, text=True, timeout=8)
|
|
67
|
+
if probe.returncode == 0 and '1' in (probe.stdout or ''):
|
|
68
|
+
alt = exe
|
|
69
|
+
break
|
|
70
|
+
except Exception:
|
|
71
|
+
continue
|
|
72
|
+
if not alt:
|
|
73
|
+
print('跳过:未安装 playwright(pip install playwright && playwright install chromium)')
|
|
74
|
+
print('静态估算仍由 validate_report.py --strict 把关。')
|
|
75
|
+
return 0
|
|
76
|
+
# 用有 playwright 的解释器重跑本脚本(避免在无 playwright 的解释器里空转)
|
|
77
|
+
import subprocess as sp
|
|
78
|
+
cmd = [alt, str(Path(__file__).resolve()), str(html),
|
|
79
|
+
'--width', str(args.width), '--height', str(args.height),
|
|
80
|
+
'--tolerance-px', str(args.tolerance_px)]
|
|
81
|
+
if args.json:
|
|
82
|
+
cmd.append('--json')
|
|
83
|
+
raise SystemExit(sp.call(cmd))
|
|
84
|
+
|
|
85
|
+
url = html.resolve().as_uri()
|
|
86
|
+
results = []
|
|
87
|
+
with sync_playwright() as p:
|
|
88
|
+
try:
|
|
89
|
+
browser = p.chromium.launch(headless=True)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
print(f'跳过:无法启动 chromium({e})。请执行 playwright install chromium。')
|
|
92
|
+
return 0
|
|
93
|
+
page = browser.new_page(viewport={'width': args.width, 'height': args.height})
|
|
94
|
+
page.goto(url, wait_until='load', timeout=60000)
|
|
95
|
+
# 关掉工具栏对页高的影响:尽量贴近演示全屏内容区
|
|
96
|
+
page.evaluate("""() => {
|
|
97
|
+
const bar = document.querySelector('.bar');
|
|
98
|
+
if (bar) bar.style.display = 'none';
|
|
99
|
+
document.documentElement.style.setProperty('--bar-h', '0px');
|
|
100
|
+
}""")
|
|
101
|
+
page.wait_for_timeout(200)
|
|
102
|
+
bands = page.evaluate("""(tol) => {
|
|
103
|
+
const vh = window.innerHeight;
|
|
104
|
+
const nodes = [...document.querySelectorAll('section.band')];
|
|
105
|
+
return nodes.map((el, i) => {
|
|
106
|
+
const r = el.getBoundingClientRect();
|
|
107
|
+
const h = Math.round(el.offsetHeight);
|
|
108
|
+
const flow = el.classList.contains('band--flow');
|
|
109
|
+
const id = el.id || ('band-' + (i + 1));
|
|
110
|
+
return {
|
|
111
|
+
index: i + 1,
|
|
112
|
+
id,
|
|
113
|
+
height: h,
|
|
114
|
+
viewport: vh,
|
|
115
|
+
overflow: !flow && h > vh + tol,
|
|
116
|
+
flow,
|
|
117
|
+
title: (el.querySelector('.shead__title, h1, h2')?.textContent || '').trim().slice(0, 40),
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
}""", args.tolerance_px)
|
|
121
|
+
# 容器级裁切真值:静态估算只看页高,看不到「表格/卡片被归属容器横向或纵向裁掉」——
|
|
122
|
+
# 裁切比压缩更糟(数据直接不可见),且 strict 全绿时依然可能发生。
|
|
123
|
+
clipped = page.evaluate("""() => {
|
|
124
|
+
const sel = '.card,.panel,.metric,.sowhat,.exhibit,.kpi,.note,.tbl-wrap,td,th';
|
|
125
|
+
const out = [];
|
|
126
|
+
document.querySelectorAll(sel).forEach(el => {
|
|
127
|
+
if (getComputedStyle(el).overflow === 'visible') return;
|
|
128
|
+
const dw = el.scrollWidth - el.clientWidth;
|
|
129
|
+
const dh = el.scrollHeight - el.clientHeight;
|
|
130
|
+
if (dw > 3 || dh > 3) {
|
|
131
|
+
const band = el.closest('section.band');
|
|
132
|
+
out.push({
|
|
133
|
+
container: (el.className || el.tagName).toString().slice(0, 30),
|
|
134
|
+
page: band ? ([...document.querySelectorAll('section.band')].indexOf(band) + 1) : 0,
|
|
135
|
+
overflowX: dw > 3 ? dw : 0,
|
|
136
|
+
overflowY: dh > 3 ? dh : 0,
|
|
137
|
+
text: (el.textContent || '').trim().slice(0, 24),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return out;
|
|
142
|
+
}""")
|
|
143
|
+
browser.close()
|
|
144
|
+
|
|
145
|
+
over = [b for b in bands if b['overflow']]
|
|
146
|
+
payload = {
|
|
147
|
+
'file': str(html),
|
|
148
|
+
'viewport': f'{args.width}x{args.height}',
|
|
149
|
+
'bands': len(bands),
|
|
150
|
+
'overflow_count': len(over),
|
|
151
|
+
'overflow': over,
|
|
152
|
+
'clipped_count': len(clipped),
|
|
153
|
+
'clipped': clipped,
|
|
154
|
+
'details': bands,
|
|
155
|
+
}
|
|
156
|
+
if args.json:
|
|
157
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
158
|
+
else:
|
|
159
|
+
print(f'页高真值测量 · {html.name} @ {args.width}×{args.height}')
|
|
160
|
+
print(f' band 数 {len(bands)} · 超一屏 {len(over)} · 容器裁切 {len(clipped)}')
|
|
161
|
+
for b in bands:
|
|
162
|
+
mark = '✗' if b['overflow'] else ('·' if b['flow'] else '✓')
|
|
163
|
+
print(f' {mark} 第{b["index"]}页 {b["id"]:16} {b["height"]:4}px {b["title"]}')
|
|
164
|
+
if clipped:
|
|
165
|
+
print(' ── 容器级裁切(内容越过归属容器,铁律 11)──')
|
|
166
|
+
for c in clipped[:8]:
|
|
167
|
+
axis = f'横向 {c["overflowX"]}px' if c['overflowX'] else f'纵向 {c["overflowY"]}px'
|
|
168
|
+
print(f' ✗ 第{c["page"]}页 {c["container"]} {axis} 「{c["text"]}」')
|
|
169
|
+
print(' 处理:换组合版式/减列/缩表 → 调容器宽度 → 拆页;不要靠容器滚动条遮掩')
|
|
170
|
+
if over:
|
|
171
|
+
print(' 处理:列表化/精炼 → 压缩间距 → 多列 → 拆页;长结构页加 band--flow')
|
|
172
|
+
if not over and not clipped:
|
|
173
|
+
print(' 结论:全部固定页高 band 未超视口,且无容器级裁切')
|
|
174
|
+
return 0 if not over and not clipped else 1
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == '__main__':
|
|
178
|
+
sys.exit(main())
|