@openpresentation/opf 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +7 -1
  2. package/dist/{catalogs-BnnkUX9O.d.ts → catalogs-DoVmvDr7.d.ts} +6 -6
  3. package/dist/catalogs.d.ts +2 -2
  4. package/dist/catalogs.js +1 -1
  5. package/dist/{chunk-GQSK3T7X.js → chunk-D3GQIREP.js} +13 -4
  6. package/dist/chunk-EWJNRHXA.js +595 -0
  7. package/dist/{chunk-3GPZMY2W.js → chunk-FHGJX5QK.js} +24 -12
  8. package/dist/{chunk-JA5W5TVA.js → chunk-PQJRDNA6.js} +624 -73
  9. package/dist/{chunk-OZULJBSG.js → chunk-RS5GZ5RW.js} +14 -0
  10. package/dist/{chunk-TWMRZ43O.js → chunk-TU7I3KSB.js} +59 -21
  11. package/dist/{chunk-ZGV6I23P.js → chunk-ZIL7MGZU.js} +50 -5
  12. package/dist/composition.d.ts +257 -10
  13. package/dist/composition.js +1 -1
  14. package/dist/docs.js +58 -10
  15. package/dist/examples.js +2284 -1417
  16. package/dist/index.d.ts +4 -3
  17. package/dist/index.js +7 -6
  18. package/dist/lint.d.ts +70 -0
  19. package/dist/lint.js +5 -0
  20. package/dist/pagination.d.ts +4 -0
  21. package/dist/pagination.js +5 -5
  22. package/dist/{schemas--QRVBhU1.d.ts → schemas-BYe5y8-i.d.ts} +1 -1
  23. package/dist/schemas.d.ts +1 -1
  24. package/dist/schemas.js +1 -1
  25. package/dist/spec/catalogs/layouts/extract/canonical.json +45 -21
  26. package/dist/spec/catalogs/layouts/index.json +10 -0
  27. package/dist/spec/catalogs/layouts/number-1x.json +1 -1
  28. package/dist/spec/catalogs/layouts/number-2x.json +2 -2
  29. package/dist/spec/catalogs/layouts/number-3x.json +3 -3
  30. package/dist/spec/catalogs/layouts/number-4x.json +4 -4
  31. package/dist/spec/catalogs/layouts/number-5x.json +5 -5
  32. package/dist/spec/catalogs/layouts/number-6x.json +6 -6
  33. package/dist/spec/catalogs/layouts/quote-1x.json +13 -0
  34. package/dist/spec/catalogs/layouts/timeline-1x.json +13 -0
  35. package/dist/spec/schemas/layout.schema.json +12 -3
  36. package/dist/spec/schemas/narrative.schema.json +1 -1
  37. package/dist/spec-files.d.ts +1 -1
  38. package/dist/spec-files.js +1 -1
  39. package/dist/types.d.ts +2 -2
  40. package/dist/validator.d.ts +2 -2
  41. package/dist/validator.js +4 -4
  42. package/package.json +11 -6
@@ -35,6 +35,45 @@ function tableRowBoundaries(value) {
35
35
  return [...allowed];
36
36
  }
37
37
 
