@openpresentation/opf 0.11.3 → 0.12.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 (64) hide show
  1. package/README.md +16 -0
  2. package/dist/audit.d.ts +156 -0
  3. package/dist/audit.js +7 -0
  4. package/dist/{catalogs-CUClB3nl.d.ts → catalogs-u6TEeMcz.d.ts} +1 -1
  5. package/dist/catalogs.d.ts +2 -2
  6. package/dist/catalogs.js +2 -1
  7. package/dist/{chunk-UDPSGTZI.js → chunk-32DIB5BN.js} +12 -9
  8. package/dist/chunk-4FWCY4ZV.js +198 -0
  9. package/dist/chunk-6TSDIPSH.js +3624 -0
  10. package/dist/chunk-FYLJVF7X.js +18448 -0
  11. package/dist/{chunk-HJL64ETN.js → chunk-GJKJL4YY.js} +14 -0
  12. package/dist/chunk-JENJZR3F.js +1690 -0
  13. package/dist/{chunk-57NWIYX3.js → chunk-JVTN3HPP.js} +18 -4
  14. package/dist/{chunk-JTXCMVVP.js → chunk-RVN5C2FX.js} +754 -39
  15. package/dist/chunk-S5W34SIJ.js +1 -0
  16. package/dist/chunk-S667QI3M.js +261 -0
  17. package/dist/chunk-UQKJWHSS.js +1332 -0
  18. package/dist/{chunk-RXNFDGPC.js → chunk-WXSPETQ6.js} +311 -40
  19. package/dist/composition-DWbCiMwF.d.ts +1523 -0
  20. package/dist/composition.d.ts +1 -1
  21. package/dist/composition.js +2 -1
  22. package/dist/convert.d.ts +261 -0
  23. package/dist/convert.js +1107 -0
  24. package/dist/diff.d.ts +145 -0
  25. package/dist/diff.js +604 -0
  26. package/dist/docs.js +69 -15
  27. package/dist/examples.d.ts +1 -1
  28. package/dist/font-policy.d.ts +15 -0
  29. package/dist/font-policy.js +1 -1
  30. package/dist/format.d.ts +20 -0
  31. package/dist/format.js +83 -0
  32. package/dist/index.d.ts +173 -24
  33. package/dist/index.js +926 -262
  34. package/dist/lint.d.ts +4 -4
  35. package/dist/lint.js +6 -5
  36. package/dist/markdown.d.ts +74 -0
  37. package/dist/markdown.js +1941 -0
  38. package/dist/pagination.d.ts +1 -1
  39. package/dist/pagination.js +6 -5
  40. package/dist/patch.d.ts +99 -0
  41. package/dist/patch.js +6 -0
  42. package/dist/{presentation-Cn6vTOCP.d.ts → presentation-DRx4hpNZ.d.ts} +479 -32
  43. package/dist/repo-readme.js +1 -1
  44. package/dist/{schemas-X_NniU4A.d.ts → schemas-BSjlG8-R.d.ts} +28 -0
  45. package/dist/schemas.d.ts +1 -1
  46. package/dist/schemas.js +1 -1
  47. package/dist/spec/reference/font-policy.json +311 -40
  48. package/dist/spec/reference/font-policy.schema.json +40 -0
  49. package/dist/spec/reference/symbol-font-encodings.json +18354 -0
  50. package/dist/spec/reference/symbol-font-encodings.schema.json +156 -0
  51. package/dist/spec/schemas/opf.schema.json +608 -35
  52. package/dist/spec-files.d.ts +1 -1
  53. package/dist/spec-files.js +1 -1
  54. package/dist/symbol-font-encodings.d.ts +128 -0
  55. package/dist/symbol-font-encodings.js +1 -0
  56. package/dist/types.d.ts +4 -4
  57. package/dist/validator-bu396ffT.d.ts +172 -0
  58. package/dist/validator.d.ts +4 -36
  59. package/dist/validator.js +5 -4
  60. package/package.json +31 -2
  61. package/dist/chunk-BVLWFMDX.js +0 -1812
  62. package/dist/chunk-JCXHSTSM.js +0 -1366
  63. package/dist/composition-DzLwY-Mi.d.ts +0 -887
  64. /package/dist/{chunk-AEUSVEUY.js → chunk-3ZGSWEHK.js} +0 -0
