@openpresentation/opf 0.27.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/browser.js +18 -18
  2. package/dist/{catalog-refs-BSfGELpt.d.ts → catalog-refs-kAefsZWT.d.ts} +2 -1
  3. package/dist/{catalog-schemas-DoxA2BgY.d.ts → catalog-schemas-CC1d_h8s.d.ts} +3 -2
  4. package/dist/{chart-data-BnNqgnC-.d.ts → chart-data-Ci9s_NDN.d.ts} +2 -1
  5. package/dist/{chunk-LRTQXGJO.js → chunk-2CUCYENF.js} +523 -89
  6. package/dist/{chunk-4ZM5G3XX.js → chunk-5Q4IYLMW.js} +30 -14
  7. package/dist/{chunk-OSM3LZ2Y.js → chunk-BQUWRXTR.js} +4 -4
  8. package/dist/{chunk-4ACS57MG.js → chunk-CR72ZJ6W.js} +2 -2
  9. package/dist/{chunk-ZGTOZA2S.js → chunk-F6DCEGB3.js} +6 -6
  10. package/dist/{chunk-O3SA5SJ4.js → chunk-KELC4K62.js} +1 -1
  11. package/dist/{chunk-WJ4XTCOD.js → chunk-KXKN3W5Z.js} +3 -3
  12. package/dist/{chunk-TMREBZBQ.js → chunk-LLEXM7TQ.js} +3 -3
  13. package/dist/{chunk-GNKESMZT.js → chunk-MTFK7QEY.js} +6 -6
  14. package/dist/{chunk-JSHMFTSY.js → chunk-N77N4AOD.js} +1 -1
  15. package/dist/{chunk-7XUEKIXH.js → chunk-OAIAFXMS.js} +2 -2
  16. package/dist/{chunk-EO3JWEO5.js → chunk-P6PKYJTS.js} +1 -1
  17. package/dist/{chunk-353VBJ6K.js → chunk-PCU4XGKV.js} +5 -5
  18. package/dist/{chunk-SYW4BCGZ.js → chunk-SEZ4EPW6.js} +2 -2
  19. package/dist/{chunk-ABFDFZIW.js → chunk-TNUTRV6E.js} +1 -1
  20. package/dist/{chunk-CXPBU5W4.js → chunk-UOHWG4OS.js} +1 -1
  21. package/dist/{chunk-APLNGCEZ.js → chunk-VGGPAMP7.js} +2 -2
  22. package/dist/{chunk-PAGVSTVF.js → chunk-WPKCWQ6W.js} +149 -26
  23. package/dist/{chunk-KZPZRHL6.js → chunk-XZ23FORL.js} +6 -6
  24. package/dist/chunk-ZROVYWGU.js +1 -0
  25. package/dist/{composition-BGmpe5e4.d.ts → composition-C-hd_Ci3.d.ts} +84 -10
  26. package/dist/composition.d.ts +3 -3
  27. package/dist/composition.js +1 -1
  28. package/dist/conversion-XJTDWQ5W.js +24 -0
  29. package/dist/{conversion-8SQD4dqu.d.ts → conversion-hbZeS5kD.d.ts} +9 -8
  30. package/dist/convert.d.ts +6 -5
  31. package/dist/convert.js +7 -7
  32. package/dist/data.d.ts +4 -3
  33. package/dist/data.js +2 -2
  34. package/dist/{deck-report-BaKgPh_N.d.ts → deck-report-DQMsvyBI.d.ts} +5 -4
  35. package/dist/deck.d.ts +10 -9
  36. package/dist/deck.js +11 -11
  37. package/dist/diff.d.ts +3 -2
  38. package/dist/diff.js +4 -4
  39. package/dist/docs.d.ts +2 -1
  40. package/dist/docs.js +29 -17
  41. package/dist/examples.d.ts +3 -2
  42. package/dist/examples.js +90 -50
  43. package/dist/{explain-JSuESV-Y.d.ts → explain-iZeNOUc6.d.ts} +2 -1
  44. package/dist/{finding-Bh56sGtF.d.ts → finding-l9dlQ2NF.d.ts} +1 -1
  45. package/dist/font-policy.d.ts +2 -1
  46. package/dist/format.d.ts +2 -1
  47. package/dist/format.js +2 -2
  48. package/dist/ids.d.ts +3 -2
  49. package/dist/ids.js +4 -4
  50. package/dist/index.d.ts +38 -17
  51. package/dist/index.js +22 -22
  52. package/dist/markdown.d.ts +8 -7
  53. package/dist/markdown.js +8 -8
  54. package/dist/node-engine.d.ts +34 -11
  55. package/dist/node-engine.js +35 -29
  56. package/dist/{pagination-BsUTsQvk.d.ts → pagination-DyAKYpXu.d.ts} +4 -3
  57. package/dist/pagination.d.ts +4 -4
  58. package/dist/pagination.js +4 -4
  59. package/dist/patch.d.ts +4 -2
  60. package/dist/patch.js +3 -3
  61. package/dist/precompiled.d.ts +1 -1
  62. package/dist/precompiled.js +2 -2
  63. package/dist/{presentation-C-nHZyMw.d.ts → presentation-8jLSTFqL.d.ts} +10 -10
  64. package/dist/repo-readme.js +1 -1
  65. package/dist/schema-engine-ajv.js +1 -1
  66. package/dist/schema-engine-precompiled.js +2 -2
  67. package/dist/schema.d.ts +4 -3
  68. package/dist/schema.js +3 -3
  69. package/dist/schemas.d.ts +2 -1
  70. package/dist/schemas.js +1 -1
  71. package/dist/source.d.ts +5 -4
  72. package/dist/source.js +10 -10
  73. package/dist/spec/README.md +1 -1
  74. package/dist/spec/schemas/audience.schema.json +1 -0
  75. package/dist/spec/schemas/catalog-index.schema.json +1 -0
  76. package/dist/spec/schemas/catalog-manifest.schema.json +1 -0
  77. package/dist/spec/schemas/chart-type.schema.json +1 -0
  78. package/dist/spec/schemas/color-scheme.schema.json +1 -0
  79. package/dist/spec/schemas/finding.schema.json +1 -0
  80. package/dist/spec/schemas/font-scheme.schema.json +1 -0
  81. package/dist/spec/schemas/language.schema.json +1 -0
  82. package/dist/spec/schemas/layout-preview-index.schema.json +1 -0
  83. package/dist/spec/schemas/layout.schema.json +8 -6
  84. package/dist/spec/schemas/narrative.schema.json +1 -0
  85. package/dist/spec/schemas/opf.schema.json +8 -7
  86. package/dist/spec/schemas/purpose.schema.json +1 -0
  87. package/dist/spec/schemas/social-platform.schema.json +1 -0
  88. package/dist/spec/schemas/theme.schema.json +3 -1
  89. package/dist/spec/schemas/tone.schema.json +1 -0
  90. package/dist/spec-files.d.ts +2 -1
  91. package/dist/symbol-font-encodings.d.ts +2 -1
  92. package/dist/types.d.ts +10 -10
  93. package/dist/{validation-types-6_jEaHeE.d.ts → validation-types-mNmfcY9A.d.ts} +5 -4
  94. package/dist/validator.d.ts +8 -8
  95. package/dist/validator.js +3 -3
  96. package/dist/yaml.d.ts +8 -7
  97. package/dist/yaml.js +7 -7
  98. package/package.json +7 -7
  99. package/dist/chunk-GJO5NUVT.js +0 -1
  100. package/dist/conversion-LN6PFRZL.js +0 -24
@@ -2068,11 +2068,28 @@ var u32be = (b, i) => (at2(b, i) << 24 | at2(b, i + 1) << 16 | at2(b, i + 2) <<
2068
2068
  var u16le = (b, i) => at2(b, i) | at2(b, i + 1) << 8;
2069
2069
  var u24le = (b, i) => at2(b, i) | at2(b, i + 1) << 8 | at2(b, i + 2) << 16;
2070
2070
  var ascii = (b, i, n) => String.fromCharCode(...b.subarray(i, i + n));
2071
+ function exifOrientation(b, start, end) {
2072
+ if (end > b.length || ascii(b, start, 4) !== "Exif" || at2(b, start + 4) !== 0) return void 0;
2073
+ const tiff = start + 6, little = ascii(b, tiff, 2) === "II";
2074
+ if (!little && ascii(b, tiff, 2) !== "MM") return void 0;
2075
+ const u16 = (index) => little ? u16le(b, index) : u16be(b, index);
2076
+ const u32 = (index) => little ? (u16le(b, index) | u16le(b, index + 2) << 16) >>> 0 : u32be(b, index);
2077
+ const directory = tiff + u32(tiff + 4);
2078
+ if (directory + 2 > end) return void 0;
2079
+ for (let entry = 0, count = u16(directory); entry < count && directory + 2 + entry * 12 + 12 <= end; entry++) {
2080
+ const at12 = directory + 2 + entry * 12;
2081
+ if (u16(at12) === 274) {
2082
+ const value = u16(at12 + 8);
2083
+ return value >= 1 && value <= 8 ? value : void 0;
2084
+ }
2085
+ }
2086
+ return void 0;
2087
+ }
2071
2088
  function rasterDimensions(b) {
2072
2089
  if (b.length >= 24 && u32be(b, 0) === 2303741511 && ascii(b, 12, 4) === "IHDR") return { width: u32be(b, 16), height: u32be(b, 20) };
2073
2090
  if (b.length >= 10 && (ascii(b, 0, 6) === "GIF87a" || ascii(b, 0, 6) === "GIF89a")) return { width: u16le(b, 6), height: u16le(b, 8) };
2074
2091
  if (b.length >= 4 && b[0] === 255 && b[1] === 216) {
2075
- let i = 2;
2092
+ let i = 2, orientation = 1;
2076
2093
  while (i + 9 < b.length) {
2077
2094
  if (at2(b, i) !== 255) {
2078
2095
  i++;
@@ -2087,7 +2104,11 @@ function rasterDimensions(b) {
2087
2104
  i += 2;
2088
2105
  continue;
2089
2106
  }
2090
- if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) return { height: u16be(b, i + 5), width: u16be(b, i + 7) };
2107
+ if (marker === 225) orientation = exifOrientation(b, i + 4, i + 2 + u16be(b, i + 2)) ?? orientation;
2108
+ if (marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204) {
2109
+ const height = u16be(b, i + 5), width = u16be(b, i + 7);
2110
+ return orientation >= 5 ? { width: height, height: width } : { width, height };
2111
+ }
2091
2112
  i += 2 + u16be(b, i + 2);
2092
2113
  }
2093
2114
  return void 0;
@@ -2167,7 +2188,7 @@ var VALUES = {
2167
2188
  contentDirection: ["horizontal", "vertical"],
2168
2189
  chartPrimary: ["none", "top", "bottom", "left", "right"],
2169
2190
  imageFit: ["cover", "contain", "stretch"],
2170
- listBullet: ["character", "image"],
2191
+ listBullet: ["character", "image", "none"],
2171
2192
  mirror: "boolean"
2172
2193
  };
2173
2194
  var themeKeys = new Set(THEME_DESIGN_KEYS);
@@ -2395,6 +2416,8 @@ function parse(layout) {
2395
2416
  }
2396
2417
  };
2397
2418
  order(children);
2419
+ const stackAnchor = anchorOf(value.anchor);
2420
+ const stack = direction === "column" && children.length > 0 && children.every((node) => node.size === "auto") && stackAnchor !== void 0 && stackAnchor !== "top" ? stackAnchor : void 0;
2398
2421
  const overflowRegion = typeof value.overflowRegion === "string" ? value.overflowRegion : regions.find((entry) => entry.role === "primary" && entry.flow !== "none")?.name;
2399
2422
  return {
2400
2423
  issues,
@@ -2406,7 +2429,8 @@ function parse(layout) {
2406
2429
  regions,
2407
2430
  ...overflowRegion !== void 0 ? { overflowRegion } : {},
2408
2431
  title: names.has("title"),
2409
- subtitle: names.has("subtitle")
2432
+ subtitle: names.has("subtitle"),
2433
+ ...stack ? { stack } : {}
2410
2434
  }
2411
2435
  };
2412
2436
  }
@@ -2755,19 +2779,32 @@ function listColumnBreaks(heights, levels, count) {
2755
2779
  return solve(0, count).breaks;
2756
2780
  }
2757
2781
  function allocateRows(rows3, total, gap) {
2758
- const auto = rows3.map((row) => row.size === "auto" ? Math.max(0, Math.min(row.need, row.kind === "below" ? total : total / 2)) : 0);
2782
+ const auto = rows3.map((row) => row.size === "auto" ? Math.max(0, Math.min(row.need, row.kind === "heading" ? total / 2 : total)) : 0);
2759
2783
  const visible = rows3.filter((row, index) => row.size !== "auto" || auto[index] > 0).length;
2760
2784
  const headings2 = rows3.reduce((sum, row, index) => sum + (row.kind === "heading" ? auto[index] : 0), 0);
2761
2785
  const space = Math.max(0, total - headings2 - Math.max(0, visible - 1) * gap);
2762
2786
  const numeric = rows3.reduce((sum, row) => sum + (row.size === "auto" ? 0 : row.size), 0);
2763
2787
  const numericNeed = rows3.reduce((sum, row) => sum + (row.size === "auto" ? 0 : Math.max(0, row.need)), 0);
2764
2788
  const reserve = numeric > 0 ? Math.min(numericNeed, space / 2) : 0;
2765
- const body = rows3.reduce((sum, row, index) => sum + (row.size === "auto" && row.kind !== "heading" ? auto[index] : 0), 0);
2766
- const factor = body > space - reserve && body > 0 ? Math.max(0, space - reserve) / body : 1;
2767
- const heights = rows3.map((row, index) => row.size === "auto" ? row.kind === "heading" ? auto[index] : auto[index] * factor : 0);
2789
+ const bodyRows = rows3.flatMap((row, index) => row.size === "auto" && row.kind !== "heading" ? [index] : []);
2790
+ const fitted = shareHeights(bodyRows.map((index) => auto[index]), bodyRows.map((index) => rows3[index].floor ?? 0), Math.max(0, space - reserve));
2791
+ const heights = rows3.map((row, index) => row.size === "auto" ? row.kind === "heading" ? auto[index] : fitted[bodyRows.indexOf(index)] : 0);
2768
2792
  const rest = Math.max(0, space - heights.reduce((sum, height, index) => sum + (rows3[index].kind === "heading" ? 0 : height), 0));
2769
2793
  return rows3.map((row, index) => row.size === "auto" ? heights[index] : numeric > 0 ? rest * row.size / numeric : 0);
2770
2794
  }
2795
+ function shareHeights(needs, floors, total) {
2796
+ const space = Math.max(0, total);
2797
+ const need = needs.map((value) => Number.isFinite(value) ? Math.max(0, value) : 0);
2798
+ const floor = need.map((value, index) => Math.min(value, Math.max(0, Number.isFinite(floors[index]) ? floors[index] : 0)));
2799
+ const sum = (values) => values.reduce((a, b) => a + b, 0);
2800
+ const needed = sum(need), lowest = sum(floor);
2801
+ if (needed <= space) return need;
2802
+ if (lowest < space) {
2803
+ const give = (needed - space) / (needed - lowest);
2804
+ return need.map((value, index) => value - give * (value - floor[index]));
2805
+ }
2806
+ return lowest > 0 ? floor.map((value) => value * space / lowest) : need.map(() => 0);
2807
+ }
2771
2808
 
2772
2809
  // src/direction.ts
2773
2810
  var strongRtl = /[\u200F\u061C]|(?=\p{L})[\p{Script=Arabic}\p{Script=Hebrew}\p{Script=Syriac}\p{Script=Thaana}\p{Script=Nko}\p{Script=Adlam}\p{Script=Hanifi_Rohingya}\p{Script=Mandaic}\p{Script=Samaritan}\p{Script=Mende_Kikakui}\p{Script=Imperial_Aramaic}\p{Script=Phoenician}\p{Script=Kharoshthi}\p{Script=Old_South_Arabian}\p{Script=Old_North_Arabian}\p{Script=Avestan}\p{Script=Inscriptional_Parthian}\p{Script=Inscriptional_Pahlavi}\p{Script=Psalter_Pahlavi}\p{Script=Old_Turkic}\p{Script=Old_Hungarian}\p{Script=Nabataean}\p{Script=Palmyrene}\p{Script=Hatran}\p{Script=Manichaean}\p{Script=Sogdian}\p{Script=Old_Sogdian}\p{Script=Elymaic}\p{Script=Chorasmian}\p{Script=Yezidi}\p{Script=Cypriot}\p{Script=Lydian}\p{Script=Meroitic_Cursive}\p{Script=Meroitic_Hieroglyphs}\u{10F70}-\u{10FAF}\u{10D40}-\u{10D8F}]/u;
@@ -3354,6 +3391,93 @@ function resolveSlideDirection(presentation, slideIndex) {
3354
3391
  }
3355
3392
  }
