@openpresentation/opf 0.10.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 (38) hide show
  1. package/dist/{catalogs-HCTav5Wv.d.ts → catalogs-DoVmvDr7.d.ts} +5 -5
  2. package/dist/catalogs.d.ts +1 -1
  3. package/dist/catalogs.js +1 -1
  4. package/dist/{chunk-GQSK3T7X.js → chunk-D3GQIREP.js} +13 -4
  5. package/dist/{chunk-NPOKUFEX.js → chunk-EWJNRHXA.js} +3 -3
  6. package/dist/{chunk-TK3QHM3U.js → chunk-FHGJX5QK.js} +24 -12
  7. package/dist/{chunk-BAWLYSUN.js → chunk-PQJRDNA6.js} +109 -6
  8. package/dist/{chunk-OZULJBSG.js → chunk-RS5GZ5RW.js} +14 -0
  9. package/dist/{chunk-TWMRZ43O.js → chunk-TU7I3KSB.js} +59 -21
  10. package/dist/{chunk-GCB6LGJM.js → chunk-ZIL7MGZU.js} +5 -3
  11. package/dist/composition.d.ts +58 -6
  12. package/dist/composition.js +1 -1
  13. package/dist/docs.js +25 -7
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.js +7 -7
  16. package/dist/lint.d.ts +1 -1
  17. package/dist/lint.js +5 -5
  18. package/dist/pagination.d.ts +2 -0
  19. package/dist/pagination.js +5 -5
  20. package/dist/schemas.js +1 -1
  21. package/dist/spec/catalogs/layouts/extract/canonical.json +45 -21
  22. package/dist/spec/catalogs/layouts/index.json +10 -0
  23. package/dist/spec/catalogs/layouts/number-1x.json +1 -1
  24. package/dist/spec/catalogs/layouts/number-2x.json +2 -2
  25. package/dist/spec/catalogs/layouts/number-3x.json +3 -3
  26. package/dist/spec/catalogs/layouts/number-4x.json +4 -4
  27. package/dist/spec/catalogs/layouts/number-5x.json +5 -5
  28. package/dist/spec/catalogs/layouts/number-6x.json +6 -6
  29. package/dist/spec/catalogs/layouts/quote-1x.json +13 -0
  30. package/dist/spec/catalogs/layouts/timeline-1x.json +13 -0
  31. package/dist/spec/schemas/layout.schema.json +12 -3
  32. package/dist/spec/schemas/narrative.schema.json +1 -1
  33. package/dist/spec-files.d.ts +1 -1
  34. package/dist/spec-files.js +1 -1
  35. package/dist/types.d.ts +1 -1
  36. package/dist/validator.d.ts +1 -1
  37. package/dist/validator.js +4 -4
  38. package/package.json +1 -1
