@classic-homes/slides 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -1
- package/dist/{chunk-2IPLZ3SA.js → chunk-GX6P2SE6.js} +17 -3
- package/dist/chunk-GX6P2SE6.js.map +1 -0
- package/dist/{chunk-GXGDPBAR.js → chunk-OUAMH2TP.js} +219 -6
- package/dist/chunk-OUAMH2TP.js.map +1 -0
- package/dist/{chunk-YI7A4EWX.js → chunk-TEE3OGRQ.js} +2 -2
- package/dist/cli.js +23 -4
- package/dist/cli.js.map +1 -1
- package/dist/{render-DcyKuBRD.d.ts → images-8N30SsCW.d.ts} +62 -1
- package/dist/index.d.ts +21 -4
- package/dist/index.js +13 -3
- package/dist/index.js.map +1 -1
- package/dist/{mcp-PUA45IS7.js → mcp-3WAVCLQ2.js} +4 -4
- package/dist/node.d.ts +15 -3
- package/dist/node.js +4 -2
- package/package.json +1 -1
- package/dist/chunk-2IPLZ3SA.js.map +0 -1
- package/dist/chunk-GXGDPBAR.js.map +0 -1
- /package/dist/{chunk-YI7A4EWX.js.map → chunk-TEE3OGRQ.js.map} +0 -0
- /package/dist/{mcp-PUA45IS7.js.map → mcp-3WAVCLQ2.js.map} +0 -0
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ npx classic-slides new decks/closing-day.yaml # basic starter
|
|
|
16
16
|
npx classic-slides new decks/orientation.yaml --starter journey
|
|
17
17
|
npx classic-slides dev decks/closing-day.yaml # live preview, reloads on save
|
|
18
18
|
npx classic-slides build decks/closing-day.yaml # → decks/closing-day.html
|
|
19
|
+
npx classic-slides export decks/closing-day.yaml # → decks/closing-day.deck.json
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
Present: open the HTML, use arrow keys or Space. **N** shows speaker notes,
|
|
@@ -165,11 +166,33 @@ The core entry has no Node dependencies (it runs in Workers and browsers);
|
|
|
165
166
|
(with `hint`s for chart, grid, url, video-url and point pickers),
|
|
166
167
|
`blankSlide(layout)` gives a valid starting slide, and `renderDeck(deck,
|
|
167
168
|
{ includeHidden: true, initialSlide })` plus the `postMessage` API drive a
|
|
168
|
-
live preview.
|
|
169
|
+
live preview. Fields flagged `advanced` (slide id, speaking time, accent
|
|
170
|
+
colour) belong behind an "Advanced" section.
|
|
171
|
+
|
|
172
|
+
Click-to-edit: render the preview with `editorMarkers: true` and, inside an
|
|
173
|
+
iframe, clicking a title, bullet or picture posts
|
|
174
|
+
`{ type: 'classic-slides:select', index, path }` to the parent (`index` is the
|
|
175
|
+
deck slide, `path` the field such as `['items', 2, 'text']`, or `null` for the
|
|
176
|
+
slide itself). Send `{ type: 'classic-slides:highlight', index, path }` to
|
|
177
|
+
outline the field being edited. Never turn it on for shared or exported decks.
|
|
178
|
+
`layoutGalleryDeck()` returns one deck with every layout's starting slide, for
|
|
179
|
+
rendering a layout picker's thumbnails in one pass.
|
|
169
180
|
|
|
170
181
|
Rendered HTML embeds the deck source (minus image data), so
|
|
171
182
|
`extractDeckSource(html)` recovers the editable deck.
|
|
172
183
|
|
|
184
|
+
### Portable decks (import / export)
|
|
185
|
+
|
|
186
|
+
`classic-slides export` (or `exportDeckFile` from `/node`) writes one JSON file
|
|
187
|
+
with every image inlined as a `data:` URI — the format to import into a hosted
|
|
188
|
+
deck editor. Keep it under ~10 MB by resizing photos to ~2000px wide.
|
|
189
|
+
|
|
190
|
+
In code, `inlineDeckImages(deck, resolveImage)` does the export side and
|
|
191
|
+
`externalizeDeckImages(deck, upload)` the import side: it hands each `data:`
|
|
192
|
+
image to `upload` and puts the returned URL in its place. Both build on
|
|
193
|
+
`mapDeckImages(deck, fn)`, which finds images written as plain strings too and
|
|
194
|
+
returns a copy in the author's shape (no schema defaults added).
|
|
195
|
+
|
|
173
196
|
## Brand
|
|
174
197
|
|
|
175
198
|
Colours come from `@classic-homes/theme-tokens`. Fonts (Figtree, Spectral) are
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
+
inlineDeckImages,
|
|
2
3
|
parseDeckSource,
|
|
3
4
|
renderDeck,
|
|
4
5
|
stringifyDeck
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-OUAMH2TP.js";
|
|
6
7
|
|
|
7
8
|
// src/node/index.ts
|
|
8
9
|
import { readFile, stat, writeFile } from "fs/promises";
|
|
@@ -94,6 +95,18 @@ async function buildDeckFile(path, options = {}) {
|
|
|
94
95
|
await writeFile(out, result.html);
|
|
95
96
|
return { ...result, out, bytes: (await stat(out)).size };
|
|
96
97
|
}
|
|
98
|
+
async function exportDeckFile(path, options = {}) {
|
|
99
|
+
const abs = options.rootDir ? confinedPath(options.rootDir, resolve2(path)) : resolve2(path);
|
|
100
|
+
const result = await inlineDeckImages(
|
|
101
|
+
await loadDeckFile(abs),
|
|
102
|
+
fileImageResolver(dirname2(abs), options)
|
|
103
|
+
);
|
|
104
|
+
const candidate = options.out ? resolve2(options.out) : abs.replace(/\.(ya?ml|json)$/i, "").replace(/\.deck$/i, "") + ".deck.json";
|
|
105
|
+
const out = options.rootDir ? confinedPath(options.rootDir, candidate) : candidate;
|
|
106
|
+
if (out === abs) throw new Error(`Refusing to overwrite the source deck ${abs}. Pass --out.`);
|
|
107
|
+
await writeFile(out, JSON.stringify(result.deck) + "\n");
|
|
108
|
+
return { ...result, out, bytes: (await stat(out)).size };
|
|
109
|
+
}
|
|
97
110
|
|
|
98
111
|
export {
|
|
99
112
|
confinedPath,
|
|
@@ -101,6 +114,7 @@ export {
|
|
|
101
114
|
fileImageResolver,
|
|
102
115
|
loadDeckFile,
|
|
103
116
|
saveDeckFile,
|
|
104
|
-
buildDeckFile
|
|
117
|
+
buildDeckFile,
|
|
118
|
+
exportDeckFile
|
|
105
119
|
};
|
|
106
|
-
//# sourceMappingURL=chunk-
|
|
120
|
+
//# sourceMappingURL=chunk-GX6P2SE6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/node/index.ts","../src/node/paths.ts"],"sourcesContent":["/** Node-only helpers: read deck files, inline local images, write HTML. */\nimport { readFile, stat, writeFile } from 'node:fs/promises';\nimport { dirname, extname, isAbsolute, join, resolve } from 'node:path';\nimport { confinedPath } from './paths.js';\nimport { inlineDeckImages, type InlineImagesResult } from '../images.js';\nimport { parseDeckSource, stringifyDeck } from '../parse.js';\nimport {\n renderDeck,\n type ImageResolver,\n type RenderOptions,\n type RenderResult,\n} from '../render.js';\n\nconst MIME: Record<string, string> = {\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.png': 'image/png',\n '.webp': 'image/webp',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.avif': 'image/avif',\n};\n\n/** Largest single image we inline without warning. Big photos bloat the deck file. */\nexport const IMAGE_WARN_BYTES = 800 * 1024;\n\nexport interface FileResolverOptions {\n /** Restrict local image reads and build paths to this root (used by the MCP server). */\n rootDir?: string;\n /** Fetch https images and inline them so the deck works offline. Default true. */\n inlineRemote?: boolean;\n onWarning?: (message: string) => void;\n}\n\n/** Resolves image paths relative to the deck file and inlines them as data URIs. */\nexport function fileImageResolver(\n baseDir: string,\n options: FileResolverOptions = {}\n): ImageResolver {\n const warn = options.onWarning ?? (() => {});\n return async (src) => {\n if (src.startsWith('data:')) return src;\n if (/^https?:/.test(src)) {\n if (options.inlineRemote === false) return src;\n const res = await fetch(src);\n if (!res.ok) throw new Error(`Image ${src} returned HTTP ${res.status}`);\n const type = res.headers.get('content-type') ?? 'application/octet-stream';\n const buf = Buffer.from(await res.arrayBuffer());\n if (buf.length > IMAGE_WARN_BYTES)\n warn(`${src} is ${(buf.length / 1024).toFixed(0)} KB; consider a smaller copy.`);\n return `data:${type};base64,${buf.toString('base64')}`;\n }\n const candidate = isAbsolute(src) ? src : join(baseDir, src);\n const path = options.rootDir ? confinedPath(options.rootDir, candidate) : candidate;\n const type = MIME[extname(path).toLowerCase()];\n if (!type)\n throw new Error(`Unsupported image type for \"${src}\". Use jpg, png, webp, gif, svg or avif.`);\n let buf: Buffer;\n try {\n buf = await readFile(path);\n } catch {\n throw new Error(`Image not found: \"${src}\" (looked in ${path})`);\n }\n if (buf.length > IMAGE_WARN_BYTES)\n warn(`${src} is ${(buf.length / 1024).toFixed(0)} KB; consider resizing to ~2000px wide.`);\n return `data:${type};base64,${buf.toString('base64')}`;\n };\n}\n\nexport async function loadDeckFile(path: string): Promise<unknown> {\n return parseDeckSource(await readFile(path, 'utf8'));\n}\n\nexport async function saveDeckFile(path: string, deck: unknown): Promise<void> {\n const text = path.endsWith('.json') ? JSON.stringify(deck, null, 2) + '\\n' : stringifyDeck(deck);\n await writeFile(path, text);\n}\n\nexport interface BuildOptions extends Omit<RenderOptions, 'resolveImage'>, FileResolverOptions {\n out?: string;\n}\n\n/** Renders a deck file to a self-contained HTML file next to it (or at `out`). */\nexport async function buildDeckFile(\n path: string,\n options: BuildOptions = {}\n): Promise<RenderResult & { out: string; bytes: number }> {\n const abs = options.rootDir ? confinedPath(options.rootDir, resolve(path)) : resolve(path);\n const deck = await loadDeckFile(abs);\n const result = await renderDeck(deck, {\n ...options,\n resolveImage: fileImageResolver(dirname(abs), options),\n });\n const candidate = options.out\n ? resolve(options.out)\n : abs.replace(/\\.(ya?ml|json)$/i, '') + '.html';\n const out = options.rootDir ? confinedPath(options.rootDir, candidate) : candidate;\n await writeFile(out, result.html);\n return { ...result, out, bytes: (await stat(out)).size };\n}\n\nexport interface ExportOptions extends FileResolverOptions {\n out?: string;\n}\n\n/**\n * Writes a portable copy of a deck file: JSON with every image inlined as a\n * data URI, ready to import into a hosted deck editor. Defaults to\n * `<name>.deck.json` next to the source.\n */\nexport async function exportDeckFile(\n path: string,\n options: ExportOptions = {}\n): Promise<InlineImagesResult & { out: string; bytes: number }> {\n const abs = options.rootDir ? confinedPath(options.rootDir, resolve(path)) : resolve(path);\n const result = await inlineDeckImages(\n await loadDeckFile(abs),\n fileImageResolver(dirname(abs), options)\n );\n const candidate = options.out\n ? resolve(options.out)\n : abs.replace(/\\.(ya?ml|json)$/i, '').replace(/\\.deck$/i, '') + '.deck.json';\n const out = options.rootDir ? confinedPath(options.rootDir, candidate) : candidate;\n if (out === abs) throw new Error(`Refusing to overwrite the source deck ${abs}. Pass --out.`);\n await writeFile(out, JSON.stringify(result.deck) + '\\n');\n return { ...result, out, bytes: (await stat(out)).size };\n}\n\nexport { renderDeck };\n","/** Canonical containment for existing files and new output files. */\nimport { lstatSync, realpathSync } from 'node:fs';\nimport { dirname, relative, resolve, sep } from 'node:path';\n\nexport function confinedPath(root: string, path: string): string {\n const canonicalRoot = realpathSync(root);\n const absolute = resolve(path);\n let ancestor = absolute;\n for (;;) {\n try {\n const canonical = resolve(realpathSync(ancestor), relative(ancestor, absolute));\n if (canonical !== canonicalRoot && !canonical.startsWith(canonicalRoot + sep)) {\n throw new Error(`Path \"${path}\" is outside the deck root ${root}.`);\n }\n return canonical;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT' || ancestor === dirname(ancestor))\n throw error;\n if (lstatSync(ancestor, { throwIfNoEntry: false })?.isSymbolicLink()) {\n throw new Error(`Cannot follow dangling symlink \"${ancestor}\".`);\n }\n ancestor = dirname(ancestor);\n }\n }\n}\n"],"mappings":"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","names":["dirname","resolve","type","buf","resolve","dirname"]}
|
|
@@ -620,6 +620,10 @@ function prose(text) {
|
|
|
620
620
|
return `<p>${inline(para)}</p>`;
|
|
621
621
|
}).join("");
|
|
622
622
|
}
|
|
623
|
+
function plain(text) {
|
|
624
|
+
if (!text) return "";
|
|
625
|
+
return text.replace(/`([^`]+)`/g, "$1").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1");
|
|
626
|
+
}
|
|
623
627
|
|
|
624
628
|
// src/video.ts
|
|
625
629
|
var YT = /^(?:https:\/\/)?(?:www\.|m\.)?(?:youtube\.com\/(?:watch\?(?:.*&)?v=|embed\/|shorts\/|live\/)|youtu\.be\/)([\w-]{11})/;
|
|
@@ -2193,8 +2197,11 @@ var LABELS = {
|
|
|
2193
2197
|
imageAspect: "Photo shape",
|
|
2194
2198
|
numberFrom: "First badge number",
|
|
2195
2199
|
url: "URL",
|
|
2196
|
-
id: "Slide id"
|
|
2200
|
+
id: "Slide id",
|
|
2201
|
+
time: "Speaking time",
|
|
2202
|
+
track: "Accent colour"
|
|
2197
2203
|
};
|
|
2204
|
+
var ADVANCED = /* @__PURE__ */ new Set(["id", "time", "track"]);
|
|
2198
2205
|
function labelFor(key) {
|
|
2199
2206
|
if (LABELS[key]) return LABELS[key];
|
|
2200
2207
|
const words = key.replace(/([A-Z])/g, " $1").toLowerCase();
|
|
@@ -2231,7 +2238,8 @@ function describeField(key, schema) {
|
|
|
2231
2238
|
required,
|
|
2232
2239
|
...description ? { description } : {},
|
|
2233
2240
|
...dflt !== void 0 ? { default: dflt } : {},
|
|
2234
|
-
...MULTILINE.has(key) ? { multiline: true } : {}
|
|
2241
|
+
...MULTILINE.has(key) ? { multiline: true } : {},
|
|
2242
|
+
...ADVANCED.has(key) ? { advanced: true } : {}
|
|
2235
2243
|
};
|
|
2236
2244
|
if (inner === Image) return { ...base, kind: "image" };
|
|
2237
2245
|
if (inner === ChartSpec) {
|
|
@@ -2335,6 +2343,16 @@ function layoutEditorInfo(name) {
|
|
|
2335
2343
|
function allLayoutEditorInfo() {
|
|
2336
2344
|
return LAYOUTS.map((l) => layoutEditorInfo(l.name));
|
|
2337
2345
|
}
|
|
2346
|
+
function layoutGalleryDeck() {
|
|
2347
|
+
const slides = [];
|
|
2348
|
+
const slideFor = {};
|
|
2349
|
+
for (const layout of LAYOUTS) {
|
|
2350
|
+
slideFor[layout.name] = slides.length;
|
|
2351
|
+
slides.push(blankSlide(layout.name));
|
|
2352
|
+
}
|
|
2353
|
+
const journey2 = { steps: ["Contract", "Design", "Build", "Close", "Move in", "Warranty"] };
|
|
2354
|
+
return { deck: { title: "Layouts", journey: journey2, slides }, slideFor };
|
|
2355
|
+
}
|
|
2338
2356
|
var PLACEHOLDER_IMAGE = "data:image/svg+xml;base64," + btoa(
|
|
2339
2357
|
'<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1000" viewBox="0 0 1600 1000"><rect width="1600" height="1000" fill="#ecebe6"/><rect x="24" y="24" width="1552" height="952" fill="none" stroke="#b9bcb4" stroke-width="6" stroke-dasharray="28 20"/><text x="800" y="520" text-anchor="middle" font-family="Figtree,Arial,sans-serif" font-size="64" fill="#7a7a7a">Add a photo</text></svg>'
|
|
2340
2358
|
);
|
|
@@ -2567,8 +2585,83 @@ var RUNTIME_JS = String.raw`
|
|
|
2567
2585
|
if (!embedded || e.source !== window.parent) return;
|
|
2568
2586
|
if (d.type === 'classic-slides:go' && Number.isInteger(d.index)) go(d.index, true);
|
|
2569
2587
|
else if (d.type === 'classic-slides:notes') { if (notes && notes.classList.contains('open') !== !!d.open) toggleNotes(); }
|
|
2588
|
+
else if (d.type === 'classic-slides:highlight' && editor) highlight(d.index, d.path);
|
|
2570
2589
|
});
|
|
2571
2590
|
|
|
2591
|
+
// ---------- editor click-to-edit ----------
|
|
2592
|
+
// Matches the clicked element to a slide field by its visible text (or an
|
|
2593
|
+
// image by its src), walking up from the target, so layouts need no markup.
|
|
2594
|
+
const editor = embedded && body.hasAttribute('data-editor');
|
|
2595
|
+
const fieldsEl = editor && document.getElementById('deck-fields');
|
|
2596
|
+
const FIELDS = fieldsEl ? JSON.parse(fieldsEl.textContent || '[]') : [];
|
|
2597
|
+
const norm = (t) => (t || '').replace(/\s+/g, ' ').trim();
|
|
2598
|
+
function ownText(el) {
|
|
2599
|
+
let t = '';
|
|
2600
|
+
el.childNodes.forEach((n) => { if (n.nodeType === 3) t += n.textContent; });
|
|
2601
|
+
return norm(t);
|
|
2602
|
+
}
|
|
2603
|
+
function fieldFor(el, candidates) {
|
|
2604
|
+
if (el.tagName === 'IMG') {
|
|
2605
|
+
const src = el.getAttribute('src');
|
|
2606
|
+
return candidates.find((c) => c.img && c.img === src);
|
|
2607
|
+
}
|
|
2608
|
+
const text = norm(el.textContent);
|
|
2609
|
+
const own = ownText(el);
|
|
2610
|
+
return candidates.find((c) => c.text && (c.text === text || (own && c.text === own)));
|
|
2611
|
+
}
|
|
2612
|
+
function locate(target) {
|
|
2613
|
+
const section = target.closest && target.closest('.slide');
|
|
2614
|
+
if (!section) return null;
|
|
2615
|
+
const index = Number(section.getAttribute('data-index'));
|
|
2616
|
+
const candidates = FIELDS[index] || [];
|
|
2617
|
+
for (let el = target; el && el !== section; el = el.parentElement) {
|
|
2618
|
+
const field = fieldFor(el, candidates);
|
|
2619
|
+
if (field) return { index, path: field.path, el };
|
|
2620
|
+
}
|
|
2621
|
+
return { index, path: null, el: section };
|
|
2622
|
+
}
|
|
2623
|
+
let hovered = null;
|
|
2624
|
+
let selected = null;
|
|
2625
|
+
function highlight(index, path) {
|
|
2626
|
+
selected?.classList.remove('cs-edit-selected');
|
|
2627
|
+
selected = null;
|
|
2628
|
+
if (!Array.isArray(path)) return;
|
|
2629
|
+
const key = JSON.stringify(path);
|
|
2630
|
+
const candidates = FIELDS[index] || [];
|
|
2631
|
+
for (const section of deck.querySelectorAll('.slide[data-index="' + index + '"]')) {
|
|
2632
|
+
for (const el of section.querySelectorAll('*')) {
|
|
2633
|
+
const field = fieldFor(el, candidates);
|
|
2634
|
+
if (field && JSON.stringify(field.path) === key) {
|
|
2635
|
+
selected = el;
|
|
2636
|
+
el.classList.add('cs-edit-selected');
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
if (editor) {
|
|
2643
|
+
const style = document.createElement('style');
|
|
2644
|
+
style.textContent = '.cs-edit-hover{outline:2px dashed rgba(0,122,255,.7);outline-offset:4px;cursor:pointer}' +
|
|
2645
|
+
'.cs-edit-selected{outline:2px solid rgb(0,122,255);outline-offset:4px}';
|
|
2646
|
+
document.head.appendChild(style);
|
|
2647
|
+
deck.addEventListener('click', (e) => {
|
|
2648
|
+
const hit = locate(e.target);
|
|
2649
|
+
if (!hit) return;
|
|
2650
|
+
// Links on a slide would navigate the preview away
|
|
2651
|
+
if (e.target.closest('a[href]')) e.preventDefault();
|
|
2652
|
+
window.parent.postMessage({ type: 'classic-slides:select', index: hit.index, path: hit.path }, '*');
|
|
2653
|
+
}, true);
|
|
2654
|
+
deck.addEventListener('mouseover', (e) => {
|
|
2655
|
+
const hit = locate(e.target);
|
|
2656
|
+
const el = hit && hit.path ? hit.el : null;
|
|
2657
|
+
if (el === hovered) return;
|
|
2658
|
+
hovered?.classList.remove('cs-edit-hover');
|
|
2659
|
+
hovered = el;
|
|
2660
|
+
el?.classList.add('cs-edit-hover');
|
|
2661
|
+
});
|
|
2662
|
+
deck.addEventListener('mouseleave', () => { hovered?.classList.remove('cs-edit-hover'); hovered = null; });
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2572
2665
|
function onKey(e) {
|
|
2573
2666
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
2574
2667
|
const k = e.key;
|
|
@@ -3286,6 +3379,48 @@ body.presenter{margin:0;height:100vh;display:grid;grid-template-rows:1fr auto;ba
|
|
|
3286
3379
|
|
|
3287
3380
|
// src/render.ts
|
|
3288
3381
|
var PREVIEW_SIZE = { width: 1280, height: 720 };
|
|
3382
|
+
var NON_VISUAL_KEYS = /* @__PURE__ */ new Set([
|
|
3383
|
+
"layout",
|
|
3384
|
+
"id",
|
|
3385
|
+
"notes",
|
|
3386
|
+
"time",
|
|
3387
|
+
"hidden",
|
|
3388
|
+
"background",
|
|
3389
|
+
"backgroundImage",
|
|
3390
|
+
"overlay",
|
|
3391
|
+
"track",
|
|
3392
|
+
"step",
|
|
3393
|
+
"stepDone",
|
|
3394
|
+
"showJourney",
|
|
3395
|
+
"href",
|
|
3396
|
+
"url",
|
|
3397
|
+
"fit",
|
|
3398
|
+
"focus",
|
|
3399
|
+
"size",
|
|
3400
|
+
"alt"
|
|
3401
|
+
]);
|
|
3402
|
+
function fieldMarkers(slide, resolveImage = (src) => src) {
|
|
3403
|
+
const out = [];
|
|
3404
|
+
const walk = (value, path) => {
|
|
3405
|
+
if (typeof value === "string") {
|
|
3406
|
+
const text = plain(value).replace(/\s+/g, " ").trim();
|
|
3407
|
+
if (text) out.push({ path, text });
|
|
3408
|
+
} else if (Array.isArray(value)) {
|
|
3409
|
+
value.forEach((v, i) => walk(v, [...path, i]));
|
|
3410
|
+
} else if (value && typeof value === "object") {
|
|
3411
|
+
const obj = value;
|
|
3412
|
+
if (typeof obj.src === "string") {
|
|
3413
|
+
out.push({ path, img: resolveImage(obj.src) });
|
|
3414
|
+
return;
|
|
3415
|
+
}
|
|
3416
|
+
for (const [key, v] of Object.entries(obj)) {
|
|
3417
|
+
if (!NON_VISUAL_KEYS.has(key)) walk(v, [...path, key]);
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
};
|
|
3421
|
+
walk(slide, []);
|
|
3422
|
+
return out;
|
|
3423
|
+
}
|
|
3289
3424
|
var passThrough = (src) => {
|
|
3290
3425
|
if (/^(data:|https?:)/.test(src)) return src;
|
|
3291
3426
|
throw new Error(
|
|
@@ -3476,6 +3611,12 @@ async function renderDeck(input, options = {}) {
|
|
|
3476
3611
|
});
|
|
3477
3612
|
});
|
|
3478
3613
|
const withNotes = options.speakerNotes !== false;
|
|
3614
|
+
const fields = options.editorMarkers ? `<script type="application/json" id="deck-fields">${JSON.stringify(
|
|
3615
|
+
deck.slides.map(
|
|
3616
|
+
(slide) => fieldMarkers(slide, (src) => images.get(src) ?? src)
|
|
3617
|
+
)
|
|
3618
|
+
).replace(/</g, "\\u003c")}</script>
|
|
3619
|
+
` : "";
|
|
3479
3620
|
const json = (v) => JSON.stringify(v).replace(/</g, "\\u003c");
|
|
3480
3621
|
const stripData = (key, v) => !withNotes && key === "notes" ? void 0 : typeof v === "string" && /^data:/i.test(v) ? "(embedded)" : v;
|
|
3481
3622
|
const source = options.embedSource === false ? "" : `<script type="application/json" id="deck-source">${JSON.stringify(deck, stripData).replace(/</g, "\\u003c")}</script>
|
|
@@ -3492,7 +3633,7 @@ ${deck.description ? `<meta name="description" content="${escapeAttr(deck.descri
|
|
|
3492
3633
|
<style id="brand-fonts">${fontsCss()}</style>
|
|
3493
3634
|
<style>${tokensCss(brand.stock ? void 0 : brand.palette)}${DECK_CSS}${DECK_CSS_EXTRA}</style>
|
|
3494
3635
|
</head>
|
|
3495
|
-
<body${wantFooter ? ' class="has-foot"' : ""}${options.initialSlide ? ` data-initial-slide="${Math.max(0, Math.floor(options.initialSlide))}"` : ""}${deck.settings.autoAdvance ? ` data-auto-advance="${deck.settings.autoAdvance}"` : ""}${deck.settings.loop ? " data-loop" : ""}${options.allowEmbeddedKiosk ? " data-embedded-kiosk" : ""}>
|
|
3636
|
+
<body${wantFooter ? ' class="has-foot"' : ""}${options.initialSlide ? ` data-initial-slide="${Math.max(0, Math.floor(options.initialSlide))}"` : ""}${deck.settings.autoAdvance ? ` data-auto-advance="${deck.settings.autoAdvance}"` : ""}${deck.settings.loop ? " data-loop" : ""}${options.allowEmbeddedKiosk ? " data-embedded-kiosk" : ""}${options.editorMarkers ? " data-editor" : ""}>
|
|
3496
3637
|
<div class="rail" id="rail"></div>
|
|
3497
3638
|
<main class="deck" id="deck">
|
|
3498
3639
|
${sections.join("\n")}
|
|
@@ -3503,7 +3644,7 @@ ${withNotes ? `<div class="hud"><span class="counter" id="counter" aria-live="po
|
|
|
3503
3644
|
<aside class="notes" id="notes" aria-live="polite"><div class="notes__head"><span class="tag">Speaker notes</span><span class="time" id="notesTime"></span></div><div class="notes__body" id="notesBody"></div></aside>` : `<div class="hud"><span class="counter" id="counter" aria-live="polite"></span></div>
|
|
3504
3645
|
<div class="hint"><kbd>↑</kbd><kbd>↓</kbd> or <kbd>Space</kbd> navigate · <kbd>O</kbd> overview · <kbd>F</kbd> fullscreen</div>`}
|
|
3505
3646
|
<script type="application/json" id="deck-notes">${json(withNotes ? notes : [])}</script>
|
|
3506
|
-
${source}<script>${RUNTIME_JS}</script>
|
|
3647
|
+
${source}${fields}<script>${RUNTIME_JS}</script>
|
|
3507
3648
|
</body>
|
|
3508
3649
|
</html>
|
|
3509
3650
|
`;
|
|
@@ -3683,6 +3824,73 @@ function stringifyDeck(deck) {
|
|
|
3683
3824
|
});
|
|
3684
3825
|
}
|
|
3685
3826
|
|
|
3827
|
+
// src/images.ts
|
|
3828
|
+
function imagePaths(value, path = [], out = []) {
|
|
3829
|
+
if (Array.isArray(value)) value.forEach((v, i) => imagePaths(v, [...path, i], out));
|
|
3830
|
+
else if (value && typeof value === "object") {
|
|
3831
|
+
const obj = value;
|
|
3832
|
+
if (typeof obj.src === "string") out.push(path);
|
|
3833
|
+
for (const [k, v] of Object.entries(obj)) imagePaths(v, [...path, k], out);
|
|
3834
|
+
}
|
|
3835
|
+
return out;
|
|
3836
|
+
}
|
|
3837
|
+
async function mapDeckImages(input, fn, options = {}) {
|
|
3838
|
+
const result = validateDeck(input, options);
|
|
3839
|
+
if (!result.ok || !result.deck) throw new DeckError(result.issues);
|
|
3840
|
+
const paths = imagePaths(result.deck);
|
|
3841
|
+
const deck = structuredClone(input);
|
|
3842
|
+
const read = (path) => path.reduce((node, key) => node[key], deck);
|
|
3843
|
+
const sources = [
|
|
3844
|
+
...new Set(
|
|
3845
|
+
paths.map((p) => {
|
|
3846
|
+
const node = read(p);
|
|
3847
|
+
return typeof node === "string" ? node : node.src;
|
|
3848
|
+
})
|
|
3849
|
+
)
|
|
3850
|
+
];
|
|
3851
|
+
const replaced = /* @__PURE__ */ new Map();
|
|
3852
|
+
let next = 0;
|
|
3853
|
+
await Promise.all(
|
|
3854
|
+
Array.from({ length: Math.min(options.concurrency ?? 4, sources.length) }, async () => {
|
|
3855
|
+
while (next < sources.length) {
|
|
3856
|
+
const src = sources[next++];
|
|
3857
|
+
replaced.set(src, await fn(src));
|
|
3858
|
+
}
|
|
3859
|
+
})
|
|
3860
|
+
);
|
|
3861
|
+
for (const path of paths) {
|
|
3862
|
+
const node = read(path);
|
|
3863
|
+
if (typeof node === "string") {
|
|
3864
|
+
const parent = read(path.slice(0, -1));
|
|
3865
|
+
parent[path[path.length - 1]] = replaced.get(node);
|
|
3866
|
+
} else {
|
|
3867
|
+
const img2 = node;
|
|
3868
|
+
img2.src = replaced.get(img2.src);
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
return deck;
|
|
3872
|
+
}
|
|
3873
|
+
async function inlineDeckImages(input, resolve, options = {}) {
|
|
3874
|
+
const warnings = [];
|
|
3875
|
+
const deck = await mapDeckImages(
|
|
3876
|
+
input,
|
|
3877
|
+
async (src) => {
|
|
3878
|
+
if (/^data:/i.test(src)) return src;
|
|
3879
|
+
try {
|
|
3880
|
+
return await resolve(src);
|
|
3881
|
+
} catch (e) {
|
|
3882
|
+
warnings.push({ src, message: e instanceof Error ? e.message : String(e) });
|
|
3883
|
+
return src;
|
|
3884
|
+
}
|
|
3885
|
+
},
|
|
3886
|
+
options
|
|
3887
|
+
);
|
|
3888
|
+
return { deck, warnings };
|
|
3889
|
+
}
|
|
3890
|
+
function externalizeDeckImages(input, upload, options = {}) {
|
|
3891
|
+
return mapDeckImages(input, (src) => /^data:/i.test(src) ? upload(src) : src, options);
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3686
3894
|
export {
|
|
3687
3895
|
baseSlide,
|
|
3688
3896
|
BrandPaletteSchema,
|
|
@@ -3727,6 +3935,7 @@ export {
|
|
|
3727
3935
|
commonSlideFields,
|
|
3728
3936
|
layoutEditorInfo,
|
|
3729
3937
|
allLayoutEditorInfo,
|
|
3938
|
+
layoutGalleryDeck,
|
|
3730
3939
|
PLACEHOLDER_IMAGE,
|
|
3731
3940
|
isPlaceholderImage,
|
|
3732
3941
|
blankSlide,
|
|
@@ -3735,10 +3944,14 @@ export {
|
|
|
3735
3944
|
formatIssues,
|
|
3736
3945
|
DeckError,
|
|
3737
3946
|
PREVIEW_SIZE,
|
|
3947
|
+
fieldMarkers,
|
|
3738
3948
|
collectImageSources,
|
|
3739
3949
|
renderDeck,
|
|
3740
3950
|
extractDeckSource,
|
|
3741
3951
|
parseDeckSource,
|
|
3742
|
-
stringifyDeck
|
|
3952
|
+
stringifyDeck,
|
|
3953
|
+
mapDeckImages,
|
|
3954
|
+
inlineDeckImages,
|
|
3955
|
+
externalizeDeckImages
|
|
3743
3956
|
};
|
|
3744
|
-
//# sourceMappingURL=chunk-
|
|
3957
|
+
//# sourceMappingURL=chunk-OUAMH2TP.js.map
|