slides-grab 1.2.0 → 1.2.1

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.
@@ -25,20 +25,28 @@ Generate high-quality `slide-XX.html` files in the selected slides workspace (`s
25
25
  1. Read approved `slide-outline.md`.
26
26
  2. Before generating slides, write a quick **visual thesis** (mood/material/energy), a **content plan** (opener → support/proof → detail/story → close/CTA), and the core design tokens (background, surface, text, muted, accent + display/headline/body/caption roles).
27
27
  3. Generate slide HTML files with 2-digit numbering in selected `--slides-dir`.
28
- 4. If the deck needs a complex diagram (architecture, workflows, relationship maps, multi-node concepts), create the diagram in `tldraw`, export it with `slides-grab tldraw`, and treat the result as a local slide asset under `<slides-dir>/assets/`.
29
- 5. Run `slides-grab validate --slides-dir <path>` after generation or edits.
30
- 6. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.
31
- 7. Run `slides-grab build-viewer --slides-dir <path>` only after validation passes.
32
- 8. Run the slide litmus check from `references/beautiful-slide-defaults.md` before presenting the deck for review.
33
- 9. Iterate on user feedback by editing only requested slide files, then re-run validation and rebuild the viewer.
34
- 10. Keep revising until user approves conversion stage.
28
+ 4. When a slide explicitly needs bespoke imagery, when the user asks for an image, or when stronger imagery would materially improve the slide, prefer `slides-grab image --prompt "<prompt>" --slides-dir <path>` to generate a local asset with Nano Banana Pro and save it under `<slides-dir>/assets/`.
29
+ 5. If the deck needs a complex diagram (architecture, workflows, relationship maps, multi-node concepts), create the diagram in `tldraw`, export it with `slides-grab tldraw`, and treat the result as a local slide asset under `<slides-dir>/assets/`.
30
+ 6. If the slide needs a local video, store the video under `<slides-dir>/assets/`, reference it as `./assets/<file>`, and prefer a `poster="./assets/<file>"` thumbnail so PDF export uses a stable still image.
31
+ 7. If the source video starts on YouTube or another supported page, use `slides-grab fetch-video --url <youtube-url> --slides-dir <path>` (or `yt-dlp` directly if needed) to download it into `<slides-dir>/assets/` before saving the slide HTML.
32
+ 8. Run `slides-grab validate --slides-dir <path>` after generation or edits.
33
+ 9. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.
34
+ 10. Run the slide litmus check from `references/beautiful-slide-defaults.md` before presenting the deck for review.
35
+ 11. Launch the interactive editor for visual review: `slides-grab edit --slides-dir <path>`
36
+ 12. Iterate on user feedback by editing only requested slide files, then re-run validation after each edit round.
37
+ 13. When the user confirms editing is complete, suggest: build the viewer (`slides-grab build-viewer --slides-dir <path>`) for a final read-only preview, or proceed to export (PDF/PPTX).
38
+ 14. Keep revising until user approves conversion stage.
35
39
 
36
40
  ## Rules
37
41
  - Keep slide size 720pt x 405pt.
38
42
  - Keep semantic text tags (`p`, `h1-h6`, `ul`, `ol`, `li`).
39
- - Put local images under `<slides-dir>/assets/` and reference them as `./assets/<file>`.
43
+ - Put local images and videos under `<slides-dir>/assets/` and reference them as `./assets/<file>`.
40
44
  - Allow `data:` URLs when the slide must be fully self-contained.
41
45
  - Do not leave remote `http(s)://` image URLs in saved slide HTML; download source images into `<slides-dir>/assets/` and reference them as `./assets/<file>`.
46
+ - Prefer `slides-grab image` with Nano Banana Pro for bespoke slide imagery before reaching for remote URLs.
47
+ - If `GOOGLE_API_KEY` (or `GEMINI_API_KEY`) is unavailable or the Nano Banana API fails, ask the user for a Google API key or fall back to web search + download into `<slides-dir>/assets/`.
48
+ - Prefer local videos with a `poster="./assets/<file>"` thumbnail so PDF export uses the still image.
49
+ - Use `slides-grab fetch-video` or `yt-dlp` to pull supported web videos into `<slides-dir>/assets/` before saving slide HTML.
42
50
  - Prefer `<img>` for slide imagery and `data-image-placeholder` when no final asset exists.
43
51
  - Default to one job per slide, one dominant visual anchor, and copy that scans in seconds.
44
52
  - Treat opening slides and section dividers like posters, not dashboards.
@@ -6,6 +6,8 @@ These are the packaged design rules for installable `slides-grab` skills.
6
6
  - Validate slides: `slides-grab validate --slides-dir <path>`
7
7
  - Build review viewer: `slides-grab build-viewer --slides-dir <path>`
8
8
  - Launch editor: `slides-grab edit --slides-dir <path>`
9
+ - Generate a bespoke image asset: `slides-grab image --prompt "<prompt>" --slides-dir <path>`
10
+ - Download a web video into slide assets: `slides-grab fetch-video --url <youtube-url> --slides-dir <path>`
9
11
  - Render `tldraw` diagrams: `slides-grab tldraw --input <path> --output <path>`
10
12
 
11
13
  ## Slide spec
@@ -18,7 +20,11 @@ These are the packaged design rules for installable `slides-grab` skills.
18
20
  ## Asset rules
19
21
  - Store deck-local assets in `<slides-dir>/assets/`
20
22
  - Reference deck-local assets as `./assets/<file>`
23
+ - Use `slides-grab image --prompt "<prompt>" --slides-dir <path>` with Nano Banana Pro for bespoke generated images when helpful
21
24
  - If an image comes from the web, download it into `<slides-dir>/assets/` before referencing it
25
+ - If a video comes from YouTube or another supported page, use `slides-grab fetch-video` (or `yt-dlp` directly) to download it into `<slides-dir>/assets/` before referencing it
26
+ - Keep local videos and their poster thumbnails together under `<slides-dir>/assets/`
27
+ - If `GOOGLE_API_KEY` / `GEMINI_API_KEY` is unavailable, ask the user for a Google API key or fall back to web search + download
22
28
  - Use `tldraw`-generated local assets for complex diagrams when possible
23
29
  - Allow `data:` URLs only when the slide must be fully self-contained
24
30
  - Do not leave remote `http(s)://` image URLs in saved slide HTML
@@ -49,6 +55,7 @@ These are the packaged design rules for installable `slides-grab` skills.
49
55
 
50
56
  ## Review loop
51
57
  - Generate or edit only the needed slide files.
58
+ - Prefer `slides-grab image` before remote image sourcing when the slide needs bespoke imagery.
52
59
  - Prefer `tldraw` for complex diagrams instead of hand-building dense diagram geometry in HTML/CSS.
53
60
  - Re-run validation after every generation/edit pass.
54
61
  - Rebuild the viewer only after validation passes.
@@ -7,11 +7,15 @@
7
7
 
8
8
  ### 4. Image Usage Rules (Local Asset / Data URL / Remote URL / Placeholder)
9
9
  - Always include alt on img tags.
10
- - Use `./assets/<file>` as the default image contract for slide HTML.
10
+ - Use `./assets/<file>` as the default image and video contract for slide HTML.
11
11
  - Keep slide assets in `<slides-dir>/assets/`.
12
12
  - Use `tldraw`-generated assets for complex diagrams whenever possible.
13
+ - Use `slides-grab image --prompt "<prompt>" --slides-dir <path>` with Nano Banana Pro when a slide needs bespoke generated imagery.
13
14
  - `data:` URLs are allowed for fully self-contained slides.
14
15
  - Do not leave remote `http(s)://` image URLs in saved slide HTML; download source images into `<slides-dir>/assets/` and reference them as `./assets/<file>`.
16
+ - Store local videos under `<slides-dir>/assets/`, reference them as `./assets/<file>`, and prefer `poster="./assets/<file>"` for export-friendly thumbnails.
17
+ - If a video starts on YouTube or another supported page, use `slides-grab fetch-video --url <youtube-url> --slides-dir <path>` (or `yt-dlp` directly if needed) before saving the slide HTML.
18
+ - If `GOOGLE_API_KEY` or `GEMINI_API_KEY` is unavailable, or the Nano Banana API fails, ask the user for a Google API key or fall back to web search + download into `<slides-dir>/assets/`.
15
19
  - Do not use absolute filesystem paths in slide HTML.
16
20
  - Do not use non-body `background-image` for content imagery; use `<img>` instead.
17
21
  - Use `data-image-placeholder` to reserve space when no image is available yet.
@@ -24,7 +28,9 @@
24
28
  - After slide generation or edits, run `slides-grab validate --slides-dir <path>`.
25
29
  - After validation passes, run `slides-grab build-viewer --slides-dir <path>`.
26
30
  - Edit only the relevant HTML file during revision loops.
31
+ - When the brief explicitly calls for an image, the user requests one, or the slide clearly benefits from it, prefer `slides-grab image` before falling back to remote image sourcing.
27
32
  - Prefer `slides-grab tldraw` + local exported assets for architecture, workflow, relationship, and other complex diagrams.
33
+ - Keep local videos and their poster thumbnails together under `<slides-dir>/assets/`.
28
34
  - Never start PPTX conversion without explicit approval.
29
35
  - Never forget to build the viewer after slide changes.
30
36
  - Do not persist runtime-only editor/viewer injections in saved slide HTML.
@@ -32,6 +38,6 @@
32
38
  ## Important Notes
33
39
  - CSS gradients are not supported in PowerPoint conversion; replace them with background images.
34
40
  - Always include the Pretendard CDN link.
35
- - Use `./assets/<file>` from each `slide-XX.html` and avoid absolute filesystem paths.
41
+ - Use `./assets/<file>` from each `slide-XX.html` for local images and videos, and avoid absolute filesystem paths.
36
42
  - Always include `#` prefix in CSS colors.
37
43
  - Never place text directly in `div`/`span`.
@@ -37,19 +37,24 @@ const EDITOR_PPT_DESIGN_DUPLICATE_PATTERNS = [
37
37
  const EDITOR_PPT_DESIGN_SKILL_FALLBACK = [
38
38
  '## Workflow',
39
39
  '1. Read approved `slide-outline.md` or the existing slide before editing.',
40
- '2. Run `slides-grab validate --slides-dir <path>` after generation or edits.',
41
- '3. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.',
42
- '4. Run `slides-grab build-viewer --slides-dir <path>` only after validation passes.',
43
- '5. Run the slide litmus check from `references/beautiful-slide-defaults.md` before presenting the deck for review.',
44
- '6. Iterate on user feedback by editing only requested slide files, then re-run validation and rebuild the viewer.',
45
- '7. Keep revising until user approves conversion stage.',
40
+ '2. When a slide needs bespoke imagery, prefer `slides-grab image --prompt "<prompt>" --slides-dir <path>` so Nano Banana Pro saves a local asset under `<slides-dir>/assets/`.',
41
+ '3. Run `slides-grab validate --slides-dir <path>` after generation or edits.',
42
+ '4. If validation fails, automatically fix the source slide HTML/CSS and re-run validation until it passes.',
43
+ '5. Run `slides-grab build-viewer --slides-dir <path>` only after validation passes.',
44
+ '6. Run the slide litmus check from `references/beautiful-slide-defaults.md` before presenting the deck for review.',
45
+ '7. Iterate on user feedback by editing only requested slide files, then re-run validation and rebuild the viewer.',
46
+ '8. Keep revising until user approves conversion stage.',
46
47
  '',
47
48
  '## Rules',
48
49
  '- Keep slide size 720pt x 405pt.',
49
50
  '- Keep semantic text tags (`p`, `h1-h6`, `ul`, `ol`, `li`).',
50
- '- Put local images under `<slides-dir>/assets/` and reference them as `./assets/<file>`.',
51
+ '- Put local images and videos under `<slides-dir>/assets/` and reference them as `./assets/<file>`.',
51
52
  '- Allow `data:` URLs when the slide must be fully self-contained.',
52
53
  '- Do not leave remote `http(s)://` image URLs in saved slide HTML; download source images into `<slides-dir>/assets/` and reference them as `./assets/<file>`.',
54
+ '- For local videos, use `<video src="./assets/<file>">` and prefer `poster="./assets/<file>"` so PDF export can use a thumbnail.',
55
+ '- If a video starts on YouTube or another supported page, use `slides-grab fetch-video --url <youtube-url> --slides-dir <path>` (or `yt-dlp` directly if needed) to download it into `<slides-dir>/assets/` before saving the slide HTML.',
56
+ '- Prefer `slides-grab image` with Nano Banana Pro for bespoke imagery when it improves the slide.',
57
+ '- If `GOOGLE_API_KEY` or `GEMINI_API_KEY` is unavailable, or the Nano Banana API fails, ask the user for a Google API key or fall back to web search + download into `<slides-dir>/assets/`.',
53
58
  '- Prefer `<img>` for slide imagery and `data-image-placeholder` when no final asset exists.',
54
59
  '- Do not present slides for review until `slides-grab validate --slides-dir <path>` passes.',
55
60
  '- Do not start conversion before approval.',
@@ -65,10 +70,14 @@ const DETAILED_DESIGN_SKILL_FALLBACK = [
65
70
  '',
66
71
  '### 4. Image Usage Rules (Local Asset / Data URL / Remote URL / Placeholder)',
67
72
  '- Always include alt on img tags.',
68
- '- Use ./assets/<file> as the default image contract for slide HTML.',
73
+ '- Use ./assets/<file> as the default image and video contract for slide HTML.',
69
74
  '- Keep slide assets in <slides-dir>/assets/.',
75
+ '- Use `slides-grab image --prompt "<prompt>" --slides-dir <path>` with Nano Banana Pro when the slide needs bespoke imagery.',
70
76
  '- data: URLs are allowed for fully self-contained slides.',
71
77
  '- Do not leave remote http(s):// image URLs in saved slide HTML; download source images into <slides-dir>/assets/ and reference them as ./assets/<file>.',
78
+ '- Store local videos under <slides-dir>/assets/, reference them as ./assets/<file>, and prefer poster images under ./assets/ for PDF export.',
79
+ '- If a video starts on YouTube or another supported page, use slides-grab fetch-video --url <youtube-url> --slides-dir <path> (or yt-dlp directly if needed) before saving slide HTML.',
80
+ '- If GOOGLE_API_KEY or GEMINI_API_KEY is unavailable, or the Nano Banana API fails, ask the user for a Google API key or fall back to web search + download into <slides-dir>/assets/.',
72
81
  '- Do not use absolute filesystem paths in slide HTML.',
73
82
  '- Do not use non-body background-image for content imagery; use <img> instead.',
74
83
  '- Use data-image-placeholder to reserve space when no image is available yet.',
@@ -80,13 +89,14 @@ const DETAILED_DESIGN_SKILL_FALLBACK = [
80
89
  '## Workflow (Stage 2: Design + Human Review)',
81
90
  '- After slide generation or edits, run slides-grab build-viewer --slides-dir <path>.',
82
91
  '- Edit only the relevant HTML file during revision loops.',
92
+ '- Prefer slides-grab image before remote image sourcing when a slide explicitly needs bespoke imagery.',
83
93
  '- Never start PPTX conversion without explicit approval.',
84
94
  '- Never forget to rebuild the viewer after slide changes.',
85
95
  '',
86
96
  '## Important Notes',
87
97
  '- CSS gradients are not supported in PowerPoint conversion; replace them with background images.',
88
98
  '- Always include the Pretendard CDN link.',
89
- '- Use ./assets/<file> from each slide-XX.html and avoid absolute filesystem paths.',
99
+ '- Use ./assets/<file> from each slide-XX.html for local images and videos, and avoid absolute filesystem paths.',
90
100
  '- Always include # prefix in CSS colors.',
91
101
  '- Never place text directly in div/span.',
92
102
  ].join('\n');
@@ -396,8 +406,11 @@ export function buildCodexEditPrompt({ slideFile, slidePath, userPrompt, selecti
396
406
  '- Keep existing structure/content unless the request requires a change.',
397
407
  '- Keep slide dimensions at 720pt x 405pt.',
398
408
  '- Keep text in semantic tags (<p>, <h1>-<h6>, <ul>, <ol>, <li>).',
399
- '- You may add or update supporting files required for the requested slide, including local assets under <slides-dir>/assets/ and tldraw source/export files used to generate those assets.',
409
+ '- You may add or update supporting files required for the requested slide, including local images and videos under <slides-dir>/assets/ and tldraw source/export files used to generate those assets.',
410
+ '- When the request needs bespoke imagery, prefer `slides-grab image --prompt "<prompt>" --slides-dir <path>` so Nano Banana Pro saves the asset under <slides-dir>/assets/.',
411
+ '- If GOOGLE_API_KEY or GEMINI_API_KEY is unavailable, or the Nano Banana API fails, ask the user for a Google API key or fall back to web search + download into <slides-dir>/assets/.',
400
412
  '- If you create or update a supporting asset, store it under <slides-dir>/assets/ and reference it from the requested slide as ./assets/<file>.',
413
+ '- If you need a web-hosted video, download it into <slides-dir>/assets/ first with slides-grab fetch-video --url <youtube-url> --slides-dir <path> (or yt-dlp directly if needed), then reference only the local file.',
401
414
  '- Keep local assets under ./assets/ and preserve portable relative paths.',
402
415
  '- Do not modify unrelated assets, shared resources, or generated files that are not required for the requested slide.',
403
416
  '- Do not persist runtime-only editor/viewer injections such as <base>, debug scripts, or viewer wrapper markup into the slide file.',
@@ -6,6 +6,27 @@ const CSS_URL_RE = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
6
6
 
7
7
  export const LOCAL_ASSET_PREFIX = './assets/';
8
8
 
9
+ const ASSET_CONTRACT_RULES = {
10
+ image: {
11
+ label: 'image',
12
+ remoteCode: 'remote-image-url',
13
+ remoteInsecureCode: 'remote-image-url-insecure',
14
+ absoluteCode: 'absolute-filesystem-image-path',
15
+ rootRelativeCode: 'root-relative-image-path',
16
+ otherSchemeCode: 'unsupported-image-url-scheme',
17
+ noncanonicalCode: 'noncanonical-relative-image-path',
18
+ },
19
+ video: {
20
+ label: 'video',
21
+ remoteCode: 'remote-video-url',
22
+ remoteInsecureCode: 'remote-video-url-insecure',
23
+ absoluteCode: 'absolute-filesystem-video-path',
24
+ rootRelativeCode: 'root-relative-video-path',
25
+ otherSchemeCode: 'unsupported-video-url-scheme',
26
+ noncanonicalCode: 'noncanonical-relative-video-path',
27
+ },
28
+ };
29
+
9
30
  export function looksLikeAbsoluteFilesystemPath(value) {
10
31
  return ABSOLUTE_FILESYSTEM_PATH_RE.test((value || '').trim());
11
32
  }
@@ -53,7 +74,9 @@ function injectIntoHead(html, snippet) {
53
74
  return `${snippet}\n${html}`;
54
75
  }
55
76
 
56
- export function buildImageContractReport({ slideFile, sources = [] }) {
77
+ function buildAssetContractReport({ slideFile, sources = [], assetType = 'image' }) {
78
+ const rules = ASSET_CONTRACT_RULES[assetType] || ASSET_CONTRACT_RULES.image;
79
+ const { label } = rules;
57
80
  const issues = [];
58
81
 
59
82
  for (const entry of sources) {
@@ -67,8 +90,8 @@ export function buildImageContractReport({ slideFile, sources = [] }) {
67
90
  if (classification.kind === 'remote-url') {
68
91
  issues.push({
69
92
  severity: 'critical',
70
- code: 'remote-image-url',
71
- message: 'Remote image URLs are unsupported in saved slide HTML. Download the image into ./assets/<file> instead.',
93
+ code: rules.remoteCode,
94
+ message: `Remote ${label} URLs are unsupported in saved slide HTML. Download the ${label} into ./assets/<file> instead.`,
72
95
  slide: slideFile,
73
96
  ...entry,
74
97
  });
@@ -78,8 +101,8 @@ export function buildImageContractReport({ slideFile, sources = [] }) {
78
101
  if (classification.kind === 'remote-url-insecure') {
79
102
  issues.push({
80
103
  severity: 'critical',
81
- code: 'remote-image-url-insecure',
82
- message: 'Remote http:// image URLs are unsupported in saved slide HTML. Download the image into ./assets/<file> instead.',
104
+ code: rules.remoteInsecureCode,
105
+ message: `Remote http:// ${label} URLs are unsupported in saved slide HTML. Download the ${label} into ./assets/<file> instead.`,
83
106
  slide: slideFile,
84
107
  ...entry,
85
108
  });
@@ -89,8 +112,8 @@ export function buildImageContractReport({ slideFile, sources = [] }) {
89
112
  if (classification.kind === 'absolute-filesystem-path') {
90
113
  issues.push({
91
114
  severity: 'critical',
92
- code: 'absolute-filesystem-image-path',
93
- message: 'Absolute filesystem paths are unsupported. Use ./assets/<file> instead.',
115
+ code: rules.absoluteCode,
116
+ message: `Absolute filesystem ${label} paths are unsupported. Use ./assets/<file> instead.`,
94
117
  slide: slideFile,
95
118
  ...entry,
96
119
  });
@@ -100,8 +123,19 @@ export function buildImageContractReport({ slideFile, sources = [] }) {
100
123
  if (classification.kind === 'root-relative-path') {
101
124
  issues.push({
102
125
  severity: 'critical',
103
- code: 'root-relative-image-path',
104
- message: 'Root-relative image paths are unsupported. Use ./assets/<file> instead.',
126
+ code: rules.rootRelativeCode,
127
+ message: `Root-relative ${label} paths are unsupported. Use ./assets/<file> instead.`,
128
+ slide: slideFile,
129
+ ...entry,
130
+ });
131
+ continue;
132
+ }
133
+
134
+ if (classification.kind === 'other-scheme' && rules.otherSchemeCode) {
135
+ issues.push({
136
+ severity: 'critical',
137
+ code: rules.otherSchemeCode,
138
+ message: `Non-file URL schemes for ${label} assets are unsupported in saved slide HTML. Download the ${label} into ./assets/<file> instead.`,
105
139
  slide: slideFile,
106
140
  ...entry,
107
141
  });
@@ -111,8 +145,8 @@ export function buildImageContractReport({ slideFile, sources = [] }) {
111
145
  if (classification.kind === 'noncanonical-relative-path') {
112
146
  issues.push({
113
147
  severity: 'warning',
114
- code: 'noncanonical-relative-image-path',
115
- message: 'Use ./assets/<file> for portable local assets.',
148
+ code: rules.noncanonicalCode,
149
+ message: `Use ./assets/<file> for portable local ${label} assets.`,
116
150
  slide: slideFile,
117
151
  ...entry,
118
152
  });
@@ -122,6 +156,14 @@ export function buildImageContractReport({ slideFile, sources = [] }) {
122
156
  return issues;
123
157
  }
124
158
 
159
+ export function buildImageContractReport({ slideFile, sources = [] }) {
160
+ return buildAssetContractReport({ slideFile, sources, assetType: 'image' });
161
+ }
162
+
163
+ export function buildVideoContractReport({ slideFile, sources = [] }) {
164
+ return buildAssetContractReport({ slideFile, sources, assetType: 'video' });
165
+ }
166
+
125
167
  export function buildSlideRuntimeHtml(html, { baseHref, slideFile }) {
126
168
  const snippets = [];
127
169
 
@@ -134,6 +176,7 @@ export function buildSlideRuntimeHtml(html, { baseHref, slideFile }) {
134
176
  const slideFile = ${JSON.stringify(slideFile)};
135
177
  const localAssetPrefix = ${JSON.stringify(LOCAL_ASSET_PREFIX)};
136
178
  const absolutePathRe = ${ABSOLUTE_FILESYSTEM_PATH_RE.toString()};
179
+ const schemeRe = ${SCHEME_RE.toString()};
137
180
  const prefix = '[slides-grab:image]';
138
181
 
139
182
  function describeElement(element) {
@@ -166,36 +209,100 @@ export function buildSlideRuntimeHtml(html, { baseHref, slideFile }) {
166
209
  console.error(prefix + ' ' + slideFile + ': ' + message, detail);
167
210
  }
168
211
 
212
+ function validateAssetSource(kind, source, { allowEmpty = true, onNoncanonical = 'warn' } = {}) {
213
+ const value = (source || '').trim();
214
+ if (!value) {
215
+ return allowEmpty;
216
+ }
217
+ if (value.startsWith('data:')) {
218
+ return true;
219
+ }
220
+ if (value.startsWith('https://')) {
221
+ fail('remote ' + kind + ' URL is unsupported in saved slides; download it into ./assets/<file>', { src: value });
222
+ return false;
223
+ }
224
+ if (value.startsWith('http://')) {
225
+ fail('remote http:// ' + kind + ' URL is unsupported in saved slides; download it into ./assets/<file>', { src: value });
226
+ return false;
227
+ }
228
+ if (absolutePathRe.test(value) || value.startsWith('/')) {
229
+ fail('non-portable ' + kind + ' path is unsupported', { src: value });
230
+ return false;
231
+ }
232
+ if (schemeRe.test(value)) {
233
+ fail('unsupported ' + kind + ' URL scheme in saved slides; download it into ./assets/<file>', { src: value });
234
+ return false;
235
+ }
236
+ if (!value.startsWith(localAssetPrefix)) {
237
+ const report = onNoncanonical === 'fail' ? fail : warn;
238
+ report('noncanonical local ' + kind + ' path should use ./assets/<file>', { src: value });
239
+ }
240
+ return true;
241
+ }
242
+
243
+ function getVideoSources(video) {
244
+ const sources = [];
245
+ const directSrc = (video.getAttribute('src') || '').trim();
246
+ if (directSrc) {
247
+ sources.push(directSrc);
248
+ }
249
+ for (const source of video.querySelectorAll('source[src]')) {
250
+ const src = (source.getAttribute('src') || '').trim();
251
+ if (src) {
252
+ sources.push(src);
253
+ }
254
+ }
255
+ return sources;
256
+ }
257
+
169
258
  window.addEventListener('error', (event) => {
170
259
  const target = event.target;
171
- if (!(target instanceof HTMLImageElement)) return;
172
- const src = (target.getAttribute('src') || target.currentSrc || '').trim();
173
- if (!src || src.startsWith('data:')) return;
174
- if (src.startsWith(localAssetPrefix)) {
175
- fail('missing local asset', { src });
260
+ if (target instanceof HTMLImageElement) {
261
+ const src = (target.getAttribute('src') || target.currentSrc || '').trim();
262
+ if (!src || src.startsWith('data:')) return;
263
+ if (src.startsWith(localAssetPrefix)) {
264
+ fail('missing local asset', { src });
265
+ return;
266
+ }
267
+ fail('image failed to load', { src });
176
268
  return;
177
269
  }
178
- fail('image failed to load', { src });
270
+
271
+ if (target instanceof HTMLVideoElement) {
272
+ const sources = getVideoSources(target);
273
+ if (sources.some((src) => src.startsWith(localAssetPrefix))) {
274
+ fail('missing local video asset', { sources });
275
+ return;
276
+ }
277
+ fail('video failed to load', { sources });
278
+ return;
279
+ }
280
+
281
+ if (target instanceof HTMLSourceElement && target.parentElement instanceof HTMLVideoElement) {
282
+ const src = (target.getAttribute('src') || '').trim();
283
+ if (!src) return;
284
+ if (src.startsWith(localAssetPrefix)) {
285
+ fail('missing local video asset', { src });
286
+ return;
287
+ }
288
+ fail('video source failed to load', { src });
289
+ }
179
290
  }, true);
180
291
 
181
292
  window.addEventListener('DOMContentLoaded', () => {
182
293
  for (const image of document.querySelectorAll('img[src]')) {
183
294
  const src = (image.getAttribute('src') || '').trim();
184
- if (!src || src.startsWith('data:')) continue;
185
- if (src.startsWith('https://')) {
186
- fail('remote image URL is unsupported in saved slides; download it into ./assets/<file>', { src });
187
- continue;
188
- }
189
- if (src.startsWith('http://')) {
190
- fail('remote http:// image URL is unsupported in saved slides; download it into ./assets/<file>', { src });
191
- continue;
192
- }
193
- if (absolutePathRe.test(src) || src.startsWith('/')) {
194
- fail('non-portable image path is unsupported', { src });
195
- continue;
295
+ validateAssetSource('image', src);
296
+ }
297
+
298
+ for (const video of document.querySelectorAll('video')) {
299
+ for (const src of getVideoSources(video)) {
300
+ validateAssetSource('video', src);
196
301
  }
197
- if (!src.startsWith(localAssetPrefix)) {
198
- warn('noncanonical local image path should use ./assets/<file>', { src });
302
+
303
+ const poster = (video.getAttribute('poster') || '').trim();
304
+ if (poster) {
305
+ validateAssetSource('image', poster);
199
306
  }
200
307
  }
201
308