@classic-homes/slides 0.7.0 → 0.8.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 CHANGED
@@ -96,7 +96,8 @@ Every slide also accepts `track` (value/red, build/teal, neutral/navy),
96
96
 
97
97
  Charts are rendered to static SVG with ECharts (server-side rendering, themed
98
98
  from `@classic-homes/charts-core`), so they work offline and in print. ECharts
99
- is loaded only when a deck has charts.
99
+ is loaded only when a deck has charts, and `/core` leaves it out entirely (see
100
+ [As a library](#as-a-library)).
100
101
 
101
102
  ### Video and embeds
102
103
 
@@ -160,8 +161,24 @@ import { buildDeckFile, fileImageResolver } from '@classic-homes/slides/node';
160
161
  const { html } = await renderDeck(deck, { resolveImage: (src) => lookUp(src) });
161
162
  ```
162
163
 
163
- The core entry has no Node dependencies (it runs in Workers and browsers);
164
- `/node` adds file-based image loading and `buildDeckFile`. For editors,
164
+ The main entry has no Node dependencies (it runs in Workers and browsers);
165
+ `/node` adds file-based image loading and `buildDeckFile`.
166
+
167
+ `/core` is the main entry without ECharts (about 650 KB minified), for bundles
168
+ that must stay small, such as a Cloudflare Worker. Its `renderDeck` draws charts
169
+ with the `renderCharts` option you pass: a `ChartSvgRenderer` that gets every
170
+ chart in the deck in one call (`{ spec, width, height, palette?, title? }`,
171
+ plain data that survives RPC) and returns their SVGs in the same order. Run
172
+ `renderChartSvgs` from the main entry wherever ECharts can live (another Worker,
173
+ a server) and return its output. A deck with charts and no renderer throws.
174
+
175
+ ````ts
176
+ import { renderDeck } from '@classic-homes/slides/core';
177
+
178
+ const { html } = await renderDeck(deck, {
179
+ renderCharts: (charts) => env.CHARTS.renderSlideChartSvgs(charts),
180
+ });
181
+ ``` For editors,
165
182
  `layoutEditorInfo()` / `deckEditorFields()` describe every field as a widget
166
183
  (with `hint`s for chart, grid, url, video-url and point pickers),
167
184
  `blankSlide(layout)` gives a valid starting slide, and `renderDeck(deck,
@@ -199,3 +216,4 @@ Colours come from `@classic-homes/theme-tokens`. Fonts (Figtree, Spectral) are
199
216
  embedded from `assets/fonts`. Styling lives in `src/styles.ts`; add a layout in
200
217
  `src/layouts/` and register it in `src/layouts/index.ts` — its example is
201
218
  tested automatically.
219
+ ````
@@ -1,9 +1,11 @@
1
+ import {
2
+ renderDeck
3
+ } from "./chunk-6ARF37DO.js";
1
4
  import {
2
5
  inlineDeckImages,
3
6
  parseDeckSource,
4
- renderDeck,
5
7
  stringifyDeck
6
- } from "./chunk-HI5ZCAHO.js";
8
+ } from "./chunk-MCG5BFJN.js";
7
9
 
8
10
  // src/node/index.ts
9
11
  import { readFile, stat, writeFile } from "fs/promises";
@@ -181,4 +183,4 @@ export {
181
183
  buildDeckFile,
182
184
  exportDeckFile
183
185
  };
184
- //# sourceMappingURL=chunk-UKRX7A2V.js.map
186
+ //# sourceMappingURL=chunk-4LBQOMMI.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 type { ImageResolver, RenderOptions, RenderResult } from '../render.js';\nimport { renderDeck } from '../render-default.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"]}
@@ -0,0 +1,7 @@
1
+ // src/generated/runtime-hash.ts
2
+ var RUNTIME_SCRIPT_HASH = "sha256-gElvQAb4VPvKu4UeyJRVKuoHYNp8NGhXwx2EhL7NB88=";
3
+
4
+ export {
5
+ RUNTIME_SCRIPT_HASH
6
+ };
7
+ //# sourceMappingURL=chunk-4NSKDQ5V.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/generated/runtime-hash.ts"],"sourcesContent":["// Generated by scripts/embed-fonts.ts. Do not edit.\n/** CSP source for the inline presenter runtime: script-src '<hash>'. */\nexport const RUNTIME_SCRIPT_HASH = 'sha256-gElvQAb4VPvKu4UeyJRVKuoHYNp8NGhXwx2EhL7NB88=';\n"],"mappings":";AAEO,IAAM,sBAAsB;","names":[]}
@@ -0,0 +1,38 @@
1
+ import {
2
+ chartOption,
3
+ finishChartSvg,
4
+ renderDeck
5
+ } from "./chunk-MCG5BFJN.js";
6
+
7
+ // src/chart-render.ts
8
+ var renderer;
9
+ function loadChartRenderer() {
10
+ renderer ??= import("./echarts-render-GDCT5QAF.js").then((m) => m.createRenderer());
11
+ return renderer;
12
+ }
13
+ async function renderChartSvg(spec, opts = {}) {
14
+ const width = opts.width ?? 960;
15
+ const height = opts.height ?? 520;
16
+ const r = await loadChartRenderer();
17
+ return finishChartSvg(r.render(chartOption(spec, opts.palette), width, height), spec, opts.title);
18
+ }
19
+ var renderChartSvgs = async (charts) => {
20
+ const out = [];
21
+ for (const c of charts) out.push(await renderChartSvg(c.spec, c));
22
+ return out;
23
+ };
24
+
25
+ // src/render-default.ts
26
+ function renderDeck2(input, options = {}) {
27
+ return renderDeck(input, {
28
+ ...options,
29
+ renderCharts: options.renderCharts ?? renderChartSvgs
30
+ });
31
+ }
32
+
33
+ export {
34
+ renderChartSvg,
35
+ renderChartSvgs,
36
+ renderDeck2 as renderDeck
37
+ };
38
+ //# sourceMappingURL=chunk-6ARF37DO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/chart-render.ts","../src/render-default.ts"],"sourcesContent":["/**\n * The built-in chart renderer: ECharts server-side SVG, in process. ECharts\n * is imported lazily (echarts-render.ts) so decks without charts never load\n * it. Only the main entry reaches this module; `/core` callers inject their\n * own `ChartSvgRenderer`.\n */\nimport type { BrandPalette } from './brand.js';\nimport {\n chartOption,\n finishChartSvg,\n type ChartRenderer,\n type ChartSpecValue,\n type ChartSvgRenderer,\n} from './charts.js';\n\nlet renderer: Promise<ChartRenderer> | undefined;\n\n/** Loads ECharts on first use. */\nexport function loadChartRenderer(): Promise<ChartRenderer> {\n renderer ??= import('./echarts-render.js').then((m) => m.createRenderer());\n return renderer;\n}\n\n/** Renders one chart spec to an accessible, responsive SVG string. */\nexport async function renderChartSvg(\n spec: ChartSpecValue,\n opts: { width?: number; height?: number; palette?: BrandPalette; title?: string } = {}\n): Promise<string> {\n const width = opts.width ?? 960;\n const height = opts.height ?? 520;\n const r = await loadChartRenderer();\n return finishChartSvg(r.render(chartOption(spec, opts.palette), width, height), spec, opts.title);\n}\n\n/** `ChartSvgRenderer` over `renderChartSvg`: the default for `renderDeck`. */\nexport const renderChartSvgs: ChartSvgRenderer = async (charts) => {\n const out: string[] = [];\n for (const c of charts) out.push(await renderChartSvg(c.spec, c));\n return out;\n};\n","/** `renderDeck` with the built-in ECharts chart renderer (main entry, CLI, node helpers). */\nimport { renderChartSvgs } from './chart-render.js';\nimport { renderDeck as renderDeckCore, type RenderOptions, type RenderResult } from './render.js';\n\nexport function renderDeck(input: unknown, options: RenderOptions = {}): Promise<RenderResult> {\n return renderDeckCore(input, {\n ...options,\n renderCharts: options.renderCharts ?? renderChartSvgs,\n });\n}\n"],"mappings":";;;;;;;AAeA,IAAI;AAGG,SAAS,oBAA4C;AAC1D,eAAa,OAAO,8BAAqB,EAAE,KAAK,CAAC,MAAM,EAAE,eAAe,CAAC;AACzE,SAAO;AACT;AAGA,eAAsB,eACpB,MACA,OAAoF,CAAC,GACpE;AACjB,QAAM,QAAQ,KAAK,SAAS;AAC5B,QAAM,SAAS,KAAK,UAAU;AAC9B,QAAM,IAAI,MAAM,kBAAkB;AAClC,SAAO,eAAe,EAAE,OAAO,YAAY,MAAM,KAAK,OAAO,GAAG,OAAO,MAAM,GAAG,MAAM,KAAK,KAAK;AAClG;AAGO,IAAM,kBAAoC,OAAO,WAAW;AACjE,QAAM,MAAgB,CAAC;AACvB,aAAW,KAAK,OAAQ,KAAI,KAAK,MAAM,eAAe,EAAE,MAAM,CAAC,CAAC;AAChE,SAAO;AACT;;;ACnCO,SAASA,YAAW,OAAgB,UAAyB,CAAC,GAA0B;AAC7F,SAAO,WAAe,OAAO;AAAA,IAC3B,GAAG;AAAA,IACH,cAAc,QAAQ,gBAAgB;AAAA,EACxC,CAAC;AACH;","names":["renderDeck"]}
@@ -147,6 +147,69 @@ function validateBrandProfile(input) {
147
147
  return { ok: true, profile: parsed.data };
148
148
  }
149
149
 
150
+ // src/inline.ts
151
+ var ESCAPES = {
152
+ "&": "&amp;",
153
+ "<": "&lt;",
154
+ ">": "&gt;",
155
+ '"': "&quot;",
156
+ "'": "&#39;"
157
+ };
158
+ function escapeHtml(value) {
159
+ return value.replace(/[&<>"']/g, (c) => ESCAPES[c]);
160
+ }
161
+ function escapeAttr(value) {
162
+ return escapeHtml(value);
163
+ }
164
+ function escapeCssString(value) {
165
+ return value.replace(/["'\\\n\r\f]/g, (c) => `\\${c.charCodeAt(0).toString(16)} `);
166
+ }
167
+ var SAFE_URL = /^(https?:|mailto:|tel:|#|\/)/i;
168
+ function isSafeLinkUrl(url) {
169
+ return SAFE_URL.test(url);
170
+ }
171
+ function inline(text) {
172
+ if (!text) return "";
173
+ text = text.replace(/[\uE000\uE001]/g, "");
174
+ if (!text) return "";
175
+ const codes = [];
176
+ let s = text.replace(/`([^`]+)`/g, (_, code) => {
177
+ codes.push(code);
178
+ return `\uE000${codes.length - 1}\uE000`;
179
+ });
180
+ const links = [];
181
+ s = s.replace(/\[([^[\]]+)\]\(([^)\s[]+)\)/g, (match, label, url) => {
182
+ if (!isSafeLinkUrl(url)) return label;
183
+ links.push(`<a href="${escapeAttr(url)}">${formatEmphasis(escapeHtml(label))}</a>`);
184
+ return `\uE001${links.length - 1}\uE001`;
185
+ });
186
+ s = formatEmphasis(escapeHtml(s));
187
+ s = s.replace(/\r?\n/g, "<br>");
188
+ s = s.replace(/\uE001(\d+)\uE001/g, (_, i) => links[Number(i)]);
189
+ s = s.replace(
190
+ /\uE000(\d+)\uE000/g,
191
+ (_, i) => `<code>${escapeHtml(codes[Number(i)])}</code>`
192
+ );
193
+ return s;
194
+ }
195
+ function formatEmphasis(escaped) {
196
+ return escaped.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/(^|[^*])\*(?!\s)([^*\n]+?)(?<!\s)\*(?!\*)/g, '$1<em class="hl">$2</em>');
197
+ }
198
+ function prose(text) {
199
+ if (!text) return "";
200
+ return text.trim().split(/\n\s*\n/).map((para) => {
201
+ const lines = para.split(/\n/);
202
+ if (lines.every((l) => /^\s*[-*]\s+/.test(l))) {
203
+ return `<ul>${lines.map((l) => `<li>${inline(l.replace(/^\s*[-*]\s+/, ""))}</li>`).join("")}</ul>`;
204
+ }
205
+ return `<p>${inline(para)}</p>`;
206
+ }).join("");
207
+ }
208
+ function plain(text) {
209
+ if (!text) return "";
210
+ return text.replace(/`([^`]+)`/g, "$1").replace(/\[([^[\]]+)\]\([^)[]+\)/g, "$1").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1");
211
+ }
212
+
150
213
  // src/theme.ts
151
214
  import { colors as colors2 } from "@classic-homes/theme-tokens";
152
215
 
@@ -1062,17 +1125,8 @@ ${fmt(p.value)}` : p.name
1062
1125
  })
