@topmindspace/tms-skills 0.1.5 → 0.1.7
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 +29 -0
- package/README.md +4 -4
- package/package.json +1 -1
- package/top-ppt-html/README.md +1 -1
- package/top-ppt-html/SKILL.md +6 -6
- package/top-ppt-html/assets/examples/2026-09-09-architecture-graphite-dark.html +10 -5
- package/top-ppt-html/assets/examples/2026-09-09-presentation-business-blue.html +10 -5
- package/top-ppt-html/assets/examples/2026-09-09-research-mckinsey.html +10 -5
- package/top-ppt-html/assets/pptx-export.js +9 -4
- package/top-ppt-html/assets/templates/architecture.html +10 -5
- package/top-ppt-html/assets/templates/presentation.html +10 -5
- package/top-ppt-html/assets/templates/research.html +10 -5
- package/top-ppt-html/package-lock.json +2 -2
- package/top-ppt-html/package.json +1 -1
- package/top-ppt-html/references/playbook.md +1 -1
- package/top-ppt-html/scripts/build_pptx.js +12 -5
- package/top-ppt-html/scripts/smoke_pptx.sh +42 -2
- package/top-ppt-html/scripts/test_feedback_gates.py +189 -0
- package/top-ppt-html/scripts/validate_pptx.py +147 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
## 0.1.7 — 2026-09-26
|
|
2
|
+
|
|
3
|
+
### top-ppt-html · defect opt (D8/D9/D11/D13)
|
|
4
|
+
|
|
5
|
+
- **P0 D8**:`chartBottom(hasSoWhat, hasFootnote)` 不再 footnote 短路;`Math.min(soWhatY, footnoteY, contentBottomWithNote)` 与 flagY 让位同口径。双通道(`build_pptx.js` / `pptx-export.js`)对齐;flagBar 在 withNote 时底边不越过 `contentBottomWithNote`;Agenda 预留 0.25in 避免 severe 误伤。
|
|
6
|
+
- **P0 D9**:`annotation_band_overlap_check` 报告页内**全部**侵入者(去掉首条即 `break`)。
|
|
7
|
+
- **P1 D11**:有 so-what 时 `band_top = soWhatY`(6.05),捕获 (6.05, 6.40] 侵入;crush = 自带顶之上压下;注释自形状/窄 accent 条豁免保留。
|
|
8
|
+
- **P1 D13**:`ci_skill_gates.sh --with-pptx` 必跑 `smoke_pptx` × business-blue **与** research-mckinsey(覆盖 soWhat+footnote);graphite-dark 可选。
|
|
9
|
+
- **CHROME_DRIFT**:页码只认 `N / M`,不再把年份/Exhibit 编号当页脚(research-mckinsey 假阳性清除)。
|
|
10
|
+
- **Fast Mode / 效率**:演讲/汇报线索→Mode A;明确跳过 vs 仍须(质检红线不降);L0+L1 / `extract_snippet` 纪律不变。
|
|
11
|
+
- **门禁**:`test_feedback_gates` 锁定 D8/D9/D11/CHROME;research-mckinsey `--strict` **0/0**。
|
|
12
|
+
- 版本对齐 **0.1.7**(根 + 技能 package / SKILL metadata / README / PUBLISHING)。
|
|
13
|
+
|
|
14
|
+
### 红线未动
|
|
15
|
+
反截断、图表多样性地板、Mode A craft、runtime SHA 同版本门禁、中文「演示文稿」术语保持。
|
|
16
|
+
|
|
17
|
+
## 0.1.6 — 2026-09-25
|
|
18
|
+
|
|
19
|
+
### CI / Release 硬化
|
|
20
|
+
- **根因 A**:`validate_pptx` 的 `ANNOTATION_BAND_OVERLAP` 把全幅背景(底边 7.50in)误判为侵入注释带;改为排除 full-bleed 背景 / 全高装饰条 / chrome 区,同时保留对图例/主图压进 so-what 带的真阳性。
|
|
21
|
+
- **根因 A′**:`FONT_SIZE_NOT_SNAPPED` 未收录 `modeTypeScale` 的 **h2=17pt**;改为从 typeScale ∪ 全模式 modeTypeScale ∪ ladder 动态取允许集。
|
|
22
|
+
- **冒烟可读性**:`smoke_pptx.sh` 可靠传播 validate 退出码;失败时 stderr 先打错误码/页码短摘要。
|
|
23
|
+
- **根因 B**:Release `npm publish` 遇已发布/已 staged 的 **E409** 时改为 exit 0(幂等);GitHub Release + 资产仍成功。
|
|
24
|
+
- **DRY**:`scripts/ci_skill_gates.sh` 为 CI/Release 共用门禁;Release 对齐 CI 的 fixture + `smoke_pptx`;CI concurrency cancel-in-progress;npm cache;python-pptx 仅 PPTX 步骤安装。
|
|
25
|
+
- 文档:新增 `docs/ci.md`;版本对齐 **0.1.6**。
|
|
26
|
+
|
|
27
|
+
### 红线未动
|
|
28
|
+
反截断、图表多样性地板、Mode A craft、runtime SHA 同版本门禁保持。
|
|
29
|
+
|
|
1
30
|
## 0.1.5 — 2026-09-25
|
|
2
31
|
|
|
3
32
|
### top-ppt-html · defect close-out
|
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
| 技能 | 版本 | 做什么 |
|
|
10
10
|
|------|------|--------|
|
|
11
|
-
| [`top-ppt-html`](./top-ppt-html/) | 0.1.
|
|
11
|
+
| [`top-ppt-html`](./top-ppt-html/) | 0.1.7 | 优雅大气的正式场合演讲/汇报演示文稿:单文件 HTML + 版式保真可编辑 16:9 PPTX |
|
|
12
12
|
|
|
13
|
-
安装器包 [`@topmindspace/tms-skills`](https://www.npmjs.com/package/@topmindspace/tms-skills) 亦为 **0.1.
|
|
13
|
+
安装器包 [`@topmindspace/tms-skills`](https://www.npmjs.com/package/@topmindspace/tms-skills) 亦为 **0.1.7**(整仓发版:安装器与当前技能同 tag;未来多技能仍可各自演进,默认随仓库 release)。
|
|
14
14
|
|
|
15
15
|
更多技能以仓库根目录下的技能目录加入(每个目录含 `SKILL.md`)。
|
|
16
16
|
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
npx @topmindspace/tms-skills list
|
|
31
31
|
npx @topmindspace/tms-skills install top-ppt-html
|
|
32
32
|
npx @topmindspace/tms-skills install top-ppt-html --to ./.claude/skills
|
|
33
|
-
npx @topmindspace/tms-skills@0.1.
|
|
33
|
+
npx @topmindspace/tms-skills@0.1.7 install top-ppt-html # 钉版本
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
### GitHub 直装
|
|
@@ -75,7 +75,7 @@ npm install
|
|
|
75
75
|
tms-skills/
|
|
76
76
|
├─ top-ppt-html/ # 技能(SKILL.md + assets + references + scripts + evals)
|
|
77
77
|
├─ bin/tms-skills.js # CLI:list / install
|
|
78
|
-
├─ docs/ # 发布规范
|
|
78
|
+
├─ docs/ # 发布规范 · CI/Release 说明(ci.md)
|
|
79
79
|
├─ scripts/ # 仓库级工具(隐私扫描)
|
|
80
80
|
├─ .github/workflows/ # CI + Release
|
|
81
81
|
├─ package.json # @topmindspace/tms-skills
|
package/package.json
CHANGED
package/top-ppt-html/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
**让 idea 飞,好想法被看见。** 面向**演讲与正式场合**的高品质演示文稿技能:把主张、调研、分析、架构方案,生成为**优雅、美观、大气**的可翻页 HTML + **版式保真可编辑 PPTX**。核心工艺是版式、排版、色彩与内容组织——克制图标/图表配文,而非 gadget 堆砌或咨询 dump 默认。
|
|
4
4
|
|
|
5
5
|
- 技能标识:`top-ppt-html`(安装/打包目录名与此一致);品牌名:**TopPPT HTML**
|
|
6
|
-
- 版本:**v0.1.
|
|
6
|
+
- 版本:**v0.1.7**(与 `@topmindspace/tms-skills@0.1.7` 同 tag)
|
|
7
7
|
- **智能体入口**:`SKILL.md` → `references/playbook.md`(L1)→ L2 按需
|
|
8
8
|
- **人类维护者**:本 README(安装 / 命令 / 目录);勿把本文件当生成规范
|
|
9
9
|
|
package/top-ppt-html/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Use when 用户要做报告、演示、汇报、PPT、slides、dec
|
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: "Python 3 stdlib for HTML generation; Node >=18 + pptxgenjs for PPTX; optional playwright for browser regression / theme captures."
|
|
6
6
|
metadata:
|
|
7
|
-
version: "0.1.
|
|
7
|
+
version: "0.1.7"
|
|
8
8
|
author: TopMindspace
|
|
9
9
|
---
|
|
10
10
|
# TopPPT HTML
|
|
@@ -25,17 +25,17 @@ metadata:
|
|
|
25
25
|
|
|
26
26
|
## Fast Mode · 快速模式(用户显式 opt-in)
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
触发词即进入(**跳过 Gate 0 与六项**):`快速模式` / `fast` / `直接生成` / `一键出稿` / `fast mode` / `少问一句`。
|
|
29
29
|
|
|
30
30
|
| 参数 | 默认 | 推断 |
|
|
31
31
|
|------|------|------|
|
|
32
|
-
| mode | **B** | **路演/汇报/发布/演讲/demo
|
|
32
|
+
| mode | **B** | **路演/汇报/发布/演讲/demo/融资/宣讲→A**;架构/拓扑→C;未点明→B |
|
|
33
33
|
| style | B→mckinsey · A→business-blue · C→graphite-dark | |
|
|
34
34
|
| theme | light(graphite→dark) | |
|
|
35
35
|
| 篇幅 | A=10 / B=12 / C=6 | |
|
|
36
|
-
| format | **html only
|
|
36
|
+
| format | **html only**(**仅当用户要 PPT/PPTX 才开** B 通道) | |
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
**跳过** Gate 0/六项/完整大纲。**仍须** 最小大纲→模型单写→strict 0/0→`quality_gate --deliver`(反截断、图表多样性、Mode A craft 不降)。读面 L0+L1(A/Fast 可加 L1.5);代码一律 `extract_snippet`。一行宣布「Fast 选用:…」→ playbook §二轻量链;声明已跳过参考图。**标准模式**仍强制 Gate 0 + 六项。
|
|
39
39
|
|
|
40
40
|
## 唯一入口流程
|
|
41
41
|
|
|
@@ -121,7 +121,7 @@ metadata:
|
|
|
121
121
|
|
|
122
122
|
## 版本口径
|
|
123
123
|
|
|
124
|
-
包 semver(现 0.1.
|
|
124
|
+
包 semver(现 0.1.7)≠ 布局 schema 线(layout-constants / model-schema 的 version,现 `0.1`);patch 不抬 schema。
|
|
125
125
|
|
|
126
126
|
## 环境依赖
|
|
127
127
|
|
|
@@ -1976,7 +1976,7 @@ window.REPORT_MODEL = {
|
|
|
1976
1976
|
|
|
1977
1977
|
/* __TOPPPT_RUNTIME_START__ */
|
|
1978
1978
|
/* ══ PPTX 导出运行时(assets/pptx-export.js 的内联副本 · 由 scripts/sync_runtime.py 注入,禁止手改) ══ */
|
|
1979
|
-
/* __TOPPPT_RUNTIME_SHA__:
|
|
1979
|
+
/* __TOPPPT_RUNTIME_SHA__:77ca318e7a510e01 */
|
|
1980
1980
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
1981
1981
|
* TopPPT HTML· 浏览器端 PPTX 预览运行时
|
|
1982
1982
|
* 零依赖、随报告单文件分发;OOXML 序列化器(仅供预览,页面不导出文件)。
|
|
@@ -2231,6 +2231,7 @@ function dataColors(style, theme) {
|
|
|
2231
2231
|
var LAY = PT.layout || {};
|
|
2232
2232
|
var CONTENT_TOP = (LAY.contentTop != null) ? LAY.contentTop : 2.5;
|
|
2233
2233
|
var CONTENT_BOTTOM = (LAY.contentBottom != null) ? LAY.contentBottom : 6.9;
|
|
2234
|
+
var CONTENT_BOTTOM_NOTE = (LAY.contentBottomWithNote != null) ? LAY.contentBottomWithNote : 6.4;
|
|
2234
2235
|
|
|
2235
2236
|
/* ── 自适应排版工具(与 build_pptx.js 同算法,保证两通道字号/换行一致) ── */
|
|
2236
2237
|
function estLines(text, widthIn, fontPt) {
|
|
@@ -2305,9 +2306,12 @@ function fitRowH(avail, n, pref, floor) {
|
|
|
2305
2306
|
return Math.max(lo, Math.min(pref, avail / Math.max(1, n)));
|
|
2306
2307
|
}
|
|
2307
2308
|
function chartBottom(hasSoWhat, hasFootnote) {
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2309
|
+
/* D8: tightest of soWhat/footnote/contentBottomWithNote — no short-circuit */
|
|
2310
|
+
var bot = CONTENT_BOTTOM;
|
|
2311
|
+
if (hasSoWhat) bot = Math.min(bot, PT.exhibit.soWhatY - 0.12);
|
|
2312
|
+
if (hasFootnote) bot = Math.min(bot, PT.exhibit.footnoteY - 0.12);
|
|
2313
|
+
if (hasSoWhat || hasFootnote) bot = Math.min(bot, CONTENT_BOTTOM_NOTE);
|
|
2314
|
+
return bot;
|
|
2311
2315
|
}
|
|
2312
2316
|
|
|
2313
2317
|
/* ── XML 工具 ── */
|
|
@@ -3195,7 +3199,7 @@ function slidesOf(model, S) {
|
|
|
3195
3199
|
head(ag, 'AGENDA', '报告大纲');
|
|
3196
3200
|
var colN = items.length > 8 ? 2 : 1;
|
|
3197
3201
|
var perCol = Math.ceil(items.length / colN) || 1;
|
|
3198
|
-
var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP) / perCol);
|
|
3202
|
+
var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP - 0.25) / Math.max(1, perCol));
|
|
3199
3203
|
var numSz = rowH >= 0.85 ? 30 : (rowH >= 0.65 ? 22 : 18);
|
|
3200
3204
|
var tSz = rowH >= 0.85 ? 16 : 14, dSz = rowH >= 0.85 ? 12 : 11;
|
|
3201
3205
|
items.forEach(function (it, i) {
|
|
@@ -3232,6 +3236,7 @@ function slidesOf(model, S) {
|
|
|
3232
3236
|
flagY = CONTENT_BOTTOM - flagH;
|
|
3233
3237
|
if (sec.footnote) flagY = Math.min(flagY, PT.exhibit.footnoteY - flagH - 0.06);
|
|
3234
3238
|
if (sec.soWhat) flagY = Math.min(flagY, PT.exhibit.soWhatY - flagH - 0.06);
|
|
3239
|
+
if (sec.footnote || sec.soWhat) flagY = Math.min(flagY, CONTENT_BOTTOM_NOTE - flagH);
|
|
3235
3240
|
bodyBottom = Math.min(bodyBottom, flagY - (_F.gap || 0.12));
|
|
3236
3241
|
}
|
|
3237
3242
|
|
|
@@ -2375,7 +2375,7 @@ window.REPORT_MODEL = {
|
|
|
2375
2375
|
|
|
2376
2376
|
/* __TOPPPT_RUNTIME_START__ */
|
|
2377
2377
|
/* ══ PPTX 导出运行时(assets/pptx-export.js 的内联副本 · 由 scripts/sync_runtime.py 注入,禁止手改) ══ */
|
|
2378
|
-
/* __TOPPPT_RUNTIME_SHA__:
|
|
2378
|
+
/* __TOPPPT_RUNTIME_SHA__:77ca318e7a510e01 */
|
|
2379
2379
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
2380
2380
|
* TopPPT HTML· 浏览器端 PPTX 预览运行时
|
|
2381
2381
|
* 零依赖、随报告单文件分发;OOXML 序列化器(仅供预览,页面不导出文件)。
|
|
@@ -2630,6 +2630,7 @@ function dataColors(style, theme) {
|
|
|
2630
2630
|
var LAY = PT.layout || {};
|
|
2631
2631
|
var CONTENT_TOP = (LAY.contentTop != null) ? LAY.contentTop : 2.5;
|
|
2632
2632
|
var CONTENT_BOTTOM = (LAY.contentBottom != null) ? LAY.contentBottom : 6.9;
|
|
2633
|
+
var CONTENT_BOTTOM_NOTE = (LAY.contentBottomWithNote != null) ? LAY.contentBottomWithNote : 6.4;
|
|
2633
2634
|
|
|
2634
2635
|
/* ── 自适应排版工具(与 build_pptx.js 同算法,保证两通道字号/换行一致) ── */
|
|
2635
2636
|
function estLines(text, widthIn, fontPt) {
|
|
@@ -2704,9 +2705,12 @@ function fitRowH(avail, n, pref, floor) {
|
|
|
2704
2705
|
return Math.max(lo, Math.min(pref, avail / Math.max(1, n)));
|
|
2705
2706
|
}
|
|
2706
2707
|
function chartBottom(hasSoWhat, hasFootnote) {
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2708
|
+
/* D8: tightest of soWhat/footnote/contentBottomWithNote — no short-circuit */
|
|
2709
|
+
var bot = CONTENT_BOTTOM;
|
|
2710
|
+
if (hasSoWhat) bot = Math.min(bot, PT.exhibit.soWhatY - 0.12);
|
|
2711
|
+
if (hasFootnote) bot = Math.min(bot, PT.exhibit.footnoteY - 0.12);
|
|
2712
|
+
if (hasSoWhat || hasFootnote) bot = Math.min(bot, CONTENT_BOTTOM_NOTE);
|
|
2713
|
+
return bot;
|
|
2710
2714
|
}
|
|
2711
2715
|
|
|
2712
2716
|
/* ── XML 工具 ── */
|
|
@@ -3594,7 +3598,7 @@ function slidesOf(model, S) {
|
|
|
3594
3598
|
head(ag, 'AGENDA', '报告大纲');
|
|
3595
3599
|
var colN = items.length > 8 ? 2 : 1;
|
|
3596
3600
|
var perCol = Math.ceil(items.length / colN) || 1;
|
|
3597
|
-
var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP) / perCol);
|
|
3601
|
+
var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP - 0.25) / Math.max(1, perCol));
|
|
3598
3602
|
var numSz = rowH >= 0.85 ? 30 : (rowH >= 0.65 ? 22 : 18);
|
|
3599
3603
|
var tSz = rowH >= 0.85 ? 16 : 14, dSz = rowH >= 0.85 ? 12 : 11;
|
|
3600
3604
|
items.forEach(function (it, i) {
|
|
@@ -3631,6 +3635,7 @@ function slidesOf(model, S) {
|
|
|
3631
3635
|
flagY = CONTENT_BOTTOM - flagH;
|
|
3632
3636
|
if (sec.footnote) flagY = Math.min(flagY, PT.exhibit.footnoteY - flagH - 0.06);
|
|
3633
3637
|
if (sec.soWhat) flagY = Math.min(flagY, PT.exhibit.soWhatY - flagH - 0.06);
|
|
3638
|
+
if (sec.footnote || sec.soWhat) flagY = Math.min(flagY, CONTENT_BOTTOM_NOTE - flagH);
|
|
3634
3639
|
bodyBottom = Math.min(bodyBottom, flagY - (_F.gap || 0.12));
|
|
3635
3640
|
}
|
|
3636
3641
|
|
|
@@ -3577,7 +3577,7 @@ window.REPORT_MODEL = {
|
|
|
3577
3577
|
|
|
3578
3578
|
/* __TOPPPT_RUNTIME_START__ */
|
|
3579
3579
|
/* ══ PPTX 导出运行时(assets/pptx-export.js 的内联副本 · 由 scripts/sync_runtime.py 注入,禁止手改) ══ */
|
|
3580
|
-
/* __TOPPPT_RUNTIME_SHA__:
|
|
3580
|
+
/* __TOPPPT_RUNTIME_SHA__:77ca318e7a510e01 */
|
|
3581
3581
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
3582
3582
|
* TopPPT HTML· 浏览器端 PPTX 预览运行时
|
|
3583
3583
|
* 零依赖、随报告单文件分发;OOXML 序列化器(仅供预览,页面不导出文件)。
|
|
@@ -3832,6 +3832,7 @@ function dataColors(style, theme) {
|
|
|
3832
3832
|
var LAY = PT.layout || {};
|
|
3833
3833
|
var CONTENT_TOP = (LAY.contentTop != null) ? LAY.contentTop : 2.5;
|
|
3834
3834
|
var CONTENT_BOTTOM = (LAY.contentBottom != null) ? LAY.contentBottom : 6.9;
|
|
3835
|
+
var CONTENT_BOTTOM_NOTE = (LAY.contentBottomWithNote != null) ? LAY.contentBottomWithNote : 6.4;
|
|
3835
3836
|
|
|
3836
3837
|
/* ── 自适应排版工具(与 build_pptx.js 同算法,保证两通道字号/换行一致) ── */
|
|
3837
3838
|
function estLines(text, widthIn, fontPt) {
|
|
@@ -3906,9 +3907,12 @@ function fitRowH(avail, n, pref, floor) {
|
|
|
3906
3907
|
return Math.max(lo, Math.min(pref, avail / Math.max(1, n)));
|
|
3907
3908
|
}
|
|
3908
3909
|
function chartBottom(hasSoWhat, hasFootnote) {
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3910
|
+
/* D8: tightest of soWhat/footnote/contentBottomWithNote — no short-circuit */
|
|
3911
|
+
var bot = CONTENT_BOTTOM;
|
|
3912
|
+
if (hasSoWhat) bot = Math.min(bot, PT.exhibit.soWhatY - 0.12);
|
|
3913
|
+
if (hasFootnote) bot = Math.min(bot, PT.exhibit.footnoteY - 0.12);
|
|
3914
|
+
if (hasSoWhat || hasFootnote) bot = Math.min(bot, CONTENT_BOTTOM_NOTE);
|
|
3915
|
+
return bot;
|
|
3912
3916
|
}
|
|
3913
3917
|
|
|
3914
3918
|
/* ── XML 工具 ── */
|
|
@@ -4796,7 +4800,7 @@ function slidesOf(model, S) {
|
|
|
4796
4800
|
head(ag, 'AGENDA', '报告大纲');
|
|
4797
4801
|
var colN = items.length > 8 ? 2 : 1;
|
|
4798
4802
|
var perCol = Math.ceil(items.length / colN) || 1;
|
|
4799
|
-
var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP) / perCol);
|
|
4803
|
+
var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP - 0.25) / Math.max(1, perCol));
|
|
4800
4804
|
var numSz = rowH >= 0.85 ? 30 : (rowH >= 0.65 ? 22 : 18);
|
|
4801
4805
|
var tSz = rowH >= 0.85 ? 16 : 14, dSz = rowH >= 0.85 ? 12 : 11;
|
|
4802
4806
|
items.forEach(function (it, i) {
|
|
@@ -4833,6 +4837,7 @@ function slidesOf(model, S) {
|
|
|
4833
4837
|
flagY = CONTENT_BOTTOM - flagH;
|
|
4834
4838
|
if (sec.footnote) flagY = Math.min(flagY, PT.exhibit.footnoteY - flagH - 0.06);
|
|
4835
4839
|
if (sec.soWhat) flagY = Math.min(flagY, PT.exhibit.soWhatY - flagH - 0.06);
|
|
4840
|
+
if (sec.footnote || sec.soWhat) flagY = Math.min(flagY, CONTENT_BOTTOM_NOTE - flagH);
|
|
4836
4841
|
bodyBottom = Math.min(bodyBottom, flagY - (_F.gap || 0.12));
|
|
4837
4842
|
}
|
|
4838
4843
|
|
|
@@ -252,6 +252,7 @@ function dataColors(style, theme) {
|
|
|
252
252
|
var LAY = PT.layout || {};
|
|
253
253
|
var CONTENT_TOP = (LAY.contentTop != null) ? LAY.contentTop : 2.5;
|
|
254
254
|
var CONTENT_BOTTOM = (LAY.contentBottom != null) ? LAY.contentBottom : 6.9;
|
|
255
|
+
var CONTENT_BOTTOM_NOTE = (LAY.contentBottomWithNote != null) ? LAY.contentBottomWithNote : 6.4;
|
|
255
256
|
|
|
256
257
|
/* ── 自适应排版工具(与 build_pptx.js 同算法,保证两通道字号/换行一致) ── */
|
|
257
258
|
function estLines(text, widthIn, fontPt) {
|
|
@@ -326,9 +327,12 @@ function fitRowH(avail, n, pref, floor) {
|
|
|
326
327
|
return Math.max(lo, Math.min(pref, avail / Math.max(1, n)));
|
|
327
328
|
}
|
|
328
329
|
function chartBottom(hasSoWhat, hasFootnote) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
/* D8: tightest of soWhat/footnote/contentBottomWithNote — no short-circuit */
|
|
331
|
+
var bot = CONTENT_BOTTOM;
|
|
332
|
+
if (hasSoWhat) bot = Math.min(bot, PT.exhibit.soWhatY - 0.12);
|
|
333
|
+
if (hasFootnote) bot = Math.min(bot, PT.exhibit.footnoteY - 0.12);
|
|
334
|
+
if (hasSoWhat || hasFootnote) bot = Math.min(bot, CONTENT_BOTTOM_NOTE);
|
|
335
|
+
return bot;
|
|
332
336
|
}
|
|
333
337
|
|
|
334
338
|
/* ── XML 工具 ── */
|
|
@@ -1216,7 +1220,7 @@ function slidesOf(model, S) {
|
|
|
1216
1220
|
head(ag, 'AGENDA', '报告大纲');
|
|
1217
1221
|
var colN = items.length > 8 ? 2 : 1;
|
|
1218
1222
|
var perCol = Math.ceil(items.length / colN) || 1;
|
|
1219
|
-
var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP) / perCol);
|
|
1223
|
+
var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP - 0.25) / Math.max(1, perCol));
|
|
1220
1224
|
var numSz = rowH >= 0.85 ? 30 : (rowH >= 0.65 ? 22 : 18);
|
|
1221
1225
|
var tSz = rowH >= 0.85 ? 16 : 14, dSz = rowH >= 0.85 ? 12 : 11;
|
|
1222
1226
|
items.forEach(function (it, i) {
|
|
@@ -1253,6 +1257,7 @@ function slidesOf(model, S) {
|
|
|
1253
1257
|
flagY = CONTENT_BOTTOM - flagH;
|
|
1254
1258
|
if (sec.footnote) flagY = Math.min(flagY, PT.exhibit.footnoteY - flagH - 0.06);
|
|
1255
1259
|
if (sec.soWhat) flagY = Math.min(flagY, PT.exhibit.soWhatY - flagH - 0.06);
|
|
1260
|
+
if (sec.footnote || sec.soWhat) flagY = Math.min(flagY, CONTENT_BOTTOM_NOTE - flagH);
|
|
1256
1261
|
bodyBottom = Math.min(bodyBottom, flagY - (_F.gap || 0.12));
|
|
1257
1262
|
}
|
|
1258
1263
|
|
|
@@ -1778,7 +1778,7 @@ window.REPORT_MODEL = {
|
|
|
1778
1778
|
|
|
1779
1779
|
/* __TOPPPT_RUNTIME_START__ */
|
|
1780
1780
|
/* ══ PPTX 导出运行时(assets/pptx-export.js 的内联副本 · 由 scripts/sync_runtime.py 注入,禁止手改) ══ */
|
|
1781
|
-
/* __TOPPPT_RUNTIME_SHA__:
|
|
1781
|
+
/* __TOPPPT_RUNTIME_SHA__:77ca318e7a510e01 */
|
|
1782
1782
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
1783
1783
|
* TopPPT HTML· 浏览器端 PPTX 预览运行时
|
|
1784
1784
|
* 零依赖、随报告单文件分发;OOXML 序列化器(仅供预览,页面不导出文件)。
|
|
@@ -2033,6 +2033,7 @@ function dataColors(style, theme) {
|
|
|
2033
2033
|
var LAY = PT.layout || {};
|
|
2034
2034
|
var CONTENT_TOP = (LAY.contentTop != null) ? LAY.contentTop : 2.5;
|
|
2035
2035
|
var CONTENT_BOTTOM = (LAY.contentBottom != null) ? LAY.contentBottom : 6.9;
|
|
2036
|
+
var CONTENT_BOTTOM_NOTE = (LAY.contentBottomWithNote != null) ? LAY.contentBottomWithNote : 6.4;
|
|
2036
2037
|
|
|
2037
2038
|
/* ── 自适应排版工具(与 build_pptx.js 同算法,保证两通道字号/换行一致) ── */
|
|
2038
2039
|
function estLines(text, widthIn, fontPt) {
|
|
@@ -2107,9 +2108,12 @@ function fitRowH(avail, n, pref, floor) {
|
|
|
2107
2108
|
return Math.max(lo, Math.min(pref, avail / Math.max(1, n)));
|
|
2108
2109
|
}
|
|
2109
2110
|
function chartBottom(hasSoWhat, hasFootnote) {
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2111
|
+
/* D8: tightest of soWhat/footnote/contentBottomWithNote — no short-circuit */
|
|
2112
|
+
var bot = CONTENT_BOTTOM;
|
|
2113
|
+
if (hasSoWhat) bot = Math.min(bot, PT.exhibit.soWhatY - 0.12);
|
|
2114
|
+
if (hasFootnote) bot = Math.min(bot, PT.exhibit.footnoteY - 0.12);
|
|
2115
|
+
if (hasSoWhat || hasFootnote) bot = Math.min(bot, CONTENT_BOTTOM_NOTE);
|
|
2116
|
+
return bot;
|
|
2113
2117
|
}
|
|
2114
2118
|
|
|
2115
2119
|
/* ── XML 工具 ── */
|
|
@@ -2997,7 +3001,7 @@ function slidesOf(model, S) {
|
|
|
2997
3001
|
head(ag, 'AGENDA', '报告大纲');
|
|
2998
3002
|
var colN = items.length > 8 ? 2 : 1;
|
|
2999
3003
|
var perCol = Math.ceil(items.length / colN) || 1;
|
|
3000
|
-
var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP) / perCol);
|
|
3004
|
+
var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP - 0.25) / Math.max(1, perCol));
|
|
3001
3005
|
var numSz = rowH >= 0.85 ? 30 : (rowH >= 0.65 ? 22 : 18);
|
|
3002
3006
|
var tSz = rowH >= 0.85 ? 16 : 14, dSz = rowH >= 0.85 ? 12 : 11;
|
|
3003
3007
|
items.forEach(function (it, i) {
|
|
@@ -3034,6 +3038,7 @@ function slidesOf(model, S) {
|
|
|
3034
3038
|
flagY = CONTENT_BOTTOM - flagH;
|
|
3035
3039
|
if (sec.footnote) flagY = Math.min(flagY, PT.exhibit.footnoteY - flagH - 0.06);
|
|
3036
3040
|
if (sec.soWhat) flagY = Math.min(flagY, PT.exhibit.soWhatY - flagH - 0.06);
|
|
3041
|
+
if (sec.footnote || sec.soWhat) flagY = Math.min(flagY, CONTENT_BOTTOM_NOTE - flagH);
|
|
3037
3042
|
bodyBottom = Math.min(bodyBottom, flagY - (_F.gap || 0.12));
|
|
3038
3043
|
}
|
|
3039
3044
|
|
|
@@ -1788,7 +1788,7 @@ window.REPORT_MODEL = {
|
|
|
1788
1788
|
|
|
1789
1789
|
/* __TOPPPT_RUNTIME_START__ */
|
|
1790
1790
|
/* ══ PPTX 导出运行时(assets/pptx-export.js 的内联副本 · 由 scripts/sync_runtime.py 注入,禁止手改) ══ */
|
|
1791
|
-
/* __TOPPPT_RUNTIME_SHA__:
|
|
1791
|
+
/* __TOPPPT_RUNTIME_SHA__:77ca318e7a510e01 */
|
|
1792
1792
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
1793
1793
|
* TopPPT HTML· 浏览器端 PPTX 预览运行时
|
|
1794
1794
|
* 零依赖、随报告单文件分发;OOXML 序列化器(仅供预览,页面不导出文件)。
|
|
@@ -2043,6 +2043,7 @@ function dataColors(style, theme) {
|
|
|
2043
2043
|
var LAY = PT.layout || {};
|
|
2044
2044
|
var CONTENT_TOP = (LAY.contentTop != null) ? LAY.contentTop : 2.5;
|
|
2045
2045
|
var CONTENT_BOTTOM = (LAY.contentBottom != null) ? LAY.contentBottom : 6.9;
|
|
2046
|
+
var CONTENT_BOTTOM_NOTE = (LAY.contentBottomWithNote != null) ? LAY.contentBottomWithNote : 6.4;
|
|
2046
2047
|
|
|
2047
2048
|
/* ── 自适应排版工具(与 build_pptx.js 同算法,保证两通道字号/换行一致) ── */
|
|
2048
2049
|
function estLines(text, widthIn, fontPt) {
|
|
@@ -2117,9 +2118,12 @@ function fitRowH(avail, n, pref, floor) {
|
|
|
2117
2118
|
return Math.max(lo, Math.min(pref, avail / Math.max(1, n)));
|
|
2118
2119
|
}
|
|
2119
2120
|
function chartBottom(hasSoWhat, hasFootnote) {
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2121
|
+
/* D8: tightest of soWhat/footnote/contentBottomWithNote — no short-circuit */
|
|
2122
|
+
var bot = CONTENT_BOTTOM;
|
|
2123
|
+
if (hasSoWhat) bot = Math.min(bot, PT.exhibit.soWhatY - 0.12);
|
|
2124
|
+
if (hasFootnote) bot = Math.min(bot, PT.exhibit.footnoteY - 0.12);
|
|
2125
|
+
if (hasSoWhat || hasFootnote) bot = Math.min(bot, CONTENT_BOTTOM_NOTE);
|
|
2126
|
+
return bot;
|
|
2123
2127
|
}
|
|
2124
2128
|
|
|
2125
2129
|
/* ── XML 工具 ── */
|
|
@@ -3007,7 +3011,7 @@ function slidesOf(model, S) {
|
|
|
3007
3011
|
head(ag, 'AGENDA', '报告大纲');
|
|
3008
3012
|
var colN = items.length > 8 ? 2 : 1;
|
|
3009
3013
|
var perCol = Math.ceil(items.length / colN) || 1;
|
|
3010
|
-
var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP) / perCol);
|
|
3014
|
+
var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP - 0.25) / Math.max(1, perCol));
|
|
3011
3015
|
var numSz = rowH >= 0.85 ? 30 : (rowH >= 0.65 ? 22 : 18);
|
|
3012
3016
|
var tSz = rowH >= 0.85 ? 16 : 14, dSz = rowH >= 0.85 ? 12 : 11;
|
|
3013
3017
|
items.forEach(function (it, i) {
|
|
@@ -3044,6 +3048,7 @@ function slidesOf(model, S) {
|
|
|
3044
3048
|
flagY = CONTENT_BOTTOM - flagH;
|
|
3045
3049
|
if (sec.footnote) flagY = Math.min(flagY, PT.exhibit.footnoteY - flagH - 0.06);
|
|
3046
3050
|
if (sec.soWhat) flagY = Math.min(flagY, PT.exhibit.soWhatY - flagH - 0.06);
|
|
3051
|
+
if (sec.footnote || sec.soWhat) flagY = Math.min(flagY, CONTENT_BOTTOM_NOTE - flagH);
|
|
3047
3052
|
bodyBottom = Math.min(bodyBottom, flagY - (_F.gap || 0.12));
|
|
3048
3053
|
}
|
|
3049
3054
|
|
|
@@ -2067,7 +2067,7 @@ window.REPORT_MODEL = {
|
|
|
2067
2067
|
|
|
2068
2068
|
/* __TOPPPT_RUNTIME_START__ */
|
|
2069
2069
|
/* ══ PPTX 导出运行时(assets/pptx-export.js 的内联副本 · 由 scripts/sync_runtime.py 注入,禁止手改) ══ */
|
|
2070
|
-
/* __TOPPPT_RUNTIME_SHA__:
|
|
2070
|
+
/* __TOPPPT_RUNTIME_SHA__:77ca318e7a510e01 */
|
|
2071
2071
|
/* ═══════════════════════════════════════════════════════════════════════════
|
|
2072
2072
|
* TopPPT HTML· 浏览器端 PPTX 预览运行时
|
|
2073
2073
|
* 零依赖、随报告单文件分发;OOXML 序列化器(仅供预览,页面不导出文件)。
|
|
@@ -2322,6 +2322,7 @@ function dataColors(style, theme) {
|
|
|
2322
2322
|
var LAY = PT.layout || {};
|
|
2323
2323
|
var CONTENT_TOP = (LAY.contentTop != null) ? LAY.contentTop : 2.5;
|
|
2324
2324
|
var CONTENT_BOTTOM = (LAY.contentBottom != null) ? LAY.contentBottom : 6.9;
|
|
2325
|
+
var CONTENT_BOTTOM_NOTE = (LAY.contentBottomWithNote != null) ? LAY.contentBottomWithNote : 6.4;
|
|
2325
2326
|
|
|
2326
2327
|
/* ── 自适应排版工具(与 build_pptx.js 同算法,保证两通道字号/换行一致) ── */
|
|
2327
2328
|
function estLines(text, widthIn, fontPt) {
|
|
@@ -2396,9 +2397,12 @@ function fitRowH(avail, n, pref, floor) {
|
|
|
2396
2397
|
return Math.max(lo, Math.min(pref, avail / Math.max(1, n)));
|
|
2397
2398
|
}
|
|
2398
2399
|
function chartBottom(hasSoWhat, hasFootnote) {
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2400
|
+
/* D8: tightest of soWhat/footnote/contentBottomWithNote — no short-circuit */
|
|
2401
|
+
var bot = CONTENT_BOTTOM;
|
|
2402
|
+
if (hasSoWhat) bot = Math.min(bot, PT.exhibit.soWhatY - 0.12);
|
|
2403
|
+
if (hasFootnote) bot = Math.min(bot, PT.exhibit.footnoteY - 0.12);
|
|
2404
|
+
if (hasSoWhat || hasFootnote) bot = Math.min(bot, CONTENT_BOTTOM_NOTE);
|
|
2405
|
+
return bot;
|
|
2402
2406
|
}
|
|
2403
2407
|
|
|
2404
2408
|
/* ── XML 工具 ── */
|
|
@@ -3286,7 +3290,7 @@ function slidesOf(model, S) {
|
|
|
3286
3290
|
head(ag, 'AGENDA', '报告大纲');
|
|
3287
3291
|
var colN = items.length > 8 ? 2 : 1;
|
|
3288
3292
|
var perCol = Math.ceil(items.length / colN) || 1;
|
|
3289
|
-
var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP) / perCol);
|
|
3293
|
+
var rowH = Math.min(1.05, (CONTENT_BOTTOM - CONTENT_TOP - 0.25) / Math.max(1, perCol));
|
|
3290
3294
|
var numSz = rowH >= 0.85 ? 30 : (rowH >= 0.65 ? 22 : 18);
|
|
3291
3295
|
var tSz = rowH >= 0.85 ? 16 : 14, dSz = rowH >= 0.85 ? 12 : 11;
|
|
3292
3296
|
items.forEach(function (it, i) {
|
|
@@ -3323,6 +3327,7 @@ function slidesOf(model, S) {
|
|
|
3323
3327
|
flagY = CONTENT_BOTTOM - flagH;
|
|
3324
3328
|
if (sec.footnote) flagY = Math.min(flagY, PT.exhibit.footnoteY - flagH - 0.06);
|
|
3325
3329
|
if (sec.soWhat) flagY = Math.min(flagY, PT.exhibit.soWhatY - flagH - 0.06);
|
|
3330
|
+
if (sec.footnote || sec.soWhat) flagY = Math.min(flagY, CONTENT_BOTTOM_NOTE - flagH);
|
|
3326
3331
|
bodyBottom = Math.min(bodyBottom, flagY - (_F.gap || 0.12));
|
|
3327
3332
|
}
|
|
3328
3333
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "top-ppt-html",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "top-ppt-html",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.7",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"pptxgenjs": "^4.0.0"
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
Gate 0 参考图 → 六项问询(1 轮)→ 最小大纲(outline-design「轻量最小集」)→ 1 张规划卡 → scaffold_report.py
|
|
47
47
|
→ **只填 REPORT_MODEL** → render_from_model.py --inplace → validate_report.py --strict → 交付
|
|
48
48
|
```
|
|
49
|
-
(Fast Mode 跳过 Gate 0
|
|
49
|
+
(Fast Mode 跳过 Gate 0/六项,仍须最小大纲+strict;默认 **HTML-only**,PPTX 显式 opt-in 走 B 通道;路演/汇报/演讲线索→Mode A;**禁止**手改 HTML 正文与模型双写。)
|
|
50
50
|
|
|
51
51
|
**完整路径**(任一命中)——研究 ≥20 页 / 演示 ≥12 页 / 材料量大且杂 / 含未敲定关键判断 / 用户要看框架。
|
|
52
52
|
```
|
|
@@ -573,9 +573,13 @@ function imageLayoutShapes(s, layout, img, items, points, isPh, fit, y0, y1) {
|
|
|
573
573
|
}
|
|
574
574
|
/* 数据图表上下文(供备注与自适应使用) */
|
|
575
575
|
function chartBottom(hasSoWhat, hasFootnote) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
576
|
+
/* 取最紧下界:so-what / footnote / contentBottomWithNote 并存时不得短路(D8)。
|
|
577
|
+
* 与 ~1630 flagY 的 Math.min 让位同口径;withNote 时对齐 regionOf 的 contentBottomWithNote。 */
|
|
578
|
+
let bot = CONTENT_BOTTOM;
|
|
579
|
+
if (hasSoWhat) bot = Math.min(bot, PT.exhibit.soWhatY - 0.12);
|
|
580
|
+
if (hasFootnote) bot = Math.min(bot, PT.exhibit.footnoteY - 0.12);
|
|
581
|
+
if (hasSoWhat || hasFootnote) bot = Math.min(bot, CONTENT_BOTTOM_NOTE);
|
|
582
|
+
return bot;
|
|
579
583
|
}
|
|
580
584
|
|
|
581
585
|
/* ═══ 原生数据图表(pptxgenjs addChart → 真 chart part + 内嵌 Excel 工作簿)═══
|
|
@@ -1581,8 +1585,9 @@ function addTable(s, tbl, x, y, w, availH, opts) {
|
|
|
1581
1585
|
const items = CONTENT.agenda;
|
|
1582
1586
|
const colN = items.length > 8 ? 2 : 1;
|
|
1583
1587
|
const perCol = Math.ceil(items.length / colN);
|
|
1584
|
-
|
|
1585
|
-
const
|
|
1588
|
+
/* 预留 0.25in,避免末行底边落入 severe-overlap 安全网(无注释页亦勿压进 withNote 带过深) */
|
|
1589
|
+
const availH = CONTENT_BOTTOM - CONTENT_TOP - 0.25;
|
|
1590
|
+
const rowH = Math.min(1.05, availH / Math.max(1, perCol));
|
|
1586
1591
|
const numSize = rowH >= 0.85 ? sz(30) : (rowH >= 0.65 ? sz(22) : sz(18));
|
|
1587
1592
|
const tSize = rowH >= 0.85 ? sz(16) : sz(14);
|
|
1588
1593
|
const dSize = rowH >= 0.85 ? sz(12) : sz(11);
|
|
@@ -1630,6 +1635,8 @@ CONTENT.sections.forEach((sec) => {
|
|
|
1630
1635
|
flagY = CONTENT_BOTTOM - flagH;
|
|
1631
1636
|
if (sec.footnote) flagY = Math.min(flagY, PT.exhibit.footnoteY - flagH - 0.06);
|
|
1632
1637
|
if (sec.soWhat) flagY = Math.min(flagY, PT.exhibit.soWhatY - flagH - 0.06);
|
|
1638
|
+
/* withNote 时 flag 底不得越过 contentBottomWithNote(与 chartBottom / regionOf 同口径) */
|
|
1639
|
+
if (sec.footnote || sec.soWhat) flagY = Math.min(flagY, CONTENT_BOTTOM_NOTE - flagH);
|
|
1633
1640
|
bodyBottom = Math.min(bodyBottom, flagY - (_F.gap || 0.12));
|
|
1634
1641
|
}
|
|
1635
1642
|
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
# TopPPT HTML · PPTX 轻量冒烟(Batch 2)
|
|
3
3
|
# extract_model → build_pptx → validate_pptx --strict --model=
|
|
4
4
|
# 用法: bash scripts/smoke_pptx.sh [example.html]
|
|
5
|
+
# CI(ci_skill_gates --with-pptx)必跑 business-blue + research-mckinsey(D13;后者覆盖 soWhat+footnote / D8)
|
|
6
|
+
# 失败时打印短摘要(错误码/页码/消息),完整 JSON 落盘到 TMPDIR。
|
|
5
7
|
set -euo pipefail
|
|
6
8
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
7
9
|
cd "$ROOT"
|
|
@@ -15,6 +17,7 @@ OUT_DIR="${TMPDIR:-/tmp}/top-ppt-html-smoke"
|
|
|
15
17
|
mkdir -p "$OUT_DIR"
|
|
16
18
|
MODEL="$OUT_DIR/${STEM}.model.json"
|
|
17
19
|
PPTX="$OUT_DIR/${STEM}.pptx"
|
|
20
|
+
REPORT="$OUT_DIR/${STEM}.validate.json"
|
|
18
21
|
|
|
19
22
|
echo "== smoke_pptx =="
|
|
20
23
|
echo "HTML $HTML"
|
|
@@ -26,5 +29,42 @@ export NODE_PATH="${ROOT}/node_modules${NODE_PATH:+:$NODE_PATH}"
|
|
|
26
29
|
node scripts/build_pptx.js "$PPTX" --model="$MODEL"
|
|
27
30
|
echo "PPTX $PPTX ($(wc -c < "$PPTX") bytes)"
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
set +e
|
|
33
|
+
python3 scripts/validate_pptx.py "$PPTX" --strict --model="$MODEL" \
|
|
34
|
+
--json-out="$REPORT" >"$OUT_DIR/${STEM}.validate.stdout.json"
|
|
35
|
+
rc=$?
|
|
36
|
+
set -e
|
|
37
|
+
|
|
38
|
+
if [[ "$rc" -ne 0 ]]; then
|
|
39
|
+
echo "smoke_pptx: VALIDATE FAIL (exit $rc)" >&2
|
|
40
|
+
python3 - "$REPORT" "$rc" <<'PY'
|
|
41
|
+
import json, sys
|
|
42
|
+
path, rc = sys.argv[1], sys.argv[2]
|
|
43
|
+
try:
|
|
44
|
+
d = json.loads(open(path, encoding="utf-8").read())
|
|
45
|
+
except Exception as e:
|
|
46
|
+
print(f" (could not read report {path}: {e})", file=sys.stderr)
|
|
47
|
+
sys.exit(int(rc))
|
|
48
|
+
errs = d.get("errors") or []
|
|
49
|
+
warns = d.get("warnings") or []
|
|
50
|
+
print(f" summary: errors={len(errs)} warnings={len(warns)} slides={d.get('summary',{}).get('slide_count')}", file=sys.stderr)
|
|
51
|
+
seen = set()
|
|
52
|
+
for e in errs + warns:
|
|
53
|
+
key = (e.get("code"), e.get("slide"), e.get("message"))
|
|
54
|
+
if key in seen:
|
|
55
|
+
continue
|
|
56
|
+
seen.add(key)
|
|
57
|
+
slide = e.get("slide")
|
|
58
|
+
slide_s = f"slide={slide} " if slide is not None else ""
|
|
59
|
+
msg = (e.get("message") or "")[:160]
|
|
60
|
+
print(f" [{e.get('code')}] {slide_s}{msg}", file=sys.stderr)
|
|
61
|
+
if len(seen) >= 40:
|
|
62
|
+
print(f" … truncated ({len(errs)+len(warns)} total findings)", file=sys.stderr)
|
|
63
|
+
break
|
|
64
|
+
print(f" full JSON: {path}", file=sys.stderr)
|
|
65
|
+
sys.exit(int(rc))
|
|
66
|
+
PY
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
echo "smoke_pptx: OK (0 errors / 0 warnings)"
|
|
70
|
+
exit 0
|
|
@@ -102,6 +102,95 @@ def test_annotation_band_overlap() -> None:
|
|
|
102
102
|
)
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def test_annotation_band_ignores_full_bleed_bg() -> None:
|
|
106
|
+
"""Full-slide background must not false-positive as band crush."""
|
|
107
|
+
xml = (
|
|
108
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
109
|
+
f'<p:sp xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
110
|
+
"<p:spPr><a:xfrm>"
|
|
111
|
+
f'<a:off x="0" y="0"/>'
|
|
112
|
+
f'<a:ext cx="{int(13.333 * EMU)}" cy="{int(7.5 * EMU)}"/>'
|
|
113
|
+
"</a:xfrm></p:spPr></p:sp>"
|
|
114
|
+
)
|
|
115
|
+
el = ET.fromstring(xml)
|
|
116
|
+
issues = V.annotation_band_overlap_check(
|
|
117
|
+
[el], 1, int(7.5 * EMU), int(13.333 * EMU),
|
|
118
|
+
)
|
|
119
|
+
codes = [i["code"] for i in issues]
|
|
120
|
+
ok(
|
|
121
|
+
"3b full-bleed background does not fire ANNOTATION_BAND_OVERLAP",
|
|
122
|
+
"ANNOTATION_BAND_OVERLAP" not in codes,
|
|
123
|
+
f"codes={codes}",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_annotation_band_allows_content_without_note() -> None:
|
|
128
|
+
"""Without so-what/source, content may use up to contentBottom (~6.9)."""
|
|
129
|
+
xml = (
|
|
130
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
131
|
+
f'<p:sp xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
132
|
+
"<p:spPr><a:xfrm>"
|
|
133
|
+
f'<a:off x="{int(0.6 * EMU)}" y="{int(5.5 * EMU)}"/>'
|
|
134
|
+
f'<a:ext cx="{int(4 * EMU)}" cy="{int(1.0 * EMU)}"/>'
|
|
135
|
+
"</a:xfrm></p:spPr>"
|
|
136
|
+
"<p:txBody><a:p><a:r><a:t>AgendaCard</a:t></a:r></a:p></p:txBody>"
|
|
137
|
+
"</p:sp>"
|
|
138
|
+
)
|
|
139
|
+
el = ET.fromstring(xml)
|
|
140
|
+
issues = V.annotation_band_overlap_check([el], 1, int(7.5 * EMU))
|
|
141
|
+
codes = [i["code"] for i in issues]
|
|
142
|
+
ok(
|
|
143
|
+
"3c content to 6.50 without annotation does not fire",
|
|
144
|
+
"ANNOTATION_BAND_OVERLAP" not in codes,
|
|
145
|
+
f"codes={codes}",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_annotation_band_fires_when_note_present() -> None:
|
|
150
|
+
"""With so-what bar present, body overlapping the withNote band must fire."""
|
|
151
|
+
def sp(y, h, w, text):
|
|
152
|
+
return ET.fromstring(
|
|
153
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
154
|
+
f'<p:sp xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
155
|
+
"<p:spPr><a:xfrm>"
|
|
156
|
+
f'<a:off x="{int(0.6 * EMU)}" y="{int(y * EMU)}"/>'
|
|
157
|
+
f'<a:ext cx="{int(w * EMU)}" cy="{int(h * EMU)}"/>'
|
|
158
|
+
"</a:xfrm></p:spPr>"
|
|
159
|
+
f"<p:txBody><a:p><a:r><a:t>{text}</a:t></a:r></a:p></p:txBody>"
|
|
160
|
+
"</p:sp>"
|
|
161
|
+
)
|
|
162
|
+
els = [
|
|
163
|
+
sp(5.5, 1.0, 4.0, "LegendSeries"),
|
|
164
|
+
sp(6.05, 0.55, 12.0, "结论 平台化是唯一路径"),
|
|
165
|
+
]
|
|
166
|
+
issues = V.annotation_band_overlap_check(els, 1, int(7.5 * EMU), int(13.333 * EMU))
|
|
167
|
+
codes = [i["code"] for i in issues]
|
|
168
|
+
ok(
|
|
169
|
+
"3d body crush with so-what present fires ANNOTATION_BAND_OVERLAP",
|
|
170
|
+
"ANNOTATION_BAND_OVERLAP" in codes,
|
|
171
|
+
f"codes={codes}",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_font_size_h2_17_allowed() -> None:
|
|
176
|
+
"""modeTypeScale h2=17 must be on the allowed set (cover/display whitelist)."""
|
|
177
|
+
xml = (
|
|
178
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
179
|
+
f'<p:sld xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
180
|
+
"<p:cSld><p:spTree><p:sp><p:txBody>"
|
|
181
|
+
'<a:p><a:r><a:rPr sz="1700"/><a:t>AgendaSectionTitle</a:t></a:r></a:p>'
|
|
182
|
+
"</p:txBody></p:sp></p:spTree></p:cSld></p:sld>"
|
|
183
|
+
)
|
|
184
|
+
root = ET.fromstring(xml)
|
|
185
|
+
issues = V.font_size_snap_check(root, 1)
|
|
186
|
+
codes = [i["code"] for i in issues]
|
|
187
|
+
ok(
|
|
188
|
+
"4b FONT_SIZE_NOT_SNAPPED allows intentional 17pt h2",
|
|
189
|
+
"FONT_SIZE_NOT_SNAPPED" not in codes,
|
|
190
|
+
f"codes={codes}",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
105
194
|
def test_font_size_not_snapped() -> None:
|
|
106
195
|
xml = (
|
|
107
196
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
@@ -120,6 +209,98 @@ def test_font_size_not_snapped() -> None:
|
|
|
120
209
|
)
|
|
121
210
|
|
|
122
211
|
|
|
212
|
+
def test_annotation_band_reports_all_invaders() -> None:
|
|
213
|
+
"""D9: do not break after the first ANNOTATION_BAND_OVERLAP on a slide."""
|
|
214
|
+
def sp(y, h, w, text):
|
|
215
|
+
return ET.fromstring(
|
|
216
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
217
|
+
f'<p:sp xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
218
|
+
"<p:spPr><a:xfrm>"
|
|
219
|
+
f'<a:off x="{int(0.6 * EMU)}" y="{int(y * EMU)}"/>'
|
|
220
|
+
f'<a:ext cx="{int(w * EMU)}" cy="{int(h * EMU)}"/>'
|
|
221
|
+
"</a:xfrm></p:spPr>"
|
|
222
|
+
f"<p:txBody><a:p><a:r><a:t>{text}</a:t></a:r></a:p></p:txBody>"
|
|
223
|
+
"</p:sp>"
|
|
224
|
+
)
|
|
225
|
+
els = [
|
|
226
|
+
sp(5.0, 1.2, 4.0, "PlotSeriesA"),
|
|
227
|
+
sp(5.2, 1.1, 4.0, "PlotSeriesB"),
|
|
228
|
+
sp(6.05, 0.55, 12.0, "SO WHAT locked path"),
|
|
229
|
+
]
|
|
230
|
+
issues = V.annotation_band_overlap_check(els, 1, int(7.5 * EMU), int(13.333 * EMU))
|
|
231
|
+
codes = [i["code"] for i in issues]
|
|
232
|
+
ok(
|
|
233
|
+
"3e D9 reports all invaders (not just first)",
|
|
234
|
+
codes.count("ANNOTATION_BAND_OVERLAP") >= 2,
|
|
235
|
+
f"codes={codes}",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_annotation_band_so_what_band_top() -> None:
|
|
240
|
+
"""D11: with so-what present, invasions in (6.05, 6.40] must fire."""
|
|
241
|
+
def sp(y, h, w, text):
|
|
242
|
+
return ET.fromstring(
|
|
243
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
244
|
+
f'<p:sp xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
245
|
+
"<p:spPr><a:xfrm>"
|
|
246
|
+
f'<a:off x="{int(0.6 * EMU)}" y="{int(y * EMU)}"/>'
|
|
247
|
+
f'<a:ext cx="{int(w * EMU)}" cy="{int(h * EMU)}"/>'
|
|
248
|
+
"</a:xfrm></p:spPr>"
|
|
249
|
+
f"<p:txBody><a:p><a:r><a:t>{text}</a:t></a:r></a:p></p:txBody>"
|
|
250
|
+
"</p:sp>"
|
|
251
|
+
)
|
|
252
|
+
els = [
|
|
253
|
+
# Body ends at 6.30 — inside (soWhatY=6.05, contentBottomWithNote=6.40]
|
|
254
|
+
sp(5.0, 1.3, 4.0, "LegendCrushMid"),
|
|
255
|
+
sp(6.05, 0.55, 12.0, "SO WHAT platform first"),
|
|
256
|
+
]
|
|
257
|
+
issues = V.annotation_band_overlap_check(els, 1, int(7.5 * EMU), int(13.333 * EMU))
|
|
258
|
+
codes = [i["code"] for i in issues]
|
|
259
|
+
msgs = " ".join(i.get("message") or "" for i in issues)
|
|
260
|
+
ok(
|
|
261
|
+
"3f D11 band_top=soWhatY catches invasion in (6.05, 6.40]",
|
|
262
|
+
"ANNOTATION_BAND_OVERLAP" in codes and "6.05" in msgs,
|
|
263
|
+
f"codes={codes} msgs={msgs[:120]}",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_chrome_footer_ignores_bare_integers() -> None:
|
|
268
|
+
"""CHROME_DRIFT FP: bare year/exhibit numbers must not count as pager."""
|
|
269
|
+
def sp(y, text):
|
|
270
|
+
return ET.fromstring(
|
|
271
|
+
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
|
272
|
+
f'<p:sp xmlns:a="{NS["a"]}" xmlns:p="{NS["p"]}">'
|
|
273
|
+
"<p:spPr><a:xfrm>"
|
|
274
|
+
f'<a:off x="{int(0.6 * EMU)}" y="{int(y * EMU)}"/>'
|
|
275
|
+
f'<a:ext cx="{int(1.0 * EMU)}" cy="{int(0.3 * EMU)}"/>'
|
|
276
|
+
"</a:xfrm></p:spPr>"
|
|
277
|
+
f'<p:txBody><a:p><a:r><a:rPr sz="1100"/><a:t>{text}</a:t></a:r></a:p></p:txBody>'
|
|
278
|
+
"</p:sp>"
|
|
279
|
+
)
|
|
280
|
+
shapes = [sp(6.14, "2024"), sp(6.14, "06"), sp(7.0, "3 / 14")]
|
|
281
|
+
y = V.chrome_footer_y_in(shapes, int(7.5 * EMU))
|
|
282
|
+
ok(
|
|
283
|
+
"5 chrome_footer_y_in uses N / M only (ignores bare ints)",
|
|
284
|
+
y is not None and abs(y - 7.0) < 0.05,
|
|
285
|
+
f"got {y}",
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_chart_bottom_no_short_circuit() -> None:
|
|
290
|
+
"""D8: chartBottom must Math.min soWhat/footnote — no footnote-first return."""
|
|
291
|
+
build = (ROOT.parent / "scripts" / "build_pptx.js").read_text(encoding="utf-8")
|
|
292
|
+
m = re.search(r"function chartBottom\([\s\S]*?\n\}", build)
|
|
293
|
+
body = m.group(0) if m else ""
|
|
294
|
+
ok(
|
|
295
|
+
"D8 chartBottom uses Math.min (no footnote short-circuit)",
|
|
296
|
+
"Math.min" in body
|
|
297
|
+
and "hasFootnote) return" not in body
|
|
298
|
+
and "CONTENT_BOTTOM_NOTE" in body,
|
|
299
|
+
f"body={body[:200]!r}",
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
|
|
123
304
|
def test_engine_static() -> None:
|
|
124
305
|
export = (ROOT.parent / "assets" / "pptx-export.js").read_text(encoding="utf-8")
|
|
125
306
|
build = (ROOT.parent / "scripts" / "build_pptx.js").read_text(encoding="utf-8")
|
|
@@ -170,6 +351,14 @@ def main() -> int:
|
|
|
170
351
|
test_shape_bounds_still_reads_a_xfrm()
|
|
171
352
|
test_text_overflow_vertical()
|
|
172
353
|
test_annotation_band_overlap()
|
|
354
|
+
test_annotation_band_ignores_full_bleed_bg()
|
|
355
|
+
test_annotation_band_allows_content_without_note()
|
|
356
|
+
test_annotation_band_fires_when_note_present()
|
|
357
|
+
test_annotation_band_reports_all_invaders()
|
|
358
|
+
test_annotation_band_so_what_band_top()
|
|
359
|
+
test_chrome_footer_ignores_bare_integers()
|
|
360
|
+
test_chart_bottom_no_short_circuit()
|
|
361
|
+
test_font_size_h2_17_allowed()
|
|
173
362
|
test_font_size_not_snapped()
|
|
174
363
|
test_engine_static()
|
|
175
364
|
print(f"\n{len(fails)} failed" if fails else "\nAll feedback gates PASS")
|
|
@@ -354,27 +354,130 @@ def annotation_band_overlap_check(
|
|
|
354
354
|
elements: list[ET.Element],
|
|
355
355
|
slide_no: int,
|
|
356
356
|
slide_height_emu: int,
|
|
357
|
+
slide_width_emu: int | None = None,
|
|
357
358
|
) -> list[dict[str, Any]]:
|
|
358
359
|
"""Detect primary content crushing into the annotation band.
|
|
359
360
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
361
|
+
Geometry (layout-constants pageTypes.layout / exhibit / note):
|
|
362
|
+
soWhatY ≈ 6.05 · contentBottomWithNote ≈ 6.4 · footnote/note ≈ 6.55 ·
|
|
363
|
+
contentBottom ≈ 6.9 · pager ≈ 7.0
|
|
364
|
+
|
|
365
|
+
Semantics (0.1.7 · D9/D11):
|
|
366
|
+
- Full-bleed backgrounds, full-height accent strips, and pager/footer
|
|
367
|
+
chrome are never "content crushing the band".
|
|
368
|
+
- so-what / 口径 / 来源 bars that live in the annotation zone are the
|
|
369
|
+
band itself — not invaders.
|
|
370
|
+
- Crush = starts ABOVE band_top and overlaps down into the band.
|
|
371
|
+
Shapes whose top is already at/below band_top are in-band chrome.
|
|
372
|
+
- When so-what is present, band_top = soWhatY so invasions in
|
|
373
|
+
(soWhatY, contentBottomWithNote] are reported (D11). Footnote/source
|
|
374
|
+
only keeps band_top = contentBottomWithNote.
|
|
375
|
+
- When no annotation is present, main content may use up to
|
|
376
|
+
contentBottom; only a severe invasion (≥0.35in into the nominal
|
|
377
|
+
withNote band) still fires as a safety net.
|
|
378
|
+
- Report ALL invaders per slide (D9) — do not break after the first.
|
|
363
379
|
"""
|
|
364
380
|
out: list[dict[str, Any]] = []
|
|
365
381
|
try:
|
|
366
382
|
lc_path = Path(__file__).resolve().parent / "layout-constants.json"
|
|
367
383
|
lc = json.loads(lc_path.read_text(encoding="utf-8"))
|
|
368
384
|
lay = ((lc.get("pageTypes") or {}).get("layout") or {})
|
|
369
|
-
|
|
385
|
+
exhibit = ((lc.get("pageTypes") or {}).get("exhibit") or {})
|
|
386
|
+
note = ((lc.get("pageTypes") or {}).get("note") or {})
|
|
387
|
+
nominal_band_top_in = float(lay.get("contentBottomWithNote") or 6.4)
|
|
370
388
|
band_bot_in = float(lay.get("contentBottom") or 6.9)
|
|
389
|
+
so_what_y_in = float(exhibit.get("soWhatY") or 6.05)
|
|
390
|
+
note_y_in = float(note.get("y") or exhibit.get("footnoteY") or 6.55)
|
|
371
391
|
except (OSError, json.JSONDecodeError, TypeError, ValueError):
|
|
372
|
-
|
|
392
|
+
nominal_band_top_in, band_bot_in, so_what_y_in, note_y_in = 6.4, 6.9, 6.05, 6.55
|
|
393
|
+
|
|
394
|
+
band_top_in = nominal_band_top_in
|
|
373
395
|
band_top = int(band_top_in * 914400)
|
|
396
|
+
nominal_band_top = int(nominal_band_top_in * 914400)
|
|
374
397
|
band_bot = int(band_bot_in * 914400)
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
398
|
+
so_what_y = int(so_what_y_in * 914400)
|
|
399
|
+
note_y = int(note_y_in * 914400)
|
|
400
|
+
slide_w = int(slide_width_emu) if slide_width_emu else int(13.333333 * 914400)
|
|
401
|
+
slide_h = int(slide_height_emu)
|
|
402
|
+
slide_area = max(slide_w * slide_h, 1)
|
|
403
|
+
pager_floor = int(min(band_bot_in + 0.05, slide_h / 914400.0 - 0.05) * 914400)
|
|
404
|
+
min_overlap = int(0.10 * 914400)
|
|
405
|
+
severe_overlap = int(0.35 * 914400)
|
|
406
|
+
full_bleed_area = 0.85
|
|
407
|
+
full_height_frac = 0.90
|
|
408
|
+
# Label-like prefixes only — body text containing「来源」must not count (agenda FP)
|
|
409
|
+
ann_pat = re.compile(
|
|
410
|
+
r"(?:^|\n)\s*(结论|SO\s*WHAT|口径\s*[::]?|来源\s*[::]|数据来源\s*[::]|"
|
|
411
|
+
r"Source\s*[::]|Note\s*[::]|footnote|注\s*[::])",
|
|
412
|
+
re.IGNORECASE,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def _text(el: ET.Element) -> str:
|
|
416
|
+
return (text_content(el) or "").strip()
|
|
417
|
+
|
|
418
|
+
def _is_chrome_or_bg(y: int, cy: int, cx: int) -> bool:
|
|
419
|
+
if (cx * cy) / slide_area >= full_bleed_area and y <= int(0.05 * 914400):
|
|
420
|
+
return True
|
|
421
|
+
if cy >= int(full_height_frac * slide_h) and y <= int(0.05 * 914400):
|
|
422
|
+
return True
|
|
423
|
+
if y >= band_bot - int(0.02 * 914400):
|
|
424
|
+
return True
|
|
425
|
+
if y >= pager_floor:
|
|
426
|
+
return True
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
def _is_bar_like(cy: int, cx: int) -> bool:
|
|
430
|
+
return cy <= int(0.75 * 914400) and cx >= int(0.45 * slide_w)
|
|
431
|
+
|
|
432
|
+
def _is_note_like(y: int, cy: int, text: str) -> bool:
|
|
433
|
+
return y >= note_y - int(0.05 * 914400) and cy <= int(0.55 * 914400) and bool(text)
|
|
434
|
+
|
|
435
|
+
def _at_so_what_slot(y: int) -> bool:
|
|
436
|
+
# Engine locks so-what to soWhatY; agenda trailing rows sit lower (~+0.12)
|
|
437
|
+
return abs(y - so_what_y) <= int(0.08 * 914400)
|
|
438
|
+
|
|
439
|
+
def _is_annotation_self(y: int, cy: int, cx: int, text: str) -> bool:
|
|
440
|
+
"""so-what / footnote / source row living in the annotation zone."""
|
|
441
|
+
if _at_so_what_slot(y) and cy <= int(0.85 * 914400):
|
|
442
|
+
# Fill rect, accent strip, or SO WHAT text — all live in the slot
|
|
443
|
+
if ann_pat.search(text) or _is_bar_like(cy, cx) or cx <= int(0.15 * 914400):
|
|
444
|
+
return True
|
|
445
|
+
if _is_note_like(y, cy, text):
|
|
446
|
+
return True
|
|
447
|
+
# Entirely inside the nominal withNote band (6.40 口径)
|
|
448
|
+
if y >= nominal_band_top - int(0.02 * 914400):
|
|
449
|
+
return True
|
|
450
|
+
return False
|
|
451
|
+
|
|
452
|
+
has_annotation = False
|
|
453
|
+
has_so_what = False
|
|
454
|
+
for el in elements:
|
|
455
|
+
box = shape_bounds(el)
|
|
456
|
+
if box is None:
|
|
457
|
+
continue
|
|
458
|
+
_x, y, cx, cy = box
|
|
459
|
+
if cx <= 0 or cy <= 0:
|
|
460
|
+
continue
|
|
461
|
+
if _is_chrome_or_bg(y, cy, cx):
|
|
462
|
+
continue
|
|
463
|
+
txt = _text(el)
|
|
464
|
+
note_like = _is_note_like(y, cy, txt)
|
|
465
|
+
labeled = bool(ann_pat.search(txt))
|
|
466
|
+
bar = _is_bar_like(cy, cx)
|
|
467
|
+
# Fill rect sits on soWhatY (±0.02); labeled SO WHAT text may sit slightly below
|
|
468
|
+
on_fill = abs(y - so_what_y) <= int(0.02 * 914400) and bar
|
|
469
|
+
labeled_slot = labeled and _at_so_what_slot(y) and cy <= int(0.85 * 914400)
|
|
470
|
+
if note_like:
|
|
471
|
+
has_annotation = True
|
|
472
|
+
elif labeled_slot or on_fill:
|
|
473
|
+
has_annotation = True
|
|
474
|
+
has_so_what = True
|
|
475
|
+
|
|
476
|
+
if has_so_what:
|
|
477
|
+
band_top_in = min(nominal_band_top_in, so_what_y_in)
|
|
478
|
+
band_top = int(band_top_in * 914400)
|
|
479
|
+
|
|
480
|
+
# Pass 2: body invaders that START ABOVE the band and crush down (D9: all of them)
|
|
378
481
|
for el in elements:
|
|
379
482
|
box = shape_bounds(el)
|
|
380
483
|
if box is None:
|
|
@@ -383,14 +486,19 @@ def annotation_band_overlap_check(
|
|
|
383
486
|
if cx <= 0 or cy <= 0:
|
|
384
487
|
continue
|
|
385
488
|
bottom = y + cy
|
|
386
|
-
|
|
489
|
+
if _is_chrome_or_bg(y, cy, cx):
|
|
490
|
+
continue
|
|
491
|
+
if _is_annotation_self(y, cy, cx, _text(el)):
|
|
492
|
+
continue
|
|
493
|
+
# Crush-from-above only: top must sit above band_top
|
|
387
494
|
if y >= band_top - int(0.02 * 914400):
|
|
388
495
|
continue
|
|
389
|
-
|
|
390
|
-
if y >= pager_floor:
|
|
496
|
+
if bottom <= band_top:
|
|
391
497
|
continue
|
|
392
|
-
overlap = min(bottom, band_bot) -
|
|
393
|
-
if overlap
|
|
498
|
+
overlap = min(bottom, band_bot) - band_top
|
|
499
|
+
if overlap < min_overlap:
|
|
500
|
+
continue
|
|
501
|
+
if has_annotation or overlap >= severe_overlap:
|
|
394
502
|
out.append(issue(
|
|
395
503
|
"ANNOTATION_BAND_OVERLAP",
|
|
396
504
|
f"主内容侵入注释带(元素底边 {bottom/914400:.2f}in 越过注释带顶 "
|
|
@@ -398,17 +506,20 @@ def annotation_band_overlap_check(
|
|
|
398
506
|
"图例/系列请收入主图区或压缩系列数,禁止压进 so-what/来源行。",
|
|
399
507
|
slide=slide_no,
|
|
400
508
|
))
|
|
401
|
-
# One finding per slide is enough to gate
|
|
402
|
-
break
|
|
403
509
|
return out
|
|
404
510
|
|
|
405
511
|
|
|
512
|
+
|
|
406
513
|
def font_size_snap_check(root: ET.Element, slide_no: int) -> list[dict[str, Any]]:
|
|
407
|
-
"""Font sizes must sit on the
|
|
514
|
+
"""Font sizes must sit on the declared type scale / fontShrink ladder.
|
|
408
515
|
|
|
409
516
|
fitFont already selects from the ladder; this gate catches callers that
|
|
410
517
|
arithmetic-shift sizes (e.g. fz-1) or hardcode off-ladder values without
|
|
411
518
|
re-snapping through the ladder / modeSize path.
|
|
519
|
+
|
|
520
|
+
Allowed set = containers.fontShrink.ladder ∪ typeScale ∪ all modeTypeScale
|
|
521
|
+
roles ∪ a small cover/hero display whitelist. Intentional h2 (17pt) and
|
|
522
|
+
other mode roles must pass; off-ladder 11.3pt etc. must still fail.
|
|
412
523
|
"""
|
|
413
524
|
out: list[dict[str, Any]] = []
|
|
414
525
|
try:
|
|
@@ -416,10 +527,25 @@ def font_size_snap_check(root: ET.Element, slide_no: int) -> list[dict[str, Any]
|
|
|
416
527
|
lc = json.loads(lc_path.read_text(encoding="utf-8"))
|
|
417
528
|
ladder = ((lc.get("containers") or {}).get("fontShrink") or {}).get("ladder") or []
|
|
418
529
|
allowed = {round(float(v), 2) for v in ladder}
|
|
530
|
+
scale_sources: list[Any] = [lc.get("typeScale") or {}]
|
|
531
|
+
mts = lc.get("modeTypeScale") or {}
|
|
532
|
+
if isinstance(mts, dict):
|
|
533
|
+
scale_sources.extend(v for v in mts.values() if isinstance(v, dict))
|
|
534
|
+
for src in scale_sources:
|
|
535
|
+
if not isinstance(src, dict):
|
|
536
|
+
continue
|
|
537
|
+
for key, val in src.items():
|
|
538
|
+
if str(key).startswith("$"):
|
|
539
|
+
continue
|
|
540
|
+
try:
|
|
541
|
+
allowed.add(round(float(val), 2))
|
|
542
|
+
except (TypeError, ValueError):
|
|
543
|
+
continue
|
|
419
544
|
except (OSError, json.JSONDecodeError, TypeError, ValueError):
|
|
420
545
|
allowed = {15, 14, 13.5, 13, 12.5, 12, 11.5, 11, 10.5, 10, 9.5, 9, 8.5}
|
|
421
|
-
|
|
422
|
-
|
|
546
|
+
allowed |= {44, 36, 30, 22, 19, 18, 17, 14, 13, 11, 10, 8.5}
|
|
547
|
+
# Cover/hero intermediate display sizes (not every mode lists every step)
|
|
548
|
+
allowed |= {28, 24, 22, 20, 16, 8.0, 7.5}
|
|
423
549
|
alien: list[float] = []
|
|
424
550
|
for run in root.findall(".//a:r", NS):
|
|
425
551
|
text = "".join(t.text or "" for t in run.findall("a:t", NS))
|
|
@@ -691,7 +817,8 @@ def chrome_footer_y_in(shapes: list[ET.Element], height: int) -> float | None:
|
|
|
691
817
|
bottom_frac = float(chrome[1] if isinstance(chrome, list) and len(chrome) > 1 else 24) / 100.0
|
|
692
818
|
threshold = int(height * (1.0 - bottom_frac))
|
|
693
819
|
ys: list[float] = []
|
|
694
|
-
|
|
820
|
+
# Engine footer is always "N / M"; bare integers are chart/exhibit labels (CHROME_DRIFT FP)
|
|
821
|
+
pager = re.compile(r"^\s*\d+\s*/\s*\d+\s*$")
|
|
695
822
|
for shape in shapes:
|
|
696
823
|
text = (text_content(shape) or "").strip()
|
|
697
824
|
box = shape_bounds(shape)
|
|
@@ -784,7 +911,7 @@ def inspect_slide(
|
|
|
784
911
|
warnings.extend(container_overflow_check(shape, slide_number))
|
|
785
912
|
|
|
786
913
|
warnings.extend(annotation_band_overlap_check(
|
|
787
|
-
[*shapes, *pictures, *graphic_frames], slide_number, height))
|
|
914
|
+
[*shapes, *pictures, *graphic_frames], slide_number, height, width))
|
|
788
915
|
warnings.extend(font_size_snap_check(root, slide_number))
|
|
789
916
|
|
|
790
917
|
for table in tables:
|