@openpresentation/opf 0.15.1 → 0.17.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.
Files changed (86) hide show
  1. package/README.md +19 -1
  2. package/dist/{catalog-refs-BSfGELpt.d.ts → catalog-refs-C4K4y-vh.d.ts} +1 -1
  3. package/dist/{catalog-schemas-CinPh5WA.d.ts → catalog-schemas-g7RFQBdc.d.ts} +1 -1
  4. package/dist/catalog.d.ts +2 -2
  5. package/dist/catalog.js +3 -23224
  6. package/dist/{chart-data-BnNqgnC-.d.ts → chart-data-DTxdRRT1.d.ts} +1 -1
  7. package/dist/{chunk-3JG4YXWF.js → chunk-BIC3SWPK.js} +12 -5
  8. package/dist/{chunk-O744SXXA.js → chunk-CW7GEAV3.js} +350 -141
  9. package/dist/{chunk-374R35TJ.js → chunk-ESRYBYLY.js} +1 -1
  10. package/dist/chunk-IPW5BS3X.js +973 -0
  11. package/dist/{chunk-3RKB4RDJ.js → chunk-IS5H5JKH.js} +4 -2
  12. package/dist/{chunk-TWQJLI6E.js → chunk-M4UR3NPN.js} +2 -2
  13. package/dist/chunk-N4EVOFEA.js +58 -0
  14. package/dist/{chunk-QDI75P2M.js → chunk-NLNYNBTS.js} +217 -20
  15. package/dist/{chunk-U4J75TRG.js → chunk-O263APNI.js} +5 -2
  16. package/dist/{chunk-UBFJ2QHH.js → chunk-O2CX5NEH.js} +36 -14
  17. package/dist/{chunk-5FTUIDUH.js → chunk-O3CVNJTL.js} +1 -1
  18. package/dist/{chunk-VGAOJEY7.js → chunk-PPROJP2E.js} +1 -1
  19. package/dist/{chunk-2CJPC4HI.js → chunk-RA64UQKI.js} +203 -53
  20. package/dist/chunk-TCD2FB2K.js +319 -0
  21. package/dist/chunk-WCG3THTD.js +23712 -0
  22. package/dist/chunk-WWYWFVOI.js +793 -0
  23. package/dist/{chunk-ZPU46TFA.js → chunk-YT7FMQER.js} +1 -1
  24. package/dist/{composition-D2tghM82.d.ts → composition-BbZ_9C-c.d.ts} +238 -139
  25. package/dist/composition.d.ts +3 -3
  26. package/dist/composition.js +1 -1
  27. package/dist/conversion-SBpTOwdg.d.ts +444 -0
  28. package/dist/convert.d.ts +4 -4
  29. package/dist/convert.js +7 -7
  30. package/dist/data.d.ts +28 -11
  31. package/dist/data.js +2 -2
  32. package/dist/deck-report-Bx3naFlF.d.ts +40 -0
  33. package/dist/deck.d.ts +47 -0
  34. package/dist/deck.js +12 -0
  35. package/dist/diff.d.ts +1 -1
  36. package/dist/diff.js +5 -5
  37. package/dist/docs.js +33 -27
  38. package/dist/examples.d.ts +1 -1
  39. package/dist/examples.js +3229 -985
  40. package/dist/{finding-CT_f_vbD.d.ts → finding-CDQ_wH8j.d.ts} +1 -1
  41. package/dist/format.js +2 -2
  42. package/dist/index.d.ts +43 -12
  43. package/dist/index.js +15 -805
  44. package/dist/markdown.d.ts +7 -8
  45. package/dist/markdown.js +7 -7
  46. package/dist/node-engine.d.ts +94 -0
  47. package/dist/node-engine.js +21 -0
  48. package/dist/node.d.ts +60 -0
  49. package/dist/node.js +54 -0
  50. package/dist/{pagination-wedbe7eq.d.ts → pagination-BhX4M48Y.d.ts} +21 -7
  51. package/dist/pagination.d.ts +4 -4
  52. package/dist/pagination.js +4 -4
  53. package/dist/patch.d.ts +1 -1
  54. package/dist/patch.js +4 -4
  55. package/dist/{presentation-CDKQVkyI.d.ts → presentation-fb8CZmRq.d.ts} +28 -83
  56. package/dist/repo-readme.js +1 -1
  57. package/dist/schemas.d.ts +14 -0
  58. package/dist/schemas.js +1 -1
  59. package/dist/spec/catalogs/layouts/business-model-canvas.json +66 -1
  60. package/dist/spec/catalogs/layouts/capability-map.json +53 -3
  61. package/dist/spec/catalogs/layouts/chart-2x.json +32 -8
  62. package/dist/spec/catalogs/layouts/chart-3x-bottom-vertical-title-left.json +16 -10
  63. package/dist/spec/catalogs/layouts/chart-3x-left-title-center.json +16 -10
  64. package/dist/spec/catalogs/layouts/chart-3x-left.json +16 -10
  65. package/dist/spec/catalogs/layouts/chart-3x-right-title-center.json +16 -10
  66. package/dist/spec/catalogs/layouts/chart-3x-right.json +16 -10
  67. package/dist/spec/catalogs/layouts/chart-3x-top-vertical-title-left.json +16 -10
  68. package/dist/spec/catalogs/layouts/chart-3x.json +48 -12
  69. package/dist/spec/catalogs/layouts/dashboard.json +37 -5
  70. package/dist/spec/catalogs/layouts/image-2x.json +32 -8
  71. package/dist/spec/catalogs/layouts/image-3x.json +48 -12
  72. package/dist/spec/catalogs/layouts/index.json +1 -1
  73. package/dist/spec/catalogs/layouts/kpi-scorecard.json +48 -3
  74. package/dist/spec/catalogs/layouts/logic-tree.json +69 -0
  75. package/dist/spec/catalogs/layouts/market-size.json +26 -2
  76. package/dist/spec/catalogs/layouts/org-chart.json +53 -6
  77. package/dist/spec/catalogs/manifest.json +3 -3
  78. package/dist/spec/schemas/layout.schema.json +172 -5
  79. package/dist/spec/schemas/opf.schema.json +27 -43
  80. package/dist/types.d.ts +10 -32
  81. package/dist/{validation-types-BOcY9GTs.d.ts → validation-types-iZbXfES6.d.ts} +19 -8
  82. package/dist/validator.d.ts +8 -8
  83. package/dist/validator.js +3 -3
  84. package/dist/yaml.d.ts +6 -6
  85. package/dist/yaml.js +8 -321
  86. package/package.json +38 -1
