@openpresentation/opf 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/dist/catalogs-x1oFFQnd.d.ts +1198 -0
  4. package/dist/catalogs.d.ts +2 -32413
  5. package/dist/catalogs.js +1 -18505
  6. package/dist/chunk-75N5I6M4.js +229 -0
  7. package/dist/chunk-EMTMQRX4.js +1007 -0
  8. package/dist/chunk-GCXOU5MZ.js +6515 -0
  9. package/dist/chunk-HJNJHB5H.js +454 -0
  10. package/dist/chunk-OZULJBSG.js +3368 -0
  11. package/dist/chunk-PIQXLBBJ.js +445 -0
  12. package/dist/chunk-SOSOBKQ2.js +145 -0
  13. package/dist/chunk-TWMRZ43O.js +18576 -0
  14. package/dist/composition.d.ts +190 -0
  15. package/dist/composition.js +1 -0
  16. package/dist/data.d.ts +32 -0
  17. package/dist/data.js +1 -0
  18. package/dist/docs.js +69 -5
  19. package/dist/examples.d.ts +1 -1
  20. package/dist/examples.js +112 -2
  21. package/dist/index.d.ts +6 -4
  22. package/dist/index.js +8 -29555
  23. package/dist/pagination.d.ts +52 -0
  24. package/dist/pagination.js +5 -0
  25. package/dist/{presentation-C8BOK0-5.d.ts → presentation-BjP4yTQ1.d.ts} +86 -3
  26. package/dist/previews.js +1 -447
  27. package/dist/repo-readme.js +1 -3
  28. package/dist/schemas-C1DxapX2.d.ts +206 -0
  29. package/dist/schemas.d.ts +1 -12373
  30. package/dist/schemas.js +1 -6275
  31. package/dist/spec/README.md +18 -3
  32. package/dist/spec/catalogs/audiences/index.json +1 -1
  33. package/dist/spec/catalogs/chart-types/index.json +1 -1
  34. package/dist/spec/catalogs/color-schemes/index.json +1 -1
  35. package/dist/spec/catalogs/font-schemes/index.json +1 -1
  36. package/dist/spec/catalogs/languages/index.json +1 -1
  37. package/dist/spec/catalogs/layouts/chart-1x.json +4 -1
  38. package/dist/spec/catalogs/layouts/chart-2x.json +5 -1
  39. package/dist/spec/catalogs/layouts/chart-3x.json +5 -1
  40. package/dist/spec/catalogs/layouts/code-1x.json +4 -1
  41. package/dist/spec/catalogs/layouts/extract/canonical.json +94 -21
  42. package/dist/spec/catalogs/layouts/image-1x.json +4 -1
  43. package/dist/spec/catalogs/layouts/image-2x.json +5 -1
  44. package/dist/spec/catalogs/layouts/image-3x.json +5 -1
  45. package/dist/spec/catalogs/layouts/index.json +1 -1
  46. package/dist/spec/catalogs/layouts/list-2x.json +4 -1
  47. package/dist/spec/catalogs/layouts/list-3x.json +4 -1
  48. package/dist/spec/catalogs/layouts/list-4x.json +5 -1
  49. package/dist/spec/catalogs/layouts/list-5x.json +5 -1
  50. package/dist/spec/catalogs/layouts/list-6x.json +5 -1
  51. package/dist/spec/catalogs/layouts/media-1x.json +4 -1
  52. package/dist/spec/catalogs/layouts/number-2x.json +4 -1
  53. package/dist/spec/catalogs/layouts/number-3x.json +4 -1
  54. package/dist/spec/catalogs/layouts/number-4x.json +5 -1
  55. package/dist/spec/catalogs/layouts/number-5x.json +5 -1
  56. package/dist/spec/catalogs/layouts/number-6x.json +5 -1
  57. package/dist/spec/catalogs/layouts/table-1x.json +4 -1
  58. package/dist/spec/catalogs/layouts/text-2x.json +4 -1
  59. package/dist/spec/catalogs/layouts/text-3x.json +4 -1
  60. package/dist/spec/catalogs/narratives/index.json +2 -2
  61. package/dist/spec/catalogs/purposes/index.json +1 -1
  62. package/dist/spec/catalogs/social-platforms/index.json +1 -1
  63. package/dist/spec/catalogs/themes/index.json +1 -1
  64. package/dist/spec/catalogs/tones/index.json +1 -1
  65. package/dist/spec/openapi.yaml +54 -14
  66. package/dist/spec/previews/layouts/index.json +71 -71
  67. package/dist/spec/schemas/catalog-index.schema.json +53 -0
  68. package/dist/spec/schemas/layout-preview-index.schema.json +54 -0
  69. package/dist/spec/schemas/layout.schema.json +63 -0
  70. package/dist/spec/schemas/opf.schema.json +182 -3
  71. package/dist/spec-files.d.ts +14 -2391
  72. package/dist/spec-files.js +1 -3356
  73. package/dist/types.d.ts +3 -1102
  74. package/dist/types.js +0 -2
  75. package/dist/validator.d.ts +3 -10
  76. package/dist/validator.js +4 -7243
  77. package/package.json +16 -3
  78. package/dist/catalogs.js.map +0 -1
  79. package/dist/docs.js.map +0 -1
  80. package/dist/examples.js.map +0 -1
  81. package/dist/index.js.map +0 -1
  82. package/dist/previews.js.map +0 -1
  83. package/dist/repo-readme.js.map +0 -1
  84. package/dist/schemas.js.map +0 -1
  85. package/dist/spec-files.js.map +0 -1
  86. package/dist/types.js.map +0 -1
  87. package/dist/validator.js.map +0 -1