1063
1126
  };
1064
1127
  }
1065
- var renderer;
1066
- function loadChartRenderer() {
1067
- renderer ??= import("./echarts-render-GDCT5QAF.js").then((m) => m.createRenderer());
1068
- return renderer;
1069
- }
1070
- async function renderChartSvg(spec, opts = {}) {
1071
- const width = opts.width ?? 960;
1072
- const height = opts.height ?? 520;
1073
- const r = await loadChartRenderer();
1074
- const svg = r.render(chartOption(spec, opts.palette), width, height);
1075
- const label = `${opts.title ? `${opts.title}: ` : ""}${describeChart(spec)}`.replace(
1128
+ function finishChartSvg(svg, spec, title2) {
1129
+ const label = `${title2 ? `${title2}: ` : ""}${describeChart(spec)}`.replace(
1076
1130
  /[&<>"]/g,
1077
1131
  (c) => `&#${c.charCodeAt(0)};`
1078
1132
  );
@@ -1098,69 +1152,6 @@ function describeChart(spec) {
1098
1152
  return `${kind} ${parts.join(". ")}.`;
1099
1153
  }
1100
1154
 
1101
- // src/inline.ts
1102
- var ESCAPES = {
1103
- "&": "&amp;",
1104
- "<": "&lt;",
1105
- ">": "&gt;",
1106
- '"': "&quot;",
1107
- "'": "&#39;"
1108
- };
1109
- function escapeHtml(value) {
1110
- return value.replace(/[&<>"']/g, (c) => ESCAPES[c]);
1111
- }
1112
- function escapeAttr(value) {
1113
- return escapeHtml(value);
1114
- }
1115
- function escapeCssString(value) {
1116
- return value.replace(/["'\\\n\r\f]/g, (c) => `\\${c.charCodeAt(0).toString(16)} `);
1117
- }
1118
- var SAFE_URL = /^(https?:|mailto:|tel:|#|\/)/i;
1119
- function isSafeLinkUrl(url) {
1120
- return SAFE_URL.test(url);
1121
- }
1122
- function inline(text) {
1123
- if (!text) return "";
1124
- text = text.replace(/[\uE000\uE001]/g, "");
1125
- if (!text) return "";
1126
- const codes = [];
1127
- let s = text.replace(/`([^`]+)`/g, (_, code) => {
1128
- codes.push(code);
1129
- return `\uE000${codes.length - 1}\uE000`;
1130
- });
1131
- const links = [];
1132
- s = s.replace(/\[([^[\]]+)\]\(([^)\s[]+)\)/g, (match, label, url) => {
1133
- if (!isSafeLinkUrl(url)) return label;
1134
- links.push(`<a href="${escapeAttr(url)}">${formatEmphasis(escapeHtml(label))}</a>`);
1135
- return `\uE001${links.length - 1}\uE001`;
1136
- });
1137
- s = formatEmphasis(escapeHtml(s));
1138
- s = s.replace(/\r?\n/g, "<br>");
1139
- s = s.replace(/\uE001(\d+)\uE001/g, (_, i) => links[Number(i)]);
1140
- s = s.replace(
1141
- /\uE000(\d+)\uE000/g,
1142
- (_, i) => `<code>${escapeHtml(codes[Number(i)])}</code>`
1143
- );
1144
- return s;
1145
- }
1146
- function formatEmphasis(escaped) {
1147
- return escaped.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/(^|[^*])\*(?!\s)([^*\n]+?)(?<!\s)\*(?!\*)/g, '$1<em class="hl">$2</em>');
1148
- }
1149
- function prose(text) {
1150
- if (!text) return "";
1151
- return text.trim().split(/\n\s*\n/).map((para) => {
1152
- const lines = para.split(/\n/);
1153
- if (lines.every((l) => /^\s*[-*]\s+/.test(l))) {
1154
- return `<ul>${lines.map((l) => `<li>${inline(l.replace(/^\s*[-*]\s+/, ""))}</li>`).join("")}</ul>`;
1155
- }
1156
- return `<p>${inline(para)}</p>`;
1157
- }).join("");
1158
- }
1159
- function plain(text) {
1160
- if (!text) return "";
1161
- return text.replace(/`([^`]+)`/g, "$1").replace(/\[([^[\]]+)\]\([^)[]+\)/g, "$1").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1");
1162
- }
1163
-
1164
1155
  // src/video.ts
1165
1156
  var YT = /^(?:https:\/\/)?(?:www\.|m\.)?(?:youtube\.com\/(?:watch\?(?:.*&)?v=|embed\/|shorts\/|live\/)|youtu\.be\/)([\w-]{11})/;
1166
1157
  var VIMEO = /^(?:https:\/\/)?(?:www\.|player\.)?vimeo\.com\/(?:video\/)?(\d{6,12})/;
@@ -4045,15 +4036,25 @@ async function renderDeck(input, options = {}) {
4045
4036
  })
4046
4037
  );
4047
4038
  const charts = /* @__PURE__ */ new Map();
4048
- for (const [key, req] of chartReqs) {
4049
- charts.set(
4050
- key,
4051
- await renderChartSvg(req.spec, {
4039
+ if (chartReqs.size) {
4040
+ if (!options.renderCharts) {
4041
+ throw new Error(
4042
+ "This deck has charts but no chart renderer: pass options.renderCharts (the @classic-homes/slides/core entry has no built-in one)."
4043
+ );
4044
+ }
4045
+ const reqs = [...chartReqs.values()];
4046
+ const svgs = await options.renderCharts(
4047
+ reqs.map((req) => ({
4048
+ spec: req.spec,
4052
4049
  width: req.width,
4053
4050
  height: req.height,
4054
4051
  palette: brand.stock ? void 0 : brand.palette
4055
- })
4052
+ }))
4056
4053
  );
4054
+ if (svgs.length !== reqs.length) {
4055
+ throw new Error(`renderCharts returned ${svgs.length} SVGs for ${reqs.length} charts.`);
4056
+ }
4057
+ reqs.forEach((req, i) => charts.set(req.key, svgs[i]));
4057
4058
  }
4058
4059
  const imageBudget = options.maxInlineImageBytes;
4059
4060
  if (imageBudget !== void 0 && (!Number.isFinite(imageBudget) || imageBudget < 0)) {
@@ -4498,6 +4499,9 @@ export {
4498
4499
  resolveBrand,
4499
4500
  checkPalette,
4500
4501
  validateBrandProfile,
4502
+ escapeHtml,
4503
+ inline,
4504
+ prose,
4501
4505
  PALETTE,
4502
4506
  LOGO_SVG,
4503
4507
  logoLockup,
@@ -4506,11 +4510,8 @@ export {
4506
4510
  ChartSpec,
4507
4511
  formatValue,
4508
4512
  chartOption,
4509
- renderChartSvg,
4513
+ finishChartSvg,
4510
4514
  describeChart,
4511
- escapeHtml,
4512
- inline,
4513
- prose,
4514
4515
  parseVideoUrl,
4515
4516
  videoEmbedUrl,
4516
4517
  LAYOUTS,
@@ -4553,4 +4554,4 @@ export {
4553
4554
  inlineDeckImages,
4554
4555
  externalizeDeckImages
4555
4556
  };
4556
- //# sourceMappingURL=chunk-HI5ZCAHO.js.map
4557
+ //# sourceMappingURL=chunk-MCG5BFJN.js.map