@ohos-ports/slides-grab 1.5.0-beta.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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README-ko.md +292 -0
  3. package/README.md +301 -0
  4. package/bin/ohos-postinstall.cjs +118 -0
  5. package/bin/ppt-agent.js +620 -0
  6. package/convert.cjs +20 -0
  7. package/native/@img/sharp-libvips-openharmony-arm64/lib/glib-2.0/include/glibconfig.h +220 -0
  8. package/native/@img/sharp-libvips-openharmony-arm64/lib/index.js +1 -0
  9. package/native/@img/sharp-libvips-openharmony-arm64/lib/libexpat.so +0 -0
  10. package/native/@img/sharp-libvips-openharmony-arm64/lib/libffi.so +0 -0
  11. package/native/@img/sharp-libvips-openharmony-arm64/lib/libjpeg.so +0 -0
  12. package/native/@img/sharp-libvips-openharmony-arm64/lib/libpcre2-8.so.0 +0 -0
  13. package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips-cpp.so.42.20.3 +0 -0
  14. package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips.so.42.20.3 +0 -0
  15. package/native/@img/sharp-libvips-openharmony-arm64/package.json +13 -0
  16. package/native/@img/sharp-openharmony-arm64/lib/sharp-openharmony-arm64.node +0 -0
  17. package/native/@img/sharp-openharmony-arm64/package.json +16 -0
  18. package/package.json +95 -0
  19. package/runtimes/claude-code/agents/design-critic-agent.md +23 -0
  20. package/runtimes/codex/agents/slides-grab-design-critic.md +22 -0
  21. package/scripts/build-viewer.js +444 -0
  22. package/scripts/design-gate.js +258 -0
  23. package/scripts/download-video.js +213 -0
  24. package/scripts/editor-server.js +1167 -0
  25. package/scripts/figma-export.js +169 -0
  26. package/scripts/generate-image.js +116 -0
  27. package/scripts/generate-images.js +164 -0
  28. package/scripts/html2pdf.js +822 -0
  29. package/scripts/html2png.js +246 -0
  30. package/scripts/html2pptx.js +162 -0
  31. package/scripts/import-template.js +86 -0
  32. package/scripts/install-runtime.js +216 -0
  33. package/scripts/render-tldraw.js +44 -0
  34. package/scripts/validate-slides.js +221 -0
  35. package/skills/slides-grab/SKILL.md +65 -0
  36. package/skills/slides-grab/references/presentation-workflow-reference.md +61 -0
  37. package/skills/slides-grab-card-news/SKILL.md +35 -0
  38. package/skills/slides-grab-design/SKILL.md +88 -0
  39. package/skills/slides-grab-design/references/beautiful-slide-defaults.md +88 -0
  40. package/skills/slides-grab-design/references/design-gate.md +349 -0
  41. package/skills/slides-grab-design/references/design-rules.md +76 -0
  42. package/skills/slides-grab-design/references/design-system-full.md +568 -0
  43. package/skills/slides-grab-design/references/detailed-design-rules.md +70 -0
  44. package/skills/slides-grab-export/SKILL.md +55 -0
  45. package/skills/slides-grab-export/references/export-rules.md +27 -0
  46. package/skills/slides-grab-export/references/html2pptx.md +627 -0
  47. package/skills/slides-grab-export/references/ooxml.md +427 -0
  48. package/skills/slides-grab-export/references/pptx-skill-reference.md +189 -0
  49. package/skills/slides-grab-html/SKILL.md +67 -0
  50. package/skills/slides-grab-image/SKILL.md +82 -0
  51. package/skills/slides-grab-plan/SKILL.md +68 -0
  52. package/skills/slides-grab-plan/references/design-md-to-slides-conversion.md +135 -0
  53. package/skills/slides-grab-plan/references/outline-format.md +47 -0
  54. package/skills/slides-grab-plan/references/plan-workflow-reference.md +140 -0
  55. package/src/codex-imagen.js +182 -0
  56. package/src/design-diversity-data.js +6932 -0
  57. package/src/design-gate-report.js +244 -0
  58. package/src/design-gate-state.js +329 -0
  59. package/src/design-import.js +164 -0
  60. package/src/design-md-parser.js +415 -0
  61. package/src/design-styles-data.js +1928 -0
  62. package/src/design-styles.js +209 -0
  63. package/src/editor/codex-edit.js +584 -0
  64. package/src/editor/edit-subprocess.js +170 -0
  65. package/src/editor/editor-codex-prompt.md +50 -0
  66. package/src/editor/editor.html +1785 -0
  67. package/src/editor/js/editor-bbox.js +332 -0
  68. package/src/editor/js/editor-chat.js +56 -0
  69. package/src/editor/js/editor-direct-edit.js +238 -0
  70. package/src/editor/js/editor-dom.js +59 -0
  71. package/src/editor/js/editor-init.js +405 -0
  72. package/src/editor/js/editor-navigation.js +54 -0
  73. package/src/editor/js/editor-select.js +558 -0
  74. package/src/editor/js/editor-send.js +175 -0
  75. package/src/editor/js/editor-sse.js +163 -0
  76. package/src/editor/js/editor-state.js +41 -0
  77. package/src/editor/js/editor-type.js +71 -0
  78. package/src/editor/js/editor-utils.js +167 -0
  79. package/src/editor/js/model-registry.js +37 -0
  80. package/src/editor/screenshot.js +82 -0
  81. package/src/export-resolution.cjs +68 -0
  82. package/src/figma.js +71 -0
  83. package/src/html2pptx-scale.cjs +120 -0
  84. package/src/html2pptx.cjs +1262 -0
  85. package/src/image-contract.js +329 -0
  86. package/src/image-native.js +468 -0
  87. package/src/nano-banana.js +841 -0
  88. package/src/ohos-browser-bridge.cjs +272 -0
  89. package/src/pptx-raster-export.cjs +299 -0
  90. package/src/resolve.js +110 -0
  91. package/src/slide-mode.cjs +72 -0
  92. package/src/template-fidelity.js +267 -0
  93. package/src/template-import.js +505 -0
  94. package/src/template-layout.js +261 -0
  95. package/src/template-pack.js +255 -0
  96. package/src/tldraw/render.js +473 -0
  97. package/src/validation/cli.js +120 -0
  98. package/src/validation/core.js +1023 -0
  99. package/templates/chart.html +121 -0
  100. package/templates/closing.html +54 -0
  101. package/templates/content.html +50 -0
  102. package/templates/contents.html +60 -0
  103. package/templates/cover.html +64 -0
  104. package/templates/custom/.gitkeep +0 -0
  105. package/templates/custom/README.md +7 -0
  106. package/templates/design-styles/README.md +20 -0
  107. package/templates/design-styles/preview.html +4438 -0
  108. package/templates/diagram-tldraw.html +56 -0
  109. package/templates/diagram.html +98 -0
  110. package/templates/quote.html +31 -0
  111. package/templates/section-divider.html +43 -0
  112. package/templates/split-layout.html +43 -0
  113. package/templates/statistics.html +55 -0
  114. package/templates/team.html +49 -0
  115. package/templates/timeline.html +59 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * OHOS Browser Bridge — Launch system Chromium browser via hdc + connect via CDP
