slides-grab 1.2.1 → 1.2.3
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/README.md +32 -4
- package/bin/ppt-agent.js +76 -36
- package/package.json +3 -4
- package/scripts/build-viewer.js +48 -21
- package/scripts/editor-server.js +113 -18
- package/scripts/figma-export.js +16 -1
- package/scripts/html2pdf.js +51 -23
- package/scripts/html2pptx.js +22 -1
- package/scripts/validate-slides.js +22 -5
- package/skills/slides-grab/SKILL.md +25 -20
- package/skills/slides-grab/references/presentation-workflow-reference.md +12 -11
- package/skills/slides-grab-card-news/SKILL.md +35 -0
- package/skills/slides-grab-design/SKILL.md +19 -16
- package/skills/slides-grab-design/references/design-rules.md +11 -7
- package/skills/slides-grab-design/references/design-system-full.md +7 -19
- package/skills/slides-grab-design/references/detailed-design-rules.md +6 -1
- package/skills/slides-grab-export/SKILL.md +15 -8
- package/skills/slides-grab-export/references/html2pptx.md +4 -4
- package/skills/slides-grab-plan/SKILL.md +7 -5
- package/src/design-styles-data.js +1928 -0
- package/src/design-styles.js +55 -0
- package/src/editor/codex-edit.js +57 -45
- package/src/editor/editor-codex-prompt.md +50 -0
- package/src/editor/js/editor-init.js +34 -2
- package/src/editor/js/editor-state.js +9 -2
- package/src/editor/screenshot.js +4 -3
- package/src/export-resolution.cjs +21 -11
- package/src/figma.js +11 -3
- package/src/pptx-raster-export.cjs +79 -21
- package/src/resolve.js +2 -51
- package/src/slide-mode.cjs +72 -0
- package/src/validation/cli.js +23 -0
- package/src/validation/core.js +39 -25
- package/templates/design-styles/README.md +19 -0
- package/templates/design-styles/preview.html +3356 -0
- package/themes/corporate.css +0 -8
- package/themes/executive.css +0 -10
- package/themes/modern-dark.css +0 -9
- package/themes/sage.css +0 -9
- package/themes/warm.css +0 -8
package/scripts/editor-server.js
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { readdir, readFile, writeFile, mkdtemp, rm, mkdir } from 'node:fs/promises';
|
|
4
4
|
import { watch as fsWatch } from 'node:fs';
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
5
7
|
import { basename, dirname, join, resolve, relative, sep } from 'node:path';
|
|
6
8
|
import { fileURLToPath } from 'node:url';
|
|
7
9
|
import { tmpdir } from 'node:os';
|
|
8
10
|
|
|
9
11
|
import {
|
|
10
|
-
SLIDE_SIZE,
|
|
11
12
|
buildCodexEditPrompt,
|
|
12
13
|
buildCodexExecArgs,
|
|
13
14
|
buildClaudeExecArgs,
|
|
@@ -23,6 +24,14 @@ import {
|
|
|
23
24
|
} from '../src/editor/edit-subprocess.js';
|
|
24
25
|
import { buildSlideRuntimeHtml } from '../src/image-contract.js';
|
|
25
26
|
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const {
|
|
29
|
+
DEFAULT_SLIDE_MODE,
|
|
30
|
+
getSlideModeChoices,
|
|
31
|
+
getSlideModeConfig,
|
|
32
|
+
normalizeSlideMode,
|
|
33
|
+
} = require('../src/slide-mode.cjs');
|
|
34
|
+
|
|
26
35
|
const __filename = fileURLToPath(import.meta.url);
|
|
27
36
|
const __dirname = dirname(__filename);
|
|
28
37
|
const PACKAGE_ROOT = process.env.PPT_AGENT_PACKAGE_ROOT || resolve(__dirname, '..');
|
|
@@ -45,6 +54,8 @@ const CODEX_MODELS = ['gpt-5.4', 'gpt-5.3-codex', 'gpt-5.3-codex-spark'];
|
|
|
45
54
|
const ALL_MODELS = [...CODEX_MODELS, ...CLAUDE_MODELS];
|
|
46
55
|
const DEFAULT_CODEX_MODEL = CODEX_MODELS[0];
|
|
47
56
|
const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
|
|
57
|
+
const PORT_PROBE_HOSTS = ['::', '127.0.0.1'];
|
|
58
|
+
const PORT_PROBE_IGNORED_CODES = new Set(['EAFNOSUPPORT', 'EADDRNOTAVAIL']);
|
|
48
59
|
|
|
49
60
|
const MAX_RUNS = 200;
|
|
50
61
|
const MAX_LOG_CHARS = 800_000;
|
|
@@ -55,6 +66,7 @@ function printUsage() {
|
|
|
55
66
|
process.stdout.write(`Options:\n`);
|
|
56
67
|
process.stdout.write(` --port <number> Server port (default: ${DEFAULT_PORT})\n`);
|
|
57
68
|
process.stdout.write(` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})\n`);
|
|
69
|
+
process.stdout.write(` --mode <mode> Slide mode: ${getSlideModeChoices().join(', ')} (default: ${DEFAULT_SLIDE_MODE})\n`);
|
|
58
70
|
process.stdout.write(` Model is selected in editor UI dropdown.\n`);
|
|
59
71
|
process.stdout.write(` -h, --help Show this help message\n`);
|
|
60
72
|
}
|
|
@@ -63,6 +75,7 @@ function parseArgs(argv) {
|
|
|
63
75
|
const opts = {
|
|
64
76
|
port: DEFAULT_PORT,
|
|
65
77
|
slidesDir: DEFAULT_SLIDES_DIR,
|
|
78
|
+
mode: DEFAULT_SLIDE_MODE,
|
|
66
79
|
help: false,
|
|
67
80
|
};
|
|
68
81
|
|
|
@@ -95,6 +108,17 @@ function parseArgs(argv) {
|
|
|
95
108
|
continue;
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
if (arg === '--mode') {
|
|
112
|
+
opts.mode = normalizeSlideMode(argv[i + 1]);
|
|
113
|
+
i += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (arg.startsWith('--mode=')) {
|
|
118
|
+
opts.mode = normalizeSlideMode(arg.slice('--mode='.length));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
98
122
|
if (arg === '--codex-model') {
|
|
99
123
|
// Backward compatibility: ignore legacy CLI option.
|
|
100
124
|
i += 1;
|
|
@@ -113,10 +137,67 @@ function parseArgs(argv) {
|
|
|
113
137
|
}
|
|
114
138
|
|
|
115
139
|
opts.slidesDir = opts.slidesDir.trim();
|
|
140
|
+
opts.mode = normalizeSlideMode(opts.mode);
|
|
116
141
|
|
|
117
142
|
return opts;
|
|
118
143
|
}
|
|
119
144
|
|
|
145
|
+
function buildPortInUseError(port) {
|
|
146
|
+
return new Error(`Editor port ${port} is already in use. Choose another port with \`--port <number>\` and try again.`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function assertHostPortAvailable(port, host) {
|
|
150
|
+
const probe = net.createServer();
|
|
151
|
+
try {
|
|
152
|
+
await new Promise((resolve, reject) => {
|
|
153
|
+
probe.once('error', reject);
|
|
154
|
+
probe.listen({ port, host, exclusive: true }, resolve);
|
|
155
|
+
});
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error?.code === 'EADDRINUSE') {
|
|
158
|
+
throw buildPortInUseError(port);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (PORT_PROBE_IGNORED_CODES.has(error?.code)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw error;
|
|
166
|
+
} finally {
|
|
167
|
+
if (probe.listening) {
|
|
168
|
+
await new Promise((resolve, reject) => {
|
|
169
|
+
probe.close((error) => {
|
|
170
|
+
if (error) {
|
|
171
|
+
reject(error);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
resolve();
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function assertPortUsable(port) {
|
|
182
|
+
for (const host of PORT_PROBE_HOSTS) {
|
|
183
|
+
await assertHostPortAvailable(port, host);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function listenOnPort(app, port) {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
const server = app.listen(port, () => resolve(server));
|
|
190
|
+
server.once('error', (error) => {
|
|
191
|
+
if (error?.code === 'EADDRINUSE') {
|
|
192
|
+
reject(buildPortInUseError(port));
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
reject(error);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
120
201
|
const sseClients = new Set();
|
|
121
202
|
|
|
122
203
|
function broadcastSSE(event, data) {
|
|
@@ -143,9 +224,9 @@ async function closeBrowser() {
|
|
|
143
224
|
}
|
|
144
225
|
}
|
|
145
226
|
|
|
146
|
-
async function withScreenshotPage(callback) {
|
|
227
|
+
async function withScreenshotPage(callback, screenshotSize) {
|
|
147
228
|
const { browser } = await getScreenshotBrowser();
|
|
148
|
-
const { context, page } = await screenshotMod.createScreenshotPage(browser);
|
|
229
|
+
const { context, page } = await screenshotMod.createScreenshotPage(browser, screenshotSize);
|
|
149
230
|
try {
|
|
150
231
|
return await callback(page);
|
|
151
232
|
} finally {
|
|
@@ -205,7 +286,7 @@ function sanitizeTargets(rawTargets) {
|
|
|
205
286
|
.filter((target) => target.xpath);
|
|
206
287
|
}
|
|
207
288
|
|
|
208
|
-
function normalizeSelections(rawSelections) {
|
|
289
|
+
function normalizeSelections(rawSelections, slideSize) {
|
|
209
290
|
if (!Array.isArray(rawSelections) || rawSelections.length === 0) {
|
|
210
291
|
throw new Error('At least one selection is required.');
|
|
211
292
|
}
|
|
@@ -215,7 +296,7 @@ function normalizeSelections(rawSelections) {
|
|
|
215
296
|
? selection.bbox
|
|
216
297
|
: selection;
|
|
217
298
|
|
|
218
|
-
const bbox = normalizeSelection(selectionSource,
|
|
299
|
+
const bbox = normalizeSelection(selectionSource, slideSize);
|
|
219
300
|
const targets = sanitizeTargets(selection?.targets);
|
|
220
301
|
|
|
221
302
|
return { bbox, targets };
|
|
@@ -392,6 +473,7 @@ function createRunStore() {
|
|
|
392
473
|
}
|
|
393
474
|
|
|
394
475
|
async function startServer(opts) {
|
|
476
|
+
await assertPortUsable(opts.port);
|
|
395
477
|
await loadDeps();
|
|
396
478
|
const slidesDirectory = resolve(process.cwd(), opts.slidesDir);
|
|
397
479
|
await mkdir(slidesDirectory, { recursive: true });
|
|
@@ -502,6 +584,18 @@ async function startServer(opts) {
|
|
|
502
584
|
}
|
|
503
585
|
});
|
|
504
586
|
|
|
587
|
+
app.get('/api/config', (_req, res) => {
|
|
588
|
+
const cfg = getSlideModeConfig(opts.mode);
|
|
589
|
+
res.json({
|
|
590
|
+
slideMode: opts.mode,
|
|
591
|
+
framePx: { width: cfg.framePx.width, height: cfg.framePx.height },
|
|
592
|
+
screenshotPx: { width: cfg.screenshotPx.width, height: cfg.screenshotPx.height },
|
|
593
|
+
sizeLabel: cfg.sizeLabel,
|
|
594
|
+
aspectRatioLabel: cfg.aspectRatioLabel,
|
|
595
|
+
coordinateSpaceLabel: cfg.coordinateSpaceLabel,
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
505
599
|
app.get('/api/models', (_req, res) => {
|
|
506
600
|
res.json({
|
|
507
601
|
models: ALL_MODELS,
|
|
@@ -570,7 +664,7 @@ async function startServer(opts) {
|
|
|
570
664
|
|
|
571
665
|
let normalizedSelections;
|
|
572
666
|
try {
|
|
573
|
-
normalizedSelections = normalizeSelections(selections);
|
|
667
|
+
normalizedSelections = normalizeSelections(selections, getSlideModeConfig(opts.mode).framePx);
|
|
574
668
|
} catch (error) {
|
|
575
669
|
return res.status(400).json({ error: error.message });
|
|
576
670
|
}
|
|
@@ -605,15 +699,15 @@ async function startServer(opts) {
|
|
|
605
699
|
slide,
|
|
606
700
|
screenshotPath,
|
|
607
701
|
`http://localhost:${opts.port}/slides`,
|
|
608
|
-
{ useHttp: true },
|
|
702
|
+
{ useHttp: true, screenshotSize: getSlideModeConfig(opts.mode).screenshotPx },
|
|
609
703
|
);
|
|
610
|
-
});
|
|
704
|
+
}, getSlideModeConfig(opts.mode).screenshotPx);
|
|
611
705
|
|
|
612
706
|
const scaledBoxes = normalizedSelections.map((selection) =>
|
|
613
707
|
scaleSelectionToScreenshot(
|
|
614
708
|
selection.bbox,
|
|
615
|
-
|
|
616
|
-
|
|
709
|
+
getSlideModeConfig(opts.mode).framePx,
|
|
710
|
+
getSlideModeConfig(opts.mode).screenshotPx,
|
|
617
711
|
),
|
|
618
712
|
);
|
|
619
713
|
|
|
@@ -623,6 +717,7 @@ async function startServer(opts) {
|
|
|
623
717
|
slideFile: slide,
|
|
624
718
|
slidePath: toSlidePathLabel(slidesDirectory, slide),
|
|
625
719
|
userPrompt: prompt,
|
|
720
|
+
slideMode: opts.mode,
|
|
626
721
|
selections: normalizedSelections,
|
|
627
722
|
});
|
|
628
723
|
|
|
@@ -708,14 +803,14 @@ async function startServer(opts) {
|
|
|
708
803
|
}, 300);
|
|
709
804
|
});
|
|
710
805
|
|
|
711
|
-
const server = app
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
806
|
+
const server = await listenOnPort(app, opts.port);
|
|
807
|
+
|
|
808
|
+
process.stdout.write('\n slides-grab editor\n');
|
|
809
|
+
process.stdout.write(' ─────────────────────────────────────\n');
|
|
810
|
+
process.stdout.write(` Local: http://localhost:${opts.port}\n`);
|
|
811
|
+
process.stdout.write(` Models: ${ALL_MODELS.join(', ')}\n`);
|
|
812
|
+
process.stdout.write(` Slides: ${slidesDirectory}\n`);
|
|
813
|
+
process.stdout.write(' ─────────────────────────────────────\n\n');
|
|
719
814
|
|
|
720
815
|
async function shutdown() {
|
|
721
816
|
process.stdout.write('\n[editor] Shutting down...\n');
|
package/scripts/figma-export.js
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
|
|
18
18
|
const require = createRequire(import.meta.url);
|
|
19
19
|
const html2pptx = require('../src/html2pptx.cjs');
|
|
20
|
+
const { DEFAULT_SLIDE_MODE, getSlideModeChoices, normalizeSlideMode } = require('../src/slide-mode.cjs');
|
|
20
21
|
|
|
21
22
|
const DEFAULT_SLIDES_DIR = 'slides';
|
|
22
23
|
|
|
@@ -28,6 +29,7 @@ function printUsage() {
|
|
|
28
29
|
'Options:',
|
|
29
30
|
` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
|
|
30
31
|
' --output <path> Output PPTX file (default: <slides-dir>-figma.pptx)',
|
|
32
|
+
` --mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
|
|
31
33
|
' -h, --help Show this help message',
|
|
32
34
|
'',
|
|
33
35
|
'Exports an experimental / unstable Figma Slides importable PPTX using the existing html2pptx pipeline.',
|
|
@@ -49,6 +51,7 @@ function parseArgs(args) {
|
|
|
49
51
|
const options = {
|
|
50
52
|
slidesDir: DEFAULT_SLIDES_DIR,
|
|
51
53
|
output: '',
|
|
54
|
+
mode: DEFAULT_SLIDE_MODE,
|
|
52
55
|
help: false,
|
|
53
56
|
};
|
|
54
57
|
|
|
@@ -81,6 +84,17 @@ function parseArgs(args) {
|
|
|
81
84
|
continue;
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
if (arg === '--mode') {
|
|
88
|
+
options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
|
|
89
|
+
i += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (arg.startsWith('--mode=')) {
|
|
94
|
+
options.mode = normalizeSlideMode(arg.slice('--mode='.length));
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
throw new Error(`Unknown option: ${arg}`);
|
|
85
99
|
}
|
|
86
100
|
|
|
@@ -89,6 +103,7 @@ function parseArgs(args) {
|
|
|
89
103
|
}
|
|
90
104
|
|
|
91
105
|
options.slidesDir = options.slidesDir.trim();
|
|
106
|
+
options.mode = normalizeSlideMode(options.mode);
|
|
92
107
|
options.output = normalizeFigmaOutput(options.slidesDir, options.output);
|
|
93
108
|
return options;
|
|
94
109
|
}
|
|
@@ -121,7 +136,7 @@ async function main() {
|
|
|
121
136
|
const files = getHtmlSlides(slidesDir);
|
|
122
137
|
|
|
123
138
|
const pres = new PptxGenJS();
|
|
124
|
-
configureFigmaExportPresentation(pres);
|
|
139
|
+
configureFigmaExportPresentation(pres, options.mode);
|
|
125
140
|
|
|
126
141
|
console.log(`Exporting ${files.length} slide(s) for Figma from ${slidesDir}`);
|
|
127
142
|
|
package/scripts/html2pdf.js
CHANGED
|
@@ -16,6 +16,12 @@ const {
|
|
|
16
16
|
getResolutionSize,
|
|
17
17
|
normalizeResolutionPreset,
|
|
18
18
|
} = require('../src/export-resolution.cjs');
|
|
19
|
+
const {
|
|
20
|
+
DEFAULT_SLIDE_MODE,
|
|
21
|
+
getSlideModeChoices,
|
|
22
|
+
getSlideModeConfig,
|
|
23
|
+
normalizeSlideMode,
|
|
24
|
+
} = require('../src/slide-mode.cjs');
|
|
19
25
|
|
|
20
26
|
const DEFAULT_OUTPUT = 'slides.pdf';
|
|
21
27
|
const DEFAULT_SLIDES_DIR = 'slides';
|
|
@@ -23,9 +29,7 @@ const DEFAULT_MODE = 'capture';
|
|
|
23
29
|
const DEFAULT_CAPTURE_RESOLUTION = '2160p';
|
|
24
30
|
const PDF_MODES = new Set(['capture', 'print']);
|
|
25
31
|
const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
|
|
26
|
-
const FALLBACK_SLIDE_SIZE = { width: 960, height: 540 };
|
|
27
32
|
const DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR = 2;
|
|
28
|
-
const TARGET_ASPECT_RATIO = 16 / 9;
|
|
29
33
|
const RENDER_SETTLE_MS = 120;
|
|
30
34
|
const CSS_PIXELS_PER_INCH = 96;
|
|
31
35
|
const PDF_POINTS_PER_INCH = 72;
|
|
@@ -40,6 +44,7 @@ function printUsage() {
|
|
|
40
44
|
` --output <path> Output PDF path (default: ${DEFAULT_OUTPUT})`,
|
|
41
45
|
` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
|
|
42
46
|
` --mode <mode> PDF export mode: capture|print (default: ${DEFAULT_MODE})`,
|
|
47
|
+
` --slide-mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
|
|
43
48
|
` --resolution <preset> Capture raster size preset: ${getResolutionChoices().join('|')}|4k (default: ${DEFAULT_CAPTURE_RESOLUTION}; ignored in print mode)`,
|
|
44
49
|
' -h, --help Show this help message',
|
|
45
50
|
'',
|
|
@@ -89,8 +94,8 @@ function cssPixelsToPdfPoints(value) {
|
|
|
89
94
|
return Math.round((normalizeDimension(value, 0) * PDF_POINTS_PER_INCH) / CSS_PIXELS_PER_INCH);
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
async function normalizeCaptureRasterSize(pngBytes, resolution = '') {
|
|
93
|
-
const targetSize = getResolutionSize(resolution);
|
|
97
|
+
async function normalizeCaptureRasterSize(pngBytes, resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
|
|
98
|
+
const targetSize = getResolutionSize(resolution, slideMode);
|
|
94
99
|
if (!targetSize) {
|
|
95
100
|
return pngBytes;
|
|
96
101
|
}
|
|
@@ -140,6 +145,7 @@ export function parseCliArgs(args) {
|
|
|
140
145
|
output: DEFAULT_OUTPUT,
|
|
141
146
|
slidesDir: DEFAULT_SLIDES_DIR,
|
|
142
147
|
mode: DEFAULT_MODE,
|
|
148
|
+
slideMode: DEFAULT_SLIDE_MODE,
|
|
143
149
|
resolution: DEFAULT_CAPTURE_RESOLUTION,
|
|
144
150
|
help: false,
|
|
145
151
|
};
|
|
@@ -185,6 +191,17 @@ export function parseCliArgs(args) {
|
|
|
185
191
|
continue;
|
|
186
192
|
}
|
|
187
193
|
|
|
194
|
+
if (arg === '--slide-mode') {
|
|
195
|
+
options.slideMode = normalizeSlideMode(readOptionValue(args, i, '--slide-mode'), { optionName: '--slide-mode' });
|
|
196
|
+
i += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (arg.startsWith('--slide-mode=')) {
|
|
201
|
+
options.slideMode = normalizeSlideMode(arg.slice('--slide-mode='.length), { optionName: '--slide-mode' });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
188
205
|
if (arg === '--resolution') {
|
|
189
206
|
options.resolution = normalizeResolutionPreset(readOptionValue(args, i, '--resolution'));
|
|
190
207
|
i += 1;
|
|
@@ -209,6 +226,7 @@ export function parseCliArgs(args) {
|
|
|
209
226
|
options.output = options.output.trim();
|
|
210
227
|
options.slidesDir = options.slidesDir.trim();
|
|
211
228
|
options.mode = normalizeMode(options.mode);
|
|
229
|
+
options.slideMode = normalizeSlideMode(options.slideMode, { optionName: '--slide-mode' });
|
|
212
230
|
options.resolution = normalizeResolutionPreset(options.resolution);
|
|
213
231
|
if (options.mode === 'print') {
|
|
214
232
|
options.resolution = '';
|
|
@@ -226,9 +244,10 @@ export async function findSlideFiles(slidesDir = resolve(process.cwd(), DEFAULT_
|
|
|
226
244
|
}
|
|
227
245
|
|
|
228
246
|
export function buildPdfOptions(widthPx, heightPx) {
|
|
247
|
+
const fallbackSize = getSlideModeConfig(DEFAULT_SLIDE_MODE).framePx;
|
|
229
248
|
return {
|
|
230
|
-
width: `${normalizeDimension(widthPx,
|
|
231
|
-
height: `${normalizeDimension(heightPx,
|
|
249
|
+
width: `${normalizeDimension(widthPx, fallbackSize.width)}px`,
|
|
250
|
+
height: `${normalizeDimension(heightPx, fallbackSize.height)}px`,
|
|
232
251
|
printBackground: true,
|
|
233
252
|
pageRanges: '1',
|
|
234
253
|
margin: { top: '0px', right: '0px', bottom: '0px', left: '0px' },
|
|
@@ -236,22 +255,25 @@ export function buildPdfOptions(widthPx, heightPx) {
|
|
|
236
255
|
};
|
|
237
256
|
}
|
|
238
257
|
|
|
239
|
-
export function buildPageOptions(mode = DEFAULT_MODE, resolution = '') {
|
|
240
|
-
const
|
|
258
|
+
export function buildPageOptions(mode = DEFAULT_MODE, resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
|
|
259
|
+
const { framePx } = getSlideModeConfig(slideMode);
|
|
260
|
+
const targetResolution = normalizeMode(mode) === 'capture' ? getResolutionSize(resolution, slideMode) : null;
|
|
241
261
|
return {
|
|
242
262
|
viewport: {
|
|
243
|
-
width:
|
|
244
|
-
height:
|
|
263
|
+
width: framePx.width,
|
|
264
|
+
height: framePx.height,
|
|
245
265
|
},
|
|
246
266
|
deviceScaleFactor: normalizeMode(mode) === 'capture'
|
|
247
267
|
? targetResolution
|
|
248
|
-
? targetResolution.height /
|
|
268
|
+
? targetResolution.height / framePx.height
|
|
249
269
|
: DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR
|
|
250
270
|
: 1,
|
|
251
271
|
};
|
|
252
272
|
}
|
|
253
273
|
|
|
254
|
-
function chooseSlideFrame(metrics) {
|
|
274
|
+
function chooseSlideFrame(metrics, slideMode = DEFAULT_SLIDE_MODE) {
|
|
275
|
+
const { framePx } = getSlideModeConfig(slideMode);
|
|
276
|
+
const targetAspectRatio = framePx.width / framePx.height;
|
|
255
277
|
const viewportArea = Math.max(1, metrics.viewport.width * metrics.viewport.height);
|
|
256
278
|
const bodyArea = Math.max(1, metrics.body.width * metrics.body.height);
|
|
257
279
|
const bodyScrollArea = Math.max(1, metrics.body.scrollWidth * metrics.body.scrollHeight);
|
|
@@ -269,7 +291,7 @@ function chooseSlideFrame(metrics) {
|
|
|
269
291
|
.map((candidate) => ({
|
|
270
292
|
...candidate,
|
|
271
293
|
area: candidate.width * candidate.height,
|
|
272
|
-
aspectDelta: Math.abs(candidate.width / candidate.height -
|
|
294
|
+
aspectDelta: Math.abs(candidate.width / candidate.height - targetAspectRatio),
|
|
273
295
|
coverage: (candidate.width * candidate.height) / viewportArea,
|
|
274
296
|
}))
|
|
275
297
|
.sort((left, right) => right.area - left.area);
|
|
@@ -359,7 +381,7 @@ export async function waitForSlideRenderReady(page, options = {}) {
|
|
|
359
381
|
}, { settleMs, runReadySignal: shouldRunReadySignal });
|
|
360
382
|
}
|
|
361
383
|
|
|
362
|
-
export async function detectSlideFrame(page) {
|
|
384
|
+
export async function detectSlideFrame(page, slideMode = DEFAULT_SLIDE_MODE) {
|
|
363
385
|
const metrics = await page.evaluate(() => {
|
|
364
386
|
function toBox(element) {
|
|
365
387
|
const rect = element.getBoundingClientRect();
|
|
@@ -398,12 +420,13 @@ export async function detectSlideFrame(page) {
|
|
|
398
420
|
};
|
|
399
421
|
});
|
|
400
422
|
|
|
401
|
-
const frame = chooseSlideFrame(metrics);
|
|
423
|
+
const frame = chooseSlideFrame(metrics, slideMode);
|
|
424
|
+
const fallbackSize = getSlideModeConfig(slideMode).framePx;
|
|
402
425
|
return {
|
|
403
426
|
x: normalizeDimension(frame.x, 0),
|
|
404
427
|
y: normalizeDimension(frame.y, 0),
|
|
405
|
-
width: normalizeDimension(frame.width,
|
|
406
|
-
height: normalizeDimension(frame.height,
|
|
428
|
+
width: normalizeDimension(frame.width, fallbackSize.width),
|
|
429
|
+
height: normalizeDimension(frame.height, fallbackSize.height),
|
|
407
430
|
candidateIndex: Number.isInteger(frame.candidateIndex) ? frame.candidateIndex : null,
|
|
408
431
|
source: frame.source,
|
|
409
432
|
};
|
|
@@ -641,20 +664,22 @@ export async function renderSlideToPdf(page, slideFile, slidesDir, options = {})
|
|
|
641
664
|
const slidePath = join(slidesDir, slideFile);
|
|
642
665
|
const slideUrl = pathToFileURL(slidePath).href;
|
|
643
666
|
const mode = normalizeMode(options.mode ?? DEFAULT_MODE);
|
|
667
|
+
const slideMode = normalizeSlideMode(options.slideMode ?? DEFAULT_SLIDE_MODE, { optionName: '--slide-mode' });
|
|
644
668
|
const captureResolution = mode === 'capture' ? normalizeResolutionPreset(options.resolution ?? '') : '';
|
|
645
669
|
|
|
646
670
|
await page.goto(slideUrl, { waitUntil: 'load' });
|
|
647
671
|
await waitForSlideRenderReady(page, options);
|
|
648
672
|
|
|
649
|
-
const slideFrame = await detectSlideFrame(page);
|
|
673
|
+
const slideFrame = await detectSlideFrame(page, slideMode);
|
|
650
674
|
const normalizedSlideFrame = await isolateSlideFrame(page, slideFrame);
|
|
651
675
|
await normalizeBodyToSlideFrame(page, normalizedSlideFrame);
|
|
652
676
|
await waitForSlideRenderReady(page, { ...options, runReadySignal: false });
|
|
653
677
|
|
|
654
678
|
if (mode === 'capture') {
|
|
679
|
+
const fallbackSize = getSlideModeConfig(slideMode).framePx;
|
|
655
680
|
const viewportSize = {
|
|
656
|
-
width: normalizeDimension(normalizedSlideFrame.width,
|
|
657
|
-
height: normalizeDimension(normalizedSlideFrame.height,
|
|
681
|
+
width: normalizeDimension(normalizedSlideFrame.width, fallbackSize.width),
|
|
682
|
+
height: normalizeDimension(normalizedSlideFrame.height, fallbackSize.height),
|
|
658
683
|
};
|
|
659
684
|
await page.setViewportSize(viewportSize);
|
|
660
685
|
await waitForSlideRenderReady(page, { ...options, runReadySignal: false });
|
|
@@ -679,9 +704,10 @@ export async function renderSlideToPdf(page, slideFile, slidesDir, options = {})
|
|
|
679
704
|
height: viewportSize.height,
|
|
680
705
|
},
|
|
681
706
|
});
|
|
682
|
-
const normalizedPngBytes = await normalizeCaptureRasterSize(pngBytes, captureResolution);
|
|
707
|
+
const normalizedPngBytes = await normalizeCaptureRasterSize(pngBytes, captureResolution, slideMode);
|
|
683
708
|
return {
|
|
684
709
|
mode,
|
|
710
|
+
slideMode,
|
|
685
711
|
width: normalizedSlideFrame.width,
|
|
686
712
|
height: normalizedSlideFrame.height,
|
|
687
713
|
pngBytes: normalizedPngBytes,
|
|
@@ -692,6 +718,7 @@ export async function renderSlideToPdf(page, slideFile, slidesDir, options = {})
|
|
|
692
718
|
|
|
693
719
|
return {
|
|
694
720
|
mode,
|
|
721
|
+
slideMode,
|
|
695
722
|
width: normalizedSlideFrame.width,
|
|
696
723
|
height: normalizedSlideFrame.height,
|
|
697
724
|
pdfBytes: await page.pdf(buildPdfOptions(normalizedSlideFrame.width, normalizedSlideFrame.height)),
|
|
@@ -740,14 +767,14 @@ async function main() {
|
|
|
740
767
|
}
|
|
741
768
|
|
|
742
769
|
const slidesDir = resolve(process.cwd(), options.slidesDir);
|
|
743
|
-
await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PDF export' });
|
|
770
|
+
await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PDF export', slideMode: options.slideMode });
|
|
744
771
|
const slideFiles = await findSlideFiles(slidesDir);
|
|
745
772
|
if (slideFiles.length === 0) {
|
|
746
773
|
throw new Error(`No slide-*.html files found in: ${slidesDir}`);
|
|
747
774
|
}
|
|
748
775
|
|
|
749
776
|
const browser = await chromium.launch({ headless: true });
|
|
750
|
-
const page = await browser.newPage(buildPageOptions(options.mode, options.resolution));
|
|
777
|
+
const page = await browser.newPage(buildPageOptions(options.mode, options.resolution, options.slideMode));
|
|
751
778
|
const diagnostics = createSlideDiagnostics();
|
|
752
779
|
diagnostics.attach(page);
|
|
753
780
|
const renderedSlides = [];
|
|
@@ -758,6 +785,7 @@ async function main() {
|
|
|
758
785
|
try {
|
|
759
786
|
const slideResult = await renderSlideToPdf(page, slideFile, slidesDir, {
|
|
760
787
|
mode: options.mode,
|
|
788
|
+
slideMode: options.slideMode,
|
|
761
789
|
resolution: options.resolution,
|
|
762
790
|
});
|
|
763
791
|
renderedSlides.push(slideResult);
|
package/scripts/html2pptx.js
CHANGED
|
@@ -10,6 +10,7 @@ import { ensureOutputDirectory, SLIDE_FILE_PATTERN, sortFigmaSlideFiles } from '
|
|
|
10
10
|
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
12
|
const html2pptx = require('../src/html2pptx.cjs');
|
|
13
|
+
const { DEFAULT_SLIDE_MODE, getSlideModeChoices, getSlideModeConfig, normalizeSlideMode } = require('../src/slide-mode.cjs');
|
|
13
14
|
|
|
14
15
|
const DEFAULT_SLIDES_DIR = 'slides';
|
|
15
16
|
const DEFAULT_OUTPUT = 'output.pptx';
|
|
@@ -22,6 +23,7 @@ function printUsage() {
|
|
|
22
23
|
'Options:',
|
|
23
24
|
` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
|
|
24
25
|
` --output <path> Output PPTX file (default: ${DEFAULT_OUTPUT})`,
|
|
26
|
+
` --mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
|
|
25
27
|
' -h, --help Show this help message',
|
|
26
28
|
'',
|
|
27
29
|
'Experimental / unstable PPTX export. Treat output as best-effort only.',
|
|
@@ -42,6 +44,7 @@ function parseArgs(args) {
|
|
|
42
44
|
const options = {
|
|
43
45
|
slidesDir: DEFAULT_SLIDES_DIR,
|
|
44
46
|
output: DEFAULT_OUTPUT,
|
|
47
|
+
mode: DEFAULT_SLIDE_MODE,
|
|
45
48
|
help: false,
|
|
46
49
|
};
|
|
47
50
|
|
|
@@ -74,6 +77,17 @@ function parseArgs(args) {
|
|
|
74
77
|
continue;
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
if (arg === '--mode') {
|
|
81
|
+
options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
|
|
82
|
+
i += 1;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (arg.startsWith('--mode=')) {
|
|
87
|
+
options.mode = normalizeSlideMode(arg.slice('--mode='.length));
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
77
91
|
throw new Error(`Unknown option: ${arg}`);
|
|
78
92
|
}
|
|
79
93
|
|
|
@@ -87,6 +101,7 @@ function parseArgs(args) {
|
|
|
87
101
|
|
|
88
102
|
options.slidesDir = options.slidesDir.trim();
|
|
89
103
|
options.output = options.output.trim();
|
|
104
|
+
options.mode = normalizeSlideMode(options.mode);
|
|
90
105
|
return options;
|
|
91
106
|
}
|
|
92
107
|
|
|
@@ -118,7 +133,13 @@ async function main() {
|
|
|
118
133
|
const files = getHtmlSlides(slidesDir);
|
|
119
134
|
|
|
120
135
|
const pres = new PptxGenJS();
|
|
121
|
-
|
|
136
|
+
const { pptxSizeIn } = getSlideModeConfig(options.mode);
|
|
137
|
+
pres.defineLayout({
|
|
138
|
+
name: 'SLIDES_GRAB_HTML2PPTX',
|
|
139
|
+
width: pptxSizeIn.width,
|
|
140
|
+
height: pptxSizeIn.height,
|
|
141
|
+
});
|
|
142
|
+
pres.layout = 'SLIDES_GRAB_HTML2PPTX';
|
|
122
143
|
|
|
123
144
|
for (const file of files) {
|
|
124
145
|
await html2pptx(resolve(slidesDir, file), pres);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
5
6
|
import { chromium } from 'playwright';
|
|
6
7
|
|
|
7
8
|
import {
|
|
@@ -20,6 +21,9 @@ import {
|
|
|
20
21
|
selectSlideFiles,
|
|
21
22
|
} from '../src/validation/core.js';
|
|
22
23
|
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
const { DEFAULT_SLIDE_MODE } = require('../src/slide-mode.cjs');
|
|
26
|
+
|
|
23
27
|
export {
|
|
24
28
|
DEFAULT_SLIDES_DIR,
|
|
25
29
|
ensureSlidesPassValidation,
|
|
@@ -160,7 +164,7 @@ function peekValidateFormat(args = []) {
|
|
|
160
164
|
return DEFAULT_VALIDATE_FORMAT;
|
|
161
165
|
}
|
|
162
166
|
|
|
163
|
-
export async function validateSlides(slidesDir, { selectedSlides = [] } = {}) {
|
|
167
|
+
export async function validateSlides(slidesDir, { mode = DEFAULT_SLIDE_MODE, selectedSlides = [] } = {}) {
|
|
164
168
|
const slideFiles = selectSlideFiles(await findSlideFiles(slidesDir), selectedSlides, slidesDir);
|
|
165
169
|
if (slideFiles.length === 0) {
|
|
166
170
|
throw new Error(`No slide-*.html files found in: ${slidesDir}`);
|
|
@@ -171,13 +175,26 @@ export async function validateSlides(slidesDir, { selectedSlides = [] } = {}) {
|
|
|
171
175
|
const page = await context.newPage();
|
|
172
176
|
|
|
173
177
|
try {
|
|
174
|
-
const slides = await scanSlides(page, slidesDir, slideFiles);
|
|
175
|
-
return createValidationResult(slides);
|
|
178
|
+
const slides = await scanSlides(page, slidesDir, slideFiles, mode);
|
|
179
|
+
return createValidationResult(slides, mode);
|
|
176
180
|
} finally {
|
|
177
181
|
await browser.close();
|
|
178
182
|
}
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
function peekValidateMode(args = []) {
|
|
186
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
187
|
+
const arg = args[i];
|
|
188
|
+
if (arg === '--mode') {
|
|
189
|
+
return args[i + 1] || DEFAULT_SLIDE_MODE;
|
|
190
|
+
}
|
|
191
|
+
if (arg.startsWith('--mode=')) {
|
|
192
|
+
return arg.slice('--mode='.length) || DEFAULT_SLIDE_MODE;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return DEFAULT_SLIDE_MODE;
|
|
196
|
+
}
|
|
197
|
+
|
|
181
198
|
export async function main(args = process.argv.slice(2)) {
|
|
182
199
|
const options = parseValidateCliArgs(args);
|
|
183
200
|
if (options.help) {
|
|
@@ -186,7 +203,7 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
186
203
|
}
|
|
187
204
|
|
|
188
205
|
const slidesDir = resolve(process.cwd(), options.slidesDir);
|
|
189
|
-
const result = await validateSlides(slidesDir, { selectedSlides: options.slides });
|
|
206
|
+
const result = await validateSlides(slidesDir, { mode: options.mode, selectedSlides: options.slides });
|
|
190
207
|
process.stdout.write(formatValidationResult(result, options.format));
|
|
191
208
|
if (result.summary.failedSlides > 0) {
|
|
192
209
|
process.exitCode = 1;
|
|
@@ -197,7 +214,7 @@ const isMain = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(imp
|
|
|
197
214
|
|
|
198
215
|
if (isMain) {
|
|
199
216
|
main().catch((error) => {
|
|
200
|
-
const failure = createValidationFailure(error);
|
|
217
|
+
const failure = createValidationFailure(error, peekValidateMode(process.argv.slice(2)));
|
|
201
218
|
process.stdout.write(formatValidationResult(failure, peekValidateFormat(process.argv.slice(2))));
|
|
202
219
|
process.exit(1);
|
|
203
220
|
});
|