@openpresentation/opf 0.11.3 → 0.11.4

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.
@@ -35,6 +35,114 @@ function tableRowBoundaries(value) {
35
35
  return [...allowed];
36
36
  }
37
37
 
38
+ // src/image-aspect.ts
39
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
40
+ function sourceOf(value) {
41
+ if (typeof value === "string") return value;
42
+ return isRecord(value) && typeof value.src === "string" ? value.src : void 0;
43
+ }
44
+ var PREFIX_BYTES = 64 * 1024;
45
+ var JPEG_PREFIX_BYTES = 1024 * 1024;
46
+ function dataBytes(uri, limit) {
47
+ const match = /^data:([^;,]*)((?:;[^;,]*)*),/i.exec(uri);
48
+ if (!match) return void 0;
49
+ const type = (match[1] ?? "").toLowerCase(), start = match[0].length;
50
+ try {
51
+ if (/;base64/i.test(match[2] ?? "")) {
52
+ const chars = Math.ceil(limit / 3) * 4, text = uri.slice(start, start + chars + chars / 8).replace(/\s+/g, "").slice(0, chars);
53
+ return { type, bytes: Uint8Array.from(atob(text.slice(0, text.length - text.length % 4)), (char) => char.charCodeAt(0)) };
54
+ }
55
+ const prefix = uri.slice(start, start + limit).replace(/%[0-9a-f]?$/i, "");
56
+ return { type, bytes: new TextEncoder().encode(decodeURIComponent(prefix)) };
57
+ } catch {
58
+ return void 0;
59
+ }
60
+ }
61
+ var at = (b, i) => b[i] ?? 0;
62
+ var u16be = (b, i) => at(b, i) << 8 | at(b, i + 1);
63
+ var u32be = (b, i) => (at(b, i) << 24 | at(b, i + 1) << 16 | at(b, i + 2) << 8 | at(b, i + 3)) >>> 0;
64
+ var u16le = (b, i) => at(b, i) | at(b, i + 1) << 8;
65
+ var u24le = (b, i) => at(b, i) | at(b, i + 1) << 8 | at(b, i + 2) << 16;
66
+ var ascii = (b, i, n) => String.fromCharCode(...b.subarray(i, i + n));
67
+ function rasterDimensions(b) {
68
+ if (b.length >= 24 && u32be(b, 0) === 2303741511 && ascii(b, 12, 4) === "IHDR") return { width: u32be(b, 16), height: u32be(b, 20) };
69
+ if (b.length >= 10 && (ascii(b, 0, 6) === "GIF87a" || ascii(b, 0, 6) === "GIF89a")) return { width: u16le(b, 6), height: u16le(b, 8) };
70
+ if (b.length >= 4 && b[0] === 255 && b[1] === 216) {
71
+ let i = 2;
72
+ while (i + 9 < b.length) {
73
+ if (at(b, i) !== 255) {
74
+ i++;
75
+ continue;
76
+ }
77
+ const marker = at(b, i + 1);
78
+ if (marker === 255) {
79
+ i++;
80
+ continue;
81
+ }
82
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
83
+ i += 2;
84
+ continue;
85
+ }
86
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) return { height: u16be(b, i + 5), width: u16be(b, i + 7) };
87
+ i += 2 + u16be(b, i + 2);
88
+ }
89
+ return void 0;
90
+ }
91
+ if (b.length >= 30 && ascii(b, 0, 4) === "RIFF" && ascii(b, 8, 4) === "WEBP") {
92
+ const chunk = ascii(b, 12, 4);
93
+ if (chunk === "VP8X") return { width: u24le(b, 24) + 1, height: u24le(b, 27) + 1 };
94
+ if (chunk === "VP8L" && at(b, 20) === 47) {
95
+ const bits = u16le(b, 21) | u16le(b, 23) << 16;
96
+ return { width: (bits & 16383) + 1, height: (bits >>> 14 & 16383) + 1 };
97
+ }
98
+ if (chunk === "VP8 ") return { width: u16le(b, 26) & 16383, height: u16le(b, 28) & 16383 };
99
+ }
100
+ return void 0;
101
+ }
102
+ function svgDimensions(text) {
103
+ const root = /<svg\b([^>]*)>/i.exec(text)?.[1];
104
+ if (root === void 0) return void 0;
105
+ const attribute = (name) => new RegExp(`\\s${name}\\s*=\\s*["']([^"']*)["']`, "i").exec(root)?.[1];
106
+ const length = (value) => {
107
+ const match = value && /^\s*([0-9]*\.?[0-9]+)\s*(px|pt|in|cm|mm|em)?\s*$/i.exec(value);
108
+ return match ? Number(match[1]) : void 0;
109
+ };
110
+ const width = length(attribute("width")), height = length(attribute("height"));
111
+ if (width && height) return { width, height };
112
+ const box = attribute("viewBox")?.trim().split(/[\s,]+/).map(Number);
113
+ if (box && box.length === 4 && box.every(Number.isFinite)) {
114
+ const [, , width2 = 0, height2 = 0] = box;
115
+ if (width2 > 0 && height2 > 0) return { width: width2, height: height2 };
116
+ }
117
+ return void 0;
118
+ }
119
+ var cache = /* @__PURE__ */ new Map();
120
+ var CACHE_ENTRIES = 64;
121
+ function readAspect(source) {
122
+ const first = dataBytes(source, PREFIX_BYTES);
123
+ if (!first) return void 0;
124
+ let dimensions;
125
+ if (first.type.startsWith("image/svg")) dimensions = svgDimensions(new TextDecoder().decode(first.bytes));
126
+ else {
127
+ dimensions = rasterDimensions(first.bytes);
128
+ if (!dimensions && first.bytes.length >= 2 && first.bytes[0] === 255 && first.bytes[1] === 216) {
129
+ const more = dataBytes(source, JPEG_PREFIX_BYTES);
130
+ if (more) dimensions = rasterDimensions(more.bytes);
131
+ }
132
+ }
133
+ return dimensions && dimensions.width > 0 && dimensions.height > 0 ? dimensions.width / dimensions.height : void 0;
134
+ }
135
+ function intrinsicImageAspect(value, assets) {
136
+ let source = sourceOf(value);
137
+ for (let hop = 0; source?.startsWith("asset:") && hop < 8; hop++) source = sourceOf(isRecord(assets) ? assets[source.slice(6)] : void 0);
138
+ if (!source?.startsWith("data:")) return void 0;
139
+ if (cache.has(source)) return cache.get(source);
140
+ const aspect = readAspect(source);
141
+ if (cache.size >= CACHE_ENTRIES) cache.delete(cache.keys().next().value);
142
+ cache.set(source, aspect);
143
+ return aspect;
144
+ }
145
+
38
146
  // src/furniture-fields.ts