@@ -0,0 +1,973 @@
1
+ import { defaultCatalog } from './chunk-WCG3THTD.js';
2
+ import { deckFormatOf, readDeckReport, stringify } from './chunk-N4EVOFEA.js';
3
+ import { paginate } from './chunk-O2CX5NEH.js';
4
+ import { validate, OPFValidationError } from './chunk-NLNYNBTS.js';
5
+ import { readFileSync, realpathSync, statSync } from 'fs';
6
+ import { createRequire } from 'module';
7
+ import path6 from 'path';
8
+ import { pathToFileURL, fileURLToPath } from 'url';
9
+ import { stat, readdir, lstat, mkdir, writeFile, rename, unlink, readFile } from 'fs/promises';
10
+ import { randomUUID } from 'crypto';
11
+ import { crc32 } from 'zlib';
12
+
13
+ // src/node/errors.ts
14
+ var OPFApiError = class extends Error {
15
+ code;
16
+ details;
17
+ findings;
18
+ constructor(message, code, extra = {}) {
19
+ super(message, extra.cause === void 0 ? void 0 : { cause: extra.cause });
20
+ this.name = new.target.name;
21
+ this.code = code;
22
+ this.details = extra.details ?? {};
23
+ this.findings = extra.findings ?? [];
24
+ }
25
+ };
26
+ var OPFExportError = class extends OPFApiError {
27
+ };
28
+ var OPFImportError = class extends OPFApiError {
29
+ };
30
+ function asApiError(Class, error, fallbackCode) {
31
+ if (error instanceof Class) return error;
32
+ if (error instanceof OPFApiError) return new Class(error.message, error.code, { details: error.details, findings: error.findings, cause: error });
33
+ const failure = error;
34
+ return new Class(failure?.message ?? String(error), typeof failure?.code === "string" ? failure.code : fallbackCode, { cause: error });
35
+ }
36
+ var RENDER_PACKAGE = "@openpresentation/opf-render";
37
+ var PPTX_PACKAGE = "@openpresentation/opf-pptx";
38
+ var PEER_RANGES = { [RENDER_PACKAGE]: "^0.17.0", [PPTX_PACKAGE]: "^0.17.0" };
39
+ var RENDER_EXTRAS = {
40
+ /** The faces of the office pack, which the CLI and `convert` always load (every format). */
41
+ fonts: ["@expo-google-fonts/roboto@0.4.3", "@expo-google-fonts/roboto-mono@0.4.2", "@expo-google-fonts/caladea@0.4.2", "@expo-google-fonts/arimo@0.4.3", "@expo-google-fonts/tinos@0.4.2", "@expo-google-fonts/cousine@0.4.3", "@expo-google-fonts/gelasio@0.4.1", "@expo-google-fonts/noto-sans@0.4.2"],
42
+ /** PNG output (and raster fallbacks): the SVG rasterizer, and sharp for WebP and rotated JPEG pictures. */
43
+ png: ["@resvg/resvg-js@^2.6.2", "sharp@^0.35.5"],
44
+ /** PDF: vector mode (the default) needs nothing for text; raster mode needs pdf-lib; pictures in a vector PDF need sharp. */
45
+ pdfRaster: ["pdf-lib@^1.17.1"],
46
+ pdfPictures: ["sharp@^0.35.5"]
47
+ };
48
+ var renderHint = () => ` ${RENDER_PACKAGE} also needs its optional peers, by output:
49
+ every format (fonts): npm install ${RENDER_EXTRAS.fonts.join(" ")}
50
+ png: npm install ${RENDER_EXTRAS.png.join(" ")}
51
+ pdf: npm install ${RENDER_EXTRAS.pdfRaster.join(" ")} (raster mode), ${RENDER_EXTRAS.pdfPictures.join(" ")} (when the deck has pictures)
52
+ svg: fonts only`;
53
+ var hint = (name) => `${name} is not installed. It is an optional peer of @openpresentation/opf, loaded only by @openpresentation/opf/node and the opf commands that need it. Install it next to the CLI or in your project:
54
+ npm install -g ${name}@${PEER_RANGES[name]} (global CLI)
55
+ npm install ${name}@${PEER_RANGES[name]} (project that imports @openpresentation/opf/node or depends on the CLI)
56
+ npx -p @openpresentation/cli -p @openpresentation/opf-render -p @openpresentation/opf-pptx opf <command> ... (one run)` + (name === RENDER_PACKAGE ? `
57
+ ${renderHint()}` : "");
58
+ function bases() {
59
+ return [import.meta.url, pathToFileURL(path6.join(process.cwd(), "noop.js")).href];
60
+ }
61
+ function locate(specifier) {
62
+ for (const base of bases()) {
63
+ try {
64
+ return createRequire(base).resolve(specifier);
65
+ } catch (error) {
66
+ const code = error.code;
67
+ if (code !== "MODULE_NOT_FOUND" && code !== "ERR_PACKAGE_PATH_NOT_EXPORTED") throw error;
68
+ }
69
+ }
70
+ return void 0;
71
+ }
72
+ var cache = /* @__PURE__ */ new Map();
73
+ async function loadPeer(name, subpath = "") {
74
+ const key = `${name}${subpath}`;
75
+ const cached = cache.get(key);
76
+ if (cached) return cached;
77
+ const manifestPath = locate(`${name}/package.json`);
78
+ const entry = locate(`${name}${subpath}`);
79
+ if (!manifestPath || !entry) throw new OPFApiError(hint(name), "peer-not-installed", { details: { package: name, range: PEER_RANGES[name] } });
80
+ let module;
81
+ try {
82
+ module = await import(pathToFileURL(entry).href);
83
+ } catch (error) {
84
+ throw new OPFApiError(`${name} is installed at ${path6.dirname(manifestPath)} but did not load: ${error.message}`, "peer-load-failed", { details: { package: name }, cause: error });
85
+ }
86
+ const version = JSON.parse(readFileSync(manifestPath, "utf8")).version ?? "unknown";
87
+ const peer = { module, version, name };
88
+ cache.set(key, peer);
89
+ return peer;
90
+ }
91
+ function requireFeature(peer, names) {
92
+ const missing = names.filter((name) => typeof peer.module[name] !== "function");
93
+ if (missing.length)
94
+ throw new OPFApiError(`${peer.name}@${peer.version} does not provide ${missing.join(", ")}. Install ${peer.name}@${PEER_RANGES[peer.name]}.`, "peer-too-old", { details: { package: peer.name } });
95
+ }
96
+ function missingPeerFrom(error) {
97
+ let current = error;
98
+ for (let depth = 0; current && typeof current === "object" && depth < 4; depth++, current = current.cause) {
99
+ const details = current.details ?? {};
100
+ const fontPackages = current.code === "font-resource-unavailable" && (Array.isArray(details.packages) || typeof details.package === "string");
101
+ if (current.code !== "converter-missing" && !fontPackages) continue;
102
+ const kept = Object.fromEntries(["package", "packages", "range", "install", "purpose"].filter((key) => details[key] !== void 0).map((key) => [key, details[key]]));
103
+ return new OPFApiError(current.message ?? String(current.code), details.installed === true ? "peer-load-failed" : "peer-not-installed", { details: kept, cause: error });
104
+ }
105
+ return void 0;
106
+ }
107
+ async function loadRenderer() {
108
+ const render = await loadPeer(RENDER_PACKAGE);
109
+ requireFeature(render, ["renderSlideSvg", "svgToPng", "svgToPdf"]);
110
+ const fonts = await loadPeer(RENDER_PACKAGE, "/fonts-node");
111
+ requireFeature(fonts, ["loadFonts"]);
112
+ return { render, fonts: fonts.module };
113
+ }
114
+ async function loadPptx() {
115
+ const pptx = await loadPeer(PPTX_PACKAGE);
116
+ requireFeature(pptx, ["toPptx", "fromPptx", "inventoryTypefaces"]);
117
+ return pptx;
118
+ }
119
+ var FONT_FILE = /\.(ttf|otf)$/i;
120
+ async function listFontDirectories(directories, label = "--font-dir") {
121
+ const files = [];
122
+ for (const directory of directories) {
123
+ const resolved = path6.resolve(directory);
124
+ let names;
125
+ try {
126
+ if (!(await stat(resolved)).isDirectory()) throw new OPFApiError(`${label} ${directory} is not a directory.`, "invalid-option");
127
+ names = await readdir(resolved);
128
+ } catch (error) {
129
+ if (error instanceof OPFApiError) throw error;
130
+ throw new OPFApiError(`Cannot read ${label} ${directory}: ${error.code ?? error.message}.`, "invalid-option");
131
+ }
132
+ const found = names.filter((name) => FONT_FILE.test(name)).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
133
+ if (!found.length) throw new OPFApiError(`${label} ${directory} contains no .ttf or .otf files.`, "invalid-option");
134
+ files.push(...found.map((name) => path6.join(resolved, name)));
135
+ }
136
+ return files;
137
+ }
138
+ async function prepareFonts(renderer, presentation, userFonts, reporter, embedScriptFonts = false) {
139
+ try {
140
+ const handle = await renderer.fonts.loadFonts({
141
+ pack: "office",
142
+ substitutionPolicy: "visual",
143
+ scripts: "auto",
144
+ embedScriptFonts,
145
+ presentation,
146
+ faces: userFonts.map((file) => ({ path: file })),
147
+ onDiagnostic: (diagnostic) => reporter.add("fonts", scriptDiagnostic(diagnostic))
148
+ });
149
+ return { handle, userFonts };
150
+ } catch (error) {
151
+ const missing = missingPeerFrom(error);
152
+ if (missing) throw missing;
153
+ const failure = error;
154
+ throw new OPFApiError(failure.message ?? String(error), failure.code ?? "font-preparation-failed", {
155
+ details: failure.details?.package ? { package: failure.details.package } : {},
156
+ cause: error
157
+ });
158
+ }
159
+ }
160
+ var NEUTRAL_DECK = { slides: [] };
161
+ var shared = /* @__PURE__ */ new Map();
162
+ async function leaseSharedFonts(renderer, presentation, userFonts, reporter, embedScriptFonts = false) {
163
+ const select = renderer.fonts.autoScriptSelection;
164
+ if (typeof select !== "function") return void 0;
165
+ const selection = select(presentation);
166
+ if (selection.detected?.length || selection.scripts?.length || selection.unavailable?.length) return void 0;
167
+ const key = JSON.stringify([userFonts, embedScriptFonts]);
168
+ let entry = shared.get(key);
169
+ if (!entry) {
170
+ const diagnostics2 = [];
171
+ const sink = { add: (_source, diagnostic) => void diagnostics2.push(diagnostic) };
172
+ const ready = prepareFonts(renderer, NEUTRAL_DECK, userFonts, sink, embedScriptFonts).then((prepared) => ({ handle: prepared.handle, diagnostics: diagnostics2 }));
173
+ const created = { ready, queue: Promise.resolve() };
174
+ entry = created;
175
+ shared.set(key, created);
176
+ ready.catch(() => {
177
+ if (shared.get(key) === created) shared.delete(key);
178
+ });
179
+ }
180
+ const { handle, diagnostics } = await entry.ready;
181
+ let release;
182
+ const previous = entry.queue;
183
+ entry.queue = new Promise((resolve) => {
184
+ release = resolve;
185
+ });
186
+ await previous;
187
+ const pending = handle.pending;
188
+ if (typeof pending === "function" && pending.call(handle, presentation).length) {
189
+ release();
190
+ return void 0;
191
+ }
192
+ handle.registry?.clearSubstitutions?.();
193
+ for (const diagnostic of diagnostics) reporter.add("fonts", diagnostic);
194
+ return { handle, release };
195
+ }
196
+ function scriptDiagnostic(diagnostic) {
197
+ if (diagnostic.code !== "script-font-not-installed") return diagnostic;
198
+ const name = typeof diagnostic.package === "string" ? diagnostic.package : "the Noto script package";
199
+ return { ...diagnostic, message: `${diagnostic.message} Install ${name} next to the CLI (npm install -g ${name}) to draw this script with a bundled open font.` };
200
+ }
201
+ function embeddedFor(handle, mode) {
202
+ return mode === "none" ? [] : handle.embeddedFonts.map((face) => ({ ...face, embed: "used" }));
203
+ }
204
+ function substitutionRows(handle) {
205
+ const rows = /* @__PURE__ */ new Map();
206
+ for (const item of handle.substitutions) {
207
+ if (!item.substitute) continue;
208
+ rows.set(`${item.requestedFamily}\0${item.resolvedFamily}`, { requested: item.requestedFamily, resolved: item.resolvedFamily, compatibility: item.compatibility });
209
+ }
210
+ return [...rows.values()].sort((a, b) => a.requested + a.resolved < b.requested + b.resolved ? -1 : 1);
211
+ }
212
+ function parseSlideSelection(spec, total, label = "--slides") {
213
+ if (total < 1) throw new OPFApiError("The presentation has no slides.", "no-slides");
214
+ const chosen = /* @__PURE__ */ new Set();
215
+ for (const part of spec.split(",")) {
216
+ const text = part.trim();
217
+ const match = /^(\d*)(-?)(\d*)$/.exec(text);
218
+ if (!text || !match || !match[1] && !match[3]) throw new OPFApiError(`${label} needs numbers like 1,3-5 (got "${spec}").`, "invalid-option");
219
+ const [, from, dash, to] = match;
220
+ const first = from ? Number(from) : 1;
221
+ const last = dash ? to ? Number(to) : total : first;
222
+ if (first < 1 || last < first) throw new OPFApiError(`${label} range "${text}" is not valid (slides count from 1).`, "invalid-option");
223
+ if (last > total) throw new OPFApiError(`${label} ${text} is outside the presentation, which has ${total} slide${total === 1 ? "" : "s"}.`, "invalid-option");
224
+ for (let n = first; n <= last; n++) chosen.add(n);
225
+ }
226
+ return [...chosen].sort((a, b) => a - b);
227
+ }
228
+ function stemOf(input) {
229
+ if (input === "-") return "deck";
230
+ return path6.basename(input).replace(/\.opf\.json$/i, "").replace(/\.(json|pptx|potx)$/i, "") || "deck";
231
+ }
232
+ function deckStem(deck, input) {
233
+ const root = deck && typeof deck === "object" && !Array.isArray(deck) ? deck : {};
234
+ const clean = (value) => value.replace(/[/:*?"<>|\p{Cc}]+/gu, "-").replace(/\s+/g, "-").replace(/-{2,}/g, "-").replace(/^[-.\s]+|[-.\s]+$/g, "");
235
+ const text = (value) => typeof value === "string" ? value : "";
236
+ const slug = (value) => value.replace(/[^\p{L}\p{N}_-]+/gu, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "");
237
+ return clean(text(root.filename).trim().replace(/\.(pptx|pdf|png|svg)$/i, "")) || slug(text(root.name)) || stemOf(input);
238
+ }
239
+ async function checkDestinations(files, overwrite, flags = true) {
240
+ for (const file of files) {
241
+ try {
242
+ const stat2 = await lstat(file);
243
+ if (!overwrite) throw new OPFApiError(`Output already exists: ${file}. ${flags ? "Use --force." : "Pass overwrite: true (the default) to replace it."}`, "output-exists", { details: { path: file } });
244
+ if (!stat2.isFile()) throw new OPFApiError(`Refusing to replace a symlink or non-regular file${flags ? "" : `: ${file}`}.`, "output-not-file", { details: { path: file } });
245
+ } catch (error) {
246
+ if (error.code !== "ENOENT") throw error;
247
+ }
248
+ }
249
+ }
250
+ async function writeFiles(files, overwrite, flags = true) {
251
+ const unique = new Set(files.map((entry) => path6.resolve(entry.file)));
252
+ if (unique.size !== files.length) throw new OPFApiError("Two outputs would write the same file.", "invalid-option");
253
+ await checkDestinations([...unique], overwrite, flags);
254
+ const staged = [];
255
+ try {
256
+ for (const entry of files) {
257
+ const output = path6.resolve(entry.file);
258
+ await mkdir(path6.dirname(output), { recursive: true });
259
+ const temporary = path6.join(path6.dirname(output), `.${path6.basename(output)}.${randomUUID()}.tmp`);
260
+ await writeFile(temporary, entry.bytes, { flag: "wx" });
261
+ staged.push({ temporary, output });
262
+ }
263
+ for (const item of staged) {
264
+ await rename(item.temporary, item.output);
265
+ item.temporary = "";
266
+ }
267
+ } finally {
268
+ for (const item of staged) if (item.temporary) await unlink(item.temporary).catch(() => void 0);
269
+ }
270
+ }
271
+ function pointerOf(location) {
272
+ if (location === "" || location.startsWith("/")) return location;
273
+ return /^[A-Za-z_$][\w$-]*(\.[\w$-]+)*$/.test(location) ? `/${location.split(".").join("/")}` : location;
274
+ }
275
+
276
+ // src/node/reporter.ts
277
+ var rank = { error: 3, warning: 2, info: 1 };
278
+ var reaches = (findings, level) => findings.some((finding) => rank[finding.severity] >= rank[level]);
279
+ var INFO = /* @__PURE__ */ new Set([
280
+ "font-glyph-fallback",
281
+ "font-substituted",
282
+ "pdf-font-embedded",
283
+ "pdf-font-fallback",
284
+ "svg-image-rasterized",
285
+ "svg-sanitized",
286
+ "script-font-unavailable",
287
+ "media-provenance-omitted"
288
+ ]);
289
+ var HELP = {
290
+ "text-overflow": "Shorten the text, or split the slide: add --paginate (measured with the same fonts) or run opf paginate.",
291
+ "small-cell": "Give the table more room or fewer columns, or run opf paginate.",
292
+ "unresolved-asset": "Embed the image as a data URI, or keep the file inside the deck folder (see --asset-dir). URLs are never fetched.",
293
+ "asset-blocked": "Move the file under the deck folder or pass --asset-dir <directory> that contains it.",
294
+ "script-font-not-installed": "Install the Noto script package named in the message next to the CLI, then run the command again.",
295
+ "font-glyph-fallback": "The chosen font lacks these characters; a bundled font draws them. Supply a font with --font-dir to change that.",
296
+ "unresolved-reference": "Use an id from opf catalog <kind>, define the record in catalogs.custom, or run opf embed to embed what the deck references.",
297
+ "unresolved-content": "Check the content at this path; it could not be drawn.",
298
+ "chart-data-adapted": "The chart data was reshaped to export a native chart; see the message.",
299
+ "chart-data-unplottable": "Provide inline chart data with at least one numeric column.",
300
+ "content-placeholder": "This content has no PowerPoint form; a placeholder frame stands in for it.",
301
+ "unsupported-image-treatment": "Remove or change the image treatment at this path.",
302
+ "date-needs-value": "Pass --date YYYY-MM-DD. The CLI never reads a clock, so output stays reproducible.",
303
+ "font-unavailable": "Pass --font-dir with the font files, or choose a font the bundled pack covers (opf catalog fontSchemes).",
304
+ "missing-glyph": "Install the script font package named by script-font-not-installed, or supply a covering font with --font-dir.",
305
+ "duplicate-font-face": "A --font-dir face repeats a bundled family, weight and style; remove it or rename the family."
306
+ };
307
+ var Reporter = class {
308
+ findings = [];
309
+ seen = /* @__PURE__ */ new Set();
310
+ constructor(initial = []) {
311
+ for (const item of initial) this.findings.push(item);
312
+ }
313
+ /** Add a library diagnostic (`code`, `path`, `message` and details). Identical entries are kept once. */
314
+ add(source, diagnostic, severity) {
315
+ const { code, path: where, message, ...details } = diagnostic;
316
+ const entry = {
317
+ ruleId: `${source}/${code}`,
318
+ severity: severity ?? (INFO.has(code) ? "info" : "warning"),
319
+ category: source,
320
+ path: pointerOf(where ?? ""),
321
+ scope: "document",
322
+ message,
323
+ help: HELP[code] ?? "",
324
+ ...Object.fromEntries(Object.entries(details).filter(([, value]) => value !== void 0))
325
+ };
326
+ const key = JSON.stringify([entry.ruleId, entry.path, entry.message]);
327
+ if (this.seen.has(key)) return;
328
+ this.seen.add(key);
329
+ this.findings.push(entry);
330
+ }
331
+ /** Add a finding as the library made it (the `findings` of a thrown OPFRenderError, OPFPptxError or core validation error). Identical entries are kept once. */
332
+ addFinding(finding) {
333
+ const key = JSON.stringify([finding.ruleId, finding.path, finding.message]);
334
+ if (this.seen.has(key)) return;
335
+ this.seen.add(key);
336
+ this.findings.push(finding);
337
+ }
338
+ error(source, code, message, where = "", details = {}) {
339
+ this.add(source, { code, path: where, message, ...details }, "error");
340
+ }
341
+ get counts() {
342
+ const counts = { error: 0, warning: 0, info: 0 };
343
+ for (const item of this.findings) counts[item.severity] += 1;
344
+ return counts;
345
+ }
346
+ get failed() {
347
+ return this.counts.error > 0;
348
+ }
349
+ };
350
+ function reportThrown(reporter, source, error) {
351
+ const failure = error;
352
+ const thrown = Array.isArray(failure.findings) ? failure.findings.filter((item) => typeof item?.ruleId === "string" && typeof item.message === "string") : [];
353
+ if (thrown.some((item) => item.severity === "error")) {
354
+ for (const item of thrown) reporter.addFinding(item);
355
+ return;
356
+ }
357
+ const code = typeof failure.code === "string" ? failure.code : "failed";
358
+ const details = failure.details ?? {};
359
+ const where = typeof details.path === "string" ? details.path : typeof failure.path === "string" ? failure.path : "";
360
+ const extra = {};
361
+ for (const key of ["fontFamily", "character", "family", "package", "script"]) if (details[key] !== void 0) extra[key] = details[key];
362
+ reporter.error(source, code, failure.message ?? String(error), where, extra);
363
+ }
364
+ function finishReport(base, reporter, failOn) {
365
+ const counts = reporter.counts;
366
+ const ok = base.valid && counts.error === 0 && !reaches(reporter.findings, failOn);
367
+ return { ok, valid: base.valid && counts.error === 0, schemaValid: base.schemaValid, findings: reporter.findings, counts, checks: base.checks };
368
+ }
369
+ var DOS_TIME = 0;
370
+ var DOS_DATE = 1980 - 1980 << 9 | 1 << 5 | 1;
371
+ function createZip(entries) {
372
+ const encoder = new TextEncoder();
373
+ const chunks = [];
374
+ const central = [];
375
+ let offset = 0;
376
+ for (const entry of [...entries].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
377
+ const name = encoder.encode(entry.name);
378
+ const crc = crc32(entry.bytes);
379
+ if (entry.bytes.length > 4294967294 || offset > 4294967294 || entries.length > 65534) throw new RangeError("The archive is too large for a plain ZIP file.");
380
+ const local = new DataView(new ArrayBuffer(30));
381
+ local.setUint32(0, 67324752, true);
382
+ local.setUint16(4, 20, true);
383
+ local.setUint16(6, 2048, true);
384
+ local.setUint16(8, 0, true);
385
+ local.setUint16(10, DOS_TIME, true);
386
+ local.setUint16(12, DOS_DATE, true);
387
+ local.setUint32(14, crc, true);
388
+ local.setUint32(18, entry.bytes.length, true);
389
+ local.setUint32(22, entry.bytes.length, true);
390
+ local.setUint16(26, name.length, true);
391
+ local.setUint16(28, 0, true);
392
+ const header = new DataView(new ArrayBuffer(46));
393
+ header.setUint32(0, 33639248, true);
394
+ header.setUint16(4, 20, true);
395
+ header.setUint16(6, 20, true);
396
+ header.setUint16(8, 2048, true);
397
+ header.setUint16(10, 0, true);
398
+ header.setUint16(12, DOS_TIME, true);
399
+ header.setUint16(14, DOS_DATE, true);
400
+ header.setUint32(16, crc, true);
401
+ header.setUint32(20, entry.bytes.length, true);
402
+ header.setUint32(24, entry.bytes.length, true);
403
+ header.setUint16(28, name.length, true);
404
+ header.setUint32(42, offset, true);
405
+ chunks.push(new Uint8Array(local.buffer), name, entry.bytes);
406
+ central.push(new Uint8Array(header.buffer), name);
407
+ offset += 30 + name.length + entry.bytes.length;
408
+ }
409
+ const size = central.reduce((total, chunk) => total + chunk.length, 0);
410
+ const end = new DataView(new ArrayBuffer(22));
411
+ end.setUint32(0, 101010256, true);
412
+ end.setUint16(8, entries.length, true);
413
+ end.setUint16(10, entries.length, true);
414
+ end.setUint32(12, size, true);
415
+ end.setUint32(16, offset, true);
416
+ return Buffer.concat([...chunks, ...central, new Uint8Array(end.buffer)]);
417
+ }
418
+ var MEDIA = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp", ".svg": "image/svg+xml" };
419
+ var MAX_BYTES = 32 * 1024 * 1024;
420
+ var startsWith = (bytes, signature, offset = 0) => signature.every((value, index) => bytes[offset + index] === value);
421
+ function looksLike(type, bytes) {
422
+ switch (type) {
423
+ case "image/png":
424
+ return startsWith(bytes, [137, 80, 78, 71]);
425
+ case "image/jpeg":
426
+ return startsWith(bytes, [255, 216, 255]);
427
+ case "image/gif":
428
+ return startsWith(bytes, [71, 73, 70, 56]);
429
+ case "image/webp":
430
+ return startsWith(bytes, [82, 73, 70, 70]) && startsWith(bytes, [87, 69, 66, 80], 8);
431
+ default:
432
+ return /<svg[\s>]/i.test(new TextDecoder().decode(bytes.subarray(0, 4096)));
433
+ }
434
+ }
435
+ var AssetError = class extends Error {
436
+ constructor(message, details) {
437
+ super(message);
438
+ this.details = details;
439
+ }
440
+ details;
441
+ code = "asset-unresolved";
442
+ };
443
+ function createImageResolver(root, reporter, passDirectory = "pass --asset-dir") {
444
+ const base = root === void 0 ? void 0 : realpathSync(root);
445
+ const cache2 = /* @__PURE__ */ new Map();
446
+ const shown = (source) => source.length > 80 ? `${source.slice(0, 77)}...` : source;
447
+ function load(source) {
448
+ if (/^[a-z][a-z0-9+.-]*:/i.test(source) && !/^file:/i.test(source) && !/^[a-z]:[\\/]/i.test(source)) return { skipped: true };
449
+ if (base === void 0) return { problem: "blocked", reason: "no asset directory was given, so no local file is read." };
450
+ let file;
451
+ try {
452
+ file = /^file:/i.test(source) ? fileURLToPath(source) : path6.resolve(base, source);
453
+ } catch {
454
+ return { problem: "blocked", reason: "the path is not valid." };
455
+ }
456
+ let real;
457
+ try {
458
+ real = realpathSync(file);
459
+ } catch {
460
+ return { problem: "missing", reason: `no such file under ${base}.` };
461
+ }
462
+ const relative = path6.relative(base, real);
463
+ if (relative.startsWith("..") || path6.isAbsolute(relative)) return { problem: "blocked", reason: `it is outside the asset directory ${base}.` };
464
+ const mediaType = MEDIA[path6.extname(real).toLowerCase()];
465
+ if (!mediaType) return { problem: "blocked", reason: "only .png, .jpg, .jpeg, .gif, .webp and .svg files are read." };
466
+ const info = statSync(real);
467
+ if (!info.isFile() || info.size > MAX_BYTES) return { problem: "blocked", reason: `it is not a regular file under ${MAX_BYTES / 1024 / 1024} MB.` };
468
+ const bytes = new Uint8Array(readFileSync(real));
469
+ if (!looksLike(mediaType, bytes)) return { problem: "blocked", reason: `the content is not a ${mediaType} image.` };
470
+ return { image: { bytes, mediaType } };
471
+ }
472
+ function read(source) {
473
+ let outcome = cache2.get(source);
474
+ if (!outcome) {
475
+ outcome = load(source);
476
+ cache2.set(source, outcome);
477
+ }
478
+ return outcome;
479
+ }
480
+ return {
481
+ /** opf-render: a data URI string, or null for the placeholder (a missing file is reported by the renderer as unresolved-asset). */
482
+ forSvg: (source, context) => {
483
+ if (!source) return null;
484
+ const outcome = read(source);
485
+ if ("image" in outcome) return `data:${outcome.image.mediaType};base64,${Buffer.from(outcome.image.bytes).toString("base64")}`;
486
+ if ("problem" in outcome && outcome.problem === "blocked") reporter.add("cli", { code: "asset-blocked", path: context.path, message: `Image "${shown(source)}" was not read: ${outcome.reason}` });
487
+ return null;
488
+ },
489
+ /** opf-pptx: bytes and media type; null for URLs and references it handles itself. An unreadable local file stops the export. */
490
+ forPptx: (source, context) => {
491
+ const outcome = read(source);
492
+ if ("image" in outcome) return { data: outcome.image.bytes, mediaType: outcome.image.mediaType };
493
+ if ("skipped" in outcome) return null;
494
+ throw new AssetError(`Image "${shown(source)}" was not read: ${outcome.reason} Fix the path, embed the image as a data URI, or ${passDirectory}.`, { path: context.path });
495
+ }
496
+ };
497
+ }
498
+
499
+ // src/node/catalogs.ts
500
+ var DEFAULT_CATALOGS = [defaultCatalog];
501
+
502
+ // src/node/export.ts
503
+ var EXPORT_FORMATS = ["svg", "png", "pdf", "pptx"];
504
+ var NO_FONTS = { fontFiles: [], useBundledFonts: false, loadSystemFonts: false };
505
+ var invalid = (message) => new OPFApiError(message, "invalid-option");
506
+ var oneOf = (name, value, allowed) => {
507
+ if (value === void 0) return void 0;
508
+ if (typeof value !== "string" || !allowed.includes(value)) throw invalid(`${name} must be one of: ${allowed.join(", ")}.`);
509
+ return value;
510
+ };
511
+ function checkDate(value, label = "date") {
512
+ if (value === void 0) return void 0;
513
+ const text = String(value);
514
+ const parsed = /^\d{4}-\d{2}-\d{2}$/.test(text) ? /* @__PURE__ */ new Date(`${text}T00:00:00Z`) : void 0;
515
+ if (!parsed || Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== text) throw invalid(`${label} must be a real calendar date, YYYY-MM-DD.`);
516
+ return text;
517
+ }
518
+ function checkScale(value, label = "scale") {
519
+ if (value === void 0) return 1;
520
+ const scale = Number(value);
521
+ if (typeof value === "boolean" || !Number.isFinite(scale) || scale < 0.1 || scale > 8) throw invalid(`${label} must be a number from 0.1 to 8 (1 draws the 1280 x 720 reference slide at 1280 x 720 pixels).`);
522
+ return scale;
523
+ }
524
+ function resolveExportOptions(options) {
525
+ if (!options || typeof options !== "object") throw invalid("An export needs options with a format: svg, png, pdf or pptx.");
526
+ const format = oneOf("format", options.format, EXPORT_FORMATS);
527
+ if (!format) throw invalid("An export needs options.format: svg, png, pdf or pptx.");
528
+ const pdfMode = oneOf("pdfMode", options.pdfMode, ["vector", "raster"]);
529
+ const chartex = oneOf("chartex", options.chartex, ["auto", "native", "fallback"]);
530
+ const provenance = oneOf("provenance", options.provenance, ["full", "references-only", "none"]);
531
+ const imageFormat = oneOf("imageFormat", options.imageFormat, ["compatible", "preserve"]);
532
+ const svgFonts = oneOf("svgFonts", options.svgFonts, ["used", "none"]);
533
+ if (pdfMode !== void 0 && format !== "pdf") throw invalid("pdfMode applies to format pdf.");
534
+ if ((chartex !== void 0 || provenance !== void 0 || imageFormat !== void 0) && format !== "pptx") throw invalid("chartex, provenance and imageFormat apply to format pptx.");
535
+ if (options.scale !== void 0 && format !== "png" && format !== "pdf") throw invalid("scale applies to format png (and raster pdf).");
536
+ if (options.includeHidden && format === "pptx") throw invalid("includeHidden applies to per-slide image and PDF output; the PPTX keeps a hidden slide as a hidden slide.");
537
+ if (options.slides !== void 0 && format === "pptx") throw invalid("slides is not available for pptx: the whole presentation is exported.");
538
+ if (svgFonts !== void 0 && format !== "svg") throw invalid("svgFonts applies to format svg.");
539
+ if (options.zip && format !== "svg" && format !== "png") throw invalid("zip applies to format svg and png.");
540
+ if (options.slides !== void 0 && typeof options.slides !== "string" && !(Array.isArray(options.slides) && options.slides.every((n) => Number.isInteger(n) && n >= 1))) throw invalid('slides must be a selection such as "1,3-5" or an array of slide numbers counted from 1.');
541
+ const scale = checkScale(options.scale);
542
+ checkDate(options.date);
543
+ if (options.fonts !== void 0 && options.fontDirs?.length) throw invalid("Give fonts or fontDirs, not both: fontDirs loads font files into the handle that fonts already is.");
544
+ return { ...options, format, svgFonts: svgFonts ?? "used", scale };
545
+ }
546
+ function assetRoot(directory, flags = false) {
547
+ if (directory === void 0) return void 0;
548
+ const resolved = path6.resolve(directory);
549
+ try {
550
+ if (!statSync(resolved).isDirectory()) throw new Error("not a directory");
551
+ } catch (error) {
552
+ throw invalid(`${flags ? "--asset-dir" : "assetDir"} ${directory} is not a readable directory (${error.code ?? error.message}).`);
553
+ }
554
+ return resolved;
555
+ }
556
+ var svgSize = (svg) => {
557
+ const tag = /<svg\b[^>]*>/.exec(svg)?.[0] ?? "";
558
+ const width = Number(/\bwidth="([\d.]+)"/.exec(tag)?.[1]);
559
+ const height = Number(/\bheight="([\d.]+)"/.exec(tag)?.[1]);
560
+ return Number.isFinite(width) && Number.isFinite(height) ? { width, height } : {};
561
+ };
562
+ var pngSize = (bytes) => bytes.length > 24 ? { width: new DataView(bytes.buffer, bytes.byteOffset).getUint32(16), height: new DataView(bytes.buffer, bytes.byteOffset).getUint32(20) } : {};
563
+ var MEDIA2 = { svg: "image/svg+xml", png: "image/png", pdf: "application/pdf", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation", zip: "application/zip" };
564
+ async function runExport(presentation, options, ctx) {
565
+ const { format, scale } = options;
566
+ const reporter = ctx.reporter;
567
+ const catalogs = options.catalogs ?? DEFAULT_CATALOGS;
568
+ const date = checkDate(options.date);
569
+ const renderer = ctx.renderer ?? await loadRenderer();
570
+ const pptx = format === "pptx" ? ctx.pptx ?? await loadPptx() : void 0;
571
+ const userFonts = options.fonts ? [] : ctx.userFonts ?? await listFontDirectories([...options.fontDirs ?? []], ctx.flags ? "--font-dir" : "fontDirs");
572
+ const resolver = createImageResolver(assetRoot(options.assetDir, ctx.flags), reporter, ctx.flags ? "pass --asset-dir" : "pass assetDir");
573
+ const embedScripts = format === "svg" && options.svgFonts === "used";
574
+ const lease = options.fonts ? void 0 : await leaseSharedFonts(renderer, presentation, userFonts, reporter, embedScripts);
575
+ try {
576
+ const handle = options.fonts ?? lease?.handle ?? (await prepareFonts(renderer, presentation, userFonts, reporter, embedScripts)).handle;
577
+ return await drawWith(presentation, handle, options, ctx, { reporter, catalogs, date, renderer, ...pptx ? { pptx } : {}, userFonts, resolver });
578
+ } finally {
579
+ lease?.release();
580
+ }
581
+ }
582
+ async function drawWith(presentation, handle, options, ctx, environment) {
583
+ const { format, scale } = options;
584
+ const { reporter, catalogs, date, renderer, pptx, userFonts, resolver } = environment;
585
+ let deck = presentation;
586
+ const fontSummary = () => ({
587
+ pack: "office",
588
+ substitutionPolicy: "visual",
589
+ userFonts,
590
+ substitutions: substitutionRows(handle),
591
+ ...handle.registry?.scriptSelection ? { scripts: handle.registry.scriptSelection } : {}
592
+ });
593
+ let pagination;
594
+ if (options.paginate) {
595
+ try {
596
+ const result = paginate(deck, { fonts: handle, catalogs });
597
+ deck = result.presentation;
598
+ pagination = { pages: result.pages };
599
+ } catch (error) {
600
+ reportThrown(reporter, "render", error);
601
+ return { files: [], fonts: fontSummary(), skippedHidden: [], failure: failureOf(error) };
602
+ }
603
+ }
604
+ const slideCount = deck.slides?.length ?? 0;
605
+ const isHidden = (number) => deck.slides?.[number - 1]?.hidden === true;
606
+ const everySlide = Array.from({ length: slideCount }, (_, index) => index + 1);
607
+ const slidesLabel = ctx.flags ? "--slides" : "slides";
608
+ const named = typeof options.slides === "string" ? parseSlideSelection(options.slides, slideCount, slidesLabel) : options.slides === void 0 ? void 0 : parseSlideSelection(options.slides.join(","), slideCount, slidesLabel);
609
+ const selected = format === "pptx" ? [] : named ?? (options.includeHidden ? everySlide : everySlide.filter((number) => !isHidden(number)));
610
+ const skippedHidden = format === "pptx" || named || options.includeHidden ? [] : everySlide.filter(isHidden);
611
+ if (format !== "pptx" && !selected.length)
612
+ throw new OPFApiError(skippedHidden.length ? ctx.flags ? "Every slide is hidden. Use --include-hidden to write them, or --slides to name slides." : "Every slide is hidden. Use includeHidden to write them, or slides to name slides." : "The presentation has no slides.", skippedHidden.length ? "all-slides-hidden" : "no-slides");
613
+ const stem = deckStem(deck, options.filename ?? "-");
614
+ const run = (extra) => ({ fonts: fontSummary(), ...pagination ? { pagination } : {}, skippedHidden, ...extra });
615
+ try {
616
+ if (format === "pptx") {
617
+ const module = pptx.module;
618
+ const bytes = await module.toPptx(deck, {
619
+ fonts: handle,
620
+ imageResolver: resolver.forPptx,
621
+ // The PNG fallback of an SVG picture is drawn by the same renderer the preview uses, with the same fonts handle,
622
+ // so SVG pictures export whichever way opf-pptx would find opf-render. A picture without text needs no faces.
623
+ svgRasterizer: async (svg, size) => new Uint8Array(await renderer.render.module.svgToPng(svg, { scale: size.scale, background: "rgba(0, 0, 0, 0)", fonts: size.text ? handle : NO_FONTS })),
624
+ onDiagnostic: (diagnostic) => reporter.add("pptx", diagnostic),
625
+ catalogs,
626
+ ...options.chartex ? { chartex: options.chartex } : {},
627
+ ...options.provenance ? { provenance: options.provenance === "none" ? false : options.provenance } : {},
628
+ ...options.imageFormat ? { imageFormat: options.imageFormat } : {},
629
+ ...date ? { date } : {}
630
+ });
631
+ return run({ files: [{ name: `${stem}.pptx`, type: MEDIA2.pptx, bytes }], skippedHidden: [] });
632
+ }
633
+ const embedded = format === "svg" ? embeddedFor(handle, options.svgFonts) : [];
634
+ const svgOptions = {
635
+ fonts: { textMeasurement: handle.textMeasurement, embeddedFonts: embedded },
636
+ imageResolver: resolver.forSvg,
637
+ onDiagnostic: (diagnostic) => reporter.add("render", diagnostic),
638
+ catalogs,
639
+ ...date ? { date } : {}
640
+ };
641
+ const svgs = [];
642
+ for (const number of selected) {
643
+ const svg = renderer.render.module.renderSlideSvg(deck, number - 1, svgOptions);
644
+ svgs.push({ slide: number, id: deck.slides[number - 1]?.id, svg });
645
+ }
646
+ const width = Math.max(3, String(slideCount).length);
647
+ const name = (slide, extension) => `${stem}-${String(slide).padStart(width, "0")}.${extension}`;
648
+ if (format === "pdf") {
649
+ const mode = options.pdfMode ?? "vector";
650
+ const title = typeof deck.name === "string" ? deck.name : void 0;
651
+ const bytes = await renderer.render.module.svgToPdf(
652
+ svgs.map((item) => item.svg),
653
+ { fonts: handle, scale, ...options.pdfMode ? { mode: options.pdfMode } : {}, ...mode === "vector" && title ? { metadata: { title } } : {}, onDiagnostic: (diagnostic) => reporter.add("pdf", diagnostic) }
654
+ );
655
+ return run({ files: [{ name: `${stem}.pdf`, type: MEDIA2.pdf, bytes, pages: svgs.length, slides: selected }], pdf: { mode } });
656
+ }
657
+ const items = [];
658
+ for (const item of svgs) {
659
+ if (format === "svg") {
660
+ const bytes = new TextEncoder().encode(item.svg);
661
+ items.push({ name: name(item.slide, "svg"), type: MEDIA2.svg, bytes, slide: item.slide, ...item.id === void 0 ? {} : { id: item.id }, ...svgSize(item.svg) });
662
+ } else {
663
+ const bytes = await renderer.render.module.svgToPng(item.svg, { fonts: handle, scale });
664
+ items.push({ name: name(item.slide, "png"), type: MEDIA2.png, bytes, slide: item.slide, ...item.id === void 0 ? {} : { id: item.id }, ...pngSize(bytes) });
665
+ }
666
+ }
667
+ if (options.zip) {
668
+ const bytes = createZip(items.map((item) => ({ name: item.name, bytes: item.bytes })));
669
+ return run({ files: [{ name: `${stem}.zip`, type: MEDIA2.zip, bytes, entries: items.map((item) => item.name) }] });
670
+ }
671
+ return run({ files: items });
672
+ } catch (error) {
673
+ if (error instanceof OPFApiError) throw error;
674
+ const missing = missingPeerFrom(error);
675
+ if (missing) throw missing;
676
+ reportThrown(reporter, format === "pptx" ? "pptx" : "render", error);
677
+ return { files: [], fonts: fontSummary(), ...pagination ? { pagination } : {}, skippedHidden: [], failure: failureOf(error) };
678
+ }
679
+ }
680
+ var failureOf = (error) => {
681
+ const failure = error;
682
+ return { code: typeof failure?.code === "string" ? failure.code : "failed", message: failure?.message ?? String(error) };
683
+ };
684
+ var VERSIONS = (renderer, pptx) => ({
685
+ renderer: { package: RENDER_PACKAGE, version: renderer.render.version },
686
+ ...pptx ? { pptx: { package: PPTX_PACKAGE, version: pptx.version } } : {}
687
+ });
688
+
689
+ // src/node/pptx-import.ts
690
+ async function runImport(bytes, options, reporter, loaded) {
691
+ const pptx = loaded ?? await loadPptx();
692
+ try {
693
+ const onDiagnostic = (diagnostic) => reporter.add("import", diagnostic);
694
+ if (options.signals) {
695
+ const result = await pptx.module.fromPptx(bytes, { onDiagnostic, signals: true });
696
+ return { presentation: result.presentation, signals: result.signals, pptx };
697
+ }
698
+ return { presentation: await pptx.module.fromPptx(bytes, { onDiagnostic }), pptx };
699
+ } catch (error) {
700
+ reportThrown(reporter, "import", error);
701
+ const failure = error;
702
+ return { pptx, failure: { code: typeof failure?.code === "string" ? failure.code : "failed", message: failure?.message ?? String(error) } };
703
+ }
704
+ }
705
+
706
+ // src/node/pipeline.ts
707
+ var checkOptions = (catalogs) => ({ only: ["format", "references"], catalogs: catalogs ?? DEFAULT_CATALOGS });
708
+ var firstError = (findings) => {
709
+ const first = findings.find((found) => found.severity === "error");
710
+ return first ? `${first.message} [${first.ruleId}]` : "see findings.";
711
+ };
712
+ async function prepareExport(options, flags = false) {
713
+ const resolved = resolveExportOptions(options);
714
+ const renderer = await loadRenderer();
715
+ const pptx = resolved.format === "pptx" ? await loadPptx() : void 0;
716
+ const userFonts = resolved.fonts ? [] : await listFontDirectories([...resolved.fontDirs ?? []], flags ? "--font-dir" : "fontDirs");
717
+ return { options: resolved, renderer, ...pptx ? { pptx } : {}, userFonts, flags };
718
+ }
719
+ async function runPreparedExport(prepared, presentation, checked) {
720
+ let initial = checked;
721
+ if (!initial) {
722
+ const check = validate(presentation, checkOptions(prepared.options.catalogs));
723
+ if (!check.valid) throw new OPFApiError(`The presentation is not valid OPF: ${firstError(check.findings)}`, "invalid-presentation", { findings: check.findings });
724
+ initial = check.findings;
725
+ }
726
+ const reporter = new Reporter(initial);
727
+ const { renderer, pptx, userFonts } = prepared;
728
+ const run = await runExport(presentation, prepared.options, { reporter, renderer, ...pptx ? { pptx } : {}, userFonts, flags: prepared.flags });
729
+ if (run.failure) throw new OPFApiError(run.failure.message, "export-failed", { details: { reason: run.failure.code }, findings: reporter.findings });
730
+ return {
731
+ files: run.files,
732
+ findings: reporter.findings,
733
+ fonts: run.fonts,
734
+ skippedHidden: run.skippedHidden,
735
+ ...run.pagination ? { pagination: run.pagination } : {},
736
+ ...run.pdf ? { pdf: run.pdf } : {},
737
+ ...VERSIONS(renderer, pptx)
738
+ };
739
+ }
740
+ async function importPresentation(bytes, options, loaded) {
741
+ const pptx = loaded ?? await loadPptx();
742
+ const reporter = new Reporter();
743
+ const run = await runImport(bytes, { signals: options.signals === true }, reporter, pptx);
744
+ if (run.failure || !run.presentation) throw new OPFApiError(run.failure?.message ?? "The file could not be read.", "import-failed", { details: { reason: run.failure?.code }, findings: reporter.findings });
745
+ const check = validate(run.presentation, checkOptions(options.catalogs));
746
+ for (const found of check.findings) reporter.addFinding(found);
747
+ if (!check.valid) throw new OPFApiError(`The imported presentation is not valid OPF: ${firstError(check.findings)}`, "invalid-presentation", { findings: reporter.findings });
748
+ return { result: { presentation: run.presentation, findings: reporter.findings, ...run.signals ? { signals: run.signals } : {}, pptx: { package: PPTX_PACKAGE, version: pptx.version } }, check };
749
+ }
750
+ var EXPORT_EXTENSIONS = { ".pdf": "pdf", ".pptx": "pptx", ".png": "png", ".svg": "svg" };
751
+ var FORMATS = ["pdf", "pptx", "png", "svg", "json", "yaml", "markdown"];
752
+ var DECK_MEDIA = { json: "application/json", yaml: "application/yaml", markdown: "text/markdown" };
753
+ var ZIP_MEDIA = "application/zip";
754
+ var INPUTS = ".pptx, .opf.md, .yaml, .yml or .json";
755
+ var OUTPUTS = ".pdf, .pptx, .png, .svg, .zip, .opf.md, .yaml, .yml or .json";
756
+ var DECKS = ".opf.md, .yaml, .yml or .json";
757
+ var invalid2 = (message, details) => new OPFApiError(message, "invalid-option", details ? { details } : {});
758
+ function deckFileFormat(file) {
759
+ const format = deckFormatOf(file);
760
+ return format !== "json" ? format : /\.json$/i.test(file) ? "json" : void 0;
761
+ }
762
+ function exportFormatOf(file) {
763
+ return EXPORT_EXTENSIONS[path6.extname(file).toLowerCase()];
764
+ }
765
+ function fileStem(file) {
766
+ return path6.basename(file).replace(/\.opf\.(md|json|ya?ml)$/i, "").replace(/\.(json|ya?ml|pptx)$/i, "") || "deck";
767
+ }
768
+ function sourceOf(input, flags) {
769
+ if (typeof input === "string") {
770
+ if (/\.pptx$/i.test(input)) return { kind: "pptx" };
771
+ const format = deckFileFormat(input);
772
+ if (format) return { kind: "deck", format };
773
+ throw invalid2(`${flags ? "opf convert" : "convert"} reads ${INPUTS} files; ${input} is none of them.`, { path: input });
774
+ }
775
+ if (input instanceof Uint8Array) return { kind: "pptx", bytes: input };
776
+ if (input instanceof ArrayBuffer) return { kind: "pptx", bytes: new Uint8Array(input) };
777
+ if (input && typeof input === "object" && !Array.isArray(input)) return { kind: "presentation" };
778
+ throw invalid2("convert takes a file path, a presentation object or the bytes of a .pptx file.");
779
+ }
780
+ function targetOf(output, options, flags) {
781
+ const format = options.format;
782
+ const formatLabel = flags ? "--format" : "format";
783
+ if (format !== void 0 && !FORMATS.includes(format)) throw invalid2(`${formatLabel} must be one of: ${FORMATS.join(", ")}.`);
784
+ if (output === void 0) {
785
+ if (format === void 0) throw invalid2(`convert without an output path needs ${formatLabel}: ${FORMATS.join(", ")}.`);
786
+ if (options.zip && format !== "png" && format !== "svg") throw invalid2("zip applies to format png and svg.");
787
+ return format === "json" || format === "yaml" || format === "markdown" ? { kind: "deck", format } : { kind: "export", format, zip: options.zip === true };
788
+ }
789
+ if (typeof output !== "string" || !output) throw invalid2(`${flags ? "opf convert" : "convert"} needs an output file path ending ${OUTPUTS}.`);
790
+ if (path6.extname(output).toLowerCase() === ".zip") {
791
+ if (format !== void 0 && format !== "png" && format !== "svg") throw invalid2(`A .zip output holds png or svg slides; ${formatLabel} ${format} is neither.`);
792
+ return { kind: "export", format: format ?? "png", zip: true };
793
+ }
794
+ if (options.zip) throw invalid2(`${flags ? "--zip" : "zip"} writes one archive: give an output ending .zip.`);
795
+ const exported = exportFormatOf(output);
796
+ const deck = exported ? void 0 : deckFileFormat(output);
797
+ const named = exported ?? deck;
798
+ if (!named) throw invalid2(`${flags ? "opf convert" : "convert"} writes ${OUTPUTS} files; ${output} is none of them.`, { path: output });
799
+ if (format !== void 0 && format !== named) throw invalid2(`${formatLabel} ${format} does not match the output ${output}, which is ${named}.`);
800
+ return exported ? { kind: "export", format: exported, zip: false } : { kind: "deck", format: deck };
801
+ }
802
+ var EXPORT_ONLY = ["slides", "includeHidden", "paginate", "scale", "pdfMode", "svgFonts", "chartex", "provenance", "imageFormat", "date", "fonts", "fontDirs", "assetDir"];
803
+ var given = (value) => value !== void 0 && value !== false && !(Array.isArray(value) && value.length === 0);
804
+ function checkApplicable(source, target, options, flags) {
805
+ const name = (key) => flags ? `--${key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`).replace(/^font-dirs$/, "font-dir")}` : key;
806
+ const names = (...keys) => keys.map(name).join(", ").replace(/, ([^,]*)$/, " and $1");
807
+ if (given(options.signals) && source.kind !== "pptx") throw invalid2(`${name("signals")} applies to a .pptx input.`);
808
+ if (target.kind === "deck") {
809
+ const extra = EXPORT_ONLY.filter((key) => given(options[key]));
810
+ if (extra.length) throw invalid2(`${names(...extra)} ${extra.length === 1 ? "applies" : "apply"} to export outputs (pdf, pptx, png, svg), not to a deck written as ${target.format}.`);
811
+ return;
812
+ }
813
+ const format = target.format;
814
+ if (options.pdfMode !== void 0 && format !== "pdf") throw invalid2(`${name("pdfMode")} applies to pdf output.`);
815
+ if ((options.chartex !== void 0 || options.provenance !== void 0 || options.imageFormat !== void 0) && format !== "pptx") throw invalid2(`${names("chartex", "provenance", "imageFormat")} apply to pptx output.`);
816
+ if (options.scale !== void 0 && format !== "png" && format !== "pdf") throw invalid2(`${name("scale")} applies to png output (and raster pdf).`);
817
+ if (given(options.includeHidden) && format === "pptx") throw invalid2(`${name("includeHidden")} applies to per-slide image and PDF output; the PPTX keeps a hidden slide as a hidden slide.`);
818
+ if (options.slides !== void 0 && format === "pptx") throw invalid2(`${name("slides")} is not available for pptx output: the whole presentation is exported.`);
819
+ if (options.svgFonts !== void 0 && format !== "svg") throw invalid2(`${name("svgFonts")} applies to svg output.`);
820
+ checkScale(options.scale, name("scale"));
821
+ checkDate(options.date, name("date"));
822
+ }
823
+ async function readInput(file) {
824
+ try {
825
+ return new Uint8Array(await readFile(file));
826
+ } catch (error) {
827
+ const reason = error.code ?? error.message;
828
+ const missing = reason === "ENOENT" || reason === "ENOTDIR";
829
+ throw new OPFApiError(`Cannot read ${file}: ${missing ? "no such file" : reason}.`, missing ? "input-not-found" : "input-unreadable", { details: { path: file, reason }, cause: error });
830
+ }
831
+ }
832
+ var inStep = async (Class, fallback, step) => {
833
+ try {
834
+ return await step();
835
+ } catch (error) {
836
+ throw asApiError(Class, error, fallback);
837
+ }
838
+ };
839
+ function deckText(presentation, format, schemaComment = false) {
840
+ try {
841
+ return stringify(presentation, { format, schemaComment });
842
+ } catch (error) {
843
+ const failure = error;
844
+ const findings = Array.isArray(failure.report?.findings) ? failure.report.findings : Array.isArray(failure.findings) ? failure.findings : [];
845
+ throw new OPFApiError(failure.message ?? String(error), "invalid-presentation", { findings, cause: error });
846
+ }
847
+ }
848
+ function stemOfDeck(presentation) {
849
+ const root = presentation;
850
+ const text = typeof root.filename === "string" ? root.filename.replace(/\.(opf\.)?(json|ya?ml|md)$/i, "") : typeof root.name === "string" ? root.name : "";
851
+ return text.replace(/[^\p{L}\p{N}_.-]+/gu, "-").replace(/-{2,}/g, "-").replace(/^[-.]+|[-.]+$/g, "") || "deck";
852
+ }
853
+ async function planConversion(input, output, options = {}, flags = false) {
854
+ if (!options || typeof options !== "object") throw invalid2("convert takes options as an object.");
855
+ const source = sourceOf(input, flags);
856
+ const target = targetOf(output, options, flags);
857
+ checkApplicable(source, target, options, flags);
858
+ const catalogs = options.catalogs ?? DEFAULT_CATALOGS;
859
+ const ExportClass = target.kind === "export" ? OPFExportError : OPFApiError;
860
+ const inputFile = typeof input === "string" ? input : void 0;
861
+ let prepared;
862
+ if (target.kind === "export") {
863
+ const { format: _format, zip: _zip, signals: _signals, overwrite: _overwrite, ...rest } = options;
864
+ const assetDir = options.assetDir ?? (inputFile === void 0 ? void 0 : path6.dirname(path6.resolve(inputFile)));
865
+ const exportOptions = {
866
+ ...rest,
867
+ format: target.format,
868
+ ...assetDir === void 0 ? {} : { assetDir },
869
+ // Without an output path the files are named as the commands name them: by the deck, else by the input file.
870
+ ...output === void 0 && inputFile !== void 0 ? { filename: fileStem(inputFile) } : {},
871
+ ...output === void 0 && target.zip ? { zip: true } : {}
872
+ };
873
+ prepared = await inStep(OPFExportError, "export-failed", () => prepareExport(exportOptions, flags));
874
+ }
875
+ let pptx;
876
+ if (source.kind === "pptx") pptx = await inStep(OPFImportError, "import-failed", () => loadPptx());
877
+ let presentation;
878
+ let check;
879
+ let findings;
880
+ let imported;
881
+ const read = inputFile === void 0 ? void 0 : { file: inputFile, bytes: await readInput(inputFile) };
882
+ if (source.kind === "pptx") {
883
+ const bytes = source.bytes ?? read?.bytes;
884
+ const result = await inStep(OPFImportError, "import-failed", () => importPresentation(bytes, { catalogs, signals: options.signals === true }, pptx));
885
+ imported = result.result;
886
+ presentation = result.result.presentation;
887
+ check = result.check;
888
+ findings = result.result.findings;
889
+ } else {
890
+ const report = source.kind === "deck" ? readDeckReport(Buffer.from(read.bytes).toString("utf8"), { format: source.format, validate: checkOptions(catalogs) }, "convert") : validate(input, checkOptions(catalogs));
891
+ if (!report.valid) throw new ExportClass(`${inputFile ?? "The presentation"} is not valid OPF: ${firstError(report.findings)}`, "invalid-presentation", { findings: report.findings });
892
+ presentation = source.kind === "deck" ? report.presentation : input;
893
+ check = report;
894
+ findings = report.findings;
895
+ }
896
+ const base = { check, format: target.format, zip: target.kind === "export" && target.zip, ...read ? { input: read } : {}, ...imported ? { imported } : {} };
897
+ if (target.kind === "deck") {
898
+ const bytes = new TextEncoder().encode(deckText(presentation, target.format));
899
+ const extension = `opf.${target.format === "markdown" ? "md" : target.format}`;
900
+ const name = output === void 0 ? `${inputFile === void 0 ? stemOfDeck(presentation) : fileStem(inputFile)}.${extension}` : path6.basename(output);
901
+ return { ...base, files: [{ name, ...output === void 0 ? {} : { path: output }, type: DECK_MEDIA[target.format], bytes }], findings };
902
+ }
903
+ const exported = await inStep(OPFExportError, "export-failed", () => runPreparedExport(prepared, presentation, findings));
904
+ const drawn = exported.files;
905
+ const facts = (file) => ({
906
+ ...file.slide === void 0 ? {} : { slide: file.slide },
907
+ ...file.id === void 0 ? {} : { id: file.id },
908
+ ...file.width === void 0 || file.height === void 0 ? {} : { width: file.width, height: file.height },
909
+ ...file.pages === void 0 ? {} : { pages: file.pages },
910
+ ...file.slides === void 0 ? {} : { slides: file.slides },
911
+ ...file.entries === void 0 ? {} : { entries: file.entries }
912
+ });
913
+ const done = (files) => ({ ...base, exported, findings: exported.findings, files });
914
+ if (output === void 0) return done(drawn.map((file) => ({ name: file.name, type: file.type, bytes: file.bytes, ...facts(file) })));
915
+ const single = (file) => done([{ name: path6.basename(output), path: output, type: file.type, bytes: file.bytes, ...facts(file) }]);
916
+ if (target.format === "pdf" || target.format === "pptx") return single(drawn[0]);
917
+ const stem = path6.basename(output, path6.extname(output));
918
+ const numbered = (file) => `${stem}-${/-(\d+)\.(?:png|svg)$/.exec(file.name)?.[1] ?? String(file.slide).padStart(3, "0")}.${target.format}`;
919
+ if (target.zip) {
920
+ const entries = drawn.map((file) => ({ name: numbered(file), bytes: file.bytes }));
921
+ return done([{ name: path6.basename(output), path: output, type: ZIP_MEDIA, bytes: createZip(entries), entries: entries.map((entry) => entry.name).sort() }]);
922
+ }
923
+ if (drawn.length === 1) return single(drawn[0]);
924
+ const folder = path6.dirname(output);
925
+ return done(drawn.map((file) => ({ name: numbered(file), path: path6.join(folder, numbered(file)), type: file.type, bytes: file.bytes, ...facts(file) })));
926
+ }
927
+ async function writePlanned(files, overwrite, flags = false) {
928
+ try {
929
+ await writeFiles(
930
+ files.map((file) => ({ file: file.path, bytes: file.bytes })),
931
+ overwrite,
932
+ flags
933
+ );
934
+ } catch (error) {
935
+ if (error instanceof OPFApiError) throw error;
936
+ const reason = error.code ?? error.message;
937
+ throw new OPFApiError(`Cannot write the output: ${error.message ?? reason}`, "output-unwritable", { details: { reason }, cause: error });
938
+ }
939
+ }
940
+ async function convertFiles(input, output, options = {}) {
941
+ const plan = await planConversion(input, output, options);
942
+ if (output !== void 0) await writePlanned(plan.files, options.overwrite !== false);
943
+ return { files: plan.files, findings: plan.findings, ...plan.imported?.signals ? { signals: plan.imported.signals } : {} };
944
+ }
945
+ async function openDeck(input, options = {}) {
946
+ const catalogs = options.catalogs ?? DEFAULT_CATALOGS;
947
+ if (input instanceof Uint8Array || input instanceof ArrayBuffer || typeof input === "string" && /\.pptx$/i.test(input)) {
948
+ const bytes = typeof input === "string" ? await readInput(input) : input instanceof Uint8Array ? input : new Uint8Array(input);
949
+ return (await inStep(OPFImportError, "import-failed", () => importPresentation(bytes, { catalogs }))).result.presentation;
950
+ }
951
+ if (typeof input !== "string" || !input) throw invalid2("open takes a file path or the bytes of a .pptx file.");
952
+ const format = deckFileFormat(input);
953
+ if (!format) throw invalid2(`open reads ${DECKS} and .pptx files; ${input} is none of them.`, { path: input });
954
+ const text = Buffer.from(await readInput(input)).toString("utf8");
955
+ const { presentation, format: _format, ...report } = readDeckReport(text, { format, catalogs, validate: checkOptions(catalogs) }, "open");
956
+ if (!report.valid) throw new OPFValidationError(report);
957
+ return presentation;
958
+ }
959
+ async function saveDeck(presentation, file, options = {}) {
960
+ if (!presentation || typeof presentation !== "object" || Array.isArray(presentation)) throw invalid2("save takes a presentation object.");
961
+ if (typeof file !== "string" || !file) throw invalid2("save takes a file path.");
962
+ const format = deckFileFormat(file);
963
+ if (!format) throw invalid2(`save writes ${DECKS} files; ${file} is none of them.${exportFormatOf(file) || /\.zip$/i.test(file) ? ` Export with convert(deck, "${file}").` : ""}`, { path: file });
964
+ if (options.validate !== false) {
965
+ const report = validate(presentation, checkOptions(options.catalogs ?? DEFAULT_CATALOGS));
966
+ if (!report.valid) throw new OPFValidationError(report);
967
+ }
968
+ const bytes = new TextEncoder().encode(deckText(presentation, format, options.schemaComment === true));
969
+ await writePlanned([{ name: path6.basename(file), path: file, type: DECK_MEDIA[format], bytes }], true);
970
+ return { path: file, format };
971
+ }
972
+
973
+ export { EXPORT_FORMATS, OPFApiError, OPFExportError, OPFImportError, PEER_RANGES, PPTX_PACKAGE, RENDER_PACKAGE, Reporter, VERSIONS, asApiError, checkDate, checkDestinations, checkOptions, checkScale, convertFiles, createZip, deckFileFormat, deckStem, embeddedFor, exportFormatOf, finishReport, importPresentation, leaseSharedFonts, listFontDirectories, loadPptx, loadRenderer, missingPeerFrom, openDeck, parseSlideSelection, planConversion, pointerOf, prepareExport, prepareFonts, reaches, readInput, reportThrown, resolveExportOptions, runExport, runImport, runPreparedExport, saveDeck, stemOf, writeFiles, writePlanned };