@@ -311,9 +311,9 @@ interface Layout {
311
311
  */
312
312
  description?: string;
313
313
  /**
314
- * Primary kind of content the layout holds. Drives pickers and AI placement decisions.
314
+ * Primary kind of content the layout holds. Drives pickers and AI placement decisions. Metric is the canonical numeric/KPI category; Number remains an accepted legacy label.
315
315
  */
316
- contentType?: ("Title" | "Text" | "List" | "Image" | "Number" | "Chart");
316
+ contentType?: ("Title" | "Text" | "List" | "Image" | "Number" | "Metric" | "Chart" | "Table" | "Code" | "Video" | "Quote" | "Timeline");
317
317
  /**
318
318
  * How many parallel content blocks the layout exposes ('2x' = two-column, '3x' = three-up, etc.).
319
319
  */
@@ -407,9 +407,9 @@ interface Layout {
407
407
  */
408
408
  interface Placeholder {
409
409
  /**
410
- * OPF placeholder kind. 'text' and 'list' are flexible textual content regions. The named kinds describe a specific content role used by pickers, AI generation, and engine defaulting.
410
+ * OPF placeholder kind. 'text' and 'list' are flexible textual content regions. 'metric' is a numeric/KPI content region filled by a metric payload, including its optional label, description, unit, delta, and trend. The named kinds describe a specific content role used by pickers, AI generation, and engine defaulting. 'picture' and legacy 'diagram' use image payloads; 'media' uses video. Quote and timeline use their corresponding payload fields.
411
411
  */
412
- type: ("title" | "subtitle" | "tag" | "text" | "list" | "chart" | "picture" | "table" | "media" | "diagram" | "code");
412
+ type: ("title" | "subtitle" | "tag" | "text" | "metric" | "quote" | "timeline" | "list" | "chart" | "picture" | "table" | "media" | "diagram" | "code");
413
413
  [k: string]: unknown;
414
414
  }
415
415
  /**
@@ -789,7 +789,7 @@ interface Beat {
789
789
  */
790
790
  slideCount?: number;
791
791
  /**
792
- * Default content kind for the beat's slide. Mirrors ContentPayload.type and helps engines choose a sensible layout when only the beat is specified.
792
+ * Default content kind for the beat's slide. Uses ContentPayload.type names to help engines choose a layout. The legacy shape value is retained for compatibility and requests an image representation; it is not a native ContentPayload type.
793
793
  */
794
794
  slideType?: ("text" | "list" | "image" | "shape" | "chart" | "table" | "video" | "code" | "metric" | "quote" | "timeline");
795
795
  /**
@@ -1,2 +1,2 @@
1
- export { C as CatalogEntry, a as CatalogIndex, b as CatalogIndexRecord, c as CatalogKind, d as CatalogRecord, i as audiences, j as catalogEntries, k as catalogIndexes, l as catalogKinds, m as catalogSchemaNames, n as catalogs, o as chartTypes, p as colorSchemes, q as fontSchemes, r as languages, s as layouts, t as narratives, u as purposes, v as socialPlatforms, w as themes, x as tones } from './catalogs-HCTav5Wv.js';
1
+ export { C as CatalogEntry, a as CatalogIndex, b as CatalogIndexRecord, c as CatalogKind, d as CatalogRecord, i as audiences, j as catalogEntries, k as catalogIndexes, l as catalogKinds, m as catalogSchemaNames, n as catalogs, o as chartTypes, p as colorSchemes, q as fontSchemes, r as languages, s as layouts, t as narratives, u as purposes, v as socialPlatforms, w as themes, x as tones } from './catalogs-DoVmvDr7.js';
2
2
  import './schemas-BYe5y8-i.js';
package/dist/catalogs.js CHANGED
@@ -1 +1 @@
1
- export { audiences, catalogEntries, catalogIndexes, catalogKinds, catalogSchemaNames, catalogs, chartTypes, colorSchemes, fontSchemes, languages, layouts, narratives, purposes, socialPlatforms, themes, tones } from './chunk-TWMRZ43O.js';
1
+ export { audiences, catalogEntries, catalogIndexes, catalogKinds, catalogSchemaNames, catalogs, chartTypes, colorSchemes, fontSchemes, languages, layouts, narratives, purposes, socialPlatforms, themes, tones } from './chunk-TU7I3KSB.js';
@@ -4873,9 +4873,15 @@ var layout = {
4873
4873
  "List",
4874
4874
  "Image",
4875
4875
  "Number",
4876
- "Chart"
4876
+ "Metric",
4877
+ "Chart",
4878
+ "Table",
4879
+ "Code",
4880
+ "Video",
4881
+ "Quote",
4882
+ "Timeline"
4877
4883
  ],
4878
- "description": "Primary kind of content the layout holds. Drives pickers and AI placement decisions."
4884
+ "description": "Primary kind of content the layout holds. Drives pickers and AI placement decisions. Metric is the canonical numeric/KPI category; Number remains an accepted legacy label."
4879
4885
  },
4880
4886
  "contentMultiple": {
4881
4887
  "type": "string",
@@ -5083,6 +5089,9 @@ var layout = {
5083
5089
  "subtitle",
5084
5090
  "tag",
5085
5091
  "text",
5092
+ "metric",
5093
+ "quote",
5094
+ "timeline",
5086
5095
  "list",
5087
5096
  "chart",
5088
5097
  "picture",
@@ -5091,7 +5100,7 @@ var layout = {
5091
5100
  "diagram",
5092
5101
  "code"
5093
5102
  ],
5094
- "description": "OPF placeholder kind. 'text' and 'list' are flexible textual content regions. The named kinds describe a specific content role used by pickers, AI generation, and engine defaulting."
5103
+ "description": "OPF placeholder kind. 'text' and 'list' are flexible textual content regions. 'metric' is a numeric/KPI content region filled by a metric payload, including its optional label, description, unit, delta, and trend. The named kinds describe a specific content role used by pickers, AI generation, and engine defaulting. 'picture' and legacy 'diagram' use image payloads; 'media' uses video. Quote and timeline use their corresponding payload fields."
5095
5104
  }
5096
5105
  }
5097
5106
  },
@@ -5871,7 +5880,7 @@ var narrative = {
5871
5880
  "quote",
5872
5881
  "timeline"
5873
5882
  ],
5874
- "description": "Default content kind for the beat's slide. Mirrors ContentPayload.type and helps engines choose a sensible layout when only the beat is specified."
5883
+ "description": "Default content kind for the beat's slide. Uses ContentPayload.type names to help engines choose a layout. The legacy shape value is retained for compatibility and requests an image representation; it is not a native ContentPayload type."
5875
5884
  },
5876
5885
  "layoutHint": {
5877
5886
  "type": "string",
@@ -1,6 +1,6 @@
1
- import { validatePresentation, validationDefinition, validateCatalogRecord } from './chunk-GCB6LGJM.js';
2
- import { schemas, schemaEntries } from './chunk-GQSK3T7X.js';
3
- import { catalogKinds, catalogEntries, catalogSchemaNames } from './chunk-TWMRZ43O.js';
1
+ import { validatePresentation, validationDefinition, validateCatalogRecord } from './chunk-ZIL7MGZU.js';
2
+ import { schemas, schemaEntries } from './chunk-D3GQIREP.js';
3
+ import { catalogKinds, catalogEntries, catalogSchemaNames } from './chunk-TU7I3KSB.js';
4
4
  import { parseTree, printParseErrorCode, getNodePath } from 'jsonc-parser';
5
5
 
6
6
  var own = (value, key) => Object.hasOwn(value, key);
@@ -1,6 +1,6 @@
1
- import { assertValidPresentation } from './chunk-GCB6LGJM.js';
2
- import { resolveFontFamilies, resolveCanvasDimensions, composeSlide, tableRowBoundaries } from './chunk-BAWLYSUN.js';
3
- import { catalogs } from './chunk-TWMRZ43O.js';
1
+ import { assertValidPresentation } from './chunk-ZIL7MGZU.js';
2
+ import { resolveFontFamilies, resolveCanvasDimensions, composeSlide, tableRowBoundaries } from './chunk-PQJRDNA6.js';
3
+ import { catalogs } from './chunk-TU7I3KSB.js';
4
4
 
5
5
  // src/pagination.ts
6
6
  var OPFPaginationError = class extends Error {
@@ -83,14 +83,23 @@ function paginateSlide(input, options = {}) {
83
83
  };
84
84
  source = withReadability(source);
85
85
  let evaluations = 0;
86
- const geometry = (slide) => {
86
+ const geometry = (slide, pageIndex = 0) => {
87
87
  if (++evaluations > 2e4) throw new OPFPaginationError("Pagination exceeded its layout evaluation limit. Split the input into smaller sections.");
88
- return composeSlide(withReadability(slide, true), options);
88
+ return composeSlide(withReadability(slide, true), { ...options, slideNumber: (options.slideNumber ?? sourceIndex + 1) + pageIndex });
89
89
  };
90
90
  const initial = geometry(source);
91
- if (!initial.diagnostics.length) return { slides: [source], pages: [{ slideIndex: sourceIndex, mappings: initial.items.map((item) => ({ sourcePath: item.path, outputPath: item.path })) }] };
92
- const headerIssues = initial.diagnostics.filter((issue) => headingFields.has(issue.path.slice(sourceBase.length + 1)));
93
- if (headerIssues.length) throw new OPFPaginationError("The repeated heading does not fit. Shorten it or change the slide design before pagination.", headerIssues);
91
+ const repeatedMappings = (pageIndex) => {
92
+ if (!initial.furniture) return [];
93
+ const paths = new Set(initial.items.filter((item) => headingFields.has(item.field)).map((item) => item.path));
94
+ for (const part of initial.furniture?.parts ?? []) {
95
+ paths.add(part.path);
96
+ if (part.sourcePath) paths.add(part.sourcePath);
97
+ }
98
+ return [...paths].map((sourcePath) => ({ sourcePath, outputPath: sourcePath.startsWith(`${sourceBase}.`) ? `slides.${sourceIndex + pageIndex}${sourcePath.slice(sourceBase.length)}` : sourcePath }));
99
+ };
100
+ if (!initial.diagnostics.length) return { slides: [source], pages: [{ slideIndex: sourceIndex, mappings: initial.items.map((item) => ({ sourcePath: item.path, outputPath: item.path })), ...initial.furniture ? { repeatedMappings: repeatedMappings(0) } : {} }] };
101
+ const headerIssues = initial.diagnostics.filter((issue) => headingFields.has(issue.path.slice(sourceBase.length + 1)) || initial.furniture?.diagnostics.includes(issue));
102
+ if (headerIssues.length) throw new OPFPaginationError("Repeated headings or header/footer content cannot fit or resolve. Change the repeated content or slide design before pagination.", headerIssues);
94
103
  const leaves = initial.items.filter((item) => !headingFields.has(item.field)).map((item) => leafFor(item.path, item.field, item.value));
95
104
  const leafPaths = new Set(leaves.map((leaf) => leaf.path));
96
105
  const slides = [], pages = [];
@@ -135,11 +144,13 @@ function paginateSlide(input, options = {}) {
135
144
  if (pageIndex > 0) delete slide.notes;
136
145
  return { slide, mappings };
137
146
  };
138
- const diagnosticsFor = (portions) => geometry(project(portions, slides.length).slide).diagnostics;
147
+ const diagnosticsFor = (portions) => geometry(project(portions, slides.length).slide, slides.length).diagnostics;
139
148
  const finish = () => {
140
149
  if (!selected.size) return;
141
150
  if (slides.length >= maxSlides) throw new OPFPaginationError(`Pagination needs more than ${maxSlides} slides. No partial result was returned.`);
142
151
  const { slide, mappings } = project(selected, slides.length);
152
+ const issues = geometry(slide, slides.length).diagnostics;
153
+ if (issues.length) throw new OPFPaginationError("A continuation page does not fit at its final page number. No partial result was returned.", issues);
143
154
  if (slides.length && typeof source.id === "string") {
144
155
  let suffix = slides.length + 1, id = `${source.id}--${suffix}`;
145
156
  while (reservedIds.has(id)) id = `${source.id}--${++suffix}`;
@@ -148,7 +159,7 @@ function paginateSlide(input, options = {}) {
148
159
  }
149
160
  assertValidPresentation({ slides: [slide] });
150
161
  slides.push(slide);
151
- pages.push({ slideIndex: sourceIndex + slides.length - 1, mappings });
162
+ pages.push({ slideIndex: sourceIndex + slides.length - 1, mappings, ...initial.furniture ? { repeatedMappings: repeatedMappings(slides.length - 1) } : {} });
152
163
  selected = /* @__PURE__ */ new Map();
153
164
  };
154
165
  for (const leaf of leaves) {
@@ -218,10 +229,11 @@ function paginatePresentation(input, options = {}) {
218
229
  const fontReference = design.fontScheme ?? theme.fontScheme ?? "roboto";
219
230
  const fontScheme = typeof fontReference === "string" ? resolve("fontSchemes", fontReference) : { ...resolve("fontSchemes", fontReference.id), ...fontReference };
220
231
  const fonts = resolveFontFamilies(fontScheme);
221
- const result = paginateSlide(slide, { ...resolveCanvasDimensions(design.dimensions ?? theme.dimensions), layout, fonts, contentAlignment: design.contentAlignment, titleAlignment: design.titleAlignment, contentBox: design.contentBox, textMeasurement: options.textMeasurement, textRasterPadding: options.textRasterPadding, ...overrides, slideIndex: index, maxSlides: maxSlides - output.length, minFontSize: options.minFontSize, reservedIds });
232
+ const result = paginateSlide(slide, { ...resolveCanvasDimensions(design.dimensions ?? theme.dimensions), layout, fonts, contentAlignment: design.contentAlignment, titleAlignment: design.titleAlignment, contentBox: design.contentBox, textMeasurement: options.textMeasurement, textRasterPadding: options.textRasterPadding, ...overrides, presentation, slideIndex: index, slideNumber: output.length + 1, maxSlides: maxSlides - output.length, minFontSize: options.minFontSize, reservedIds });
222
233
  const outputStart = output.length;
223
234
  result.pages.forEach((page, pageIndex) => {
224
- pages.push({ sourceSlideIndex: index, slideIndex: outputStart + pageIndex, mappings: page.mappings.map((mapping) => ({ ...mapping, outputPath: mapping.outputPath.replace(/^slides\.\d+/, `slides.${outputStart + pageIndex}`) })) });
235
+ const remap = (mapping) => ({ ...mapping, outputPath: mapping.outputPath.replace(/^slides\.\d+/, `slides.${outputStart + pageIndex}`) });
236
+ pages.push({ sourceSlideIndex: index, slideIndex: outputStart + pageIndex, mappings: page.mappings.map(remap), ...page.repeatedMappings ? { repeatedMappings: page.repeatedMappings.map(remap) } : {} });
225
237
  });
226
238
  output.push(...result.slides);
227
239
  reservedIds.push(...result.slides.map((slide2) => slide2.id).filter(Boolean));
@@ -146,8 +146,105 @@ var headings = /* @__PURE__ */ new Set(["title", "subtitle", "tag"]);
146
146
  var rows = ["top", "middle", "bottom"];
147
147
  var columns = ["left", "center", "right"];
148
148
  var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
149
- var kind = (field) => field === "items" || field === "bullets" ? "list" : field;
149
+ var kind = (field) => field === "items" ? "list" : field === "bullets" ? "text" : field;
150
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
+ }
151
248
  var textSegments = new Intl.Segmenter("und", { granularity: "grapheme" });
152
249
  function measureText(text, fontSize) {
153
250
  let units = 0;
@@ -1078,7 +1175,11 @@ function composeSlide(input, options = {}) {
1078
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));
1079
1176
  const path = `slides.${options.slideIndex ?? 0}`;
1080
1177
  const items = [], diagnostics = [];
1081
- 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);
1082
1183
  for (const field of ["tag", "title", "subtitle"]) {
1083
1184
  if (!slide[field]) continue;
1084
1185
  const requested = (field === "title" ? 54 : field === "tag" ? 16 : 25) * scale;
@@ -1086,11 +1187,12 @@ function composeSlide(input, options = {}) {
1086
1187
  const box = { x: padding, y, width: width - padding * 2, height: maxHeight };
1087
1188
  const text = fitPlacedText(field, slide[field], String(slide[field]), box, requested, minSize, `${path}.${field}`);
1088
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." });
1089
1191
  items.push({ path: `${path}.${field}`, field, type: "text", value: slide[field], payload: { text: slide[field] }, box, text, textStyle: styleFor(field, `${path}.${field}`), composition });
1090
1192
  y += box.height + gap * 0.5;
1091
1193
  }
1092
1194
  if (items.length) y += gap * 0.5;
1093
- 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) };
1094
1196
  const collect = (host, basePath, depth = 0, ancestors = []) => {
1095
1197
  if (Array.isArray(host.blocks)) {
1096
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.`);
@@ -1285,6 +1387,7 @@ function composeSlide(input, options = {}) {
1285
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]);
1286
1388
  const strictPaths = new Set(items.filter((item) => item.composition.overflow === "error").map((item) => item.path));
1287
1389
  const failures = diagnostics.filter((diagnostic) => {
1390
+ if (composition.overflow === "error") return true;
1288
1391
  let path2 = diagnostic.path;
1289
1392
  while (path2) {
1290
1393
  if (strictPaths.has(path2)) return true;
@@ -1295,7 +1398,7 @@ function composeSlide(input, options = {}) {
1295
1398
  return false;
1296
1399
  });
1297
1400
  const explanation = decisions ? {
1298
- algorithm: "grid-score-v8",
1401
+ algorithm: "grid-score-v9",
1299
1402
  textMeasurement: options.textMeasurement ? "provided" : "estimated",
1300
1403
  textOutlines: options.textMeasurement?.outlineBounds ? "provided" : "unavailable",
1301
1404
  textRasterPadding: rasterPadding,
@@ -1303,7 +1406,7 @@ function composeSlide(input, options = {}) {
1303
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)
1304
1407
  } : void 0;
1305
1408
  if (failures.length) throw new OPFCompositionError(failures, explanation);
1306
- 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 } : {} };
1307
1410
  }
1308
1411
  function resolveCanvasDimensions(input) {
1309
1412
  const presets = {
@@ -1323,4 +1426,4 @@ function resolveCanvasDimensions(input) {
1323
1426
  return { width, height };
1324
1427
  }
1325
1428
 
1326
- export { MAX_COMPOSITION_DEPTH, OPFCompositionError, chartColorForFill, colorContrast, composeSlide, fitList, fitRichText, fitText, layoutCode, layoutMetric, layoutQuote, layoutTable, layoutTimeline, measureText, measureTextOutline, placeTextLines, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, tableGrid, tableRowBoundaries, textColorForFill, 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 };
@@ -1872,6 +1872,12 @@ var specFileEntries = [
1872
1872
  "kind": "catalogRecord",
1873
1873
  "mediaType": "application/json"
1874
1874
  },
1875
+ {
1876
+ "path": "catalogs/layouts/quote-1x.json",
1877
+ "packagePath": "@openpresentation/opf/spec/catalogs/layouts/quote-1x.json",
1878
+ "kind": "catalogRecord",
1879
+ "mediaType": "application/json"
1880
+ },
1875
1881
  {
1876
1882
  "path": "catalogs/layouts/table-1x.json",
1877
1883
  "packagePath": "@openpresentation/opf/spec/catalogs/layouts/table-1x.json",
@@ -1896,6 +1902,12 @@ var specFileEntries = [
1896
1902
  "kind": "catalogRecord",
1897
1903
  "mediaType": "application/json"
1898
1904
  },
1905
+ {
1906
+ "path": "catalogs/layouts/timeline-1x.json",
1907
+ "packagePath": "@openpresentation/opf/spec/catalogs/layouts/timeline-1x.json",
1908
+ "kind": "catalogRecord",
1909
+ "mediaType": "application/json"
1910
+ },
1899
1911
  {
1900
1912
  "path": "catalogs/layouts/title-subtitle.json",
1901
1913
  "packagePath": "@openpresentation/opf/spec/catalogs/layouts/title-subtitle.json",
@@ -3188,10 +3200,12 @@ var specFilePaths = [
3188
3200
  "catalogs/layouts/number-4x.json",
3189
3201
  "catalogs/layouts/number-5x.json",
3190
3202
  "catalogs/layouts/number-6x.json",
3203
+ "catalogs/layouts/quote-1x.json",
3191
3204
  "catalogs/layouts/table-1x.json",
3192
3205
  "catalogs/layouts/text-1x.json",
3193
3206
  "catalogs/layouts/text-2x.json",
3194
3207
  "catalogs/layouts/text-3x.json",
3208
+ "catalogs/layouts/timeline-1x.json",
3195
3209
  "catalogs/layouts/title-subtitle.json",
3196
3210
  "catalogs/layouts/title.json",
3197
3211
  "catalogs/narratives/board-meeting.json",
@@ -1177,7 +1177,7 @@ var layouts = [
1177
1177
  "type": "title"
1178
1178
  },
1179
1179
  {
1180
- "type": "text"
1180
+ "type": "metric"
1181
1181
  }
1182
1182
  ]
1183
1183
  },
@@ -1190,10 +1190,10 @@ var layouts = [
1190
1190
  "type": "title"
1191
1191
  },
1192
1192
  {
1193
- "type": "text"
1193
+ "type": "metric"
1194
1194
  },
1195
1195
  {
1196
- "type": "text"
1196
+ "type": "metric"
1197
1197
  }
1198
1198
  ],
1199
1199
  "composition": {
@@ -1209,13 +1209,13 @@ var layouts = [
1209
1209
  "type": "title"
1210
1210
  },
1211
1211
  {
1212
- "type": "text"
1212
+ "type": "metric"
1213
1213
  },
1214
1214
  {
1215
- "type": "text"
1215
+ "type": "metric"
1216
1216
  },
1217
1217
  {
1218
- "type": "text"
1218
+ "type": "metric"
1219
1219
  }
1220
1220
  ],
1221
1221
  "composition": {
@@ -1231,16 +1231,16 @@ var layouts = [
1231
1231
  "type": "title"
1232
1232
  },
1233
1233
  {
1234
- "type": "text"
1234
+ "type": "metric"
1235
1235
  },
1236
1236
  {
1237
- "type": "text"
1237
+ "type": "metric"
1238
1238
  },
1239
1239
  {
1240
- "type": "text"
1240
+ "type": "metric"
1241
1241
  },
1242
1242
  {
1243
- "type": "text"
1243
+ "type": "metric"
1244
1244
  }
1245
1245
  ],
1246
1246
  "composition": {
@@ -1257,19 +1257,19 @@ var layouts = [
1257
1257
  "type": "title"
1258
1258
  },
1259
1259
  {
1260
- "type": "text"
1260
+ "type": "metric"
1261
1261
  },
1262
1262
  {
1263
- "type": "text"
1263
+ "type": "metric"
1264
1264
  },
1265
1265
  {
1266
- "type": "text"
1266
+ "type": "metric"
1267
1267
  },
1268
1268
  {
1269
- "type": "text"
1269
+ "type": "metric"
1270
1270
  },
1271
1271
  {
1272
- "type": "text"
1272
+ "type": "metric"
1273
1273
  }
1274
1274
  ],
1275
1275
  "composition": {
@@ -1286,22 +1286,22 @@ var layouts = [
1286
1286
  "type": "title"
1287
1287
  },
1288
1288
  {
1289
- "type": "text"
1289
+ "type": "metric"
1290
1290
  },
1291
1291
  {
1292
- "type": "text"
1292
+ "type": "metric"
1293
1293
  },
1294
1294
  {
1295
- "type": "text"
1295
+ "type": "metric"
1296
1296
  },
1297
1297
  {
1298
- "type": "text"
1298
+ "type": "metric"
1299
1299
  },
1300
1300
  {
1301
- "type": "text"
1301
+ "type": "metric"
1302
1302
  },
1303
1303
  {
1304
- "type": "text"
1304
+ "type": "metric"
1305
1305
  }
1306
1306
  ],
1307
1307
  "composition": {
@@ -1309,6 +1309,19 @@ var layouts = [
1309
1309
  "columns": 3
1310
1310
  }
1311
1311
  },
1312
+ {
1313
+ "$schema": "https://openpresentation.org/schema/opf-layout/v1",
1314
+ "id": "quote-1x",
1315
+ "name": "Quote 1x",
1316
+ "placeholders": [
1317
+ {
1318
+ "type": "title"
1319
+ },
1320
+ {
1321
+ "type": "quote"
1322
+ }
1323
+ ]
1324
+ },
1312
1325
  {
1313
1326
  "$schema": "https://openpresentation.org/schema/opf-layout/v1",
1314
1327
  "id": "table-1x",
@@ -1382,6 +1395,19 @@ var layouts = [
1382
1395
  "mode": "row"
1383
1396
  }
1384
1397
  },
1398
+ {
1399
+ "$schema": "https://openpresentation.org/schema/opf-layout/v1",
1400
+ "id": "timeline-1x",
1401
+ "name": "Timeline 1x",
1402
+ "placeholders": [
1403
+ {
1404
+ "type": "title"
1405
+ },
1406
+ {
1407
+ "type": "timeline"
1408
+ }
1409
+ ]
1410
+ },
1385
1411
  {
1386
1412
  "$schema": "https://openpresentation.org/schema/opf-layout/v1",
1387
1413
  "id": "title",
@@ -15562,6 +15588,11 @@ var catalogIndexes = {
15562
15588
  "name": "Number 6x",
15563
15589
  "file": "number-6x.json"
15564
15590
  },
15591
+ {
15592
+ "id": "quote-1x",
15593
+ "name": "Quote 1x",
15594
+ "file": "quote-1x.json"
15595
+ },
15565
15596
  {
15566
15597
  "id": "table-1x",
15567
15598
  "name": "Table 1x",
@@ -15582,6 +15613,11 @@ var catalogIndexes = {
15582
15613
  "name": "Text 3x",
15583
15614
  "file": "text-3x.json"
15584
15615
  },
15616
+ {
15617
+ "id": "timeline-1x",
15618
+ "name": "Timeline 1x",
15619
+ "file": "timeline-1x.json"
15620
+ },
15585
15621
  {
15586
15622
  "id": "title",
15587
15623
  "name": "Title",
@@ -18231,10 +18267,12 @@ var catalogEntries = [
18231
18267
  "number-4x.json",
18232
18268
  "number-5x.json",
18233
18269
  "number-6x.json",
18270
+ "quote-1x.json",
18234
18271
  "table-1x.json",
18235
18272
  "text-1x.json",
18236
18273
  "text-2x.json",
18237
18274
  "text-3x.json",
18275
+ "timeline-1x.json",
18238
18276
  "title.json",
18239
18277
  "title-subtitle.json"
18240
18278
  ], records: layouts, index: catalogIndexes.layouts },
@@ -1,6 +1,6 @@
1
- import { MAX_COMPOSITION_DEPTH, tableGrid } from './chunk-BAWLYSUN.js';
2
- import { schemas, schemaEntries } from './chunk-GQSK3T7X.js';
3
- import { catalogSchemaNames } from './chunk-TWMRZ43O.js';
1
+ import { MAX_COMPOSITION_DEPTH, tableGrid } from './chunk-PQJRDNA6.js';
2
+ import { schemas, schemaEntries } from './chunk-D3GQIREP.js';
3
+ import { catalogSchemaNames } from './chunk-TU7I3KSB.js';
4
4
  import Ajv2020 from 'ajv/dist/2020.js';
5
5
  import addFormats from 'ajv-formats';
6
6
 
@@ -67,10 +67,12 @@ var catalogIds = {
67
67
  "number-4x",
68
68
  "number-5x",
69
69
  "number-6x",
70
+ "quote-1x",
70
71
  "table-1x",
71
72
  "text-1x",
72
73
  "text-2x",
73
74
  "text-3x",
75
+ "timeline-1x",
74
76
  "title",
75
77
  "title-subtitle"
76
78
  ],