39
147
  var DEFAULT_FURNITURE_DATE_FORMAT = "M/d/yyyy";
40
148
  var DEFAULT_SLIDE_NUMBER_FORMAT = "{current}";
@@ -268,10 +376,12 @@ function resolveFontSchemeReference(reference, lookup, path = "design.fontScheme
268
376
  function resolveFontFamilies(input) {
269
377
  const scheme = record2(input);
270
378
  const family = (value) => typeof value === "string" ? value : record2(value).family;
379
+ const accent = family(scheme.accent);
271
380
  return {
272
381
  heading: family(scheme.heading) ?? scheme.major ?? scheme.minor ?? DEFAULT_FONT_FAMILIES.heading,
273
382
  body: family(scheme.body) ?? scheme.minor ?? scheme.major ?? DEFAULT_FONT_FAMILIES.body,
274
- code: family(scheme.code) ?? FALLBACK_CODE_FAMILY
383
+ code: family(scheme.code) ?? FALLBACK_CODE_FAMILY,
384
+ ...typeof accent === "string" && accent ? { accent } : {}
275
385
  };
276
386
  }
277
387
  function resolveTextStyle(style, measurement) {
@@ -438,6 +548,45 @@ function resolveSlideImage(slide, layout, presentation, width, height, path, pad
438
548
  }
439
549
  return result;
440
550
  }
551
+ var LOGO_LOCKUP_CHAINS = {
552
+ dark: ["light", "default", "stackedLight", "stacked", "wordmarkLight", "wordmark", "iconLight", "icon", "dark", "stackedDark", "wordmarkDark", "iconDark"],
553
+ light: ["dark", "default", "stackedDark", "stacked", "wordmarkDark", "wordmark", "iconDark", "icon", "light", "stackedLight", "wordmarkLight", "iconLight"]
554
+ };
555
+ var LOGO_SET_KEYS = new Set(LOGO_LOCKUP_CHAINS.dark);
556
+ function resolveLogo(presentation, slide, options = {}) {
557
+ const slot = options.slot === "icon" || options.slot === "stacked" ? options.slot : "lockup";
558
+ if (options.slot !== void 0 && slot !== options.slot) throw new RangeError("Logo slot must be lockup, icon or stacked.");
559
+ const tone = options.onDark === true ? "dark" : "light";
560
+ const lockup = LOGO_LOCKUP_CHAINS[tone];
561
+ const chain = slot === "icon" ? [tone === "dark" ? "iconLight" : "iconDark", "icon", ...lockup] : slot === "stacked" ? [tone === "dark" ? "stackedLight" : "stackedDark", "stacked", ...lockup] : lockup;
562
+ const usable = (value) => {
563
+ const source = assetSource(value);
564
+ return typeof source === "string" && source.length > 0;
565
+ };
566
+ const pick = (value, path) => {
567
+ if (value === void 0 || value === null || value === false) return null;
568
+ if (typeof value === "string" || typeof value === "object" && !Array.isArray(value) && "src" in record2(value)) {
569
+ return usable(value) ? { source: value, path, variant: "default", slot } : null;
570
+ }
571
+ const set = record2(value);
572
+ if (!Object.keys(set).some((key) => LOGO_SET_KEYS.has(key))) return null;
573
+ for (const key of chain) {
574
+ const variant = set[key];
575
+ if (variant !== void 0 && usable(variant)) return { source: variant, path: `${path}.${key}`, variant: key, slot };
576
+ }
577
+ return null;
578
+ };
579
+ const deck = record2(presentation), slideIndex = Number.isSafeInteger(options.slideIndex) && options.slideIndex >= 0 ? options.slideIndex : 0;
580
+ const own = pick(record2(record2(slide).design).logo, `slides.${slideIndex}.design.logo`);
581
+ if (own) return own;
582
+ const shared = pick(record2(deck.design).logo, "design.logo");
583
+ if (shared) return shared;
584
+ const organizations = Array.isArray(deck.organization) ? deck.organization : [deck.organization];
585
+ const primaryIndex = organizations.findIndex((item) => record2(item).role === "primary");
586
+ const index = primaryIndex >= 0 ? primaryIndex : organizations.findIndex(Boolean);
587
+ if (index < 0) return null;
588
+ return pick(record2(organizations[index]).logo, Array.isArray(deck.organization) ? `organization.${index}.logo` : "organization.logo");
589
+ }
441
590
  var webUrl = /^https?:\/\/\S+$/i;
442
591
  function resolveSocialProfile(platform, value, records = [], owner = "organization") {
443
592
  const raw = String(value).trim().replace(/\s+/gu, " ");
@@ -516,8 +665,20 @@ function layoutFurniture(input, options = {}) {
516
665
  if (accepted.overflow) error(partPath, "Repeated text exceeds its zone at the selected readability floor; change the furniture or slide design.");
517
666
  y += box.height;
518
667
  };
668
+ const imageBox = (source) => {
669
+ const imageHeight = Math.max(32 * scale, Math.min(height * 0.05, 72 * scale)), imageWidth = Math.min(zoneWidth, imageHeight * (intrinsicImageAspect(source, record2(options.presentation).assets) ?? 1));
670
+ return { x: zone === "left" ? x : zone === "center" ? x + (zoneWidth - imageWidth) / 2 : x + zoneWidth - imageWidth, y, width: imageWidth, height: imageHeight };
671
+ };
672
+ if (content.logo === true) {
673
+ const resolved = resolveLogo(options.presentation, slide, { slot: "icon", onDark: options.darkBackground, slideIndex: options.slideIndex });
674
+ if (resolved) {
675
+ const box = imageBox(resolved.source);
676
+ zoneParts.push({ type: "image", kind: kind2, zone, field: "logo", path: `${path}.logo`, sourcePath: resolved.path, generated: true, image: resolved.source, box, alignment: zone });
677
+ y += box.height;
678
+ } else error(`${path}.logo`, "Generated logo needs design.logo or a primary organization logo.", "unresolved-content");
679
+ }
519
680
  if (content.image !== void 0) {
520
- const box = { x, y, width: zoneWidth, height: Math.max(32 * scale, Math.min(height * 0.05, 72 * scale)) };
681
+ const box = imageBox(content.image);
521
682
  zoneParts.push({ type: "image", kind: kind2, zone, field: "image", path: `${path}.image`, sourcePath: `${path}.image`, generated: false, image: content.image, box, alignment: zone });
522
683
  y += box.height;
523
684
  }
@@ -644,7 +805,7 @@ function layoutQuote(value, box, options = {}) {
644
805
  [source(bodyPath, quote.text, 1)],
645
806
  { x: box.x + 18, y: box.y + 18, width: box.width - 36, height: box.height - (footer ? 94 : 36) },
646
807
  28,
647
- options.fonts?.heading ?? "sans-serif",
808
+ options.fonts?.accent ?? options.fonts?.heading ?? "sans-serif",
648
809
  600,
649
810
  bodyPath
650
811
  );
@@ -780,15 +941,15 @@ function layoutMetric(value, box, options = {}) {
780
941
  const width = Math.max(...lineInk(part, fit).map((line) => Math.max(line.width, line.outline ? line.outline.x + line.outline.width : 0) - Math.min(0, line.outline?.x ?? 0)));
781
942
  return width > 0 ? width + 2 * rasterPadding : 0;
782
943
  };
783
- const cache = /* @__PURE__ */ new Map();
944
+ const cache2 = /* @__PURE__ */ new Map();
784
945
  const measure = (part, size, width) => {
785
946
  const key = JSON.stringify([part.role, size, width]);
786
- let fit = cache.get(key);
947
+ let fit = cache2.get(key);
787
948
  if (!fit) {
788
949
  fit = fitCodeText(part.text, { ...box, width: hasOutlines ? Math.max(Number.MIN_VALUE, width - 2 * rasterPadding) : width }, size, size, scale, textWidthMeasurer(part.style, options.textMeasurement));
789
950
  fit = place(part, fit, width);
790
951
  if (!Number.isFinite(occupied(fit))) throw new RangeError("Metric text layout exceeds finite coordinates.");
791
- cache.set(key, fit);
952
+ cache2.set(key, fit);
792
953
  }
793
954
  return fit;
794
955
  };
@@ -904,10 +1065,10 @@ function layoutTimeline(value, box, options = {}) {
904
1065
  const metadata = [], events = timeline.events.map(() => []);
905
1066
  for (const part of source) if (part.eventIndex === void 0) metadata.push(part);
906
1067
  else events[part.eventIndex].push(part);
907
- const cache = /* @__PURE__ */ new Map();
1068
+ const cache2 = /* @__PURE__ */ new Map();
908
1069
  const measure = (part, size, width, alignment) => {
909
1070
  const key = JSON.stringify([part.path, size, width, alignment]);
910
- let measured = cache.get(key);
1071
+ let measured = cache2.get(key);
911
1072
  if (measured) return measured;
912
1073
  const fit = fitText(part.text, { width: Math.max(Number.MIN_VALUE, width - (outlines ? 2 * padding : 0)), height: Number.MAX_VALUE }, size, size, textWidthMeasurer(part.style, options.textMeasurement));
913
1074
  const ink = fit.sourceLines.map((line, index) => {
@@ -930,7 +1091,7 @@ function layoutTimeline(value, box, options = {}) {
930
1091
  const height = placement?.height ?? fit.sourceLines.length * fit.lineHeight;
931
1092
  if (!Number.isFinite(height)) throw new RangeError("Timeline text exceeds finite layout coordinates.");
932
1093
  measured = { fit, ink, height };
933
- cache.set(key, measured);
1094
+ cache2.set(key, measured);
934
1095
  return measured;
935
1096
  };
936
1097
  let selected, attempts = 0;
@@ -1267,6 +1428,7 @@ function richTextLayouter(input, box, requestedSize, options) {
1267
1428
  };
1268
1429
  return layout;
1269
1430
  }
1431
+ var PICTURE_BULLET_SCALE = 0.65;
1270
1432
  function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
1271
1433
  if (!Array.isArray(input) || ![box.width, box.height, requestedSize, minFontSize].every((value) => Number.isFinite(value) && value > 0)) throw new RangeError("List content, dimensions and font sizes must be valid.");
1272
1434
  const source = input.map((value, index) => {
@@ -1300,6 +1462,7 @@ function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { s
1300
1462
  y += description.height;
1301
1463
  }
1302
1464
  overflow ||= text.overflow || !!description?.overflow;
1465
+ const marker = { text: ["\u2022", "\u25E6", "\u25AA"][item.level % 3], x: box.x + offset, y: textBox.y + (text.richLines[0]?.baseline ?? fontSize), fontSize, style: resolveTextStyle(style, options.textMeasurement), indent }, side = fontSize * PICTURE_BULLET_SCALE;
1303
1466
  entries.push({
1304
1467
  index: item.index,
1305
1468
  level: item.level,
@@ -1311,7 +1474,8 @@ function fitList(input, box, requestedSize = 25, minFontSize = 16, options = { s
1311
1474
  textBox,
1312
1475
  description,
1313
1476
  descriptionBox,
1314
- marker: { text: ["\u2022", "\u25E6", "\u25AA"][item.level % 3], x: box.x + offset, y: textBox.y + (text.richLines[0]?.baseline ?? fontSize), fontSize, style: resolveTextStyle(style, options.textMeasurement), indent }
1477
+ marker,
1478
+ ...options.bulletImage ? { bulletImage: options.bulletImage, bulletBox: { x: marker.x, y: marker.y - side, width: side, height: side } } : {}
1315
1479
  });
1316
1480
  if (item.index < source.length - 1) y += fontSize * 0.28;
1317
1481
  }
@@ -1465,12 +1629,20 @@ function composeSlide(input, options = {}) {
1465
1629
  const minSize = (composition.minFontSize ?? 16) * scale;
1466
1630
  const rasterPadding = (options.textRasterPadding ?? 1) * scale;
1467
1631
  if (!Number.isFinite(rasterPadding) || rasterPadding < 0) throw new RangeError("Text raster padding must be finite and nonnegative.");
1468
- const alignmentFor = (field) => (field === "title" ? record2(slide.design).titleAlignment ?? options.titleAlignment : record2(slide.design).contentAlignment ?? options.contentAlignment) ?? "left";
1632
+ let coverGroup = false;
1633
+ const alignmentFor = (field) => (field === "title" || coverGroup && (field === "tag" || field === "subtitle") && record2(slide.design).contentAlignment === void 0 ? record2(slide.design).titleAlignment ?? options.titleAlignment : record2(slide.design).contentAlignment ?? options.contentAlignment) ?? "left";
1469
1634
  const styleFor = (field, path2) => resolveTextStyle({
1470
- fontFamily: (field === "title" ? options.fonts?.heading : field === "code" ? options.fonts?.code : options.fonts?.body) ?? (field === "code" ? "monospace" : "sans-serif"),
1635
+ fontFamily: (field === "title" ? options.fonts?.heading : field === "code" ? options.fonts?.code : field === "tag" ? options.fonts?.accent ?? options.fonts?.body : options.fonts?.body) ?? (field === "code" ? "monospace" : "sans-serif"),
1471
1636
  fontWeight: field === "title" ? 700 : 400,
1472
1637
  path: path2
1473
1638
  }, options.textMeasurement);
1639
+ const slideDesign = record2(slide.design), deckDesign = record2(record2(options.presentation).design);
1640
+ const designHint = (field) => slideDesign[field] !== void 0 ? { value: slideDesign[field], path: `slides.${options.slideIndex ?? 0}.design.${field}` } : deckDesign[field] !== void 0 ? { value: deckDesign[field], path: `design.${field}` } : void 0;
1641
+ const listBullet = designHint("listBullet");
1642
+ const bulletImage = listBullet?.value === "image" ? (() => {
1643
+ const resolved = resolveLogo(options.presentation, slide, { slot: "icon", onDark: options.darkBackground, slideIndex: options.slideIndex });
1644
+ return resolved ? { source: resolved.source, path: resolved.path } : void 0;
1645
+ })() : void 0;
1474
1646
  const widthFor = (field, path2) => textWidthMeasurer(styleFor(field, path2), options.textMeasurement);
1475
1647
  const fitPlacedText = (field, value, text, box, size, minimum, path2) => {
1476
1648
  const style = styleFor(field, path2), rich = field === "text" && Array.isArray(value);
@@ -1522,7 +1694,7 @@ function composeSlide(input, options = {}) {
1522
1694
  }
1523
1695
  throw new Error("Text placement did not evaluate its bounded floor trial.");
1524
1696
  };
1525
- const fitContent = (field, value, text, box, size, minimum, path2) => field === "text" ? fitPlacedText(field, value, text, box, size, minimum, path2) : field === "items" || field === "bullets" ? fitList(value, box, size, minimum, { style: styleFor(field, path2), textMeasurement: options.textMeasurement }) : fitText(text, box, size, minimum, widthFor(field, path2));
1697
+ 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, ...bulletImage ? { bulletImage } : {} }) : fitText(text, box, size, minimum, widthFor(field, path2));
1526
1698
  const path = `slides.${options.slideIndex ?? 0}`;
1527
1699
  const slideImageDiagnostics = [];
1528
1700
  const slideImage = resolveSlideImage(slide, layout, options.presentation, width, height, path, padding, scale, slideImageDiagnostics);
@@ -1544,7 +1716,17 @@ function composeSlide(input, options = {}) {
1544
1716
  const emptyHost = (host) => emptyPayload(host.blocks) && fields.every((field) => emptyPayload(host[field]));
1545
1717
  const hasBodyPayload = regions.some((key) => !emptyHost(record2(slide[key]))) || !emptyPayload(slide.blocks) || fields.some((field) => !emptyPayload(slide[field]));
1546
1718
  const isCover = !hasBodyPayload && (headingOnlyLayout || !layout.id && !layoutPlaceholders.some((placeholder) => !headings.has(placeholder.type ?? "")));
1547
- let y = headingTop;
1719
+ coverGroup = isCover;
1720
+ let logo;
1721
+ if (isCover) {
1722
+ const resolved = resolveLogo(options.presentation, slide, { slot: "lockup", onDark: options.darkBackground, slideIndex: options.slideIndex });
1723
+ if (resolved) {
1724
+ const logoHeight = 56 * scale;
1725
+ const box = { x: round(area.left + padding), y: round(headingTop), width: round(Math.max(scale, Math.min(4 * logoHeight, area.right - area.left - 2 * padding))), height: round(logoHeight) };
1726
+ logo = { box, slot: "lockup", path: resolved.path, source: resolved.source, variant: resolved.variant, anchor: "left" };
1727
+ }
1728
+ }
1729
+ let y = logo ? logo.box.y + logo.box.height + gap : headingTop;
1548
1730
  const headingItems = [];
1549
1731
  for (const field of ["tag", "title", "subtitle"]) {
1550
1732
  if (!slide[field]) continue;
@@ -1681,14 +1863,14 @@ function composeSlide(input, options = {}) {
1681
1863
  const boxes = gridBoxes(node.children.length, area2, cols, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), own.weights ?? [], modeFor(own) === "column");
1682
1864
  return node.children.reduce((score, child, index) => score + scoreNode(child, boxes[index], own, penalties), 0);
1683
1865
  };
1684
- const arrange = (nodes, area2, settings, reserved = 0, gapOverride, containerPath = path) => {
1866
+ const arrange = (nodes, area2, settings, reserved = 0, gapOverride, containerPath = path, recorded = true) => {
1685
1867
  const count = Math.max(nodes.length, reserved);
1686
1868
  if (!count) return;
1687
1869
  const mode = modeFor(settings), localGap = (settings.gap ?? 1 / 30) * Math.min(area2.width, area2.height);
1688
1870
  const actualGap = gapOverride ?? (settings === rootSettings ? gap : localGap);
1689
1871
  let cols = defaultColumns(count, area2, settings);
1690
1872
  const hasRegions = nodes.some((node) => node.region);
1691
- const candidates = decisions ? [] : void 0;
1873
+ const candidates = decisions && recorded ? [] : void 0;
1692
1874
  if (mode === "auto" && !hasRegions) {
1693
1875
  let best = Infinity;
1694
1876
  for (let candidate = 1; candidate <= Math.min(count, settings.columns ?? 6); candidate++) {
@@ -1703,22 +1885,22 @@ function composeSlide(input, options = {}) {
1703
1885
  }
1704
1886
  }
1705
1887
  }
1706
- decisions?.push({
1888
+ if (recorded) decisions?.push({
1707
1889
  path: containerPath,
1708
1890
  mode: hasRegions ? "regions" : mode,
1709
- reason: hasRegions ? "promoted-regions" : mode === "auto" ? "lowest-score" : "configured-mode",
1891
+ reason: hasRegions ? "promoted-regions" : settings === rootSettings && chartPrimary ? "chart-primary" : mode === "auto" ? "lowest-score" : "configured-mode",
1710
1892
  ...hasRegions ? {} : { selectedColumns: cols },
1711
1893
  candidates: candidates ?? []
1712
1894
  });
1713
1895
  const grid = gridGeometry(count, area2, cols, actualGap, settings.weights ?? [], mode === "column");
1714
1896
  const boxes = grid.boxes;
1715
- if (!nodes.some((node) => node.region)) flows.push({ path: containerPath, box: { ...area2 }, composition: { ...settings }, columns: grid.columns, rows: grid.rows, gap: grid.gap, itemCount: nodes.length, slotCount: count });
1897
+ if (recorded && !hasRegions) flows.push({ path: containerPath, box: { ...area2 }, composition: { ...settings }, columns: grid.columns, rows: grid.rows, gap: grid.gap, itemCount: nodes.length, slotCount: count });
1716
1898
  nodes.forEach((node, index) => {
1717
1899
  let box = node.region ? regionBox(node.region, area2, actualGap) : boxes[index];
1718
1900
  if (node.children) {
1719
1901
  const own = inheritedSettings(settings, node.composition), inner = inset(box, own);
1720
- groups.push({ path: node.path, box, contentBox: inner, composition: own });
1721
- arrange(node.children, inner, own, 0, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), node.path);
1902
+ if (!node.synthetic) groups.push({ path: node.path, box, contentBox: inner, composition: own });
1903
+ arrange(node.children, inner, own, 0, (own.gap ?? 1 / 30) * Math.min(box.width, box.height), node.path, !node.synthetic);
1722
1904
  } else {
1723
1905
  const frameBox = hasCards ? acceptedBox(box) : void 0;
1724
1906
  box = payloadBox(box);
@@ -1744,7 +1926,8 @@ function composeSlide(input, options = {}) {
1744
1926
  ...quoteLayout ? { quoteLayout } : {},
1745
1927
  ...codeLayout ? { codeLayout } : {},
1746
1928
  ...metricLayout ? { metricLayout } : {},
1747
- ...timelineLayout ? { timelineLayout } : {}
1929
+ ...timelineLayout ? { timelineLayout } : {},
1930
+ ...bulletImage && (node.field === "items" || node.field === "bullets") ? { bulletImage } : {}
1748
1931
  });
1749
1932
  if (box.width < 100 * scale || box.height < 60 * scale) diagnostics.push({ code: "small-cell", path: node.path, message: "Content cell is too small for comfortable reading; use fewer blocks or a different composition." });
1750
1933
  }
@@ -1755,8 +1938,23 @@ function composeSlide(input, options = {}) {
1755
1938
  const picture = placeholders.findIndex((p) => p.type === "picture");
1756
1939
  if (picture >= 0) placeholders.splice(picture, 1);
1757
1940
  }
1758
- const rootSettings = { ...composition, mode: composition.mode ?? (layout.slideLayoutDirection === "Vertical" ? "column" : layout.slideLayoutDirection === "Horizontal" ? "row" : "auto") };
1759
- arrange(pending, contentBox, rootSettings, composition.mode ? 0 : placeholders.length);
1941
+ const ownMode = record2(slide.composition).mode;
1942
+ const direction = designHint("contentDirection")?.value;
1943
+ const directionMode = direction === "vertical" ? "column" : direction === "horizontal" ? "row" : void 0;
1944
+ const layoutDirectionMode = layout.slideLayoutDirection === "Vertical" ? "column" : layout.slideLayoutDirection === "Horizontal" ? "row" : "auto";
1945
+ const chartHint = designHint("chartPrimary")?.value ?? (typeof layout.contentTypeChartPrimary === "string" ? layout.contentTypeChartPrimary.toLowerCase() : void 0);
1946
+ const chartSide = chartHint === "left" || chartHint === "right" || chartHint === "top" || chartHint === "bottom" ? chartHint : void 0;
1947
+ const chartIndex = pending.findIndex((node) => !node.children && node.field === "chart");
1948
+ const chartPrimary = chartSide !== void 0 && !ownMode && !regions.length && chartIndex >= 0 && pending.some((node) => node.children || node.field !== "chart") ? chartSide : void 0;
1949
+ const rootSettings = chartPrimary ? { ...composition, mode: chartPrimary === "left" || chartPrimary === "right" ? "row" : "column", columns: void 0, weights: chartPrimary === "left" || chartPrimary === "top" ? [3, 2] : [2, 3] } : { ...composition, mode: composition.mode ?? directionMode ?? layoutDirectionMode };
1950
+ if (chartPrimary) {
1951
+ const primary = pending[chartIndex], rest = pending.filter((_, index) => index !== chartIndex);
1952
+ const container = { field: "blocks", type: "group", value: rest.map((node) => node.payload), path, payload: {}, children: rest, composition: {}, synthetic: true };
1953
+ arrange(chartPrimary === "left" || chartPrimary === "top" ? [primary, container] : [container, primary], contentBox, rootSettings, 0);
1954
+ } else arrange(pending, contentBox, rootSettings, composition.mode ? 0 : placeholders.length);
1955
+ if (listBullet?.value === "image" && !bulletImage && items.some((item) => item.field === "items" || item.field === "bullets")) {
1956
+ diagnostics.push({ code: "unresolved-content", path: listBullet.path, message: "Picture bullets (listBullet: image) need design.logo or a primary organization logo; the marker glyph is drawn instead." });
1957
+ }
1760
1958
  for (const item of items) {
1761
1959
  for (const key of ["x", "y", "width", "height"]) item.box[key] = round(item.box[key]);
1762
1960
  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." });
@@ -1789,7 +1987,7 @@ function composeSlide(input, options = {}) {
1789
1987
  } : void 0;
1790
1988
  if (failures.length) throw new OPFCompositionError(failures, explanation);
1791
1989
  diagnostics.push(...slideImageDiagnostics);
1792
- return { width, height, contentBox, items, groups, flows, diagnostics, composition, ...furniture ? { furniture } : {}, ...slideImage ? { slideImage } : {}, ...explanation ? { explanation } : {} };
1990
+ return { width, height, contentBox, items, groups, flows, diagnostics, composition, ...furniture ? { furniture } : {}, ...slideImage ? { slideImage } : {}, ...logo ? { logo } : {}, ...explanation ? { explanation } : {} };
1793
1991
  }
1794
1992
  function resolveCanvasDimensions(input) {
1795
1993
  const presets = {
@@ -1809,4 +2007,4 @@ function resolveCanvasDimensions(input) {
1809
2007
  return { width, height };
1810
2008
  }
1811
2009
 
1812
- export { DEFAULT_FONT_SCHEME, DEFAULT_FURNITURE_DATE_FORMAT, DEFAULT_SLIDE_NUMBER_FORMAT, MAX_COMPOSITION_DEPTH, OPFCompositionError, chartColorForFill, colorContrast, composeSlide, fitList, fitRichText, fitText, formatFurnitureDate, formatSlideNumber, layoutCode, layoutFurniture, layoutMetric, layoutQuote, layoutTable, layoutTimeline, measureText, measureTextOutline, normalizeHexColor, placeTextLines, resolveCanvasDimensions, resolveColorRef, resolveFontFamilies, resolveFontSchemeReference, resolveSocialProfile, resolveTextStyle, slideImageShape, tableGrid, tableRowBoundaries, textColorForFill, textWidthMeasurer, wrapText };
2010
+ export { DEFAULT_FONT_SCHEME, DEFAULT_FURNITURE_DATE_FORMAT, DEFAULT_SLIDE_NUMBER_FORMAT, MAX_COMPOSITION_DEPTH, OPFCompositionError, PICTURE_BULLET_SCALE, chartColorForFill, colorContrast, composeSlide, fitList, fitRichText, fitText, formatFurnitureDate, formatSlideNumber, layoutCode, layoutFurniture, layoutMetric, layoutQuote, layoutTable, layoutTimeline, measureText, measureTextOutline, normalizeHexColor, placeTextLines, resolveCanvasDimensions, resolveColorRef, resolveFontFamilies, resolveFontSchemeReference, resolveLogo, resolveSocialProfile, resolveTextStyle, slideImageShape, tableGrid, tableRowBoundaries, textColorForFill, textWidthMeasurer, wrapText };
@@ -1,5 +1,5 @@
1
- import { validatePresentation, validationDefinition, validateCatalogRecord } from './chunk-JCXHSTSM.js';
2
- import { schemas, schemaEntries } from './chunk-JTXCMVVP.js';
1
+ import { validatePresentation, validationDefinition, validateCatalogRecord } from './chunk-FDCWQHWO.js';
2
+ import { schemas, schemaEntries } from './chunk-KJKASEPE.js';
3
3
  import { catalogKinds, catalogEntries, catalogSchemaNames } from './chunk-AEUSVEUY.js';
4
4
  import { parseTree, printParseErrorCode, getNodePath } from 'jsonc-parser';
5
5
 
@@ -1,5 +1,5 @@
1
- import { MAX_COMPOSITION_DEPTH, tableGrid } from './chunk-BVLWFMDX.js';
2
- import { schemas, schemaEntries } from './chunk-JTXCMVVP.js';
1
+ import { MAX_COMPOSITION_DEPTH, tableGrid } from './chunk-3H2U7FDR.js';
2
+ import { schemas, schemaEntries } from './chunk-KJKASEPE.js';
3
3
  import { catalogSchemaNames } from './chunk-AEUSVEUY.js';
4
4
  import Ajv2020 from 'ajv/dist/2020.js';
5
5
  import addFormats from 'ajv-formats';
@@ -847,7 +847,7 @@ Deck-level concerns that aren't part of the storyline (audience, tone, takeaway,
847
847
  },
848
848
  "logo": {
849
849
  "$ref": "#/$defs/Asset",
850
- "description": "Source for the organization's logo image. Accepts an HTTPS URL, data URI, relative path (resolved against the OPF file location), local path, or 'asset:<id>' reference. Common formats are SVG (preferred for vector logos), PNG (with transparency), or JPG. The primary organization's logo is used by default for cover-slide and footer branding; design.logo overrides the source.",
850
+ "description": "Source for the organization's logo image. Accepts an HTTPS URL, data URI, relative path (resolved against the OPF file location), local path, or 'asset:<id>' reference. Common formats are SVG (preferred for vector logos), PNG (with transparency), or JPG. The primary organization's logo (role 'primary', else the first organization) is the fallback source wherever the deck logo is drawn: a slide's design.logo, then design.logo, then this value. It is therefore drawn on cover and section slides and by header/footer zones with logo: true, exactly as design.logo would be (see design.logo and docs/design-resolution.md).",
851
851
  "examples": [
852
852
  "https://cdn.acme.com/logo.svg",
853
853
  "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9V3iWggAAAAASUVORK5CYII=",
@@ -947,7 +947,7 @@ Deck-level concerns that aren't part of the storyline (audience, tone, takeaway,
947
947
  },
948
948
  "photo": {
949
949
  "$ref": "#/$defs/Asset",
950
- "description": "Source for the speaker's headshot image. Accepts an HTTPS URL, data URI, relative path (resolved against the OPF file location), local path, or 'asset:<id>' reference. Common formats are JPG or PNG; SVG is not appropriate for photographic content. Used on cover and bio slides.",
950
+ "description": "Source for the speaker's headshot image. Accepts an HTTPS URL, data URI, relative path (resolved against the OPF file location), local path, or 'asset:<id>' reference. Common formats are JPG or PNG; SVG is not appropriate for photographic content. Authoring metadata for hosts and layouts (cover and bio slides built by a host): the reference renderer and exporter do not draw it, because no slide field or layout slot places a speaker; it round-trips through PPTX provenance.",
951
951
  "examples": [
952
952
  "https://cdn.acme.com/headshots/alice.jpg",
953
953
  "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAr/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AJ//2Q==",
@@ -1355,7 +1355,7 @@ Resolution order: inline catalogs.fontSchemes.records[] -> catalogs.fontSchemes.
1355
1355
  "$ref": "#/$defs/LogoSet"
1356
1356
  }
1357
1357
  ],
1358
- "description": "Deck logo assets used by layouts, covers, section dividers, headers, and footers. A string is the default logo source; object form provides light/dark, stacked, icon, and wordmark variants. When omitted, the renderer falls back to the primary organization logo.",
1358
+ "description": "Deck logo assets used by covers, section dividers, headers, footers and picture bullets. A string or Asset object is the default logo source; the LogoSet object form provides light/dark, stacked, icon, and wordmark variants (see LogoSet for the selection order). Source precedence: a slide's design.logo, then this value, then the primary organization's logo; absence inherits. Placement (reference engines, vetoable): on a cover or section slide (no body payload on a heading-only layout such as title, title-subtitle or section-divider, or no layout) the lockup variant is drawn at the top-left of the free area, inside the slide padding and below any header furniture, in a box 56 reference pixels tall and at most four times as wide; the tag/title/subtitle group then centers in the remaining span. Content slides never get an automatic logo. Header and footer zones draw the icon variant when they set logo: true, and design.listBullet 'image' draws it as the picture bullet. composeSlide returns the cover box as geometry.logo.",
1359
1359
  "examples": [
1360
1360
  "asset:acme-logo",
1361
1361
  {
@@ -1634,7 +1634,7 @@ Resolution order: inline catalogs.fontSchemes.records[] -> catalogs.fontSchemes.
1634
1634
  "horizontal",
1635
1635
  "vertical"
1636
1636
  ],
1637
- "description": "Axis along which parallel body/content regions are arranged."
1637
+ "description": "Axis along which parallel body content is arranged. Sets the root arrangement mode of blocks and root payloads when no composition.mode is set on the slide or on its layout record: 'vertical' is column, 'horizontal' is row. Precedence: composition.mode (the slide's own, else the layout record's geometry contract), then this value (slide design, then deck design), then the layout record's slideLayoutDirection, then automatic selection. A layout that carries a composition.mode keeps its grid or direction whatever this value says. Promoted regions (left, top:left, ...) keep their explicit geometry and nested groups keep their own composition."
1638
1638
  },
1639
1639
  "chartPrimary": {
1640
1640
  "type": "string",
@@ -1645,7 +1645,7 @@ Resolution order: inline catalogs.fontSchemes.records[] -> catalogs.fontSchemes.
1645
1645
  "left",
1646
1646
  "right"
1647
1647
  ],
1648
- "description": "For chart layouts, where the primary chart sits relative to supporting content. 'none' means chart regions have equal weight."
1648
+ "description": "Where the primary chart sits relative to supporting content. Effective value: slide design, then deck design, then the layout record's contentTypeChartPrimary. When the slide has no promoted regions and no composition.mode of its own, and its root nodes mix at least one chart with other content, the first chart becomes a primary track and the other nodes form one synthetic sub-grid: 'left'/'right' is a two-track row and 'top'/'bottom' a two-track column, weighted 3:2 in favor of the chart; explicit root columns and weights are ignored while it applies. 'none' (the default) keeps the ordinary automatic grid with equal weight."
1649
1649
  },
1650
1650
  "imageFill": {
1651
1651
  "type": "string",
@@ -1661,7 +1661,7 @@ Resolution order: inline catalogs.fontSchemes.records[] -> catalogs.fontSchemes.
1661
1661
  "character",
1662
1662
  "image"
1663
1663
  ],
1664
- "description": "Default bullet rendering style for list layouts."
1664
+ "description": "Marker style for items and bullets lists. 'character' (the default) draws the glyph marker. 'image' draws the deck's icon logo (a slide's design.logo, then design.logo, then the primary organization's logo; light variants on dark backgrounds) as a picture bullet: a square of the marker's font size whose bottom sits on the marker baseline, so list geometry does not change. Without a logo the glyph is drawn and the engine reports unresolved-content at this field."
1665
1665
  }
1666
1666
  }
1667
1667
  },
@@ -2098,7 +2098,7 @@ Resolution order: inline catalogs.fontSchemes.records[] -> catalogs.fontSchemes.
2098
2098
  },
2099
2099
  "accent": {
2100
2100
  "$ref": "#/$defs/Font",
2101
- "description": "Abstract role: font used for accent text such as quotes or callouts. No direct OOXML slot."
2101
+ "description": "Abstract role: font used for accent text. When set, the slide tag (eyebrow) and the quote body use this family instead of the body and heading families; nothing else changes. resolveFontFamilies() returns it as accent only when the scheme defines it. No direct OOXML slot: PPTX export writes it on those runs, and the theme fonts stay major/minor."
2102
2102
  },
2103
2103
  "code": {
2104
2104
  "$ref": "#/$defs/Font",
@@ -2365,7 +2365,7 @@ Resolution order: inline catalogs.fontSchemes.records[] -> catalogs.fontSchemes.
2365
2365
  },
2366
2366
  "color": {
2367
2367
  "type": "string",
2368
- "description": "Fixed solid fill color, usually a hex string. Use { type: 'theme', slot: ... } for PowerPoint's four theme-controlled background choices.",
2368
+ "description": "Fixed solid fill color: a hex string, a color-scheme slot or role name, or a var:<id> variable reference (a ColorRef, resolved against the effective color scheme and the deck variables). Use { type: 'theme', slot: ... } for PowerPoint's four theme-controlled background choices.",
2369
2369
  "examples": [
2370
2370
  "#FFFFFF",
2371
2371
  "#0F172A",
@@ -2414,7 +2414,7 @@ Resolution order: inline catalogs.fontSchemes.records[] -> catalogs.fontSchemes.
2414
2414
  "properties": {
2415
2415
  "color": {
2416
2416
  "type": "string",
2417
- "description": "Stop color as a hex string.",
2417
+ "description": "Stop color: a hex string, a color-scheme slot or role name, or a var:<id> variable reference (a ColorRef).",
2418
2418
  "examples": [
2419
2419
  "#1E40AF",
2420
2420
  "#22D3EE"
@@ -2519,14 +2519,14 @@ Resolution order: inline catalogs.fontSchemes.records[] -> catalogs.fontSchemes.
2519
2519
  },
2520
2520
  "foregroundColor": {
2521
2521
  "type": "string",
2522
- "description": "Foreground color for the pattern, usually a hex string.",
2522
+ "description": "Foreground color for the pattern: a hex string, a color-scheme slot or role name, or a var:<id> variable reference (a ColorRef).",
2523
2523
  "examples": [
2524
2524
  "#0F172A"
2525
2525
  ]
2526
2526
  },
2527
2527
  "backgroundColor": {
2528
2528
  "type": "string",
2529
- "description": "Background color behind the pattern, usually a hex string.",
2529
+ "description": "Background color behind the pattern: a hex string, a color-scheme slot or role name, or a var:<id> variable reference (a ColorRef).",
2530
2530
  "examples": [
2531
2531
  "#FFFFFF"
2532
2532
  ]
@@ -2543,7 +2543,7 @@ Resolution order: inline catalogs.fontSchemes.records[] -> catalogs.fontSchemes.
2543
2543
  },
2544
2544
  "LogoSet": {
2545
2545
  "type": "object",
2546
- "description": "Deck logo variants surfaced by layouts, covers, section dividers, headers, and footers. Organization identity lives in organization; this object only controls visual rendering assets. Renderer convention: on dark backgrounds prefer the 'light' variant, on light backgrounds prefer the 'dark' variant, and in square/vertical slots prefer the stacked family when present.",
2546
+ "description": "Deck logo variants surfaced by covers, section dividers, headers, footers and picture bullets. Organization identity lives in organization; this object only controls visual rendering assets. Engines select one variant per slot and background tone (resolveLogo in @openpresentation/opf): same-tone variants first, neutral ones next, the opposite tone last. Lockup on a dark background: light, default, stackedLight, stacked, wordmarkLight, wordmark, iconLight, icon, then dark, stackedDark, wordmarkDark, iconDark; on a light background: dark, default, stackedDark, stacked, wordmarkDark, wordmark, iconDark, icon, then light, stackedLight, wordmarkLight, iconLight. The icon slot (headers, footers, picture bullets) tries iconLight or iconDark for the tone, then icon, then the lockup chain; the stacked slot tries stackedLight or stackedDark, then stacked, then the lockup chain.",
2547
2547
  "additionalProperties": false,
2548
2548
  "properties": {
2549
2549
  "default": {
@@ -2636,8 +2636,12 @@ Resolution order: inline catalogs.fontSchemes.records[] -> catalogs.fontSchemes.
2636
2636
  },
2637
2637
  "HeaderFooterItem": {
2638
2638
  "type": "object",
2639
- "description": "One header/footer zone. Every configured field renders; fields in one zone stack top to bottom in the order image, text, organization, section, slide number, date. Put a date and a slide number in different zones to keep each on the zone's single line.",
2639
+ "description": "One header/footer zone. Every configured field renders; fields in one zone stack top to bottom in the order logo, image, text, organization, socials, section, slide number, date. Put a date and a slide number in different zones to keep each on the zone's single line.",
2640
2640
  "properties": {
2641
+ "logo": {
2642
+ "type": "boolean",
2643
+ "description": "Whether to render the deck's icon logo in this zone: a slide's design.logo, then design.logo, then the primary organization's logo (LogoSet icon variants first, light ones on dark backgrounds). It is a generated image part in the same box as image; without a logo the engine reports unresolved-content at this field. Use image for a literal picture that is not the deck logo."
2644
+ },
2641
2645
  "text": {
2642
2646
  "type": "string",
2643
2647
  "description": "Literal text rendered in this zone.",
@@ -1,5 +1,5 @@
1
- import { assertValidPresentation, visitContentPayloads } from './chunk-JCXHSTSM.js';
2
- import { DEFAULT_FONT_SCHEME, resolveFontSchemeReference, resolveFontFamilies, resolveCanvasDimensions, composeSlide, tableRowBoundaries } from './chunk-BVLWFMDX.js';
1
+ import { assertValidPresentation, visitContentPayloads } from './chunk-FDCWQHWO.js';
2
+ import { DEFAULT_FONT_SCHEME, resolveFontSchemeReference, resolveFontFamilies, resolveCanvasDimensions, composeSlide, tableRowBoundaries } from './chunk-3H2U7FDR.js';
3
3
  import { catalogs } from './chunk-AEUSVEUY.js';
4
4
 
5
5
  // src/pagination.ts
@@ -95,6 +95,7 @@ function paginateSlide(input, options = {}) {
95
95
  return composeSlide(withReadability(slide, true), { ...options, slideNumber: (options.slideNumber ?? sourceIndex + 1) + pageIndex });
96
96
  };
97
97
  const initial = geometry(source);
98
+ const fit = (diagnostics) => diagnostics.filter((issue) => issue.code === "text-overflow" || issue.code === "small-cell" || !!initial.furniture?.diagnostics.includes(issue));
98
99
  const repeatedMappings = (pageIndex) => {
99
100
  if (!initial.furniture) return [];
100
101
  const paths = new Set(initial.items.filter((item) => headingFields.has(item.field)).map((item) => item.path));
@@ -104,7 +105,7 @@ function paginateSlide(input, options = {}) {
104
105
  }
105
106
  return [...paths].map((sourcePath) => ({ sourcePath, outputPath: sourcePath.startsWith(`${sourceBase}.`) ? `slides.${sourceIndex + pageIndex}${sourcePath.slice(sourceBase.length)}` : sourcePath }));
106
107
  };
107
- 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) } : {} }] };
108
+ if (!fit(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) } : {} }] };
108
109
  const headerIssues = initial.diagnostics.filter((issue) => headingFields.has(issue.path.slice(sourceBase.length + 1)) || initial.furniture?.diagnostics.includes(issue));
109
110
  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);
110
111
  const leaves = initial.items.filter((item) => !headingFields.has(item.field)).map((item) => leafFor(item.path, item.field, item.value));
@@ -170,12 +171,12 @@ function paginateSlide(input, options = {}) {
170
171
  if (pageIndex > 0) delete slide.notes;
171
172
  return { slide, mappings };
172
173
  };
173
- const diagnosticsFor = (portions) => geometry(project(portions, slides.length).slide, slides.length).diagnostics;
174
+ const diagnosticsFor = (portions) => fit(geometry(project(portions, slides.length).slide, slides.length).diagnostics);
174
175
  const finish = () => {
175
176
  if (!selected.size) return;
176
177
  if (slides.length >= maxSlides) throw new OPFPaginationError(`Pagination needs more than ${maxSlides} slides. No partial result was returned.`);
177
178
  const { slide, mappings } = project(selected, slides.length);
178
- const issues = geometry(slide, slides.length).diagnostics;
179
+ const issues = fit(geometry(slide, slides.length).diagnostics);
179
180
  if (issues.length) throw new OPFPaginationError("A continuation page does not fit at its final page number. No partial result was returned.", issues);
180
181
  assignPageIds(slide, slides.length);
181
182
  assertValidPresentation({ slides: [slide] });