38
+ // src/color.ts
39
+ function luminance(value) {
40
+ if (typeof value !== "string") return void 0;
41
+ let hex = value.trim();
42
+ if (/^#[0-9a-f]{3}$/i.test(hex)) hex = `#${[...hex.slice(1)].map((c) => c + c).join("")}`;
43
+ if (/^#[0-9a-f]{6}ff$/i.test(hex)) hex = hex.slice(0, 7);
44
+ if (!/^#[0-9a-f]{6}$/i.test(hex)) return void 0;
45
+ return [0.2126, 0.7152, 0.0722].reduce((sum, weight, index) => {
46
+ const offset = index * 2 + 1;
47
+ const n = Number.parseInt(hex.slice(offset, offset + 2), 16) / 255;
48
+ return sum + weight * (n <= 0.04045 ? n / 12.92 : ((n + 0.055) / 1.055) ** 2.4);
49
+ }, 0);
50
+ }
51
+ function colorContrast(foreground, background) {
52
+ const a = luminance(foreground), b = luminance(background);
53
+ return a === void 0 || b === void 0 ? void 0 : (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
54
+ }
55
+ function textColorForFill(fill, preferred) {
56
+ const background = luminance(fill), contrast = colorContrast(preferred, fill);
57
+ if (background === void 0 || contrast === void 0 || contrast >= 4.5) return preferred;
58
+ return (background + 0.05) / 0.05 >= 1.05 / (background + 0.05) ? "#000000" : "#FFFFFF";
59
+ }
60
+ function chartColorForFill(fill, preferred) {
61
+ const contrast = colorContrast(preferred, fill);
62
+ if (contrast === void 0 || contrast >= 3) return preferred;
63
+ let hex = preferred.trim();
64
+ if (hex.length === 4) hex = `#${[...hex.slice(1)].map((c) => c + c).join("")}`;
65
+ const channels = [1, 3, 5].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16));
66
+ for (let step = 1; step <= 255; step++) {
67
+ const candidates = [0, 255].map((target) => {
68
+ const color = `#${channels.map((channel) => Math.round(channel + (target - channel) * step / 255).toString(16).padStart(2, "0")).join("")}`.toUpperCase();
69
+ return { color, contrast: colorContrast(color, fill) ?? 0 };
70
+ }).sort((a, b) => b.contrast - a.contrast);
71
+ const best = candidates[0];
72
+ if (best && best.contrast >= 3) return best.color;
73
+ }
74
+ return textColorForFill(fill, preferred);
75
+ }
76
+
38
77
  // src/composition.ts
39
78
  var MAX_COMPOSITION_DEPTH = 32;
40
79
  function resolveFontFamilies(input) {
@@ -57,6 +96,40 @@ function textWidthMeasurer(style, measurement) {
57
96
  return width;
58
97
  };
59
98
  }
99
+ function measureTextOutline(text, fontSize, style, measurement) {
100
+ if (measurement?.outlineBounds === void 0) return void 0;
101
+ if (typeof measurement.outlineBounds !== "function") throw new TypeError("Text outline provider must be a function.");
102
+ const bounds = measurement.outlineBounds(text, fontSize, style);
103
+ if (bounds === null) return null;
104
+ if (!bounds || ![bounds.x, bounds.y, bounds.width, bounds.height].every(Number.isFinite) || bounds.width < 0 || bounds.height < 0) {
105
+ throw new RangeError("Text outline measurement must return null or finite bounds with nonnegative dimensions.");
106
+ }
107
+ return { x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height };
108
+ }
109
+ function placeTextLines(lines, box, alignment = "left", rasterPadding = 0) {
110
+ 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.");
111
+ const factor = alignment === "right" ? 1 : alignment === "center" ? 0.5 : 0, placed = [];
112
+ let shift = 0, bottom = box.y, height = 0, overflow = false;
113
+ for (const line of lines) {
114
+ 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.");
115
+ const ink = line.outline;
116
+ 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.");
117
+ let x = box.x + (box.width - line.width) * factor;
118
+ if (ink) {
119
+ const low = box.x + rasterPadding - ink.x, high = box.x + box.width - rasterPadding - ink.x - ink.width;
120
+ if (low > high + 0.01) overflow = true;
121
+ else x = Math.max(low, Math.min(x, high));
122
+ shift += Math.max(0, bottom + rasterPadding - (box.y + line.baseline + shift + ink.y));
123
+ }
124
+ const y = box.y + line.y + shift, baseline = box.y + line.baseline + shift;
125
+ const outline = ink ? { ...ink, x: x + ink.x, y: baseline + ink.y } : null;
126
+ if (outline) bottom = outline.y + outline.height + rasterPadding;
127
+ height = Math.max(height, y + line.height - box.y, outline ? bottom - box.y : 0);
128
+ if (line.width > box.width + 0.01 || height > box.height + 0.01) overflow = true;
129
+ placed.push({ x, y, baseline, height: line.height, width: line.width, outline });
130
+ }
131
+ return { alignment, rasterPadding, lines: placed, height, overflow };
132
+ }
60
133
  var OPFCompositionError = class extends Error {
61
134
  constructor(diagnostics, explanation) {
62
135
  super("Slide content does not fit its composition.");
@@ -73,8 +146,105 @@ var headings = /* @__PURE__ */ new Set(["title", "subtitle", "tag"]);
73
146
  var rows = ["top", "middle", "bottom"];
74
147
  var columns = ["left", "center", "right"];
75
148
  var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
76
- var kind = (field) => field === "items" || field === "bullets" ? "list" : field;
149
+ var kind = (field) => field === "items" ? "list" : field === "bullets" ? "text" : field;
77
150
  var round = (value) => Math.round(value * 1e6) / 1e6 || value;
151
+ function layoutFurniture(input, options = {}) {
152
+ const slide = record2(input), width = options.width ?? 1280, height = options.height ?? 720, scale = Math.min(width, height) / 720;
153
+ const settings = { ...record2(record2(options.layout).composition), ...record2(slide.composition) };
154
+ assertComposition(settings);
155
+ const minimum = (settings.minFontSize ?? 16) * scale, size = Math.max(13 * scale, minimum), padding = (options.textRasterPadding ?? 2) * scale;
156
+ 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.");
157
+ const number = options.slideNumber ?? (options.slideIndex ?? 0) + 1;
158
+ if (!Number.isSafeInteger(number) || number < 1) throw new RangeError("Displayed slide number must be a positive safe integer.");
159
+ const outlines = options.textMeasurement?.outlineBounds !== void 0, parts = [], diagnostics = [];
160
+ const sourceRoot = `slides.${options.slideIndex ?? 0}`, organizations = Array.isArray(options.presentation?.organization) ? options.presentation.organization : [options.presentation?.organization];
161
+ const primaryIndex = organizations.findIndex((item) => record2(item).role === "primary"), organizationIndex = primaryIndex >= 0 ? primaryIndex : organizations.findIndex(Boolean);
162
+ const organization = record2(organizations[organizationIndex]), organizationPath = Array.isArray(options.presentation?.organization) ? `organization.${organizationIndex}.name` : "organization.name";
163
+ const fontFamily = options.fonts?.body ?? "sans-serif";
164
+ let headerBottom = 0, footerTop = height, configured = false;
165
+ const error = (path, message, code = "text-overflow") => diagnostics.push({ code, path, message });
166
+ for (const kind2 of ["header", "footer"]) {
167
+ const local = record2(slide.design)[kind2] !== void 0, value = local ? record2(slide.design)[kind2] : options.presentation?.design?.[kind2];
168
+ if (value === void 0 || value === false) continue;
169
+ const root = local ? `${sourceRoot}.design.${kind2}` : `design.${kind2}`;
170
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Header/footer content must be an object or false.");
171
+ configured = true;
172
+ const zones = [];
173
+ for (const [index, zone] of ["left", "center", "right"].entries()) {
174
+ const content = record2(record2(value)[zone]), path = `${root}.${zone}`, x = width * (0.07 + index * 0.3), zoneWidth = width * 0.26, zoneParts = [];
175
+ let y = 0;
176
+ const add = (field, text, generated = false, sourcePath) => {
177
+ if (text === void 0) return;
178
+ if (typeof text !== "string") throw new TypeError(`Furniture field ${path}.${field} requires string content.`);
179
+ const partPath = `${path}.${field}`, style = resolveTextStyle({ fontFamily, fontWeight: 400, italic: false, path: partPath }, options.textMeasurement);
180
+ 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));
181
+ const ink = fit.sourceLines.map((line, index2) => {
182
+ let outline = null;
183
+ if (outlines) {
184
+ for (const segment of line.segments) if (segment.kind === "text") {
185
+ const bounds = measureTextOutline(text.slice(segment.start, segment.end), size, style, options.textMeasurement);
186
+ if (bounds) {
187
+ const next = { ...bounds, x: bounds.x + segment.x };
188
+ if (!outline) outline = next;
189
+ else {
190
+ const right = Math.max(outline.x + outline.width, next.x + next.width), bottom = Math.max(outline.y + outline.height, next.y + next.height);
191
+ outline.x = Math.min(outline.x, next.x);
192
+ outline.y = Math.min(outline.y, next.y);
193
+ outline.width = right - outline.x;
194
+ outline.height = bottom - outline.y;
195
+ }
196
+ }
197
+ }
198
+ }
199
+ return { width: line.width, y: index2 * fit.lineHeight, baseline: size + index2 * fit.lineHeight, height: fit.lineHeight, outline };
200
+ });
201
+ const natural = outlines ? placeTextLines(ink, { x, y, width: zoneWidth, height: Number.MAX_VALUE }, zone, padding) : void 0;
202
+ const partHeight = natural?.height ?? fit.sourceLines.length * fit.lineHeight;
203
+ if (!Number.isFinite(partHeight)) throw new RangeError("Furniture exceeds finite layout coordinates.");
204
+ const box = { x, y, width: zoneWidth, height: Math.max(scale, partHeight) }, placement = outlines ? placeTextLines(ink, box, zone, padding) : void 0;
205
+ const accepted = { ...fit, ...placement ? { placement } : {}, overflow: fit.overflow || !!placement?.overflow };
206
+ 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 });
207
+ if (accepted.overflow) error(partPath, "Repeated text exceeds its zone at the selected readability floor; change the furniture or slide design.");
208
+ y += box.height;
209
+ };
210
+ if (content.image !== void 0) {
211
+ const box = { x, y, width: zoneWidth, height: Math.max(32 * scale, Math.min(height * 0.05, 72 * scale)) };
212
+ zoneParts.push({ type: "image", kind: kind2, zone, field: "image", path: `${path}.image`, sourcePath: `${path}.image`, generated: false, image: content.image, box, alignment: zone });
213
+ y += box.height;
214
+ }
215
+ add("text", content.text);
216
+ if (content.organization === true) {
217
+ if (typeof organization.name === "string") add("organization", organization.name, true, organizationPath);
218
+ else error(`${path}.organization`, "Generated organization name needs a named organization in the presentation.", "unresolved-content");
219
+ }
220
+ if (content.section === true) {
221
+ if (typeof slide.section === "string") add("section", slide.section, true, `${sourceRoot}.section`);
222
+ else error(`${path}.section`, "Generated section needs a literal slide section.", "unresolved-content");
223
+ }
224
+ if (content.slideNumber === true) add("slideNumber", String(number), true);
225
+ if (content.date === true) error(`${path}.date`, "Use a literal date string for deterministic header/footer content.", "unresolved-content");
226
+ else if (typeof content.date === "string") add("date", content.date);
227
+ zones.push(zoneParts);
228
+ }
229
+ const tallest = Math.max(0, ...zones.map((zone) => zone.reduce((sum, part) => sum + part.box.height, 0)));
230
+ if (!tallest) continue;
231
+ const top = kind2 === "header" ? height * 0.025 : Math.max(height * 0.025, height - height * 0.04 - tallest);
232
+ if (kind2 === "header") headerBottom = top + tallest;
233
+ else footerTop = top;
234
+ for (const zone of zones) for (const part of zone) {
235
+ part.box.y += top;
236
+ if (part.type === "text" && part.fit.placement) for (const line of part.fit.placement.lines) {
237
+ line.y += top;
238
+ line.baseline += top;
239
+ if (line.outline) line.outline.y += top;
240
+ }
241
+ 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.");
242
+ parts.push(part);
243
+ }
244
+ }
245
+ 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.");
246
+ return { algorithm: "furniture-flow-v2", configured, textMeasurement: options.textMeasurement ? "provided" : "estimated", textOutlines: outlines ? "provided" : "unavailable", parts, headerBottom, footerTop, diagnostics, overflow: diagnostics.length > 0 };
247
+ }
78
248
  var textSegments = new Intl.Segmenter("und", { granularity: "grapheme" });