3356
3393
 
3394
+ // src/quote-marks.ts
3395
+ var marks = (open, close) => ({ open, close });
3396
+ var ENGLISH = marks("\u201C", "\u201D");
3397
+ var GUILLEMETS = marks("\xAB", "\xBB");
3398
+ var LOW_HIGH = marks("\u201E", "\u201C");
3399
+ var LOW_RIGHT = marks("\u201E", "\u201D");
3400
+ var RIGHT_ONLY = marks("\u201D", "\u201D");
3401
+ var CORNER = marks("\u300C", "\u300D");
3402
+ var REVERSED = marks("\xBB", "\xAB");
3403
+ var QUOTE_MARKS = {
3404
+ en: ENGLISH,
3405
+ es: GUILLEMETS,
3406
+ fr: GUILLEMETS,
3407
+ de: LOW_HIGH,
3408
+ it: GUILLEMETS,
3409
+ pt: ENGLISH,
3410
+ nl: ENGLISH,
3411
+ ca: GUILLEMETS,
3412
+ gl: GUILLEMETS,
3413
+ eu: GUILLEMETS,
3414
+ sv: RIGHT_ONLY,
3415
+ fi: RIGHT_ONLY,
3416
+ da: REVERSED,
3417
+ nb: GUILLEMETS,
3418
+ nn: GUILLEMETS,
3419
+ no: GUILLEMETS,
3420
+ is: LOW_HIGH,
3421
+ pl: LOW_RIGHT,
3422
+ hu: LOW_RIGHT,
3423
+ ro: LOW_RIGHT,
3424
+ cs: LOW_HIGH,
3425
+ sk: LOW_HIGH,
3426
+ sl: LOW_HIGH,
3427
+ hr: LOW_HIGH,
3428
+ sr: LOW_HIGH,
3429
+ bg: LOW_HIGH,
3430
+ lt: LOW_HIGH,
3431
+ lv: LOW_HIGH,
3432
+ et: LOW_HIGH,
3433
+ ru: GUILLEMETS,
3434
+ uk: GUILLEMETS,
3435
+ el: GUILLEMETS,
3436
+ tr: ENGLISH,
3437
+ ga: ENGLISH,
3438
+ cy: ENGLISH,
3439
+ ja: CORNER,
3440
+ zh: ENGLISH,
3441
+ "zh-tw": CORNER,
3442
+ "zh-hk": CORNER,
3443
+ "zh-hant": CORNER,
3444
+ ko: ENGLISH,
3445
+ ar: GUILLEMETS,
3446
+ fa: GUILLEMETS,
3447
+ he: ENGLISH,
3448
+ ur: ENGLISH,
3449
+ hi: ENGLISH,
3450
+ th: ENGLISH,
3451
+ vi: ENGLISH,
3452
+ id: ENGLISH,
3453
+ ms: ENGLISH,
3454
+ "de-ch": GUILLEMETS,
3455
+ "fr-ch": GUILLEMETS,
3456
+ "pt-pt": GUILLEMETS
3457
+ };
3458
+ var ASCII_QUOTE_MARKS = marks('"', '"');
3459
+ function quoteMarksFor(tag) {
3460
+ if (tag === void 0 || tag === "") return ENGLISH;
3461
+ const lower = tag.toLowerCase().replaceAll("_", "-"), primary = lower.split("-")[0];
3462
+ return QUOTE_MARKS[lower] ?? QUOTE_MARKS[primary] ?? ASCII_QUOTE_MARKS;
3463
+ }
3464
+ function quoteLanguage(presentation, slideIndex) {
3465
+ if (presentation === null || typeof presentation !== "object" || presentation.language === void 0) return void 0;
3466
+ try {
3467
+ const resolved = resolveScriptFonts(presentation, Number.isInteger(slideIndex) ? { slideIndex } : {});
3468
+ return resolved.languageSource === "document" ? resolved.bcp47 : void 0;
3469
+ } catch {
3470
+ return void 0;
3471
+ }
3472
+ }
3473
+ var PAIRS = new Set(Object.values(QUOTE_MARKS).concat(ASCII_QUOTE_MARKS).map((entry) => entry.open + entry.close));
3474
+ function isWrappedInQuoteMarks(text2) {
3475
+ return text2.length >= 2 && PAIRS.has(text2[0] + text2.at(-1));
3476
+ }
3477
+ function unwrapQuoteMarks(text2) {
3478
+ return isWrappedInQuoteMarks(text2) ? text2.slice(1, -1) : text2;
3479
+ }
3480
+
3357
3481
  // src/numbering.ts
3358
3482
  var NUMBERING_STYLES = ["arabic", "roman-upper", "roman-lower", "alpha-upper", "alpha-lower"];
3359
3483
  var NUMBERING_SUFFIXES = ["period", "paren", "paren-both"];
@@ -5340,7 +5464,7 @@ function fitImage(frame, fit, aspect, focus = { x: 0.5, y: 0.5 }) {
5340
5464
  right: round2((image.x + width - frame.x - frame.width) / width),
5341
5465
  bottom: round2((image.y + height - frame.y - frame.height) / height)
5342
5466
  };
5343
- for (const key of ["left", "top", "right", "bottom"]) if (Object.is(crop[key], -0)) crop[key] = 0;
5467
+ for (const key of ["left", "top", "right", "bottom"]) if (Math.abs(crop[key]) < 1e-9) crop[key] = 0;
5344
5468
  return { image: roundBox(image), crop };
5345
5469
  }