@@ -1,1812 +0,0 @@
1
- // src/table.ts
2
- var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
3
- function tableGrid(value, path = "table") {
4
- const table = record(value), hasHeaders = Array.isArray(table.columns) && table.columns.length > 0;
5
- const values = [...hasHeaders ? [table.columns] : [], ...Array.isArray(table.rows) ? table.rows : []];
6
- const columnCount = values.reduce((count, row) => Math.max(count, Array.isArray(row) ? row.length : 0), 1), rowCount = values.length;
7
- const owners = Array.from({ length: rowCount }, () => Array(columnCount));
8
- const rows2 = Array.from({ length: rowCount }, () => []), issues = [];
9
- const location = (r, c) => `${path}.${hasHeaders && r === 0 ? "columns" : `rows.${r - Number(hasHeaders)}`}.${c}`;
10
- for (let r = 0; r < rowCount; r++) for (let c = 0; c < columnCount; c++) {
11
- const input = values[r]?.[c], cellPath = location(r, c), existing = owners[r][c];
12
- if (existing) {
13
- if (input !== null) issues.push({ path: cellPath, message: "A position covered by a spanning cell must explicitly contain null; content cannot be hidden." });
14
- continue;
15
- }
16
- const object = record(input), styled = Object.hasOwn(object, "value");
17
- const rowSpan = styled ? object.rowSpan ?? 1 : 1, colSpan = styled ? object.colSpan ?? 1 : 1;
18
- if (!Number.isSafeInteger(rowSpan) || !Number.isSafeInteger(colSpan) || rowSpan < 1 || colSpan < 1 || r + rowSpan > rowCount || c + colSpan > columnCount || hasHeaders && r === 0 && rowSpan > 1) {
19
- issues.push({ path: cellPath, message: "Cell spans must be positive integers within the table grid and cannot cross from headers into body rows." });
20
- continue;
21
- }
22
- const cell = { input, value: styled ? object.value : input, style: styled ? record(object.style) : {}, row: r, column: c, rowSpan, colSpan, header: hasHeaders && r === 0, path: cellPath, valuePath: styled ? cellPath + ".value" : cellPath };
23
- rows2[r].push(cell);
24
- for (let y = r; y < r + rowSpan; y++) for (let x = c; x < c + colSpan; x++) {
25
- if (owners[y][x]) issues.push({ path: cellPath, message: "Cell spans cannot overlap another spanning cell." });
26
- else owners[y][x] = cell;
27
- }
28
- }
29
- return { rows: rows2, columnCount, rowCount, hasHeaders, owners, issues };
30
- }
31
- function tableRowBoundaries(value) {
32
- const grid = tableGrid(value), offset = Number(grid.hasHeaders), count = grid.rowCount - offset;
33
- const allowed = new Set(Array.from({ length: count + 1 }, (_, i) => i));
34
- for (const row of grid.rows) for (const cell of row) if (!cell.header) for (let r = 1; r < cell.rowSpan; r++) allowed.delete(cell.row - offset + r);
35
- return [...allowed];
36
- }
37
-
38
- // src/furniture-fields.ts
39
- var DEFAULT_FURNITURE_DATE_FORMAT = "M/d/yyyy";
40
- var DEFAULT_SLIDE_NUMBER_FORMAT = "{current}";
41
- var months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
42
- var weekdays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
43
- var tokenWidths = { y: [1, 2, 4], M: [1, 2, 3, 4], d: [1, 2], E: [1, 2, 3, 4] };
44
- function parseFurnitureDateFormat(pattern) {
45
- if (!pattern) return "dateFormat must not be empty.";
46
- const tokens = [];
47
- const literal = (text) => {
48
- const last = tokens[tokens.length - 1];
49
- if (last?.kind === "literal") last.text += text;
50
- else tokens.push({ kind: "literal", text });
51
- };
52
- for (let index = 0; index < pattern.length; ) {
53
- const character = pattern.charAt(index);
54
- if (character === "'") {
55
- if (pattern[index + 1] === "'") {
56
- literal("'");
57
- index += 2;
58
- continue;
59
- }
60
- let end = index + 1, text = "";
61
- for (; ; ) {
62
- if (end >= pattern.length) return "dateFormat has an unterminated quoted literal.";
63
- if (pattern[end] === "'") {
64
- if (pattern[end + 1] === "'") {
65
- text += "'";
66
- end += 2;
67
- continue;
68
- }
69
- break;
70
- }
71
- text += pattern[end];
72
- end++;
73
- }
74
- literal(text);
75
- index = end + 1;
76
- continue;
77
- }
78
- if (/[A-Za-z]/.test(character)) {
79
- let end = index;
80
- while (pattern[end] === character) end++;
81
- const width = end - index;
82
- if (!tokenWidths[character]?.includes(width)) return `dateFormat token '${pattern.slice(index, end)}' is not supported; use yyyy, yy, MMMM, MMM, MM, M, dd, d, EEEE or EEE, and quote literal letters.`;
83
- tokens.push({ kind: "field", letter: character, width });
84
- index = end;
85
- continue;
86
- }
87
- literal(character);
88
- index++;
89
- }
90
- return tokens;
91
- }
92
- function parseIsoDate(value) {
93
- const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
94
- if (!match) return null;
95
- const year = Number(match[1]), month = Number(match[2]), day = Number(match[3]);
96
- const date = new Date(Date.UTC(2e3, month - 1, day));
97
- date.setUTCFullYear(year);
98
- if (date.getUTCFullYear() !== year || date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) return null;
99
- return { year, month, day };
100
- }
101
- function formatFurnitureDate(iso, pattern) {
102
- const date = parseIsoDate(iso);
103
- if (!date) return { error: `'${iso}' is not an ISO YYYY-MM-DD calendar date.` };
104
- const tokens = parseFurnitureDateFormat(pattern);
105
- if (typeof tokens === "string") return { error: tokens };
106
- const utc = new Date(Date.UTC(2e3, date.month - 1, date.day));
107
- utc.setUTCFullYear(date.year);
108
- const pad = (value, width) => String(value).padStart(width, "0");
109
- let text = "";
110
- for (const token of tokens) {
111
- if (token.kind === "literal") {
112
- text += token.text;
113
- continue;
114
- }
115
- const { letter, width } = token;
116
- if (letter === "y") text += width === 2 ? pad(date.year % 100, 2) : width === 4 ? pad(date.year, 4) : String(date.year);
117
- else if (letter === "M") {
118
- const name = months[date.month - 1] ?? "";
119
- text += width >= 3 ? width === 4 ? name : name.slice(0, 3) : pad(date.month, width);
120
- } else if (letter === "d") text += pad(date.day, width);
121
- else {
122
- const name = weekdays[utc.getUTCDay()] ?? "";
123
- text += width === 4 ? name : name.slice(0, 3);
124
- }
125
- }
126
- return { text };
127
- }
128
- function formatSlideNumber(format, current, total) {
129
- if (!format.includes("{current}")) return { error: "slideNumberFormat must contain {current}." };
130
- if (format.includes("{total}") && total === void 0) return { error: "slideNumberFormat uses {total}, which needs the rendered slide count." };
131
- let text = "";
132
- const fields2 = [];
133
- for (const piece of format.split(/(\{current\}|\{total\})/)) {
134
- if (piece === "{current}") {
135
- const value = String(current);
136
- fields2.push({ type: "slideNumber", start: text.length, end: text.length + value.length });
137
- text += value;
138
- } else if (piece === "{total}") text += String(total);
139
- else text += piece;
140
- }
141
- return { text, fields: fields2 };
142
- }
143
-
144
- // src/color.ts
145
- var schemeColorSlots = /* @__PURE__ */ new Set([
146
- "accent1",
147
- "accent2",
148
- "accent3",
149
- "accent4",
150
- "accent5",
151
- "accent6",
152
- "dark1",
153
- "dark2",
154
- "light1",
155
- "light2",
156
- "hyperlink",
157
- "followedHyperlink"
158
- ]);
159
- var schemeColorRoles = /* @__PURE__ */ new Set([
160
- "primary",
161
- "secondary",
162
- "accent",
163
- "background",
164
- "surface",
165
- "text",
166
- "textSecondary"
167
- ]);
168
- var defaultRoleSlots = {
169
- primary: "accent1",
170
- secondary: "accent2",
171
- accent: "accent3",
172
- background: "light1",
173
- surface: "light2",
174
- text: "dark1",
175
- textSecondary: "dark2"
176
- };
177
- function normalizeHexColor(value) {
178
- if (typeof value !== "string") return void 0;
179
- let hex = value.trim();
180
- if (/^#[0-9a-f]{3}$/i.test(hex)) hex = `#${[...hex.slice(1)].map((char) => char + char).join("")}`;
181
- if (/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(hex)) return hex.length === 9 ? `#${hex.slice(1, 7).toUpperCase()}` : hex.toUpperCase();
182
- return void 0;
183
- }
184
- function schemeSlot(colorScheme, slot, fallback) {
185
- return normalizeHexColor(colorScheme[slot]) ?? fallback;
186
- }
187
- function resolveVariable(variables, id, fallback) {
188
- const entry = variables[id];
189
- if (typeof entry === "string") return normalizeHexColor(entry) ?? fallback;
190
- if (entry && typeof entry === "object" && entry.type === "color") {
191
- return normalizeHexColor(entry.value) ?? fallback;
192
- }
193
- return fallback;
194
- }
195
- function resolveColorRef(reference, options) {
196
- const { colorScheme, roles, variables = {}, fallback } = options;
197
- const literal = normalizeHexColor(reference);
198
- if (literal) return literal;
199
- if (reference.startsWith("var:")) {
200
- const id = reference.slice("var:".length);
201
- if (/^[a-z][a-z0-9-]*$/.test(id)) return resolveVariable(variables, id, fallback);
202
- return fallback;
203
- }
204
- if (schemeColorRoles.has(reference)) {
205
- const roleKey = reference;
206
- const fromRoles = roles?.[roleKey];
207
- if (fromRoles) return normalizeHexColor(fromRoles) ?? fallback;
208
- const fromScheme = normalizeHexColor(colorScheme[reference]);
209
- if (fromScheme) return fromScheme;
210
- const slot = defaultRoleSlots[reference];
211
- return slot ? schemeSlot(colorScheme, slot, fallback) : fallback;
212
- }
213
- if (schemeColorSlots.has(reference)) return schemeSlot(colorScheme, reference, fallback);
214
- return fallback;
215
- }
216
- function luminance(value) {
217
- if (typeof value !== "string") return void 0;
218
- let hex = value.trim();
219
- if (/^#[0-9a-f]{3}$/i.test(hex)) hex = `#${[...hex.slice(1)].map((c) => c + c).join("")}`;
220
- if (/^#[0-9a-f]{6}ff$/i.test(hex)) hex = hex.slice(0, 7);
221
- if (!/^#[0-9a-f]{6}$/i.test(hex)) return void 0;
222
- return [0.2126, 0.7152, 0.0722].reduce((sum, weight, index) => {
223
- const offset = index * 2 + 1;
224
- const n = Number.parseInt(hex.slice(offset, offset + 2), 16) / 255;
225
- return sum + weight * (n <= 0.04045 ? n / 12.92 : ((n + 0.055) / 1.055) ** 2.4);
226
- }, 0);
227
- }
228
- function colorContrast(foreground, background) {
229
- const a = luminance(foreground), b = luminance(background);
230
- return a === void 0 || b === void 0 ? void 0 : (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
231
- }
232
- function textColorForFill(fill, preferred) {
233
- const background = luminance(fill), contrast = colorContrast(preferred, fill);
234
- if (background === void 0 || contrast === void 0 || contrast >= 4.5) return preferred;
235
- return (background + 0.05) / 0.05 >= 1.05 / (background + 0.05) ? "#000000" : "#FFFFFF";
236
- }
237
- function chartColorForFill(fill, preferred) {
238
- const contrast = colorContrast(preferred, fill);
239
- if (contrast === void 0 || contrast >= 3) return preferred;
240
- let hex = preferred.trim();
241
- if (hex.length === 4) hex = `#${[...hex.slice(1)].map((c) => c + c).join("")}`;
242
- const channels = [1, 3, 5].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16));
243
- for (let step = 1; step <= 255; step++) {
244
- const candidates = [0, 255].map((target) => {
245
- const color = `#${channels.map((channel) => Math.round(channel + (target - channel) * step / 255).toString(16).padStart(2, "0")).join("")}`.toUpperCase();
246
- return { color, contrast: colorContrast(color, fill) ?? 0 };
247
- }).sort((a, b) => b.contrast - a.contrast);
248
- const best = candidates[0];
249
- if (best && best.contrast >= 3) return best.color;
250
- }
251
- return textColorForFill(fill, preferred);
252
- }
253
-
254
- // src/composition.ts
255
- var MAX_COMPOSITION_DEPTH = 32;
256
- var FALLBACK_CODE_FAMILY = "Roboto Mono";
257
- var DEFAULT_FONT_SCHEME = "aptos";
258
- var DEFAULT_FONT_FAMILIES = { heading: "Aptos Display", body: "Aptos" };
259
- function resolveFontSchemeReference(reference, lookup, path = "design.fontScheme") {
260
- const overrides = typeof reference === "object" && reference !== null && !Array.isArray(reference) ? reference : void 0;
261
- const id = typeof reference === "string" ? reference : typeof overrides?.id === "string" ? overrides.id : void 0;
262
- const found = id === void 0 ? void 0 : lookup(id);
263
- const base = record2(found ?? lookup(DEFAULT_FONT_SCHEME));
264
- const scheme = overrides ? { ...base, ...overrides } : { ...base };
265
- if (id === void 0 || found !== void 0) return { scheme };
266
- return { scheme, diagnostic: { code: "unresolved-font-scheme", path, id, fallback: DEFAULT_FONT_SCHEME, message: `Font scheme '${id}' is not in the inline or bundled catalogs; using the default font scheme '${DEFAULT_FONT_SCHEME}'.` } };
267
- }
268
- function resolveFontFamilies(input) {
269
- const scheme = record2(input);
270
- const family = (value) => typeof value === "string" ? value : record2(value).family;
271
- return {
272
- heading: family(scheme.heading) ?? scheme.major ?? scheme.minor ?? DEFAULT_FONT_FAMILIES.heading,
273
- body: family(scheme.body) ?? scheme.minor ?? scheme.major ?? DEFAULT_FONT_FAMILIES.body,
274
- code: family(scheme.code) ?? FALLBACK_CODE_FAMILY
275
- };
276
- }
277
- function resolveTextStyle(style, measurement) {
278
- return measurement?.resolveStyle ? measurement.resolveStyle(style) : style;
279
- }
280
- function textWidthMeasurer(style, measurement) {
281
- if (!measurement) return measureText;
282
- return (text, size) => {
283
- const width = measurement.measure(text, size, style);
284
- if (!Number.isFinite(width) || width < 0) throw new RangeError("Text measurement must return a finite, nonnegative width.");
285
- return width;
286
- };
287
- }
288
- function measureTextOutline(text, fontSize, style, measurement) {
289
- if (measurement?.outlineBounds === void 0) return void 0;
290
- if (typeof measurement.outlineBounds !== "function") throw new TypeError("Text outline provider must be a function.");
291
- const bounds = measurement.outlineBounds(text, fontSize, style);
292
- if (bounds === null) return null;
293
- if (!bounds || ![bounds.x, bounds.y, bounds.width, bounds.height].every(Number.isFinite) || bounds.width < 0 || bounds.height < 0) {
294
- throw new RangeError("Text outline measurement must return null or finite bounds with nonnegative dimensions.");
295
- }
296
- return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
297
- }
298
- function placeTextLines(lines, box, alignment = "left", rasterPadding = 0) {
299
- if (!Array.isArray(lines) || !box || ![box.x, box.y, box.width, box.height, rasterPadding].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || rasterPadding < 0 || !["left", "center", "right"].includes(alignment)) throw new RangeError("Text placement requires lines, finite positive dimensions, nonnegative padding and a valid alignment.");
300
- const factor = alignment === "right" ? 1 : alignment === "center" ? 0.5 : 0, placed = [];
301
- let shift = 0, bottom = box.y, height = 0, overflow = false;
302
- for (const line of lines) {
303
- if (!line || ![line.width, line.y, line.baseline, line.height].every(Number.isFinite) || line.width < 0 || line.height <= 0 || line.y < 0 || line.baseline < line.y) throw new RangeError("Text lines require finite coordinates, nonnegative advances and a baseline at or below their top.");
304
- const ink = line.outline;
305
- if (ink !== null && (!ink || ![ink.x, ink.y, ink.width, ink.height].every(Number.isFinite) || ink.width < 0 || ink.height < 0)) throw new RangeError("Text outlines must be null or finite coordinates with nonnegative dimensions.");
306
- let x = box.x + (box.width - line.width) * factor;
307
- if (ink) {
308
- const low = box.x + rasterPadding - ink.x, high = box.x + box.width - rasterPadding - ink.x - ink.width;
309
- if (low > high + 0.01) overflow = true;
310
- else x = Math.max(low, Math.min(x, high));
311
- shift += Math.max(0, bottom + rasterPadding - (box.y + line.baseline + shift + ink.y));
312
- }
313
- const y = box.y + line.y + shift, baseline = box.y + line.baseline + shift;
314
- const outline = ink ? { ...ink, x: x + ink.x, y: baseline + ink.y } : null;
315
- if (outline) bottom = outline.y + outline.height + rasterPadding;
316
- height = Math.max(height, y + line.height - box.y, outline ? bottom - box.y : 0);
317
- if (line.width > box.width + 0.01 || height > box.height + 0.01) overflow = true;
318
- placed.push({ x, y, baseline, height: line.height, width: line.width, outline });
319
- }
320
- return { alignment, rasterPadding, lines: placed, height, overflow };
321
- }
322
- function translateTextFit(fit, dy) {
323
- if (!dy || !fit.placement) return fit;
324
- return {
325
- ...fit,
326
- placement: {
327
- ...fit.placement,
328
- lines: fit.placement.lines.map((line) => ({
329
- ...line,
330
- y: line.y + dy,
331
- baseline: line.baseline + dy,
332
- outline: line.outline ? { ...line.outline, y: line.outline.y + dy } : line.outline
333
- }))
334
- }
335
- };
336
- }
337
- var OPFCompositionError = class extends Error {
338
- constructor(diagnostics, explanation) {
339
- super("Slide content does not fit its composition.");
340
- this.diagnostics = diagnostics;
341
- this.name = "OPFCompositionError";
342
- if (explanation) this.explanation = explanation;
343
- }
344
- diagnostics;
345
- code = "layout-overflow";
346
- explanation;
347
- };
348
- var fields = ["text", "items", "bullets", "image", "video", "chart", "table", "code", "metric", "quote", "timeline"];
349
- var headings = /* @__PURE__ */ new Set(["title", "subtitle", "tag"]);
350
- var COVER_LAYOUT_IDS = /* @__PURE__ */ new Set(["title", "title-subtitle"]);
351
- var rows = ["top", "middle", "bottom"];
352
- var columns = ["left", "center", "right"];
353
- var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
354
- var kind = (field) => field === "items" ? "list" : field === "bullets" ? "text" : field;
355
- var round = (value) => Math.round(value * 1e6) / 1e6 || value;
356
- var SLIDE_IMAGE_POSITIONS = ["background", "top", "bottom", "left", "right"];
357
- var SLIDE_IMAGE_BAND = 0.5;
358
- var assetSource = (value) => typeof value === "string" ? value : record2(value).src;
359
- var finite = (value, min, max) => typeof value === "number" && Number.isFinite(value) && value >= min && value <= max ? value : void 0;
360
- var pathNumber = (value) => String(round(value));
361
- var SLIDE_IMAGE_SHAPES = { rectangle: "rect", rounded: "roundRect", circle: "ellipse", hexagon: "hexagon" };
362
- function slideImageShape(kind2, box, cornerRadius = 1 / 6) {
363
- const { x, y, width: w, height: h } = box, r = x + w, b = y + h, ss = Math.min(w, h), n = pathNumber;
364
- const rect = `M${n(x)} ${n(y)}H${n(r)}V${n(b)}H${n(x)}Z`;
365
- if (kind2 === "rounded") {
366
- const adj = Math.round(Math.min(0.5, Math.max(0, cornerRadius)) * 1e5), radius = ss * adj / 1e5;
367
- const path = radius <= 0 ? rect : `M${n(x)} ${n(y + radius)}A${n(radius)} ${n(radius)} 0 0 1 ${n(x + radius)} ${n(y)}H${n(r - radius)}A${n(radius)} ${n(radius)} 0 0 1 ${n(r)} ${n(y + radius)}V${n(b - radius)}A${n(radius)} ${n(radius)} 0 0 1 ${n(r - radius)} ${n(b)}H${n(x + radius)}A${n(radius)} ${n(radius)} 0 0 1 ${n(x)} ${n(b - radius)}Z`;
368
- return { kind: kind2, preset: "roundRect", adjust: { adj }, path };
369
- }
370
- if (kind2 === "circle") {
371
- const cy = y + h / 2;
372
- return { kind: kind2, preset: "ellipse", adjust: {}, path: `M${n(x)} ${n(cy)}A${n(w / 2)} ${n(h / 2)} 0 1 1 ${n(r)} ${n(cy)}A${n(w / 2)} ${n(h / 2)} 0 1 1 ${n(x)} ${n(cy)}Z` };
373
- }
374
- if (kind2 === "hexagon") {
375
- const adj = 25e3, vf = 115470, a = Math.min(Math.max(adj, 0), 5e4 * w / ss);
376
- const x1 = ss * a / 1e5, x2 = r - x1, vc = y + h / 2, dy1 = h / 2 * vf / 1e5 * Math.sin(Math.PI / 3);
377
- return { kind: kind2, preset: "hexagon", adjust: { adj, vf }, path: `M${n(x)} ${n(vc)}L${n(x + x1)} ${n(vc - dy1)}L${n(x2)} ${n(vc - dy1)}L${n(r)} ${n(vc)}L${n(x2)} ${n(vc + dy1)}L${n(x + x1)} ${n(vc + dy1)}Z` };
378
- }
379
- return { kind: "rectangle", preset: "rect", adjust: {}, path: rect };
380
- }
381
- function resolveSlideImage(slide, layout, presentation, width, height, path, padding, scale, diagnostics) {
382
- const own = record2(slide.design), deck = record2(record2(presentation).design);
383
- const local = own.slideImage !== void 0;
384
- const configured = local ? own.slideImage : deck.slideImage;
385
- if (!configured || typeof configured !== "object" && typeof configured !== "string") return void 0;
386
- const treatment = typeof configured === "object" && !Array.isArray(configured) && "position" in configured ? record2(configured) : void 0;
387
- const alignment = typeof layout.slideImageAlignment === "string" ? layout.slideImageAlignment.toLowerCase() : void 0;
388
- const position = treatment ? treatment.position : SLIDE_IMAGE_POSITIONS.find((value2) => value2 === alignment) ?? "background";
389
- if (!SLIDE_IMAGE_POSITIONS.includes(position)) return void 0;
390
- const designSource = treatment ? treatment.src : configured;
391
- const root = slide.image, sameSource = root !== void 0 && designSource !== void 0 && assetSource(root) === assetSource(designSource);
392
- if (!local && layout.slideImage !== true && !sameSource) return void 0;
393
- const replacesContent = root !== void 0 && (designSource === void 0 || sameSource);
394
- const value = replacesContent ? root : designSource;
395
- const source = assetSource(value);
396
- if (typeof source !== "string" || !source) return void 0;
397
- const t = treatment ?? {};
398
- const fill = t.fill === "crop" || t.fill === "fit" ? t.fill : (own.imageFill ?? deck.imageFill) === "fit" ? "fit" : "crop";
399
- const share = finite(t.size, 0.1, 0.9) ?? SLIDE_IMAGE_BAND;
400
- const band = { width: round(width * share), height: round(height * share) };
401
- const region = position === "left" ? { x: 0, y: 0, width: band.width, height } : position === "right" ? { x: round(width - band.width), y: 0, width: band.width, height } : position === "top" ? { x: 0, y: 0, width, height: band.height } : position === "bottom" ? { x: 0, y: round(height - band.height), width, height: band.height } : { x: 0, y: 0, width, height };
402
- const designPath = local ? `${path}.design.slideImage` : "design.slideImage";
403
- const kind2 = Object.hasOwn(SLIDE_IMAGE_SHAPES, t.shape) ? t.shape : "rectangle";
404
- let box = t.inset === true ? { x: region.x + padding, y: region.y + padding, width: Math.max(scale, region.width - 2 * padding), height: Math.max(scale, region.height - 2 * padding) } : { ...region };
405
- const aspect = kind2 === "circle" ? 1 : finite(t.aspectRatio, Number.MIN_VALUE, 10);
406
- if (aspect) {
407
- const frameWidth = Math.min(box.width, box.height * aspect), frameHeight = frameWidth / aspect;
408
- box = { x: box.x + (box.width - frameWidth) / 2, y: box.y + (box.height - frameHeight) / 2, width: frameWidth, height: frameHeight };
409
- }
410
- box = { x: round(box.x), y: round(box.y), width: round(box.width), height: round(box.height) };
411
- const result = {
412
- path: designPath,
413
- sourcePath: replacesContent ? `${path}.image` : designPath,
414
- value,
415
- position,
416
- fill,
417
- region,
418
- box,
419
- replacesContent,
420
- shape: slideImageShape(kind2, box, finite(t.cornerRadius, 0, 0.5))
421
- };
422
- if (typeof t.alt === "string") result.alt = t.alt;
423
- const border = record2(t.border), borderWidth = finite(border.width, 0, 64);
424
- if (border.color !== void 0 && borderWidth) result.border = { color: border.color, width: round(borderWidth * scale) };
425
- const opacity = finite(t.opacity, 0, 1);
426
- if (opacity !== void 0 && opacity < 1) result.opacity = opacity;
427
- if (t.recolor === "grayscale") result.recolor = { type: "grayscale" };
428
- else if (record2(t.recolor).dark !== void 0 && record2(t.recolor).light !== void 0) result.recolor = { type: "duotone", dark: t.recolor.dark, light: t.recolor.light };
429
- const overlay = record2(t.overlay), overlayOpacity = finite(overlay.opacity, 0, 1);
430
- if (overlay.color !== void 0 && overlayOpacity !== void 0) {
431
- const edge = ["top", "bottom", "left", "right"].includes(overlay.edge) ? overlay.edge : void 0;
432
- if (edge && kind2 !== "rectangle") diagnostics.push({ code: "unsupported-image-treatment", path: `${designPath}.overlay.edge`, message: "An edge overlay needs a rectangle frame; a band cannot follow a rounded, circular or hexagonal mask as one native shape. Remove edge or use shape rectangle." });
433
- else {
434
- const part = finite(overlay.size, 0.05, 1) ?? 0.3;
435
- const bandBox = !edge ? box : edge === "top" ? { ...box, height: round(box.height * part) } : edge === "bottom" ? { ...box, y: round(box.y + box.height * (1 - part)), height: round(box.height * part) } : edge === "left" ? { ...box, width: round(box.width * part) } : { ...box, x: round(box.x + box.width * (1 - part)), width: round(box.width * part) };
436
- result.overlay = { color: overlay.color, opacity: overlayOpacity, box: bandBox, shape: edge ? slideImageShape("rectangle", bandBox) : result.shape };
437
- }
438
- }
439
- return result;
440
- }
441
- var webUrl = /^https?:\/\/\S+$/i;
442
- function resolveSocialProfile(platform, value, records = [], owner = "organization") {
443
- const raw = String(value).trim().replace(/\s+/gu, " ");
444
- const display = (url2) => url2.replace(/^https:\/\//i, "");
445
- if (webUrl.test(raw)) return { text: display(raw), href: raw, resolved: false };
446
- const platformRecord = records.find((item) => item?.id === platform);
447
- const base = typeof platformRecord?.baseUrl === "string" && webUrl.test(platformRecord.baseUrl) ? `${platformRecord.baseUrl.replace(/\/+$/u, "")}/{handle}` : void 0;
448
- const pattern = (owner === "organization" ? platformRecord?.companyUrlPattern : void 0) ?? platformRecord?.profileUrlPattern ?? base;
449
- const prefix = platformRecord?.handlePrefix ?? "";
450
- const handle = prefix && raw.startsWith(prefix) ? raw.slice(prefix.length) : raw;
451
- if (!handle || typeof pattern !== "string" || !pattern.includes("{handle}")) return { text: raw, resolved: false };
452
- const url = pattern.split("{handle}").join(handle);
453
- if (!webUrl.test(url)) return { text: raw, resolved: false };
454
- return { text: display(url), href: encodeURI(url), resolved: true };
455
- }
456
- function layoutFurniture(input, options = {}) {
457
- const slide = record2(input), width = options.width ?? 1280, height = options.height ?? 720, scale = Math.min(width, height) / 720;
458
- const settings = { ...record2(record2(options.layout).composition), ...record2(slide.composition) };
459
- assertComposition(settings);
460
- const minimum = (settings.minFontSize ?? 16) * scale, size = Math.max(13 * scale, minimum), padding = (options.textRasterPadding ?? 2) * scale;
461
- if (![width, height, scale, minimum, padding].every(Number.isFinite) || width <= 0 || height <= 0 || minimum <= 0 || padding < 0) throw new RangeError("Furniture requires finite positive dimensions and nonnegative raster padding.");
462
- const number = options.slideNumber ?? (options.slideIndex ?? 0) + 1;
463
- if (!Number.isSafeInteger(number) || number < 1) throw new RangeError("Displayed slide number must be a positive safe integer.");
464
- const presentationSlides = options.presentation?.slides, slideCount = options.slideCount ?? (Array.isArray(presentationSlides) && presentationSlides.length ? presentationSlides.length : void 0);
465
- if (slideCount !== void 0 && (!Number.isSafeInteger(slideCount) || slideCount < 1)) throw new RangeError("Displayed slide count must be a positive safe integer.");
466
- if (options.date !== void 0 && (typeof options.date !== "string" || !parseIsoDate(options.date))) throw new RangeError("The furniture date option must be an ISO YYYY-MM-DD calendar date.");
467
- const outlines = options.textMeasurement?.outlineBounds !== void 0, parts = [], diagnostics = [];
468
- const sourceRoot = `slides.${options.slideIndex ?? 0}`, organizations = Array.isArray(options.presentation?.organization) ? options.presentation.organization : [options.presentation?.organization];
469
- const primaryIndex = organizations.findIndex((item) => record2(item).role === "primary"), organizationIndex = primaryIndex >= 0 ? primaryIndex : organizations.findIndex(Boolean);
470
- const organization = record2(organizations[organizationIndex]), organizationRoot = Array.isArray(options.presentation?.organization) ? `organization.${organizationIndex}` : "organization", organizationPath = `${organizationRoot}.name`;
471
- const inlinePlatforms = record2(record2(options.presentation?.catalogs).socialPlatforms).records, platformRecords = [...Array.isArray(inlinePlatforms) ? inlinePlatforms : [], ...options.socialPlatforms ?? []];
472
- const fontFamily = options.fonts?.body ?? "sans-serif";
473
- let headerBottom = 0, footerTop = height, configured = false;
474
- const error = (path, message, code = "text-overflow") => diagnostics.push({ code, path, message });
475
- for (const kind2 of ["header", "footer"]) {
476
- const local = record2(slide.design)[kind2] !== void 0, value = local ? record2(slide.design)[kind2] : options.presentation?.design?.[kind2];
477
- if (value === void 0 || value === false) continue;
478
- const root = local ? `${sourceRoot}.design.${kind2}` : `design.${kind2}`;
479
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Header/footer content must be an object or false.");
480
- configured = true;
481
- const zones = [];
482
- for (const [index, zone] of ["left", "center", "right"].entries()) {
483
- const content = record2(record2(value)[zone]), path = `${root}.${zone}`, x = width * (0.07 + index * 0.3), zoneWidth = width * 0.26, zoneParts = [];
484
- let y = 0;
485
- const add = (field, text, generated = false, sourcePath, extras = {}) => {
486
- if (text === void 0) return;
487
- if (typeof text !== "string") throw new TypeError(`Furniture field ${path}.${field} requires string content.`);
488
- const partPath = `${path}.${field}`, style = resolveTextStyle({ fontFamily, fontWeight: 400, italic: false, path: partPath }, options.textMeasurement);
489
- const fit = fitText(text, { width: Math.max(Number.MIN_VALUE, zoneWidth - (outlines ? 2 * padding : 0)), height: Number.MAX_VALUE }, size, size, textWidthMeasurer(style, options.textMeasurement));
490
- const ink = fit.sourceLines.map((line, index2) => {
491
- let outline = null;
492
- if (outlines) {
493
- for (const segment of line.segments) if (segment.kind === "text") {
494
- const bounds = measureTextOutline(text.slice(segment.start, segment.end), size, style, options.textMeasurement);
495
- if (bounds) {
496
- const next = { ...bounds, x: bounds.x + segment.x };
497
- if (!outline) outline = next;
498
- else {
499
- const right = Math.max(outline.x + outline.width, next.x + next.width), bottom = Math.max(outline.y + outline.height, next.y + next.height);
500
- outline.x = Math.min(outline.x, next.x);
501
- outline.y = Math.min(outline.y, next.y);
502
- outline.width = right - outline.x;
503
- outline.height = bottom - outline.y;
504
- }
505
- }
506
- }
507
- }
508
- return { width: line.width, y: index2 * fit.lineHeight, baseline: size + index2 * fit.lineHeight, height: fit.lineHeight, outline };
509
- });
510
- const natural = outlines ? placeTextLines(ink, { x, y, width: zoneWidth, height: Number.MAX_VALUE }, zone, padding) : void 0;
511
- const partHeight = natural?.height ?? fit.sourceLines.length * fit.lineHeight;
512
- if (!Number.isFinite(partHeight)) throw new RangeError("Furniture exceeds finite layout coordinates.");
513
- const box = { x, y, width: zoneWidth, height: Math.max(scale, partHeight) }, placement = outlines ? placeTextLines(ink, box, zone, padding) : void 0;
514
- const accepted = { ...fit, ...placement ? { placement } : {}, overflow: fit.overflow || !!placement?.overflow };
515
- zoneParts.push({ type: "text", kind: kind2, zone, field, path: partPath, sourcePath: sourcePath ?? (!generated ? partPath : void 0), generated, text, style, requestedFontSize: 13 * scale, minFontSize: minimum, box, alignment: zone, fit: accepted, ...extras.fields?.length ? { fields: extras.fields } : {}, ...extras.links?.length ? { links: extras.links } : {} });
516
- if (accepted.overflow) error(partPath, "Repeated text exceeds its zone at the selected readability floor; change the furniture or slide design.");
517
- y += box.height;
518
- };
519
- if (content.image !== void 0) {
520
- const box = { x, y, width: zoneWidth, height: Math.max(32 * scale, Math.min(height * 0.05, 72 * scale)) };
521
- zoneParts.push({ type: "image", kind: kind2, zone, field: "image", path: `${path}.image`, sourcePath: `${path}.image`, generated: false, image: content.image, box, alignment: zone });
522
- y += box.height;
523
- }
524
- add("text", content.text);
525
- if (content.organization === true) {
526
- if (typeof organization.name === "string") add("organization", organization.name, true, organizationPath);
527
- else error(`${path}.organization`, "Generated organization name needs a named organization in the presentation.", "unresolved-content");
528
- }
529
- if (content.socials === true) {
530
- const links = Object.entries(record2(organization.socials)).filter(([, value2]) => typeof value2 === "string" && value2.trim()).map(([platform, value2]) => ({ platform, sourcePath: `${organizationRoot}.socials.${platform}`, ...resolveSocialProfile(platform, value2, platformRecords, "organization") }));
531
- if (links.length) add("socials", links.map((link) => link.text).join("\n"), true, `${organizationRoot}.socials`, { links });
532
- else error(`${path}.socials`, "Generated social profiles need a primary organization with socials.", "unresolved-content");
533
- }
534
- if (content.section === true) {
535
- if (typeof slide.section === "string") add("section", slide.section, true, `${sourceRoot}.section`);
536
- else error(`${path}.section`, "Generated section needs a literal slide section.", "unresolved-content");
537
- }
538
- for (const setting of ["slideNumberFormat", "dateFormat"]) if (content[setting] !== void 0 && typeof content[setting] !== "string") throw new TypeError(`Furniture setting ${path}.${setting} requires a string.`);
539
- if (content.slideNumber === true) {
540
- const resolved = formatSlideNumber(content.slideNumberFormat ?? DEFAULT_SLIDE_NUMBER_FORMAT, number, slideCount);
541
- if ("error" in resolved) error(`${path}.slideNumberFormat`, resolved.error, "unresolved-content");
542
- else add("slideNumber", resolved.text, true, void 0, { fields: resolved.fields });
543
- }
544
- if (content.date === true) {
545
- const format = content.dateFormat ?? DEFAULT_FURNITURE_DATE_FORMAT;
546
- if (options.date === void 0) error(`${path}.date`, "A current date needs a host-supplied ISO date option. For fixed content use a literal date, or an ISO date with dateFormat.", "unresolved-content");
547
- else {
548
- const resolved = formatFurnitureDate(options.date, format);
549
- if ("error" in resolved) error(`${path}.dateFormat`, resolved.error, "unresolved-content");
550
- else add("date", resolved.text, true, void 0, { fields: [{ type: "date", start: 0, end: resolved.text.length, format }] });
551
- }
552
- } else if (typeof content.date === "string") {
553
- if (content.dateFormat === void 0) add("date", content.date);
554
- else {
555
- const resolved = formatFurnitureDate(content.date, content.dateFormat);
556
- if ("error" in resolved) error(`${path}.${parseIsoDate(content.date) ? "dateFormat" : "date"}`, resolved.error, "unresolved-content");
557
- else add("date", resolved.text, true, `${path}.date`);
558
- }
559
- }
560
- zones.push(zoneParts);
561
- }
562
- const tallest = Math.max(0, ...zones.map((zone) => zone.reduce((sum, part) => sum + part.box.height, 0)));
563
- if (!tallest) continue;
564
- const top = kind2 === "header" ? height * 0.025 : Math.max(height * 0.025, height - height * 0.04 - tallest);
565
- if (kind2 === "header") headerBottom = top + tallest;
566
- else footerTop = top;
567
- for (const zone of zones) for (const part of zone) {
568
- part.box.y += top;
569
- if (part.type === "text" && part.fit.placement) for (const line of part.fit.placement.lines) {
570
- line.y += top;
571
- line.baseline += top;
572
- if (line.outline) line.outline.y += top;
573
- }
574
- if (part.box.y + part.box.height > height - height * 0.04 + 0.01) error(part.path, "Repeated header/footer content extends beyond the usable slide height. Change its content or design before pagination.");
575
- parts.push(part);
576
- }
577
- }
578
- if (headerBottom > footerTop + 0.01) error(parts.find((part) => part.kind === "footer")?.path ?? sourceRoot, "Header and footer content overlap; repeated content cannot be repaired by splitting body content.");
579
- return { algorithm: "furniture-flow-v2", configured, textMeasurement: options.textMeasurement ? "provided" : "estimated", textOutlines: outlines ? "provided" : "unavailable", parts, headerBottom, footerTop, diagnostics, overflow: diagnostics.length > 0 };
580
- }
581
- var textSegments = new Intl.Segmenter("und", { granularity: "grapheme" });
582
- function measureText(text, fontSize) {
583
- let units = 0;
584
- for (const character of text) {
585
- if (/\p{Mark}|\u200d|\ufe0f/u.test(character)) continue;
586
- units += character === " " ? 0.32 : /[A-Z0-9]/.test(character) ? 0.62 : character.codePointAt(0) > 11904 ? 1 : 0.54;
587
- }
588
- return units * fontSize;
589
- }
590
- function wrapText(text, width, fontSize, measure = measureText) {
591
- return fitSourceText(text, { width, height: Number.MAX_VALUE }, fontSize, fontSize, 1, measure, true).lines;
592
- }
593
- function fitText(text, box, requestedSize = 25, minFontSize = 16, measure = measureText) {
594
- if (![box.width, box.height, requestedSize, minFontSize].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || requestedSize <= 0 || minFontSize <= 0) {
595
- throw new RangeError("Text dimensions and font sizes must be finite and positive.");
596
- }
597
- return fitSourceText(text, box, Math.max(requestedSize, minFontSize), minFontSize, 1, measure, true);
598
- }
599
- function fitAtSizes(layout, requested, minimum, step = 1) {
600
- const start = Math.max(requested, minimum);
601
- if (![start, minimum, step].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("Readable font sizes and fitting steps must be finite and positive.");
602
- for (let trial = 0; trial <= 64; trial++) {
603
- const size = trial === 64 ? minimum : Math.max(minimum, start - trial * step), result = layout(size);
604
- if (!result.overflow || size === minimum) return result;
605
- }
606
- throw new Error("Text fitting did not evaluate its bounded floor trial.");
607
- }
608
- function layoutQuote(value, box, options = {}) {
609
- const shorthand = typeof value === "string";
610
- const quote = shorthand ? { text: value } : value;
611
- if (!quote || Array.isArray(quote) || typeof quote.text !== "string" || [quote.attribution, quote.source].some((field) => field !== void 0 && typeof field !== "string")) {
612
- throw new TypeError("Quote content must be a string or a text object with optional string attribution/source.");
613
- }
614
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale;
615
- if (![box.x, box.y, box.width, box.height, scale, minimum].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || scale <= 0 || minimum <= 0) {
616
- throw new RangeError("Quote dimensions, scale and minimum font size must be finite and positive.");
617
- }
618
- if (options.overflow !== void 0 && !["warn", "error"].includes(options.overflow)) throw new RangeError("Invalid quote overflow policy.");
619
- const path = options.path ?? "quote";
620
- const diagnostics = [], parts = [];
621
- const report = (reason, diagnosticPath, roles, message) => diagnostics.push({ code: "text-overflow", reason, path: diagnosticPath, parts: roles, message });
622
- const source = (sourcePath, text, outputStart) => ({ path: sourcePath, start: 0, end: text.length, outputStart, outputEnd: outputStart + text.length });
623
- const add = (role, text, sources, area, fontSize, fontFamily, fontWeight, partPath) => {
624
- const requestedStyle = { fontFamily, fontWeight, italic: false, path: partPath };
625
- const style = resolveTextStyle({ ...requestedStyle }, options.textMeasurement);
626
- const requestedFontSize = Math.max(fontSize * scale, minimum);
627
- const part = { role, path: partPath, text, sources, box: area, requestedFontSize, minFontSize: minimum, requestedStyle, style };
628
- parts.push(part);
629
- return part;
630
- };
631
- let footer = "";
632
- const footerSources = [];
633
- for (const field of ["attribution", "source"]) {
634
- const text = quote[field];
635
- if (!text) continue;
636
- if (footer) footer += " - ";
637
- footerSources.push(source(`${path}.${field}`, text, footer.length));
638
- footer += text;
639
- }
640
- const bodyPath = shorthand ? path : `${path}.text`;
641
- const body = add(
642
- "body",
643
- `"${quote.text}"`,
644
- [source(bodyPath, quote.text, 1)],
645
- { x: box.x + 18, y: box.y + 18, width: box.width - 36, height: box.height - (footer ? 94 : 36) },
646
- 28,
647
- options.fonts?.heading ?? "sans-serif",
648
- 600,
649
- bodyPath
650
- );
651
- const attribution = footer ? add(
652
- "footer",
653
- footer,
654
- footerSources,
655
- { x: box.x + 18, y: box.y + box.height - 58, width: box.width - 36, height: 40 },
656
- 17,
657
- options.fonts?.body ?? "sans-serif",
658
- 500,
659
- path
660
- ) : void 0;
661
- const usable = (area) => [area.x, area.y, area.width, area.height].every(Number.isFinite) && area.width > 0 && area.height > 0;
662
- const fit = (part, area, size = part.requestedFontSize, floor = minimum) => usable(area) ? fitText(part.text, area, size, floor, textWidthMeasurer(part.style, options.textMeasurement)) : void 0;
663
- const inner = { x: box.x + 18, y: box.y + 18, width: box.width - 36, height: box.height - 36 };
664
- if (!attribution) body.fit = fit(body, body.box);
665
- else if (usable(inner) && inner.height > 18) {
666
- const available = inner.height - 18;
667
- const preferredBody = fit(body, inner, body.requestedFontSize, body.requestedFontSize);
668
- const minimumBody = body.requestedFontSize === minimum ? preferredBody : fit(body, inner, minimum, minimum);
669
- const preferredBodyHeight = preferredBody.lines.length * preferredBody.lineHeight;
670
- const minimumBodyHeight = minimumBody.lines.length * minimumBody.lineHeight;
671
- let selected;
672
- for (const size of /* @__PURE__ */ new Set([attribution.requestedFontSize, minimum])) {
673
- const natural = fit(attribution, inner, size, size);
674
- const naturalHeight = natural.lines.length * natural.lineHeight;
675
- const preferredHeight = Math.max(40, naturalHeight);
676
- const bodyReservation = Math.min(minimumBodyHeight, Math.max(minimumBody.lineHeight, available - natural.lineHeight));
677
- const footerHeight = preferredBodyHeight + preferredHeight <= available + 0.01 ? preferredHeight : Math.min(naturalHeight, Math.max(0, available - bodyReservation));
678
- const bodyBox = { ...inner, height: available - footerHeight };
679
- const footerBox = { ...inner, y: inner.y + inner.height - footerHeight, height: footerHeight };
680
- const bodyFit = fit(body, bodyBox);
681
- const footerFit = usable(footerBox) ? { ...natural, overflow: natural.overflow || naturalHeight > footerHeight + 0.01 } : void 0;
682
- const overflow = !bodyFit || !footerFit || bodyFit.overflow || footerFit.overflow;
683
- const score = (body.requestedFontSize - (bodyFit?.fontSize ?? minimum) + attribution.requestedFontSize - size) / scale;
684
- if (!selected || !overflow && (selected.overflow || score < selected.score) || overflow && selected.overflow) {
685
- selected = { bodyBox, footerBox, bodyFit, footerFit, score, overflow };
686
- }
687
- }
688
- if (selected) {
689
- body.box = selected.bodyBox;
690
- body.fit = selected.bodyFit;
691
- attribution.box = selected.footerBox;
692
- attribution.fit = selected.footerFit;
693
- }
694
- }
695
- for (const part of parts) {
696
- const area = part.box;
697
- if (!part.fit) {
698
- report("invalid-part-box", part.path, [part.role], `Quote ${part.role} has no usable space after its insets; increase the cell size or change the arrangement.`);
699
- } else if (part.fit.overflow) {
700
- report("text-fit", part.path, [part.role], `Quote ${part.role} exceeds its available space at the readability floor; increase its space or change the arrangement.`);
701
- }
702
- if (usable(area) && (area.x < box.x || area.y < box.y || area.x + area.width > box.x + box.width + 0.01 || area.y + area.height > box.y + box.height + 0.01)) {
703
- report("part-outside-cell", part.path, [part.role], `Quote ${part.role} extends outside its cell; increase the cell size or change the arrangement.`);
704
- }
705
- }
706
- if (body?.fit && attribution?.fit && body.box.y + body.fit.lines.length * body.fit.lineHeight > attribution.box.y + 0.01 && attribution.box.y + attribution.fit.lines.length * attribution.fit.lineHeight > body.box.y + 0.01) {
707
- report("part-overlap", path, ["body", "footer"], "Quote body and footer line boxes overlap; do not accept this layout without more space.");
708
- }
709
- if (diagnostics.length && options.overflow === "error") throw new OPFCompositionError(diagnostics);
710
- return { algorithm: "quote-flow-v1", textMeasurement: options.textMeasurement ? "provided" : "estimated", parts, diagnostics, overflow: diagnostics.length > 0 };
711
- }
712
- function layoutMetric(value, box, options = {}) {
713
- const scalar = typeof value === "string" || typeof value === "number";
714
- const metric = scalar ? { value } : value;
715
- const isValue = (input) => typeof input === "string" || typeof input === "number" && Number.isFinite(input);
716
- if (!metric || Array.isArray(metric) || !isValue(metric.value) || [metric.label, metric.description, metric.unit].some((field) => field !== void 0 && typeof field !== "string") || metric.delta !== void 0 && !isValue(metric.delta) || metric.trend !== void 0 && !["up", "down", "flat"].includes(metric.trend)) {
717
- throw new TypeError("Metric content requires a finite numeric or string value and schema-valid display metadata.");
718
- }
719
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale;
720
- const rasterPadding = (options.textRasterPadding ?? 1) * scale, hasOutlines = options.textMeasurement?.outlineBounds !== void 0;
721
- if (![box.x, box.y, box.width, box.height, box.x + box.width, box.y + box.height, scale, minimum, Math.max(76 * scale, minimum) * 1.22].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || scale <= 0 || minimum <= 0) {
722
- throw new RangeError("Metric dimensions, scale and minimum font size must be finite and positive.");
723
- }
724
- if (!Number.isFinite(rasterPadding) || rasterPadding < 0) throw new RangeError("Metric raster padding must be finite and nonnegative.");
725
- if (options.overflow !== void 0 && !["warn", "error"].includes(options.overflow)) throw new RangeError("Invalid metric overflow policy.");
726
- const alignment = options.align ?? "left";
727
- if (!["left", "center", "right"].includes(alignment)) throw new RangeError("Invalid metric alignment.");
728
- const sourcePath = options.path ?? "metric", parts = [], diagnostics = [];
729
- for (const role of ["value", "unit", "label", "description", "delta", "trend"]) {
730
- const sourceValue = metric[role];
731
- if (sourceValue === void 0) continue;
732
- const text = String(sourceValue), path = scalar ? sourcePath : `${sourcePath}.${role}`;
733
- const nominal = role === "value" ? Math.min(76 * scale, box.height * 0.28) : (role === "description" ? 20 : role === "trend" ? 18 : 23) * scale;
734
- const requestedStyle = { fontFamily: (role === "value" ? options.fonts?.heading : options.fonts?.body) ?? "sans-serif", fontWeight: role === "value" ? 800 : role === "description" ? 400 : 500, italic: false, path };
735
- const part = {
736
- role,
737
- path,
738
- text,
739
- sources: [{ path, value: sourceValue, start: 0, end: text.length }],
740
- visible: role === "value" || text.length > 0,
741
- linePositions: [],
742
- box: { ...box, height: 0 },
743
- requestedFontSize: Math.max(nominal, minimum),
744
- minFontSize: minimum,
745
- requestedStyle,
746
- style: resolveTextStyle({ ...requestedStyle }, options.textMeasurement)
747
- };
748
- if (!part.visible) part.fit = { lines: [], sourceLines: [], fontSize: part.requestedFontSize, lineHeight: part.requestedFontSize * 1.22, tabSize: 4, tabWidth: 0, overflow: false };
749
- parts.push(part);
750
- }
751
- const primary = parts[0], metadata = parts.filter((part) => part !== primary && part.visible), unit = metadata.find((part) => part.role === "unit");
752
- const usable = (area) => [area.x, area.y, area.width, area.height, area.x + area.width, area.y + area.height].every(Number.isFinite) && area.width > 0 && area.height > 0;
753
- const occupied = (fit) => fit.placement?.height ?? fit.lines.length * fit.lineHeight;
754
- const gap = 8 * scale, primaryGap = 12 * scale;
755
- const lineInk = (part, fit) => fit.sourceLines.map((line, index) => {
756
- let outline = null;
757
- for (const segment of line.segments) {
758
- if (segment.kind !== "text") continue;
759
- const bounds = measureTextOutline(part.text.slice(segment.start, segment.end), fit.fontSize, part.style, options.textMeasurement);
760
- if (!bounds) continue;
761
- const next = { ...bounds, x: segment.x + bounds.x };
762
- if (!outline) outline = next;
763
- else {
764
- const right = Math.max(outline.x + outline.width, next.x + next.width), bottom = Math.max(outline.y + outline.height, next.y + next.height);
765
- outline.x = Math.min(outline.x, next.x);
766
- outline.y = Math.min(outline.y, next.y);
767
- outline.width = right - outline.x;
768
- outline.height = bottom - outline.y;
769
- }
770
- }
771
- return { width: line.width, y: index * fit.lineHeight, baseline: fit.fontSize + index * fit.lineHeight, height: fit.lineHeight, outline };
772
- });
773
- const place = (part, fit, width) => {
774
- if (!hasOutlines) return fit;
775
- const placement = placeTextLines(lineInk(part, fit), { x: 0, y: 0, width, height: Number.MAX_VALUE }, alignment, rasterPadding);
776
- return { ...fit, placement, overflow: placement.overflow };
777
- };
778
- const naturalWidth = (part, fit) => {
779
- if (!hasOutlines) return Math.max(...fit.sourceLines.map((line) => line.width));
780
- const width = Math.max(...lineInk(part, fit).map((line) => Math.max(line.width, line.outline ? line.outline.x + line.outline.width : 0) - Math.min(0, line.outline?.x ?? 0)));
781
- return width > 0 ? width + 2 * rasterPadding : 0;
782
- };
783
- const cache = /* @__PURE__ */ new Map();
784
- const measure = (part, size, width) => {
785
- const key = JSON.stringify([part.role, size, width]);
786
- let fit = cache.get(key);
787
- if (!fit) {
788
- fit = fitCodeText(part.text, { ...box, width: hasOutlines ? Math.max(Number.MIN_VALUE, width - 2 * rasterPadding) : width }, size, size, scale, textWidthMeasurer(part.style, options.textMeasurement));
789
- fit = place(part, fit, width);
790
- if (!Number.isFinite(occupied(fit))) throw new RangeError("Metric text layout exceeds finite coordinates.");
791
- cache.set(key, fit);
792
- }
793
- return fit;
794
- };
795
- const fitValue = (area, singleLine) => {
796
- if (!usable(area)) return void 0;
797
- for (let step = 0; step <= 76; step++) {
798
- const size = Math.max(minimum, primary.requestedFontSize - step * scale);
799
- const natural = measure(primary, size, area.width);
800
- const fit = { ...natural, overflow: !!natural.placement?.overflow || singleLine && natural.lines.length !== 1 || occupied(natural) > area.height + 0.01 || natural.sourceLines.some((line) => line.width > area.width + 0.01) };
801
- if (!fit.overflow || size === minimum) return fit;
802
- }
803
- };
804
- let selected, attempts = 0;
805
- const reductions = Math.min(23, Math.ceil(Math.max(0, ...metadata.map((part) => (part.requestedFontSize - minimum) / scale))));
806
- for (let reduction = 0; reduction <= reductions; reduction++) {
807
- const measured = new Map(metadata.map((part) => [part, measure(part, Math.max(minimum, part.requestedFontSize - reduction * scale), box.width)]));
808
- const measuredUnit = unit ? measured.get(unit) : void 0;
809
- const unitWidth = unit && measuredUnit ? naturalWidth(unit, measuredUnit) : 0;
810
- const unitFit = unit && measuredUnit && unitWidth > 0 ? place(unit, measuredUnit, unitWidth) : measuredUnit;
811
- const inline = unitFit?.lines.length === 1 && unitWidth > 0 && unitWidth <= box.width * 0.35 && unitWidth + gap < box.width;
812
- for (const arrangement of inline ? ["inline-unit", "stacked"] : ["stacked"]) {
813
- attempts++;
814
- const tail = metadata.filter((part) => arrangement !== "inline-unit" || part !== unit);
815
- const tailHeight = tail.reduce((sum, part) => sum + occupied(measured.get(part)), 0) + Math.max(0, tail.length - 1) * gap;
816
- if (!Number.isFinite(tailHeight)) throw new RangeError("Metric metadata exceeds finite coordinates.");
817
- const area = { ...box, width: arrangement === "inline-unit" ? box.width - unitWidth - gap : box.width, height: box.height - tailHeight - (tail.length ? primaryGap : 0) };
818
- const valueFit = fitValue(area, arrangement === "inline-unit"), valueHeight = valueFit ? occupied(valueFit) : 0;
819
- const allocations = [];
820
- let primaryHeight = valueHeight;
821
- if (arrangement === "inline-unit" && unit && unitFit && valueFit) {
822
- const valueBaseline = valueFit.placement?.lines.at(-1)?.baseline ?? valueHeight - valueFit.lineHeight + valueFit.fontSize;
823
- const unitBaseline = unitFit.placement?.lines.at(-1)?.baseline ?? occupied(unitFit) - unitFit.lineHeight + unitFit.fontSize;
824
- const valueY = box.y + Math.max(0, unitBaseline - valueBaseline), unitY = box.y + Math.max(0, valueBaseline - unitBaseline);
825
- const measuredWidth = naturalWidth(primary, valueFit);
826
- const valueWidth = Math.min(area.width, measuredWidth || valueFit.fontSize);
827
- allocations.push({ part: primary, box: { ...area, y: valueY, width: valueWidth, height: valueHeight }, fit: place(primary, valueFit, valueWidth) });
828
- allocations.push({ part: unit, box: { x: box.x + valueWidth + gap, y: unitY, width: unitWidth, height: occupied(unitFit) }, fit: { ...unitFit, overflow: !!unitFit.placement?.overflow } });
829
- primaryHeight = Math.max(valueY - box.y + valueHeight, unitY - box.y + occupied(unitFit));
830
- } else allocations.push({ part: primary, box: valueFit ? { ...area, height: valueHeight } : area, fit: valueFit });
831
- let y = box.y + primaryHeight + (tail.length ? primaryGap : 0);
832
- for (const part of tail) {
833
- const natural = measured.get(part), height = occupied(natural);
834
- allocations.push({ part, box: { ...box, y, height }, fit: { ...natural, overflow: !!natural.placement?.overflow || natural.sourceLines.some((line) => line.width > box.width + 0.01) } });
835
- y += height + gap;
836
- }
837
- const overflow = !valueFit || valueFit.overflow || arrangement === "inline-unit" && valueFit.lines.length !== 1 || primaryHeight > area.height + 0.01 || allocations.some(({ box: area2, fit }) => !fit || fit.overflow || !usable(area2) || area2.y + area2.height > box.y + box.height + 0.01);
838
- const score = allocations.reduce((sum, { part, fit }) => sum + (part.requestedFontSize - (fit?.fontSize ?? minimum)) / scale, 0);
839
- const candidate = { arrangement, allocations, score, overflow };
840
- if (!selected || !overflow && (selected.overflow || score < selected.score) || overflow && selected.overflow) selected = candidate;
841
- }
842
- if (selected && !selected.overflow && selected.score === 0) break;
843
- }
844
- for (const allocation of selected.allocations) {
845
- allocation.part.box = allocation.box;
846
- allocation.part.fit = allocation.fit;
847
- }
848
- const alignmentFactor = alignment === "center" ? 0.5 : alignment === "right" ? 1 : 0;
849
- if (selected.arrangement === "inline-unit" && unit) {
850
- const offset = (box.width - (unit.box.x + unit.box.width - box.x)) * alignmentFactor;
851
- primary.box.x += offset;
852
- unit.box.x += offset;
853
- }
854
- for (const part of parts) if (part.fit) {
855
- if (part.fit.placement) {
856
- part.fit = { ...part.fit, placement: { ...part.fit.placement, lines: part.fit.placement.lines.map((line) => ({ ...line, x: line.x + part.box.x, y: line.y + part.box.y, baseline: line.baseline + part.box.y, outline: line.outline ? { ...line.outline, x: line.outline.x + part.box.x, y: line.outline.y + part.box.y } : null })) } };
857
- part.linePositions = part.fit.placement.lines.map(({ x, baseline }) => ({ x, baseline }));
858
- } else part.linePositions = part.fit.sourceLines.map((line, index) => ({
859
- x: part.box.x + (part.box.width - line.width) * alignmentFactor,
860
- baseline: part.box.y + part.fit.fontSize + index * part.fit.lineHeight
861
- }));
862
- }
863
- const report = (reason, part, roles, message) => diagnostics.push({ code: "text-overflow", reason, path: part.path, parts: roles, message });
864
- for (const part of parts.filter((part2) => part2.visible)) {
865
- if (!part.fit || !usable(part.box)) report("invalid-part-box", part, [part.role], `Metric ${part.role} has no usable space after metadata; increase the cell or change the arrangement.`);
866
- else if (part.fit.overflow) report("text-fit", part, [part.role], `Metric ${part.role} exceeds its space at the readability floor; increase the cell or change the arrangement.`);
867
- if (usable(part.box) && (part.box.x < box.x || part.box.y < box.y || part.box.x + part.box.width > box.x + box.width + 0.01 || part.box.y + part.box.height > box.y + box.height + 0.01)) {
868
- report("part-outside-cell", part, [part.role], `Metric ${part.role} extends outside its cell; increase the cell or change the arrangement.`);
869
- }
870
- }
871
- const visible = parts.filter((part) => part.visible && part.fit);
872
- for (const [index, first] of visible.entries()) for (const second of visible.slice(index + 1)) {
873
- if (first.box.x < second.box.x + second.box.width - 0.01 && second.box.x < first.box.x + first.box.width - 0.01 && first.box.y < second.box.y + occupied(second.fit) - 0.01 && second.box.y < first.box.y + occupied(first.fit) - 0.01) {
874
- report("part-overlap", first, [first.role, second.role], "Metric part line boxes overlap; do not accept this layout without more space.");
875
- }
876
- }
877
- if (diagnostics.length && options.overflow === "error") throw new OPFCompositionError(diagnostics);
878
- return { algorithm: "metric-flow-v1", alignment, textMeasurement: options.textMeasurement ? "provided" : "estimated", arrangement: selected.arrangement, attempts, parts, diagnostics, overflow: diagnostics.length > 0 };
879
- }
880
- function layoutTimeline(value, box, options = {}) {
881
- const shorthand = Array.isArray(value), timeline = shorthand ? { events: value } : value;
882
- if (!timeline || !Array.isArray(timeline.events) || !timeline.events.length || [timeline.name, timeline.description].some((field) => field !== void 0 && typeof field !== "string") || timeline.events.some((event) => !event || typeof event.what !== "string" || [event.when, event.description].some((field) => field !== void 0 && typeof field !== "string"))) {
883
- throw new TypeError("Timeline content requires ordered events with string labels and optional string metadata.");
884
- }
885
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale, padding = (options.textRasterPadding ?? 1) * scale;
886
- if (![box.x, box.y, box.width, box.height, scale, minimum, padding].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || scale <= 0 || minimum <= 0 || padding < 0) throw new RangeError("Timeline dimensions, scale and minimum must be positive, with finite nonnegative raster padding.");
887
- if (options.overflow !== void 0 && !["warn", "error"].includes(options.overflow)) throw new RangeError("Invalid timeline overflow policy.");
888
- const outlines = options.textMeasurement?.outlineBounds !== void 0;
889
- if (outlines && typeof options.textMeasurement?.outlineBounds !== "function") throw new TypeError("Text outline provider must be a function.");
890
- const path = options.path ?? "timeline", eventPath = (index) => shorthand ? `${path}.${index}` : `${path}.events.${index}`;
891
- const fonts = resolveFontFamilies(options.fonts), source = [];
892
- const add = (role, text, partPath, size, weight, eventIndex) => {
893
- if (text === void 0) return;
894
- const requestedStyle = { fontFamily: fonts.body, fontWeight: weight, italic: false, path: partPath };
895
- source.push({ role, eventIndex, path: partPath, text, sources: [{ path: partPath, start: 0, end: text.length }], box: { ...box }, alignment: "center", requestedFontSize: Math.max(size * scale, minimum), minFontSize: minimum, requestedStyle, style: resolveTextStyle({ ...requestedStyle }, options.textMeasurement) });
896
- };
897
- add("name", timeline.name, `${path}.name`, 24, 700);
898
- add("description", timeline.description, `${path}.description`, 18, 400);
899
- timeline.events.forEach((event, index) => {
900
- add("when", event.when, `${eventPath(index)}.when`, 16, 500, index);
901
- add("what", event.what, `${eventPath(index)}.what`, 16, 500, index);
902
- add("event-description", event.description, `${eventPath(index)}.description`, 16, 500, index);
903
- });
904
- const metadata = [], events = timeline.events.map(() => []);
905
- for (const part of source) if (part.eventIndex === void 0) metadata.push(part);
906
- else events[part.eventIndex].push(part);
907
- const cache = /* @__PURE__ */ new Map();
908
- const measure = (part, size, width, alignment) => {
909
- const key = JSON.stringify([part.path, size, width, alignment]);
910
- let measured = cache.get(key);
911
- if (measured) return measured;
912
- const fit = fitText(part.text, { width: Math.max(Number.MIN_VALUE, width - (outlines ? 2 * padding : 0)), height: Number.MAX_VALUE }, size, size, textWidthMeasurer(part.style, options.textMeasurement));
913
- const ink = fit.sourceLines.map((line, index) => {
914
- let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity, hasInk = false;
915
- if (outlines) {
916
- for (const segment of line.segments) if (segment.kind === "text") {
917
- const outline = measureTextOutline(part.text.slice(segment.start, segment.end), size, part.style, options.textMeasurement);
918
- if (outline) {
919
- hasInk = true;
920
- left = Math.min(left, outline.x + segment.x);
921
- top = Math.min(top, outline.y);
922
- right = Math.max(right, outline.x + segment.x + outline.width);
923
- bottom = Math.max(bottom, outline.y + outline.height);
924
- }
925
- }
926
- }
927
- return { width: line.width, y: index * fit.lineHeight, baseline: size + index * fit.lineHeight, height: fit.lineHeight, outline: hasInk ? { x: left, y: top, width: right - left, height: bottom - top } : null };
928
- });
929
- const placement = outlines ? placeTextLines(ink, { x: 0, y: 0, width, height: Number.MAX_VALUE }, alignment, padding) : void 0;
930
- const height = placement?.height ?? fit.sourceLines.length * fit.lineHeight;
931
- if (!Number.isFinite(height)) throw new RangeError("Timeline text exceeds finite layout coordinates.");
932
- measured = { fit, ink, height };
933
- cache.set(key, measured);
934
- return measured;
935
- };
936
- let selected, attempts = 0;
937
- let reductionLimit = 0;
938
- for (const part of source) reductionLimit = Math.max(reductionLimit, (part.requestedFontSize - minimum) / scale);
939
- const reductions = Math.min(24, Math.ceil(reductionLimit));
940
- for (let reduction = 0; reduction <= reductions; reduction++) {
941
- for (const arrangement of ["alternating", "vertical"]) {
942
- attempts++;
943
- const parts = [], markers = [], diagnostics = [];
944
- let score2 = 0;
945
- const report = (part, reason, message, excess = 1) => {
946
- diagnostics.push({ code: "text-overflow", reason, path: part.path, message });
947
- score2 += Math.max(1, excess);
948
- };
949
- const place = (part, x, y2, width, alignment) => {
950
- const measured = measure(part, Math.max(minimum, part.requestedFontSize - reduction * scale), Math.max(scale, width), alignment);
951
- const area = { x, y: y2, width: Math.max(scale, width), height: Math.max(scale, measured.height) }, placement = outlines ? placeTextLines(measured.ink, area, alignment, padding) : void 0;
952
- const fit = { ...measured.fit, ...placement ? { placement } : {}, overflow: measured.fit.overflow || !!placement?.overflow };
953
- const accepted = { ...part, box: area, alignment, fit };
954
- parts.push(accepted);
955
- if (fit.overflow) report(accepted, "text-fit", "Timeline field exceeds its readable width; increase its space or split the timeline.");
956
- if (area.x < box.x - 0.01 || area.y < box.y - 0.01 || area.x + area.width > box.x + box.width + 0.01 || area.y + area.height > box.y + box.height + 0.01) report(accepted, "part-outside-cell", "Timeline field extends outside its cell; increase the cell or paginate events.", Math.max(area.x + area.width - box.x - box.width, area.y + area.height - box.y - box.height));
957
- return accepted;
958
- };
959
- let y = box.y;
960
- for (const part of metadata) {
961
- const placed = place(part, box.x, y, box.width, "center");
962
- y += placed.box.height + 8 * scale;
963
- }
964
- const available = box.y + box.height - y, count = events.length, radius = Math.min(9 * scale, box.width / Math.max(2, count) / 5, Math.max(scale, available) * 0.04);
965
- if (arrangement === "alternating") {
966
- const width = box.width / Math.max(2, count), step = (box.width - width) / Math.max(1, count - 1), start = count === 1 ? box.x + box.width / 2 : box.x + width / 2;
967
- const lineY = y + Math.max(scale, available) * 0.46;
968
- events.forEach((fields2, index) => {
969
- const x = start + index * step, top = index % 2 === 0 ? y : lineY + 24 * scale, bottom = index % 2 === 0 ? lineY - 24 * scale : box.y + box.height;
970
- markers.push({ path: eventPath(index), eventIndex: index, x, y: lineY, radius });
971
- let cursor = top;
972
- for (const part of fields2) {
973
- const placed = place(part, x - width / 2, cursor, width, "center");
974
- cursor += placed.box.height;
975
- if (cursor > bottom + 0.01) report(placed, "event-space", "Timeline event labels exceed their side of the connector; change the arrangement or paginate events.", cursor - bottom);
976
- }
977
- });
978
- } else {
979
- const x = box.x + radius, textX = box.x + 24 * scale, width = box.width - 24 * scale;
980
- events.forEach((fields2, index) => {
981
- const top = y;
982
- let first2;
983
- for (const part of fields2) {
984
- const placed = place(part, textX, y, width, "left");
985
- first2 ??= placed;
986
- y += placed.box.height;
987
- }
988
- markers.push({ path: eventPath(index), eventIndex: index, x, y: top + Math.min(first2?.box.height ?? 2 * radius, 2 * radius) / 2, radius });
989
- y += 16 * scale;
990
- });
991
- }
992
- for (const marker of markers) if (marker.x - marker.radius < box.x - 0.01 || marker.y - marker.radius < box.y - 0.01 || marker.x + marker.radius > box.x + box.width + 0.01 || marker.y + marker.radius > box.y + box.height + 0.01) {
993
- diagnostics.push({ code: "text-overflow", reason: "event-space", path: marker.path, message: "Timeline marker has no usable space; increase the cell or paginate events." });
994
- score2 += 1;
995
- }
996
- const first = markers[0], last = markers.at(-1), connector = { x1: first.x, y1: first.y, x2: last.x, y2: last.y };
997
- const candidate = { arrangement, parts, markers, connector, diagnostics, score: score2 };
998
- if (!selected || score2 < selected.score) selected = candidate;
999
- if (!diagnostics.length) break;
1000
- }
1001
- if (!selected.diagnostics.length) break;
1002
- }
1003
- const { score, ...result } = selected;
1004
- if (result.diagnostics.length && options.overflow === "error") throw new OPFCompositionError(result.diagnostics);
1005
- return { algorithm: "timeline-flow-v1", attempts, textMeasurement: options.textMeasurement ? "provided" : "estimated", textOutlines: outlines ? "provided" : "unavailable", ...result, overflow: result.diagnostics.length > 0 };
1006
- }
1007
- function fitCodeText(text, box, size, minimum, step, measure) {
1008
- return fitSourceText(text, box, size, minimum, step, measure);
1009
- }
1010
- function fitSourceText(text, box, size, minimum, step, measure, prose = false) {
1011
- const layout = (fontSize) => {
1012
- const tabWidth = measure(" ", fontSize) * 4;
1013
- if (!Number.isFinite(tabWidth) || text.includes(" ") && tabWidth <= 0) throw new RangeError("Text tabs require a positive finite measured space advance.");
1014
- const measureLine = (start2, end2, record3 = false) => {
1015
- let x = 0, offset = start2;
1016
- const segments = [];
1017
- for (const [index, segment] of text.slice(start2, end2).split(" ").entries()) {
1018
- if (index) {
1019
- const next = (Math.floor(x / tabWidth + 1e-9) + 1) * tabWidth;
1020
- if (record3) segments.push({ kind: "tab", start: offset, end: offset + 1, x, width: next - x });
1021
- x = next;
1022
- offset++;
1023
- }
1024
- if (segment) {
1025
- const advance = measure(segment, fontSize);
1026
- if (record3) segments.push({ kind: "text", start: offset, end: offset + segment.length, x, width: advance });
1027
- x += advance;
1028
- offset += segment.length;
1029
- }
1030
- }
1031
- return { width: x, segments };
1032
- };
1033
- const width = (start2, end2) => measureLine(start2, end2).width;
1034
- const sourceLines = [];
1035
- let start = 0, end = 0;
1036
- const push = (last, nextStart, boundary) => {
1037
- sourceLines.push({ start, end: last, nextStart, boundary, ...measureLine(start, last, true) });
1038
- start = nextStart;
1039
- end = nextStart;
1040
- };
1041
- const tokens = prose ? /\r\n|\r|\n|[^\S\r\n\u00a0\u202f\ufeff]+|(?:[^\s]|\u00a0|\u202f|\ufeff)+/gu : /\r\n|\r|\n|[^\S\r\n]+|[^\s]+/gu;
1042
- for (const token of text.matchAll(tokens)) {
1043
- if (token.index === void 0) throw new Error("Missing code token source offset.");
1044
- const a = token.index, b = a + token[0].length;
1045
- if (/^[\r\n]/.test(token[0])) {
1046
- push(a, b, "hard");
1047
- continue;
1048
- }
1049
- if (width(start, b) <= box.width + 0.01) {
1050
- end = b;
1051
- continue;
1052
- }
1053
- if (end > start && /\S/u.test(text.slice(start, end))) push(a, a, "soft");
1054
- if (width(start, b) <= box.width + 0.01) {
1055
- end = b;
1056
- continue;
1057
- }
1058
- if (prose && /[\u00a0\u202f\u2060\ufeff]/u.test(token[0])) {
1059
- end = b;
1060
- continue;
1061
- }
1062
- for (const segment of textSegments.segment(token[0])) {
1063
- const next = a + segment.index + segment.segment.length;
1064
- if (end > start && width(start, next) > box.width + 0.01) push(a + segment.index, a + segment.index, "soft");
1065
- end = next;
1066
- }
1067
- }
1068
- push(end, end, "end");
1069
- const lineHeight = fontSize * 1.22;
1070
- return {
1071
- lines: sourceLines.map((line) => text.slice(line.start, line.end)),
1072
- sourceLines,
1073
- fontSize,
1074
- lineHeight,
1075
- tabSize: 4,
1076
- tabWidth,
1077
- overflow: sourceLines.length * lineHeight > box.height + 0.01 || sourceLines.some((line) => line.width > box.width + 0.01)
1078
- };
1079
- };
1080
- return fitAtSizes(layout, size, minimum, step);
1081
- }
1082
- function layoutCode(value, box, options = {}) {
1083
- const shorthand = typeof value === "string", code = shorthand ? { source: value } : value;
1084
- if (!code || Array.isArray(code) || typeof code.source !== "string" || [code.language, code.filename].some((field) => field !== void 0 && typeof field !== "string")) {
1085
- throw new TypeError("Code content must be a string or source object with optional string language/filename.");
1086
- }
1087
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale;
1088
- if (![box.x, box.y, box.width, box.height, box.x + box.width, box.y + box.height, scale, minimum, Math.max(18 * scale, minimum) * 1.22].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || scale <= 0 || minimum <= 0) {
1089
- throw new RangeError("Code dimensions, scale and minimum font size must be finite and positive.");
1090
- }
1091
- if (options.overflow !== void 0 && !["warn", "error"].includes(options.overflow)) throw new RangeError("Invalid code overflow policy.");
1092
- const path = options.path ?? "code", inner = { x: box.x + 18, y: box.y + 18, width: box.width - 36, height: box.height - 36 };
1093
- const parts = [], diagnostics = [];
1094
- const add = (role, text, partPath, generated = false) => {
1095
- const requestedStyle = { fontFamily: options.fonts?.code ?? "monospace", fontWeight: role === "body" ? 400 : 700, italic: false, path: partPath };
1096
- const part = {
1097
- role,
1098
- path: partPath,
1099
- text,
1100
- sources: generated ? [] : [{ path: partPath, start: 0, end: text.length }],
1101
- generated,
1102
- box: { ...inner },
1103
- requestedFontSize: Math.max((role === "body" ? 18 : 14) * scale, minimum),
1104
- minFontSize: minimum,
1105
- requestedStyle,
1106
- style: resolveTextStyle({ ...requestedStyle }, options.textMeasurement)
1107
- };
1108
- parts.push(part);
1109
- return part;
1110
- };
1111
- if (code.filename) add("filename", code.filename, `${path}.filename`);
1112
- if (code.language) add("language", code.language, `${path}.language`);
1113
- if (!parts.length) add("language", "code", path, true);
1114
- const body = add("body", code.source, shorthand ? path : `${path}.source`), headers = parts.filter((part) => part !== body);
1115
- const usable = (area) => [area.x, area.y, area.width, area.height].every(Number.isFinite) && area.width > 0 && area.height > 0;
1116
- const fit = (part, area, size = part.requestedFontSize, floor = minimum) => usable(area) ? fitCodeText(part.text, area, size, floor, scale, textWidthMeasurer(part.style, options.textMeasurement)) : void 0;
1117
- if (usable(inner)) {
1118
- const combinations = headers.reduce((choices, part) => choices.flatMap((choice) => [.../* @__PURE__ */ new Set([part.requestedFontSize, minimum])].map((size) => [...choice, { part, size }])), [[]]);
1119
- let selected;
1120
- for (const combination of combinations) {
1121
- let y = inner.y;
1122
- const allocations = [];
1123
- for (const [index, { part, size }] of combination.entries()) {
1124
- const measured = fitCodeText(part.text, inner, size, size, scale, textWidthMeasurer(part.style, options.textMeasurement));
1125
- const area2 = { ...inner, y, height: measured.lines.length * measured.lineHeight };
1126
- allocations.push({ part, box: area2, fit: usable(area2) ? { ...measured, overflow: measured.sourceLines.some((line) => line.width > area2.width + 0.01) } : void 0 });
1127
- y += area2.height + (index < headers.length - 1 ? 8 : 12);
1128
- }
1129
- const area = { ...inner, y, height: inner.y + inner.height - y };
1130
- allocations.push({ part: body, box: area, fit: fit(body, area) });
1131
- const overflow = allocations.some(({ box: box2, fit: fit2 }) => !fit2 || fit2.overflow || !usable(box2) || box2.y + box2.height > inner.y + inner.height + 0.01);
1132
- const score = allocations.reduce((sum, { part, fit: fit2 }) => sum + (part.requestedFontSize - (fit2?.fontSize ?? minimum)) / scale, 0);
1133
- if (!selected || !overflow && (selected.overflow || score < selected.score) || overflow && selected.overflow) selected = { allocations, score, overflow };
1134
- }
1135
- if (selected) for (const { part, box: box2, fit: fit2 } of selected.allocations) {
1136
- part.box = box2;
1137
- part.fit = fit2;
1138
- }
1139
- }
1140
- const report = (reason, part, roles, message) => diagnostics.push({ code: "text-overflow", reason, path: part.path, parts: roles, message });
1141
- for (const part of parts) {
1142
- if (!part.fit) report("invalid-part-box", part, [part.role], `Code ${part.role} has no usable space after metadata and insets; increase the cell or change the arrangement.`);
1143
- else if (part.fit.overflow) report("text-fit", part, [part.role], `Code ${part.role} exceeds its space at the readability floor; increase its space or change the arrangement.`);
1144
- const area = part.box;
1145
- if (usable(area) && (area.x < box.x || area.y < box.y || area.x + area.width > box.x + box.width + 0.01 || area.y + area.height > box.y + box.height + 0.01)) {
1146
- report("part-outside-cell", part, [part.role], `Code ${part.role} extends outside its cell; increase the cell or change the arrangement.`);
1147
- }
1148
- }
1149
- for (const [index, first] of parts.entries()) {
1150
- const second = parts[index + 1];
1151
- if (first.fit && second?.fit && first.box.y + first.fit.lines.length * first.fit.lineHeight > second.box.y + 0.01) {
1152
- report("part-overlap", first, [first.role, second.role], "Code part line boxes overlap; do not accept this layout without more space.");
1153
- }
1154
- }
1155
- if (diagnostics.length && options.overflow === "error") throw new OPFCompositionError(diagnostics);
1156
- return { algorithm: "code-flow-v1", textMeasurement: options.textMeasurement ? "provided" : "estimated", parts, diagnostics, overflow: diagnostics.length > 0 };
1157
- }
1158
- function fitRichText(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
1159
- 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.");
1160
- const layout = richTextLayouter(input, box, requestedSize, options);
1161
- return fitAtSizes(layout, requestedSize, richMinimum(input, requestedSize, minFontSize));
1162
- }
1163
- function richMinimum(input, requested, minimum) {
1164
- let floor = Math.min(requested, minimum), visible = false;
1165
- for (const value of input) {
1166
- const run = typeof value === "string" ? { text: value } : value;
1167
- if (!run.text) continue;
1168
- const ratio = (run.fontSize === void 0 ? 1 : run.fontSize * 4 / 3 / requested) * (run.superscript || run.subscript ? 0.7 : 1);
1169
- floor = Math.max(floor, minimum / ratio);
1170
- visible = true;
1171
- }
1172
- return visible ? floor : minimum;
1173
- }
1174
- function richTextLayouter(input, box, requestedSize, options) {
1175
- let offset = 0;
1176
- const source = input.map((value, runIndex) => {
1177
- const run = typeof value === "string" ? { text: value } : value;
1178
- 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.");
1179
- const start = offset;
1180
- offset += run.text.length;
1181
- 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);
1182
- return { run, runIndex, start, end: offset, style };
1183
- });
1184
- const whole = source.map((entry) => entry.run.text).join("");
1185
- const layout = (fontSize) => {
1186
- const ratio = fontSize / requestedSize;
1187
- const fragments = (start, end) => {
1188
- let x = 0;
1189
- const result = [];
1190
- for (const entry of source) {
1191
- const a = Math.max(start, entry.start), b = Math.min(end, entry.end);
1192
- if (b <= a) continue;
1193
- const text = whole.slice(a, b), normalSize = (entry.run.fontSize !== void 0 ? entry.run.fontSize * 4 / 3 : requestedSize) * ratio;
1194
- const script = entry.run.superscript || entry.run.subscript;
1195
- const size = normalSize * (script ? 0.7 : 1), baselineShift = entry.run.superscript ? -normalSize * 0.35 : entry.run.subscript ? normalSize * 0.2 : 0;
1196
- const measure = textWidthMeasurer(entry.style, options.textMeasurement);
1197
- let cursor = 0;
1198
- for (const [index, segment] of text.split(" ").entries()) {
1199
- if (index) {
1200
- const tabStart = a + cursor, tabWidth = measure(" ", size) * 4;
1201
- if (!Number.isFinite(tabWidth) || tabWidth <= 0) throw new RangeError("Rich text tabs require a positive finite measured space advance.");
1202
- const next = (Math.floor(x / tabWidth + 1e-9) + 1) * tabWidth;
1203
- result.push({ kind: "tab", text: " ", runIndex: entry.runIndex, start: tabStart - entry.start, end: tabStart - entry.start + 1, x, width: next - x, fontSize: size, baselineShift, style: entry.style, run: entry.run });
1204
- x = next;
1205
- cursor++;
1206
- }
1207
- if (segment) {
1208
- const width2 = measure(segment, size), segmentStart = a + cursor;
1209
- result.push({ text: segment, runIndex: entry.runIndex, start: segmentStart - entry.start, end: segmentStart - entry.start + segment.length, x, width: width2, fontSize: size, baselineShift, style: entry.style, run: entry.run });
1210
- x += width2;
1211
- cursor += segment.length;
1212
- }
1213
- }
1214
- }
1215
- return result;
1216
- };
1217
- const width = (start, end) => fragments(start, end).reduce((sum, fragment) => sum + fragment.width, 0);
1218
- const ranges = [];
1219
- let lineStart = 0, lineEnd = 0;
1220
- const push = (end) => {
1221
- ranges.push({ start: lineStart, end });
1222
- lineStart = end;
1223
- lineEnd = end;
1224
- };
1225
- for (const match of whole.matchAll(/\r\n|\r|\n|[^\S\r\n]+|[^\s]+/gu)) {
1226
- const start = match.index, end = start + match[0].length;
1227
- if (/^[\r\n]/.test(match[0])) {
1228
- push(start);
1229
- lineStart = end;
1230
- lineEnd = end;
1231
- continue;
1232
- }
1233
- if (width(lineStart, end) <= box.width) {
1234
- lineEnd = end;
1235
- continue;
1236
- }
1237
- if (lineEnd > lineStart) push(start);
1238
- if (width(start, end) <= box.width) {
1239
- lineEnd = end;
1240
- continue;
1241
- }
1242
- for (const { segment, index } of textSegments.segment(match[0])) {
1243
- const next = start + index + segment.length;
1244
- if (lineEnd > lineStart && width(lineStart, next) > box.width) push(start + index);
1245
- lineEnd = next;
1246
- }
1247
- }
1248
- ranges.push({ start: lineStart, end: lineEnd });
1249
- let y = 0;
1250
- const richLines = ranges.map((range) => {
1251
- 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));
1252
- const height = ascent + descent, line = { fragments: parts, width: parts.reduce((sum, p) => sum + p.width, 0), y, baseline: y + ascent, height };
1253
- y += height;
1254
- return line;
1255
- });
1256
- if (options.uniformLineHeight) {
1257
- const height = Math.max(...richLines.map((line) => line.height));
1258
- y = 0;
1259
- for (const line of richLines) {
1260
- line.baseline += y - line.y;
1261
- line.y = y;
1262
- line.height = height;
1263
- y += height;
1264
- }
1265
- }
1266
- 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) };
1267
- };
1268
- return layout;
1269
- }
1270
- function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
1271
- 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.");
1272
- const source = input.map((value, index) => {
1273
- const object = typeof value === "object" && !Array.isArray(value) ? value : void 0;
1274
- const text = object ? object.text : value, description = object?.description, level = object?.level ?? 0;
1275
- if (!Number.isInteger(level) || level < 0) throw new RangeError("List levels must be nonnegative integers.");
1276
- const runs = (value2) => typeof value2 === "string" ? [value2] : value2;
1277
- 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.");
1278
- const base = options.style.path ? `${options.style.path}.${index}` : void 0;
1279
- 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) };
1280
- });
1281
- const layout = (fontSize) => {
1282
- let y = box.y, overflow = false;
1283
- const entries = [], lines = [];
1284
- for (const item of source) {
1285
- 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);
1286
- if (offset + indent >= box.width) overflow = true;
1287
- const textBox = { x, y, width, height: box.height };
1288
- const style = { ...options.style, path: item.textPath };
1289
- const text = richTextLayouter(item.runs, textBox, requestedSize, { ...options, style })(fontSize);
1290
- textBox.height = text.height;
1291
- lines.push(...text.lines);
1292
- y += text.height;
1293
- let description, descriptionBox;
1294
- if (item.descriptionRuns !== void 0) {
1295
- y += fontSize * 0.12;
1296
- descriptionBox = { x, y, width, height: box.height };
1297
- description = richTextLayouter(item.descriptionRuns, descriptionBox, requestedSize * 0.82, { ...options, style: { ...options.style, path: item.descriptionPath } })(fontSize * 0.82);
1298
- descriptionBox.height = description.height;
1299
- lines.push(...description.lines);
1300
- y += description.height;
1301
- }
1302
- overflow ||= text.overflow || !!description?.overflow;
1303
- entries.push({
1304
- index: item.index,
1305
- level: item.level,
1306
- value: item.value,
1307
- descriptionValue: item.descriptionValue,
1308
- textPath: item.textPath,
1309
- descriptionPath: item.descriptionPath,
1310
- text,
1311
- textBox,
1312
- description,
1313
- descriptionBox,
1314
- 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 }
1315
- });
1316
- if (item.index < source.length - 1) y += fontSize * 0.28;
1317
- }
1318
- const height = y - box.y;
1319
- return { listEntries: entries, lines, fontSize, lineHeight: fontSize * 1.22, height, overflow: overflow || height > box.height + 0.01 };
1320
- };
1321
- let minimum = minFontSize;
1322
- for (const item of source) {
1323
- minimum = Math.max(minimum, richMinimum(item.runs, requestedSize, minFontSize));
1324
- if (item.descriptionRuns !== void 0) minimum = Math.max(minimum, richMinimum(item.descriptionRuns, requestedSize * 0.82, minFontSize) / 0.82);
1325
- }
1326
- return fitAtSizes(layout, requestedSize, minimum);
1327
- }
1328
- function contentText(field, value) {
1329
- if (field === "text" || headings.has(field)) return flatten(value);
1330
- if (field === "items" || field === "bullets") return (Array.isArray(value) ? value : [value]).map((item) => {
1331
- const entry = record2(item);
1332
- return `\u2022 ${flatten(item)}${entry.description ? `
1333
- ${flatten(entry.description)}` : ""}`;
1334
- }).join("\n");
1335
- if (field === "code") return flatten(record2(value).source ?? value);
1336
- if (field === "quote") return flatten(record2(value).text ?? value);
1337
- return void 0;
1338
- }
1339
- function layoutTable(value, box, options = {}) {
1340
- const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale, requested = Math.max(15 * scale, minimum);
1341
- if (![box.x, box.y, box.width, box.height, scale, minimum].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || scale <= 0 || minimum <= 0) throw new RangeError("Table dimensions, scale and font sizes must be finite and positive.");
1342
- const grid = tableGrid(value, options.path ?? "table");
1343
- if (grid.issues.length) throw new RangeError(`${grid.issues[0].path}: ${grid.issues[0].message}`);
1344
- const columnCount = grid.columnCount, cellWidth = box.width / columnCount;
1345
- const cells = grid.rows.flat().map((cell) => {
1346
- const padding = { top: 8, right: 10, bottom: 4, left: 10, ...cell.style.padding };
1347
- const width = cellWidth * cell.colSpan - (padding.left + padding.right) * scale;
1348
- const style = { fontFamily: options.fontFamily ?? "sans-serif", fontWeight: cell.header ? 700 : 400, italic: false, path: cell.valuePath };
1349
- return { ...cell, padding, width, textStyle: style };
1350
- });
1351
- const cellMinimum = (cell) => Array.isArray(cell.value) ? richMinimum(cell.value, requested, minimum) : minimum;
1352
- const fitCell = (cell, height, min) => {
1353
- const textBox = { width: Math.max(scale, cell.width), height: Math.max(scale, height) };
1354
- return Array.isArray(cell.value) ? fitRichText(cell.value, textBox, requested, min, { style: cell.textStyle, textMeasurement: options.textMeasurement, uniformLineHeight: true }) : fitText(flatten(cell.value), textBox, requested, min, textWidthMeasurer(resolveTextStyle(cell.textStyle, options.textMeasurement), options.textMeasurement));
1355
- };
1356
- const textHeight = (fit) => "height" in fit ? fit.height : fit.lines.length * fit.lineHeight;
1357
- const required = (cell, natural2) => {
1358
- const floor = cellMinimum(cell), size = natural2 ? Math.max(requested, floor) : floor;
1359
- const textBox = { width: Math.max(scale, cell.width), height: scale };
1360
- const fit = Array.isArray(cell.value) ? richTextLayouter(cell.value, textBox, requested, { style: cell.textStyle, textMeasurement: options.textMeasurement, uniformLineHeight: true })(size) : fitText(flatten(cell.value), textBox, size, size, textWidthMeasurer(resolveTextStyle(cell.textStyle, options.textMeasurement), options.textMeasurement));
1361
- return textHeight(fit) + (cell.padding.top + cell.padding.bottom) * scale;
1362
- };
1363
- const natural = Array(grid.rowCount).fill(0), needed = Array(grid.rowCount).fill(0);
1364
- for (const cell of cells) if (cell.rowSpan === 1) {
1365
- natural[cell.row] = Math.max(natural[cell.row], required(cell, true));
1366
- needed[cell.row] = Math.max(needed[cell.row], required(cell, false));
1367
- }
1368
- for (const cell of cells) if (cell.rowSpan > 1) for (const [heights2, preferredSize] of [[natural, true], [needed, false]]) {
1369
- const current = heights2.slice(cell.row, cell.row + cell.rowSpan).reduce((a, b) => a + b, 0);
1370
- const extra = Math.max(0, required(cell, preferredSize) - current) / cell.rowSpan;
1371
- for (let r = cell.row; r < cell.row + cell.rowSpan; r++) heights2[r] += extra;
1372
- }
1373
- const preferred = natural.map((height) => Math.max(54 * scale, height));
1374
- for (let r = 0; r < needed.length; r++) needed[r] = Math.min(preferred[r], needed[r]);
1375
- const sum = (values) => values.reduce((a, b) => a + b, 0);
1376
- const preferredTotal = sum(preferred), naturalTotal = sum(natural), neededTotal = sum(needed);
1377
- const heights = preferred.map((height, r) => preferredTotal <= box.height ? height : naturalTotal <= box.height ? natural[r] + (height - natural[r]) * (box.height - naturalTotal) / Math.max(Number.EPSILON, preferredTotal - naturalTotal) : neededTotal <= box.height ? needed[r] + (natural[r] - needed[r]) * (box.height - neededTotal) / Math.max(Number.EPSILON, naturalTotal - neededTotal) : needed[r] * box.height / neededTotal);
1378
- const ys = [box.y];
1379
- for (const height of heights) ys.push(ys.at(-1) + height);
1380
- let overflow = false;
1381
- const rows2 = heights.map((height, r) => ({ box: { x: box.x, y: ys[r], width: box.width, height }, cells: [] }));
1382
- for (const cell of cells) {
1383
- const cellBox = { x: box.x + cell.column * cellWidth, y: ys[cell.row], width: cell.colSpan * cellWidth, height: ys[cell.row + cell.rowSpan] - ys[cell.row] };
1384
- const available = cellBox.height - (cell.padding.top + cell.padding.bottom) * scale;
1385
- const fit = fitCell(cell, available, minimum), height = textHeight(fit);
1386
- const offset = cell.style.verticalAlign === "bottom" ? Math.max(0, available - height) : cell.style.verticalAlign === "middle" ? Math.max(0, (available - height) / 2) : 0;
1387
- const textBox = { x: cellBox.x + cell.padding.left * scale, y: cellBox.y + cell.padding.top * scale + offset, width: Math.max(scale, cell.width), height: Math.max(scale, available - offset) };
1388
- overflow ||= cell.width <= 0 || available <= 0 || fit.overflow;
1389
- rows2[cell.row].cells.push({ value: cell.value, input: cell.input, path: cell.valuePath, sourcePath: cell.path, style: cell.style, row: cell.row, column: cell.column, rowSpan: cell.rowSpan, colSpan: cell.colSpan, header: cell.header, rich: Array.isArray(cell.value), box: cellBox, textBox, textStyle: resolveTextStyle(cell.textStyle, options.textMeasurement), fit });
1390
- }
1391
- return { rows: rows2, columnCount, height: sum(heights), overflow };
1392
- }
1393
- function tableOverflows(value, box, scale, settings, options, path) {
1394
- return box.width <= 0 || box.height <= 0 || layoutTable(value, box, { scale, minFontSize: settings.minFontSize, fontFamily: options.fonts?.body, textMeasurement: options.textMeasurement, path }).overflow;
1395
- }
1396
- function flatten(value) {
1397
- if (value == null) return "";
1398
- if (typeof value !== "object") return String(value);
1399
- if (Array.isArray(value)) return value.map(flatten).join("");
1400
- const item = record2(value);
1401
- return flatten(item.text ?? item.value ?? item.source ?? JSON.stringify(value));
1402
- }
1403
- function regionParts(key) {
1404
- const span = (part, order) => {
1405
- const indices = part.split("+").map((value) => order.indexOf(value));
1406
- return indices.every((index) => index >= 0) ? indices : void 0;
1407
- };
1408
- const parts = key.split(":");
1409
- if (parts.length === 2) {
1410
- const r = span(parts[0], rows), c = span(parts[1], columns);
1411
- if (r && c) return [r, c];
1412
- } else if (parts.length === 1) {
1413
- const r = span(key, rows), c = span(key, columns);
1414
- if (r) return [r, [0, 1, 2]];
1415
- if (c) return [[0, 1, 2], c];
1416
- }
1417
- return void 0;
1418
- }
1419
- function regionBox(parts, area, requestedGap) {
1420
- const gap = Math.min(requestedGap, area.width / 6, area.height / 6);
1421
- const cw = (area.width - gap * 2) / 3, ch = (area.height - gap * 2) / 3;
1422
- const [r, c] = parts;
1423
- 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 };
1424
- }
1425
- function gridGeometry(count, area, cols, gap, weights, vertical) {
1426
- const rowCount = Math.ceil(count / cols);
1427
- gap = Math.min(gap, area.width / (cols * 2), area.height / (rowCount * 2));
1428
- const tracks = (length, total, weighted) => {
1429
- const values = Array.from({ length }, (_, i) => weighted ? weights[i] ?? 1 : 1);
1430
- const sum = values.reduce((a, b) => a + b, 0);
1431
- let cursor = 0;
1432
- return values.map((weight) => {
1433
- const size = (total - gap * (length - 1)) * weight / sum;
1434
- const track = { offset: cursor, size };
1435
- cursor += size + gap;
1436
- return track;
1437
- });
1438
- };
1439
- const xs = tracks(cols, area.width, !vertical), ys = tracks(rowCount, area.height, vertical);
1440
- 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 }));
1441
- return { boxes, columns: xs, rows: ys, gap };
1442
- }
1443
- function gridBoxes(count, area, cols, gap, weights, vertical) {
1444
- return gridGeometry(count, area, cols, gap, weights, vertical).boxes;
1445
- }
1446
- function assertComposition(value) {
1447
- if (value.mode !== void 0 && !["auto", "grid", "row", "column"].includes(value.mode)) throw new RangeError("Unknown composition mode.");
1448
- for (const [key, min, max] of [["gap", 0, 0.1], ["padding", 0, 0.2], ["minFontSize", 8, 32], ["columns", 1, 12]]) {
1449
- const number = value[key];
1450
- if (number !== void 0 && (!Number.isFinite(number) || number < min || number > max || key === "columns" && !Number.isInteger(number))) throw new RangeError(`Invalid composition.${key}.`);
1451
- }
1452
- 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.");
1453
- if (value.overflow !== void 0 && !["warn", "error"].includes(value.overflow)) throw new RangeError("Invalid composition.overflow.");
1454
- }
1455
- function composeSlide(input, options = {}) {
1456
- const slide = record2(input), layout = record2(options.layout);
1457
- const width = options.width ?? 1280, height = options.height ?? 720;
1458
- if (![width, height].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("Canvas dimensions must be finite and positive.");
1459
- const scale = Math.min(width, height) / 720;
1460
- const hasCards = record2(slide.design).contentBox ?? options.contentBox ?? false;
1461
- const composition = { ...record2(layout.composition), ...record2(slide.composition) };
1462
- assertComposition(composition);
1463
- const padding = (composition.padding ?? 0.08) * Math.min(width, height);
1464
- const gap = (composition.gap ?? 1 / 30) * Math.min(width, height);
1465
- const minSize = (composition.minFontSize ?? 16) * scale;
1466
- const rasterPadding = (options.textRasterPadding ?? 1) * scale;
1467
- if (!Number.isFinite(rasterPadding) || rasterPadding < 0) throw new RangeError("Text raster padding must be finite and nonnegative.");
1468
- const alignmentFor = (field) => (field === "title" ? record2(slide.design).titleAlignment ?? options.titleAlignment : record2(slide.design).contentAlignment ?? options.contentAlignment) ?? "left";
1469
- const styleFor = (field, path2) => resolveTextStyle({
1470
- fontFamily: (field === "title" ? options.fonts?.heading : field === "code" ? options.fonts?.code : options.fonts?.body) ?? (field === "code" ? "monospace" : "sans-serif"),
1471
- fontWeight: field === "title" ? 700 : 400,
1472
- path: path2
1473
- }, options.textMeasurement);
1474
- const widthFor = (field, path2) => textWidthMeasurer(styleFor(field, path2), options.textMeasurement);
1475
- const fitPlacedText = (field, value, text, box, size, minimum, path2) => {
1476
- const style = styleFor(field, path2), rich = field === "text" && Array.isArray(value);
1477
- if (!options.textMeasurement?.outlineBounds) return rich ? fitRichText(value, box, size, minimum, { style, textMeasurement: options.textMeasurement }) : fitText(text, box, size, minimum, textWidthMeasurer(style, options.textMeasurement));
1478
- const alignment = alignmentFor(field);
1479
- const richLayout = rich ? richTextLayouter(value, box, size, { style, textMeasurement: options.textMeasurement }) : void 0;
1480
- const measure = textWidthMeasurer(style, options.textMeasurement), floor = rich ? richMinimum(value, size, minimum) : minimum, start = Math.max(size, floor);
1481
- for (let trial = 0; trial <= 64; trial++) {
1482
- const fontSize = trial === 64 ? floor : Math.max(floor, start - trial * scale);
1483
- const fit = richLayout ? richLayout(fontSize) : fitText(text, box, fontSize, fontSize, measure);
1484
- const richLines = "richLines" in fit ? fit.richLines : void 0;
1485
- const lines = richLines ? richLines.map((line) => {
1486
- let outline = null;
1487
- for (const fragment of line.fragments) {
1488
- if (fragment.kind === "tab") continue;
1489
- const bounds = measureTextOutline(fragment.text, fragment.fontSize, fragment.style, options.textMeasurement);
1490
- if (!bounds) continue;
1491
- const next = { ...bounds, x: fragment.x + bounds.x, y: fragment.baselineShift + bounds.y };
1492
- if (!outline) outline = next;
1493
- else {
1494
- const right = Math.max(outline.x + outline.width, next.x + next.width), bottom = Math.max(outline.y + outline.height, next.y + next.height);
1495
- outline.x = Math.min(outline.x, next.x);
1496
- outline.y = Math.min(outline.y, next.y);
1497
- outline.width = right - outline.x;
1498
- outline.height = bottom - outline.y;
1499
- }
1500
- }
1501
- return { width: line.width, y: line.y, baseline: line.baseline, height: line.height, outline };
1502
- }) : fit.sourceLines.map((line, index) => {
1503
- let outline = null;
1504
- for (const segment of line.segments) {
1505
- if (segment.kind === "tab") continue;
1506
- const bounds = measureTextOutline(text.slice(segment.start, segment.end), fontSize, style, options.textMeasurement);
1507
- if (!bounds) continue;
1508
- const next = { ...bounds, x: segment.x + bounds.x };
1509
- if (!outline) outline = next;
1510
- else {
1511
- const right = Math.max(outline.x + outline.width, next.x + next.width), bottom = Math.max(outline.y + outline.height, next.y + next.height);
1512
- outline.x = Math.min(outline.x, next.x);
1513
- outline.y = Math.min(outline.y, next.y);
1514
- outline.width = right - outline.x;
1515
- outline.height = bottom - outline.y;
1516
- }
1517
- }
1518
- return { width: line.width, y: index * fit.lineHeight, baseline: fontSize + index * fit.lineHeight, height: fit.lineHeight, outline };
1519
- });
1520
- const placement = placeTextLines(lines, box, alignment, rasterPadding), result = { ...fit, placement, overflow: fit.overflow || placement.overflow };
1521
- if (!result.overflow || fontSize === floor) return result;
1522
- }
1523
- throw new Error("Text placement did not evaluate its bounded floor trial.");
1524
- };
1525
- const fitContent = (field, value, text, box, size, minimum, path2) => field === "text" ? fitPlacedText(field, value, text, box, size, minimum, path2) : field === "items" || field === "bullets" ? fitList(value, box, size, minimum, { style: styleFor(field, path2), textMeasurement: options.textMeasurement }) : fitText(text, box, size, minimum, widthFor(field, path2));
1526
- const path = `slides.${options.slideIndex ?? 0}`;
1527
- const slideImageDiagnostics = [];
1528
- const slideImage = resolveSlideImage(slide, layout, options.presentation, width, height, path, padding, scale, slideImageDiagnostics);
1529
- const area = { left: 0, top: 0, right: width, bottom: height };
1530
- if (slideImage?.position === "left") area.left = slideImage.region.width;
1531
- else if (slideImage?.position === "right") area.right = slideImage.region.x;
1532
- else if (slideImage?.position === "top") area.top = slideImage.region.height;
1533
- else if (slideImage?.position === "bottom") area.bottom = slideImage.region.y;
1534
- const items = [], diagnostics = [];
1535
- const measuredFurniture = layoutFurniture(slide, options);
1536
- const furniture = measuredFurniture.configured || measuredFurniture.diagnostics.length ? measuredFurniture : void 0;
1537
- if (furniture) diagnostics.push(...furniture.diagnostics);
1538
- const bodyBottom = Math.min(area.bottom - padding, furniture && furniture.footerTop < height ? furniture.footerTop - gap * 0.5 : height - padding);
1539
- const headingTop = Math.max(area.top + padding, furniture?.headerBottom ? furniture.headerBottom + gap * 0.5 : padding);
1540
- const layoutPlaceholders = Array.isArray(layout.placeholders) ? layout.placeholders : [];
1541
- const headingOnlyLayout = COVER_LAYOUT_IDS.has(String(layout.id ?? "")) || layoutPlaceholders.length > 0 && layoutPlaceholders.every((placeholder) => headings.has(placeholder.type ?? ""));
1542
- const regions = Object.keys(slide).filter((key) => regionParts(key)).sort();
1543
- const emptyPayload = (value) => value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0;
1544
- const emptyHost = (host) => emptyPayload(host.blocks) && fields.every((field) => emptyPayload(host[field]));
1545
- const hasBodyPayload = regions.some((key) => !emptyHost(record2(slide[key]))) || !emptyPayload(slide.blocks) || fields.some((field) => !emptyPayload(slide[field]));
1546
- const isCover = !hasBodyPayload && (headingOnlyLayout || !layout.id && !layoutPlaceholders.some((placeholder) => !headings.has(placeholder.type ?? "")));
1547
- let y = headingTop;
1548
- const headingItems = [];
1549
- for (const field of ["tag", "title", "subtitle"]) {
1550
- if (!slide[field]) continue;
1551
- const requested = (field === "title" ? 54 : field === "tag" ? 16 : 25) * scale;
1552
- const maxHeight = Math.max(height * (field === "title" ? 0.26 : field === "subtitle" ? 0.12 : 0.045), minSize * 1.22 + 2 * rasterPadding);
1553
- const box = { x: area.left + padding, y, width: area.right - area.left - padding * 2, height: maxHeight };
1554
- const text = fitPlacedText(field, slide[field], String(slide[field]), box, requested, minSize, `${path}.${field}`);
1555
- box.height = Math.min(maxHeight, Math.max(text.lines.length * text.lineHeight, text.placement?.height ?? 0));
1556
- if (furniture && box.y + box.height > bodyBottom + 0.01) diagnostics.push({ code: "text-overflow", path: `${path}.${field}`, message: "Repeated furniture leaves too little room for this heading. Change the header/footer or slide design." });
1557
- const item = { path: `${path}.${field}`, field, type: "text", value: slide[field], payload: { text: slide[field] }, box, text, textStyle: styleFor(field, `${path}.${field}`), composition, alignment: alignmentFor(field) };
1558
- headingItems.push(item);
1559
- items.push(item);
1560
- y += box.height + gap * 0.5;
1561
- }
1562
- if (headingItems.length) {
1563
- if (isCover) {
1564
- const last = headingItems[headingItems.length - 1];
1565
- const shift = Math.max(0, (bodyBottom - (last.box.y + last.box.height)) / 2);
1566
- if (shift) for (const item of headingItems) {
1567
- item.box.y += shift;
1568
- if (item.text) item.text = translateTextFit(item.text, shift);
1569
- }
1570
- y = last.box.y + last.box.height + gap;
1571
- } else y += gap * 0.5;
1572
- }
1573
- const contentBox = { x: area.left + padding, y, width: area.right - area.left - padding * 2, height: Math.max(scale, bodyBottom - y) };
1574
- const collect = (host, basePath, depth = 0, ancestors = []) => {
1575
- if (Array.isArray(host.blocks)) {
1576
- if (depth >= MAX_COMPOSITION_DEPTH || ancestors.includes(host)) throw new RangeError(`Content groups must be acyclic and nest at most ${MAX_COMPOSITION_DEPTH} levels.`);
1577
- const settings = record2(host.composition);
1578
- assertComposition(settings);
1579
- return [{
1580
- field: "blocks",
1581
- type: "group",
1582
- value: host.blocks,
1583
- path: basePath,
1584
- payload: host,
1585
- composition: settings,
1586
- children: host.blocks.flatMap((block, index) => collect(record2(block), `${basePath}.blocks.${index}`, depth + 1, [...ancestors, host]))
1587
- }];
1588
- }
1589
- 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] } }));
1590
- };
1591
- const pending = regions.length ? regions.flatMap((key) => collect(record2(slide[key]), `${path}.${key}`).map((item) => ({ ...item, region: regionParts(key) }))) : Array.isArray(slide.blocks) ? slide.blocks.flatMap((block, index) => collect(record2(block), `${path}.blocks.${index}`)) : collect(slideImage?.replacesContent ? { ...slide, image: void 0 } : slide, path);
1592
- const groups = [], flows = [];
1593
- const decisions = options.explain ? [] : void 0;
1594
- const inheritedSettings = (parent, own = {}) => ({
1595
- minFontSize: parent.minFontSize,
1596
- ...own,
1597
- overflow: parent.overflow === "error" ? "error" : own.overflow ?? parent.overflow
1598
- });
1599
- const inset = (box, settings) => {
1600
- const amount = (settings.padding ?? 0) * Math.min(box.width, box.height);
1601
- return { x: box.x + amount, y: box.y + amount, width: box.width - amount * 2, height: box.height - amount * 2 };
1602
- };
1603
- const acceptedBox = (box) => ({ x: round(box.x), y: round(box.y), width: round(box.width), height: round(box.height) });
1604
- const payloadBox = (box) => {
1605
- if (!hasCards) return box;
1606
- const frame = acceptedBox(box), padding2 = Math.min(12 * scale, frame.width / 4, frame.height / 4);
1607
- return acceptedBox({ x: frame.x + padding2, y: frame.y + padding2, width: frame.width - 2 * padding2, height: frame.height - 2 * padding2 });
1608
- };
1609
- const measureQuote = (node, box, settings) => layoutQuote(node.value, acceptedBox(box), {
1610
- fonts: options.fonts,
1611
- textMeasurement: options.textMeasurement,
1612
- scale,
1613
- minFontSize: settings.minFontSize,
1614
- path: node.path
1615
- });
1616
- const measureCode = (node, box, settings) => layoutCode(node.value, acceptedBox(box), {
1617
- fonts: options.fonts,
1618
- textMeasurement: options.textMeasurement,
1619
- scale,
1620
- minFontSize: settings.minFontSize,
1621
- path: node.path
1622
- });
1623
- const measureMetric = (node, box, settings) => layoutMetric(node.value, acceptedBox(box), {
1624
- fonts: options.fonts,
1625
- textMeasurement: options.textMeasurement,
1626
- scale,
1627
- minFontSize: settings.minFontSize,
1628
- path: node.path,
1629
- textRasterPadding: options.textRasterPadding,
1630
- align: alignmentFor("metric")
1631
- });
1632
- const measureTimeline = (node, box, settings) => layoutTimeline(node.value, acceptedBox(box), {
1633
- fonts: options.fonts,
1634
- textMeasurement: options.textMeasurement,
1635
- scale,
1636
- minFontSize: settings.minFontSize,
1637
- path: node.path,
1638
- textRasterPadding: options.textRasterPadding
1639
- });
1640
- const leafScore = (node, box, settings, penalties) => {
1641
- box = payloadBox(box);
1642
- const text = contentText(node.field, node.value);
1643
- let score = Math.abs(Math.log(box.width / box.height / 1.6));
1644
- if (penalties) penalties.cellProportions += score;
1645
- if (node.field === "quote" || node.field === "code" || node.field === "metric" || node.field === "timeline") {
1646
- const internal = node.field === "quote" ? measureQuote(node, box, settings) : node.field === "code" ? measureCode(node, box, settings) : node.field === "metric" ? measureMetric(node, box, settings) : measureTimeline(node, box, settings);
1647
- const reduction = internal.parts.reduce((sum, part) => sum + (part.fit ? (part.requestedFontSize - part.fit.fontSize) / scale : 0), 0);
1648
- score += reduction + (internal.overflow ? 1e3 : 0);
1649
- if (penalties) {
1650
- penalties.fontReduction += reduction;
1651
- penalties.textOverflow += internal.overflow ? 1e3 : 0;
1652
- }
1653
- } else if (text) {
1654
- const fit = fitContent(node.field, node.value, text, box, 25 * scale, (settings.minFontSize ?? 16) * scale, node.path);
1655
- const reduction = Math.max(0, 25 * scale - fit.fontSize) / scale;
1656
- score += reduction + (fit.overflow ? 1e3 : 0);
1657
- if (penalties) {
1658
- penalties.fontReduction += reduction;
1659
- penalties.textOverflow += fit.overflow ? 1e3 : 0;
1660
- }
1661
- }
1662
- if (node.field === "table" && tableOverflows(node.value, box, scale, settings, options, node.path)) {
1663
- score += 1e3;
1664
- if (penalties) penalties.tableOverflow += 1e3;
1665
- }
1666
- if (box.width < 100 * scale || box.height < 60 * scale) {
1667
- score += 100;
1668
- if (penalties) penalties.smallCells += 100;
1669
- }
1670
- return score;
1671
- };
1672
- const modeFor = (settings) => settings.mode ?? "auto";
1673
- const defaultColumns = (count, area2, settings) => {
1674
- const mode = modeFor(settings);
1675
- return mode === "column" ? 1 : mode === "row" ? Math.max(1, count) : settings.columns ?? Math.max(1, Math.min(count, Math.ceil(Math.sqrt(count * area2.width / area2.height / 1.6))));
1676
- };
1677
- const scoreNode = (node, box, settings, penalties) => {
1678
- if (!node.children) return leafScore(node, box, settings, penalties);
1679
- const own = inheritedSettings(settings, node.composition), area2 = inset(box, own);
1680
- const cols = defaultColumns(node.children.length, area2, own);
1681
- const boxes = gridBoxes(node.children.length, area2, cols, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), own.weights ?? [], modeFor(own) === "column");
1682
- return node.children.reduce((score, child, index) => score + scoreNode(child, boxes[index], own, penalties), 0);
1683
- };
1684
- const arrange = (nodes, area2, settings, reserved = 0, gapOverride, containerPath = path) => {
1685
- const count = Math.max(nodes.length, reserved);
1686
- if (!count) return;
1687
- const mode = modeFor(settings), localGap = (settings.gap ?? 1 / 30) * Math.min(area2.width, area2.height);
1688
- const actualGap = gapOverride ?? (settings === rootSettings ? gap : localGap);
1689
- let cols = defaultColumns(count, area2, settings);
1690
- const hasRegions = nodes.some((node) => node.region);
1691
- const candidates = decisions ? [] : void 0;
1692
- if (mode === "auto" && !hasRegions) {
1693
- let best = Infinity;
1694
- for (let candidate = 1; candidate <= Math.min(count, settings.columns ?? 6); candidate++) {
1695
- const boxes2 = gridBoxes(count, area2, candidate, actualGap, settings.weights ?? [], false);
1696
- const emptySlots = (Math.ceil(count / candidate) * candidate - count) * 2;
1697
- const penalties = candidates ? { cellProportions: 0, fontReduction: 0, textOverflow: 0, tableOverflow: 0, smallCells: 0, emptySlots } : void 0;
1698
- const score = nodes.reduce((sum, node, i) => sum + scoreNode(node, boxes2[i], settings, penalties), 0) + emptySlots;
1699
- if (penalties && candidates) candidates.push({ columns: candidate, rows: Math.ceil(count / candidate), score, penalties });
1700
- if (score < best) {
1701
- best = score;
1702
- cols = candidate;
1703
- }
1704
- }
1705
- }
1706
- decisions?.push({
1707
- path: containerPath,
1708
- mode: hasRegions ? "regions" : mode,
1709
- reason: hasRegions ? "promoted-regions" : mode === "auto" ? "lowest-score" : "configured-mode",
1710
- ...hasRegions ? {} : { selectedColumns: cols },
1711
- candidates: candidates ?? []
1712
- });
1713
- const grid = gridGeometry(count, area2, cols, actualGap, settings.weights ?? [], mode === "column");
1714
- const boxes = grid.boxes;
1715
- if (!nodes.some((node) => node.region)) flows.push({ path: containerPath, box: { ...area2 }, composition: { ...settings }, columns: grid.columns, rows: grid.rows, gap: grid.gap, itemCount: nodes.length, slotCount: count });
1716
- nodes.forEach((node, index) => {
1717
- let box = node.region ? regionBox(node.region, area2, actualGap) : boxes[index];
1718
- if (node.children) {
1719
- const own = inheritedSettings(settings, node.composition), inner = inset(box, own);
1720
- groups.push({ path: node.path, box, contentBox: inner, composition: own });
1721
- arrange(node.children, inner, own, 0, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), node.path);
1722
- } else {
1723
- const frameBox = hasCards ? acceptedBox(box) : void 0;
1724
- box = payloadBox(box);
1725
- const textValue = contentText(node.field, node.value);
1726
- const quoteLayout = node.field === "quote" ? measureQuote(node, box, settings) : void 0;
1727
- const codeLayout = node.field === "code" ? measureCode(node, box, settings) : void 0;
1728
- const metricLayout = node.field === "metric" ? measureMetric(node, box, settings) : void 0;
1729
- const timelineLayout = node.field === "timeline" ? measureTimeline(node, box, settings) : void 0;
1730
- const internal = quoteLayout ?? codeLayout ?? metricLayout ?? timelineLayout, body = internal?.parts.find((part) => part.role === "body" || part.role === "value");
1731
- const text = internal ? body?.fit : textValue !== void 0 ? fitContent(node.field, node.value, textValue, box, 25 * scale, (settings.minFontSize ?? 16) * scale, node.path) : void 0;
1732
- items.push({
1733
- path: node.path,
1734
- field: node.field,
1735
- type: node.type,
1736
- value: node.value,
1737
- payload: node.payload,
1738
- box: internal ? acceptedBox(box) : box,
1739
- ...frameBox ? { frameBox } : {},
1740
- text,
1741
- textStyle: body?.style ?? styleFor(node.field, node.path),
1742
- composition: settings,
1743
- alignment: alignmentFor(node.field),
1744
- ...quoteLayout ? { quoteLayout } : {},
1745
- ...codeLayout ? { codeLayout } : {},
1746
- ...metricLayout ? { metricLayout } : {},
1747
- ...timelineLayout ? { timelineLayout } : {}
1748
- });
1749
- 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." });
1750
- }
1751
- });
1752
- };
1753
- const placeholders = Array.isArray(layout.placeholders) ? layout.placeholders.filter((p) => !headings.has(p.type)) : [];
1754
- if (slideImage?.replacesContent) {
1755
- const picture = placeholders.findIndex((p) => p.type === "picture");
1756
- if (picture >= 0) placeholders.splice(picture, 1);
1757
- }
1758
- const rootSettings = { ...composition, mode: composition.mode ?? (layout.slideLayoutDirection === "Vertical" ? "column" : layout.slideLayoutDirection === "Horizontal" ? "row" : "auto") };
1759
- arrange(pending, contentBox, rootSettings, composition.mode ? 0 : placeholders.length);
1760
- for (const item of items) {
1761
- for (const key of ["x", "y", "width", "height"]) item.box[key] = round(item.box[key]);
1762
- if (item.field === "table" && tableOverflows(item.value, item.box, scale, item.composition, options, item.path)) 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." });
1763
- if (item.quoteLayout) diagnostics.push(...item.quoteLayout.diagnostics);
1764
- else if (item.codeLayout) diagnostics.push(...item.codeLayout.diagnostics);
1765
- else if (item.metricLayout) diagnostics.push(...item.metricLayout.diagnostics);
1766
- else if (item.timelineLayout) diagnostics.push(...item.timelineLayout.diagnostics);
1767
- else 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." });
1768
- }
1769
- 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]);
1770
- const strictPaths = new Set(items.filter((item) => item.composition.overflow === "error").map((item) => item.path));
1771
- const failures = diagnostics.filter((diagnostic) => {
1772
- if (composition.overflow === "error") return true;
1773
- let path2 = diagnostic.path;
1774
- while (path2) {
1775
- if (strictPaths.has(path2)) return true;
1776
- const boundary = path2.lastIndexOf(".");
1777
- if (boundary < 0) break;
1778
- path2 = path2.slice(0, boundary);
1779
- }
1780
- return false;
1781
- });
1782
- const explanation = decisions ? {
1783
- algorithm: "grid-score-v9",
1784
- textMeasurement: options.textMeasurement ? "provided" : "estimated",
1785
- textOutlines: options.textMeasurement?.outlineBounds ? "provided" : "unavailable",
1786
- textRasterPadding: rasterPadding,
1787
- decisions,
1788
- unmeasuredPayloads: items.filter((item) => !headings.has(item.field) && !item.quoteLayout && !item.codeLayout && !item.metricLayout && !item.timelineLayout && !["text", "items", "bullets", "table"].includes(item.field)).map((item) => item.path)
1789
- } : void 0;
1790
- if (failures.length) throw new OPFCompositionError(failures, explanation);
1791
- diagnostics.push(...slideImageDiagnostics);
1792
- return { width, height, contentBox, items, groups, flows, diagnostics, composition, ...furniture ? { furniture } : {}, ...slideImage ? { slideImage } : {}, ...explanation ? { explanation } : {} };
1793
- }
1794
- function resolveCanvasDimensions(input) {
1795
- const presets = {
1796
- widescreen: [40 / 3, 7.5],
1797
- "16:9": [40 / 3, 7.5],
1798
- standard: [10, 7.5],
1799
- "4:3": [10, 7.5],
1800
- "16:10": [10, 6.25],
1801
- letter: [11, 8.5],
1802
- a4: [11.69, 8.27]
1803
- };
1804
- const value = record2(input);
1805
- const preset = presets[typeof input === "string" ? input : value.preset] ?? presets.widescreen;
1806
- const width = (value.widthInches ?? preset[0]) * 96;
1807
- const height = (value.heightInches ?? preset[1]) * 96;
1808
- if (![width, height].every((n) => Number.isFinite(n) && n > 0)) throw new RangeError("Canvas dimensions must be finite and positive.");
1809
- return { width, height };
1810
- }
1811
-
1812
- export { DEFAULT_FONT_SCHEME, DEFAULT_FURNITURE_DATE_FORMAT, DEFAULT_SLIDE_NUMBER_FORMAT, MAX_COMPOSITION_DEPTH, OPFCompositionError, chartColorForFill, colorContrast, composeSlide, fitList, fitRichText, fitText, formatFurnitureDate, formatSlideNumber, layoutCode, layoutFurniture, layoutMetric, layoutQuote, layoutTable, layoutTimeline, measureText, measureTextOutline, normalizeHexColor, placeTextLines, resolveCanvasDimensions, resolveColorRef, resolveFontFamilies, resolveFontSchemeReference, resolveSocialProfile, resolveTextStyle, slideImageShape, tableGrid, tableRowBoundaries, textColorForFill, textWidthMeasurer, wrapText };