@classic-homes/slides 0.7.0 → 0.8.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.
@@ -1 +0,0 @@
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\n/** Largest remote image we download. Anything bigger is refused, not truncated. */\nconst REMOTE_IMAGE_MAX_BYTES = 25 * 1024 * 1024;\n\n/** How long one remote image (including redirects) may take to download. */\nconst REMOTE_IMAGE_TIMEOUT_MS = 15_000;\n\nconst MAX_REDIRECTS = 5;\n\n/**\n * Downloads a remote image for inlining. Only https is fetched, redirects included, so a\n * deck (or an agent building one over MCP) can't reach plain-http hosts such as cloud\n * metadata endpoints; non-image responses, slow servers and oversized bodies are refused.\n */\nasync function fetchRemoteImage(src: string): Promise<{ type: string; buf: Buffer }> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), REMOTE_IMAGE_TIMEOUT_MS);\n try {\n return await downloadImage(src, controller.signal);\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function downloadImage(src: string, signal: AbortSignal) {\n let url = src;\n let res: Response;\n for (let hop = 0; ; hop++) {\n if (new URL(url).protocol !== 'https:') throw new Error(`Image ${src} must use https.`);\n try {\n res = await fetch(url, { redirect: 'manual', signal });\n } catch (e) {\n if (signal.aborted)\n throw new Error(`Image ${src} took over ${REMOTE_IMAGE_TIMEOUT_MS / 1000}s to download.`);\n throw e;\n }\n const location = res.headers.get('location');\n if (res.status < 300 || res.status >= 400 || !location) break;\n if (hop === MAX_REDIRECTS) throw new Error(`Image ${src} redirected too many times.`);\n url = new URL(location, url).href;\n }\n if (!res.ok) throw new Error(`Image ${src} returned HTTP ${res.status}`);\n const type = (res.headers.get('content-type') ?? '').split(';')[0].trim().toLowerCase();\n if (!type.startsWith('image/'))\n throw new Error(`Image ${src} is not an image (content-type \"${type || 'none'}\").`);\n const tooBig = () =>\n new Error(\n `Image ${src} is over ${REMOTE_IMAGE_MAX_BYTES / 1024 / 1024} MB; use a smaller copy.`\n );\n if (Number(res.headers.get('content-length')) > REMOTE_IMAGE_MAX_BYTES) {\n await res.body?.cancel();\n throw tooBig();\n }\n const chunks: Uint8Array[] = [];\n let size = 0;\n if (res.body) {\n const reader = res.body.getReader();\n try {\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > REMOTE_IMAGE_MAX_BYTES) {\n await reader.cancel();\n throw tooBig();\n }\n chunks.push(value);\n }\n } catch (e) {\n if (signal.aborted)\n throw new Error(`Image ${src} took over ${REMOTE_IMAGE_TIMEOUT_MS / 1000}s to download.`);\n throw e;\n }\n }\n return { type, buf: Buffer.concat(chunks) };\n}\n\nexport interface FileResolverOptions {\n /** Restrict local image reads and build paths to this root (used by the MCP server). */\n rootDir?: string;\n /**\n * Fetch https images and inline them so the deck works offline. Default true.\n * Plain-http images are never fetched; they stay remote links (with a warning).\n */\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?:/i.test(src)) {\n if (options.inlineRemote === false) return src;\n if (!/^https:/i.test(src)) {\n warn(`${src} is not https, so it was left as a remote link; use an https URL.`);\n return src;\n }\n const { type, buf } = await fetchRemoteImage(src);\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"]}