@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,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* TopPPT HTML ยท ไธป้ขๅ่ๅพ็ๆๅจ๏ผ็ปดๆคๅทฅๅ
ท๏ผ้ไบคไปไพ่ตใ้ไบคไป็กฌ้จ็ฆ๏ผ
|
|
3
|
+
*
|
|
4
|
+
* ็จๆณ๏ผ
|
|
5
|
+
* npm install playwright # ๆๅจไปปๆไฝ็ฝฎๅฎ่ฃ
playwright ๅ่ฎพ็ฝฎ NODE_PATH
|
|
6
|
+
* npx playwright install chromium # ้ฆๆฌก้่ฆไธ่ฝฝๆต่งๅจ
|
|
7
|
+
* node scripts/capture_theme_overview.js [--channel=msedge|chrome|chromium] [--scale=1.25]
|
|
8
|
+
*
|
|
9
|
+
* --scale = deviceScaleFactor๏ผ้ป่ฎค 1.25๏ผใๅ่ๅพๅช็จไบ"่ฎฉ็จๆท็่ง้ฃๆ ผไธไธป้ข"๏ผ
|
|
10
|
+
* 1.25 ๅจ 1480px ่งๅฃไธๅทฒ่ถณๅคๆธ
ๆฐ๏ผๅๆถๆๅๅผ PNG ๆงๅถๅจ ~1MB ๅ
๏ผไพฟไบๅจ IDE ้ๅฟซ้ๆๅผ๏ผใ
|
|
11
|
+
*
|
|
12
|
+
* ๆต่งๅจ้้่ชๅจๆขๆต๏ผไผๅ
ๅฝไปค่ก --channel=๏ผๅ
ถๆฌก็ฏๅขๅ้ TOP_PPT_BROWSER_CHANNEL๏ผ
|
|
13
|
+
* ๆๅๅ่ฝๅฐ Playwright ่ชๅธฆ็ chromium๏ผๆ ๆฌๆบๆต่งๅจไพ่ต๏ผใ
|
|
14
|
+
*
|
|
15
|
+
* ไบง็ฉ๏ผassets/๏ผ่ฆ็ๆงๅพ๏ผ๏ผ
|
|
16
|
+
* theme-overview.png โโ ้ป่ฎค๏ผๆผ็คบ Tab๏ผๆดไฝๅ่ๅพ
|
|
17
|
+
* theme-overview-presentation.png โโ ๆผ็คบๆจกๅผ Tab
|
|
18
|
+
* theme-overview-research.png โโ ็ ็ฉถๆจกๅผ Tab๏ผๅจ่ฏขๅฏๆๅฝขๆ๏ผ
|
|
19
|
+
* theme-overview-architecture.png โโ ๆถๆๆจกๅผ Tab๏ผๅพไธบ็ๅฝขๆ๏ผ
|
|
20
|
+
*
|
|
21
|
+
* ๆฏๆฌกๆชๅพๅๆญ่จๅฝๅ Tab ๆๆก๏ผ้ฒๆญข Tab ๅๆขๆช็ๆๅฏผ่ดไบงๅบ้ทๅๅพใ */
|
|
22
|
+
const path = require('path');
|
|
23
|
+
let chromium;
|
|
24
|
+
try {
|
|
25
|
+
({ chromium } = require('playwright'));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error('ๆชๆพๅฐ playwrightใ่ฏทๅ
ๅฎ่ฃ
๏ผnpm install playwright && npx playwright install chromium');
|
|
28
|
+
console.error('๏ผๆฌ่ๆฌไป
็จไบๅทๆฐ assets/theme-overview*.png ๅ่ๅพ๏ผ้ๆ่ฝไบคไปไพ่ตใ๏ผ');
|
|
29
|
+
process.exit(2);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const TAB_TEXT = { presentation: 'ๆผ็คบๆฑๆฅ', research: '็ ็ฉถๆฅๅ', architecture: 'ไฟกๆฏๆถๆๅพ' };
|
|
33
|
+
const channelArg = (process.argv.find((a) => a.startsWith('--channel=')) || '').split('=')[1];
|
|
34
|
+
const scaleArg = parseFloat((process.argv.find((a) => a.startsWith('--scale=')) || '').split('=')[1]);
|
|
35
|
+
const SCALE = Number.isFinite(scaleArg) && scaleArg > 0 ? scaleArg : 1.25;
|
|
36
|
+
const CHANNEL = channelArg || process.env.TOP_PPT_BROWSER_CHANNEL
|
|
37
|
+
|| process.env.TOP_PPT_BROWSER_CHANNEL || '';
|
|
38
|
+
|
|
39
|
+
(async () => {
|
|
40
|
+
const gallery = 'file:///' + path.resolve(__dirname, '../assets/style-gallery.html').replace(/\\/g, '/');
|
|
41
|
+
const outDir = path.resolve(__dirname, '../assets');
|
|
42
|
+
/* channel ไธบ็ฉบ โ ็จ Playwright ่ชๅธฆ chromium๏ผไธไพ่ตๆฌๆบๅทฒ่ฃ
ๆต่งๅจ๏ผ */
|
|
43
|
+
const browser = await chromium.launch(CHANNEL ? { channel: CHANNEL } : {});
|
|
44
|
+
const page = await browser.newPage({ viewport: { width: 1480, height: 1200 }, deviceScaleFactor: SCALE });
|
|
45
|
+
await page.goto(gallery);
|
|
46
|
+
await page.waitForSelector('.preset');
|
|
47
|
+
|
|
48
|
+
const modes = ['presentation', 'research', 'architecture'];
|
|
49
|
+
for (const m of modes) {
|
|
50
|
+
const idx = modes.indexOf(m);
|
|
51
|
+
await page.evaluate((i) => document.querySelectorAll('.mtab')[i].click(), idx);
|
|
52
|
+
await page.waitForTimeout(500);
|
|
53
|
+
const on = await page.evaluate(() =>
|
|
54
|
+
(document.querySelector('.mtab.on .mtab__t') || {}).textContent || '');
|
|
55
|
+
if (on !== TAB_TEXT[m]) {
|
|
56
|
+
throw new Error(`Tab ๅๆขๆญ่จๅคฑ่ดฅ๏ผๆๆใ${TAB_TEXT[m]}ใๅฎ้
ใ${on}ใ๏ผ้ฒๆญข้ทๅๅพๅๅฝ๏ผ`);
|
|
57
|
+
}
|
|
58
|
+
/* ๅก็ๆฐๅฟ
้กป็ญไบๅๆบ้ฃๆ ผๆฐ๏ผPRESETS ็ฑ sync_runtime.py ไป layout-constants.json ๆณจๅ
ฅ๏ผ๏ผ
|
|
59
|
+
* ้ฒๆญขใๆฉไบ้ฃๆ ผไฝ็ปๅป/ๅ่ๅพๆฒก่ทไธใ็้้ปๆผ็งป๏ผๆงๆญ่จๅชๆฅ n < 9๏ผๅคชๅผฑ๏ผใ */
|
|
60
|
+
const n = await page.evaluate(() => document.querySelectorAll('.preset').length);
|
|
61
|
+
const expected = await page.evaluate(() =>
|
|
62
|
+
(typeof PRESETS === 'object' && PRESETS) ? Object.keys(PRESETS).length : 0);
|
|
63
|
+
if (expected && n !== expected) {
|
|
64
|
+
throw new Error(`็ปๅปๅก็ๆฐ ${n} โ ๅๆบ้ฃๆ ผๆฐ ${expected}๏ผ็ปๅปไธ layout-constants.json ๆผ็งป๏ผ`);
|
|
65
|
+
}
|
|
66
|
+
if (n < 9) throw new Error(`ๅก็ๆฐๅผๅธธ๏ผ${n}`);
|
|
67
|
+
await page.screenshot({ path: path.join(outDir, `theme-overview-${m}.png`), fullPage: true });
|
|
68
|
+
console.log(`captured: theme-overview-${m}.png (tab=${on}, cards=${n})`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* ไธปๅพๅๅฐๆผ็คบ Tab๏ผ้ป่ฎคๆ๏ผ */
|
|
72
|
+
await page.evaluate(() => document.querySelectorAll('.mtab')[0].click());
|
|
73
|
+
await page.waitForTimeout(500);
|
|
74
|
+
await page.screenshot({ path: path.join(outDir, 'theme-overview.png'), fullPage: true });
|
|
75
|
+
console.log('captured: theme-overview.png (default presentation tab)');
|
|
76
|
+
await browser.close();
|
|
77
|
+
console.log(`done: 4 ๅผ ไธป้ขๅ่ๅพๅทฒๅทๆฐ๏ผdeviceScaleFactor=${SCALE}๏ผ`);
|
|
78
|
+
})().catch((e) => { console.error(e); process.exit(1); });
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML ยท HTML ๆฃๆฅๅ
ฌๅ
ฑๅบ๏ผๅๆบ๏ผๆฟ่ฝฝๆญฃๅ / ๅพ่กจ้้ / ๅคๆ ทๆงไธ้ / ็ปๅ็ๅผ๏ผ
|
|
4
|
+
|
|
5
|
+
ๆถ่ดนๆน๏ผvalidate_report.py ยท quality_gate.py ยท evals/run_evals.py
|
|
6
|
+
็ฆๆญขๅจๆถ่ดนๆนๅๆๆ CARRIERS / ๅคๆ ทๆงๅ
ฌๅผ / ็ปๅ็ๅผๅคๅฎใ
|
|
7
|
+
้ๅผไป่ฏป layout-constants.json๏ผcharts.variety / charts.registry๏ผใ
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
16
|
+
_LC_PATH = ROOT / 'scripts' / 'layout-constants.json'
|
|
17
|
+
|
|
18
|
+
# ๆฟ่ฝฝ็ฑปๅๆญฃๅ๏ผ็ปๅ็ๅผ๏ผไธ้กตๆฏๅฆๅซ โฅ2 ็งๆฟ่ฝฝ๏ผ
|
|
19
|
+
CARRIERS = [
|
|
20
|
+
r'<svg\b[^>]*data-chart=',
|
|
21
|
+
r'class="tbl-wrap',
|
|
22
|
+
r'class="card[\s"]',
|
|
23
|
+
r'class="metric[\s"]',
|
|
24
|
+
r'class="media[\s"]',
|
|
25
|
+
r'class="arch[\s"]',
|
|
26
|
+
r'class="lane[\s"]',
|
|
27
|
+
r'class="matrix[\s"]',
|
|
28
|
+
r'class="heat[\s"]',
|
|
29
|
+
r'class="bul[\s"]',
|
|
30
|
+
r'class="pyr[\s"]',
|
|
31
|
+
r'class="steps[\s"]',
|
|
32
|
+
r'class="exhibit[\s"]',
|
|
33
|
+
r'class="cols-2',
|
|
34
|
+
r'class="cols-3',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
SKIP_BAND_IDS = ('agenda', 'cover', 'refs', 'appendix', 'next')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_lc() -> dict:
|
|
41
|
+
return json.loads(_LC_PATH.read_text(encoding='utf-8'))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def chart_registry(lc: dict | None = None) -> dict:
|
|
45
|
+
lc = lc or load_lc()
|
|
46
|
+
return {k: v for k, v in ((lc.get('charts') or {}).get('registry') or {}).items()
|
|
47
|
+
if isinstance(v, dict)}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def chart_variety(lc: dict | None = None) -> dict:
|
|
51
|
+
lc = lc or load_lc()
|
|
52
|
+
return (lc.get('charts') or {}).get('variety') or {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def bands(html: str) -> list[str]:
|
|
56
|
+
return re.split(r'(?=<section class="band)', html)[1:]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def content_bands(html: str, skip_ids: tuple[str, ...] = SKIP_BAND_IDS) -> list[str]:
|
|
60
|
+
out = []
|
|
61
|
+
for b in bands(html):
|
|
62
|
+
head = b[:240]
|
|
63
|
+
if any(f'id="{i}"' in head for i in skip_ids):
|
|
64
|
+
continue
|
|
65
|
+
out.append(b)
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def chart_types_per_page(html: str) -> list[list[str]]:
|
|
70
|
+
return [re.findall(r'data-chart="([^"]+)"', b) for b in bands(html)]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def distinct_chart_types(html: str) -> list[str]:
|
|
74
|
+
used = [t for ts in chart_types_per_page(html) for t in ts]
|
|
75
|
+
return sorted(set(used))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def variety_floor(mode: str, n_chart_pages: int, variety: dict | None = None) -> int:
|
|
79
|
+
"""็ฑปๅๆฐไธ้ = min(ๆจกๅผไธ้, max(1, โๅพ่กจ้กตๆฐ ร ratioโ))"""
|
|
80
|
+
v = variety if variety is not None else chart_variety()
|
|
81
|
+
floor = int((v.get('minTypes') or {}).get(mode, 4) or 4)
|
|
82
|
+
ratio = float(v.get('minTypesRatio') or 0.6)
|
|
83
|
+
return min(floor, max(1, int(-(-n_chart_pages * ratio // 1))))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def adjacent_same_type(pages: list[list[str]]) -> list[str]:
|
|
87
|
+
"""็ธ้ปๅพ่กจ้กตๅๅๅ่ญฆๆๆกๅ่กจใ"""
|
|
88
|
+
adj, prev, prev_i = [], None, 0
|
|
89
|
+
for i, ts in enumerate(pages):
|
|
90
|
+
if not ts:
|
|
91
|
+
continue
|
|
92
|
+
if prev and set(ts) & set(prev):
|
|
93
|
+
adj.append(f'็ฌฌ {prev_i + 1}/{i + 1} ้กตๅๅ {sorted(set(ts) & set(prev))}')
|
|
94
|
+
prev, prev_i = ts, i
|
|
95
|
+
return adj
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def carrier_count(band: str) -> int:
|
|
99
|
+
return sum(1 for pat in CARRIERS if re.search(pat, band))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def composite_pages(html: str) -> tuple[int, int]:
|
|
103
|
+
"""่ฟๅ (ๅซ โฅ2 ็งๆฟ่ฝฝ็ๅ
ๅฎน้กตๆฐ, ๅ
ๅฎน้กตๆปๆฐ)ใ"""
|
|
104
|
+
content = content_bands(html)
|
|
105
|
+
multi = sum(1 for b in content if carrier_count(b) >= 2)
|
|
106
|
+
return multi, len(content)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def composite_required(mode: str, variety: dict | None = None) -> float:
|
|
110
|
+
"""็ปๅ็ๅผๆฏไพไธ้๏ผ้ compositeModes ่ฟๅ 0ใ"""
|
|
111
|
+
v = variety if variety is not None else chart_variety()
|
|
112
|
+
if mode not in (v.get('compositeModes') or ['research']):
|
|
113
|
+
return 0.0
|
|
114
|
+
return float(v.get('compositeMinRatio') or 0)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def chart_channel(chart_type: str, reg: dict | None = None) -> str:
|
|
118
|
+
reg = reg if reg is not None else chart_registry()
|
|
119
|
+
spec = reg.get(str(chart_type or 'bar').lower()) or {}
|
|
120
|
+
return str(spec.get('pptx') or 'native')
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def chart_datatable_mode(chart: dict, reg: dict | None = None) -> str:
|
|
124
|
+
reg = reg if reg is not None else chart_registry()
|
|
125
|
+
spec = reg.get(str(chart.get('type') or 'bar').lower()) or {}
|
|
126
|
+
mode = str(chart.get('dataTable') or spec.get('dataTable') or 'notes').lower()
|
|
127
|
+
return 'inline' if mode == 'appendix' else mode
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML ยท ๅ้้ไบคๅ้ช่ฏ๏ผๅฏ้ไพ่ต python-pptx๏ผ๏ผ
|
|
4
|
+
โ A ้้๏ผๆๅ OOXML๏ผไธ B ้้๏ผpptxgenjs๏ผไบง็ฉๅๅฏ่ขซ python-pptx ไธฅๆ ผ่งฃๆ
|
|
5
|
+
โก ไธค้้้้กตๆๆฌ้ๅไธ่ด๏ผๅไธๆจกๅ โ ๅไธๅ
ๅฎน๏ผๆฎต่ฝๆข่ก่กจ็คบๅทฎๅผๅทฒๅฝไธ๏ผ
|
|
6
|
+
ๅฝไธๅ๏ผB ้้ๅธฆๆฐๆฎ็ๅพ่กจไธบๅ็ chart part๏ผๅฏ็ผ่พๆฐๆฎ๏ผโโๅพ่กจ็ฑปๅซๆ ็ญพๅนถๅ
ฅ
|
|
7
|
+
ๆๆฌ้ๅใ็บฏๆฐๅผ token๏ผๅซ็พๅๆฐ๏ผไธค็ซฏ่ฟๆปค๏ผA ้้ๅฝข็ถ็็ๆฐๅผๆ ็ญพ vs B ้้ chart ๆฐๆฎๅผ๏ผ
|
|
8
|
+
โข ๆๆฌๆบขๅบๅฏๅๅผ๏ผๆๆฌไผฐ็ฎๅฎฝ vs shape ๅฎฝ ร ๅฏๅฎน่กๆฐ
|
|
9
|
+
โฃ B ้้ๅ็ๅพ่กจๆฐๅผ โ ๆจกๅๆฐๅผ๏ผ็ฑปๅซๆ ็ญพไธ่ดไธไปฃ่กจๆฐๅผไธ่ด๏ผๆฐๅผ้ไนฑๅช่ฝ่ฟ้ๆ๏ผ
|
|
10
|
+
โค ๅญๅทๆฏไพๅฐบๅๆบ๏ผB ็ๅญๅทๅฟ
้กปๆฏ A ๅญๅท้็ๅญ้๏ผ้ฒ modeSize()/sz() ๅๅค็ๆผ็งป๏ผ
|
|
11
|
+
็จๆณ๏ผๅ
node scripts/gen_channel_a.js ็ๆ A ้้ไบง็ฉ๏ผๅ python scripts/cross_verify.py
|
|
12
|
+
ๆน pptx-export.js ๅบๅๅ้ชจๆถๅๅฟ
่ท๏ผ่ง references/pptx-export.md ๅ่ฃๅคๆ่ฎญ๏ผใ
|
|
13
|
+
"""
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
20
|
+
# ไพ่ต็ผบๅคฑไธๅจๅฏผๅ
ฅๆ้ๅบโโๆฌๆจกๅ็ๆ ธๅฏนๅฝๆฐไผ่ขซ negative_tests.py ็ดๆฅๅค็จ
|
|
21
|
+
try:
|
|
22
|
+
from pptx import Presentation
|
|
23
|
+
except ImportError:
|
|
24
|
+
Presentation = None
|
|
25
|
+
|
|
26
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
27
|
+
A_DIR = ROOT / 'dist' / 'regression-a' # A ้้๏ผ้กต้ข็ดๅฏผๅผๆ๏ผ
|
|
28
|
+
B_DIR = ROOT / 'dist' / 'regression' # B ้้๏ผpptxgenjs๏ผ
|
|
29
|
+
MODEL_DIR = ROOT / 'assets' / 'examples'
|
|
30
|
+
|
|
31
|
+
# ๆๆฌๅบฆ้ๅๆบ๏ผไธ build_pptx.js / validate_pptx.py ๅๅผ๏ผโโไธไพงๅ่ช็กฌ็ผ็ ไผ่ฎฉ
|
|
32
|
+
# ๆบขๅบๅคๅฎๅจไธๅ่ฃๅค้ด้ฃ็งป๏ผๅบ็ฐโไธไพงๆฅๆบขๅบใไธไพง่ฏดๆฒกไบโใ
|
|
33
|
+
try:
|
|
34
|
+
_TM = (json.loads((ROOT / 'scripts' / 'layout-constants.json').read_text(encoding='utf-8'))
|
|
35
|
+
.get('containers', {}).get('textMetrics', {}))
|
|
36
|
+
except (OSError, json.JSONDecodeError):
|
|
37
|
+
_TM = {}
|
|
38
|
+
LINE_FACTOR = float(_TM.get('lineFactor') or 1.45)
|
|
39
|
+
EM_ASCII_RATIO = float(_TM.get('emAsciiRatio') or 0.52)
|
|
40
|
+
|
|
41
|
+
# ๅฝขๆๅๆข็ฑปๅพ่กจ๏ผๆจกๅ้็่ดๅผๅจๅพไธๆฏใๅไธ็ๆญฃ้ซๅบฆๆกใ๏ผๆ็ปๅฏนๅผๆฏๅฏน
|
|
42
|
+
SIGNLESS_CHARTS = {'waterfall', 'gauge', 'funnel', 'bullet'}
|
|
43
|
+
|
|
44
|
+
# ็บฏๆฐๅผ token๏ผๅซๅๅไฝ/ๅฐๆฐ/็พๅๅท/ๅธธ่งๅไฝๅ็ผ๏ผโโๅพ่กจๆฐๆฎๆ ็ญพๅจ A ้้ๆฏ
|
|
45
|
+
# ๆๆฌ๏ผๅผ+unit๏ผๅฆ "9pt"๏ผ๏ผB ้้่ฟ chart part๏ผๆฏๅฏนๅไธค็ซฏ่ฟๆปคใ็ฑปๅซๆ ็ญพ๏ผๅฆ "2026Q3"๏ผ
|
|
46
|
+
# ไธๅน้
ๆญคๆจกๅผ๏ผไปๅไธไธ่ดๆงๆฃๆฅใ
|
|
47
|
+
NUMERIC_TOKEN = re.compile(
|
|
48
|
+
r'^[+-]?[\d.,]+\s*(?:%|โฐ|pt|pct|pp|TB|GB|MB|KB|k|w|ไธ|ไบฟ|ๅ
|ๅ|ไธช|ไบบ|ๅคฉ|ๅฎถ|ๆฌก|้กน|ๅนด'
|
|
49
|
+
r'|ๆก|็ฌ|ๅฐ|ๅฅ|ไปฝ|ๆฌพ|ๅผ |็ฏ|้กต|ไปถ|ไพ|ๅบ|่ฝฎ|่|่พ|ๅจ|็ฑณ|็ง|ๅ|ๆถ|ๆกฃ|็บง)?$')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def slide_texts(prs):
|
|
53
|
+
"""ๆฏ้กตๆๆฌ้ๅ๏ผ้่กๆๅโโA ้้ๆฎต่ฝ \\n ไธ B ้้ breakLine \\n\\n ่กจ็คบๅทฎๅผๅฝไธ๏ผ
|
|
54
|
+
ๅฟฝ็ฅ็ฉบ็ฝใ้กต็ ไธ็บฏๆฐๅผ token๏ผB ้้ๅ็ๅพ่กจ็็ฑปๅซๆ ็ญพๅนถๅ
ฅ้ๅ๏ผใ"""
|
|
55
|
+
pages = []
|
|
56
|
+
for slide in prs.slides:
|
|
57
|
+
texts = []
|
|
58
|
+
for shape in slide.shapes:
|
|
59
|
+
if shape.has_text_frame:
|
|
60
|
+
for line in shape.text_frame.text.splitlines():
|
|
61
|
+
t = line.strip()
|
|
62
|
+
if t and not NUMERIC_TOKEN.match(t):
|
|
63
|
+
texts.append(t)
|
|
64
|
+
elif shape.has_table:
|
|
65
|
+
for row in shape.table.rows:
|
|
66
|
+
for cell in row.cells:
|
|
67
|
+
for line in cell.text.splitlines():
|
|
68
|
+
t = line.strip()
|
|
69
|
+
if t and not NUMERIC_TOKEN.match(t):
|
|
70
|
+
texts.append(t)
|
|
71
|
+
elif getattr(shape, 'has_chart', False):
|
|
72
|
+
# B ้้ๅ็ๆฐๆฎๅพ่กจโโ็ฑปๅซๆ ็ญพ๏ผๅฎ่ดจๅ
ๅฎน๏ผๅนถๅ
ฅ๏ผๆฐๆฎๅผๅทฒๆ็บฏๆฐๅผ่ฟๆปคใ
|
|
73
|
+
# ็ฑปๅซๆ ็ญพๅๆ ท่ฟ NUMERIC_TOKENโโA ้้ๆๆฌ็ปไธ่ฟๆปค๏ผไธค็ซฏ่ฏญไนๅฏน็งฐ
|
|
74
|
+
# ๏ผๅฆๅ gauge ็ '76%'ใstack ็ๅนดไปฝ '2023' ่ฟ็ฑปๆฐๅผๅ็ฑปๅซไผ้ ๆๅ่พน่ฏฏๆฅ๏ผใ
|
|
75
|
+
try:
|
|
76
|
+
cats = list(shape.chart.plots[0].categories)
|
|
77
|
+
texts.extend(str(c).strip() for c in cats
|
|
78
|
+
if str(c).strip() and not NUMERIC_TOKEN.match(str(c).strip()))
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
pages.append(sorted(set(texts))) # ้ๅ่ฏญไนๅป้๏ผB ็ซฏ chart categories ไธ่ช็ปๅพไพๅฏ่ฝ้ๅค๏ผ
|
|
82
|
+
return pages
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _model_chart(sec):
|
|
86
|
+
"""ๅ็ซ ่้กต็ๅพ่กจๅฏน่ฑก๏ผๅฃๅพไธ validate_pptx._section_chart ไธ่ด๏ผใ"""
|
|
87
|
+
st = sec.get('type') or ''
|
|
88
|
+
if st in {'bar', 'donut', 'exhibit', 'halftable'}:
|
|
89
|
+
c = sec.get('chart') or {}
|
|
90
|
+
elif st == 'split':
|
|
91
|
+
c = sec.get('right') or {}
|
|
92
|
+
if (c.get('type') or 'bar') == 'table':
|
|
93
|
+
return None
|
|
94
|
+
else:
|
|
95
|
+
return None
|
|
96
|
+
return c if isinstance(c, dict) and c.get('labels') and c.get('values') else None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _model_values(chart):
|
|
100
|
+
"""ๆจกๅๅฃฐๆ็ๅ
จ้จๆฐๅผ๏ผๅ็ณปๅ values ๆๅค็ณปๅ series[].values๏ผใ"""
|
|
101
|
+
out = []
|
|
102
|
+
if isinstance(chart.get('series'), list) and chart['series']:
|
|
103
|
+
for se in chart['series']:
|
|
104
|
+
out += [v for v in (se.get('values') or []) if isinstance(v, (int, float))]
|
|
105
|
+
else:
|
|
106
|
+
out += [v for v in (chart.get('values') or []) if isinstance(v, (int, float))]
|
|
107
|
+
return out
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def chart_data_verify(prs, model, name):
|
|
111
|
+
"""B ้้ๅ็ chart part ็ๆฐๅผๅฟ
้กป่ฆ็ๆจกๅๅฃฐๆๅผใ
|
|
112
|
+
|
|
113
|
+
ๅชๅใๆจกๅๅผ โ ๅพ่กจๅผใ็ๅ
ๅซๅคๅฎ๏ผไธๅไธฅๆ ผ็ธ็ญโโwaterfall ็็ดฏ่ฎกๅบๅบงใ
|
|
114
|
+
gauge ็่กฅ่ง่ฟ็ฑปๆดพ็็ณปๅๆฏๅๆณ็้ขๅคๆฐๆฎ๏ผไธฅๆ ผ็ธ็ญไผๅถ้ ๅ้ณๆงใ
|
|
115
|
+
ๅฝขๆๅๆข็ฑปๅพ่กจ๏ผwaterfall ๆ -3 ็ปๆ้ซๅบฆ 3 ็ๆฌๆตฎๆก๏ผๆ็ปๅฏนๅผๆฏๅฏนใ
|
|
116
|
+
"""
|
|
117
|
+
issues = []
|
|
118
|
+
secs = [s for s in (model.get('sections') or []) if isinstance(s, dict)]
|
|
119
|
+
first = 3 if model.get('agenda') else 2
|
|
120
|
+
slides = list(prs.slides)
|
|
121
|
+
for i, sec in enumerate(secs):
|
|
122
|
+
chart = _model_chart(sec)
|
|
123
|
+
if chart is None:
|
|
124
|
+
continue
|
|
125
|
+
idx = first + i - 1
|
|
126
|
+
if idx >= len(slides):
|
|
127
|
+
break
|
|
128
|
+
pool = []
|
|
129
|
+
for shape in slides[idx].shapes:
|
|
130
|
+
if not getattr(shape, 'has_chart', False):
|
|
131
|
+
continue
|
|
132
|
+
try:
|
|
133
|
+
for plot in shape.chart.plots:
|
|
134
|
+
for se in plot.series:
|
|
135
|
+
pool += [v for v in se.values if v is not None]
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
if not pool:
|
|
139
|
+
continue # ่ฏฅ้กตๆฏๅฝข็ถ่ฟๅ้้๏ผๆฐๅผ่ตฐๆฐๆฎ่กจ/ๅคๆณจ๏ผๅฆๆ้จ็ฆ
|
|
140
|
+
ctype = str(chart.get('type') or 'bar').lower()
|
|
141
|
+
signless = ctype in SIGNLESS_CHARTS
|
|
142
|
+
if signless:
|
|
143
|
+
pool = [abs(p) for p in pool]
|
|
144
|
+
missing = []
|
|
145
|
+
for v in _model_values(chart):
|
|
146
|
+
target = abs(v) if signless else v
|
|
147
|
+
if not any(abs(target - p) <= max(0.01, abs(target) * 0.001) for p in pool):
|
|
148
|
+
missing.append(v)
|
|
149
|
+
if missing:
|
|
150
|
+
issues.append(f'{name} ็ฌฌ{idx + 1}้กต {ctype} ๅพ่กจๆฐๅผไธๆจกๅไธ็ฌฆ๏ผ'
|
|
151
|
+
f'็ผบ {missing[:5]}๏ผๅพ่กจๅฎ้
ๅซ {sorted(set(pool))[:6]}๏ผ')
|
|
152
|
+
return issues
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def font_sizes(prs):
|
|
156
|
+
"""ๅ
จๆๆๆฌๆกๅบ็ฐ่ฟ็ๅญๅท้ๅ๏ผpt๏ผใ"""
|
|
157
|
+
out = set()
|
|
158
|
+
for slide in prs.slides:
|
|
159
|
+
for sh in slide.shapes:
|
|
160
|
+
if not sh.has_text_frame:
|
|
161
|
+
continue
|
|
162
|
+
for p in sh.text_frame.paragraphs:
|
|
163
|
+
for r in p.runs:
|
|
164
|
+
if r.font.size and r.text.strip():
|
|
165
|
+
out.add(round(r.font.size.pt, 2))
|
|
166
|
+
return out
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def overflow_estimate(prs, name):
|
|
170
|
+
"""ๅฏๅๅผ๏ผๆๆฌไผฐ็ฎๅฎฝ๏ผCJK ๅญ็ฌฆๆ 1emใASCII ๆ emAsciiRatio๏ผvs shape ๅฎฝ ร ๅฏๅฎน่กๆฐใ"""
|
|
171
|
+
issues = []
|
|
172
|
+
for si, slide in enumerate(prs.slides, 1):
|
|
173
|
+
for shape in slide.shapes:
|
|
174
|
+
if not shape.has_text_frame or not shape.width or not shape.height:
|
|
175
|
+
continue
|
|
176
|
+
for para in shape.text_frame.paragraphs:
|
|
177
|
+
line = ''.join(r.text for r in para.runs)
|
|
178
|
+
if not line.strip():
|
|
179
|
+
continue
|
|
180
|
+
sz = None
|
|
181
|
+
for r in para.runs:
|
|
182
|
+
if r.font.size:
|
|
183
|
+
sz = r.font.size.pt
|
|
184
|
+
break
|
|
185
|
+
sz = sz or 12
|
|
186
|
+
emu_w = int(shape.width)
|
|
187
|
+
in_w = emu_w / 914400
|
|
188
|
+
# ๆฏ่กๅฏๅฎนๅญ็ฌฆๅฎฝ๏ผ่ฑๅฏธ๏ผโ em ๆฐ
|
|
189
|
+
est_w = sum(1.0 if ord(c) > 0x2E80 else EM_ASCII_RATIO for c in line) * sz / 72
|
|
190
|
+
lines_avail = max(1, int(int(shape.height) / 914400 / (sz / 72 * LINE_FACTOR)))
|
|
191
|
+
if est_w > in_w * lines_avail * 1.18: # 18% ๅฎนๅทฎ
|
|
192
|
+
issues.append(f'{name} ็ฌฌ{si}้กต "{line[:14]}โฆ" ไผฐ็ฎ{est_w:.1f}in > ๅฎน้{in_w*lines_avail:.1f}in')
|
|
193
|
+
return issues
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def main() -> int:
|
|
197
|
+
if Presentation is None:
|
|
198
|
+
print('python-pptx ๆชๅฎ่ฃ
')
|
|
199
|
+
return 2
|
|
200
|
+
fails = []
|
|
201
|
+
a_files = sorted(A_DIR.glob('*.pptx'))
|
|
202
|
+
print(f'A ้้ไบง็ฉ {len(a_files)} ไธช ยท B ้้ไบง็ฉ {len(sorted(B_DIR.glob("*.pptx")))} ไธช\n')
|
|
203
|
+
all_overflow = []
|
|
204
|
+
|
|
205
|
+
for ap in a_files:
|
|
206
|
+
bp = B_DIR / ap.name
|
|
207
|
+
name = ap.stem
|
|
208
|
+
# โ ๅฏ่งฃๆๆง
|
|
209
|
+
try:
|
|
210
|
+
prs_a = Presentation(str(ap))
|
|
211
|
+
except Exception as e:
|
|
212
|
+
fails.append(f'{name}: A ้้ python-pptx ่งฃๆๅคฑ่ดฅ {e}')
|
|
213
|
+
continue
|
|
214
|
+
if not bp.exists():
|
|
215
|
+
fails.append(f'{name}: B ้้ไบง็ฉ็ผบๅคฑ')
|
|
216
|
+
continue
|
|
217
|
+
try:
|
|
218
|
+
prs_b = Presentation(str(bp))
|
|
219
|
+
except Exception as e:
|
|
220
|
+
fails.append(f'{name}: B ้้ python-pptx ่งฃๆๅคฑ่ดฅ {e}')
|
|
221
|
+
continue
|
|
222
|
+
# โก ้้กตๆๆฌไธ่ดๆง๏ผv9๏ผA/B ไธๅฟ
้ๅญๅ
จ็ญโโA ้ข่งไธบๅฝข็ถ่ฟไผผ๏ผ
|
|
223
|
+
# ่ฆๆฑใๆ ้ข+ไธปๆๆฌ โฅ80% ไบค้่ฆ็ใ๏ผๆ็ปๆชๆญ/ไธฒ้กต๏ผๅไธๅ ๅพ่กจๆ ็ญพๅฝขๆๅทฎ่ฏฏๆ๏ผ
|
|
224
|
+
ta, tb = slide_texts(prs_a), slide_texts(prs_b)
|
|
225
|
+
if len(ta) != len(tb):
|
|
226
|
+
fails.append(f'{name}: ้กตๆฐไธไธ่ด A={len(ta)} B={len(tb)}')
|
|
227
|
+
continue
|
|
228
|
+
min_cov = 0.80
|
|
229
|
+
try:
|
|
230
|
+
min_cov = float((json.loads((ROOT / 'scripts' / 'layout-constants.json')
|
|
231
|
+
.read_text(encoding='utf-8'))
|
|
232
|
+
.get('qualityGates') or {}).get('roundtripMin') or 0.8)
|
|
233
|
+
except Exception:
|
|
234
|
+
pass
|
|
235
|
+
diff_pages = []
|
|
236
|
+
for i, (x, y) in enumerate(zip(ta, tb), 1):
|
|
237
|
+
sx, sy = set(x), set(y)
|
|
238
|
+
if not sx and not sy:
|
|
239
|
+
continue
|
|
240
|
+
inter = sx & sy
|
|
241
|
+
cov = len(inter) / max(1, max(len(sx), len(sy)))
|
|
242
|
+
if cov < min_cov:
|
|
243
|
+
diff_pages.append((i, cov, sorted(sx - sy)[:3], sorted(sy - sx)[:3]))
|
|
244
|
+
if diff_pages:
|
|
245
|
+
for i, cov, only_a, only_b in diff_pages[:3]:
|
|
246
|
+
fails.append(f'{name}: ็ฌฌ{i}้กตๆๆฌ่ฆ็ {cov:.0%} <{min_cov:.0%} '
|
|
247
|
+
f'A็ฌๆ{only_a} B็ฌๆ{only_b}')
|
|
248
|
+
# โข ๆบขๅบๅฏๅๅผ๏ผๅฏนไธค้้้ฝ่ท๏ผ
|
|
249
|
+
all_overflow += overflow_estimate(prs_a, name + '/A')
|
|
250
|
+
all_overflow += overflow_estimate(prs_b, name + '/B')
|
|
251
|
+
# โฃ B ้้ๅ็ๅพ่กจๆฐๅผ โ ๆจกๅ๏ผA ้้ๆฏๅฝข็ถ่ฟๅ๏ผไธไบง chart part๏ผ
|
|
252
|
+
chart_note = ''
|
|
253
|
+
mp = MODEL_DIR / f'{name}.model.json'
|
|
254
|
+
if mp.exists():
|
|
255
|
+
try:
|
|
256
|
+
model = json.loads(mp.read_text(encoding='utf-8'))
|
|
257
|
+
except json.JSONDecodeError as e:
|
|
258
|
+
fails.append(f'{name}: ๆจกๅไธๅฏ่งฃๆ {e}')
|
|
259
|
+
model = None
|
|
260
|
+
if model:
|
|
261
|
+
data_issues = chart_data_verify(prs_b, model, name)
|
|
262
|
+
fails += data_issues
|
|
263
|
+
chart_note = ' ๅพ่กจๆฐๅผ=' + ('PASS' if not data_issues else f'DIFF({len(data_issues)})')
|
|
264
|
+
status = 'PASS' if not diff_pages else f'LOWCOV p{[d[0] for d in diff_pages]}'
|
|
265
|
+
# โค ๅญๅทๆฏไพๅฐบๅๆบ๏ผB ็ๅญๅทๅฟ
้กป้ฝๆฅ่ช A ๅไธๅผ ่กจใ
|
|
266
|
+
# ๅๅไธๆ็ซโโA ๆๅพ่กจๆธฒๆๆๅฝข็ถ๏ผๆ ็ญพๅญๅทไผๅคๅบๆฅ๏ผB ็ๅจ chart part ๅ
๏ผใ
|
|
267
|
+
fa, fb = font_sizes(prs_a), font_sizes(prs_b)
|
|
268
|
+
alien = sorted(fb - fa)
|
|
269
|
+
if alien:
|
|
270
|
+
fails.append(f'{name}: B ้้ๅบ็ฐ A ๆฒกๆ็ๅญๅท {alien}'
|
|
271
|
+
f'๏ผไธค้้ๅบๅ่ตฐ modeTypeScale๏ผmodeSize() ไธ sz() ็้ณๅถ/ๅๆด้กปไธ่ด๏ผ')
|
|
272
|
+
sz_note = ' ๅญๅทๆฏไพๅฐบ=' + ('PASS' if not alien else 'DRIFT')
|
|
273
|
+
print(f'{name}: Aๅฏ่งฃๆโ Bๅฏ่งฃๆโ ้กตๆฐ={len(ta)} ้้กตๆๆฌ={status}{chart_note}{sz_note}')
|
|
274
|
+
|
|
275
|
+
print('\nโโ ๆบขๅบๅฏๅๅผ๏ผcheck-overflow ยท 18% ๅฎนๅทฎ๏ผโโ')
|
|
276
|
+
if all_overflow:
|
|
277
|
+
for i in all_overflow[:20]:
|
|
278
|
+
print(' [WARN]', i)
|
|
279
|
+
else:
|
|
280
|
+
print(' ไธค้้ๅ
จ้จ้่ฟ๏ผๆ ๆๆฌ่ถ
ๅฎน shape๏ผ')
|
|
281
|
+
|
|
282
|
+
print('\n' + '=' * 50)
|
|
283
|
+
if fails:
|
|
284
|
+
print('ไบคๅ้ช่ฏๅคฑ่ดฅ:')
|
|
285
|
+
for f in fails:
|
|
286
|
+
print(' [FAIL]', f)
|
|
287
|
+
return 1
|
|
288
|
+
print('ๅ้้ไบคๅ้ช่ฏ้่ฟ๏ผpython-pptx ็ฌฌไธๆน่ฃๅค็กฎ่ฎค A/B ไบง็ฉๅๅฏ่งฃๆใ'
|
|
289
|
+
'้้กตๆๆฌไธ่ดใๅ็ๅพ่กจๆฐๅผไธๆจกๅไธ่ด')
|
|
290
|
+
return 0
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
if __name__ == '__main__':
|
|
294
|
+
sys.exit(main())
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""TopPPT HTML ยท ่ฟ่ก็ฏๅขๆขๆต๏ผๅ
ฌๅ
ฑๆจกๅ ยท ่ขซ regression / probe_image_export ็ญๅค็จ๏ผ
|
|
4
|
+
|
|
5
|
+
็ปไธ Node / pptxgenjs / python-pptx ็ๆขๆตๅฃๅพ๏ผ้ฟๅ
ๅ่ๆฌๅๅไธไปฝๅฏผ่ดๆผ็งปใ
|
|
6
|
+
**ไธ็ปๅฎไปปไฝๆบๅจ็ๅบๅฎ่ทฏๅพ**๏ผ็ฏๅขๅ้ > PATH > ๅธธ่งๆ็ฎก็ฎๅฝ๏ผๅญๅจๆ็จ๏ผไธๅญๅจไธๅฝฑๅ๏ผใ
|
|
7
|
+
|
|
8
|
+
่งฃๆไผๅ
็บง๏ผ
|
|
9
|
+
Node ๅฏๆง่กๆไปถ
|
|
10
|
+
TOP_PPT_NODE_EXE > NODE_EXE > NODE
|
|
11
|
+
> PATH ไธ็ node > ๅธธ่งๆ็ฎก็ฎๅฝๆๆฐ็
|
|
12
|
+
ๅซ pptxgenjs ็ node_modules
|
|
13
|
+
TOP_PPT_NODE_PATH > NODE_PATH
|
|
14
|
+
> ไปๅบๅ
node_modules > ้็บง็ถ็ฎๅฝ node_modules > ๅธธ่งๆ็ฎก็ฎๅฝ > ๅ
จๅฑ npm root
|
|
15
|
+
python-pptx
|
|
16
|
+
ๅญ่ฟ็จ `import pptx` ๆฏๅฆๆๅ๏ผๅฏ้็ฌฌไธๆน่ฃๅค๏ผๆช่ฃ
ๅ่ฐ็จๆน่ทณ่ฟ๏ผ
|
|
17
|
+
|
|
18
|
+
่ชๆฃ๏ผ
|
|
19
|
+
python scripts/env_probe.py
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import shutil
|
|
25
|
+
import subprocess
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
35
|
+
|
|
36
|
+
NODE_ENV_VARS = ('TOP_PPT_NODE_EXE', 'NODE_EXE', 'NODE')
|
|
37
|
+
NODE_PATH_ENV_VARS = ('TOP_PPT_NODE_PATH', 'NODE_PATH')
|
|
38
|
+
NODE_BASES = (
|
|
39
|
+
Path.home() / '.local' / 'share' / 'node',
|
|
40
|
+
Path.home() / 'AppData' / 'Local' / 'nvs',
|
|
41
|
+
)
|
|
42
|
+
MODULE_BASES = (
|
|
43
|
+
Path.home() / 'node_modules',
|
|
44
|
+
)
|
|
45
|
+
NODE_EXES = ('node.exe', 'bin/node', 'bin/node.exe')
|
|
46
|
+
DEFAULT_PKG = 'pptxgenjs'
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# โโ Node ๅฏๆง่กๆไปถ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
50
|
+
|
|
51
|
+
def node_candidates() -> list[str]:
|
|
52
|
+
"""ๆไผๅ
็บง่ฟๅๅ้ node ๅฏๆง่กๆไปถ๏ผๅป้ใไฟๅบ๏ผใ"""
|
|
53
|
+
cands: list[str] = []
|
|
54
|
+
for var in NODE_ENV_VARS:
|
|
55
|
+
val = os.environ.get(var)
|
|
56
|
+
if val and Path(val).is_file():
|
|
57
|
+
cands.append(val)
|
|
58
|
+
found = shutil.which('node')
|
|
59
|
+
if found:
|
|
60
|
+
cands.append(found)
|
|
61
|
+
for base in NODE_BASES:
|
|
62
|
+
if not base.is_dir():
|
|
63
|
+
continue
|
|
64
|
+
try:
|
|
65
|
+
for ver in sorted((p for p in base.iterdir() if p.is_dir()), reverse=True):
|
|
66
|
+
for exe in NODE_EXES:
|
|
67
|
+
p = ver / exe
|
|
68
|
+
if p.is_file():
|
|
69
|
+
cands.append(str(p))
|
|
70
|
+
break
|
|
71
|
+
except OSError:
|
|
72
|
+
pass
|
|
73
|
+
seen, out = set(), []
|
|
74
|
+
for c in cands:
|
|
75
|
+
if c not in seen:
|
|
76
|
+
seen.add(c)
|
|
77
|
+
out.append(c)
|
|
78
|
+
return out
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def find_node() -> str | None:
|
|
82
|
+
for c in node_candidates():
|
|
83
|
+
if Path(c).is_file():
|
|
84
|
+
return c
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# โโ ๅซ pptxgenjs ็ node_modules โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
89
|
+
|
|
90
|
+
def node_modules_candidates() -> list[str]:
|
|
91
|
+
"""ๆไผๅ
็บง่ฟๅๅฏ่ฝๅซ็ฎๆ ๅ
็ node_modules ็ฎๅฝ๏ผๅป้ใไฟๅบ๏ผใ"""
|
|
92
|
+
cands: list[str] = []
|
|
93
|
+
for var in NODE_PATH_ENV_VARS:
|
|
94
|
+
val = os.environ.get(var)
|
|
95
|
+
if val:
|
|
96
|
+
cands.extend(p for p in val.split(os.pathsep) if p)
|
|
97
|
+
cands.append(str(ROOT / 'node_modules'))
|
|
98
|
+
cands.extend(str(p / 'node_modules') for p in list(ROOT.parents)[:3]) # monorepo ๅบๆฏ
|
|
99
|
+
cands.extend(str(b) for b in MODULE_BASES)
|
|
100
|
+
try:
|
|
101
|
+
npm_root = subprocess.run(['npm', 'root', '-g'], capture_output=True, text=True,
|
|
102
|
+
encoding='utf-8', timeout=30).stdout.strip()
|
|
103
|
+
if npm_root:
|
|
104
|
+
cands.append(npm_root)
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
seen, out = set(), []
|
|
108
|
+
for c in cands:
|
|
109
|
+
if c and c not in seen:
|
|
110
|
+
seen.add(c)
|
|
111
|
+
out.append(c)
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def find_node_modules(pkg: str = DEFAULT_PKG) -> str | None:
|
|
116
|
+
for c in node_modules_candidates():
|
|
117
|
+
if (Path(c) / pkg).is_dir():
|
|
118
|
+
return c
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# โโ ็ปๅไธๆ็คบ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
123
|
+
|
|
124
|
+
def node_env(node_modules: str | None = None) -> dict[str, str]:
|
|
125
|
+
"""่ฟๅๅฏ็ดๆฅไผ ็ป subprocess ็็ฏๅข๏ผๆ NODE_PATH ๆๅฐ่งฃๆๅบ็ node_modules๏ผใ"""
|
|
126
|
+
env = dict(os.environ)
|
|
127
|
+
if node_modules:
|
|
128
|
+
env['NODE_PATH'] = node_modules
|
|
129
|
+
return env
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def has_python_pptx(python_exe: str | None = None) -> bool:
|
|
133
|
+
exe = python_exe or sys.executable
|
|
134
|
+
try:
|
|
135
|
+
return subprocess.run([exe, '-c', 'import pptx'], capture_output=True).returncode == 0
|
|
136
|
+
except OSError:
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
NODE_HINT = ('ๆชๆพๅฐ Node ๅฏๆง่กๆไปถ๏ผPPTX ็ฒพๅฏผไธๅๅฝๅฟ
้๏ผใๅฎ่ฃ
Node ๅ้่ฏ๏ผ'
|
|
141
|
+
'ๆ็จ็ฏๅขๅ้ TOP_PPT_NODE_EXE ๆๅฎ่ทฏๅพใ')
|
|
142
|
+
NODE_MODULES_HINT = ('ๆชๆพๅฐๅซ pptxgenjs ็ node_modules๏ผPPTX ็ๆๅฟ
้๏ผใ'
|
|
143
|
+
'ๅจๆ่ฝ็ฎๅฝๆง่ก `npm install`๏ผไพ package.json๏ผ๏ผ'
|
|
144
|
+
'ๆ็จ็ฏๅขๅ้ TOP_PPT_NODE_PATH ๆๅฎ node_modules ่ทฏๅพใ')
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def resolve(pkg: str = DEFAULT_PKG) -> tuple[str | None, str | None]:
|
|
148
|
+
"""ไธๆฌกๆง่ฟๅ (node, node_modules)ใ"""
|
|
149
|
+
return find_node(), find_node_modules(pkg)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == '__main__':
|
|
153
|
+
node, mods = resolve()
|
|
154
|
+
print('็ฏๅขๆขๆต๏ผTopPPT HTML๏ผ')
|
|
155
|
+
print(f' node : {node or "ๆชๆพๅฐ"}')
|
|
156
|
+
print(f' node_modules : {mods or "ๆชๆพๅฐ๏ผ็ผบ pptxgenjs๏ผ"}')
|
|
157
|
+
print(f' python-pptx : {"ๅฏ็จ" if has_python_pptx() else "ๆชๅฎ่ฃ
๏ผๅฏ้๏ผไบคๅ่ฃๅคไผ่ทณ่ฟ๏ผ"}')
|
|
158
|
+
sys.exit(0 if (node and mods) else 1)
|