79
249
  function measureText(text, fontSize) {
80
250
  let units = 0;
@@ -85,42 +255,22 @@ function measureText(text, fontSize) {
85
255
  return units * fontSize;
86
256
  }
87
257
  function wrapText(text, width, fontSize, measure = measureText) {
88
- const result = [];
89
- for (const paragraph of text.replace(/\r\n?/g, "\n").split("\n")) {
90
- let line = "";
91
- for (const word of paragraph.split(/\s+/u).filter(Boolean)) {
92
- if (line && measure(`${line} ${word}`, fontSize) > width) {
93
- result.push(line);
94
- line = "";
95
- }
96
- if (measure(word, fontSize) <= width) {
97
- line += (line ? " " : "") + word;
98
- continue;
99
- }
100
- for (const { segment: character } of textSegments.segment(word)) {
101
- if (line && measure(line + character, fontSize) > width) {
102
- result.push(line);
103
- line = "";
104
- }
105
- line += character;
106
- }
107
- }
108
- result.push(line);
109
- }
110
- return result;
258
+ return fitSourceText(text, { width, height: Number.MAX_VALUE }, fontSize, fontSize, 1, measure, true).lines;
111
259
  }
112
260
  function fitText(text, box, requestedSize = 25, minFontSize = 16, measure = measureText) {
113
261
  if (![box.width, box.height, requestedSize, minFontSize].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || requestedSize <= 0 || minFontSize <= 0) {
114
262
  throw new RangeError("Text dimensions and font sizes must be finite and positive.");
115
263
  }
116
- const minimum = Math.min(minFontSize, requestedSize);
117
- let fontSize = requestedSize;
118
- let lines = wrapText(text, box.width, fontSize, measure);
119
- while (fontSize > minimum && (lines.length * fontSize * 1.22 > box.height || lines.some((line) => measure(line, fontSize) > box.width))) {
120
- fontSize = Math.max(minimum, fontSize - 1);
121
- lines = wrapText(text, box.width, fontSize, measure);
264
+ return fitSourceText(text, box, Math.max(requestedSize, minFontSize), minFontSize, 1, measure, true);
265
+ }
266
+ function fitAtSizes(layout, requested, minimum, step = 1) {
267
+ const start = Math.max(requested, minimum);
268
+ 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.");
269
+ for (let trial = 0; trial <= 64; trial++) {
270
+ const size = trial === 64 ? minimum : Math.max(minimum, start - trial * step), result = layout(size);
271
+ if (!result.overflow || size === minimum) return result;
122
272
  }
123
- return { lines, fontSize, lineHeight: fontSize * 1.22, overflow: lines.length * fontSize * 1.22 > box.height + 0.01 || lines.some((line) => measure(line, fontSize) > box.width + 0.01) };
273
+ throw new Error("Text fitting did not evaluate its bounded floor trial.");
124
274
  }
125
275
  function layoutQuote(value, box, options = {}) {
126
276
  const shorthand = typeof value === "string";
@@ -226,10 +376,308 @@ function layoutQuote(value, box, options = {}) {
226
376
  if (diagnostics.length && options.overflow === "error") throw new OPFCompositionError(diagnostics);
227
377
  return { algorithm: "quote-flow-v1", textMeasurement: options.textMeasurement ? "provided" : "estimated", parts, diagnostics, overflow: diagnostics.length > 0 };
228
378
  }
379
+ function layoutMetric(value, box, options = {}) {
380
+ const scalar = typeof value === "string" || typeof value === "number";
381
+ const metric = scalar ? { value } : value;
382
+ const isValue = (input) => typeof input === "string" || typeof input === "number" && Number.isFinite(input);
383
+ 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)) {
384
+ throw new TypeError("Metric content requires a finite numeric or string value and schema-valid display metadata.");
385
+ }
386
+ const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale;
387
+ const rasterPadding = (options.textRasterPadding ?? 1) * scale, hasOutlines = options.textMeasurement?.outlineBounds !== void 0;
388
+ 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) {
389
+ throw new RangeError("Metric dimensions, scale and minimum font size must be finite and positive.");
390
+ }
391
+ if (!Number.isFinite(rasterPadding) || rasterPadding < 0) throw new RangeError("Metric raster padding must be finite and nonnegative.");
392
+ if (options.overflow !== void 0 && !["warn", "error"].includes(options.overflow)) throw new RangeError("Invalid metric overflow policy.");
393
+ const alignment = options.align ?? "left";
394
+ if (!["left", "center", "right"].includes(alignment)) throw new RangeError("Invalid metric alignment.");
395
+ const sourcePath = options.path ?? "metric", parts = [], diagnostics = [];
396
+ for (const role of ["value", "unit", "label", "description", "delta", "trend"]) {
397
+ const sourceValue = metric[role];
398
+ if (sourceValue === void 0) continue;
399
+ const text = String(sourceValue), path = scalar ? sourcePath : `${sourcePath}.${role}`;
400
+ const nominal = role === "value" ? Math.min(76 * scale, box.height * 0.28) : (role === "description" ? 20 : role === "trend" ? 18 : 23) * scale;
401
+ const requestedStyle = { fontFamily: (role === "value" ? options.fonts?.heading : options.fonts?.body) ?? "sans-serif", fontWeight: role === "value" ? 800 : role === "description" ? 400 : 500, italic: false, path };
402
+ const part = {
403
+ role,
404
+ path,
405
+ text,
406
+ sources: [{ path, value: sourceValue, start: 0, end: text.length }],
407
+ visible: role === "value" || text.length > 0,
408
+ linePositions: [],
409
+ box: { ...box, height: 0 },
410
+ requestedFontSize: Math.max(nominal, minimum),
411
+ minFontSize: minimum,
412
+ requestedStyle,
413
+ style: resolveTextStyle({ ...requestedStyle }, options.textMeasurement)
414
+ };
415
+ if (!part.visible) part.fit = { lines: [], sourceLines: [], fontSize: part.requestedFontSize, lineHeight: part.requestedFontSize * 1.22, tabSize: 4, tabWidth: 0, overflow: false };
416
+ parts.push(part);
417
+ }
418
+ const primary = parts[0], metadata = parts.filter((part) => part !== primary && part.visible), unit = metadata.find((part) => part.role === "unit");
419
+ 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;
420
+ const occupied = (fit) => fit.placement?.height ?? fit.lines.length * fit.lineHeight;
421
+ const gap = 8 * scale, primaryGap = 12 * scale;
422
+ const lineInk = (part, fit) => fit.sourceLines.map((line, index) => {
423
+ let outline = null;
424
+ for (const segment of line.segments) {
425
+ if (segment.kind !== "text") continue;
426
+ const bounds = measureTextOutline(part.text.slice(segment.start, segment.end), fit.fontSize, part.style, options.textMeasurement);
427
+ if (!bounds) continue;
428
+ const next = { ...bounds, x: segment.x + bounds.x };
429
+ if (!outline) outline = next;
430
+ else {
431
+ const right = Math.max(outline.x + outline.width, next.x + next.width), bottom = Math.max(outline.y + outline.height, next.y + next.height);
432
+ outline.x = Math.min(outline.x, next.x);
433
+ outline.y = Math.min(outline.y, next.y);
434
+ outline.width = right - outline.x;
435
+ outline.height = bottom - outline.y;
436
+ }
437
+ }
438
+ return { width: line.width, y: index * fit.lineHeight, baseline: fit.fontSize + index * fit.lineHeight, height: fit.lineHeight, outline };
439
+ });
440
+ const place = (part, fit, width) => {
441
+ if (!hasOutlines) return fit;
442
+ const placement = placeTextLines(lineInk(part, fit), { x: 0, y: 0, width, height: Number.MAX_VALUE }, alignment, rasterPadding);
443
+ return { ...fit, placement, overflow: placement.overflow };
444
+ };
445
+ const naturalWidth = (part, fit) => {
446
+ if (!hasOutlines) return Math.max(...fit.sourceLines.map((line) => line.width));
447
+ 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)));
448
+ return width > 0 ? width + 2 * rasterPadding : 0;
449
+ };
450
+ const cache = /* @__PURE__ */ new Map();
451
+ const measure = (part, size, width) => {
452
+ const key = JSON.stringify([part.role, size, width]);
453
+ let fit = cache.get(key);
454
+ if (!fit) {
455
+ fit = fitCodeText(part.text, { ...box, width: hasOutlines ? Math.max(Number.MIN_VALUE, width - 2 * rasterPadding) : width }, size, size, scale, textWidthMeasurer(part.style, options.textMeasurement));
456
+ fit = place(part, fit, width);
457
+ if (!Number.isFinite(occupied(fit))) throw new RangeError("Metric text layout exceeds finite coordinates.");
458
+ cache.set(key, fit);
459
+ }
460
+ return fit;
461
+ };
462
+ const fitValue = (area, singleLine) => {
463
+ if (!usable(area)) return void 0;
464
+ for (let step = 0; step <= 76; step++) {
465
+ const size = Math.max(minimum, primary.requestedFontSize - step * scale);
466
+ const natural = measure(primary, size, area.width);
467
+ 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) };
468
+ if (!fit.overflow || size === minimum) return fit;
469
+ }
470
+ };
471
+ let selected, attempts = 0;
472
+ const reductions = Math.min(23, Math.ceil(Math.max(0, ...metadata.map((part) => (part.requestedFontSize - minimum) / scale))));
473
+ for (let reduction = 0; reduction <= reductions; reduction++) {
474
+ const measured = new Map(metadata.map((part) => [part, measure(part, Math.max(minimum, part.requestedFontSize - reduction * scale), box.width)]));
475
+ const measuredUnit = unit ? measured.get(unit) : void 0;
476
+ const unitWidth = unit && measuredUnit ? naturalWidth(unit, measuredUnit) : 0;
477
+ const unitFit = unit && measuredUnit && unitWidth > 0 ? place(unit, measuredUnit, unitWidth) : measuredUnit;
478
+ const inline = unitFit?.lines.length === 1 && unitWidth > 0 && unitWidth <= box.width * 0.35 && unitWidth + gap < box.width;
479
+ for (const arrangement of inline ? ["inline-unit", "stacked"] : ["stacked"]) {
480
+ attempts++;
481
+ const tail = metadata.filter((part) => arrangement !== "inline-unit" || part !== unit);
482
+ const tailHeight = tail.reduce((sum, part) => sum + occupied(measured.get(part)), 0) + Math.max(0, tail.length - 1) * gap;
483
+ if (!Number.isFinite(tailHeight)) throw new RangeError("Metric metadata exceeds finite coordinates.");
484
+ const area = { ...box, width: arrangement === "inline-unit" ? box.width - unitWidth - gap : box.width, height: box.height - tailHeight - (tail.length ? primaryGap : 0) };
485
+ const valueFit = fitValue(area, arrangement === "inline-unit"), valueHeight = valueFit ? occupied(valueFit) : 0;
486
+ const allocations = [];
487
+ let primaryHeight = valueHeight;
488
+ if (arrangement === "inline-unit" && unit && unitFit && valueFit) {
489
+ const valueBaseline = valueFit.placement?.lines.at(-1)?.baseline ?? valueHeight - valueFit.lineHeight + valueFit.fontSize;
490
+ const unitBaseline = unitFit.placement?.lines.at(-1)?.baseline ?? occupied(unitFit) - unitFit.lineHeight + unitFit.fontSize;
491
+ const valueY = box.y + Math.max(0, unitBaseline - valueBaseline), unitY = box.y + Math.max(0, valueBaseline - unitBaseline);
492
+ const measuredWidth = naturalWidth(primary, valueFit);
493
+ const valueWidth = Math.min(area.width, measuredWidth || valueFit.fontSize);
494
+ allocations.push({ part: primary, box: { ...area, y: valueY, width: valueWidth, height: valueHeight }, fit: place(primary, valueFit, valueWidth) });
495
+ allocations.push({ part: unit, box: { x: box.x + valueWidth + gap, y: unitY, width: unitWidth, height: occupied(unitFit) }, fit: { ...unitFit, overflow: !!unitFit.placement?.overflow } });
496
+ primaryHeight = Math.max(valueY - box.y + valueHeight, unitY - box.y + occupied(unitFit));
497
+ } else allocations.push({ part: primary, box: valueFit ? { ...area, height: valueHeight } : area, fit: valueFit });
498
+ let y = box.y + primaryHeight + (tail.length ? primaryGap : 0);
499
+ for (const part of tail) {
500
+ const natural = measured.get(part), height = occupied(natural);
501
+ allocations.push({ part, box: { ...box, y, height }, fit: { ...natural, overflow: !!natural.placement?.overflow || natural.sourceLines.some((line) => line.width > box.width + 0.01) } });
502
+ y += height + gap;
503
+ }
504
+ 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);
505
+ const score = allocations.reduce((sum, { part, fit }) => sum + (part.requestedFontSize - (fit?.fontSize ?? minimum)) / scale, 0);
506
+ const candidate = { arrangement, allocations, score, overflow };
507
+ if (!selected || !overflow && (selected.overflow || score < selected.score) || overflow && selected.overflow) selected = candidate;
508
+ }
509
+ if (selected && !selected.overflow && selected.score === 0) break;
510
+ }
511
+ for (const allocation of selected.allocations) {
512
+ allocation.part.box = allocation.box;
513
+ allocation.part.fit = allocation.fit;
514
+ }
515
+ const alignmentFactor = alignment === "center" ? 0.5 : alignment === "right" ? 1 : 0;
516
+ if (selected.arrangement === "inline-unit" && unit) {
517
+ const offset = (box.width - (unit.box.x + unit.box.width - box.x)) * alignmentFactor;
518
+ primary.box.x += offset;
519
+ unit.box.x += offset;
520
+ }
521
+ for (const part of parts) if (part.fit) {
522
+ if (part.fit.placement) {
523
+ 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 })) } };
524
+ part.linePositions = part.fit.placement.lines.map(({ x, baseline }) => ({ x, baseline }));
525
+ } else part.linePositions = part.fit.sourceLines.map((line, index) => ({
526
+ x: part.box.x + (part.box.width - line.width) * alignmentFactor,
527
+ baseline: part.box.y + part.fit.fontSize + index * part.fit.lineHeight
528
+ }));
529
+ }
530
+ const report = (reason, part, roles, message) => diagnostics.push({ code: "text-overflow", reason, path: part.path, parts: roles, message });
531
+ for (const part of parts.filter((part2) => part2.visible)) {
532
+ 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.`);
533
+ 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.`);
534
+ 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)) {
535
+ report("part-outside-cell", part, [part.role], `Metric ${part.role} extends outside its cell; increase the cell or change the arrangement.`);
536
+ }
537
+ }
538
+ const visible = parts.filter((part) => part.visible && part.fit);
539
+ for (const [index, first] of visible.entries()) for (const second of visible.slice(index + 1)) {
540
+ 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) {
541
+ report("part-overlap", first, [first.role, second.role], "Metric part line boxes overlap; do not accept this layout without more space.");
542
+ }
543
+ }
544
+ if (diagnostics.length && options.overflow === "error") throw new OPFCompositionError(diagnostics);
545
+ return { algorithm: "metric-flow-v1", alignment, textMeasurement: options.textMeasurement ? "provided" : "estimated", arrangement: selected.arrangement, attempts, parts, diagnostics, overflow: diagnostics.length > 0 };
546
+ }
547
+ function layoutTimeline(value, box, options = {}) {
548
+ const shorthand = Array.isArray(value), timeline = shorthand ? { events: value } : value;
549
+ 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"))) {
550
+ throw new TypeError("Timeline content requires ordered events with string labels and optional string metadata.");
551
+ }
552
+ const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale, padding = (options.textRasterPadding ?? 1) * scale;
553
+ 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.");
554
+ if (options.overflow !== void 0 && !["warn", "error"].includes(options.overflow)) throw new RangeError("Invalid timeline overflow policy.");
555
+ const outlines = options.textMeasurement?.outlineBounds !== void 0;
556
+ if (outlines && typeof options.textMeasurement?.outlineBounds !== "function") throw new TypeError("Text outline provider must be a function.");
557
+ const path = options.path ?? "timeline", eventPath = (index) => shorthand ? `${path}.${index}` : `${path}.events.${index}`;
558
+ const fonts = resolveFontFamilies(options.fonts), source = [];
559
+ const add = (role, text, partPath, size, weight, eventIndex) => {
560
+ if (text === void 0) return;
561
+ const requestedStyle = { fontFamily: fonts.body, fontWeight: weight, italic: false, path: partPath };
562
+ 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) });
563
+ };
564
+ add("name", timeline.name, `${path}.name`, 24, 700);
565
+ add("description", timeline.description, `${path}.description`, 18, 400);
566
+ timeline.events.forEach((event, index) => {
567
+ add("when", event.when, `${eventPath(index)}.when`, 16, 500, index);
568
+ add("what", event.what, `${eventPath(index)}.what`, 16, 500, index);
569
+ add("event-description", event.description, `${eventPath(index)}.description`, 16, 500, index);
570
+ });
571
+ const metadata = [], events = timeline.events.map(() => []);
572
+ for (const part of source) if (part.eventIndex === void 0) metadata.push(part);
573
+ else events[part.eventIndex].push(part);
574
+ const cache = /* @__PURE__ */ new Map();
575
+ const measure = (part, size, width, alignment) => {
576
+ const key = JSON.stringify([part.path, size, width, alignment]);
577
+ let measured = cache.get(key);
578
+ if (measured) return measured;
579
+ 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));
580
+ const ink = fit.sourceLines.map((line, index) => {
581
+ let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity, hasInk = false;
582
+ if (outlines) {
583
+ for (const segment of line.segments) if (segment.kind === "text") {
584
+ const outline = measureTextOutline(part.text.slice(segment.start, segment.end), size, part.style, options.textMeasurement);
585
+ if (outline) {
586
+ hasInk = true;
587
+ left = Math.min(left, outline.x + segment.x);
588
+ top = Math.min(top, outline.y);
589
+ right = Math.max(right, outline.x + segment.x + outline.width);
590
+ bottom = Math.max(bottom, outline.y + outline.height);
591
+ }
592
+ }
593
+ }
594
+ 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 };
595
+ });
596
+ const placement = outlines ? placeTextLines(ink, { x: 0, y: 0, width, height: Number.MAX_VALUE }, alignment, padding) : void 0;
597
+ const height = placement?.height ?? fit.sourceLines.length * fit.lineHeight;
598
+ if (!Number.isFinite(height)) throw new RangeError("Timeline text exceeds finite layout coordinates.");
599
+ measured = { fit, ink, height };
600
+ cache.set(key, measured);
601
+ return measured;
602
+ };
603
+ let selected, attempts = 0;
604
+ let reductionLimit = 0;
605
+ for (const part of source) reductionLimit = Math.max(reductionLimit, (part.requestedFontSize - minimum) / scale);
606
+ const reductions = Math.min(24, Math.ceil(reductionLimit));
607
+ for (let reduction = 0; reduction <= reductions; reduction++) {
608
+ for (const arrangement of ["alternating", "vertical"]) {
609
+ attempts++;
610
+ const parts = [], markers = [], diagnostics = [];
611
+ let score2 = 0;
612
+ const report = (part, reason, message, excess = 1) => {
613
+ diagnostics.push({ code: "text-overflow", reason, path: part.path, message });
614
+ score2 += Math.max(1, excess);
615
+ };
616
+ const place = (part, x, y2, width, alignment) => {
617
+ const measured = measure(part, Math.max(minimum, part.requestedFontSize - reduction * scale), Math.max(scale, width), alignment);
618
+ 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;
619
+ const fit = { ...measured.fit, ...placement ? { placement } : {}, overflow: measured.fit.overflow || !!placement?.overflow };
620
+ const accepted = { ...part, box: area, alignment, fit };
621
+ parts.push(accepted);
622
+ if (fit.overflow) report(accepted, "text-fit", "Timeline field exceeds its readable width; increase its space or split the timeline.");
623
+ 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));
624
+ return accepted;
625
+ };
626
+ let y = box.y;
627
+ for (const part of metadata) {
628
+ const placed = place(part, box.x, y, box.width, "center");
629
+ y += placed.box.height + 8 * scale;
630
+ }
631
+ 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);
632
+ if (arrangement === "alternating") {
633
+ 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;
634
+ const lineY = y + Math.max(scale, available) * 0.46;
635
+ events.forEach((fields2, index) => {
636
+ const x = start + index * step, top = index % 2 === 0 ? y : lineY + 24 * scale, bottom = index % 2 === 0 ? lineY - 24 * scale : box.y + box.height;
637
+ markers.push({ path: eventPath(index), eventIndex: index, x, y: lineY, radius });
638
+ let cursor = top;
639
+ for (const part of fields2) {
640
+ const placed = place(part, x - width / 2, cursor, width, "center");
641
+ cursor += placed.box.height;
642
+ 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);
643
+ }
644
+ });
645
+ } else {
646
+ const x = box.x + radius, textX = box.x + 24 * scale, width = box.width - 24 * scale;
647
+ events.forEach((fields2, index) => {
648
+ const top = y;
649
+ let first2;
650
+ for (const part of fields2) {
651
+ const placed = place(part, textX, y, width, "left");
652
+ first2 ??= placed;
653
+ y += placed.box.height;
654
+ }
655
+ markers.push({ path: eventPath(index), eventIndex: index, x, y: top + Math.min(first2?.box.height ?? 2 * radius, 2 * radius) / 2, radius });
656
+ y += 16 * scale;
657
+ });
658
+ }
659
+ 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) {
660
+ diagnostics.push({ code: "text-overflow", reason: "event-space", path: marker.path, message: "Timeline marker has no usable space; increase the cell or paginate events." });
661
+ score2 += 1;
662
+ }
663
+ const first = markers[0], last = markers.at(-1), connector = { x1: first.x, y1: first.y, x2: last.x, y2: last.y };
664
+ const candidate = { arrangement, parts, markers, connector, diagnostics, score: score2 };
665
+ if (!selected || score2 < selected.score) selected = candidate;
666
+ if (!diagnostics.length) break;
667
+ }
668
+ if (!selected.diagnostics.length) break;
669
+ }
670
+ const { score, ...result } = selected;
671
+ if (result.diagnostics.length && options.overflow === "error") throw new OPFCompositionError(result.diagnostics);
672
+ return { algorithm: "timeline-flow-v1", attempts, textMeasurement: options.textMeasurement ? "provided" : "estimated", textOutlines: outlines ? "provided" : "unavailable", ...result, overflow: result.diagnostics.length > 0 };
673
+ }
229
674
  function fitCodeText(text, box, size, minimum, step, measure) {
675
+ return fitSourceText(text, box, size, minimum, step, measure);
676
+ }
677
+ function fitSourceText(text, box, size, minimum, step, measure, prose = false) {
230
678
  const layout = (fontSize) => {
231
679
  const tabWidth = measure(" ", fontSize) * 4;
232
- if (!Number.isFinite(tabWidth) || text.includes(" ") && tabWidth <= 0) throw new RangeError("Code tabs require a positive finite measured space advance.");
680
+ if (!Number.isFinite(tabWidth) || text.includes(" ") && tabWidth <= 0) throw new RangeError("Text tabs require a positive finite measured space advance.");
233
681
  const measureLine = (start2, end2, record3 = false) => {
234
682
  let x = 0, offset = start2;
235
683
  const segments = [];
@@ -257,7 +705,8 @@ function fitCodeText(text, box, size, minimum, step, measure) {
257
705
  start = nextStart;
258
706
  end = nextStart;
259
707
  };
260
- for (const token of text.matchAll(/\r\n|\r|\n|[^\S\r\n]+|[^\s]+/gu)) {
708
+ 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;
709
+ for (const token of text.matchAll(tokens)) {
261
710
  if (token.index === void 0) throw new Error("Missing code token source offset.");
262
711
  const a = token.index, b = a + token[0].length;
263
712
  if (/^[\r\n]/.test(token[0])) {
@@ -273,6 +722,10 @@ function fitCodeText(text, box, size, minimum, step, measure) {
273
722
  end = b;
274
723
  continue;
275
724
  }
725
+ if (prose && /[\u00a0\u202f\u2060\ufeff]/u.test(token[0])) {
726
+ end = b;
727
+ continue;
728
+ }
276
729
  for (const segment of textSegments.segment(token[0])) {
277
730
  const next = a + segment.index + segment.segment.length;
278
731
  if (end > start && width(start, next) > box.width + 0.01) push(a + segment.index, a + segment.index, "soft");
@@ -291,12 +744,7 @@ function fitCodeText(text, box, size, minimum, step, measure) {
291
744
  overflow: sourceLines.length * lineHeight > box.height + 0.01 || sourceLines.some((line) => line.width > box.width + 0.01)
292
745
  };
293
746
  };
294
- let result = layout(size);
295
- while (result.overflow && size > minimum) {
296
- size = Math.max(minimum, size - step);
297
- result = layout(size);
298
- }
299
- return result;
747
+ return fitAtSizes(layout, size, minimum, step);
300
748
  }
301
749
  function layoutCode(value, box, options = {}) {
302
750
  const shorthand = typeof value === "string", code = shorthand ? { source: value } : value;
@@ -376,13 +824,19 @@ function layoutCode(value, box, options = {}) {
376
824
  }
377
825
  function fitRichText(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
378
826
  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.");
379
- const layout = richTextLayouter(input, box, requestedSize, options), minimum = Math.min(requestedSize, minFontSize);
380
- let size = requestedSize, result = layout(size);
381
- while (result.overflow && size > minimum) {
382
- size = Math.max(minimum, size - 1);
383
- result = layout(size);
827
+ const layout = richTextLayouter(input, box, requestedSize, options);
828
+ return fitAtSizes(layout, requestedSize, richMinimum(input, requestedSize, minFontSize));
829
+ }
830
+ function richMinimum(input, requested, minimum) {
831
+ let floor = Math.min(requested, minimum), visible = false;
832
+ for (const value of input) {
833
+ const run = typeof value === "string" ? { text: value } : value;
834
+ if (!run.text) continue;
835
+ const ratio = (run.fontSize === void 0 ? 1 : run.fontSize * 4 / 3 / requested) * (run.superscript || run.subscript ? 0.7 : 1);
836
+ floor = Math.max(floor, minimum / ratio);
837
+ visible = true;
384
838
  }
385
- return result;
839
+ return visible ? floor : minimum;
386
840
  }
387
841
  function richTextLayouter(input, box, requestedSize, options) {
388
842
  let offset = 0;
@@ -516,13 +970,12 @@ function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { s
516
970
  const height = y - box.y;
517
971
  return { listEntries: entries, lines, fontSize, lineHeight: fontSize * 1.22, height, overflow: overflow || height > box.height + 0.01 };
518
972
  };
519
- const minimum = Math.min(requestedSize, minFontSize);
520
- let size = requestedSize, result = layout(size);
521
- while (result.overflow && size > minimum) {
522
- size = Math.max(minimum, size - 1);
523
- result = layout(size);
973
+ let minimum = minFontSize;
974
+ for (const item of source) {
975
+ minimum = Math.max(minimum, richMinimum(item.runs, requestedSize, minFontSize));
976
+ if (item.descriptionRuns !== void 0) minimum = Math.max(minimum, richMinimum(item.descriptionRuns, requestedSize * 0.82, minFontSize) / 0.82);
524
977
  }
525
- return result;
978
+ return fitAtSizes(layout, requestedSize, minimum);
526
979
  }
527
980
  function contentText(field, value) {
528
981
  if (field === "text" || headings.has(field)) return flatten(value);
@@ -536,7 +989,7 @@ ${flatten(entry.description)}` : ""}`;
536
989
  return void 0;
537
990
  }
538
991
  function layoutTable(value, box, options = {}) {
539
- const scale = options.scale ?? 1, requested = 15 * scale, minimum = (options.minFontSize ?? 16) * scale;
992
+ const scale = options.scale ?? 1, minimum = (options.minFontSize ?? 16) * scale, requested = Math.max(15 * scale, minimum);
540
993
  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.");
541
994
  const grid = tableGrid(value, options.path ?? "table");
542
995
  if (grid.issues.length) throw new RangeError(`${grid.issues[0].path}: ${grid.issues[0].message}`);
@@ -547,20 +1000,26 @@ function layoutTable(value, box, options = {}) {
547
1000
  const style = { fontFamily: options.fontFamily ?? "sans-serif", fontWeight: cell.header ? 700 : 400, italic: false, path: cell.valuePath };
548
1001
  return { ...cell, padding, width, textStyle: style };
549
1002
  });
1003
+ const cellMinimum = (cell) => Array.isArray(cell.value) ? richMinimum(cell.value, requested, minimum) : minimum;
550
1004
  const fitCell = (cell, height, min) => {
551
1005
  const textBox = { width: Math.max(scale, cell.width), height: Math.max(scale, height) };
552
1006
  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));
553
1007
  };
554
1008
  const textHeight = (fit) => "height" in fit ? fit.height : fit.lines.length * fit.lineHeight;
555
- const required = (cell, size) => textHeight(fitCell(cell, scale, size)) + (cell.padding.top + cell.padding.bottom) * scale;
1009
+ const required = (cell, natural2) => {
1010
+ const floor = cellMinimum(cell), size = natural2 ? Math.max(requested, floor) : floor;
1011
+ const textBox = { width: Math.max(scale, cell.width), height: scale };
1012
+ 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));
1013
+ return textHeight(fit) + (cell.padding.top + cell.padding.bottom) * scale;
1014
+ };
556
1015
  const natural = Array(grid.rowCount).fill(0), needed = Array(grid.rowCount).fill(0);
557
1016
  for (const cell of cells) if (cell.rowSpan === 1) {
558
- natural[cell.row] = Math.max(natural[cell.row], required(cell, requested));
559
- needed[cell.row] = Math.max(needed[cell.row], required(cell, minimum));
1017
+ natural[cell.row] = Math.max(natural[cell.row], required(cell, true));
1018
+ needed[cell.row] = Math.max(needed[cell.row], required(cell, false));
560
1019
  }
561
- for (const cell of cells) if (cell.rowSpan > 1) for (const [heights2, size] of [[natural, requested], [needed, minimum]]) {
1020
+ for (const cell of cells) if (cell.rowSpan > 1) for (const [heights2, preferredSize] of [[natural, true], [needed, false]]) {
562
1021
  const current = heights2.slice(cell.row, cell.row + cell.rowSpan).reduce((a, b) => a + b, 0);
563
- const extra = Math.max(0, required(cell, size) - current) / cell.rowSpan;
1022
+ const extra = Math.max(0, required(cell, preferredSize) - current) / cell.rowSpan;
564
1023
  for (let r = cell.row; r < cell.row + cell.rowSpan; r++) heights2[r] += extra;
565
1024
  }
566
1025
  const preferred = natural.map((height) => Math.max(54 * scale, height));
@@ -650,33 +1109,90 @@ function composeSlide(input, options = {}) {
650
1109
  const width = options.width ?? 1280, height = options.height ?? 720;
651
1110
  if (![width, height].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("Canvas dimensions must be finite and positive.");
652
1111
  const scale = Math.min(width, height) / 720;
1112
+ const hasCards = record2(slide.design).contentBox ?? options.contentBox ?? false;
653
1113
  const composition = { ...record2(layout.composition), ...record2(slide.composition) };
654
1114
  assertComposition(composition);
655
1115
  const padding = (composition.padding ?? 0.08) * Math.min(width, height);
656
1116
  const gap = (composition.gap ?? 1 / 30) * Math.min(width, height);
657
1117
  const minSize = (composition.minFontSize ?? 16) * scale;
1118
+ const rasterPadding = (options.textRasterPadding ?? 1) * scale;
1119
+ if (!Number.isFinite(rasterPadding) || rasterPadding < 0) throw new RangeError("Text raster padding must be finite and nonnegative.");
658
1120
  const styleFor = (field, path2) => resolveTextStyle({
659
1121
  fontFamily: (field === "title" ? options.fonts?.heading : field === "code" ? options.fonts?.code : options.fonts?.body) ?? (field === "code" ? "monospace" : "sans-serif"),
660
1122
  fontWeight: field === "title" ? 700 : 400,
661
1123
  path: path2
662
1124
  }, options.textMeasurement);
663
1125
  const widthFor = (field, path2) => textWidthMeasurer(styleFor(field, path2), options.textMeasurement);
664
- const fitContent = (field, value, text, box, size, minimum, path2) => field === "text" && Array.isArray(value) ? fitRichText(value, box, size, minimum, { style: styleFor(field, path2), textMeasurement: options.textMeasurement }) : field === "items" || field === "bullets" ? fitList(value, box, size, minimum, { style: styleFor(field, path2), textMeasurement: options.textMeasurement }) : fitText(text, box, size, minimum, widthFor(field, path2));
1126
+ const fitPlacedText = (field, value, text, box, size, minimum, path2) => {
1127
+ const style = styleFor(field, path2), rich = field === "text" && Array.isArray(value);
1128
+ if (!options.textMeasurement?.outlineBounds) return rich ? fitRichText(value, box, size, minimum, { style, textMeasurement: options.textMeasurement }) : fitText(text, box, size, minimum, textWidthMeasurer(style, options.textMeasurement));
1129
+ const alignment = (field === "title" ? record2(slide.design).titleAlignment ?? options.titleAlignment : record2(slide.design).contentAlignment ?? options.contentAlignment) ?? "left";
1130
+ const richLayout = rich ? richTextLayouter(value, box, size, { style, textMeasurement: options.textMeasurement }) : void 0;
1131
+ const measure = textWidthMeasurer(style, options.textMeasurement), floor = rich ? richMinimum(value, size, minimum) : minimum, start = Math.max(size, floor);
1132
+ for (let trial = 0; trial <= 64; trial++) {
1133
+ const fontSize = trial === 64 ? floor : Math.max(floor, start - trial * scale);
1134
+ const fit = richLayout ? richLayout(fontSize) : fitText(text, box, fontSize, fontSize, measure);
1135
+ const richLines = "richLines" in fit ? fit.richLines : void 0;
1136
+ const lines = richLines ? richLines.map((line) => {
1137
+ let outline = null;
1138
+ for (const fragment of line.fragments) {
1139
+ const bounds = measureTextOutline(fragment.text, fragment.fontSize, fragment.style, options.textMeasurement);
1140
+ if (!bounds) continue;
1141
+ const next = { ...bounds, x: fragment.x + bounds.x, y: fragment.baselineShift + bounds.y };
1142
+ if (!outline) outline = next;
1143
+ else {
1144
+ const right = Math.max(outline.x + outline.width, next.x + next.width), bottom = Math.max(outline.y + outline.height, next.y + next.height);
1145
+ outline.x = Math.min(outline.x, next.x);
1146
+ outline.y = Math.min(outline.y, next.y);
1147
+ outline.width = right - outline.x;
1148
+ outline.height = bottom - outline.y;
1149
+ }
1150
+ }
1151
+ return { width: line.width, y: line.y, baseline: line.baseline, height: line.height, outline };
1152
+ }) : fit.sourceLines.map((line, index) => {
1153
+ let outline = null;
1154
+ for (const segment of line.segments) {
1155
+ if (segment.kind === "tab") continue;
1156
+ const bounds = measureTextOutline(text.slice(segment.start, segment.end), fontSize, style, options.textMeasurement);
1157
+ if (!bounds) continue;
1158
+ const next = { ...bounds, x: segment.x + bounds.x };
1159
+ if (!outline) outline = next;
1160
+ else {
1161
+ const right = Math.max(outline.x + outline.width, next.x + next.width), bottom = Math.max(outline.y + outline.height, next.y + next.height);
1162
+ outline.x = Math.min(outline.x, next.x);
1163
+ outline.y = Math.min(outline.y, next.y);
1164
+ outline.width = right - outline.x;
1165
+ outline.height = bottom - outline.y;
1166
+ }
1167
+ }
1168
+ return { width: line.width, y: index * fit.lineHeight, baseline: fontSize + index * fit.lineHeight, height: fit.lineHeight, outline };
1169
+ });
1170
+ const placement = placeTextLines(lines, box, alignment, rasterPadding), result = { ...fit, placement, overflow: fit.overflow || placement.overflow };
1171
+ if (!result.overflow || fontSize === floor) return result;
1172
+ }
1173
+ throw new Error("Text placement did not evaluate its bounded floor trial.");
1174
+ };
1175
+ 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));
665
1176
  const path = `slides.${options.slideIndex ?? 0}`;
666
1177
  const items = [], diagnostics = [];
667
- let y = padding;
1178
+ const measuredFurniture = layoutFurniture(slide, options);
1179
+ const furniture = measuredFurniture.configured || measuredFurniture.diagnostics.length ? measuredFurniture : void 0;
1180
+ if (furniture) diagnostics.push(...furniture.diagnostics);
1181
+ const bodyBottom = Math.min(height - padding, furniture && furniture.footerTop < height ? furniture.footerTop - gap * 0.5 : height - padding);
1182
+ let y = Math.max(padding, furniture?.headerBottom ? furniture.headerBottom + gap * 0.5 : padding);
668
1183
  for (const field of ["tag", "title", "subtitle"]) {
669
1184
  if (!slide[field]) continue;
670
1185
  const requested = (field === "title" ? 54 : field === "tag" ? 16 : 25) * scale;
671
- const maxHeight = height * (field === "title" ? 0.26 : field === "subtitle" ? 0.12 : 0.045);
1186
+ const maxHeight = Math.max(height * (field === "title" ? 0.26 : field === "subtitle" ? 0.12 : 0.045), minSize * 1.22 + 2 * rasterPadding);
672
1187
  const box = { x: padding, y, width: width - padding * 2, height: maxHeight };
673
- const text = fitText(String(slide[field]), box, requested, minSize, widthFor(field, `${path}.${field}`));
674
- box.height = Math.min(maxHeight, text.lines.length * text.lineHeight);
1188
+ const text = fitPlacedText(field, slide[field], String(slide[field]), box, requested, minSize, `${path}.${field}`);
1189
+ box.height = Math.min(maxHeight, Math.max(text.lines.length * text.lineHeight, text.placement?.height ?? 0));
1190
+ 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." });
675
1191
  items.push({ path: `${path}.${field}`, field, type: "text", value: slide[field], payload: { text: slide[field] }, box, text, textStyle: styleFor(field, `${path}.${field}`), composition });
676
1192
  y += box.height + gap * 0.5;
677
1193
  }
678
1194
  if (items.length) y += gap * 0.5;
679
- const contentBox = { x: padding, y, width: width - padding * 2, height: Math.max(scale, height - padding - y) };
1195
+ const contentBox = { x: padding, y, width: width - padding * 2, height: Math.max(scale, bodyBottom - y) };
680
1196
  const collect = (host, basePath, depth = 0, ancestors = []) => {
681
1197
  if (Array.isArray(host.blocks)) {
682
1198
  if (depth >= MAX_COMPOSITION_DEPTH || ancestors.includes(host)) throw new RangeError(`Content groups must be acyclic and nest at most ${MAX_COMPOSITION_DEPTH} levels.`);
@@ -708,6 +1224,11 @@ function composeSlide(input, options = {}) {
708
1224
  return { x: box.x + amount, y: box.y + amount, width: box.width - amount * 2, height: box.height - amount * 2 };
709
1225
  };
710
1226
  const acceptedBox = (box) => ({ x: round(box.x), y: round(box.y), width: round(box.width), height: round(box.height) });
1227
+ const payloadBox = (box) => {
1228
+ if (!hasCards) return box;
1229
+ const frame = acceptedBox(box), padding2 = Math.min(12 * scale, frame.width / 4, frame.height / 4);
1230
+ return acceptedBox({ x: frame.x + padding2, y: frame.y + padding2, width: frame.width - 2 * padding2, height: frame.height - 2 * padding2 });
1231
+ };
711
1232
  const measureQuote = (node, box, settings) => layoutQuote(node.value, acceptedBox(box), {
712
1233
  fonts: options.fonts,
713
1234
  textMeasurement: options.textMeasurement,
@@ -722,12 +1243,30 @@ function composeSlide(input, options = {}) {
722
1243
  minFontSize: settings.minFontSize,
723
1244
  path: node.path
724
1245
  });
1246
+ const measureMetric = (node, box, settings) => layoutMetric(node.value, acceptedBox(box), {
1247
+ fonts: options.fonts,
1248
+ textMeasurement: options.textMeasurement,
1249
+ scale,
1250
+ minFontSize: settings.minFontSize,
1251
+ path: node.path,
1252
+ textRasterPadding: options.textRasterPadding,
1253
+ align: record2(slide.design).contentAlignment ?? options.contentAlignment
1254
+ });
1255
+ const measureTimeline = (node, box, settings) => layoutTimeline(node.value, acceptedBox(box), {
1256
+ fonts: options.fonts,
1257
+ textMeasurement: options.textMeasurement,
1258
+ scale,
1259
+ minFontSize: settings.minFontSize,
1260
+ path: node.path,
1261
+ textRasterPadding: options.textRasterPadding
1262
+ });
725
1263
  const leafScore = (node, box, settings, penalties) => {
1264
+ box = payloadBox(box);
726
1265
  const text = contentText(node.field, node.value);
727
1266
  let score = Math.abs(Math.log(box.width / box.height / 1.6));
728
1267
  if (penalties) penalties.cellProportions += score;
729
- if (node.field === "quote" || node.field === "code") {
730
- const internal = node.field === "quote" ? measureQuote(node, box, settings) : measureCode(node, box, settings);
1268
+ if (node.field === "quote" || node.field === "code" || node.field === "metric" || node.field === "timeline") {
1269
+ 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);
731
1270
  const reduction = internal.parts.reduce((sum, part) => sum + (part.fit ? (part.requestedFontSize - part.fit.fontSize) / scale : 0), 0);
732
1271
  score += reduction + (internal.overflow ? 1e3 : 0);
733
1272
  if (penalties) {
@@ -736,7 +1275,7 @@ function composeSlide(input, options = {}) {
736
1275
  }
737
1276
  } else if (text) {
738
1277
  const fit = fitContent(node.field, node.value, text, box, 25 * scale, (settings.minFontSize ?? 16) * scale, node.path);
739
- const reduction = (25 * scale - fit.fontSize) / scale;
1278
+ const reduction = Math.max(0, 25 * scale - fit.fontSize) / scale;
740
1279
  score += reduction + (fit.overflow ? 1e3 : 0);
741
1280
  if (penalties) {
742
1281
  penalties.fontReduction += reduction;
@@ -798,16 +1337,20 @@ function composeSlide(input, options = {}) {
798
1337
  const boxes = grid.boxes;
799
1338
  if (!nodes.some((node) => node.region)) flows.push({ path: containerPath, box: { ...area }, composition: { ...settings }, columns: grid.columns, rows: grid.rows, gap: grid.gap, itemCount: nodes.length, slotCount: count });
800
1339
  nodes.forEach((node, index) => {
801
- const box = node.region ? regionBox(node.region, area, actualGap) : boxes[index];
1340
+ let box = node.region ? regionBox(node.region, area, actualGap) : boxes[index];
802
1341
  if (node.children) {
803
1342
  const own = inheritedSettings(settings, node.composition), inner = inset(box, own);
804
1343
  groups.push({ path: node.path, box, contentBox: inner, composition: own });
805
1344
  arrange(node.children, inner, own, 0, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), node.path);
806
1345
  } else {
1346
+ const frameBox = hasCards ? acceptedBox(box) : void 0;
1347
+ box = payloadBox(box);
807
1348
  const textValue = contentText(node.field, node.value);
808
1349
  const quoteLayout = node.field === "quote" ? measureQuote(node, box, settings) : void 0;
809
1350
  const codeLayout = node.field === "code" ? measureCode(node, box, settings) : void 0;
810
- const internal = quoteLayout ?? codeLayout, body = internal?.parts.find((part) => part.role === "body");
1351
+ const metricLayout = node.field === "metric" ? measureMetric(node, box, settings) : void 0;
1352
+ const timelineLayout = node.field === "timeline" ? measureTimeline(node, box, settings) : void 0;
1353
+ const internal = quoteLayout ?? codeLayout ?? metricLayout ?? timelineLayout, body = internal?.parts.find((part) => part.role === "body" || part.role === "value");
811
1354
  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;
812
1355
  items.push({
813
1356
  path: node.path,
@@ -816,11 +1359,14 @@ function composeSlide(input, options = {}) {
816
1359
  value: node.value,
817
1360
  payload: node.payload,
818
1361
  box: internal ? acceptedBox(box) : box,
1362
+ ...frameBox ? { frameBox } : {},
819
1363
  text,
820
1364
  textStyle: body?.style ?? styleFor(node.field, node.path),
821
1365
  composition: settings,
822
1366
  ...quoteLayout ? { quoteLayout } : {},
823
- ...codeLayout ? { codeLayout } : {}
1367
+ ...codeLayout ? { codeLayout } : {},
1368
+ ...metricLayout ? { metricLayout } : {},
1369
+ ...timelineLayout ? { timelineLayout } : {}
824
1370
  });
825
1371
  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." });
826
1372
  }
@@ -834,11 +1380,14 @@ function composeSlide(input, options = {}) {
834
1380
  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." });
835
1381
  if (item.quoteLayout) diagnostics.push(...item.quoteLayout.diagnostics);
836
1382
  else if (item.codeLayout) diagnostics.push(...item.codeLayout.diagnostics);
1383
+ else if (item.metricLayout) diagnostics.push(...item.metricLayout.diagnostics);
1384
+ else if (item.timelineLayout) diagnostics.push(...item.timelineLayout.diagnostics);
837
1385
  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." });
838
1386
  }
839
1387
  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]);
840
1388
  const strictPaths = new Set(items.filter((item) => item.composition.overflow === "error").map((item) => item.path));
841
1389
  const failures = diagnostics.filter((diagnostic) => {
1390
+ if (composition.overflow === "error") return true;
842
1391
  let path2 = diagnostic.path;
843
1392
  while (path2) {
844
1393
  if (strictPaths.has(path2)) return true;
@@ -849,13 +1398,15 @@ function composeSlide(input, options = {}) {
849
1398
  return false;
850
1399
  });
851
1400
  const explanation = decisions ? {
852
- algorithm: "grid-score-v3",
1401
+ algorithm: "grid-score-v9",
853
1402
  textMeasurement: options.textMeasurement ? "provided" : "estimated",
1403
+ textOutlines: options.textMeasurement?.outlineBounds ? "provided" : "unavailable",
1404
+ textRasterPadding: rasterPadding,
854
1405
  decisions,
855
- unmeasuredPayloads: items.filter((item) => !headings.has(item.field) && !item.quoteLayout && !item.codeLayout && !["text", "items", "bullets", "table"].includes(item.field)).map((item) => item.path)
1406
+ 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)
856
1407
  } : void 0;
857
1408
  if (failures.length) throw new OPFCompositionError(failures, explanation);
858
- return { width, height, contentBox, items, groups, flows, diagnostics, composition, ...explanation ? { explanation } : {} };
1409
+ return { width, height, contentBox, items, groups, flows, diagnostics, composition, ...furniture ? { furniture } : {}, ...explanation ? { explanation } : {} };
859
1410
  }
860
1411
  function resolveCanvasDimensions(input) {
861
1412
  const presets = {
@@ -875,4 +1426,4 @@ function resolveCanvasDimensions(input) {
875
1426
  return { width, height };
876
1427
  }
877
1428
 
878
- export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, layoutCode, layoutQuote, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textWidthMeasurer, wrapText };
1429
+ export { MAX_COMPOSITION_DEPTH, OPFCompositionError, chartColorForFill, colorContrast, composeSlide, fitList, fitRichText, fitText, layoutCode, layoutFurniture, layoutMetric, layoutQuote, layoutTable, layoutTimeline, measureText, measureTextOutline, placeTextLines, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textColorForFill, textWidthMeasurer, wrapText };