slides-grab 1.2.0 → 1.2.2

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.
@@ -4,6 +4,7 @@ import { pathToFileURL } from 'node:url';
4
4
  import { chromium } from 'playwright';
5
5
  import {
6
6
  buildImageContractReport,
7
+ buildVideoContractReport,
7
8
  classifyImageSource,
8
9
  resolveSlideSourcePath,
9
10
  } from '../image-contract.js';
@@ -114,6 +115,18 @@ async function fileExists(filePath) {
114
115
  }
115
116
  }
116
117
 
118
+ function shouldSkipLocalAssetExistenceCheck(classification) {
119
+ return (
120
+ classification.kind === 'empty'
121
+ || classification.kind === 'data-url'
122
+ || classification.kind === 'remote-url'
123
+ || classification.kind === 'remote-url-insecure'
124
+ || classification.kind === 'absolute-filesystem-path'
125
+ || classification.kind === 'root-relative-path'
126
+ || classification.kind === 'other-scheme'
127
+ );
128
+ }
129
+
117
130
  async function inspectImageContract(slidesDir, fileName, inspection) {
118
131
  const critical = [];
119
132
  const warning = [];
@@ -135,15 +148,7 @@ async function inspectImageContract(slidesDir, fileName, inspection) {
135
148
  target.push(issue);
136
149
  }
137
150
 
138
- if (
139
- classification.kind === 'empty'
140
- || classification.kind === 'data-url'
141
- || classification.kind === 'remote-url'
142
- || classification.kind === 'remote-url-insecure'
143
- || classification.kind === 'absolute-filesystem-path'
144
- || classification.kind === 'root-relative-path'
145
- || classification.kind === 'other-scheme'
146
- ) {
151
+ if (shouldSkipLocalAssetExistenceCheck(classification)) {
147
152
  continue;
148
153
  }
149
154
 
@@ -201,15 +206,7 @@ async function inspectImageContract(slidesDir, fileName, inspection) {
201
206
 
202
207
  for (const source of background.urls) {
203
208
  const classification = classifyImageSource(source);
204
- if (
205
- classification.kind === 'empty'
206
- || classification.kind === 'data-url'
207
- || classification.kind === 'remote-url'
208
- || classification.kind === 'remote-url-insecure'
209
- || classification.kind === 'absolute-filesystem-path'
210
- || classification.kind === 'root-relative-path'
211
- || classification.kind === 'other-scheme'
212
- ) {
209
+ if (shouldSkipLocalAssetExistenceCheck(classification)) {
213
210
  continue;
214
211
  }
215
212
 
@@ -233,6 +230,94 @@ async function inspectImageContract(slidesDir, fileName, inspection) {
233
230
  return { critical, warning };
234
231
  }
235
232
 
233
+ async function inspectVideoContract(slidesDir, fileName, inspection) {
234
+ const critical = [];
235
+ const warning = [];
236
+ const slidePath = join(slidesDir, fileName);
237
+
238
+ for (const video of inspection.videos) {
239
+ const sources = [...new Set([
240
+ typeof video.src === 'string' ? video.src : '',
241
+ ...video.sources,
242
+ ].map((source) => source.trim()).filter(Boolean))];
243
+
244
+ const issues = buildVideoContractReport({
245
+ slideFile: fileName,
246
+ sources: sources.map((source) => ({
247
+ element: buildElementPath(video.element),
248
+ source,
249
+ })),
250
+ });
251
+
252
+ for (const issue of issues) {
253
+ const target = issue.severity === 'critical' ? critical : warning;
254
+ target.push(issue);
255
+ }
256
+
257
+ for (const source of sources) {
258
+ const classification = classifyImageSource(source);
259
+ if (shouldSkipLocalAssetExistenceCheck(classification)) {
260
+ continue;
261
+ }
262
+
263
+ const assetPath = resolveSlideSourcePath(slidePath, source);
264
+ if (!(await fileExists(assetPath))) {
265
+ critical.push(buildImageIssue(
266
+ 'critical',
267
+ 'missing-local-video-asset',
268
+ 'Local video asset is missing.',
269
+ {
270
+ slide: fileName,
271
+ element: buildElementPath(video.element),
272
+ source,
273
+ assetPath,
274
+ },
275
+ ));
276
+ }
277
+ }
278
+
279
+ const poster = typeof video.poster === 'string' ? video.poster.trim() : '';
280
+ if (!poster) {
281
+ continue;
282
+ }
283
+
284
+ const posterIssues = buildImageContractReport({
285
+ slideFile: fileName,
286
+ sources: [{
287
+ element: buildElementPath(video.element),
288
+ source: poster,
289
+ }],
290
+ });
291
+
292
+ for (const issue of posterIssues) {
293
+ const target = issue.severity === 'critical' ? critical : warning;
294
+ target.push(issue);
295
+ }
296
+
297
+ const posterClassification = classifyImageSource(poster);
298
+ if (shouldSkipLocalAssetExistenceCheck(posterClassification)) {
299
+ continue;
300
+ }
301
+
302
+ const posterPath = resolveSlideSourcePath(slidePath, poster);
303
+ if (!(await fileExists(posterPath))) {
304
+ critical.push(buildImageIssue(
305
+ 'critical',
306
+ 'missing-local-video-poster-asset',
307
+ 'Video poster image is missing.',
308
+ {
309
+ slide: fileName,
310
+ element: buildElementPath(video.element),
311
+ source: poster,
312
+ assetPath: posterPath,
313
+ },
314
+ ));
315
+ }
316
+ }
317
+
318
+ return { critical, warning };
319
+ }
320
+
236
321
  export async function findSlideFiles(slidesDir) {
237
322
  const entries = await readdir(slidesDir, { withFileTypes: true });
238
323
  return entries
@@ -417,6 +502,31 @@ export async function inspectSlide(page, fileName, slidesDir) {
417
502
  return values;
418
503
  };
419
504
 
505
+ // Detect persisted runtime-only editor/viewer injections.
506
+ const baseElements = Array.from(document.querySelectorAll('head base[href]'));
507
+ for (const base of baseElements) {
508
+ critical.push({
509
+ code: 'persisted-editor-base-tag',
510
+ message: 'Slide contains a <base> tag injected by the editor runtime. Remove it so asset paths resolve correctly outside the editor.',
511
+ element: 'head > base',
512
+ detail: base.getAttribute('href'),
513
+ });
514
+ }
515
+
516
+ const editorScriptSignatures = ['[slides-grab:image]', '[slides-grab:'];
517
+ const scripts = Array.from(document.querySelectorAll('head script:not([src])'));
518
+ for (const script of scripts) {
519
+ const text = script.textContent || '';
520
+ const matched = editorScriptSignatures.some((sig) => text.includes(sig));
521
+ if (matched) {
522
+ critical.push({
523
+ code: 'persisted-editor-script',
524
+ message: 'Slide contains a runtime-only editor script that should not be persisted. Remove the injected <script> block.',
525
+ element: 'head > script',
526
+ });
527
+ }
528
+ }
529
+
420
530
  const bodyRect = document.body.getBoundingClientRect();
421
531
  const frameRect = {
422
532
  left: bodyRect.left,
@@ -519,6 +629,15 @@ export async function inspectSlide(page, fileName, slidesDir) {
519
629
  alt: (element.getAttribute('alt') || '').trim(),
520
630
  }));
521
631
 
632
+ const videos = Array.from(document.querySelectorAll('video')).map((element) => ({
633
+ element: elementPath(element),
634
+ src: (element.getAttribute('src') || '').trim(),
635
+ sources: Array.from(element.querySelectorAll('source[src]'))
636
+ .map((source) => (source.getAttribute('src') || '').trim())
637
+ .filter(Boolean),
638
+ poster: (element.getAttribute('poster') || '').trim(),
639
+ }));
640
+
522
641
  const backgrounds = [document.body, ...Array.from(document.body.querySelectorAll('*'))]
523
642
  .map((element) => {
524
643
  const computedBackgroundImage = window.getComputedStyle(element).backgroundImage;
@@ -539,6 +658,7 @@ export async function inspectSlide(page, fileName, slidesDir) {
539
658
  critical,
540
659
  warning,
541
660
  images,
661
+ videos,
542
662
  backgrounds,
543
663
  };
544
664
  },
@@ -553,9 +673,14 @@ export async function inspectSlide(page, fileName, slidesDir) {
553
673
  images: inspection.images,
554
674
  backgrounds: inspection.backgrounds,
555
675
  });
676
+ const videoContractIssues = await inspectVideoContract(slidesDir, fileName, {
677
+ videos: inspection.videos,
678
+ });
556
679
 
557
680
  inspection.critical.push(...imageContractIssues.critical);
558
681
  inspection.warning.push(...imageContractIssues.warning);
682
+ inspection.critical.push(...videoContractIssues.critical);
683
+ inspection.warning.push(...videoContractIssues.warning);
559
684
 
560
685
  const summary = {
561
686
  criticalCount: inspection.critical.length,
@@ -620,13 +745,21 @@ export function formatValidationFailureForExport(result, exportLabel = 'Export')
620
745
 
621
746
  const EXPORT_BLOCKING_IMAGE_CONTRACT_CODES = new Set([
622
747
  'absolute-filesystem-image-path',
748
+ 'absolute-filesystem-video-path',
623
749
  'missing-local-asset',
624
750
  'missing-local-background-asset',
751
+ 'missing-local-video-asset',
752
+ 'missing-local-video-poster-asset',
625
753
  'remote-background-image-url',
626
754
  'remote-background-image-url-insecure',
627
755
  'remote-image-url',
628
756
  'remote-image-url-insecure',
757
+ 'remote-video-url',
758
+ 'remote-video-url-insecure',
629
759
  'root-relative-image-path',
760
+ 'root-relative-video-path',
761
+ 'unsupported-image-url-scheme',
762
+ 'unsupported-video-url-scheme',
630
763
  'unsupported-background-image',
631
764
  ]);
632
765
 
@@ -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`