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
|
@@ -9,14 +9,19 @@ const {
|
|
|
9
9
|
getResolutionSize,
|
|
10
10
|
normalizeResolutionPreset,
|
|
11
11
|
} = require('./export-resolution.cjs');
|
|
12
|
+
const {
|
|
13
|
+
DEFAULT_SLIDE_MODE,
|
|
14
|
+
getSlideModeChoices,
|
|
15
|
+
getSlideModeConfig,
|
|
16
|
+
normalizeSlideMode,
|
|
17
|
+
} = require('./slide-mode.cjs');
|
|
12
18
|
|
|
13
19
|
const DEFAULT_SLIDES_DIR = 'slides';
|
|
14
20
|
const DEFAULT_OUTPUT = 'output.pptx';
|
|
15
21
|
const DEFAULT_RESOLUTION = '2160p';
|
|
16
|
-
const DEFAULT_CAPTURE_VIEWPORT = { width: 960, height: 540 };
|
|
17
22
|
const DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR = 2;
|
|
18
23
|
const TARGET_RASTER_DPI = 150;
|
|
19
|
-
const
|
|
24
|
+
const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
|
|
20
25
|
|
|
21
26
|
function normalizeDimension(value, fallback) {
|
|
22
27
|
if (!Number.isFinite(value) || value <= 0) {
|
|
@@ -25,31 +30,63 @@ function normalizeDimension(value, fallback) {
|
|
|
25
30
|
return Math.max(1, Math.round(value));
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
function buildPageOptions(resolution = '') {
|
|
29
|
-
const
|
|
33
|
+
function buildPageOptions(resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
|
|
34
|
+
const { framePx } = getSlideModeConfig(slideMode);
|
|
35
|
+
const targetResolution = getResolutionSize(resolution, slideMode);
|
|
30
36
|
return {
|
|
31
37
|
viewport: {
|
|
32
|
-
width:
|
|
33
|
-
height:
|
|
38
|
+
width: framePx.width,
|
|
39
|
+
height: framePx.height,
|
|
34
40
|
},
|
|
35
41
|
deviceScaleFactor: targetResolution
|
|
36
|
-
? targetResolution.height /
|
|
42
|
+
? targetResolution.height / framePx.height
|
|
37
43
|
: DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR,
|
|
38
44
|
};
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
function getTargetRasterSize(resolution = '') {
|
|
42
|
-
const targetResolution = getResolutionSize(resolution);
|
|
47
|
+
function getTargetRasterSize(resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
|
|
48
|
+
const targetResolution = getResolutionSize(resolution, slideMode);
|
|
43
49
|
if (targetResolution) {
|
|
44
50
|
return targetResolution;
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
const { pptxSizeIn } = getSlideModeConfig(slideMode);
|
|
47
54
|
return {
|
|
48
|
-
width: Math.round(
|
|
49
|
-
height: Math.round(
|
|
55
|
+
width: Math.round(pptxSizeIn.width * TARGET_RASTER_DPI),
|
|
56
|
+
height: Math.round(pptxSizeIn.height * TARGET_RASTER_DPI),
|
|
50
57
|
};
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
function toSlideOrder(fileName) {
|
|
61
|
+
const match = fileName.match(/\d+/);
|
|
62
|
+
return match ? Number.parseInt(match[0], 10) : Number.POSITIVE_INFINITY;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sortSlideFiles(a, b) {
|
|
66
|
+
const orderA = toSlideOrder(a);
|
|
67
|
+
const orderB = toSlideOrder(b);
|
|
68
|
+
if (orderA !== orderB) {
|
|
69
|
+
return orderA - orderB;
|
|
70
|
+
}
|
|
71
|
+
return a.localeCompare(b);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getHtmlSlides(slidesDir) {
|
|
75
|
+
if (!fs.existsSync(slidesDir)) {
|
|
76
|
+
throw new Error(`Slides directory not found: ${slidesDir}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const files = fs.readdirSync(slidesDir)
|
|
80
|
+
.filter((fileName) => SLIDE_FILE_PATTERN.test(fileName))
|
|
81
|
+
.sort(sortSlideFiles);
|
|
82
|
+
|
|
83
|
+
if (files.length === 0) {
|
|
84
|
+
throw new Error(`No slide-*.html files found in ${slidesDir}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return files;
|
|
88
|
+
}
|
|
89
|
+
|
|
53
90
|
function printUsage() {
|
|
54
91
|
process.stdout.write(
|
|
55
92
|
[
|
|
@@ -58,6 +95,7 @@ function printUsage() {
|
|
|
58
95
|
'Options:',
|
|
59
96
|
` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
|
|
60
97
|
` --output <path> Output pptx path (default: ${DEFAULT_OUTPUT})`,
|
|
98
|
+
` --mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
|
|
61
99
|
` --resolution <preset> Raster size preset: ${getResolutionChoices().join('|')}|4k (default: ${DEFAULT_RESOLUTION})`,
|
|
62
100
|
' -h, --help Show this help message',
|
|
63
101
|
].join('\n'),
|
|
@@ -77,6 +115,7 @@ function parseArgs(args) {
|
|
|
77
115
|
const options = {
|
|
78
116
|
slidesDir: DEFAULT_SLIDES_DIR,
|
|
79
117
|
output: DEFAULT_OUTPUT,
|
|
118
|
+
mode: DEFAULT_SLIDE_MODE,
|
|
80
119
|
resolution: DEFAULT_RESOLUTION,
|
|
81
120
|
help: false,
|
|
82
121
|
};
|
|
@@ -110,6 +149,17 @@ function parseArgs(args) {
|
|
|
110
149
|
continue;
|
|
111
150
|
}
|
|
112
151
|
|
|
152
|
+
if (arg === '--mode') {
|
|
153
|
+
options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
|
|
154
|
+
i += 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (arg.startsWith('--mode=')) {
|
|
159
|
+
options.mode = normalizeSlideMode(arg.slice('--mode='.length));
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
|
|
113
163
|
if (arg === '--resolution') {
|
|
114
164
|
options.resolution = normalizeResolutionPreset(readOptionValue(args, i, '--resolution'));
|
|
115
165
|
i += 1;
|
|
@@ -134,14 +184,17 @@ function parseArgs(args) {
|
|
|
134
184
|
|
|
135
185
|
options.slidesDir = options.slidesDir.trim();
|
|
136
186
|
options.output = options.output.trim();
|
|
187
|
+
options.mode = normalizeSlideMode(options.mode);
|
|
137
188
|
options.resolution = normalizeResolutionPreset(options.resolution);
|
|
138
189
|
return options;
|
|
139
190
|
}
|
|
140
191
|
|
|
141
192
|
async function convertSlide(htmlFile, pres, browser, options = {}) {
|
|
142
193
|
const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile);
|
|
194
|
+
const slideMode = normalizeSlideMode(options.mode || DEFAULT_SLIDE_MODE);
|
|
195
|
+
const fallbackSize = getSlideModeConfig(slideMode).framePx;
|
|
143
196
|
|
|
144
|
-
const page = await browser.newPage(buildPageOptions(options.resolution));
|
|
197
|
+
const page = await browser.newPage(buildPageOptions(options.resolution, slideMode));
|
|
145
198
|
await page.goto(`file://${filePath}`);
|
|
146
199
|
|
|
147
200
|
const bodyDimensions = await page.evaluate(() => {
|
|
@@ -154,14 +207,14 @@ async function convertSlide(htmlFile, pres, browser, options = {}) {
|
|
|
154
207
|
});
|
|
155
208
|
|
|
156
209
|
await page.setViewportSize({
|
|
157
|
-
width: normalizeDimension(bodyDimensions.width,
|
|
158
|
-
height: normalizeDimension(bodyDimensions.height,
|
|
210
|
+
width: normalizeDimension(bodyDimensions.width, fallbackSize.width),
|
|
211
|
+
height: normalizeDimension(bodyDimensions.height, fallbackSize.height),
|
|
159
212
|
});
|
|
160
213
|
|
|
161
214
|
const screenshot = await page.screenshot({ type: 'png' });
|
|
162
215
|
await page.close();
|
|
163
216
|
|
|
164
|
-
const targetSize = getTargetRasterSize(options.resolution);
|
|
217
|
+
const targetSize = getTargetRasterSize(options.resolution, slideMode);
|
|
165
218
|
|
|
166
219
|
const resized = await sharp(screenshot)
|
|
167
220
|
.resize(targetSize.width, targetSize.height, { fit: 'fill' })
|
|
@@ -191,14 +244,18 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
191
244
|
}
|
|
192
245
|
|
|
193
246
|
const pres = new PptxGenJS();
|
|
194
|
-
|
|
247
|
+
const { pptxSizeIn } = getSlideModeConfig(options.mode);
|
|
248
|
+
pres.defineLayout({
|
|
249
|
+
name: 'SLIDES_GRAB_DYNAMIC',
|
|
250
|
+
width: pptxSizeIn.width,
|
|
251
|
+
height: pptxSizeIn.height,
|
|
252
|
+
});
|
|
253
|
+
pres.layout = 'SLIDES_GRAB_DYNAMIC';
|
|
195
254
|
|
|
196
255
|
const slidesDir = path.resolve(process.cwd(), options.slidesDir);
|
|
197
256
|
const { ensureSlidesPassValidation } = await import('../scripts/validate-slides.js');
|
|
198
|
-
await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PPTX export' });
|
|
199
|
-
const files =
|
|
200
|
-
.filter((fileName) => fileName.endsWith('.html'))
|
|
201
|
-
.sort();
|
|
257
|
+
await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PPTX export', slideMode: options.mode });
|
|
258
|
+
const files = getHtmlSlides(slidesDir);
|
|
202
259
|
|
|
203
260
|
console.log(`Converting ${files.length} slides...`);
|
|
204
261
|
|
|
@@ -209,7 +266,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
209
266
|
const filePath = path.join(slidesDir, file);
|
|
210
267
|
console.log(` Processing: ${file}`);
|
|
211
268
|
try {
|
|
212
|
-
const tmpPath = await convertSlide(filePath, pres, browser, { resolution: options.resolution });
|
|
269
|
+
const tmpPath = await convertSlide(filePath, pres, browser, { mode: options.mode, resolution: options.resolution });
|
|
213
270
|
tmpFiles.push(tmpPath);
|
|
214
271
|
console.log(` ✓ ${file} done`);
|
|
215
272
|
} catch (error) {
|
|
@@ -232,6 +289,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
232
289
|
|
|
233
290
|
module.exports = {
|
|
234
291
|
buildPageOptions,
|
|
292
|
+
getHtmlSlides,
|
|
235
293
|
getTargetRasterSize,
|
|
236
294
|
main,
|
|
237
295
|
parseArgs,
|
package/src/resolve.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* 1. Local (user's CWD) — per-project overrides
|
|
6
6
|
* 2. Package root — built-in defaults
|
|
7
7
|
*
|
|
8
|
-
* slides directory, slide-outline.md
|
|
9
|
-
* templates
|
|
8
|
+
* slides directory, slide-outline.md → always local (CWD)
|
|
9
|
+
* templates/ → local first, package fallback
|
|
10
10
|
* scripts/ → always package
|
|
11
11
|
*/
|
|
12
12
|
|
|
@@ -60,27 +60,6 @@ export function resolveTemplate(name) {
|
|
|
60
60
|
return null;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
/**
|
|
64
|
-
* Resolve a theme file. Local first, then package fallback.
|
|
65
|
-
* @param {string} name — theme name without extension (e.g. "modern-dark")
|
|
66
|
-
* @returns {{ path: string, source: 'local' | 'package' } | null}
|
|
67
|
-
*/
|
|
68
|
-
export function resolveTheme(name) {
|
|
69
|
-
const fileName = name.endsWith('.css') ? name : `${name}.css`;
|
|
70
|
-
|
|
71
|
-
const localPath = join(getCwd(), 'themes', fileName);
|
|
72
|
-
if (existsSync(localPath)) {
|
|
73
|
-
return { path: localPath, source: 'local' };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const packagePath = join(PACKAGE_ROOT, 'themes', fileName);
|
|
77
|
-
if (existsSync(packagePath)) {
|
|
78
|
-
return { path: packagePath, source: 'package' };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
63
|
/**
|
|
85
64
|
* List all available templates (local + package, deduplicated).
|
|
86
65
|
* @returns {Array<{ name: string, source: 'local' | 'package' }>}
|
|
@@ -122,34 +101,6 @@ export function listTemplates() {
|
|
|
122
101
|
return Array.from(seen.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
123
102
|
}
|
|
124
103
|
|
|
125
|
-
/**
|
|
126
|
-
* List all available themes (local + package, deduplicated).
|
|
127
|
-
* @returns {Array<{ name: string, source: 'local' | 'package' }>}
|
|
128
|
-
*/
|
|
129
|
-
export function listThemes() {
|
|
130
|
-
const seen = new Map();
|
|
131
|
-
|
|
132
|
-
const localDir = join(getCwd(), 'themes');
|
|
133
|
-
if (existsSync(localDir)) {
|
|
134
|
-
for (const f of readdirSync(localDir)) {
|
|
135
|
-
if (f.endsWith('.css')) {
|
|
136
|
-
seen.set(f, { name: f.replace('.css', ''), source: 'local' });
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const pkgDir = join(PACKAGE_ROOT, 'themes');
|
|
142
|
-
if (existsSync(pkgDir)) {
|
|
143
|
-
for (const f of readdirSync(pkgDir)) {
|
|
144
|
-
if (f.endsWith('.css') && !seen.has(f)) {
|
|
145
|
-
seen.set(f, { name: f.replace('.css', ''), source: 'package' });
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return Array.from(seen.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
151
|
-
}
|
|
152
|
-
|
|
153
104
|
/**
|
|
154
105
|
* Resolve a script path. Always from package.
|
|
155
106
|
* @param {string} relativePath — e.g. "scripts/validate-slides.js"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const DEFAULT_SLIDE_MODE = 'presentation';
|
|
2
|
+
const PT_TO_PX = 96 / 72;
|
|
3
|
+
|
|
4
|
+
const SLIDE_MODES = Object.freeze({
|
|
5
|
+
presentation: Object.freeze({
|
|
6
|
+
name: 'presentation',
|
|
7
|
+
framePt: Object.freeze({ width: 720, height: 405 }),
|
|
8
|
+
framePx: Object.freeze({ width: 720 * PT_TO_PX, height: 405 * PT_TO_PX }),
|
|
9
|
+
screenshotPx: Object.freeze({ width: 1600, height: 900 }),
|
|
10
|
+
pptxSizeIn: Object.freeze({ width: 13.33, height: 7.5 }),
|
|
11
|
+
figmaSizeIn: Object.freeze({ width: 10, height: 5.625 }),
|
|
12
|
+
sizeLabel: '720pt x 405pt',
|
|
13
|
+
coordinateSpaceLabel: '960x540',
|
|
14
|
+
aspectRatioLabel: '16:9',
|
|
15
|
+
}),
|
|
16
|
+
'card-news': Object.freeze({
|
|
17
|
+
name: 'card-news',
|
|
18
|
+
framePt: Object.freeze({ width: 720, height: 720 }),
|
|
19
|
+
framePx: Object.freeze({ width: 720 * PT_TO_PX, height: 720 * PT_TO_PX }),
|
|
20
|
+
screenshotPx: Object.freeze({ width: 1600, height: 1600 }),
|
|
21
|
+
pptxSizeIn: Object.freeze({ width: 10, height: 10 }),
|
|
22
|
+
figmaSizeIn: Object.freeze({ width: 10, height: 10 }),
|
|
23
|
+
sizeLabel: '720pt x 720pt',
|
|
24
|
+
coordinateSpaceLabel: '960x960',
|
|
25
|
+
aspectRatioLabel: '1:1',
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function getSlideModeChoices() {
|
|
30
|
+
return Object.keys(SLIDE_MODES);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeSlideMode(value, options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
allowEmpty = false,
|
|
36
|
+
optionName = '--mode',
|
|
37
|
+
} = options;
|
|
38
|
+
|
|
39
|
+
if (typeof value !== 'string') {
|
|
40
|
+
if (allowEmpty) {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`${optionName} must be one of: ${getSlideModeChoices().join(', ')}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const normalized = value.trim().toLowerCase();
|
|
47
|
+
if (normalized === '') {
|
|
48
|
+
if (allowEmpty) {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`${optionName} must be one of: ${getSlideModeChoices().join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!SLIDE_MODES[normalized]) {
|
|
55
|
+
throw new Error(`Unknown ${optionName} value: ${value}. Expected one of: ${getSlideModeChoices().join(', ')}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getSlideModeConfig(value = DEFAULT_SLIDE_MODE) {
|
|
62
|
+
return SLIDE_MODES[normalizeSlideMode(value, { optionName: '--mode' })];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
DEFAULT_SLIDE_MODE,
|
|
67
|
+
PT_TO_PX,
|
|
68
|
+
SLIDE_MODES,
|
|
69
|
+
getSlideModeChoices,
|
|
70
|
+
getSlideModeConfig,
|
|
71
|
+
normalizeSlideMode,
|
|
72
|
+
};
|
package/src/validation/cli.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const {
|
|
5
|
+
DEFAULT_SLIDE_MODE,
|
|
6
|
+
getSlideModeChoices,
|
|
7
|
+
normalizeSlideMode,
|
|
8
|
+
} = require('../slide-mode.cjs');
|
|
9
|
+
|
|
1
10
|
export const DEFAULT_SLIDES_DIR = 'slides';
|
|
2
11
|
export const DEFAULT_VALIDATE_FORMAT = 'concise';
|
|
3
12
|
export const VALIDATE_FORMATS = ['concise', 'json', 'json-full'];
|
|
@@ -14,6 +23,7 @@ export function parseValidateCliArgs(args) {
|
|
|
14
23
|
const options = {
|
|
15
24
|
slidesDir: DEFAULT_SLIDES_DIR,
|
|
16
25
|
format: DEFAULT_VALIDATE_FORMAT,
|
|
26
|
+
mode: DEFAULT_SLIDE_MODE,
|
|
17
27
|
help: false,
|
|
18
28
|
slides: [],
|
|
19
29
|
};
|
|
@@ -48,6 +58,17 @@ export function parseValidateCliArgs(args) {
|
|
|
48
58
|
continue;
|
|
49
59
|
}
|
|
50
60
|
|
|
61
|
+
if (arg === '--mode') {
|
|
62
|
+
options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
|
|
63
|
+
i += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (arg.startsWith('--mode=')) {
|
|
68
|
+
options.mode = normalizeSlideMode(arg.slice('--mode='.length));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
51
72
|
if (arg === '--slide') {
|
|
52
73
|
options.slides.push(readOptionValue(args, i, '--slide'));
|
|
53
74
|
i += 1;
|
|
@@ -72,6 +93,7 @@ export function parseValidateCliArgs(args) {
|
|
|
72
93
|
|
|
73
94
|
options.slidesDir = options.slidesDir.trim();
|
|
74
95
|
options.format = options.format.trim();
|
|
96
|
+
options.mode = normalizeSlideMode(options.mode);
|
|
75
97
|
|
|
76
98
|
if (!VALIDATE_FORMATS.includes(options.format)) {
|
|
77
99
|
throw new Error(`Unknown --format value: ${options.format}. Expected one of: ${VALIDATE_FORMATS.join(', ')}`);
|
|
@@ -91,6 +113,7 @@ export function getValidateUsage() {
|
|
|
91
113
|
'Options:',
|
|
92
114
|
` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
|
|
93
115
|
` --format <format> Output format: ${VALIDATE_FORMATS.join(', ')} (default: ${DEFAULT_VALIDATE_FORMAT})`,
|
|
116
|
+
` --mode <mode> Slide mode: ${getSlideModeChoices().join(', ')} (default: ${DEFAULT_SLIDE_MODE})`,
|
|
94
117
|
' --slide <file> Validate only the named slide file (repeatable)',
|
|
95
118
|
' -h, --help Show this help message',
|
|
96
119
|
].join('\n');
|
package/src/validation/core.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { access, readdir } from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
2
3
|
import { basename, join } from 'node:path';
|
|
3
4
|
import { pathToFileURL } from 'node:url';
|
|
4
5
|
import { chromium } from 'playwright';
|
|
@@ -9,12 +10,14 @@ import {
|
|
|
9
10
|
resolveSlideSourcePath,
|
|
10
11
|
} from '../image-contract.js';
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const {
|
|
15
|
+
DEFAULT_SLIDE_MODE,
|
|
16
|
+
getSlideModeConfig,
|
|
17
|
+
} = require('../slide-mode.cjs');
|
|
18
|
+
|
|
19
|
+
export const FRAME_PT = getSlideModeConfig(DEFAULT_SLIDE_MODE).framePt;
|
|
20
|
+
export const FRAME_PX = getSlideModeConfig(DEFAULT_SLIDE_MODE).framePx;
|
|
18
21
|
export const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
|
|
19
22
|
export const TEXT_SELECTOR = 'p,h1,h2,h3,h4,h5,h6,li';
|
|
20
23
|
export const TOLERANCE_PX = 0.5;
|
|
@@ -58,28 +61,30 @@ export function summarizeSlides(slides) {
|
|
|
58
61
|
return summary;
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
export function createValidationResult(slides) {
|
|
64
|
+
export function createValidationResult(slides, slideMode = DEFAULT_SLIDE_MODE) {
|
|
65
|
+
const { framePt, framePx } = getSlideModeConfig(slideMode);
|
|
62
66
|
return {
|
|
63
67
|
generatedAt: new Date().toISOString(),
|
|
64
68
|
frame: {
|
|
65
|
-
widthPt:
|
|
66
|
-
heightPt:
|
|
67
|
-
widthPx:
|
|
68
|
-
heightPx:
|
|
69
|
+
widthPt: framePt.width,
|
|
70
|
+
heightPt: framePt.height,
|
|
71
|
+
widthPx: framePx.width,
|
|
72
|
+
heightPx: framePx.height,
|
|
69
73
|
},
|
|
70
74
|
slides,
|
|
71
75
|
summary: summarizeSlides(slides),
|
|
72
76
|
};
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
export function createValidationFailure(error) {
|
|
79
|
+
export function createValidationFailure(error, slideMode = DEFAULT_SLIDE_MODE) {
|
|
80
|
+
const { framePt, framePx } = getSlideModeConfig(slideMode);
|
|
76
81
|
return {
|
|
77
82
|
generatedAt: new Date().toISOString(),
|
|
78
83
|
frame: {
|
|
79
|
-
widthPt:
|
|
80
|
-
heightPt:
|
|
81
|
-
widthPx:
|
|
82
|
-
heightPx:
|
|
84
|
+
widthPt: framePt.width,
|
|
85
|
+
heightPt: framePt.height,
|
|
86
|
+
widthPx: framePx.width,
|
|
87
|
+
heightPx: framePx.height,
|
|
83
88
|
},
|
|
84
89
|
slides: [],
|
|
85
90
|
summary: {
|
|
@@ -347,9 +352,10 @@ export function selectSlideFiles(slideFiles, selectedSlides = [], slidesDir = ''
|
|
|
347
352
|
return slideFiles.filter((slide) => available.has(slide) && requested.includes(slide));
|
|
348
353
|
}
|
|
349
354
|
|
|
350
|
-
export async function inspectSlide(page, fileName, slidesDir) {
|
|
355
|
+
export async function inspectSlide(page, fileName, slidesDir, slideMode = DEFAULT_SLIDE_MODE) {
|
|
351
356
|
const slidePath = join(slidesDir, fileName);
|
|
352
357
|
const slideUrl = pathToFileURL(slidePath).href;
|
|
358
|
+
const { framePx, sizeLabel } = getSlideModeConfig(slideMode);
|
|
353
359
|
|
|
354
360
|
await page.goto(slideUrl, { waitUntil: 'load' });
|
|
355
361
|
await page.evaluate(async () => {
|
|
@@ -359,7 +365,7 @@ export async function inspectSlide(page, fileName, slidesDir) {
|
|
|
359
365
|
});
|
|
360
366
|
|
|
361
367
|
const inspection = await page.evaluate(
|
|
362
|
-
({ framePx, textSelector, tolerancePx }) => {
|
|
368
|
+
({ framePx, sizeLabel, textSelector, tolerancePx }) => {
|
|
363
369
|
const skipTags = new Set(['SCRIPT', 'STYLE', 'META', 'LINK', 'HEAD', 'TITLE', 'NOSCRIPT']);
|
|
364
370
|
const critical = [];
|
|
365
371
|
const warning = [];
|
|
@@ -552,7 +558,7 @@ export async function inspectSlide(page, fileName, slidesDir) {
|
|
|
552
558
|
if (outsideFrame) {
|
|
553
559
|
critical.push({
|
|
554
560
|
code: 'overflow-outside-frame',
|
|
555
|
-
message:
|
|
561
|
+
message: `Element exceeds the ${sizeLabel} slide frame.`,
|
|
556
562
|
element: elementPath(element),
|
|
557
563
|
bbox: normalizeRect(rect),
|
|
558
564
|
frame: normalizeRect(frameRect),
|
|
@@ -663,7 +669,8 @@ export async function inspectSlide(page, fileName, slidesDir) {
|
|
|
663
669
|
};
|
|
664
670
|
},
|
|
665
671
|
{
|
|
666
|
-
framePx
|
|
672
|
+
framePx,
|
|
673
|
+
sizeLabel,
|
|
667
674
|
textSelector: TEXT_SELECTOR,
|
|
668
675
|
tolerancePx: TOLERANCE_PX,
|
|
669
676
|
},
|
|
@@ -696,12 +703,12 @@ export async function inspectSlide(page, fileName, slidesDir) {
|
|
|
696
703
|
};
|
|
697
704
|
}
|
|
698
705
|
|
|
699
|
-
export async function scanSlides(page, slidesDir, slideFiles) {
|
|
706
|
+
export async function scanSlides(page, slidesDir, slideFiles, slideMode = DEFAULT_SLIDE_MODE) {
|
|
700
707
|
const slides = [];
|
|
701
708
|
|
|
702
709
|
for (const slideFile of slideFiles) {
|
|
703
710
|
try {
|
|
704
|
-
const result = await inspectSlide(page, slideFile, slidesDir);
|
|
711
|
+
const result = await inspectSlide(page, slideFile, slidesDir, slideMode);
|
|
705
712
|
slides.push(result);
|
|
706
713
|
} catch (error) {
|
|
707
714
|
slides.push({
|
|
@@ -740,7 +747,10 @@ export function formatValidationFailureForExport(result, exportLabel = 'Export')
|
|
|
740
747
|
}
|
|
741
748
|
|
|
742
749
|
const suffix = findings.length > 0 ? `\n${findings.join('\n')}` : '';
|
|
743
|
-
|
|
750
|
+
const modeHint = result.slideMode && result.slideMode !== DEFAULT_SLIDE_MODE
|
|
751
|
+
? ` --mode ${result.slideMode}`
|
|
752
|
+
: '';
|
|
753
|
+
return `${exportLabel} blocked by slide validation. Run \`slides-grab validate --slides-dir <path>${modeHint}\` for full diagnostics.${suffix}`;
|
|
744
754
|
}
|
|
745
755
|
|
|
746
756
|
const EXPORT_BLOCKING_IMAGE_CONTRACT_CODES = new Set([
|
|
@@ -799,6 +809,7 @@ export async function ensureSlidesPassValidation(
|
|
|
799
809
|
slidesDir,
|
|
800
810
|
{
|
|
801
811
|
exportLabel = 'Export',
|
|
812
|
+
slideMode = DEFAULT_SLIDE_MODE,
|
|
802
813
|
shouldBlockIssue = isBlockingImageContractIssue,
|
|
803
814
|
} = {},
|
|
804
815
|
) {
|
|
@@ -812,8 +823,11 @@ export async function ensureSlidesPassValidation(
|
|
|
812
823
|
const page = await context.newPage();
|
|
813
824
|
|
|
814
825
|
try {
|
|
815
|
-
const slides = await scanSlides(page, slidesDir, slideFiles);
|
|
816
|
-
const result =
|
|
826
|
+
const slides = await scanSlides(page, slidesDir, slideFiles, slideMode);
|
|
827
|
+
const result = {
|
|
828
|
+
...createValidationResult(slides, slideMode),
|
|
829
|
+
slideMode,
|
|
830
|
+
};
|
|
817
831
|
const blockingResult = filterExportBlockingSlides(result, shouldBlockIssue);
|
|
818
832
|
if (blockingResult.summary.failedSlides > 0) {
|
|
819
833
|
throw new Error(formatValidationFailureForExport(blockingResult, exportLabel));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Design Style Collections
|
|
2
|
+
|
|
3
|
+
slides-grab bundles 35 design styles: 30 derived from [corazzon/pptx-design-styles](https://github.com/corazzon/pptx-design-styles) (MIT) plus 5 slides-grab originals.
|
|
4
|
+
|
|
5
|
+
These styles are reference directions for slide generation, not drop-in HTML slide templates. Agents may also design fully custom visuals beyond the bundled collection.
|
|
6
|
+
|
|
7
|
+
## Recommended workflow
|
|
8
|
+
|
|
9
|
+
1. `slides-grab list-styles`
|
|
10
|
+
2. `slides-grab preview-styles` to open the visual gallery in browser
|
|
11
|
+
3. Tell the agent which style to use, or ask for something custom
|
|
12
|
+
|
|
13
|
+
The preview/select flow is intentionally simple: it keeps design approval inside the CLI and a local HTML preview page instead of adding a separate app.
|
|
14
|
+
|
|
15
|
+
## Citation
|
|
16
|
+
|
|
17
|
+
- Upstream collection: `corazzon/pptx-design-styles`
|
|
18
|
+
- URL: <https://github.com/corazzon/pptx-design-styles>
|
|
19
|
+
- Reference used in this repo: `references/styles.md`
|