3
+ *
4
+ * Replaces playwright's chromium.launch() on HarmonyOS.
5
+ * Supports: Haitai browser (Chromium M132), Chrome 鸿蒙版, custom browsers.
6
+ *
7
+ * Based on browser-auto-migration (gitcode.com/zhenghongfei):
8
+ * 1. hdc shell aa force-stop (clean stale process)
9
+ * 2. hdc shell aa start --ps cmdArgs "--remote-debugging-port=PORT ..."
10
+ * 3. Wait for CDP endpoint (HTTP poll /json/version)
11
+ * 4. chromium.connectOverCDP(wsEndpoint)
12
+ */
13
+
14
+ const { execFileSync } = require('child_process');
15
+ const http = require('http');
16
+
17
+ // ─── Browser registry ────────────────────────────────────────────────────
18
+ // Supports multiple Chromium-based browsers on HarmonyOS
19
+ const CHANNEL_MAP = {
20
+ chrome: { pkg: 'com.haitai.htbrowser', ability: 'EntryAbility' },
21
+ 'chrome-beta': { pkg: 'com.huawei.ohos_chromium', ability: 'BrowserAbility' },
22
+ };
23
+
24
+ const ENV_MAP = {
25
+ haitai: { pkg: 'com.haitai.htbrowser', ability: 'EntryAbility' },
26
+ chrome: { pkg: 'com.huawei.ohos_chromium', ability: 'BrowserAbility' },
27
+ };
28
+
29
+ const DEFAULT_BROWSER = { pkg: 'com.haitai.htbrowser', ability: 'EntryAbility' };
30
+
31
+ function resolveBrowser(options = {}) {
32
+ // 1. Explicit bundle name
33
+ if (options.harmonyBundleName) {
34
+ return { pkg: options.harmonyBundleName, ability: options.harmonyAbilityName || 'EntryAbility' };
35
+ }
36
+ // 2. Channel mapping (chrome / chrome-beta)
37
+ if (options.channel && CHANNEL_MAP[options.channel]) {
38
+ return CHANNEL_MAP[options.channel];
39
+ }
40
+ // 3. Environment variable (HARMONY_BROWSER=haitai|chrome)
41
+ const env = process.env.HARMONY_BROWSER;
42
+ if (env && ENV_MAP[env]) {
43
+ return ENV_MAP[env];
44
+ }
45
+ // 4. Default
46
+ return DEFAULT_BROWSER;
47
+ }
48
+
49
+ // ─── Chromium launch flags ──────────────────────────────────────────────
50
+ const BOOL_FLAGS = [
51
+ 'disable-extensions', 'disable-default-apps', 'no-first-run',
52
+ 'no-default-browser-check', 'disable-background-networking',
53
+ 'disable-client-side-phishing-detection', 'disable-popup-blocking',
54
+ 'disable-prompt-on-repost', 'disable-breakpad', 'disable-hang-monitor',
55
+ 'disable-ipc-flooding-protection', 'metrics-recording-only', 'disable-sync',
56
+ 'disable-search-engine-choice-screen', 'disable-background-timer-throttling',
57
+ 'disable-renderer-backgrounding', 'disable-backgrounding-occluded-windows',
58
+ 'disable-field-trial-config', 'disable-component-update', 'disable-dev-shm-usage',
59
+ 'no-service-autorun', 'disable-infobars', 'allow-file-access-from-files',
60
+ 'enable-automation', 'allow-pre-commit-input',
61
+ ];
62
+
63
+ const DISABLED_FEATURES = [
64
+ 'AvoidUnnecessaryBeforeUnloadCheckSync', 'DestroyProfileOnBrowserClose',
65
+ 'DialMediaRouteProvider', 'GlobalMediaControls', 'HttpsUpgrades',
66
+ 'LensOverlay', 'MediaRouter', 'PaintHolding',
67
+ 'ThirdPartyStoragePartitioning', 'Translate', 'AutoDeElevate',
68
+ 'RenderDocument', 'OptimizationHints',
69
+ ].join(',');
70
+
71
+ // ─── hdc ────────────────────────────────────────────────────────────────
72
+ function getHdcPath() {
73
+ // Try standard HarmonyOS path first, then env var, then PATH
74
+ try { require('fs').accessSync('/data/service/hnp/bin/hdc'); return '/data/service/hnp/bin/hdc'; } catch {}
75
+ return process.env.HDC_PATH || 'hdc';
76
+ }
77
+
78
+ function execHdc(args, timeout = 30000) {
79
+ return execFileSync(getHdcPath(), args, { timeout, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
80
+ }
81
+
82
+ function ensureDeviceConnected() {
83
+ const targets = execHdc(['list', 'targets']).split('\n').map(s => s.trim()).filter(Boolean);
84
+ if (targets.length > 0 && targets[0] !== '[Empty]') return;
85
+ try {
86
+ const port = parseInt(execFileSync('param', ['get', 'persist.hdc.port'], { timeout: 3000, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(), 10);
87
+ if (port > 0 && execHdc(['tconn', `127.0.0.1:${port}`], 10000).includes('Connect OK')) return;
88
+ } catch {}
89
+ throw new Error('No HarmonyOS device connected. Enable Developer Options + Wireless Debugging.');
90
+ }
91
+
92
+ // ─── CDP ────────────────────────────────────────────────────────────────
93
+ function buildCmdArgs(port, options = {}) {
94
+ const args = [
95
+ `--remote-debugging-port=${port}`,
96
+ `--remote-allow-origins=http://127.0.0.1:${port}`,
97
+ ];
98
+ for (const f of BOOL_FLAGS) args.push(`--${f}`);
99
+ args.push('--password-store=basic', '--use-mock-keychain', '--force-color-profile=srgb');
100
+ args.push(`--disable-features=${DISABLED_FEATURES}`);
101
+ if (options.userDataDir) args.push(`--user-data-dir=${options.userDataDir}`);
102
+ return args.join(' ');
103
+ }
104
+
105
+ function tryCdpEndpoint(url) {
106
+ return new Promise((resolve, reject) => {
107
+ const req = http.get(url, (res) => {
108
+ let d = '';
109
+ res.on('data', (c) => d += c);
110
+ res.on('end', () => {
111
+ if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`));
112
+ try {
113
+ const j = JSON.parse(d);
114
+ j.webSocketDebuggerUrl ? resolve(j.webSocketDebuggerUrl) : reject(new Error('No webSocketDebuggerUrl'));
115
+ } catch { reject(new Error('Parse failed')); }
116
+ });
117
+ });
118
+ req.on('error', reject);
119
+ req.setTimeout(2000, () => { req.destroy(); reject(new Error('Timeout')); });
120
+ });
121
+ }
122
+
123
+ async function waitForCdpEndpoint(port, timeoutMs = 30000) {
124
+ const start = Date.now();
125
+ while (Date.now() - start < timeoutMs) {
126
+ try {
127
+ return await tryCdpEndpoint(`http://127.0.0.1:${port}/json/version`);
128
+ } catch {
129
+ try {
130
+ return await tryCdpEndpoint(`http://[::1]:${port}/json/version`);
131
+ } catch {
132
+ await new Promise(r => setTimeout(r, 500));
133
+ }
134
+ }
135
+ }
136
+ throw new Error(`CDP endpoint timeout on port ${port} after ${timeoutMs}ms`);
137
+ }
138
+
139
+ // ─── launchBrowserViaCdp ────────────────────────────────────────────────
140
+ // Cached browser instance (system browsers typically support single instance)
141
+ let _cachedBrowser = null;
142
+ let _launchPromise = null;
143
+
144
+ async function launchBrowserViaCdp(options = {}) {
145
+ if (_cachedBrowser) return _cachedBrowser;
146
+ if (_launchPromise) return _launchPromise;
147
+
148
+ _launchPromise = (async () => {
149
+ const { pkg, ability } = resolveBrowser(options);
150
+ const port = options.cdpPort || options.debuggingPort || 9333;
151
+
152
+ // 1. Connect to device
153
+ ensureDeviceConnected();
154
+
155
+ // 2. Check browser installed
156
+ if (!execHdc(['shell', 'bm', 'dump', '-a']).includes(pkg)) {
157
+ throw new Error(
158
+ `Browser "${pkg}" is not installed. ` +
159
+ `Install Haitai browser, or set HARMONY_BROWSER=chrome for Chrome 鸿蒙版, ` +
160
+ `or pass options.harmonyBundleName.`
161
+ );
162
+ }
163
+
164
+ // 3. Stop stale browser process
165
+ try { execHdc(['shell', 'aa', 'force-stop', pkg]); } catch {}
166
+ await new Promise(r => setTimeout(r, 2000));
167
+
168
+ // 4. Launch browser with CDP port
169
+ const cmdArgs = buildCmdArgs(port, options);
170
+ execHdc(['shell', 'aa', 'start', '-b', pkg, '-a', ability, '--ps', 'cmdArgs', cmdArgs]);
171
+
172
+ // 5. Wait for CDP endpoint
173
+ const wsEndpoint = await waitForCdpEndpoint(port);
174
+
175
+ // 6. Connect via Playwright connectOverCDP
176
+ const { chromium } = require('playwright');
177
+ const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
178
+
179
+ _cachedBrowser = browser;
180
+
181
+ // Override close to keep browser alive for reuse (single instance limitation)
182
+ browser.close = async () => {
183
+ // Don't actually close — keep cached for reuse. Process exit will clean up.
184
+ };
185
+
186
+ // Real cleanup on process exit
187
+ process.on('exit', () => {
188
+ try { execHdc(['shell', 'aa', 'force-stop', pkg]); } catch {}
189
+ });
190
+
191
+ return browser;
192
+ })();
193
+
194
+ try {
195
+ return await _launchPromise;
196
+ } finally {
197
+ _launchPromise = null;
198
+ }
199
+ }
200
+
201
+ // ─── loadPageUrl ─────────────────────────────────────────────────────────
202
+ /**
203
+ * Load an HTML file into a page — uses setContent on HarmonyOS (file:// not accessible)
204
+ * Drop-in replacement for: await page.goto('file://' + filePath)
205
+ */
206
+ async function loadPageUrl(page, url, options = {}) {
207
+ if (process.platform === 'openharmony' && url.startsWith('file://')) {
208
+ const filePath = url.replace(/^file:\/\//, '');
209
+ const fs = require('fs');
210
+ const html = fs.readFileSync(decodeURIComponent(filePath), 'utf-8');
211
+ await page.setContent(html, { waitUntil: options.waitUntil || 'load', timeout: options.timeout || 30000 });
212
+ return;
213
+ }
214
+ await page.goto(url, options);
215
+ }
216
+
217
+ // ─── Patch playwright-core (postinstall) ────────────────────────────────
218
+ /**
219
+ * Patch playwright-core to support openharmony platform.
220
+ * Called from bin/ohos-postinstall.cjs during npm install.
221
+ */
222
+ function patchPlaywrightCore() {
223
+ const fs = require('fs');
224
+ const path = require('path');
225
+
226
+ let pcRoot;
227
+ try {
228
+ pcRoot = path.dirname(require.resolve('playwright-core/package.json'));
229
+ } catch {
230
+ console.log(' [ohos-bridge] playwright-core not found, skipping patch');
231
+ return false;
232
+ }
233
+
234
+ const targetFile = path.join(pcRoot, 'lib', 'server', 'registry', 'index.js');
235
+ if (!fs.existsSync(targetFile)) {
236
+ console.log(' [ohos-bridge] registry/index.js not found, skipping patch');
237
+ return false;
238
+ }
239
+
240
+ let content = fs.readFileSync(targetFile, 'utf-8');
241
+ const MARKER = '/* @ohos-ports/slides-grab-patched */';
242
+
243
+ if (content.includes(MARKER)) {
244
+ console.log(' [ohos-bridge] Already patched');
245
+ return true;
246
+ }
247
+
248
+ // Patch: add openharmony to platform check
249
+ const oldCode = `else if (process.platform === "win32")
250
+ cacheDirectory = process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local");
251
+ else
252
+ throw new Error("Unsupported platform: " + process.platform);`;
253
+
254
+ const newCode = `else if (process.platform === "win32")
255
+ cacheDirectory = process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local");
256
+ else if (process.platform === "openharmony")
257
+ cacheDirectory = process.env.XDG_CACHE_HOME || import_path.default.join(import_os.default.homedir(), ".cache");
258
+ else
259
+ throw new Error("Unsupported platform: " + process.platform);`;
260
+
261
+ if (content.includes(oldCode)) {
262
+ content = MARKER + '\n' + content.replace(oldCode, newCode);
263
+ fs.writeFileSync(targetFile, content, 'utf-8');
264
+ console.log(' [ohos-bridge] ✓ Patched playwright-core registry/index.js');
265
+ return true;
266
+ }
267
+
268
+ console.log(' [ohos-bridge] ⚠ Could not find target code to patch (playwright version mismatch?)');
269
+ return false;
270
+ }
271
+
272
+ module.exports = { launchBrowserViaCdp, loadPageUrl, patchPlaywrightCore, resolveBrowser };
@@ -0,0 +1,299 @@
1
+ const PptxGenJS = require('pptxgenjs');
2
+ // OHOS: Use system Haitai browser via CDP bridge instead of standalone Chromium
3
+ const { launchBrowserViaCdp, loadPageUrl } = require('./ohos-browser-bridge.cjs');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const sharp = require('sharp');
7
+
8
+ const {
9
+ getResolutionChoices,
10
+ getResolutionSize,
11
+ normalizeResolutionPreset,
12
+ } = require('./export-resolution.cjs');
13
+ const {
14
+ DEFAULT_SLIDE_MODE,
15
+ getSlideModeChoices,
16
+ getSlideModeConfig,
17
+ normalizeSlideMode,
18
+ } = require('./slide-mode.cjs');
19
+
20
+ const DEFAULT_SLIDES_DIR = 'slides';
21
+ const DEFAULT_OUTPUT = 'output.pptx';
22
+ const DEFAULT_RESOLUTION = '2160p';
23
+ const DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR = 2;
24
+ const TARGET_RASTER_DPI = 150;
25
+ const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
26
+
27
+ function normalizeDimension(value, fallback) {
28
+ if (!Number.isFinite(value) || value <= 0) {
29
+ return fallback;
30
+ }
31
+ return Math.max(1, Math.round(value));
32
+ }
33
+
34
+ function buildPageOptions(resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
35
+ const { framePx } = getSlideModeConfig(slideMode);
36
+ const targetResolution = getResolutionSize(resolution, slideMode);
37
+ return {
38
+ viewport: {
39
+ width: framePx.width,
40
+ height: framePx.height,
41
+ },
42
+ deviceScaleFactor: targetResolution
43
+ ? targetResolution.height / framePx.height
44
+ : DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR,
45
+ };
46
+ }
47
+
48
+ function getTargetRasterSize(resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
49
+ const targetResolution = getResolutionSize(resolution, slideMode);
50
+ if (targetResolution) {
51
+ return targetResolution;
52
+ }
53
+
54
+ const { pptxSizeIn } = getSlideModeConfig(slideMode);
55
+ return {
56
+ width: Math.round(pptxSizeIn.width * TARGET_RASTER_DPI),
57
+ height: Math.round(pptxSizeIn.height * TARGET_RASTER_DPI),
58
+ };
59
+ }
60
+
61
+ function toSlideOrder(fileName) {
62
+ const match = fileName.match(/\d+/);
63
+ return match ? Number.parseInt(match[0], 10) : Number.POSITIVE_INFINITY;
64
+ }
65
+
66
+ function sortSlideFiles(a, b) {
67
+ const orderA = toSlideOrder(a);
68
+ const orderB = toSlideOrder(b);
69
+ if (orderA !== orderB) {
70
+ return orderA - orderB;
71
+ }
72
+ return a.localeCompare(b);
73
+ }
74
+
75
+ function getHtmlSlides(slidesDir) {
76
+ if (!fs.existsSync(slidesDir)) {
77
+ throw new Error(`Slides directory not found: ${slidesDir}`);
78
+ }
79
+
80
+ const files = fs.readdirSync(slidesDir)
81
+ .filter((fileName) => SLIDE_FILE_PATTERN.test(fileName))
82
+ .sort(sortSlideFiles);
83
+
84
+ if (files.length === 0) {
85
+ throw new Error(`No slide-*.html files found in ${slidesDir}`);
86
+ }
87
+
88
+ return files;
89
+ }
90
+
91
+ function printUsage() {
92
+ process.stdout.write(
93
+ [
94
+ 'Usage: node convert.cjs [options]',
95
+ '',
96
+ 'Options:',
97
+ ` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
98
+ ` --output <path> Output pptx path (default: ${DEFAULT_OUTPUT})`,
99
+ ` --mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
100
+ ` --resolution <preset> Raster size preset: ${getResolutionChoices().join('|')}|4k (default: ${DEFAULT_RESOLUTION})`,
101
+ ' -h, --help Show this help message',
102
+ ].join('\n'),
103
+ );
104
+ process.stdout.write('\n');
105
+ }
106
+
107
+ function readOptionValue(args, index, optionName) {
108
+ const next = args[index + 1];
109
+ if (!next || next.startsWith('-')) {
110
+ throw new Error(`Missing value for ${optionName}.`);
111
+ }
112
+ return next;
113
+ }
114
+
115
+ function parseArgs(args) {
116
+ const options = {
117
+ slidesDir: DEFAULT_SLIDES_DIR,
118
+ output: DEFAULT_OUTPUT,
119
+ mode: DEFAULT_SLIDE_MODE,
120
+ resolution: DEFAULT_RESOLUTION,
121
+ help: false,
122
+ };
123
+
124
+ for (let i = 0; i < args.length; i += 1) {
125
+ const arg = args[i];
126
+ if (arg === '-h' || arg === '--help') {
127
+ options.help = true;
128
+ continue;
129
+ }
130
+
131
+ if (arg === '--slides-dir') {
132
+ options.slidesDir = readOptionValue(args, i, '--slides-dir');
133
+ i += 1;
134
+ continue;
135
+ }
136
+
137
+ if (arg.startsWith('--slides-dir=')) {
138
+ options.slidesDir = arg.slice('--slides-dir='.length);
139
+ continue;
140
+ }
141
+
142
+ if (arg === '--output') {
143
+ options.output = readOptionValue(args, i, '--output');
144
+ i += 1;
145
+ continue;
146
+ }
147
+
148
+ if (arg.startsWith('--output=')) {
149
+ options.output = arg.slice('--output='.length);
150
+ continue;
151
+ }
152
+
153
+ if (arg === '--mode') {
154
+ options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
155
+ i += 1;
156
+ continue;
157
+ }
158
+
159
+ if (arg.startsWith('--mode=')) {
160
+ options.mode = normalizeSlideMode(arg.slice('--mode='.length));
161
+ continue;
162
+ }
163
+
164
+ if (arg === '--resolution') {
165
+ options.resolution = normalizeResolutionPreset(readOptionValue(args, i, '--resolution'));
166
+ i += 1;
167
+ continue;
168
+ }
169
+
170
+ if (arg.startsWith('--resolution=')) {
171
+ options.resolution = normalizeResolutionPreset(arg.slice('--resolution='.length));
172
+ continue;
173
+ }
174
+
175
+ throw new Error(`Unknown option: ${arg}`);
176
+ }
177
+
178
+ if (typeof options.slidesDir !== 'string' || options.slidesDir.trim() === '') {
179
+ throw new Error('--slides-dir must be a non-empty string.');
180
+ }
181
+
182
+ if (typeof options.output !== 'string' || options.output.trim() === '') {
183
+ throw new Error('--output must be a non-empty string.');
184
+ }
185
+
186
+ options.slidesDir = options.slidesDir.trim();
187
+ options.output = options.output.trim();
188
+ options.mode = normalizeSlideMode(options.mode);
189
+ options.resolution = normalizeResolutionPreset(options.resolution);
190
+ return options;
191
+ }
192
+
193
+ async function convertSlide(htmlFile, pres, browser, options = {}) {
194
+ const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile);
195
+ const slideMode = normalizeSlideMode(options.mode || DEFAULT_SLIDE_MODE);
196
+ const fallbackSize = getSlideModeConfig(slideMode).framePx;
197
+
198
+ const page = await browser.newPage(buildPageOptions(options.resolution, slideMode));
199
+ await page.goto(`file://${filePath}`);
200
+
201
+ const bodyDimensions = await page.evaluate(() => {
202
+ const body = document.body;
203
+ const style = window.getComputedStyle(body);
204
+ return {
205
+ width: parseFloat(style.width),
206
+ height: parseFloat(style.height),
207
+ };
208
+ });
209
+
210
+ await page.setViewportSize({
211
+ width: normalizeDimension(bodyDimensions.width, fallbackSize.width),
212
+ height: normalizeDimension(bodyDimensions.height, fallbackSize.height),
213
+ });
214
+
215
+ const screenshot = await page.screenshot({ type: 'png' });
216
+ await page.close();
217
+
218
+ const targetSize = getTargetRasterSize(options.resolution, slideMode);
219
+
220
+ const resized = await sharp(screenshot)
221
+ .resize(targetSize.width, targetSize.height, { fit: 'fill' })
222
+ .png()
223
+ .toBuffer();
224
+
225
+ const tmpPath = path.join(process.env.TMPDIR || '/tmp', `slide-${Date.now()}-${Math.random().toString(36).slice(2)}.png`);
226
+ fs.writeFileSync(tmpPath, resized);
227
+
228
+ const slide = pres.addSlide();
229
+ slide.addImage({
230
+ path: tmpPath,
231
+ x: 0,
232
+ y: 0,
233
+ w: '100%',
234
+ h: '100%',
235
+ });
236
+
237
+ return tmpPath;
238
+ }
239
+
240
+ async function main(argv = process.argv.slice(2)) {
241
+ const options = parseArgs(argv);
242
+ if (options.help) {
243
+ printUsage();
244
+ return;
245
+ }
246
+
247
+ const pres = new PptxGenJS();
248
+ const { pptxSizeIn } = getSlideModeConfig(options.mode);
249
+ pres.defineLayout({
250
+ name: 'SLIDES_GRAB_DYNAMIC',
251
+ width: pptxSizeIn.width,
252
+ height: pptxSizeIn.height,
253
+ });
254
+ pres.layout = 'SLIDES_GRAB_DYNAMIC';
255
+
256
+ const slidesDir = path.resolve(process.cwd(), options.slidesDir);
257
+ const { ensureSlidesPassValidation } = await import('../scripts/validate-slides.js');
258
+ await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PPTX export', slideMode: options.mode });
259
+ const files = getHtmlSlides(slidesDir);
260
+
261
+ console.log(`Converting ${files.length} slides...`);
262
+
263
+ const browser = process.platform === 'openharmony'
264
+ ? await launchBrowserViaCdp()
265
+ : await require('playwright').chromium.launch();
266
+ const tmpFiles = [];
267
+
268
+ for (const file of files) {
269
+ const filePath = path.join(slidesDir, file);
270
+ console.log(` Processing: ${file}`);
271
+ try {
272
+ const tmpPath = await convertSlide(filePath, pres, browser, { mode: options.mode, resolution: options.resolution });
273
+ tmpFiles.push(tmpPath);
274
+ console.log(` ✓ ${file} done`);
275
+ } catch (error) {
276
+ console.error(` ✗ ${file} error: ${error.message}`);
277
+ }
278
+ }
279
+
280
+ await browser.close();
281
+
282
+ const outputFile = path.resolve(process.cwd(), options.output);
283
+ await pres.writeFile({ fileName: outputFile });
284
+ console.log(`\nSaved: ${outputFile}`);
285
+
286
+ for (const filePath of tmpFiles) {
287
+ try {
288
+ fs.unlinkSync(filePath);
289
+ } catch {}
290
+ }
291
+ }
292
+
293
+ module.exports = {
294
+ buildPageOptions,
295
+ getHtmlSlides,
296
+ getTargetRasterSize,
297
+ main,
298
+ parseArgs,
299
+ };
package/src/resolve.js ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Path resolution for slides-grab.
3
+ *
4
+ * Resolution order:
5
+ * 1. Local (user's CWD) — per-project overrides
6
+ * 2. Package root — built-in defaults
7
+ *
8
+ * slides directory, slide-outline.md → always local (CWD)
9
+ * templates/ → local first, package fallback
10
+ * scripts/ → always package
11
+ */
12
+
13
+ import { existsSync, readdirSync } from 'node:fs';
14
+ import { dirname, join, resolve } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url));
18
+ const PACKAGE_ROOT = resolve(__dirname, '..');
19
+
20
+ /**
21
+ * Get the package root directory (where slides-grab is installed).
22
+ */
23
+ export function getPackageRoot() {
24
+ return PACKAGE_ROOT;
25
+ }
26
+
27
+ /**
28
+ * Get the user's working directory (where slides are created).
29
+ */
30
+ export function getCwd() {
31
+ return process.cwd();
32
+ }
33
+
34
+ /**
35
+ * Resolve the slides directory in CWD.
36
+ * @param {string} [slidesDir='slides'] - relative or absolute slides directory path
37
+ */
38
+ export function getSlidesDir(slidesDir = process.env.PPT_AGENT_SLIDES_DIR || 'slides') {
39
+ return resolve(getCwd(), slidesDir);
40
+ }
41
+
42
+ /**
43
+ * Resolve a template file. Local first, then package fallback.
44
+ * @param {string} name — template name without extension (e.g. "cover")
45
+ * @returns {{ path: string, source: 'local' | 'package' } | null}
46
+ */
47
+ export function resolveTemplate(name) {
48
+ const fileName = name.endsWith('.html') ? name : `${name}.html`;
49
+
50
+ const localPath = join(getCwd(), 'templates', fileName);
51
+ if (existsSync(localPath)) {
52
+ return { path: localPath, source: 'local' };
53
+ }
54
+
55
+ const packagePath = join(PACKAGE_ROOT, 'templates', fileName);
56
+ if (existsSync(packagePath)) {
57
+ return { path: packagePath, source: 'package' };
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * List all available templates (local + package, deduplicated).
65
+ * @returns {Array<{ name: string, source: 'local' | 'package' }>}
66
+ */
67
+ export function listTemplates() {
68
+ const seen = new Map();
69
+
70
+ // Local templates first (take priority)
71
+ const localDir = join(getCwd(), 'templates');
72
+ if (existsSync(localDir)) {
73
+ for (const f of readdirSync(localDir)) {
74
+ if (f.endsWith('.html')) {
75
+ seen.set(f, { name: f.replace('.html', ''), source: 'local' });
76
+ }
77
+ }
78
+ }
79
+
80
+ // Also check local custom/ subdirectory
81
+ const localCustomDir = join(localDir, 'custom');
82
+ if (existsSync(localCustomDir)) {
83
+ for (const f of readdirSync(localCustomDir)) {
84
+ if (f.endsWith('.html')) {
85
+ const key = `custom/${f}`;
86
+ seen.set(key, { name: `custom/${f.replace('.html', '')}`, source: 'local' });
87
+ }
88
+ }
89
+ }
90
+
91
+ // Package templates (only if not already overridden)
92
+ const pkgDir = join(PACKAGE_ROOT, 'templates');
93
+ if (existsSync(pkgDir)) {
94
+ for (const f of readdirSync(pkgDir)) {
95
+ if (f.endsWith('.html') && !seen.has(f)) {
96
+ seen.set(f, { name: f.replace('.html', ''), source: 'package' });
97
+ }
98
+ }
99
+ }
100
+
101
+ return Array.from(seen.values()).sort((a, b) => a.name.localeCompare(b.name));
102
+ }
103
+
104
+ /**
105
+ * Resolve a script path. Always from package.
106
+ * @param {string} relativePath — e.g. "scripts/validate-slides.js"
107
+ */
108
+ export function resolveScript(relativePath) {
109
+ return join(PACKAGE_ROOT, relativePath);
110
+ }