5346
5470
  function imageBackground(value) {
@@ -5375,17 +5499,29 @@ function composeOverlay(value, frame, shape, path, mirrorSide, diagnostics) {
5375
5499
  const box = !edge ? frame : edge === "top" ? { ...frame, height: round2(frame.height * part) } : edge === "bottom" ? { ...frame, y: round2(frame.y + frame.height * (1 - part)), height: round2(frame.height * part) } : edge === "left" ? { ...frame, width: round2(frame.width * part) } : { ...frame, x: round2(frame.x + frame.width * (1 - part)), width: round2(frame.width * part) };
5376
5500
  return { path, color: overlay.color, opacity, ...edge ? { edge } : {}, box, shape: edge ? imageShape("rectangle", box) : shape };
5377
5501
  }
5502
+ function anchoredOffset(spare, anchor) {
5503
+ return Math.max(0, anchor === "middle" ? spare / 2 : anchor === "bottom" ? spare : 0);
5504
+ }
5505
+ function containedBox(cell, aspect, anchor) {
5506
+ const width = Math.min(cell.width, cell.height * aspect), height = width / aspect;
5507
+ return { x: cell.x + (cell.width - width) / 2, y: cell.y + anchoredOffset(cell.height - height, anchor), width, height };
5508
+ }
5378
5509
  function composeImage(host, hostPath, region, frameArea, context) {
5379
5510
  const kindName = Object.hasOwn(IMAGE_SHAPES, host.shape) ? host.shape : "rectangle";
5380
5511
  const { scale: scale2 } = context;
5512
+ const fit = IMAGE_FITS.includes(host.fit) ? host.fit : context.fit ?? "cover";
5513
+ const pictureAspect = intrinsicImageAspect(host.image, context.assets);
5381
5514
  let box = { ...frameArea };
5382
5515
  const aspect = kindName === "circle" ? 1 : finite(host.aspectRatio, Number.MIN_VALUE, 10);
5383
5516
  if (aspect) {
5384
5517
  const frameWidth = Math.min(box.width, box.height * aspect), frameHeight = frameWidth / aspect;
5385
5518
  box = { x: box.x + (box.width - frameWidth) / 2, y: box.y + (box.height - frameHeight) / 2, width: frameWidth, height: frameHeight };
5519
+ } else if (context.anchor && fit === "contain" && pictureAspect) {
5520
+ const frameWidth = Math.min(box.width, box.height * pictureAspect);
5521
+ box = { x: box.x + (box.width - frameWidth) / 2, y: box.y, width: frameWidth, height: frameWidth / pictureAspect };
5386
5522
  }
5523
+ if (context.anchor && fit === "contain" && !aspect && box.height < frameArea.height) box.y = frameArea.y + anchoredOffset(frameArea.height - box.height, context.anchor);
5387
5524
  box = roundBox(box);
5388
- const fit = IMAGE_FITS.includes(host.fit) ? host.fit : context.fit ?? "cover";
5389
5525
  const focus = imageFocus(host.focus);
5390
5526
  const shape = imageShape(kindName, box, finite(host.cornerRadius, 0, 0.5));
5391
5527
  const result = { region: roundBox(region), box, fit, focus, shape };
@@ -5398,7 +5534,6 @@ function composeImage(host, hostPath, region, frameArea, context) {
5398
5534
  const overlay = composeOverlay(host.overlay, box, shape, `${hostPath}.overlay`, context.mirrorSide, context.diagnostics);
5399
5535
  if (overlay) result.overlay = overlay;
5400
5536
  if (context.placement) result.placement = context.placement;
5401
- const pictureAspect = intrinsicImageAspect(host.image, context.assets);
5402
5537
  if (pictureAspect) result.picture = fitImage(box, fit, pictureAspect, focus);
5403
5538
  return result;
5404
5539
  }
@@ -5622,7 +5757,72 @@ function layoutFurniture(input, options = {}) {
5622
5757
  return { algorithm: "furniture-flow-v2", configured, textMeasurement: options.textMeasurement ? "provided" : "estimated", textOutlines: outlines ? "provided" : "unavailable", parts, headerBottom, footerTop, diagnostics, overflow: diagnostics.length > 0 };
5623
5758
  }
5624
5759
  var textSegments = new Intl.Segmenter("und", { granularity: "grapheme" });
5760
+ var PROSE_NOMINAL_SIZE = 28;
5761
+ var LIST_NOMINAL_SIZE = 32;
5762
+ var LEAD_NOMINAL_SIZE = 32;
5763
+ var TABLE_NOMINAL_SIZE = 25;
5764
+ var QUOTE_NOMINAL_SIZE = 40;
5765
+ var CODE_NOMINAL_SIZE = 22;
5766
+ var LEGACY_CODE_NOMINAL_SIZE = 18;
5767
+ var QUOTE_SOURCE_SEPARATOR = " \u2013 ";
5768
+ var PROSE_MEASURE_CHARS = 70;
5769
+ var AVERAGE_CHARACTER_EM = 0.5;
5625
5770
  var WRAP_TOLERANCE = 0.01;
5771
+ var SENTENCE_END = /[.!?\u2026\u3002\uff01\uff1f]["'\u201d\u2019)\]\u00bb]*$/u;
5772
+ var CLAUSE_END = /[,;:\u2013\u2014\u060c\u061b\uff0c\u3001\uff1b\uff1a]["'\u201d\u2019)\]\u00bb]*$/u;
5773
+ var BALANCE_MAX_WORDS = 48;
5774
+ function balancedBreaks(text2, lines, widthOf, boxWidth) {
5775
+ const forced = /* @__PURE__ */ new Set();
5776
+ const paragraph = (group2) => {
5777
+ const count = group2.length;
5778
+ if (count < 2) return;
5779
+ const from = group2[0].start, to = group2.at(-1).end;
5780
+ const words = [...text2.slice(from, to).matchAll(/(?:[^\s]|\u00a0|\u202f|\ufeff)+/gu)].map((match) => ({ start: from + match.index, end: from + match.index + match[0].length, text: match[0] }));
5781
+ const total = words.length;
5782
+ if (total < count || total > BALANCE_MAX_WORDS) return;
5783
+ const cache2 = /* @__PURE__ */ new Map(), span = (first, last) => {
5784
+ const key = first * (BALANCE_MAX_WORDS + 1) + last;
5785
+ let value = cache2.get(key);
5786
+ if (value === void 0) {
5787
+ value = widthOf(words[first].start, words[last - 1].end);
5788
+ cache2.set(key, value);
5789
+ }
5790
+ return value;
5791
+ };
5792
+ const target = span(0, total) / count;
5793
+ if (!(target > 0)) return;
5794
+ const credit = (last) => last >= total ? 0 : SENTENCE_END.test(words[last - 1].text) ? 0.02 : CLAUSE_END.test(words[last - 1].text) ? 0.01 : 0;
5795
+ const best = Array.from({ length: count + 1 }, () => Array(total + 1).fill(Infinity)), from_ = Array.from({ length: count + 1 }, () => Array(total + 1).fill(-1));
5796
+ best[0][0] = 0;
5797
+ for (let line2 = 1; line2 <= count; line2++) for (let last = line2; last <= total - (count - line2); last++) for (let first = line2 - 1; first < last; first++) {
5798
+ const before = best[line2 - 1][first];
5799
+ if (before === Infinity) continue;
5800
+ const width = span(first, last);
5801
+ if (width > boxWidth + WRAP_TOLERANCE) continue;
5802
+ const cost = before + ((width - target) / target) ** 2 - credit(last);
5803
+ if (cost < best[line2][last]) {
5804
+ best[line2][last] = cost;
5805
+ from_[line2][last] = first;
5806
+ }
5807
+ }
5808
+ if (best[count][total] === Infinity) return;
5809
+ for (let line2 = count, last = total; line2 > 1; line2--) {
5810
+ const first = from_[line2][last];
5811
+ forced.add(words[first].start);
5812
+ last = first;
5813
+ }
5814
+ };
5815
+ let group = [];
5816
+ for (const line2 of lines) {
5817
+ group.push(line2);
5818
+ if (line2.hard) {
5819
+ paragraph(group);
5820
+ group = [];
5821
+ }
5822
+ }
5823
+ paragraph(group);
5824
+ return forced.size ? forced : void 0;
5825
+ }
5626
5826
  var HANGING_SPACE = /[^\S\r\n\u00a0\u202f\ufeff]/u;
5627
5827
  var SCRIPT_WIDTH_FACTORS = [
5628
5828
  [/\p{Script=Tamil}/u, 1.53],
@@ -5646,11 +5846,11 @@ function measureText(text2, fontSize) {
5646
5846
  function wrapText(text2, width, fontSize, measure = measureText) {
5647
5847
  return fitSourceText(text2, { width, height: Number.MAX_VALUE }, fontSize, fontSize, 1, measure, true, false).lines;
5648
5848
  }
5649
- function fitText(text2, box, requestedSize = 25, minFontSize = 16, measure = measureText, direction) {
5849
+ function fitText(text2, box, requestedSize = PROSE_NOMINAL_SIZE, minFontSize = 16, measure = measureText, direction, balance = false) {
5650
5850
  if (![box.width, box.height, requestedSize, minFontSize].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || requestedSize <= 0 || minFontSize <= 0) {
5651
5851
  throw new RangeError("Text dimensions and font sizes must be finite and positive.");
5652
5852
  }
5653
- return fitSourceText(text2, box, Math.max(requestedSize, minFontSize), minFontSize, 1, measure, true, true, direction);
5853
+ return fitSourceText(text2, box, Math.max(requestedSize, minFontSize), minFontSize, 1, measure, true, true, direction, balance);
5654
5854
  }
5655
5855
  var FONT_SIZE_GRID_PER_PX = 75;
5656
5856
  var FONT_GRID_EPSILON = 1e-6;
@@ -5675,11 +5875,11 @@ function fitAtSizes(layout, requested, minimum, step = 1, snap = true) {
5675
5875
  }
5676
5876
  var PHOTO_RATIO = 3;
5677
5877
  var PHOTO_GAP = 0.75;
5678
- function quoteBodyRuns(runs) {
5679
- if (!runs.length) return ['""'];
5878
+ function quoteBodyRuns(runs, marks2) {
5879
+ if (!runs.length) return [marks2.open + marks2.close];
5680
5880
  const last = runs.length - 1;
5681
5881
  return runs.map((run, index) => {
5682
- const before = index === 0 ? '"' : "", after = index === last ? '"' : "";
5882
+ const before = index === 0 ? marks2.open : "", after = index === last ? marks2.close : "";
5683
5883
  return typeof run === "string" ? before + run + after : { ...run, text: before + run.text + after };
5684
5884
  });
5685
5885
  }
@@ -5714,23 +5914,24 @@ function layoutQuote(value, box, options = {}) {
5714
5914
  for (const field of ["attribution", "role", "source"]) {
5715
5915
  const text2 = quote[field];
5716
5916
  if (!text2) continue;
5717
- if (footer) footer += field === "role" ? "\n" : " - ";
5917
+ if (footer) footer += field === "role" ? "\n" : QUOTE_SOURCE_SEPARATOR;
5718
5918
  footerSources.push(source(`${path}.${field}`, text2, footer.length));
5719
5919
  footer += text2;
5720
5920
  }
5721
5921
  const hasPhoto = photoValue !== void 0, hasFooter = footer !== "", hasBlock = hasPhoto || hasFooter;
5722
5922
  const photoPath = `${path}.photo`, rtl = options.direction === "rtl";
5723
5923
  const bodyPath = shorthand ? path : `${path}.text`;
5924
+ const marks2 = quoteMarksFor(options.language);
5724
5925
  const body = add(
5725
5926
  "body",
5726
- `"${quoteText}"`,
5727
- [source(bodyPath, quoteText, 1)],
5927
+ `${marks2.open}${quoteText}${marks2.close}`,
5928
+ [source(bodyPath, quoteText, marks2.open.length)],
5728
5929
  { x: box.x + 18, y: box.y + 18, width: box.width - 36, height: box.height - (hasBlock ? 94 : 36) },
5729
- 28,
5930
+ QUOTE_NOMINAL_SIZE,
5730
5931
  options.fontFamilies?.accent ?? options.fontFamilies?.heading ?? "sans-serif",
5731
5932
  600,
5732
5933
  bodyPath,
5733
- richBody ? quoteBodyRuns(richBody) : void 0
5934
+ richBody ? quoteBodyRuns(richBody, marks2) : void 0
5734
5935
  );
5735
5936
  const attribution = hasFooter ? add(
5736
5937
  "footer",
@@ -5852,7 +6053,7 @@ function layoutMetric(value, box, options = {}) {
5852
6053
  const sourceValue = metric[role];
5853
6054
  if (sourceValue === void 0) continue;
5854
6055
  const text2 = String(sourceValue), path = scalar ? sourcePath : `${sourcePath}.${role}`;
5855
- const nominal = role === "value" ? Math.min(76 * scale2, box.height * 0.28) : (role === "description" ? 20 : role === "trend" ? 18 : 23) * scale2;
6056
+ const nominal = (role === "value" ? 76 : role === "description" ? 20 : role === "trend" ? 18 : 23) * scale2;
5856
6057
  const requestedStyle = { fontFamily: (role === "value" ? options.fontFamilies?.heading : options.fontFamilies?.body) ?? "sans-serif", fontWeight: role === "value" ? 800 : role === "description" ? 400 : 500, italic: false, path };
5857
6058
  const part = {
5858
6059
  role,
@@ -6167,9 +6368,9 @@ function layoutTimeline(value, box, options = {}) {
6167
6368
  function fitCodeText(text2, box, size, minimum, step, measure, direction) {
6168
6369
  return fitSourceText(text2, box, size, minimum, step, measure, false, true, direction);
6169
6370
  }
6170
- function fitSourceText(text2, box, size, minimum, step, measure, prose = false, snap = true, direction) {
6371
+ function fitSourceText(text2, box, size, minimum, step, measure, prose = false, snap = true, direction, balance = false) {
6171
6372
  const directionAt = direction === "rtl" ? paragraphDirectionAt(text2, "rtl") : void 0;
6172
- const layout = (fontSize) => {
6373
+ const layout = (fontSize, forced) => {
6173
6374
  const tabWidth = measure(" ", fontSize) * 4;
6174
6375
  if (!Number.isFinite(tabWidth) || text2.includes(" ") && tabWidth <= 0) throw new RangeError("Text tabs require a positive finite measured space advance.");
6175
6376
  const measureLine = (start2, end2, record8 = false) => {
@@ -6210,6 +6411,7 @@ function fitSourceText(text2, box, size, minimum, step, measure, prose = false,
6210
6411
  push(a, b, "hard");
6211
6412
  continue;
6212
6413
  }
6414
+ if (forced?.has(a) && end > start && /\S/u.test(text2.slice(start, end))) push(a, a, "soft");
6213
6415
  if (width(start, b) <= box.width + WRAP_TOLERANCE) {
6214
6416
  end = b;
6215
6417
  continue;
@@ -6247,7 +6449,22 @@ function fitSourceText(text2, box, size, minimum, step, measure, prose = false,
6247
6449
  overflow: sourceLines.length * lineHeight > box.height + 0.01 || sourceLines.some((line2) => line2.width > box.width + 0.01)
6248
6450
  };
6249
6451
  };
6250
- return fitAtSizes(layout, size, minimum, step, snap);
6452
+ const balanced = (fontSize) => {
6453
+ const greedy = layout(fontSize);
6454
+ if (!balance || greedy.overflow || greedy.sourceLines.length < 2) return greedy;
6455
+ const widthOf = (start, end) => {
6456
+ let x = 0;
6457
+ for (const [index, segment2] of text2.slice(start, end).split(" ").entries()) {
6458
+ if (index) x = (Math.floor(x / (measure(" ", fontSize) * 4) + 1e-9) + 1) * measure(" ", fontSize) * 4;
6459
+ if (segment2) x += measure(segment2, fontSize);
6460
+ }
6461
+ return x;
6462
+ };
6463
+ const forced = balancedBreaks(text2, greedy.sourceLines.map((line2) => ({ start: line2.start, end: line2.end, hard: line2.boundary !== "soft" })), widthOf, box.width);
6464
+ const result = forced ? layout(fontSize, forced) : greedy;
6465
+ return result.sourceLines.length === greedy.sourceLines.length && !result.overflow ? result : greedy;
6466
+ };
6467
+ return fitAtSizes(balance ? balanced : layout, size, minimum, step, snap);
6251
6468
  }
6252
6469
  function layoutCode(value, box, options = {}) {
6253
6470
  const shorthand = typeof value === "string", code = shorthand ? { source: value } : value;
@@ -6270,7 +6487,7 @@ function layoutCode(value, box, options = {}) {
6270
6487
  sources: generated ? [] : [{ path: partPath, start: 0, end: text2.length }],
6271
6488
  generated,
6272
6489
  box: { ...inner },
6273
- requestedFontSize: gridFontSize((role === "body" ? 18 : 14) * scale2, minimum),
6490
+ requestedFontSize: gridFontSize((role === "body" ? CODE_NOMINAL_SIZE : 14) * scale2, minimum),
6274
6491
  minFontSize: minimum,
6275
6492
  requestedStyle,
6276
6493
  style: resolveTextStyle({ ...requestedStyle }, options.textMeasurement)
@@ -6283,7 +6500,16 @@ function layoutCode(value, box, options = {}) {
6283
6500
  if (!parts.length) add("language", "code", path, true);
6284
6501
  const body = add("body", code.source, shorthand ? path : `${path}.source`), headers = parts.filter((part) => part !== body);
6285
6502
  const usable2 = (area2) => [area2.x, area2.y, area2.width, area2.height].every(Number.isFinite) && area2.width > 0 && area2.height > 0;
6286
- const fit = (part, area2, size = part.requestedFontSize, floor = minimum) => usable2(area2) ? fitCodeText(part.text, area2, size, floor, scale2, textWidthMeasurer(part.style, options.textMeasurement)) : void 0;
6503
+ const measureCode = textWidthMeasurer(body.style, options.textMeasurement);
6504
+ const fit = (part, area2, size = part.requestedFontSize, floor = minimum) => {
6505
+ if (!usable2(area2)) return void 0;
6506
+ const wanted = fitCodeText(part.text, area2, size, floor, scale2, textWidthMeasurer(part.style, options.textMeasurement));
6507
+ const wraps = (value2) => value2.sourceLines.filter((line2) => line2.boundary === "soft").length;
6508
+ const legacy = gridFontSize(LEGACY_CODE_NOMINAL_SIZE * scale2, minimum);
6509
+ if (part.role !== "body" || part.requestedFontSize <= legacy || !wraps(wanted)) return wanted;
6510
+ const earlier = fitCodeText(part.text, area2, legacy, floor, scale2, measureCode);
6511
+ return !earlier.overflow && wraps(earlier) < wraps(wanted) ? earlier : wanted;
6512
+ };
6287
6513
  if (usable2(inner)) {
6288
6514
  const combinations = headers.reduce((choices, part) => choices.flatMap((choice) => [.../* @__PURE__ */ new Set([part.requestedFontSize, minimum])].map((size) => [...choice, { part, size }])), [[]]);
6289
6515
  let selected;
@@ -6306,6 +6532,7 @@ function layoutCode(value, box, options = {}) {
6306
6532
  part.box = box2;
6307
6533
  part.fit = fit2;
6308
6534
  }
6535
+ if (selected && !selected.overflow && body.fit) body.box = { ...body.box, height: Math.min(body.box.height, body.fit.lines.length * body.fit.lineHeight) };
6309
6536
  }
6310
6537
  const report = (reason, part, roles, message) => diagnostics.push({ code: "text-overflow", reason, path: part.path, parts: roles, message });
6311
6538
  for (const part of parts) {
@@ -6323,9 +6550,11 @@ function layoutCode(value, box, options = {}) {
6323
6550
  }
6324
6551
  }
6325
6552
  if (diagnostics.length && options.overflow === "error") throw new OPFCompositionError(diagnostics);
6326
- return { algorithm: "code-flow-v1", textMeasurement: options.textMeasurement ? "provided" : "estimated", parts, diagnostics, overflow: diagnostics.length > 0 };
6553
+ const bottom = body.fit && !body.fit.overflow ? body.box.y + body.box.height + 18 : box.y + box.height;
6554
+ const bounds = { x: box.x, y: box.y, width: box.width, height: Math.min(box.height, Math.max(bottom - box.y, 0)) };
6555
+ return { bounds, algorithm: "code-flow-v1", textMeasurement: options.textMeasurement ? "provided" : "estimated", parts, diagnostics, overflow: diagnostics.length > 0 };
6327
6556
  }
6328
- function fitRichText(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
6557
+ function fitRichText(input, box, requestedSize = PROSE_NOMINAL_SIZE, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
6329
6558
  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.");
6330
6559
  const layout = richTextLayouter(input, box, requestedSize, options, minFontSize);
6331
6560
  return fitAtSizes(layout, requestedSize, richMinimum(input, requestedSize, minFontSize));
@@ -6342,7 +6571,7 @@ function richMinimum(input, requested, minimum) {
6342
6571
  return visible ? floor : minimum;
6343
6572
  }
6344
6573
  function richTextLayouter(input, box, requestedSize, options, minimum = 0) {
6345
- let offset = 0;
6574
+ let offset = 0, lastWidth = () => 0, lastRanges = [];
6346
6575
  const source = input.map((value, runIndex) => {
6347
6576
  const run = typeof value === "string" ? { text: value } : value;
6348
6577
  if (typeof run?.text !== "string" || run.fontSize !== void 0 && (!Number.isFinite(run.fontSize) || run.fontSize <= 0)) throw new RangeError("Rich text runs need text and a positive finite font size.");
@@ -6356,7 +6585,7 @@ function richTextLayouter(input, box, requestedSize, options, minimum = 0) {
6356
6585
  const whole = source.map((entry) => entry.run.text).join("");
6357
6586
  const paint = (raw) => raw >= minimum - 1e-6 ? gridFontSize(raw, minimum) : snapFontSizeDown(raw);
6358
6587
  const directionAt = options.direction === "rtl" ? paragraphDirectionAt(whole, "rtl") : void 0;
6359
- const layout = (rawFontSize) => {
6588
+ const build = (rawFontSize, forced) => {
6360
6589
  const fontSize = paint(rawFontSize), ratio = rawFontSize / requestedSize;
6361
6590
  const fragments = (start, end) => {
6362
6591
  let x = 0;
@@ -6411,6 +6640,7 @@ function richTextLayouter(input, box, requestedSize, options, minimum = 0) {
6411
6640
  lineEnd = end;
6412
6641
  continue;
6413
6642
  }
6643
+ if (forced?.has(start) && lineEnd > lineStart && /\S/u.test(whole.slice(lineStart, lineEnd))) push(start);
6414
6644
  if (width(lineStart, end) <= box.width + WRAP_TOLERANCE) {
6415
6645
  lineEnd = end;
6416
6646
  continue;
@@ -6454,7 +6684,7 @@ function richTextLayouter(input, box, requestedSize, options, minimum = 0) {
6454
6684
  y += height;
6455
6685
  }
6456
6686
  }
6457
- return {
6687
+ const fit = {
6458
6688
  lines: ranges.map((range) => whole.slice(range.start, range.end)),
6459
6689
  fontSize,
6460
6690
  lineHeight: Math.max(fontSize * 1.22, ...richLines.map((line2) => line2.height)),
@@ -6463,12 +6693,22 @@ function richTextLayouter(input, box, requestedSize, options, minimum = 0) {
6463
6693
  overflow: y > box.height + 0.01 || richLines.some((line2) => line2.width > box.width + 0.01),
6464
6694
  ...directionAt ? { directions: ranges.map((range) => directionAt(range.start)) } : {}
6465
6695
  };
6696
+ lastWidth = width;
6697
+ lastRanges = ranges;
6698
+ return fit;
6699
+ };
6700
+ const layout = (rawFontSize) => {
6701
+ const greedy = build(rawFontSize);
6702
+ if (!options.balance || greedy.overflow || greedy.richLines.length < 2) return greedy;
6703
+ const forced = balancedBreaks(whole, lastRanges.map((range) => ({ start: range.start, end: range.end, hard: /^[\r\n]/.test(whole.slice(range.end, range.end + 1)) || range.end >= whole.length })), lastWidth, box.width);
6704
+ const result = forced ? build(rawFontSize, forced) : greedy;
6705
+ return result.richLines.length === greedy.richLines.length && !result.overflow ? result : greedy;
6466
6706
  };
6467
6707
  return layout;
6468
6708
  }
6469
6709
  var PICTURE_BULLET_SCALE = 0.65;
6470
6710
  function contentNominalSize(field) {
6471
- return field === "items" || field === "bullets" ? 32 : 25;
6711
+ return field === "items" || field === "bullets" ? LIST_NOMINAL_SIZE : field === "table" ? TABLE_NOMINAL_SIZE : PROSE_NOMINAL_SIZE;
6472
6712
  }
6473
6713
  function fitList(input, box, requestedSize = contentNominalSize("items"), minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
6474
6714
  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.");
@@ -6493,10 +6733,11 @@ function fitList(input, box, requestedSize = contentNominalSize("items"), minFon
6493
6733
  const entries = [], lines = [], directions = [];
6494
6734
  const markerStyles = numbers ? source.map((item) => resolveTextStyle(markerStyleFor(item), options.textMeasurement)) : [];
6495
6735
  const markerWidths = numbers ? numbers.map((number, index) => textWidthMeasurer(markerStyles[index], options.textMeasurement)(number.text, fontSize)) : [];
6736
+ const none = options.bullet === "none" && !numbers;
6496
6737
  const hanging = numbers ? Math.max(fontSize * 1.1, Math.max(0, ...markerWidths) + fontSize * 0.3) : fontSize * 1.1;
6497
6738
  for (const item of source) {
6498
6739
  const rtl = item.direction === "rtl";
6499
- const indent = hanging, offset = Math.min(box.width, item.level * indent), x = rtl ? box.x : box.x + offset + indent, width = Math.max(1, rtl ? box.width - offset - indent : box.x + box.width - x);
6740
+ const indent = none ? 0 : hanging, offset = Math.min(box.width, item.level * hanging), x = rtl ? box.x : box.x + offset + indent, width = Math.max(1, rtl ? box.width - offset - indent : box.x + box.width - x);
6500
6741
  if (offset + indent >= box.width) overflow = true;
6501
6742
  const textBox = { x, y, width, height: box.height };
6502
6743
  const style = { ...options.style, path: item.textPath };
@@ -6517,7 +6758,7 @@ function fitList(input, box, requestedSize = contentNominalSize("items"), minFon
6517
6758
  }
6518
6759
  overflow ||= text2.overflow || !!description?.overflow;
6519
6760
  const number = numbers?.[item.index];
6520
- const marker = { text: number ? number.text : ["\u2022", "\u25E6", "\u25AA"][item.level % 3], x: rtl ? box.x + box.width - offset : box.x + offset, y: textBox.y + (text2.richLines[0]?.baseline ?? fontSize), fontSize, style: number ? markerStyles[item.index] : resolveTextStyle(style, options.textMeasurement), indent, ...number ? { number, width: markerWidths[item.index] } : {}, ...rtl ? { anchor: "end" } : {} }, side = fontSize * PICTURE_BULLET_SCALE;
6761
+ const marker = { text: number ? number.text : none ? "" : ["\u2022", "\u25E6", "\u25AA"][item.level % 3], x: rtl ? box.x + box.width - offset : box.x + offset, y: textBox.y + (text2.richLines[0]?.baseline ?? fontSize), fontSize, style: number ? markerStyles[item.index] : resolveTextStyle(style, options.textMeasurement), indent, ...number ? { number, width: markerWidths[item.index] } : {}, ...none ? { none: true } : {}, ...rtl ? { anchor: "end" } : {} }, side = fontSize * PICTURE_BULLET_SCALE;
6521
6762
  entries.push({
6522
6763
  index: item.index,
6523
6764
  level: item.level,
@@ -6531,7 +6772,7 @@ function fitList(input, box, requestedSize = contentNominalSize("items"), minFon
6531
6772
  descriptionBox,
6532
6773
  marker,
6533
6774
  ...item.direction ? { direction: item.direction } : {},
6534
- ...options.bulletImage && !number ? { bulletImage: options.bulletImage, bulletBox: { x: rtl ? marker.x - side : marker.x, y: marker.y - side, width: side, height: side } } : {}
6775
+ ...options.bulletImage && !number && !none ? { bulletImage: options.bulletImage, bulletBox: { x: rtl ? marker.x - side : marker.x, y: marker.y - side, width: side, height: side } } : {}
6535
6776
  });
6536
6777
  if (item.index < source.length - 1) y += fontSize * 0.28;
6537
6778
  }
@@ -6798,14 +7039,15 @@ function composeSlide(input, options = {}) {
6798
7039
  const minSize = snapFontSizeUp((composition.minFontSize ?? 16) * scale2);
6799
7040
  const rasterPadding = (options.textRasterPadding ?? 1) * scale2;
6800
7041
  if (!Number.isFinite(rasterPadding) || rasterPadding < 0) throw new RangeError("Text raster padding must be finite and nonnegative.");
6801
- let coverGroup = false;
6802
- const alignmentFor = (field) => (field === "title" || (field === "tag" || coverGroup && field === "subtitle") && hints.sources.contentAlignment !== "slide" ? hints.titleAlignment : hints.contentAlignment) ?? (field === "metric" ? "center" : "left");
7042
+ let subtitleInGroup = true;
7043
+ const alignmentFor = (field) => (field === "title" || (field === "tag" || subtitleInGroup && field === "subtitle") && hints.sources.contentAlignment !== "slide" ? hints.titleAlignment : hints.contentAlignment) ?? (field === "metric" ? "center" : "left");
6803
7044
  const styleFor = (field, path2) => resolveTextStyle({
6804
7045
  fontFamily: (field === "title" ? options.fontFamilies?.heading : field === "code" ? options.fontFamilies?.code : field === "tag" ? options.fontFamilies?.accent ?? options.fontFamilies?.body : options.fontFamilies?.body) ?? (field === "code" ? "monospace" : "sans-serif"),
6805
7046
  fontWeight: field === "title" ? 700 : 400,
6806
7047
  path: path2
6807
7048
  }, options.textMeasurement);
6808
7049
  const listBullet = hints.listBullet !== void 0 ? { value: hints.listBullet, path: hints.paths.listBullet } : void 0;
7050
+ const noListBullet = listBullet?.value === "none";
6809
7051
  const bulletImage = listBullet?.value === "image" ? (() => {
6810
7052
  const resolved = resolveLogo(options.presentation, slide, { shape: "icon", onDark: options.darkBackground, slideIndex: options.slideIndex });
6811
7053
  return resolved ? { source: resolved.source, path: resolved.path } : void 0;
@@ -6816,13 +7058,14 @@ function composeSlide(input, options = {}) {
6816
7058
  const richOptions = (style) => ({ style, textMeasurement: options.textMeasurement, ...options.fontFamilies?.code ? { codeFontFamily: options.fontFamilies.code } : {}, ...citationMarker ? { citationMarker } : {}, ...rtl ? { direction: "rtl" } : {} });
6817
7059
  const fitPlacedText = (field, value, text2, box, size, minimum, path2, explicitAlignment) => {
6818
7060
  const style = styleFor(field, path2), rich = (field === "text" || headings.has(field)) && Array.isArray(value);
6819
- if (!options.textMeasurement?.outlineBounds) return rich ? fitRichText(value, box, size, minimum, richOptions(style)) : fitText(text2, box, size, minimum, textWidthMeasurer(style, options.textMeasurement), textDirection);
7061
+ const balance = field === "title" || field === "subtitle", textOptions = balance ? { ...richOptions(style), balance } : richOptions(style);
7062
+ if (!options.textMeasurement?.outlineBounds) return rich ? fitRichText(value, box, size, minimum, textOptions) : fitText(text2, box, size, minimum, textWidthMeasurer(style, options.textMeasurement), textDirection, balance);
6820
7063
  const alignment = explicitAlignment ?? alignmentFor(field);
6821
- const richLayout = rich ? richTextLayouter(value, box, size, richOptions(style), minimum) : void 0;
7064
+ const richLayout = rich ? richTextLayouter(value, box, size, textOptions, minimum) : void 0;
6822
7065
  const measure = textWidthMeasurer(style, options.textMeasurement), floor = snapFontSizeUp(rich ? richMinimum(value, size, minimum) : minimum), start = Math.max(size, floor);
6823
7066
  for (let trial = 0; trial <= 64; trial++) {
6824
7067
  const raw = start - trial * scale2, last = trial === 64 || raw <= floor, fontSize = last ? floor : gridFontSize(raw, floor);
6825
- const fit = richLayout ? richLayout(fontSize) : fitText(text2, box, fontSize, fontSize, measure, textDirection);
7068
+ const fit = richLayout ? richLayout(fontSize) : fitText(text2, box, fontSize, fontSize, measure, textDirection, balance);
6826
7069
  const richLines = "richLines" in fit ? fit.richLines : void 0;
6827
7070
  const lines = richLines ? richLines.map((line2) => {
6828
7071
  let outline = null;
@@ -6864,7 +7107,7 @@ function composeSlide(input, options = {}) {
6864
7107
  }
6865
7108
  throw new Error("Text placement did not evaluate its bounded floor trial.");
6866
7109
  };
6867
- const fitContent = (field, value, text2, box, size, minimum, path2, numbering) => field === "text" ? fitPlacedText(field, value, text2, box, size, minimum, path2) : field === "items" || field === "bullets" ? fitList(value, box, size, minimum, { ...richOptions(styleFor(field, path2)), ...numbering !== void 0 ? { numbering } : bulletImage ? { bulletImage } : {} }) : fitText(text2, box, size, minimum, widthFor(field, path2), field === "code" ? void 0 : textDirection);
7110
+ const fitContent = (field, value, text2, box, size, minimum, path2, numbering) => field === "text" ? fitPlacedText(field, value, text2, box, size, minimum, path2) : field === "items" || field === "bullets" ? fitList(value, box, size, minimum, { ...richOptions(styleFor(field, path2)), ...numbering !== void 0 ? { numbering } : bulletImage ? { bulletImage } : noListBullet ? { bullet: "none" } : {} }) : fitText(text2, box, size, minimum, widthFor(field, path2), field === "code" ? void 0 : textDirection);
6868
7111
  const path = `slides.${options.slideIndex ?? 0}`;
6869
7112
  const annotationFit = (value, box, requestedSize, minFontSize, fitPath, alignment) => fitPlacedText("text", value, annotationText(value), box, requestedSize, minFontSize, fitPath, alignment);
6870
7113
  const annotationOptions = { scale: scale2, minFontSize: minSize, fit: annotationFit, textStyle: (fitPath) => styleFor("text", fitPath) };
@@ -6873,7 +7116,7 @@ function composeSlide(input, options = {}) {
6873
7116
  const backgroundImage = resolveBackgroundImage(slide, options.presentation, options.themeBackground, width, height, path, mirrorSide, imageDiagnostics);
6874
7117
  const regions = Object.keys(slide).filter((key) => regionParts(key)).sort();
6875
7118
  const assets = record7(options.presentation).assets;
6876
- const imageContext = (placement) => ({ fit: hints.imageFit, scale: scale2, mirrorSide, diagnostics: imageDiagnostics, assets, ...placement ? { placement } : {} });
7119
+ const imageContext = (placement, anchor) => ({ fit: hints.imageFit ?? (!placement && autoComposition() ? "contain" : void 0), scale: scale2, mirrorSide, diagnostics: imageDiagnostics, assets, ...placement ? { placement } : {}, ...anchor ? { anchor } : {} });
6877
7120
  const area2 = { left: 0, top: 0, right: width, bottom: height };
6878
7121
  const topImages = regions.length ? [] : Array.isArray(slide.blocks) ? slide.blocks.flatMap((block, index) => !Array.isArray(record7(block).blocks) && record7(block).image !== void 0 ? [{ host: record7(block), hostPath: `${path}.blocks.${index}`, block: index }] : []) : slide.image !== void 0 ? [{ host: { type: "image", image: slide.image }, hostPath: path }] : [];
6879
7122
  const placed = [], usedEdges = /* @__PURE__ */ new Set();
@@ -6915,8 +7158,9 @@ function composeSlide(input, options = {}) {
6915
7158
  const flowBlocks = Array.isArray(slide.blocks) ? slide.blocks.filter((_, index) => !placedBlocks.has(index)) : slide.blocks;
6916
7159
  const hasBodyPayload = regions.some((key) => !emptyHost(record7(slide[key]))) || !emptyPayload(flowBlocks) || fields.some((field) => !(placedRoot && field === "image") && !emptyPayload(slide[field]));
6917
7160
  const useTemplate = template !== void 0 && !regions.length;
7161
+ const autoComposition = () => !hasLayoutRecord && !regions.length;
6918
7162
  const isCover = !hasBodyPayload && (useTemplate || !hasLayoutRecord);
6919
- coverGroup = isCover;
7163
+ subtitleInGroup = !(useTemplate && template?.subtitle === true);
6920
7164
  let logo;
6921
7165
  if (isCover && options[LAYOUT_MASTER] !== true) {
6922
7166
  const resolved = resolveLogo(options.presentation, slide, { shape: "full", onDark: options.darkBackground, slideIndex: options.slideIndex });
@@ -6931,7 +7175,7 @@ function composeSlide(input, options = {}) {
6931
7175
  const headingItems = [];
6932
7176
  for (const field of useTemplate ? [] : ["tag", "title", "subtitle"]) {
6933
7177
  if (!slide[field] || Array.isArray(slide[field]) && !annotationText(slide[field])) continue;
6934
- const requested = (field === "title" ? 54 : field === "tag" ? 16 : 25) * scale2;
7178
+ const requested = (field === "title" ? 54 : field === "tag" ? 16 : PROSE_NOMINAL_SIZE) * scale2;
6935
7179
  const maxHeight = Math.max(height * (field === "title" ? 0.26 : field === "subtitle" ? 0.12 : 0.045), minSize * 1.22 + 2 * rasterPadding);
6936
7180
  const box = { x: area2.left + padding, y, width: area2.right - area2.left - padding * 2, height: maxHeight };
6937
7181
  const text2 = fitPlacedText(field, slide[field], annotationText(slide[field]), box, requested, minSize, `${path}.${field}`);
@@ -7008,6 +7252,8 @@ function composeSlide(input, options = {}) {
7008
7252
  const regionsInReadingOrder = visualReadingOrder(regions.map((key) => ({ key, box: regionBox(regionParts(key), contentBox, gap) }))).map((entry) => entry.key);
7009
7253
  const pending = regions.length ? regionsInReadingOrder.flatMap((key) => collect(record7(slide[key]), `${path}.${key}`).map((item) => ({ ...item, region: regionParts(key) }))) : Array.isArray(slide.blocks) ? slide.blocks.flatMap((block, index) => placedBlocks.has(index) ? [] : collect(record7(block), `${path}.blocks.${index}`)) : collect(placedRoot ? { ...slide, image: void 0 } : slide, path);
7010
7254
  const groups = [], flows = [];
7255
+ const peerSets = [];
7256
+ const range = (from, to) => Array.from({ length: to - from }, (_, index) => from + index);
7011
7257
  const decisions = options.explain ? [] : void 0;
7012
7258
  const inheritedSettings = (parent, own3 = {}) => ({
7013
7259
  minFontSize: parent.minFontSize,
@@ -7024,6 +7270,11 @@ function composeSlide(input, options = {}) {
7024
7270
  const frame = acceptedBox(box), padding2 = Math.min(12 * scale2, frame.width / 4, frame.height / 4);
7025
7271
  return acceptedBox({ x: frame.x + padding2, y: frame.y + padding2, width: frame.width - 2 * padding2, height: frame.height - 2 * padding2 });
7026
7272
  };
7273
+ let languageTag = null;
7274
+ const deckLanguage = () => {
7275
+ if (languageTag === null) languageTag = quoteLanguage(options.presentation, options.slideIndex);
7276
+ return languageTag;
7277
+ };
7027
7278
  const measureQuote = (node, box, settings) => layoutQuote(node.value, acceptedBox(box), {
7028
7279
  fontFamilies: options.fontFamilies,
7029
7280
  textMeasurement: options.textMeasurement,
@@ -7031,7 +7282,8 @@ function composeSlide(input, options = {}) {
7031
7282
  minFontSize: settings.minFontSize,
7032
7283
  path: node.path,
7033
7284
  direction: textDirection,
7034
- ...citationMarker ? { citationMarker } : {}
7285
+ ...citationMarker ? { citationMarker } : {},
7286
+ ...deckLanguage() !== void 0 ? { language: deckLanguage() } : {}
7035
7287
  });
7036
7288
  const measureCode = (node, box, settings) => layoutCode(node.value, acceptedBox(box), {
7037
7289
  fontFamilies: options.fontFamilies,
@@ -7060,14 +7312,21 @@ function composeSlide(input, options = {}) {
7060
7312
  direction: textDirection
7061
7313
  });
7062
7314
  const captionOf = (node, box) => node.caption !== void 0 && node.captionPath ? layoutCaption(node.caption, box, node.captionPath, annotationOptions) : void 0;
7315
+ const wholePictureAspect = (node) => {
7316
+ const host = node.host;
7317
+ if (node.field !== "image" || !host || !autoComposition() || host.aspectRatio !== void 0 || host.shape === "circle") return void 0;
7318
+ const fit = IMAGE_FITS.includes(host.fit) ? host.fit : hints.imageFit ?? "contain";
7319
+ return fit === "contain" ? intrinsicImageAspect(host.image, assets) : void 0;
7320
+ };
7063
7321
  const leafScore = (node, box, settings, penalties) => {
7064
7322
  box = payloadBox(box);
7065
7323
  const captioned = captionOf(node, box);
7066
7324
  if (captioned) box = captioned.mediaBox;
7067
7325
  const text2 = contentText(node.field, node.value);
7068
- let score = Math.abs(Math.log(box.width / box.height / 1.6)) + (captioned?.overflow ? 1e3 : 0);
7326
+ const proportions = Math.abs(Math.log(box.width / box.height / (wholePictureAspect(node) ?? 1.6)));
7327
+ let score = proportions + (captioned?.overflow ? 1e3 : 0);
7069
7328
  if (penalties) {
7070
- penalties.cellProportions += Math.abs(Math.log(box.width / box.height / 1.6));
7329
+ penalties.cellProportions += proportions;
7071
7330
  if (captioned?.overflow) penalties.textOverflow += 1e3;
7072
7331
  }
7073
7332
  if (node.field === "quote" || node.field === "code" || node.field === "metric" || node.field === "timeline") {
@@ -7079,8 +7338,9 @@ function composeSlide(input, options = {}) {
7079
7338
  penalties.textOverflow += internal.overflow ? 1e3 : 0;
7080
7339
  }
7081
7340
  } else if (text2) {
7082
- const fit = fitContent(node.field, node.value, text2, box, contentNominalSize(node.field) * scale2, snapFontSizeUp((settings.minFontSize ?? 16) * scale2), node.path, node.payload.numbering);
7083
- const reduction = Math.max(0, snapFontSizeDown(contentNominalSize(node.field) * scale2) - fit.fontSize) / scale2;
7341
+ const prose = proseMeasure(node, box.width);
7342
+ const fit = fitContent(node.field, node.value, text2, { ...box, width: prose.width }, prose.size, snapFontSizeUp((settings.minFontSize ?? 16) * scale2), node.path, node.payload.numbering);
7343
+ const reduction = Math.max(0, snapFontSizeDown(prose.size) - fit.fontSize) / scale2;
7084
7344
  score += reduction + (fit.overflow ? 1e3 : 0);
7085
7345
  if (penalties) {
7086
7346
  penalties.fontReduction += reduction;
@@ -7109,6 +7369,74 @@ function composeSlide(input, options = {}) {
7109
7369
  const boxes = gridBoxes(slotCount, area3, cols, (own3.gap ?? 1 / 30) * Math.min(box.width, box.height), own3.weights ?? [], modeFor(own3) === "column");
7110
7370
  return node.children.reduce((score, child, index) => score + scoreNode(child, boxes[index], own3, penalties), 0);
7111
7371
  };
7372
+ const contentSized = /* @__PURE__ */ new WeakSet();
7373
+ const stackedBoxes = (nodes, area3, requestedGap, settings, cards = hasCards, anchor = "top") => {
7374
+ const between = Math.max(0, Math.min(requestedGap, area3.width / 2, area3.height / (nodes.length * 2)));
7375
+ const space = Math.max(0, area3.height - between * (nodes.length - 1)), share = space / nodes.length;
7376
+ const elastic = nodes.map((node) => !measuredNode(node)), stretch = elastic.filter(Boolean).length;
7377
+ const needs = nodes.map((node, index) => elastic[index] ? share : naturalHeight(node, area3.width, cards, void 0, settings));
7378
+ const floors = nodes.map((node, index) => elastic[index] ? share : naturalHeight(node, area3.width, cards, void 0, settings, true));
7379
+ let heights = shareHeights(needs, floors, space);
7380
+ const textual = nodes.every((node) => !node.children && node.caption === void 0 && (node.field === "text" || node.field === "items" || node.field === "bullets"));
7381
+ if (textual && nodes.length > 1 && needs.reduce((sum, size) => sum + size, 0) > space + 1e-6) {
7382
+ const floor = snapFontSizeUp((settings.minFontSize ?? 16) * scale2), start = Math.max(...nodes.map((node) => proseMeasure(node, area3.width).size));
7383
+ for (let size = start; ; size -= scale2) {
7384
+ const trial = Math.max(floor, snapFontSizeDown(size));
7385
+ const natural = nodes.map((node) => naturalHeight(node, area3.width, cards, void 0, settings, false, Math.min(trial, proseMeasure(node, area3.width).size)));
7386
+ if (natural.reduce((sum, height2) => sum + height2, 0) <= space + 1e-6) {
7387
+ heights = natural;
7388
+ break;
7389
+ }
7390
+ if (trial <= floor) break;
7391
+ }
7392
+ }
7393
+ if (stretch && heights.some((size, index) => elastic[index] && size < share - 1e-9)) {
7394
+ const measured = nodes.flatMap((_, index) => elastic[index] ? [] : [index]);
7395
+ const rest = shareHeights(measured.map((index) => needs[index]), measured.map((index) => floors[index]), space - stretch * share);
7396
+ heights = nodes.map((_, index) => elastic[index] ? share : rest[measured.indexOf(index)]);
7397
+ }
7398
+ const spare = space - heights.reduce((sum, size) => sum + size, 0);
7399
+ if (stretch && spare > 0) heights = heights.map((size, index) => elastic[index] ? size + spare / stretch : size);
7400
+ const used = heights.reduce((sum, size) => sum + size, 0) + between * (nodes.length - 1);
7401
+ nodes.forEach((node, index) => {
7402
+ if (!node.children && !elastic[index] && heights[index] >= needs[index] - 1e-6) contentSized.add(node);
7403
+ });
7404
+ let y2 = area3.y + Math.max(0, anchor === "middle" ? (area3.height - used) / 2 : anchor === "bottom" ? area3.height - used : 0);
7405
+ return heights.map((size) => {
7406
+ const box = { x: area3.x, y: y2, width: area3.width, height: size };
7407
+ y2 += size + between;
7408
+ return box;
7409
+ });
7410
+ };
7411
+ const WEIGHTED_DEMAND_RATIO = 2.5, WEIGHTED_SHARES = [0.55, 0.6, 0.65, 0.7];
7412
+ const autoRowWeights = (nodes, count, cols, area3, rowGap, settings) => {
7413
+ if (!autoComposition() || settings.weights?.length || count < 2 || count > 3 || cols !== count || nodes.length !== count) return void 0;
7414
+ const equal = gridGeometry(count, area3, cols, rowGap, [], false).boxes;
7415
+ const wholeWidths = nodes.map((node, index) => {
7416
+ const aspect = node.caption === void 0 ? wholePictureAspect(node) : void 0;
7417
+ return aspect === void 0 ? void 0 : Math.min(equal[index].width, equal[index].height * aspect);
7418
+ });
7419
+ const media = wholeWidths.flatMap((width2, index) => width2 !== void 0 && width2 < equal[index].width - scale2 ? [index] : []);
7420
+ const others = nodes.flatMap((_, index) => wholeWidths[index] === void 0 ? [index] : []);
7421
+ if (media.length && others.length) {
7422
+ const freed = media.reduce((sum, index) => sum + equal[index].width - wholeWidths[index], 0);
7423
+ return nodes.map((_, index) => media.includes(index) ? wholeWidths[index] : equal[index].width + freed / others.length);
7424
+ }
7425
+ const prose = (node) => !node.children && node.caption === void 0 && (node.field === "text" || node.field === "items" || node.field === "bullets");
7426
+ if (count !== 2 || !nodes.every(prose)) return void 0;
7427
+ const need = (node, width2) => naturalHeight(node, width2, hasCards, void 0, settings);
7428
+ const heights = nodes.map((node, index) => need(node, equal[index].width));
7429
+ const long = heights[0] >= heights[1] ? 0 : 1, short = 1 - long;
7430
+ if (heights[long] < WEIGHTED_DEMAND_RATIO * heights[short]) return void 0;
7431
+ const total = equal[0].width + equal[1].width, line2 = contentNominalSize(nodes[short].field) * scale2 * 1.3;
7432
+ let best;
7433
+ for (const share of WEIGHTED_SHARES) {
7434
+ const longHeight = need(nodes[long], total * share), shortHeight = need(nodes[short], total * (1 - share));
7435
+ if (shortHeight > heights[short] + line2 || longHeight > heights[long] - line2 * 0.5) continue;
7436
+ if (!best || longHeight < best.height - 1e-6) best = { share, height: longHeight };
7437
+ }
7438
+ return best ? long === 0 ? [best.share, 1 - best.share] : [1 - best.share, best.share] : void 0;
7439
+ };
7112
7440
  const arrange = (nodes, area3, settings, reserved2 = 0, gapOverride, containerPath = path, recorded = true) => {
7113
7441
  const count = Math.max(nodes.length, reserved2);
7114
7442
  if (!count) return;
@@ -7116,7 +7444,12 @@ function composeSlide(input, options = {}) {
7116
7444
  const actualGap = gapOverride ?? (settings === rootSettings ? gap : localGap);
7117
7445
  let cols = defaultColumns(count, area3, settings);
7118
7446
  const hasRegions = nodes.some((node) => node.region);
7447
+ if (!hasRegions && (mode === "auto" || nodes.length === 1)) for (const node of nodes) {
7448
+ node.auto = true;
7449
+ node.lone = nodes.length === 1 && reserved2 === 0;
7450
+ }
7119
7451
  const candidates = decisions && recorded ? [] : void 0;
7452
+ const stacks = !hasRegions && count === nodes.length && count > 1 && !(mode === "column" && settings.weights?.length);
7120
7453
  if (mode === "auto" && !hasRegions) {
7121
7454
  let best = Infinity;
7122
7455
  for (let candidate = 1; candidate <= Math.min(count, settings.columns ?? 6); candidate++) {
@@ -7138,14 +7471,24 @@ function composeSlide(input, options = {}) {
7138
7471
  ...hasRegions ? {} : { selectedColumns: cols },
7139
7472
  candidates: candidates ?? []
7140
7473
  });
7141
- const grid = gridGeometry(count, area3, cols, actualGap, settings.weights ?? [], mode === "column");
7474
+ const grid = gridGeometry(count, area3, cols, actualGap, (mode === "auto" && !hasRegions ? autoRowWeights(nodes, count, cols, area3, actualGap, settings) : void 0) ?? settings.weights ?? [], mode === "column");
7475
+ if (cols === 1 && stacks) {
7476
+ grid.boxes = stackedBoxes(nodes, area3, actualGap, settings);
7477
+ grid.rows = grid.boxes.map((box) => ({ offset: box.y - area3.y, size: box.height }));
7478
+ }
7142
7479
  const mirrored = (box) => rtl ? { ...box, x: area3.x + area3.width - (box.x - area3.x) - box.width } : box;
7143
7480
  const boxes = grid.boxes.map(mirrored);
7144
7481
  if (recorded && !hasRegions) flows.push({ path: containerPath, box: { ...area3 }, composition: { ...settings }, columns: rtl ? grid.columns.map((track) => ({ offset: area3.width - track.offset - track.size, size: track.size })) : grid.columns, rows: grid.rows, gap: grid.gap, itemCount: nodes.length, slotCount: count });
7482
+ const rowItems = /* @__PURE__ */ new Map(), stack = [];
7145
7483
  nodes.forEach((node, index) => {
7146
7484
  const box = node.region ? mirrored(regionBox(node.region, area3, actualGap)) : boxes[index];
7485
+ const first = items.length;
7147
7486
  placeNode(node, box, settings);
7487
+ if (!hasRegions && cols > 1 && !node.children) rowItems.set(Math.floor(index / cols), [...rowItems.get(Math.floor(index / cols)) ?? [], ...range(first, items.length)]);
7488
+ else if (!hasRegions && cols === 1 && stacks && !node.children) stack.push(...range(first, items.length));
7148
7489
  });
7490
+ for (const set2 of rowItems.values()) peerSets.push(set2);
7491
+ if (stack.length > 1) peerSets.push(stack);
7149
7492
  };
7150
7493
  let regionTag, templateCell = false;
7151
7494
  const composeChart = (node, box, settings) => {
@@ -7153,7 +7496,15 @@ function composeSlide(input, options = {}) {
7153
7496
  const valueAxis = chartValueAxis(node.value, resolved, { width: box.width, height: box.height, scale: scale2, minFontSize: settings.minFontSize });
7154
7497
  return valueAxis ? { valueAxis } : void 0;
7155
7498
  };
7156
- const placeNode = (node, box, settings, cards = hasCards, listColumns, metricReferenceHeight) => {
7499
+ const VIDEO_ASPECT = 16 / 9, CHART_MAX_ASPECT_RATIO = 3 / 4;
7500
+ const settleCaption = (caption, frame, anchor) => {
7501
+ if (caption.position === "above" || anchor !== "top") return;
7502
+ const target = round2(frame.y + frame.height + caption.fontSize * 0.4), dy = target - caption.box.y;
7503
+ if (dy >= -0.01) return;
7504
+ caption.box = { ...caption.box, y: target };
7505
+ caption.fit = translateTextFit(caption.fit, dy);
7506
+ };
7507
+ const placeNode = (node, box, settings, cards = hasCards, listColumns, metricReferenceHeight, anchor = "top") => {
7157
7508
  if (node.children) {
7158
7509
  const own3 = inheritedSettings(settings, node.composition), inner = inset(box, own3);
7159
7510
  if (!node.synthetic) groups.push({ path: node.path, box, contentBox: inner, composition: own3 });
@@ -7175,8 +7526,24 @@ function composeSlide(input, options = {}) {
7175
7526
  const timelineLayout = node.field === "timeline" ? measureTimeline(node, box, settings) : void 0;
7176
7527
  const internal = quoteLayout ?? codeLayout ?? metricLayout ?? timelineLayout, body = internal?.parts.find((part) => part.role === "body" || part.role === "value");
7177
7528
  const columns2 = textValue !== void 0 && !internal ? listColumnsFor(node, box, settings, listColumns) : void 0;
7178
- const text2 = internal ? body?.fit : columns2 ? columns2.length > 1 ? mergeListColumns(columns2) : columns2[0].text : textValue !== void 0 ? fitContent(node.field, node.value, textValue, box, contentNominalSize(node.field) * scale2, snapFontSizeUp((settings.minFontSize ?? 16) * scale2), node.path, node.payload.numbering) : void 0;
7179
- const image = node.field === "image" ? composeImage(node.host ?? {}, node.hostPath ?? node.path, box, box, imageContext()) : void 0;
7529
+ const floorSize = snapFontSizeUp((settings.minFontSize ?? 16) * scale2);
7530
+ const prose = textValue !== void 0 && !internal ? proseMeasure(node, box.width) : void 0, wholeBox = box;
7531
+ if (prose && prose.width < box.width - 0.5) {
7532
+ const side = alignmentFor(node.field), atEnd = rtl ? side === "left" : side === "right";
7533
+ box = { ...box, x: side === "center" ? box.x + (box.width - prose.width) / 2 : atEnd ? box.x + box.width - prose.width : box.x, width: prose.width };
7534
+ }
7535
+ let text2 = internal ? body?.fit : columns2 ? columns2.length > 1 ? mergeListColumns(columns2) : columns2[0].text : textValue !== void 0 ? fitContent(node.field, node.value, textValue, box, prose?.size ?? contentNominalSize(node.field) * scale2, floorSize, node.path, node.payload.numbering) : void 0;
7536
+ if (prose && box !== wholeBox && text2 && textValue !== void 0) {
7537
+ const wide = fitContent(node.field, node.value, textValue, wholeBox, prose.size, floorSize, node.path, node.payload.numbering);
7538
+ if (wide.fontSize > text2.fontSize + 1e-9 || text2.overflow && !wide.overflow) {
7539
+ text2 = wide;
7540
+ box = wholeBox;
7541
+ }
7542
+ }
7543
+ if (node.field === "video") box = roundBox(containedBox(box, VIDEO_ASPECT, anchor));
7544
+ else if (node.field === "chart" && !templateCell && modeFor(settings) === "auto" && box.height > box.width * CHART_MAX_ASPECT_RATIO) box = roundBox({ ...box, height: box.width * CHART_MAX_ASPECT_RATIO });
7545
+ const image = node.field === "image" ? composeImage(node.host ?? {}, node.hostPath ?? node.path, box, box, imageContext(void 0, anchor)) : void 0;
7546
+ if (caption) settleCaption(caption, image?.box ?? box, anchor);
7180
7547
  const chart = node.field === "chart" ? composeChart(node, box, settings) : void 0;
7181
7548
  items.push({
7182
7549
  path: node.path,
@@ -7184,7 +7551,7 @@ function composeSlide(input, options = {}) {
7184
7551
  type: node.type,
7185
7552
  value: node.value,
7186
7553
  payload: node.payload,
7187
- box: internal ? acceptedBox(box) : box,
7554
+ box: codeLayout && !codeLayout.overflow ? { ...acceptedBox(box), height: Math.min(acceptedBox(box).height, round2(codeLayout.bounds.height)) } : internal ? acceptedBox(box) : box,
7188
7555
  ...image ? { image } : {},
7189
7556
  ...chart ? { chart } : {},
7190
7557
  ...frameBox ? { frameBox } : {},
@@ -7201,7 +7568,7 @@ function composeSlide(input, options = {}) {
7201
7568
  ...regionTag !== void 0 ? { region: regionTag } : {},
7202
7569
  ...columns2 && columns2.length > 1 ? { listColumns: columns2 } : {}
7203
7570
  });
7204
- if (box.width < 100 * scale2 || !templateCell && box.height < 60 * scale2) diagnostics.push({ code: "small-cell", path: node.path, message: "Content cell is too small for comfortable reading; use fewer blocks or a different composition." });
7571
+ if (box.width < 100 * scale2 || !templateCell && !contentSized.has(node) && box.height < 60 * scale2) diagnostics.push({ code: "small-cell", path: node.path, message: "Content cell is too small for comfortable reading; use fewer blocks or a different composition." });
7205
7572
  };
7206
7573
  const listColumnsFor = (node, box, settings, regionMode) => {
7207
7574
  if (node.field !== "items" && node.field !== "bullets") return void 0;
@@ -7212,7 +7579,7 @@ function composeSlide(input, options = {}) {
7212
7579
  if (mode === void 0 || mode === 1) return void 0;
7213
7580
  const start = contentNominalSize(node.field) * scale2, floor = snapFontSizeUp((settings.minFontSize ?? 16) * scale2);
7214
7581
  const numbering = node.payload.numbering;
7215
- const listOptions = { ...richOptions(styleFor(node.field, node.path)), ...numbering !== void 0 ? { numbering } : bulletImage ? { bulletImage } : {} };
7582
+ const listOptions = { ...richOptions(styleFor(node.field, node.path)), ...numbering !== void 0 ? { numbering } : bulletImage ? { bulletImage } : noListBullet ? { bullet: "none" } : {} };
7216
7583
  const columnGap = (count2) => Math.min(gap, box.width / (count2 * 2));
7217
7584
  const columnBoxes = (count2) => {
7218
7585
  const between = columnGap(count2), columnWidth = (box.width - between * (count2 - 1)) / count2;
@@ -7248,7 +7615,7 @@ function composeSlide(input, options = {}) {
7248
7615
  const size = Math.min(...shrunk.map((column) => column.text.fontSize));
7249
7616
  return shrunk.map((column) => ({ ...column, text: renumber(fitList(chunk(column.start, column.end), column.box, size, size, listOptions), column.start) }));
7250
7617
  };
7251
- const LONG_LINES = 6, LONG_CODE_LINES = 12, startSize = 25 * scale2;
7618
+ const LONG_LINES = 6, LONG_CODE_LINES = 12, startSize = PROSE_NOMINAL_SIZE * scale2;
7252
7619
  const estimatedLines = (value, cellWidth, size) => flatten(value).split("\n").reduce((sum, paragraph) => sum + Math.max(1, wrapText(paragraph, Math.max(1, cellWidth), size).length), 0);
7253
7620
  const isLong = (node, cellWidth) => {
7254
7621
  if (node.children) return node.children.some((child) => isLong(child, cellWidth));
@@ -7272,22 +7639,37 @@ function composeSlide(input, options = {}) {
7272
7639
  if (node.field === "code") return String(typeof node.value === "string" ? node.value : record7(node.value).source ?? "").split("\n").length > LONG_CODE_LINES;
7273
7640
  return false;
7274
7641
  };
7275
- const naturalHeight = (node, cellWidth, cards, metricReferenceHeight, naturalSettings = composition) => {
7642
+ const proseMeasure = (node, cellWidth) => {
7643
+ const role = contentNominalSize(node.field) * scale2;
7644
+ if (node.field !== "text" || node.children || !node.auto) return { size: role, width: cellWidth };
7645
+ const linesAt = (width2, size) => estimatedLines(node.value, width2, size);
7646
+ const measureAt = (size) => Math.min(cellWidth, PROSE_MEASURE_CHARS * AVERAGE_CHARACTER_EM * size);
7647
+ const lead = LEAD_NOMINAL_SIZE * scale2;
7648
+ if (node.lone && lead > role && linesAt(measureAt(lead), lead) <= 2) return { size: lead, width: linesAt(cellWidth, lead) <= 1 ? cellWidth : measureAt(lead) };
7649
+ return { size: role, width: linesAt(cellWidth, role) <= 1 ? cellWidth : measureAt(role) };
7650
+ };
7651
+ const MEASURED_FIELDS = /* @__PURE__ */ new Set(["text", "items", "bullets", "quote", "metric", "code"]);
7652
+ const measuredNode = (node) => node.children ? node.children.length > 0 && node.children.every(measuredNode) : MEASURED_FIELDS.has(node.field);
7653
+ const naturalHeight = (node, cellWidth, cards, metricReferenceHeight, naturalSettings = composition, atFloor = false, sizeOverride) => {
7276
7654
  const pad = cards ? 2 * Math.min(12 * scale2, cellWidth / 4) : 0, inner = Math.max(scale2, cellWidth - pad);
7277
- if (node.children) return node.children.reduce((sum, child) => sum + naturalHeight(child, cellWidth, false), 0) + gap * Math.max(0, node.children.length - 1) + pad;
7655
+ if (node.children) return node.children.reduce((sum, child) => sum + naturalHeight(child, cellWidth, false, void 0, naturalSettings, atFloor), 0) + gap * Math.max(0, node.children.length - 1) + pad;
7656
+ const floorSize = snapFontSizeUp((naturalSettings.minFontSize ?? 16) * scale2);
7278
7657
  let content;
7279
7658
  if (node.field === "text" || node.field === "items" || node.field === "bullets") {
7280
- const size = contentNominalSize(node.field) * scale2;
7281
- const fit = fitContent(node.field, node.value, contentText(node.field, node.value) ?? "", { x: 0, y: 0, width: inner, height: 1e6 }, size, size, node.path, node.payload.numbering);
7282
- content = "height" in fit && typeof fit.height === "number" ? fit.height : fit.lines.length * fit.lineHeight;
7659
+ const prose = proseMeasure(node, inner), size = sizeOverride ?? (atFloor ? floorSize : prose.size);
7660
+ const fit = fitContent(node.field, node.value, contentText(node.field, node.value) ?? "", { x: 0, y: 0, width: atFloor || sizeOverride !== void 0 ? inner : prose.width, height: 1e6 }, size, size, node.path, node.payload.numbering);
7661
+ content = Math.max("height" in fit && typeof fit.height === "number" ? fit.height : fit.lines.length * fit.lineHeight, fit.placement?.height ?? 0);
7283
7662
  } else if (node.field === "quote") content = measureQuote(node, { x: 0, y: 0, width: inner, height: 1e6 }, naturalSettings).bounds.height + 18;
7284
- else if (node.field === "code") content = String(typeof node.value === "string" ? node.value : record7(node.value).source ?? "").split("\n").length * 18 * 1.4 * scale2 + 24 * scale2;
7285
- else if (node.field === "metric") content = measureMetric(node, { x: 0, y: 0, width: inner, height: metricReferenceHeight === void 0 ? 1e6 : Math.max(scale2, metricReferenceHeight - pad) }, naturalSettings).bounds.height;
7663
+ else if (node.field === "code") {
7664
+ const code = measureCode(node, { x: 0, y: 0, width: inner, height: 1e6 }, naturalSettings), source = code.parts.find((part) => part.role === "body");
7665
+ content = source?.fit ? source.box.y + source.fit.lines.length * source.fit.lineHeight + 18 : 1e6;
7666
+ } else if (node.field === "metric") content = measureMetric(node, { x: 0, y: 0, width: inner, height: metricReferenceHeight === void 0 ? 1e6 : Math.max(scale2, metricReferenceHeight - pad) }, naturalSettings).bounds.height;
7286
7667
  else if (node.field === "timeline") content = 170 * scale2;
7287
7668
  else if (node.field === "table") {
7288
7669
  const value = record7(node.value);
7289
7670
  content = ((Array.isArray(value.rows) ? value.rows.length : 4) + 1) * 36 * scale2;
7290
7671
  } else content = inner / 1.6;
7672
+ if (atFloor && !(node.field === "text" || node.field === "items" || node.field === "bullets")) content *= Math.min(1, floorSize / startSize);
7291
7673
  return content + (node.caption !== void 0 ? 32 * scale2 : 0) + pad;
7292
7674
  };
7293
7675
  const templateRegions = [], headingAreas = [];
@@ -7302,6 +7684,13 @@ function composeSlide(input, options = {}) {
7302
7684
  return parts.length === 1 ? parts[0] : { field: "blocks", type: "group", value: parts.map((part) => part.payload), path: bound.path, payload: {}, children: parts, composition: { mode: "column" }, synthetic: true };
7303
7685
  };
7304
7686
  const contents = new Map(binding.regions.map((entry) => [entry.name, [...entry.blocks, ...entry.overflow].map(unitOf)]));
7687
+ for (const region of plan2.regions) {
7688
+ const units = contents.get(region.name) ?? [];
7689
+ if (region.flow === "auto") for (const unit of units) {
7690
+ unit.auto = true;
7691
+ unit.lone = units.length === 1;
7692
+ }
7693
+ }
7305
7694
  const BELOW = "(below)";
7306
7695
  const belowUnits = binding.below.map(unitOf);
7307
7696
  const belowRegion = { name: BELOW, accepts: [], role: "supporting", flow: "auto", max: belowUnits.length, bleed: false, listColumns: 1, anchor: "top", empty: "collapse" };
@@ -7340,7 +7729,7 @@ function composeSlide(input, options = {}) {
7340
7729
  const list = [];
7341
7730
  let top = y2;
7342
7731
  for (const field of fields2) {
7343
- const requested = (field === "title" ? 54 : field === "tag" ? 16 : 25) * scale2;
7732
+ const requested = (field === "title" ? 54 : field === "tag" ? 16 : PROSE_NOMINAL_SIZE) * scale2;
7344
7733
  const maxHeight = Math.max(height * (field === "title" ? 0.26 : field === "subtitle" ? 0.12 : 0.045), minSize * 1.22 + 2 * rasterPadding);
7345
7734
  const box = { x, y: top, width: areaWidth, height: maxHeight };
7346
7735
  const text2 = fitPlacedText(field, slide[field], annotationText(slide[field]), box, requested, minSize, `${path}.${field}`);
@@ -7361,33 +7750,35 @@ function composeSlide(input, options = {}) {
7361
7750
  const cellWidth = (cell.width - gap2 * (shape.columns - 1)) / shape.columns - (cards ? 2 * Math.min(12 * scale2, cell.width / 4) : 0);
7362
7751
  return units.some((unit) => isLong(unit, cellWidth)) ? "column" : "grid";
7363
7752
  };
7364
- const regionNeed = (name, areaWidth) => {
7753
+ const regionNeed = (name, areaWidth, atFloor = false) => {
7365
7754
  const units = unitsOf(name), region = name === BELOW ? belowRegion : regionOf.get(name);
7366
- if (!units.length) return region.empty === "keep" ? 2 * startSize * 1.22 + (hasCards && !region.bleed ? 2 * Math.min(12 * scale2, areaWidth / 4) : 0) : 0;
7755
+ if (!units.length) return region.empty === "keep" && !atFloor ? 2 * (region.flow === "auto" ? LEAD_NOMINAL_SIZE * scale2 : startSize) * 1.22 + (hasCards && !region.bleed ? 2 * Math.min(12 * scale2, areaWidth / 4) : 0) : 0;
7367
7756
  const shape = flowShape(arrangementOf(region, units, { width: areaWidth, height: areaWidth / 4 }), units.length, true);
7368
7757
  const cellWidth = (areaWidth - gap2 * (shape.columns - 1)) / shape.columns, cards = hasCards && !region.bleed;
7369
7758
  let total = 0, index = 0;
7370
7759
  for (const count of shape.perRow) {
7371
7760
  let row = 0;
7372
- for (let k = 0; k < count; k++) row = Math.max(row, naturalHeight(units[index++], cellWidth, cards));
7761
+ for (let k = 0; k < count; k++) row = Math.max(row, naturalHeight(units[index++], cellWidth, cards, void 0, composition, atFloor));
7373
7762
  total += row;
7374
7763
  }
7375
7764
  return total + gap2 * Math.max(0, shape.rows - 1);
7376
7765
  };
7377
7766
  const areaNeed = (name, rect) => name === "title" ? groupHeight(headingItemsAt(titleFields, 0, 0, spanWidth(rect))) : name === "subtitle" ? groupHeight(headingItemsAt(subtitleFields, 0, 0, spanWidth(rect))) : regionNeed(name, spanWidth(rect));
7378
7767
  const demands = rowSizes.map((size, row) => {
7379
- let need = 0, heading = false, below = false;
7768
+ let need = 0, floor = 0, heading = false, below = false;
7380
7769
  for (const [name, rect] of rects) {
7381
- if (size === "auto" ? rect.row !== row || rect.rowSpan !== 1 : row < rect.row || row >= rect.row + rect.rowSpan) continue;
7770
+ const autoSpan = size === "auto" && rect.rowSpan > 1 && rect.row === row && rowSizes.slice(rect.row, rect.row + rect.rowSpan).every((entry) => entry === "auto");
7771
+ if (size === "auto" ? rect.row !== row || rect.rowSpan !== 1 && !autoSpan : row < rect.row || row >= rect.row + rect.rowSpan) continue;
7382
7772
  if (size !== "auto" && rect.rowSpan > 1 && rowSizes.slice(rect.row, rect.row + rect.rowSpan).some((entry) => entry === "auto")) continue;
7383
- const value = areaNeed(name, rect) / rect.rowSpan;
7773
+ const value = areaNeed(name, rect) / (autoSpan ? 1 : rect.rowSpan);
7774
+ if (size === "auto" && name !== "title" && name !== "subtitle") floor = Math.max(floor, regionNeed(name, spanWidth(rect), true));
7384
7775
  if (value > need) {
7385
7776
  need = value;
7386
7777
  heading = name === "title" || name === "subtitle";
7387
7778
  below = name === BELOW;
7388
7779
  }
7389
7780
  }
7390
- return { size, need, kind: heading ? "heading" : below ? "below" : "body" };
7781
+ return { size, need, ...size === "auto" && !heading ? { floor } : {}, kind: heading ? "heading" : below ? "below" : "body" };
7391
7782
  });
7392
7783
  const heights = allocateRows(demands, frame.height, gap2);
7393
7784
  const rowY = [];
@@ -7395,9 +7786,11 @@ function composeSlide(input, options = {}) {
7395
7786
  rowY.push(size > 0 || !heights.slice(0, row).some((above) => above > 0) ? y2 : y2 - gap2);
7396
7787
  return size > 0 ? y2 + size + gap2 : y2;
7397
7788
  }, 0);
7789
+ const stackUsed = heights.reduce((sum, size) => sum + size, 0) + gap2 * Math.max(0, heights.filter((size) => size > 0).length - 1);
7790
+ const stackShift = plan2.stack === void 0 ? 0 : Math.max(0, frame.height - stackUsed) * (plan2.stack === "middle" ? 0.5 : 1);
7398
7791
  const logical = (rect) => {
7399
- const last = rect.row + rect.rowSpan - 1, y2 = frame.y + rowY[rect.row];
7400
- return { x: frame.x + columnX[rect.column], y: y2, width: spanWidth(rect), height: Math.max(0, frame.y + rowY[last] + heights[last] - y2) };
7792
+ const last = rect.row + rect.rowSpan - 1, y2 = frame.y + stackShift + rowY[rect.row];
7793
+ return { x: frame.x + columnX[rect.column], y: y2, width: spanWidth(rect), height: Math.max(0, frame.y + stackShift + rowY[last] + heights[last] - y2) };
7401
7794
  };
7402
7795
  const flip = hints.mirror === true !== rtl;
7403
7796
  const physical = (cell) => flip ? { ...cell, x: frame.x + frame.width - (cell.x - frame.x) - cell.width } : cell;
@@ -7454,18 +7847,8 @@ function composeSlide(input, options = {}) {
7454
7847
  const arrangement = arrangementOf(region, units, cell);
7455
7848
  let boxes = flowCells(flowShape(arrangement, units.length, cell.width >= cell.height), cell, gap2).cells;
7456
7849
  const referenceHeights = boxes.map((box) => box.height);
7457
- if (arrangement === "column" && region.anchor !== "top" && units.every((unit) => !unit.children && (unit.field === "text" || unit.field === "items" || unit.field === "bullets" || !unit.caption && (unit.field === "quote" || unit.field === "metric")))) {
7458
- const naturals = units.map((unit, index) => naturalHeight(unit, cell.width, cards, referenceHeights[index], settings) + 2 * rasterPadding + scale2);
7459
- const used = naturals.reduce((a, b) => a + b, 0) + gap2 * (units.length - 1);
7460
- if (used < cell.height) {
7461
- let y2 = cell.y + (region.anchor === "middle" ? (cell.height - used) / 2 : cell.height - used);
7462
- boxes = naturals.map((size) => {
7463
- const next = { x: cell.x, y: y2, width: cell.width, height: size };
7464
- y2 += size + gap2;
7465
- return next;
7466
- });
7467
- }
7468
- }
7850
+ const stacked = arrangement === "column" && !(units.length === 1 && region.anchor === "top");
7851
+ if (stacked) boxes = stackedBoxes(units, cell, gap2, settings, cards, region.anchor);
7469
7852
  if ((arrangement === "grid" || arrangement === "row") && region.anchor !== "top") {
7470
7853
  const naturals = boxes.map((box, index) => {
7471
7854
  const unit = units[index];
@@ -7480,22 +7863,44 @@ function composeSlide(input, options = {}) {
7480
7863
  });
7481
7864
  }
7482
7865
  if (rtl) boxes = boxes.map((box) => ({ ...box, x: cell.x + cell.width - (box.x - cell.x) - box.width }));
7866
+ const shape = flowShape(arrangement, units.length, cell.width >= cell.height), rowItems = shape.perRow.map(() => []);
7867
+ const rowOf = (index) => {
7868
+ let row = 0;
7869
+ for (let seen = shape.perRow[0] ?? 0; index >= seen; seen += shape.perRow[++row] ?? Infinity) ;
7870
+ return row;
7871
+ };
7872
+ const stack = [];
7483
7873
  units.forEach((unit, index) => {
7484
- placeNode(unit, boxes[index], settings, cards, region.listColumns, region.anchor === "top" ? void 0 : referenceHeights[index]);
7874
+ const first = items.length;
7875
+ placeNode(unit, boxes[index], settings, cards, region.listColumns, region.anchor === "top" || stacked ? void 0 : referenceHeights[index], region.anchor);
7876
+ if (!unit.children) {
7877
+ rowItems[rowOf(index)]?.push(...range(first, items.length));
7878
+ leafItems.push(...range(first, items.length));
7879
+ stack.push(...range(first, items.length));
7880
+ }
7485
7881
  });
7882
+ if (arrangement !== "column") peerSets.push(...rowItems.filter((_, row) => (shape.perRow[row] ?? 0) > 1));
7883
+ else if (stacked && stack.length > 1) peerSets.push(stack);
7486
7884
  return arrangement;
7487
7885
  } finally {
7488
7886
  regionTag = void 0;
7489
7887
  templateCell = false;
7490
7888
  }
7491
7889
  };
7890
+ const bands = /* @__PURE__ */ new Map();
7891
+ let leafItems = [];
7492
7892
  for (const region of plan2.regions) {
7493
7893
  const units = unitsOf(region.name), bound = binding.regions.find((entry) => entry.name === region.name);
7494
7894
  const rect = rects.get(region.name);
7495
7895
  let cell = physical(logical(rect ?? ownRects.get(region.name)));
7496
7896
  const areaBox = region.bleed && rect ? roundBox(cell) : void 0;
7497
7897
  if (region.bleed && rect) cell = bleed(cell);
7898
+ leafItems = [];
7498
7899
  const arrangement = units.length && rect ? placeRegion(region, units, cell) : void 0;
7900
+ if (arrangement && rect) {
7901
+ const band = `${rect.row}:${rect.rowSpan}:${region.role}`;
7902
+ bands.set(band, [...bands.get(band) ?? [], leafItems]);
7903
+ }
7499
7904
  templateRegions.push({
7500
7905
  name: region.name,
7501
7906
  box: roundBox(cell),
@@ -7512,8 +7917,36 @@ function composeSlide(input, options = {}) {
7512
7917
  ...areaBox ? { areaBox } : {}
7513
7918
  });
7514
7919
  }
7920
+ for (const members of bands.values()) if (members.length > 1) peerSets.push(members.flat());
7515
7921
  if (belowUnits.length) placeRegion(belowRegion, belowUnits, physical(logical(rects.get(BELOW))));
7516
7922
  };
7923
+ const sharePeerSizes = () => {
7924
+ const parent = /* @__PURE__ */ new Map();
7925
+ const find = (index) => {
7926
+ let root = index;
7927
+ while (parent.has(root) && parent.get(root) !== root) root = parent.get(root);
7928
+ parent.set(index, root);
7929
+ return root;
7930
+ };
7931
+ for (const set2 of peerSets) for (const index of set2) parent.set(find(index), find(set2[0]));
7932
+ const kindOf = (item) => !item.text || item.quoteLayout || item.codeLayout || item.metricLayout || item.timelineLayout || item.listColumns ? void 0 : item.field === "text" ? "text" : item.field === "items" || item.field === "bullets" ? "list" : void 0;
7933
+ const sets = /* @__PURE__ */ new Map();
7934
+ for (const index of parent.keys()) {
7935
+ const item = items[index], kind2 = item && kindOf(item);
7936
+ if (kind2) sets.set(`${find(index)}:${kind2}`, [...sets.get(`${find(index)}:${kind2}`) ?? [], index]);
7937
+ }
7938
+ for (const members of sets.values()) {
7939
+ if (members.length < 2) continue;
7940
+ const size = Math.min(...members.map((index) => items[index].text.fontSize));
7941
+ for (const index of members) {
7942
+ const item = items[index];
7943
+ if (item.text.fontSize <= size + 1e-9) continue;
7944
+ const text2 = contentText(item.field, item.value) ?? "";
7945
+ const refit = fitContent(item.field, item.value, text2, item.box, size, snapFontSizeUp((item.composition.minFontSize ?? 16) * scale2), item.path, item.payload.numbering);
7946
+ if (!refit.overflow && Math.abs(refit.fontSize - size) < 1e-9) item.text = refit;
7947
+ }
7948
+ }
7949
+ };
7517
7950
  const ownMode = record7(slide.composition).mode;
7518
7951
  const direction = hints.contentDirection;
7519
7952
  const directionMode = direction === "vertical" ? "column" : direction === "horizontal" ? "row" : "auto";
@@ -7529,6 +7962,7 @@ function composeSlide(input, options = {}) {
7529
7962
  const support = { field: "blocks", type: "group", value: rest.map((node) => node.payload), path, payload: {}, children: rest, composition: {}, synthetic: true };
7530
7963
  arrange(firstChart ? [primary, support] : [support, primary], contentBox, rootSettings, 0);
7531
7964
  } else arrange(pending, contentBox, rootSettings, 0);
7965
+ sharePeerSizes();
7532
7966
  if (listBullet?.value === "image" && !bulletImage && items.some((item) => (item.field === "items" || item.field === "bullets") && item.payload.numbering === void 0)) {
7533
7967
  diagnostics.push({ code: "unresolved-content", path: listBullet.path, message: "Picture bullets (listBullet: image) need an organization logo (the primary organization, or the one design.logo names); the marker glyph is drawn instead." });
7534
7968
  }
@@ -7591,4 +8025,4 @@ function resolveCanvasDimensions(input) {
7591
8025
  return { width, height };
7592
8026
  }
7593
8027
 
7594
- export { AUTO_LAYOUT, CAPTIONABLE_FIELDS, CAPTION_FONT_RATIO, CAPTION_MAX_RATIO, CATALOG_GROUP_PATTERN, CATALOG_REFERENCE_PATTERN, CHART_HIGHLIGHT_MUTED_MIN_CONTRAST, CHART_HIGHLIGHT_MUTED_MIX, CHART_LEAD_MIN_CONTRAST, CHART_MAX_INTERVALS, CHART_MIN_TICK_SPACING_PX, CHART_SERIES_MIN_DIFFERENCE, CHART_SERIES_MIN_LIGHTNESS_STEP, CHART_SERIES_TIER_SIZE, CHART_SERIES_TINT_MIX, CHART_TEXT_FLOOR_PX, CHART_TEXT_PX, CHART_TYPES, CITATION_MARKER_RAISE, CITATION_MARKER_SCALE, CODE_HIGHLIGHT_LANGUAGES, CODE_HIGHLIGHT_MAX_LENGTH, CODE_PANEL_BACKGROUND, CODE_PANEL_FOREGROUND, CODE_SYNTAX_MIN_CONTRAST, DARK_BACKGROUND_LUMINANCE, DEFAULT_CHART_LABEL_SEPARATOR, DEFAULT_FURNITURE_DATE_FORMAT, DEFAULT_VARIABLE_DATE_FORMAT, DESIGN_HINT_KEYS, ENGINE_DEFAULT_CHART_TYPES, ENGINE_DEFAULT_COLOR_SCHEME, ENGINE_DEFAULT_FONT_SCHEME, ENGINE_DEFAULT_THEME, FONT_SIZE_GRID_PER_PX, FOOTNOTE_MAX_RATIO, FURNITURE_GAP, FURNITURE_IMAGE_SHARE, FURNITURE_ZONE_GUTTER, HEADING_AREAS, LANGUAGES, LAYOUT_BODY_KINDS, LOGO_SHAPES, MAX_COMPOSITION_DEPTH, MAX_NUMBERING_LEVELS, MAX_NUMBERING_VALUE, MAX_REGION_BLOCKS, MAX_REGION_DEPTH, MAX_REGION_ENTRIES, MAX_ROMAN_VALUE, MAX_TEMPLATE_TRACKS, METRIC_TREND_MIN_CONTRAST, METRIC_TREND_SHAPES, METRIC_VALUE_MIN_CONTRAST, NEUTRAL_SHARES, NUMBERING_STYLES, NUMBERING_SUFFIXES, OPFCatalogsOptionError, OPFCompositionError, OPFLayoutTemplateError, OPFUnresolvedReferenceError, OPFVariableError, PATTERN_PRESETS, PATTERN_PRESET_ALIASES, PATTERN_TILE_SIZE, PICTURE_BULLET_SCALE, PROMOTED_REGION_KEY, REGION_ANCHORS, REGION_FLOWS, REGION_KINDS, REGION_ROLES, RESERVED_REGION_NAMES, SLIDE_SCOPED_BUILTINS, SOCIAL_PLATFORMS, SURFACE_ALT_MIN_CONTRAST, SURFACE_ALT_MIX, THEME_DESIGN_KEYS, TIMELINE_OUTLINE_MIN_CONTRAST, TIMELINE_STATUSES, TIMELINE_TEXT_MIN_CONTRAST, VARIABLE_KINDS, allocateRows, annotationText, applyLuminance, bindRegions, blockKind, captionSettings, catalogGroupDeclared, catalogGroupSource, catalogKinds, catalogRecords, catalogReferenceSites, chartColorForFill, chartHighlightColors, chartHighlightMarks, chartIntervalsFor, chartLabelText, chartLeadFromScheme, chartNumber, chartNumberFixesByCell, chartOptionSupport, chartOptionTarget, chartPaletteForFill, chartSeriesPalette, chartTextSize, chartValueAxis, checkCatalogsOption, citationMarkerText, clrMapAttributes, codeHighlightBands, codeHighlightColors, codeHighlightLines, codeHighlightSlice, codeLineNumbers, codeLineRuns, codeSyntaxPalette, codeSyntaxPaletteForRoles, codeSyntaxPaletteForScheme, coerceVariableValue, collectCitations, colorContrast, colorMapFor, composeLayoutAreas, composeSlide, datasetDiagnostics, defaultSlideBackground, dottedPath, embeddedCopy, fitImage, fitList, fitRichText, fitText, formatChartLabelNumber, formatChartLabelPercent, formatDataNumber, formatFurnitureDate, formatListNumber, formatVariableNumber, fromExcelNumberFormat, gridFlowShape, hasContentVariables, imageBackground, imageShape, inlineChartData, inlineDatasets, inlineTableData, instantiateForValidation, intrinsicImageAspect, intrinsicImageSize, isColorVariant, isDarkColor, isDatasetRef, isLayoutTemplate, isRecord, isTemplate, isXYChartType, layoutCaption, layoutCode, layoutContent, layoutEntries, layoutFootnotes, layoutFurniture, layoutMetric, layoutQuote, layoutRegion, layoutStructure, layoutTable, layoutTemplate, layoutTemplateIssues, layoutTimeline, layoutWatermark, listBuiltinVariables, listColumnBreaks, listNumbers, listVariables, maxIntervalsFor, measureText, measureTextOutline, metricTrendColor, metricTrendMark, metricTrendPoints, metricValueColor, niceScale, normalizeColorVariant, normalizeHexColor, normalizeLanguageFamily, numberFormatError, numberingAtLevel, numberingFindings, numberingStyleDraws, paragraphDirection, paragraphDirectionAt, parseReference, pathFor, patternBitmap, patternRuns, physicalAlignment, placeTextLines, placedImageBlocks, pointerPath, promotedRegionKeys, provenanceOf, readableSchemeColor, readingRows, recordReferenceSites, referencesSlide, regionAccepts, regionRoleRank, registeredCatalog, resolveCanvasDimensions, resolveChartColors, resolveChartData, resolveChartOptions, resolveCodeLanguage, resolveColorRef, resolveColorRoles, resolveDesignHints, resolveDesignRecords, resolveFontFamilies, resolveFontScheme, resolveLogo, resolveNumbering, resolvePatternPreset, resolveReference, resolveScriptFonts, resolveSlideDirection, resolveSlideVariables, resolveSocialProfile, resolveTableData, resolveTextStyle, resolveVariables, rootNodes, scriptFontRole, sliceNumberedItems, slideCitations, slideTokenSpans, snapFontSizeDown, snapFontSizeUp, stackCategoryValues, suggestChartNumberFix, surfaceAltColor, tableCellDisplayValue, tableGrid, tableRowBoundaries, textColorForFill, textWidthMeasurer, timelineMarkerShapes, timelineTextColor, toExcelNumberFormat, tokenizeCode, unresolvedReference, unusedDatasets, usesSlideBuiltin, variableDeclarations, visitContentPayloads, visualReadingOrder, walkCitationRuns, wrapText };
8028
+ export { ASCII_QUOTE_MARKS, AUTO_LAYOUT, CAPTIONABLE_FIELDS, CAPTION_FONT_RATIO, CAPTION_MAX_RATIO, CATALOG_GROUP_PATTERN, CATALOG_REFERENCE_PATTERN, CHART_HIGHLIGHT_MUTED_MIN_CONTRAST, CHART_HIGHLIGHT_MUTED_MIX, CHART_LEAD_MIN_CONTRAST, CHART_MAX_INTERVALS, CHART_MIN_TICK_SPACING_PX, CHART_SERIES_MIN_DIFFERENCE, CHART_SERIES_MIN_LIGHTNESS_STEP, CHART_SERIES_TIER_SIZE, CHART_SERIES_TINT_MIX, CHART_TEXT_FLOOR_PX, CHART_TEXT_PX, CHART_TYPES, CITATION_MARKER_RAISE, CITATION_MARKER_SCALE, CODE_HIGHLIGHT_LANGUAGES, CODE_HIGHLIGHT_MAX_LENGTH, CODE_NOMINAL_SIZE, CODE_PANEL_BACKGROUND, CODE_PANEL_FOREGROUND, CODE_SYNTAX_MIN_CONTRAST, DARK_BACKGROUND_LUMINANCE, DEFAULT_CHART_LABEL_SEPARATOR, DEFAULT_FURNITURE_DATE_FORMAT, DEFAULT_VARIABLE_DATE_FORMAT, DESIGN_HINT_KEYS, ENGINE_DEFAULT_CHART_TYPES, ENGINE_DEFAULT_COLOR_SCHEME, ENGINE_DEFAULT_FONT_SCHEME, ENGINE_DEFAULT_THEME, FONT_SIZE_GRID_PER_PX, FOOTNOTE_MAX_RATIO, FURNITURE_GAP, FURNITURE_IMAGE_SHARE, FURNITURE_ZONE_GUTTER, HEADING_AREAS, LANGUAGES, LAYOUT_BODY_KINDS, LEAD_NOMINAL_SIZE, LIST_NOMINAL_SIZE, LOGO_SHAPES, MAX_COMPOSITION_DEPTH, MAX_NUMBERING_LEVELS, MAX_NUMBERING_VALUE, MAX_REGION_BLOCKS, MAX_REGION_DEPTH, MAX_REGION_ENTRIES, MAX_ROMAN_VALUE, MAX_TEMPLATE_TRACKS, METRIC_TREND_MIN_CONTRAST, METRIC_TREND_SHAPES, METRIC_VALUE_MIN_CONTRAST, NEUTRAL_SHARES, NUMBERING_STYLES, NUMBERING_SUFFIXES, OPFCatalogsOptionError, OPFCompositionError, OPFLayoutTemplateError, OPFUnresolvedReferenceError, OPFVariableError, PATTERN_PRESETS, PATTERN_PRESET_ALIASES, PATTERN_TILE_SIZE, PICTURE_BULLET_SCALE, PROMOTED_REGION_KEY, PROSE_NOMINAL_SIZE, QUOTE_MARKS, QUOTE_NOMINAL_SIZE, QUOTE_SOURCE_SEPARATOR, REGION_ANCHORS, REGION_FLOWS, REGION_KINDS, REGION_ROLES, RESERVED_REGION_NAMES, SLIDE_SCOPED_BUILTINS, SOCIAL_PLATFORMS, SURFACE_ALT_MIN_CONTRAST, SURFACE_ALT_MIX, TABLE_NOMINAL_SIZE, THEME_DESIGN_KEYS, TIMELINE_OUTLINE_MIN_CONTRAST, TIMELINE_STATUSES, TIMELINE_TEXT_MIN_CONTRAST, VARIABLE_KINDS, allocateRows, annotationText, applyLuminance, bindRegions, blockKind, captionSettings, catalogGroupDeclared, catalogGroupSource, catalogKinds, catalogRecords, catalogReferenceSites, chartColorForFill, chartHighlightColors, chartHighlightMarks, chartIntervalsFor, chartLabelText, chartLeadFromScheme, chartNumber, chartNumberFixesByCell, chartOptionSupport, chartOptionTarget, chartPaletteForFill, chartSeriesPalette, chartTextSize, chartValueAxis, checkCatalogsOption, citationMarkerText, clrMapAttributes, codeHighlightBands, codeHighlightColors, codeHighlightLines, codeHighlightSlice, codeLineNumbers, codeLineRuns, codeSyntaxPalette, codeSyntaxPaletteForRoles, codeSyntaxPaletteForScheme, coerceVariableValue, collectCitations, colorContrast, colorMapFor, composeLayoutAreas, composeSlide, datasetDiagnostics, defaultSlideBackground, dottedPath, embeddedCopy, fitImage, fitList, fitRichText, fitText, formatChartLabelNumber, formatChartLabelPercent, formatDataNumber, formatFurnitureDate, formatListNumber, formatVariableNumber, fromExcelNumberFormat, gridFlowShape, hasContentVariables, imageBackground, imageShape, inlineChartData, inlineDatasets, inlineTableData, instantiateForValidation, intrinsicImageAspect, intrinsicImageSize, isColorVariant, isDarkColor, isDatasetRef, isLayoutTemplate, isRecord, isTemplate, isWrappedInQuoteMarks, isXYChartType, layoutCaption, layoutCode, layoutContent, layoutEntries, layoutFootnotes, layoutFurniture, layoutMetric, layoutQuote, layoutRegion, layoutStructure, layoutTable, layoutTemplate, layoutTemplateIssues, layoutTimeline, layoutWatermark, listBuiltinVariables, listColumnBreaks, listNumbers, listVariables, maxIntervalsFor, measureText, measureTextOutline, metricTrendColor, metricTrendMark, metricTrendPoints, metricValueColor, niceScale, normalizeColorVariant, normalizeHexColor, normalizeLanguageFamily, numberFormatError, numberingAtLevel, numberingFindings, numberingStyleDraws, paragraphDirection, paragraphDirectionAt, parseReference, pathFor, patternBitmap, patternRuns, physicalAlignment, placeTextLines, placedImageBlocks, pointerPath, promotedRegionKeys, provenanceOf, quoteLanguage, quoteMarksFor, readableSchemeColor, readingRows, recordReferenceSites, referencesSlide, regionAccepts, regionRoleRank, registeredCatalog, resolveCanvasDimensions, resolveChartColors, resolveChartData, resolveChartOptions, resolveCodeLanguage, resolveColorRef, resolveColorRoles, resolveDesignHints, resolveDesignRecords, resolveFontFamilies, resolveFontScheme, resolveLogo, resolveNumbering, resolvePatternPreset, resolveReference, resolveScriptFonts, resolveSlideDirection, resolveSlideVariables, resolveSocialProfile, resolveTableData, resolveTextStyle, resolveVariables, rootNodes, scriptFontRole, shareHeights, sliceNumberedItems, slideCitations, slideTokenSpans, snapFontSizeDown, snapFontSizeUp, stackCategoryValues, suggestChartNumberFix, surfaceAltColor, tableCellDisplayValue, tableGrid, tableRowBoundaries, textColorForFill, textWidthMeasurer, timelineMarkerShapes, timelineTextColor, toExcelNumberFormat, tokenizeCode, unresolvedReference, unusedDatasets, unwrapQuoteMarks, usesSlideBuiltin, variableDeclarations, visitContentPayloads, visualReadingOrder, walkCitationRuns, wrapText };