@@ -0,0 +1,454 @@
1
+ // src/composition.ts
2
+ var MAX_COMPOSITION_DEPTH = 32;
3
+ function resolveFontFamilies(input) {
4
+ const scheme = record(input);
5
+ const family = (value) => typeof value === "string" ? value : record(value).family;
6
+ return {
7
+ heading: family(scheme.heading) ?? scheme.major ?? scheme.minor ?? "Roboto",
8
+ body: family(scheme.body) ?? scheme.minor ?? scheme.major ?? "Roboto",
9
+ code: family(scheme.code) ?? "Roboto Mono"
10
+ };
11
+ }
12
+ function resolveTextStyle(style, measurement) {
13
+ return measurement?.resolveStyle ? measurement.resolveStyle(style) : style;
14
+ }
15
+ function textWidthMeasurer(style, measurement) {
16
+ if (!measurement) return measureText;
17
+ return (text, size) => {
18
+ const width = measurement.measure(text, size, style);
19
+ if (!Number.isFinite(width) || width < 0) throw new RangeError("Text measurement must return a finite, nonnegative width.");
20
+ return width;
21
+ };
22
+ }
23
+ var OPFCompositionError = class extends Error {
24
+ constructor(diagnostics) {
25
+ super("Slide content does not fit its composition.");
26
+ this.diagnostics = diagnostics;
27
+ this.name = "OPFCompositionError";
28
+ }
29
+ diagnostics;
30
+ code = "layout-overflow";
31
+ };
32
+ var fields = ["text", "items", "bullets", "image", "video", "chart", "table", "code", "metric", "quote", "timeline"];
33
+ var headings = /* @__PURE__ */ new Set(["title", "subtitle", "tag"]);
34
+ var rows = ["top", "middle", "bottom"];
35
+ var columns = ["left", "center", "right"];
36
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
37
+ var kind = (field) => field === "items" || field === "bullets" ? "list" : field;
38
+ var round = (value) => Math.round(value * 1e6) / 1e6 || value;
39
+ var textSegments = new Intl.Segmenter("und", { granularity: "grapheme" });
40
+ function measureText(text, fontSize) {
41
+ let units = 0;
42
+ for (const character of text) {
43
+ if (/\p{Mark}|\u200d|\ufe0f/u.test(character)) continue;
44
+ units += character === " " ? 0.32 : /[A-Z0-9]/.test(character) ? 0.62 : character.codePointAt(0) > 11904 ? 1 : 0.54;
45
+ }
46
+ return units * fontSize;
47
+ }
48
+ function wrapText(text, width, fontSize, measure = measureText) {
49
+ const result = [];
50
+ for (const paragraph of text.replace(/\r\n?/g, "\n").split("\n")) {
51
+ let line = "";
52
+ for (const word of paragraph.split(/\s+/u).filter(Boolean)) {
53
+ if (line && measure(`${line} ${word}`, fontSize) > width) {
54
+ result.push(line);
55
+ line = "";
56
+ }
57
+ if (measure(word, fontSize) <= width) {
58
+ line += (line ? " " : "") + word;
59
+ continue;
60
+ }
61
+ for (const { segment: character } of textSegments.segment(word)) {
62
+ if (line && measure(line + character, fontSize) > width) {
63
+ result.push(line);
64
+ line = "";
65
+ }
66
+ line += character;
67
+ }
68
+ }
69
+ result.push(line);
70
+ }
71
+ return result;
72
+ }
73
+ function fitText(text, box, requestedSize = 25, minFontSize = 16, measure = measureText) {
74
+ if (![box.width, box.height, requestedSize, minFontSize].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || requestedSize <= 0 || minFontSize <= 0) {
75
+ throw new RangeError("Text dimensions and font sizes must be finite and positive.");
76
+ }
77
+ const minimum = Math.min(minFontSize, requestedSize);
78
+ let fontSize = requestedSize;
79
+ let lines = wrapText(text, box.width, fontSize, measure);
80
+ while (fontSize > minimum && (lines.length * fontSize * 1.22 > box.height || lines.some((line) => measure(line, fontSize) > box.width))) {
81
+ fontSize = Math.max(minimum, fontSize - 1);
82
+ lines = wrapText(text, box.width, fontSize, measure);
83
+ }
84
+ return { lines, fontSize, lineHeight: fontSize * 1.22, overflow: lines.length * fontSize * 1.22 > box.height + 0.01 || lines.some((line) => measure(line, fontSize) > box.width + 0.01) };
85
+ }
86
+ function fitRichText(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
87
+ if (![box.width, box.height, requestedSize, minFontSize].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("Rich text dimensions and font sizes must be finite and positive.");
88
+ const layout = richTextLayouter(input, box, requestedSize, options), minimum = Math.min(requestedSize, minFontSize);
89
+ let size = requestedSize, result = layout(size);
90
+ while (result.overflow && size > minimum) {
91
+ size = Math.max(minimum, size - 1);
92
+ result = layout(size);
93
+ }
94
+ return result;
95
+ }
96
+ function richTextLayouter(input, box, requestedSize, options) {
97
+ let offset = 0;
98
+ const source = input.map((value, runIndex) => {
99
+ const run = typeof value === "string" ? { text: value } : value;
100
+ if (typeof run?.text !== "string" || run.fontSize !== void 0 && (!Number.isFinite(run.fontSize) || run.fontSize <= 0)) throw new RangeError("Rich text runs need text and a positive finite font size.");
101
+ const start = offset;
102
+ offset += run.text.length;
103
+ const style = resolveTextStyle({ ...options.style, fontFamily: run.fontFamily ?? options.style.fontFamily, fontWeight: run.bold === void 0 ? options.style.fontWeight : run.bold ? 700 : 400, italic: run.italic ?? options.style.italic, path: options.style.path ? `${options.style.path}.${runIndex}` : void 0 }, options.textMeasurement);
104
+ return { run, runIndex, start, end: offset, style };
105
+ });
106
+ const whole = source.map((entry) => entry.run.text).join("");
107
+ const layout = (fontSize) => {
108
+ const ratio = fontSize / requestedSize;
109
+ const fragments = (start, end) => {
110
+ let x = 0;
111
+ const result = [];
112
+ for (const entry of source) {
113
+ const a = Math.max(start, entry.start), b = Math.min(end, entry.end);
114
+ if (b <= a) continue;
115
+ const text = whole.slice(a, b), normalSize = (entry.run.fontSize !== void 0 ? entry.run.fontSize * 4 / 3 : requestedSize) * ratio;
116
+ const script = entry.run.superscript || entry.run.subscript;
117
+ const size = normalSize * (script ? 0.7 : 1), baselineShift = entry.run.superscript ? -normalSize * 0.35 : entry.run.subscript ? normalSize * 0.2 : 0;
118
+ const width2 = textWidthMeasurer(entry.style, options.textMeasurement)(text, size);
119
+ result.push({ text, runIndex: entry.runIndex, start: a - entry.start, end: b - entry.start, x, width: width2, fontSize: size, baselineShift, style: entry.style, run: entry.run });
120
+ x += width2;
121
+ }
122
+ return result;
123
+ };
124
+ const width = (start, end) => fragments(start, end).reduce((sum, fragment) => sum + fragment.width, 0);
125
+ const ranges = [];
126
+ let lineStart = 0, lineEnd = 0;
127
+ const push = (end) => {
128
+ ranges.push({ start: lineStart, end });
129
+ lineStart = end;
130
+ lineEnd = end;
131
+ };
132
+ for (const match of whole.matchAll(/\r\n|\r|\n|[^\S\r\n]+|[^\s]+/gu)) {
133
+ const start = match.index, end = start + match[0].length;
134
+ if (/^[\r\n]/.test(match[0])) {
135
+ push(start);
136
+ lineStart = end;
137
+ lineEnd = end;
138
+ continue;
139
+ }
140
+ if (width(lineStart, end) <= box.width) {
141
+ lineEnd = end;
142
+ continue;
143
+ }
144
+ if (lineEnd > lineStart) push(start);
145
+ if (width(start, end) <= box.width) {
146
+ lineEnd = end;
147
+ continue;
148
+ }
149
+ for (const { segment, index } of textSegments.segment(match[0])) {
150
+ const next = start + index + segment.length;
151
+ if (lineEnd > lineStart && width(lineStart, next) > box.width) push(start + index);
152
+ lineEnd = next;
153
+ }
154
+ }
155
+ ranges.push({ start: lineStart, end: lineEnd });
156
+ let y = 0;
157
+ const richLines = ranges.map((range) => {
158
+ const parts = fragments(range.start, range.end), ascent = Math.max(fontSize, ...parts.map((part) => part.fontSize - part.baselineShift)), descent = Math.max(fontSize * 0.22, ...parts.map((part) => part.fontSize * 0.22 + part.baselineShift));
159
+ const height = ascent + descent, line = { fragments: parts, width: parts.reduce((sum, p) => sum + p.width, 0), y, baseline: y + ascent, height };
160
+ y += height;
161
+ return line;
162
+ });
163
+ return { lines: ranges.map((range) => whole.slice(range.start, range.end)), fontSize, lineHeight: Math.max(fontSize * 1.22, ...richLines.map((line) => line.height)), richLines, height: y, overflow: y > box.height + 0.01 || richLines.some((line) => line.width > box.width + 0.01) };
164
+ };
165
+ return layout;
166
+ }
167
+ function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
168
+ if (!Array.isArray(input) || ![box.width, box.height, requestedSize, minFontSize].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("List content, dimensions and font sizes must be valid.");
169
+ const source = input.map((value, index) => {
170
+ const object = typeof value === "object" && !Array.isArray(value) ? value : void 0;
171
+ const text = object ? object.text : value, description = object?.description, level = object?.level ?? 0;
172
+ if (!Number.isInteger(level) || level < 0) throw new RangeError("List levels must be nonnegative integers.");
173
+ const runs = (value2) => typeof value2 === "string" ? [value2] : value2;
174
+ if (!Array.isArray(runs(text)) || description !== void 0 && !Array.isArray(runs(description))) throw new TypeError("List text and descriptions must be strings or text runs.");
175
+ const base = options.style.path ? `${options.style.path}.${index}` : void 0;
176
+ return { index, level, value: text, descriptionValue: description, textPath: base ? base + (object ? ".text" : "") : void 0, descriptionPath: base ? base + ".description" : void 0, runs: runs(text), descriptionRuns: description === void 0 ? void 0 : runs(description) };
177
+ });
178
+ const layout = (fontSize) => {
179
+ let y = box.y, overflow = false;
180
+ const entries = [], lines = [];
181
+ for (const item of source) {
182
+ const indent = fontSize * 1.1, offset = Math.min(box.width, item.level * indent), x = box.x + offset + indent, width = Math.max(1, box.x + box.width - x);
183
+ if (offset + indent >= box.width) overflow = true;
184
+ const textBox = { x, y, width, height: box.height };
185
+ const style = { ...options.style, path: item.textPath };
186
+ const text = richTextLayouter(item.runs, textBox, requestedSize, { ...options, style })(fontSize);
187
+ textBox.height = text.height;
188
+ lines.push(...text.lines);
189
+ y += text.height;
190
+ let description, descriptionBox;
191
+ if (item.descriptionRuns !== void 0) {
192
+ y += fontSize * 0.12;
193
+ descriptionBox = { x, y, width, height: box.height };
194
+ description = richTextLayouter(item.descriptionRuns, descriptionBox, requestedSize * 0.82, { ...options, style: { ...options.style, path: item.descriptionPath } })(fontSize * 0.82);
195
+ descriptionBox.height = description.height;
196
+ lines.push(...description.lines);
197
+ y += description.height;
198
+ }
199
+ overflow ||= text.overflow || !!description?.overflow;
200
+ entries.push({
201
+ index: item.index,
202
+ level: item.level,
203
+ value: item.value,
204
+ descriptionValue: item.descriptionValue,
205
+ textPath: item.textPath,
206
+ descriptionPath: item.descriptionPath,
207
+ text,
208
+ textBox,
209
+ description,
210
+ descriptionBox,
211
+ marker: { text: ["\u2022", "\u25E6", "\u25AA"][item.level % 3], x: box.x + offset, y: textBox.y + (text.richLines[0]?.baseline ?? fontSize), fontSize, style: resolveTextStyle(style, options.textMeasurement), indent }
212
+ });
213
+ if (item.index < source.length - 1) y += fontSize * 0.28;
214
+ }
215
+ const height = y - box.y;
216
+ return { listEntries: entries, lines, fontSize, lineHeight: fontSize * 1.22, height, overflow: overflow || height > box.height + 0.01 };
217
+ };
218
+ const minimum = Math.min(requestedSize, minFontSize);
219
+ let size = requestedSize, result = layout(size);
220
+ while (result.overflow && size > minimum) {
221
+ size = Math.max(minimum, size - 1);
222
+ result = layout(size);
223
+ }
224
+ return result;
225
+ }
226
+ function contentText(field, value) {
227
+ if (field === "text" || headings.has(field)) return flatten(value);
228
+ if (field === "items" || field === "bullets") return (Array.isArray(value) ? value : [value]).map((item) => {
229
+ const entry = record(item);
230
+ return `\u2022 ${flatten(item)}${entry.description ? `
231
+ ${flatten(entry.description)}` : ""}`;
232
+ }).join("\n");
233
+ if (field === "code") return flatten(record(value).source ?? value);
234
+ if (field === "quote") return flatten(record(value).text ?? value);
235
+ return void 0;
236
+ }
237
+ function tableOverflows(value, box, scale, settings, options) {
238
+ const table = record(value), rows2 = Array.isArray(table.rows) ? table.rows : [];
239
+ const allRows = Array.isArray(table.columns) && table.columns.length ? [table.columns, ...rows2] : rows2;
240
+ const columns2 = Math.max(1, ...allRows.map((row) => row.length));
241
+ const width = box.width / columns2 - 20 * scale;
242
+ const height = Math.min(54 * scale, box.height / Math.max(1, allRows.length)) - 12 * scale;
243
+ return width <= 0 || height <= 0 || allRows.some((row, index) => row.some((value2) => fitText(flatten(value2), { width, height }, 15 * scale, (settings.minFontSize ?? 16) * scale, textWidthMeasurer(resolveTextStyle({ fontFamily: options.fonts?.body ?? "sans-serif", fontWeight: index === 0 && table.columns?.length ? 700 : 400 }, options.textMeasurement), options.textMeasurement)).overflow));
244
+ }
245
+ function flatten(value) {
246
+ if (value == null) return "";
247
+ if (typeof value !== "object") return String(value);
248
+ if (Array.isArray(value)) return value.map(flatten).join("");
249
+ const item = record(value);
250
+ return flatten(item.text ?? item.value ?? item.source ?? JSON.stringify(value));
251
+ }
252
+ function regionParts(key) {
253
+ const span = (part, order) => {
254
+ const indices = part.split("+").map((value) => order.indexOf(value));
255
+ return indices.every((index) => index >= 0) ? indices : void 0;
256
+ };
257
+ const parts = key.split(":");
258
+ if (parts.length === 2) {
259
+ const r = span(parts[0], rows), c = span(parts[1], columns);
260
+ if (r && c) return [r, c];
261
+ } else if (parts.length === 1) {
262
+ const r = span(key, rows), c = span(key, columns);
263
+ if (r) return [r, [0, 1, 2]];
264
+ if (c) return [[0, 1, 2], c];
265
+ }
266
+ return void 0;
267
+ }
268
+ function regionBox(parts, area, requestedGap) {
269
+ const gap = Math.min(requestedGap, area.width / 6, area.height / 6);
270
+ const cw = (area.width - gap * 2) / 3, ch = (area.height - gap * 2) / 3;
271
+ const [r, c] = parts;
272
+ return { x: area.x + Math.min(...c) * (cw + gap), y: area.y + Math.min(...r) * (ch + gap), width: (Math.max(...c) - Math.min(...c) + 1) * (cw + gap) - gap, height: (Math.max(...r) - Math.min(...r) + 1) * (ch + gap) - gap };
273
+ }
274
+ function gridGeometry(count, area, cols, gap, weights, vertical) {
275
+ const rowCount = Math.ceil(count / cols);
276
+ gap = Math.min(gap, area.width / (cols * 2), area.height / (rowCount * 2));
277
+ const tracks = (length, total, weighted) => {
278
+ const values = Array.from({ length }, (_, i) => weighted ? weights[i] ?? 1 : 1);
279
+ const sum = values.reduce((a, b) => a + b, 0);
280
+ let cursor = 0;
281
+ return values.map((weight) => {
282
+ const size = (total - gap * (length - 1)) * weight / sum;
283
+ const track = { offset: cursor, size };
284
+ cursor += size + gap;
285
+ return track;
286
+ });
287
+ };
288
+ const xs = tracks(cols, area.width, !vertical), ys = tracks(rowCount, area.height, vertical);
289
+ const boxes = Array.from({ length: count }, (_, i) => ({ x: area.x + xs[i % cols].offset, y: area.y + ys[Math.floor(i / cols)].offset, width: xs[i % cols].size, height: ys[Math.floor(i / cols)].size }));
290
+ return { boxes, columns: xs, rows: ys, gap };
291
+ }
292
+ function gridBoxes(count, area, cols, gap, weights, vertical) {
293
+ return gridGeometry(count, area, cols, gap, weights, vertical).boxes;
294
+ }
295
+ function assertComposition(value) {
296
+ if (value.mode !== void 0 && !["auto", "grid", "row", "column"].includes(value.mode)) throw new RangeError("Unknown composition mode.");
297
+ for (const [key, min, max] of [["gap", 0, 0.1], ["padding", 0, 0.2], ["minFontSize", 8, 32], ["columns", 1, 12]]) {
298
+ const number = value[key];
299
+ if (number !== void 0 && (!Number.isFinite(number) || number < min || number > max || key === "columns" && !Number.isInteger(number))) throw new RangeError(`Invalid composition.${key}.`);
300
+ }
301
+ if (value.weights !== void 0 && (!Array.isArray(value.weights) || !value.weights.length || value.weights.length > 12 || value.weights.some((weight) => !Number.isFinite(weight) || weight <= 0 || weight > 100))) throw new RangeError("Invalid composition.weights.");
302
+ if (value.overflow !== void 0 && !["warn", "error"].includes(value.overflow)) throw new RangeError("Invalid composition.overflow.");
303
+ }
304
+ function composeSlide(input, options = {}) {
305
+ const slide = record(input), layout = record(options.layout);
306
+ const width = options.width ?? 1280, height = options.height ?? 720;
307
+ if (![width, height].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("Canvas dimensions must be finite and positive.");
308
+ const scale = Math.min(width, height) / 720;
309
+ const composition = { ...record(layout.composition), ...record(slide.composition) };
310
+ assertComposition(composition);
311
+ const padding = (composition.padding ?? 0.08) * Math.min(width, height);
312
+ const gap = (composition.gap ?? 1 / 30) * Math.min(width, height);
313
+ const minSize = (composition.minFontSize ?? 16) * scale;
314
+ const styleFor = (field, path2) => resolveTextStyle({
315
+ fontFamily: (field === "title" ? options.fonts?.heading : field === "code" ? options.fonts?.code : options.fonts?.body) ?? (field === "code" ? "monospace" : "sans-serif"),
316
+ fontWeight: field === "title" ? 700 : 400,
317
+ path: path2
318
+ }, options.textMeasurement);
319
+ const widthFor = (field, path2) => textWidthMeasurer(styleFor(field, path2), options.textMeasurement);
320
+ const fitContent = (field, value, text, box, size, minimum, path2) => field === "text" && Array.isArray(value) ? fitRichText(value, box, size, minimum, { style: styleFor(field, path2), textMeasurement: options.textMeasurement }) : field === "items" || field === "bullets" ? fitList(value, box, size, minimum, { style: styleFor(field, path2), textMeasurement: options.textMeasurement }) : fitText(text, box, size, minimum, widthFor(field, path2));
321
+ const path = `slides.${options.slideIndex ?? 0}`;
322
+ const items = [], diagnostics = [];
323
+ let y = padding;
324
+ for (const field of ["tag", "title", "subtitle"]) {
325
+ if (!slide[field]) continue;
326
+ const requested = (field === "title" ? 54 : field === "tag" ? 16 : 25) * scale;
327
+ const maxHeight = height * (field === "title" ? 0.26 : field === "subtitle" ? 0.12 : 0.045);
328
+ const box = { x: padding, y, width: width - padding * 2, height: maxHeight };
329
+ const text = fitText(String(slide[field]), box, requested, minSize, widthFor(field, `${path}.${field}`));
330
+ box.height = Math.min(maxHeight, text.lines.length * text.lineHeight);
331
+ items.push({ path: `${path}.${field}`, field, type: "text", value: slide[field], payload: { text: slide[field] }, box, text, textStyle: styleFor(field, `${path}.${field}`), composition });
332
+ y += box.height + gap * 0.5;
333
+ }
334
+ if (items.length) y += gap * 0.5;
335
+ const contentBox = { x: padding, y, width: width - padding * 2, height: Math.max(scale, height - padding - y) };
336
+ const collect = (host, basePath, depth = 0, ancestors = []) => {
337
+ if (Array.isArray(host.blocks)) {
338
+ if (depth >= MAX_COMPOSITION_DEPTH || ancestors.includes(host)) throw new RangeError(`Content groups must be acyclic and nest at most ${MAX_COMPOSITION_DEPTH} levels.`);
339
+ const settings = record(host.composition);
340
+ assertComposition(settings);
341
+ return [{
342
+ field: "blocks",
343
+ type: "group",
344
+ value: host.blocks,
345
+ path: basePath,
346
+ payload: host,
347
+ composition: settings,
348
+ children: host.blocks.flatMap((block, index) => collect(record(block), `${basePath}.blocks.${index}`, depth + 1, [...ancestors, host]))
349
+ }];
350
+ }
351
+ return fields.filter((field) => host[field] !== void 0).map((field) => ({ field, type: host.type ?? kind(field), value: host[field], path: `${basePath}.${field}`, payload: { type: host.type ?? kind(field), [field]: host[field] } }));
352
+ };
353
+ const regions = Object.keys(slide).filter((key) => regionParts(key)).sort();
354
+ const pending = regions.length ? regions.flatMap((key) => collect(record(slide[key]), `${path}.${key}`).map((item) => ({ ...item, region: regionParts(key) }))) : Array.isArray(slide.blocks) ? slide.blocks.flatMap((block, index) => collect(record(block), `${path}.blocks.${index}`)) : collect(slide, path);
355
+ const groups = [], flows = [];
356
+ const inheritedSettings = (parent, own = {}) => ({
357
+ minFontSize: parent.minFontSize,
358
+ ...own,
359
+ overflow: parent.overflow === "error" ? "error" : own.overflow ?? parent.overflow
360
+ });
361
+ const inset = (box, settings) => {
362
+ const amount = (settings.padding ?? 0) * Math.min(box.width, box.height);
363
+ return { x: box.x + amount, y: box.y + amount, width: box.width - amount * 2, height: box.height - amount * 2 };
364
+ };
365
+ const leafScore = (node, box, settings) => {
366
+ const text = contentText(node.field, node.value);
367
+ let score = Math.abs(Math.log(box.width / box.height / 1.6));
368
+ if (text) {
369
+ const fit = fitContent(node.field, node.value, text, box, 25 * scale, (settings.minFontSize ?? 16) * scale, node.path);
370
+ score += (25 * scale - fit.fontSize) / scale + (fit.overflow ? 1e3 : 0);
371
+ }
372
+ if (node.field === "table" && tableOverflows(node.value, box, scale, settings, options)) score += 1e3;
373
+ if (box.width < 100 * scale || box.height < 60 * scale) score += 100;
374
+ return score;
375
+ };
376
+ const modeFor = (settings) => settings.mode ?? "auto";
377
+ const defaultColumns = (count, area, settings) => {
378
+ const mode = modeFor(settings);
379
+ return mode === "column" ? 1 : mode === "row" ? Math.max(1, count) : settings.columns ?? Math.max(1, Math.min(count, Math.ceil(Math.sqrt(count * area.width / area.height / 1.6))));
380
+ };
381
+ const scoreNode = (node, box, settings) => {
382
+ if (!node.children) return leafScore(node, box, settings);
383
+ const own = inheritedSettings(settings, node.composition), area = inset(box, own);
384
+ const cols = defaultColumns(node.children.length, area, own);
385
+ const boxes = gridBoxes(node.children.length, area, cols, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), own.weights ?? [], modeFor(own) === "column");
386
+ return node.children.reduce((score, child, index) => score + scoreNode(child, boxes[index], own), 0);
387
+ };
388
+ const arrange = (nodes, area, settings, reserved = 0, gapOverride, containerPath = path) => {
389
+ const count = Math.max(nodes.length, reserved);
390
+ if (!count) return;
391
+ const mode = modeFor(settings), localGap = (settings.gap ?? 1 / 30) * Math.min(area.width, area.height);
392
+ const actualGap = gapOverride ?? (settings === rootSettings ? gap : localGap);
393
+ let cols = defaultColumns(count, area, settings);
394
+ if (mode === "auto" && !nodes.some((node) => node.region)) {
395
+ let best = Infinity;
396
+ for (let candidate = 1; candidate <= Math.min(count, settings.columns ?? 6); candidate++) {
397
+ const boxes2 = gridBoxes(count, area, candidate, actualGap, settings.weights ?? [], false);
398
+ const score = nodes.reduce((sum, node, i) => sum + scoreNode(node, boxes2[i], settings), 0) + (Math.ceil(count / candidate) * candidate - count) * 2;
399
+ if (score < best) {
400
+ best = score;
401
+ cols = candidate;
402
+ }
403
+ }
404
+ }
405
+ const grid = gridGeometry(count, area, cols, actualGap, settings.weights ?? [], mode === "column");
406
+ const boxes = grid.boxes;
407
+ if (!nodes.some((node) => node.region)) flows.push({ path: containerPath, box: { ...area }, composition: { ...settings }, columns: grid.columns, rows: grid.rows, gap: grid.gap, itemCount: nodes.length, slotCount: count });
408
+ nodes.forEach((node, index) => {
409
+ const box = node.region ? regionBox(node.region, area, actualGap) : boxes[index];
410
+ if (node.children) {
411
+ const own = inheritedSettings(settings, node.composition), inner = inset(box, own);
412
+ groups.push({ path: node.path, box, contentBox: inner, composition: own });
413
+ arrange(node.children, inner, own, 0, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), node.path);
414
+ } else {
415
+ const textValue = contentText(node.field, node.value);
416
+ const text = textValue !== void 0 ? fitContent(node.field, node.value, textValue, box, 25 * scale, (settings.minFontSize ?? 16) * scale, node.path) : void 0;
417
+ items.push({ path: node.path, field: node.field, type: node.type, value: node.value, payload: node.payload, box, text, textStyle: styleFor(node.field, node.path), composition: settings });
418
+ if (box.width < 100 * scale || box.height < 60 * scale) diagnostics.push({ code: "small-cell", path: node.path, message: "Content cell is too small for comfortable reading; use fewer blocks or a different composition." });
419
+ }
420
+ });
421
+ };
422
+ const placeholders = Array.isArray(layout.placeholders) ? layout.placeholders.filter((p) => !headings.has(p.type)) : [];
423
+ const rootSettings = { ...composition, mode: composition.mode ?? (layout.slideLayoutDirection === "Vertical" ? "column" : layout.slideLayoutDirection === "Horizontal" ? "row" : "auto") };
424
+ arrange(pending, contentBox, rootSettings, composition.mode ? 0 : placeholders.length);
425
+ for (const item of items) {
426
+ for (const key of ["x", "y", "width", "height"]) item.box[key] = round(item.box[key]);
427
+ if (item.field === "table" && tableOverflows(item.value, item.box, scale, item.composition, options)) diagnostics.push({ code: "text-overflow", path: item.path, message: "Table cells do not fit; use fewer rows, fewer columns, or split the table across slides." });
428
+ if (item.text?.overflow) diagnostics.push({ code: "text-overflow", path: item.path, message: "Text exceeds its cell at the minimum font size; shorten it, increase its space, or split the slide." });
429
+ }
430
+ for (const group of groups) for (const box of [group.box, group.contentBox]) for (const key of ["x", "y", "width", "height"]) box[key] = round(box[key]);
431
+ const strictPaths = new Set(items.filter((item) => item.composition.overflow === "error").map((item) => item.path));
432
+ const failures = diagnostics.filter((diagnostic) => strictPaths.has(diagnostic.path));
433
+ if (failures.length) throw new OPFCompositionError(failures);
434
+ return { width, height, contentBox, items, groups, flows, diagnostics, composition };
435
+ }
436
+ function resolveCanvasDimensions(input) {
437
+ const presets = {
438
+ widescreen: [40 / 3, 7.5],
439
+ "16:9": [40 / 3, 7.5],
440
+ standard: [10, 7.5],
441
+ "4:3": [10, 7.5],
442
+ "16:10": [10, 6.25],
443
+ letter: [11, 8.5],
444
+ a4: [11.69, 8.27]
445
+ };
446
+ const value = record(input);
447
+ const preset = presets[typeof input === "string" ? input : value.preset] ?? presets.widescreen;
448
+ const width = (value.widthInches ?? preset[0]) * 96;
449
+ const height = (value.heightInches ?? preset[1]) * 96;
450
+ if (![width, height].every((n) => Number.isFinite(n) && n > 0)) throw new RangeError("Canvas dimensions must be finite and positive.");
451
+ return { width, height };
452
+ }
453
+
454
+ export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textWidthMeasurer, wrapText };