@classic-homes/slides 0.4.0 → 0.5.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.
- package/README.md +13 -0
- package/dist/{chunk-L7G3VH7K.js → chunk-GX6P2SE6.js} +17 -3
- package/dist/chunk-GX6P2SE6.js.map +1 -0
- package/dist/{chunk-PTTLL6PQ.js → chunk-OUAMH2TP.js} +72 -2
- package/dist/chunk-OUAMH2TP.js.map +1 -0
- package/dist/{chunk-JFUJ47TP.js → chunk-TEE3OGRQ.js} +2 -2
- package/dist/cli.js +23 -4
- package/dist/cli.js.map +1 -1
- package/dist/{render-CU0sXO2y.d.ts → images-8N30SsCW.d.ts} +38 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/{mcp-UX32IP2J.js → mcp-3WAVCLQ2.js} +4 -4
- package/dist/node.d.ts +15 -3
- package/dist/node.js +4 -2
- package/package.json +4 -3
- package/dist/chunk-L7G3VH7K.js.map +0 -1
- package/dist/chunk-PTTLL6PQ.js.map +0 -1
- /package/dist/{chunk-JFUJ47TP.js.map → chunk-TEE3OGRQ.js.map} +0 -0
- /package/dist/{mcp-UX32IP2J.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,
|
|
@@ -180,6 +181,18 @@ rendering a layout picker's thumbnails in one pass.
|
|
|
180
181
|
Rendered HTML embeds the deck source (minus image data), so
|
|
181
182
|
`extractDeckSource(html)` recovers the editable deck.
|
|
182
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
|
+
|
|
183
196
|
## Brand
|
|
184
197
|
|
|
185
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"]}
|
|
@@ -3824,6 +3824,73 @@ function stringifyDeck(deck) {
|
|
|
3824
3824
|
});
|
|
3825
3825
|
}
|
|
3826
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
|
+
|
|
3827
3894
|
export {
|
|
3828
3895
|
baseSlide,
|
|
3829
3896
|
BrandPaletteSchema,
|
|
@@ -3882,6 +3949,9 @@ export {
|
|
|
3882
3949
|
renderDeck,
|
|
3883
3950
|
extractDeckSource,
|
|
3884
3951
|
parseDeckSource,
|
|
3885
|
-
stringifyDeck
|
|
3952
|
+
stringifyDeck,
|
|
3953
|
+
mapDeckImages,
|
|
3954
|
+
inlineDeckImages,
|
|
3955
|
+
externalizeDeckImages
|
|
3886
3956
|
};
|
|
3887
|
-
//# sourceMappingURL=chunk-
|
|
3957
|
+
//# sourceMappingURL=chunk-OUAMH2TP.js.map
|