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