@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,919 @@
1
+ import { OPFDataImportError, parseTabularData } from './chunk-ZCFSDSUX.js';
2
+ import { applyPatchWithInverse } from './chunk-YZXTXX2D.js';
3
+ import { validate, OPFValidationError } from './chunk-JT3QXA22.js';
4
+ import { isRecord, visitContentPayloads, listVariables, resolveVariables, LOGO_SHAPES, resolveDesignRecords, resolveFontFamilies, resolveCanvasDimensions, imageBackground } from './chunk-MBU42S5Y.js';
5
+ import { embeddedCopy, checkCatalogsOption, catalogReferenceSites, parseReference, catalogGroupDeclared, resolveReference, dottedPath, unresolvedReference, recordReferenceSites, catalogGroupSource, registeredCatalog, catalogKinds, CATALOG_GROUP_PATTERN } from './chunk-SPFV27O5.js';
6
+
7
+ // src/api-errors.ts
8
+ var OPFApiError = class extends Error {
9
+ code;
10
+ details;
11
+ findings;
12
+ constructor(message, code, extra = {}) {
13
+ super(message, extra.cause === void 0 ? void 0 : { cause: extra.cause });
14
+ this.name = new.target.name;
15
+ this.code = code;
16
+ this.details = extra.details ?? {};
17
+ this.findings = extra.findings ?? [];
18
+ }
19
+ };
20
+ var OPFExportError = class extends OPFApiError {
21
+ };
22
+ var OPFImportError = class extends OPFApiError {
23
+ };
24
+ function asApiError(Class, error, fallbackCode) {
25
+ if (error instanceof Class) return error;
26
+ if (error instanceof OPFApiError) return new Class(error.message, error.code, { details: error.details, findings: error.findings, cause: error });
27
+ const failure = error;
28
+ return new Class(failure?.message ?? String(error), typeof failure?.code === "string" ? failure.code : fallbackCode, { cause: error });
29
+ }
30
+
31
+ // src/slide-selection.ts
32
+ var isSlideNumber = (value) => typeof value === "number" && Number.isInteger(value) && value >= 1;
33
+ function parseSlideSelection(selection, total, label = "slides") {
34
+ if (!(total >= 1)) throw new OPFApiError("The presentation has no slides.", "no-slides");
35
+ const listed = typeof selection === "number" ? [selection] : Array.isArray(selection) ? selection : void 0;
36
+ if (listed) {
37
+ const bad = listed.find((value) => !isSlideNumber(value));
38
+ if (bad !== void 0 || listed.length === 0) throw new OPFApiError(`${label} needs slide numbers counted from 1 (got ${JSON.stringify(bad ?? listed)}).`, "invalid-option");
39
+ } else if (typeof selection !== "string") throw new OPFApiError(`${label} needs a slide number, a list of numbers or text like "1,3-5".`, "invalid-option");
40
+ const spec = listed ? listed.join(",") : selection;
41
+ const chosen = /* @__PURE__ */ new Set();
42
+ for (const part of spec.split(",")) {
43
+ const text = part.trim();
44
+ const match = /^(\d*)(-?)(\d*)$/.exec(text);
45
+ if (!text || !match || !match[1] && !match[3]) throw new OPFApiError(`${label} needs numbers like 1,3-5 (got "${spec}").`, "invalid-option");
46
+ const [, from, dash, to] = match;
47
+ const first = from ? Number(from) : 1;
48
+ const last = dash ? to ? Number(to) : total : first;
49
+ if (first < 1 || last < first) throw new OPFApiError(`${label} range "${text}" is not valid (slides count from 1).`, "invalid-option");
50
+ if (last > total) throw new OPFApiError(`${label} ${text} is outside the presentation, which has ${total} slide${total === 1 ? "" : "s"}.`, "invalid-option");
51
+ for (let n = first; n <= last; n++) chosen.add(n);
52
+ }
53
+ return [...chosen].sort((a, b) => a - b);
54
+ }
55
+
56
+ // src/catalog-helpers.ts
57
+ var isRec = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
58
+ var rec = (value) => isRec(value) ? value : {};
59
+ function canonical(value) {
60
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
61
+ if (isRec(value))
62
+ return `{${Object.keys(value).filter((key) => value[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
63
+ return JSON.stringify(value);
64
+ }
65
+ function sameRecord(a, b) {
66
+ return canonical(embeddedCopy(a)) === canonical(embeddedCopy(b));
67
+ }
68
+ function setAt(root, path, value) {
69
+ let node = root;
70
+ for (const part of path.slice(0, -1)) node = node[part];
71
+ node[path[path.length - 1]] = value;
72
+ }
73
+ var kindsOf = (group, kind) => {
74
+ if (!isRec(group[kind])) group[kind] = {};
75
+ return group[kind];
76
+ };
77
+ function embed(document, options = {}) {
78
+ const catalogs = { catalogs: checkCatalogsOption(options.catalogs, "embed") };
79
+ const out = structuredClone(rec(document));
80
+ const added = [];
81
+ const unresolved = [];
82
+ const queue = catalogReferenceSites(out);
83
+ const reported = /* @__PURE__ */ new Set();
84
+ while (queue.length) {
85
+ const site = queue.shift();
86
+ const parsed = parseReference(site.reference);
87
+ if (parsed?.group !== void 0 && !catalogGroupDeclared(out, parsed.group)) continue;
88
+ const found = resolveReference(out, site.kind, site.reference, { ...catalogs, ...site.group !== void 0 ? { group: site.group } : {} });
89
+ if (!found) {
90
+ const path = dottedPath(site.path);
91
+ if (!reported.has(path)) {
92
+ reported.add(path);
93
+ unresolved.push(unresolvedReference(out, site.kind, site.reference, path, catalogs));
94
+ }
95
+ continue;
96
+ }
97
+ if (found.origin === "document") continue;
98
+ if (!isRec(out.catalogs)) out.catalogs = {};
99
+ const groups = out.catalogs;
100
+ if (!isRec(groups[found.group])) groups[found.group] = {};
101
+ const group = groups[found.group];
102
+ if (found.group !== "custom" && typeof group.source !== "string" && found.source !== void 0) {
103
+ const rest = { ...group };
104
+ for (const key of Object.keys(group)) delete group[key];
105
+ Object.assign(group, { source: found.source }, rest);
106
+ }
107
+ const copy = embeddedCopy(found.record);
108
+ kindsOf(group, site.kind)[found.id] = copy;
109
+ added.push({ group: found.group, kind: site.kind, id: found.id, ...found.source !== void 0 ? { source: found.source } : {} });
110
+ queue.push(...recordReferenceSites(site.kind, copy, ["catalogs", found.group, site.kind, found.id], found.group));
111
+ }
112
+ return { document: out, added, unresolved };
113
+ }
114
+ function groupNameFor(source) {
115
+ const base = source.startsWith("pkg:") ? source.slice(4).replace(/^@/, "").split("/").slice(0, 2).join("-") : (() => {
116
+ try {
117
+ return new URL(source).hostname.replace(/^www\./, "");
118
+ } catch {
119
+ return source;
120
+ }
121
+ })();
122
+ const name = base.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
123
+ return /^[a-z]/.test(name) ? name : `catalog-${name || "source"}`;
124
+ }
125
+ function copySlides(from, to, indexes, options = {}) {
126
+ const catalogs = { catalogs: checkCatalogsOption(options.catalogs, "copySlides") };
127
+ const source = rec(from);
128
+ const target = structuredClone(rec(to));
129
+ const sourceSlides = Array.isArray(source.slides) ? source.slides : [];
130
+ const targetSlides = Array.isArray(target.slides) ? target.slides : [];
131
+ for (const index of indexes)
132
+ if (!Number.isInteger(index) || index < 0 || index >= sourceSlides.length) throw new RangeError(`Slide index ${String(index)} is outside the source presentation's ${sourceSlides.length} slides.`);
133
+ const at = options.at ?? targetSlides.length;
134
+ if (!Number.isInteger(at) || at < 0 || at > targetSlides.length) throw new RangeError(`Insertion index ${String(at)} is outside the target presentation's ${targetSlides.length + 1} positions.`);
135
+ const renamed = [];
136
+ const addedGroups = [];
137
+ const added = [];
138
+ const unresolved = [];
139
+ const hadCatalogs = isRec(target.catalogs);
140
+ const groups = () => {
141
+ if (!isRec(target.catalogs)) target.catalogs = {};
142
+ return target.catalogs;
143
+ };
144
+ const memo = /* @__PURE__ */ new Map();
145
+ const targetGroupFor = (catalogSource, sourceGroupName) => {
146
+ for (const [name2, value] of Object.entries(rec(target.catalogs))) if (name2 !== "custom" && isRec(value) && value.source === catalogSource) return name2;
147
+ if (rec(target.catalogs).default !== false && catalogGroupSource(target, "default", catalogs) === catalogSource) return "default";
148
+ let name = sourceGroupName === "default" || sourceGroupName === "custom" ? groupNameFor(catalogSource) : sourceGroupName;
149
+ for (let n = 2; catalogGroupDeclared(target, name) || name === "default" || name === "custom"; n++) name = `${sourceGroupName === "default" ? groupNameFor(catalogSource) : sourceGroupName}-${n}`;
150
+ groups()[name] = { source: catalogSource };
151
+ addedGroups.push({ name, source: catalogSource });
152
+ return name;
153
+ };
154
+ const bareTaken = (kind, id) => resolveReference(target, kind, id, catalogs) !== void 0;
155
+ const addCustom = (kind, wanted, record, renamed2 = false) => {
156
+ const custom = rec(rec(target.catalogs).custom);
157
+ const existing = rec(custom[kind]);
158
+ for (const [id2, value] of Object.entries(existing)) if ((!renamed2 && id2 === wanted || /^-\d+$/.test(id2.slice(wanted.length))) && id2.startsWith(wanted) && sameRecord(value, record)) return { id: id2, reused: true };
159
+ let n = renamed2 ? 2 : 1;
160
+ let id = renamed2 ? `${wanted}-2` : wanted;
161
+ while (Object.hasOwn(existing, id) || bareTaken(kind, id)) id = `${wanted}-${++n}`;
162
+ if (!isRec(groups().custom)) groups().custom = {};
163
+ kindsOf(groups().custom, kind)[id] = record;
164
+ added.push({ group: "custom", kind, id });
165
+ return { id, reused: false };
166
+ };
167
+ const referenceTo = (kind, group, id, holder) => {
168
+ const found = resolveReference(target, kind, id, { ...catalogs, ...holder !== void 0 ? { group: holder } : {} });
169
+ return found && found.group === group && found.id === id ? id : `${group}:${id}`;
170
+ };
171
+ const place = (kind, reference2, path, holder) => {
172
+ const found = resolveReference(source, kind, reference2, { ...catalogs, ...holder !== void 0 ? { group: holder } : {} });
173
+ if (!found) {
174
+ unresolved.push(unresolvedReference(source, kind, reference2, path, catalogs));
175
+ return void 0;
176
+ }
177
+ const key = `${kind}\0${found.group}\0${found.source ?? ""}\0${found.id}`;
178
+ const known = memo.get(key);
179
+ if (known) return known;
180
+ const incoming = embeddedCopy(found.record);
181
+ const catalogSource = found.group === "custom" ? void 0 : found.source;
182
+ const group = catalogSource === void 0 ? "custom" : targetGroupFor(catalogSource, found.group);
183
+ const inner = recordReferenceSites(kind, incoming, []).map((site) => ({ site, placed: place(site.kind, site.reference, `${path} -> ${site.path.join(".")}`, found.group) }));
184
+ const rewrite = (holderGroup) => {
185
+ const copy = structuredClone(incoming);
186
+ for (const { site, placed: placed2 } of inner) if (placed2) setAt(copy, site.path, referenceTo(site.kind, placed2.group, placed2.id, holderGroup));
187
+ return copy;
188
+ };
189
+ let placed;
190
+ if (group === "custom") {
191
+ const result = addCustom(kind, found.id, rewrite("custom"));
192
+ if (result.id !== found.id && !result.reused) renamed.push({ kind, from: reference2, to: result.id, reason: "custom-conflict" });
193
+ placed = { group: "custom", id: result.id };
194
+ } else {
195
+ const record = rewrite(group);
196
+ const embedded = rec(rec(rec(target.catalogs)[group])[kind]);
197
+ const current = Object.hasOwn(embedded, found.id) ? embedded[found.id] : registeredCatalog(catalogGroupSource(target, group, catalogs), catalogs)?.[kind]?.[found.id];
198
+ if (current === void 0 || sameRecord(current, record)) {
199
+ if (!Object.hasOwn(embedded, found.id)) {
200
+ if (!isRec(groups()[group])) groups()[group] = {};
201
+ const holder2 = groups()[group];
202
+ if (typeof holder2.source !== "string" && catalogSource !== void 0) {
203
+ const rest = { ...holder2 };
204
+ for (const key2 of Object.keys(holder2)) delete holder2[key2];
205
+ Object.assign(holder2, { source: catalogSource }, rest);
206
+ }
207
+ kindsOf(holder2, kind)[found.id] = record;
208
+ added.push({ group, kind, id: found.id, ...catalogSource !== void 0 ? { source: catalogSource } : {} });
209
+ }
210
+ placed = { group, id: found.id };
211
+ } else {
212
+ const { id, reused } = addCustom(kind, found.id, rewrite("custom"), true);
213
+ if (!reused) renamed.push({ kind, from: reference2, to: id, reason: "catalog-revision" });
214
+ placed = { group: "custom", id };
215
+ }
216
+ }
217
+ memo.set(key, placed);
218
+ return placed;
219
+ };
220
+ const copied = [];
221
+ for (const index of indexes) {
222
+ const slide = structuredClone(sourceSlides[index]);
223
+ const holder = { slides: [slide] };
224
+ for (const site of catalogReferenceSites(holder, { records: false })) {
225
+ const placed = place(site.kind, site.reference, `slides.${index}.${site.path.slice(2).join(".")}`);
226
+ if (placed) setAt(holder, site.path, referenceTo(site.kind, placed.group, placed.id));
227
+ }
228
+ copied.push(holder.slides[0]);
229
+ }
230
+ targetSlides.splice(at, 0, ...copied);
231
+ target.slides = targetSlides;
232
+ if (!hadCatalogs && isRec(target.catalogs) && Object.keys(target.catalogs).length === 0) delete target.catalogs;
233
+ return { document: target, slides: copied.map((_, offset) => at + offset), renamed, addedGroups, added, unresolved };
234
+ }
235
+ var pointerPart = (part) => part.replaceAll("~", "~0").replaceAll("/", "~1");
236
+ function updateFromCatalog(document, catalogs, refs) {
237
+ const options = { catalogs: checkCatalogsOption(catalogs, "updateFromCatalog") };
238
+ const groups = rec(rec(document).catalogs);
239
+ const wanted = refs?.map((ref) => {
240
+ const parsed = parseReference(ref.reference);
241
+ return parsed ? { kind: ref.kind, group: parsed.group ?? "default", id: parsed.id } : void 0;
242
+ });
243
+ const changes = [];
244
+ for (const [group, value] of Object.entries(groups)) {
245
+ if (group === "custom" || !isRec(value)) continue;
246
+ const catalogSource = catalogGroupSource(document, group, options);
247
+ const registered = registeredCatalog(catalogSource, options);
248
+ if (!registered || catalogSource === void 0) continue;
249
+ for (const kind of catalogKinds)
250
+ for (const [id, embedded] of Object.entries(rec(value[kind]))) {
251
+ if (wanted && !wanted.some((entry) => entry && entry.kind === kind && entry.group === group && entry.id === id)) continue;
252
+ const current = registered[kind]?.[id];
253
+ if (current === void 0 || sameRecord(embedded, current)) continue;
254
+ changes.push({ kind, reference: group === "default" ? id : `${group}:${id}`, group, id, source: catalogSource, embedded: rec(embedded), current: embeddedCopy(current) });
255
+ }
256
+ }
257
+ return {
258
+ changes,
259
+ patch: changes.map((change) => ({ op: "replace", path: `/catalogs/${pointerPart(change.group)}/${change.kind}/${pointerPart(change.id)}`, value: change.current }))
260
+ };
261
+ }
262
+ var OPFMoveToCustomError = class extends Error {
263
+ code;
264
+ constructor(code, message) {
265
+ super(message);
266
+ this.name = "OPFMoveToCustomError";
267
+ this.code = code;
268
+ }
269
+ };
270
+ var pointerOf = (path) => `/${path.map((part) => pointerPart(String(part))).join("/")}`;
271
+ function moveToCustom(document, ref, options = {}) {
272
+ const catalogs = { catalogs: checkCatalogsOption(options.catalogs, "moveToCustom") };
273
+ const parsed = parseReference(ref.reference);
274
+ if (!parsed || !catalogKinds.includes(ref.kind)) throw new OPFMoveToCustomError("invalid-reference", `moveToCustom: ${JSON.stringify(ref.reference)} is not a ${String(ref.kind)} reference (an id or name:id).`);
275
+ const { kind } = ref;
276
+ const fromGroup = parsed.group ?? "default";
277
+ if (fromGroup === "custom") throw new OPFMoveToCustomError("already-custom", `moveToCustom: ${ref.reference} already names a record of catalogs.custom.`);
278
+ const before = rec(document);
279
+ const records = rec(rec(rec(before.catalogs)[fromGroup])[kind]);
280
+ if (!Object.hasOwn(records, parsed.id)) throw new OPFMoveToCustomError("not-embedded", `moveToCustom: catalogs.${fromGroup}.${kind} does not embed ${JSON.stringify(parsed.id)}.`);
281
+ const fromId = parsed.id;
282
+ const record = rec(records[fromId]);
283
+ const sites = catalogReferenceSites(before);
284
+ const targets = sites.map((site) => resolveReference(before, site.kind, site.reference, { ...catalogs, ...site.group !== void 0 ? { group: site.group } : {} }));
285
+ const isMoved = (found) => found?.origin === "document" && found.kind === kind && found.group === fromGroup && found.id === fromId;
286
+ const inMoved = (site) => site.path.length >= 4 && site.path[0] === "catalogs" && site.path[1] === fromGroup && site.path[2] === kind && site.path[3] === fromId;
287
+ const after = structuredClone(before);
288
+ const groups = after.catalogs;
289
+ const custom = rec(groups.custom);
290
+ const existing = rec(custom[kind]);
291
+ const moved = structuredClone(record);
292
+ const ownSites = sites.map((site, index) => ({ site, found: targets[index] })).filter(({ site }) => inMoved(site));
293
+ if (options.id !== void 0 && (typeof options.id !== "string" || !CATALOG_GROUP_PATTERN.test(options.id))) throw new OPFMoveToCustomError("invalid-id", `moveToCustom: the new id ${JSON.stringify(options.id)} is not a lowercase kebab-case id.`);
294
+ const wanted = options.id ?? fromId;
295
+ let toId = wanted;
296
+ for (let n = 2; Object.hasOwn(existing, toId) && !sameRecord(existing[toId], moved); n++) toId = `${wanted}-${n}`;
297
+ const reused = Object.hasOwn(existing, toId);
298
+ const patch = [];
299
+ const fromRecords = groups[fromGroup][kind];
300
+ delete fromRecords[fromId];
301
+ patch.push({ op: "remove", path: pointerOf(["catalogs", fromGroup, kind, fromId]) });
302
+ if (Object.keys(fromRecords).length === 0) {
303
+ delete groups[fromGroup][kind];
304
+ patch.push({ op: "remove", path: pointerOf(["catalogs", fromGroup, kind]) });
305
+ }
306
+ if (!isRec(groups.custom)) {
307
+ groups.custom = {};
308
+ patch.push({ op: "add", path: pointerOf(["catalogs", "custom"]), value: {} });
309
+ }
310
+ if (!isRec(groups.custom[kind])) {
311
+ groups.custom[kind] = {};
312
+ patch.push({ op: "add", path: pointerOf(["catalogs", "custom", kind]), value: {} });
313
+ }
314
+ const referenceTo = (targetKind, group, id, holder) => {
315
+ const found = resolveReference(after, targetKind, id, { ...catalogs, ...holder !== void 0 ? { group: holder } : {} });
316
+ return found && found.group === group && found.id === id ? id : `${group}:${id}`;
317
+ };
318
+ if (!reused) {
319
+ groups.custom[kind][toId] = moved;
320
+ for (const { site, found } of ownSites) {
321
+ if (!found) continue;
322
+ const target = isMoved(found) ? { group: "custom", id: toId } : { group: found.group, id: found.id };
323
+ setAt(moved, site.path.slice(4), referenceTo(site.kind, target.group, target.id, "custom"));
324
+ }
325
+ patch.push({ op: "add", path: pointerOf(["catalogs", "custom", kind, toId]), value: structuredClone(moved) });
326
+ }
327
+ const references = [];
328
+ sites.forEach((site, index) => {
329
+ if (inMoved(site) || !isMoved(targets[index])) return;
330
+ const written = referenceTo(kind, "custom", toId, site.group);
331
+ if (written === site.reference) return;
332
+ setAt(after, site.path, written);
333
+ references.push(pointerOf(site.path));
334
+ patch.push({ op: "replace", path: pointerOf(site.path), value: written });
335
+ });
336
+ return {
337
+ document: after,
338
+ from: { kind, group: fromGroup, id: fromId, reference: fromGroup === "default" ? fromId : `${fromGroup}:${fromId}` },
339
+ to: { kind, group: "custom", id: toId, reference: toId },
340
+ references,
341
+ patch,
342
+ ...toId !== wanted ? { renamed: { from: wanted, to: toId } } : {}
343
+ };
344
+ }
345
+
346
+ // src/stats.ts
347
+ var DEFAULT_WORDS_PER_MINUTE = 130;
348
+ var KIND_ORDER = ["text", "items", "bullets", "quote", "metric", "code", "timeline", "chart", "table", "image", "video"];
349
+ var FURNITURE_FIELDS = ["image", "text", "socials", "date"];
350
+ var ZONES = ["left", "center", "right"];
351
+ var WORD = /[\p{L}\p{N}\p{M}]+(?:['’.,-][\p{L}\p{N}\p{M}]+)*/gu;
352
+ var UNSPACED = /[฀-໿က-႟ក-៿぀-ヿ㐀-䶿一-鿿豈-﫿]/gu;
353
+ var wordCount = (text) => {
354
+ let extra = 0;
355
+ const spaced = text.replace(UNSPACED, () => {
356
+ extra++;
357
+ return " ";
358
+ });
359
+ return (spaced.match(WORD)?.length ?? 0) + extra;
360
+ };
361
+ var sorted = (values) => [...new Set(values)].sort();
362
+ var sortedCounts = (counts) => Object.fromEntries([...counts].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
363
+ var bump = (counts, key) => counts.set(key, (counts.get(key) ?? 0) + 1);
364
+ var str = (value) => typeof value === "string" ? value : null;
365
+ var decimals = (value, places) => Math.round(value * 10 ** places) / 10 ** places;
366
+ var reference = (value) => {
367
+ if (typeof value === "string") return { id: value, inline: false };
368
+ if (isRecord(value)) return { id: str(value.id), inline: true };
369
+ return null;
370
+ };
371
+ var list = (value) => Array.isArray(value) ? value : value === void 0 ? [] : [value];
372
+ var assetForm = (src) => /^asset:/i.test(src) ? "reference" : /^data:/i.test(src) ? "embedded" : /^(https?:)?\/\//i.test(src) || /^pkg:/i.test(src) ? "remote" : "file";
373
+ var embeddedBytes = (src) => {
374
+ const comma = src.indexOf(",");
375
+ if (comma < 0) return 0;
376
+ const payload = src.slice(comma + 1);
377
+ if (!/;base64$/i.test(src.slice(0, comma))) return payload.length;
378
+ const padding = payload.endsWith("==") ? 2 : payload.endsWith("=") ? 1 : 0;
379
+ return Math.max(0, Math.floor(payload.length * 3 / 4) - padding);
380
+ };
381
+ var srcOf = (asset) => typeof asset === "string" ? asset : isRecord(asset) && typeof asset.src === "string" ? asset.src : void 0;
382
+ function slideSize(deck, catalogs) {
383
+ const design = isRecord(deck.design) ? deck.design : {};
384
+ const records = resolveDesignRecords(deck, void 0, catalogs);
385
+ const theme2 = records.theme;
386
+ const themed = typeof design.theme === "string" && !records.diagnostics.some((diagnostic) => diagnostic.kind === "themes");
387
+ const source = design.dimensions !== void 0 ? "design" : themed && theme2.dimensions !== void 0 ? "theme" : "default";
388
+ const dimensions = design.dimensions ?? theme2.dimensions;
389
+ let width = 40 / 3, height = 7.5;
390
+ try {
391
+ const resolved = resolveCanvasDimensions(dimensions);
392
+ width = resolved.width / 96;
393
+ height = resolved.height / 96;
394
+ } catch {
395
+ }
396
+ const preset = typeof dimensions === "string" ? dimensions : isRecord(dimensions) && typeof dimensions.preset === "string" && dimensions.widthInches === void 0 && dimensions.heightInches === void 0 ? dimensions.preset : null;
397
+ return { preset, widthInches: decimals(width, 4), heightInches: decimals(height, 4), aspectRatio: decimals(width / height, 3), source };
398
+ }
399
+ var furniture = (value) => {
400
+ const zones = [], fields = /* @__PURE__ */ new Set();
401
+ if (isRecord(value))
402
+ for (const zone of ZONES) {
403
+ const item = value[zone];
404
+ if (!isRecord(item)) continue;
405
+ const present = FURNITURE_FIELDS.filter((field) => item[field] !== void 0 && item[field] !== false);
406
+ if (present.length) {
407
+ zones.push(zone);
408
+ for (const field of present) fields.add(field);
409
+ }
410
+ }
411
+ return { configured: isRecord(value), suppressed: value === false, zones, fields: FURNITURE_FIELDS.filter((field) => fields.has(field)) };
412
+ };
413
+ function visitRuns(value, visit) {
414
+ if (typeof value === "string") visit({ text: value });
415
+ else if (typeof value === "number") visit({ text: String(value) });
416
+ else if (Array.isArray(value)) for (const entry of value) visitRuns(entry, visit);
417
+ else if (isRecord(value)) {
418
+ if (typeof value.text === "string") {
419
+ visit(value);
420
+ if (value.description !== void 0) visitRuns(value.description, visit);
421
+ } else if (Array.isArray(value.text)) {
422
+ visitRuns(value.text, visit);
423
+ if (value.description !== void 0) visitRuns(value.description, visit);
424
+ } else if (value.value !== void 0) visitRuns(value.value, visit);
425
+ else if (typeof value.name === "string") visit({ text: value.name });
426
+ }
427
+ }
428
+ function stats(presentation2, options = {}) {
429
+ const catalogs = { catalogs: checkCatalogsOption(options.catalogs, "stats") };
430
+ if (!isRecord(presentation2)) throw new TypeError("stats needs a presentation object.");
431
+ const deck = presentation2;
432
+ const wordsPerMinute = options.wordsPerMinute ?? DEFAULT_WORDS_PER_MINUTE;
433
+ if (!Number.isFinite(wordsPerMinute) || wordsPerMinute <= 0) throw new RangeError("wordsPerMinute must be a positive number.");
434
+ const slides = Array.isArray(deck.slides) ? deck.slides.filter(isRecord) : [];
435
+ const design = isRecord(deck.design) ? deck.design : {};
436
+ const registry = isRecord(deck.assets) ? deck.assets : {};
437
+ const uses = { total: 0, references: 0, embedded: 0, files: 0, remote: 0 };
438
+ let embeddedTotal = 0;
439
+ const useAsset = (asset) => {
440
+ const src = srcOf(asset);
441
+ if (src === void 0) return;
442
+ const form = assetForm(src);
443
+ uses.total++;
444
+ uses[form === "reference" ? "references" : form === "embedded" ? "embedded" : form === "remote" ? "remote" : "files"]++;
445
+ if (form === "embedded") embeddedTotal += embeddedBytes(src);
446
+ };
447
+ const registryFacts = { entries: 0, embedded: 0, files: 0, remote: 0 };
448
+ for (const entry of Object.values(registry)) {
449
+ const src = srcOf(entry);
450
+ if (src === void 0) continue;
451
+ registryFacts.entries++;
452
+ const form = assetForm(src);
453
+ if (form === "embedded") {
454
+ registryFacts.embedded++;
455
+ embeddedTotal += embeddedBytes(src);
456
+ } else if (form === "remote") registryFacts.remote++;
457
+ else if (form === "file") registryFacts.files++;
458
+ }
459
+ const altOf = (asset) => {
460
+ if (isRecord(asset) && typeof asset.alt === "string") return asset.alt;
461
+ const src = srcOf(asset);
462
+ if (src?.startsWith("asset:")) {
463
+ const entry = registry[src.slice("asset:".length)];
464
+ if (isRecord(entry) && typeof entry.alt === "string") return entry.alt;
465
+ }
466
+ return void 0;
467
+ };
468
+ const images = { content: { total: 0, withAlt: 0, decorative: 0, missingAlt: 0 }, logos: 0, watermarks: { deck: false, slides: 0 }, backgrounds: { deck: false, slides: 0 }, headerFooter: 0, speakerPhotos: 0, videos: 0 };
469
+ const contentImage = (asset) => {
470
+ if (srcOf(asset) === void 0) return 0;
471
+ if (!String(srcOf(asset)).startsWith("var:organization.")) useAsset(asset);
472
+ images.content.total++;
473
+ const alt = altOf(asset);
474
+ if (alt === void 0 || alt !== "" && alt.trim() === "") images.content.missingAlt++;
475
+ else if (alt === "") images.content.decorative++;
476
+ else images.content.withAlt++;
477
+ return 1;
478
+ };
479
+ const logoAssets = (value) => {
480
+ if (srcOf(value) !== void 0) return [value];
481
+ if (!isRecord(value)) return [];
482
+ return LOGO_SHAPES.flatMap((shape) => {
483
+ const entry = value[shape];
484
+ if (srcOf(entry) !== void 0) return [entry];
485
+ return isRecord(entry) ? [entry.onLight, entry.onDark].filter((tone2) => srcOf(tone2) !== void 0) : [];
486
+ });
487
+ };
488
+ const watermarkOf = (value) => {
489
+ if (value === void 0 || value === false) return false;
490
+ if (srcOf(value) !== void 0) useAsset(value);
491
+ return srcOf(value) !== void 0;
492
+ };
493
+ const backgroundImage = (value) => {
494
+ const picture = imageBackground(value);
495
+ if (!picture) return false;
496
+ useAsset(picture.src);
497
+ return true;
498
+ };
499
+ const furnitureImages = (value) => {
500
+ if (!isRecord(value)) return;
501
+ for (const zone of ZONES) {
502
+ const item = value[zone];
503
+ if (!isRecord(item) || srcOf(item.image) === void 0) continue;
504
+ if (!String(srcOf(item.image)).startsWith("var:organization.")) useAsset(item.image);
505
+ images.headerFooter++;
506
+ }
507
+ };
508
+ const designAssets = (value, scope) => {
509
+ if (!isRecord(value)) return;
510
+ if (watermarkOf(value.watermark)) {
511
+ if (scope === "deck") images.watermarks.deck = true;
512
+ else images.watermarks.slides++;
513
+ }
514
+ if (backgroundImage(value.background)) {
515
+ if (scope === "deck") images.backgrounds.deck = true;
516
+ else images.backgrounds.slides++;
517
+ }
518
+ furnitureImages(value.header);
519
+ furnitureImages(value.footer);
520
+ };
521
+ const organizations = list(deck.organization).filter(isRecord).map((entry) => {
522
+ const logos = logoAssets(entry.logo);
523
+ for (const logo of logos) {
524
+ useAsset(logo);
525
+ images.logos++;
526
+ }
527
+ const hasLogo = logos.length > 0;
528
+ return { id: str(entry.id) ?? "", name: str(entry.name) ?? "", role: str(entry.role), hasLogo };
529
+ });
530
+ const speakers = list(deck.speaker).filter(isRecord).map((entry) => {
531
+ const hasPhoto = srcOf(entry.photo) !== void 0;
532
+ if (hasPhoto) {
533
+ useAsset(entry.photo);
534
+ images.speakerPhotos++;
535
+ }
536
+ return { id: str(entry.id) ?? "", name: str(entry.name) ?? "", title: str(entry.title), organizationId: str(entry.organizationId), hasPhoto };
537
+ });
538
+ designAssets(design, "deck");
539
+ for (const variable of Object.values(isRecord(deck.variables) ? deck.variables : {})) if (isRecord(variable) && variable.type === "image") useAsset(variable.value);
540
+ const narrative2 = reference(deck.narrative);
541
+ const deckFacts = {
542
+ name: str(deck.name),
543
+ description: typeof deck.description === "string" && deck.description !== "",
544
+ filename: str(deck.filename),
545
+ author: list(deck.author).filter((entry) => typeof entry === "string"),
546
+ language: reference(deck.language),
547
+ slideSize: slideSize(deck, catalogs),
548
+ template: deck.template === true,
549
+ tags: Array.isArray(deck.tags) ? deck.tags.filter((tag) => typeof tag === "string") : [],
550
+ takeaways: list(deck.takeaway).filter((entry) => typeof entry === "string").length,
551
+ declaredMinutes: typeof deck.duration === "number" ? deck.duration : null,
552
+ design: { theme: reference(design.theme), colorScheme: reference(design.colorScheme), fontScheme: reference(design.fontScheme) },
553
+ audience: list(deck.audience).flatMap((entry) => reference(entry) ?? []),
554
+ purpose: reference(deck.purpose),
555
+ tone: reference(deck.tone),
556
+ narrative: narrative2 ? { ...narrative2, beats: isRecord(deck.narrative) && Array.isArray(deck.narrative.beats) ? deck.narrative.beats.length : 0 } : null
557
+ };
558
+ const datasetRecords = isRecord(deck.datasets) ? Object.entries(deck.datasets).filter(([, value]) => isRecord(value)) : [];
559
+ const datasetUse = /* @__PURE__ */ new Map();
560
+ const payloadCounts = { text: 0, items: 0, bullets: 0, quote: 0, metric: 0, code: 0, timeline: 0, chart: 0, table: 0, image: 0, video: 0, blocks: 0, regions: 0, groups: 0, maxDepth: 0 };
561
+ const layouts = /* @__PURE__ */ new Map(), chartTypes = /* @__PURE__ */ new Map(), extensions = /* @__PURE__ */ new Set();
562
+ const chartData = { inline: 0, dataset: 0, source: 0 };
563
+ const tableItems = [];
564
+ let tableRows = 0, tableColumns = 0, datasetBackedTables = 0, chartTotal = 0, captions = 0;
565
+ const hidden = [], withoutNotes = [], sections = [];
566
+ let withTitle = 0, withBeat = 0, unsectioned = 0, withoutLayout = 0, withOwnDesign = 0, withNotes = 0, contentWords = 0, notesWords = 0;
567
+ const headerOverrides = { header: 0, headerSuppressed: 0, footer: 0, footerSuppressed: 0 };
568
+ const total = { citations: 0, footnotes: 0, links: 0, cited: /* @__PURE__ */ new Set(), families: /* @__PURE__ */ new Set() };
569
+ const perSlide = [];
570
+ const contentPerSlide = [];
571
+ const schemeDesigns = [void 0];
572
+ slides.forEach((slide, index) => {
573
+ const id = str(slide.id);
574
+ const ref = { index, id };
575
+ const slideDesign = isRecord(slide.design) ? slide.design : void 0;
576
+ const slideTally = { words: 0, citations: 0, footnotes: 0, links: 0, cited: /* @__PURE__ */ new Set(), families: /* @__PURE__ */ new Set() };
577
+ const kinds = /* @__PURE__ */ new Set();
578
+ let slideImages = 0, slideCharts = 0, slideTables = 0;
579
+ const count = (text) => {
580
+ slideTally.words += wordCount(text);
581
+ };
582
+ const run = (value) => visitRuns(value, (entry) => {
583
+ count(entry.text);
584
+ if (typeof entry.link === "string" && entry.link !== "") slideTally.links++;
585
+ if (entry.cite !== void 0) {
586
+ slideTally.citations++;
587
+ for (const cite of list(entry.cite)) if (typeof cite === "string") slideTally.cited.add(cite);
588
+ }
589
+ if (entry.footnote !== void 0) slideTally.footnotes++;
590
+ if (typeof entry.fontFamily === "string" && entry.fontFamily !== "") slideTally.families.add(entry.fontFamily);
591
+ });
592
+ const caption = (node) => {
593
+ if (node.caption !== void 0) {
594
+ captions++;
595
+ run(node.caption);
596
+ }
597
+ };
598
+ const visitNode = (node, path, depth, kind) => {
599
+ if (kind === "block") payloadCounts.blocks++;
600
+ if (kind === "region") payloadCounts.regions++;
601
+ if (kind !== "root" && Array.isArray(node.blocks)) payloadCounts.groups++;
602
+ if (depth > payloadCounts.maxDepth) payloadCounts.maxDepth = depth;
603
+ for (const key of KIND_ORDER) if (node[key] !== void 0) {
604
+ payloadCounts[key]++;
605
+ kinds.add(key);
606
+ }
607
+ if (isRecord(node.extensions)) for (const key of Object.keys(node.extensions)) extensions.add(key);
608
+ run(node.text);
609
+ run(node.items);
610
+ run(node.bullets);
611
+ caption(node);
612
+ if (node.quote !== void 0) {
613
+ const quote = node.quote;
614
+ if (isRecord(quote)) {
615
+ run(quote.text);
616
+ run(quote.attribution);
617
+ run(quote.source);
618
+ } else run(quote);
619
+ }
620
+ if (isRecord(node.metric)) {
621
+ run(node.metric.value);
622
+ run(node.metric.label);
623
+ run(node.metric.description);
624
+ run(node.metric.unit);
625
+ run(node.metric.delta);
626
+ }
627
+ if (node.timeline !== void 0) {
628
+ const timeline = node.timeline, events = Array.isArray(timeline) ? timeline : isRecord(timeline) && Array.isArray(timeline.events) ? timeline.events : [];
629
+ if (isRecord(timeline)) {
630
+ run(timeline.name);
631
+ run(timeline.description);
632
+ }
633
+ for (const event of events) if (isRecord(event)) {
634
+ run(event.when);
635
+ run(event.what);
636
+ run(event.description);
637
+ }
638
+ }
639
+ if (node.image !== void 0) slideImages += contentImage(node.image);
640
+ if (node.video !== void 0) {
641
+ useAsset(node.video);
642
+ images.videos++;
643
+ }
644
+ if (isRecord(node.chart)) {
645
+ chartTotal++;
646
+ slideCharts++;
647
+ bump(chartTypes, str(node.chart.type) ?? "unknown");
648
+ const data = node.chart.data;
649
+ if (isRecord(data)) {
650
+ if (typeof data.dataset === "string") {
651
+ chartData.dataset++;
652
+ bump(datasetUse, data.dataset);
653
+ } else if (Array.isArray(data.columns) || Array.isArray(data.rows)) chartData.inline++;
654
+ else if (typeof data.src === "string") {
655
+ chartData.source++;
656
+ useAsset(data.src);
657
+ }
658
+ }
659
+ }
660
+ if (isRecord(node.table)) {
661
+ slideTables++;
662
+ const table = node.table;
663
+ let rows = 0, columns = 0, dataset = null;
664
+ if (typeof table.dataset === "string") {
665
+ dataset = table.dataset;
666
+ datasetBackedTables++;
667
+ bump(datasetUse, dataset);
668
+ const source = isRecord(deck.datasets) && isRecord(deck.datasets[dataset]) ? deck.datasets[dataset] : void 0;
669
+ rows = Array.isArray(source?.rows) ? source.rows.length : 0;
670
+ columns = Array.isArray(table.fields) ? table.fields.length : Array.isArray(source?.columns) ? source.columns.length : 0;
671
+ } else {
672
+ const body = Array.isArray(table.rows) ? table.rows : [];
673
+ rows = body.length;
674
+ columns = Array.isArray(table.columns) ? table.columns.length : body.reduce((widest, row) => Math.max(widest, Array.isArray(row) ? row.length : 0), 0);
675
+ run(table.columns);
676
+ for (const row of body) run(row);
677
+ }
678
+ tableRows += rows;
679
+ tableColumns += columns;
680
+ tableItems.push({ slide: index, path: `${path}/table`, rows, columns, dataset });
681
+ }
682
+ };
683
+ run(slide.title);
684
+ run(slide.subtitle);
685
+ run(slide.tag);
686
+ const slidePath = `/slides/${index}`;
687
+ visitNode(slide, slidePath, 0, "root");
688
+ visitContentPayloads(slide, slidePath, (node, path) => {
689
+ const relative = path.slice(slidePath.length + 1);
690
+ visitNode(node, path, (relative.match(/(^|\/)blocks\//g) ?? []).length, /(^|\/)blocks\/\d+$/.test(relative) ? "block" : "region");
691
+ });
692
+ const notes = typeof slide.notes === "string" ? slide.notes : "";
693
+ const noteWords = wordCount(notes);
694
+ const hasNotes = notes.trim() !== "";
695
+ if (hasNotes) withNotes++;
696
+ else withoutNotes.push(ref);
697
+ notesWords += noteWords;
698
+ contentWords += slideTally.words;
699
+ contentPerSlide.push(slideTally.words);
700
+ total.citations += slideTally.citations;
701
+ total.footnotes += slideTally.footnotes;
702
+ total.links += slideTally.links;
703
+ for (const cite of slideTally.cited) total.cited.add(cite);
704
+ for (const family of slideTally.families) total.families.add(family);
705
+ const title = typeof slide.title === "string" && slide.title.trim() !== "" ? slide.title : null;
706
+ if (title) withTitle++;
707
+ if (slide.beat !== void 0) withBeat++;
708
+ if (slide.hidden === true) hidden.push(ref);
709
+ const layout2 = str(slide.layout);
710
+ if (layout2) bump(layouts, layout2);
711
+ else withoutLayout++;
712
+ const section = str(slide.section);
713
+ if (section !== null && section !== "") {
714
+ const last = sections.at(-1);
715
+ if (last && last.name === section && last.firstSlide + last.slides === index) last.slides++;
716
+ else sections.push({ name: section, slides: 1, firstSlide: index });
717
+ } else unsectioned++;
718
+ if (slideDesign) {
719
+ withOwnDesign++;
720
+ designAssets(slideDesign, "slide");
721
+ for (const kind of ["header", "footer"]) {
722
+ if (slideDesign[kind] === false) headerOverrides[`${kind}Suppressed`]++;
723
+ else if (isRecord(slideDesign[kind])) headerOverrides[kind]++;
724
+ }
725
+ if (slideDesign.fontScheme !== void 0 || slideDesign.theme !== void 0 && design.fontScheme === void 0) schemeDesigns.push(index);
726
+ }
727
+ if (options.perSlide)
728
+ perSlide.push({
729
+ index,
730
+ id,
731
+ title,
732
+ layout: layout2,
733
+ section: section === "" ? null : section,
734
+ hidden: slide.hidden === true,
735
+ payloads: KIND_ORDER.filter((kind) => kinds.has(kind)),
736
+ words: { content: slideTally.words, notes: noteWords },
737
+ hasNotes,
738
+ speakingSeconds: Math.round(noteWords / wordsPerMinute * 60),
739
+ images: slideImages,
740
+ charts: slideCharts,
741
+ tables: slideTables,
742
+ citations: slideTally.citations
743
+ });
744
+ });
745
+ if (isRecord(deck.extensions)) for (const key of Object.keys(deck.extensions)) extensions.add(key);
746
+ const datasetItems = datasetRecords.map(([id, value]) => ({
747
+ id,
748
+ title: str(value.title),
749
+ columns: Array.isArray(value.columns) ? value.columns.length : 0,
750
+ rows: Array.isArray(value.rows) ? value.rows.length : 0,
751
+ source: isRecord(value.source) ? str(value.source.src) : null,
752
+ referencedBy: datasetUse.get(id) ?? 0
753
+ }));
754
+ const references = Array.isArray(deck.references) ? deck.references.filter(isRecord) : [];
755
+ const referenceIds = references.map((entry) => str(entry.id)).filter((entry) => entry !== null);
756
+ const cited = referenceIds.filter((entry) => total.cited.has(entry));
757
+ const variables = listVariables(deck, isRecord(options.values) ? options.values : {});
758
+ const byKind = /* @__PURE__ */ new Map();
759
+ for (const variable of variables) bump(byKind, variable.kind);
760
+ const variableFacts = {
761
+ declared: variables.length,
762
+ byKind: sortedCounts(byKind),
763
+ required: variables.filter((variable) => variable.required).length,
764
+ optional: variables.filter((variable) => !variable.required).length,
765
+ filled: variables.filter((variable) => variable.filled).length,
766
+ unfilled: variables.filter((variable) => !variable.filled).map((variable) => variable.id),
767
+ unfilledRequired: variables.filter((variable) => !variable.filled && variable.required).map((variable) => variable.id),
768
+ unused: variables.filter((variable) => variable.uses.length === 0).map((variable) => variable.id)
769
+ };
770
+ const families = new Set(total.families), schemeIds = /* @__PURE__ */ new Set(), unresolved = /* @__PURE__ */ new Set();
771
+ const addScheme = (slideIndex) => {
772
+ const resolved = resolveDesignRecords(deck, slideIndex, catalogs);
773
+ for (const diagnostic of resolved.diagnostics) if (diagnostic.kind === "fontSchemes") unresolved.add(diagnostic.reference);
774
+ if (resolved.fontSchemeReference !== void 0) schemeIds.add(resolved.fontSchemeReference);
775
+ const scheme = resolved.fontScheme;
776
+ const named = resolveFontFamilies(scheme);
777
+ families.add(named.heading);
778
+ families.add(named.body);
779
+ if (scheme.code !== void 0) families.add(named.code);
780
+ if (named.accent) families.add(named.accent);
781
+ };
782
+ for (const entry of schemeDesigns) addScheme(entry);
783
+ const catalogGroups = {};
784
+ if (isRecord(deck.catalogs))
785
+ for (const group of Object.keys(deck.catalogs).sort()) {
786
+ const entry = deck.catalogs[group];
787
+ if (!isRecord(entry)) continue;
788
+ const records = {};
789
+ for (const kind of catalogKinds) if (isRecord(entry[kind])) records[kind] = Object.keys(entry[kind]).length;
790
+ catalogGroups[group] = { source: str(entry.source), records };
791
+ }
792
+ const colorVariables = variables.filter((variable) => variable.kind === "color").map((variable) => ({ id: variable.id, value: typeof variable.value === "string" ? variable.value : null, uses: variable.uses.length }));
793
+ const contentMean = slides.length ? decimals(contentWords / slides.length, 1) : 0;
794
+ const result = {
795
+ deck: deckFacts,
796
+ people: { organizations, speakers },
797
+ slides: {
798
+ total: slides.length,
799
+ hidden,
800
+ withTitle,
801
+ withoutTitle: slides.length - withTitle,
802
+ withBeat,
803
+ sections,
804
+ unsectioned,
805
+ layouts: sortedCounts(layouts),
806
+ withoutLayout,
807
+ withOwnDesign
808
+ },
809
+ payloads: payloadCounts,
810
+ words: {
811
+ content: contentWords,
812
+ notes: notesWords,
813
+ total: contentWords + notesWords,
814
+ perSlide: { min: contentPerSlide.length ? Math.min(...contentPerSlide) : 0, max: contentPerSlide.length ? Math.max(...contentPerSlide) : 0, mean: contentMean }
815
+ },
816
+ notes: { withNotes, withoutNotes },
817
+ speakingTime: { basis: "estimate", wordsPerMinute, notesWords, minutes: decimals(notesWords / wordsPerMinute, 1), declaredMinutes: deckFacts.declaredMinutes },
818
+ images,
819
+ charts: { total: chartTotal, byType: sortedCounts(chartTypes), data: chartData },
820
+ tables: { total: tableItems.length, datasetBacked: datasetBackedTables, rows: tableRows, columns: tableColumns, items: tableItems },
821
+ datasets: { count: datasetItems.length, withSource: datasetItems.filter((item) => item.source !== null).length, items: datasetItems },
822
+ citations: { references: references.length, cited, citations: total.citations, footnotes: total.footnotes, captions, links: total.links },
823
+ variables: variableFacts,
824
+ assets: { registry: registryFacts, uses, embeddedBytes: embeddedTotal },
825
+ headerFooter: {
826
+ header: furniture(design.header),
827
+ footer: furniture(design.footer),
828
+ slides: { headerOverrides: headerOverrides.header, headerSuppressed: headerOverrides.headerSuppressed, footerOverrides: headerOverrides.footer, footerSuppressed: headerOverrides.footerSuppressed }
829
+ },
830
+ fonts: { families: sorted(families), schemeIds: sorted(schemeIds), unresolvedSchemeIds: sorted(unresolved), runOverrides: sorted(total.families) },
831
+ colors: { variables: colorVariables },
832
+ extensions: sorted(extensions),
833
+ catalogs: catalogGroups
834
+ };
835
+ if (options.perSlide) result.perSlide = perSlide;
836
+ return result;
837
+ }
838
+
839
+ // src/edit.ts
840
+ function edit(deck, patch, options = {}) {
841
+ const result = applyPatchWithInverse(deck, patch);
842
+ const presentation2 = result.presentation;
843
+ if (options.validate === false) return { presentation: presentation2, findings: [], inverse: result.inverse };
844
+ const report = validate(presentation2, { only: ["format", "references"], ...options.catalogs ? { catalogs: options.catalogs } : {} });
845
+ if (!report.valid) throw new OPFValidationError(report);
846
+ return { presentation: presentation2, findings: report.findings, inverse: result.inverse };
847
+ }
848
+
849
+ // src/fill.ts
850
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
851
+ function fillRecords(raw, options = {}) {
852
+ const format2 = options.format ?? (/^[\s\uFEFF]*[[{]/.test(raw) ? "json" : "csv");
853
+ if (format2 === "json") {
854
+ let value;
855
+ try {
856
+ value = JSON.parse(raw.replace(/^\uFEFF/, ""));
857
+ } catch {
858
+ throw new OPFDataImportError("Invalid JSON data.");
859
+ }
860
+ if (isRecord2(value) && !("columns" in value && "rows" in value)) return [value];
861
+ if (Array.isArray(value) && value.length && value.every(isRecord2)) return value;
862
+ if (Array.isArray(value) && !value.length) throw new OPFDataImportError("Data contains no records.");
863
+ }
864
+ const table = parseTabularData(raw, { format: format2, header: options.header, delimiter: options.delimiter });
865
+ if (!table.rows.length) throw new OPFDataImportError("Data contains no records.");
866
+ return table.rows.map((row) => Object.fromEntries(table.columns.map((column, index) => [column, row[index] === "" ? null : row[index]])));
867
+ }
868
+ function fill(template, data, options = {}) {
869
+ if (!isRecord2(template)) throw new TypeError("fill expects a template object.");
870
+ const records = data === void 0 ? [{}] : typeof data === "string" ? fillRecords(data, options) : Array.isArray(data) ? [...data] : [data];
871
+ if (!records.length) throw new OPFDataImportError("Data contains no records.");
872
+ if (!records.every(isRecord2)) throw new OPFDataImportError("Each record must be an object keyed by variable id.");
873
+ const decks = records.map((record, offset) => ({ index: offset + 1, record, ...resolveVariables(template, record, { template: false, partial: options.partial === true, examples: options.examples === true }) }));
874
+ return {
875
+ decks: decks.map(({ index, record, presentation: presentation2, diagnostics, unfilled, complete }) => ({ index, record, presentation: presentation2, diagnostics, unfilled, complete })),
876
+ ...options.combine ? { presentation: combine(decks) } : {},
877
+ complete: decks.every((deck) => deck.complete),
878
+ unfilled: [...new Set(decks.flatMap((deck) => deck.unfilled))],
879
+ diagnostics: summarize(decks)
880
+ };
881
+ }
882
+ function combine(decks) {
883
+ const first = decks[0];
884
+ const seen = /* @__PURE__ */ new Set();
885
+ const slides = [];
886
+ const assets = {};
887
+ for (const deck of decks) {
888
+ for (const slide of Array.isArray(deck.presentation.slides) ? deck.presentation.slides : []) {
889
+ if (isRecord2(slide) && typeof slide.id === "string") {
890
+ let id = slide.id;
891
+ if (seen.has(id)) {
892
+ id = `${slide.id}-${deck.index}`;
893
+ for (let suffix = 2; seen.has(id); suffix++) id = `${slide.id}-${deck.index}-${suffix}`;
894
+ }
895
+ seen.add(id);
896
+ slides.push(id === slide.id ? slide : { ...slide, id });
897
+ } else slides.push(slide);
898
+ }
899
+ if (isRecord2(deck.presentation.assets)) {
900
+ for (const [key, value] of Object.entries(deck.presentation.assets)) if (!(key in assets)) assets[key] = value;
901
+ }
902
+ }
903
+ return { ...first.presentation, ...Object.keys(assets).length ? { assets } : {}, slides };
904
+ }
905
+ function summarize(decks) {
906
+ const merged = /* @__PURE__ */ new Map();
907
+ for (const deck of decks) {
908
+ for (const diagnostic of deck.diagnostics) {
909
+ if (diagnostic.severity === "info") continue;
910
+ const key = `${diagnostic.code}\0${diagnostic.id}\0${diagnostic.path}`;
911
+ const entry = merged.get(key);
912
+ if (entry) entry.records.push(deck.index);
913
+ else merged.set(key, { ...diagnostic, records: [deck.index] });
914
+ }
915
+ }
916
+ return [...merged.values()];
917
+ }
918
+
919
+ export { OPFApiError, OPFExportError, OPFImportError, OPFMoveToCustomError, asApiError, copySlides, edit, embed, fill, fillRecords, moveToCustom, parseSlideSelection, sameRecord, stats, updateFromCatalog };