@openpresentation/opf 0.27.0 → 0.29.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-EO3JWEO5.js → chunk-5OMZW44K.js} +1 -1
  6. package/dist/{chunk-4ZM5G3XX.js → chunk-5Q4IYLMW.js} +30 -14
  7. package/dist/{chunk-WJ4XTCOD.js → chunk-7UO6H32I.js} +3 -3
  8. package/dist/{chunk-4ACS57MG.js → chunk-ARFAQNOH.js} +2 -2
  9. package/dist/{chunk-GNKESMZT.js → chunk-F5QC7E2O.js} +6 -6
  10. package/dist/{chunk-SYW4BCGZ.js → chunk-HRKV43GC.js} +2 -2
  11. package/dist/{chunk-TMREBZBQ.js → chunk-I6G32TB5.js} +3 -3
  12. package/dist/{chunk-O3SA5SJ4.js → chunk-KELC4K62.js} +1 -1
  13. package/dist/{chunk-ABFDFZIW.js → chunk-KOCXV7H3.js} +1 -1
  14. package/dist/{chunk-PAGVSTVF.js → chunk-M7YZU3DX.js} +149 -26
  15. package/dist/{chunk-JSHMFTSY.js → chunk-P5E3FNYO.js} +1 -1
  16. package/dist/{chunk-ZGTOZA2S.js → chunk-R455YZNO.js} +6 -6
  17. package/dist/{chunk-OSM3LZ2Y.js → chunk-SYCHWS7C.js} +4 -4
  18. package/dist/{chunk-353VBJ6K.js → chunk-UKWNHK62.js} +5 -5
  19. package/dist/{chunk-LRTQXGJO.js → chunk-W3SEWSO5.js} +543 -94
  20. package/dist/{chunk-CXPBU5W4.js → chunk-W7KPMUJF.js} +1 -1
  21. package/dist/{chunk-APLNGCEZ.js → chunk-WHDOOLIO.js} +2 -2
  22. package/dist/{chunk-KZPZRHL6.js → chunk-XG4JKFZ6.js} +6 -6
  23. package/dist/{chunk-7XUEKIXH.js → chunk-Z7XNHIW7.js} +2 -2
  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-8SQD4dqu.d.ts → conversion-CCAmVKp8.d.ts} +9 -8
  29. package/dist/conversion-KV2WWCMK.js +24 -0
  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],
@@ -5632,25 +5832,40 @@ var SCRIPT_WIDTH_FACTORS = [
5632
5832
  [/\p{Script=Ethiopic}/u, 1.13],
5633
5833
  [/\p{Script=Georgian}/u, 1.28]
5634
5834
  ];
5835
+ var ZERO_WIDTH_MARK = /\p{Mn}|\p{Me}|\u200d|\ufe0f/u;
5836
+ var SPACING_MARK = /\p{Mc}/u;
5837
+ var LATIN_CJK = /\p{Script=Latin}|\p{Script=Han}|\p{Script=Hangul}|\p{Script=Hiragana}|\p{Script=Katakana}/u;
5838
+ function characterUnits(character) {
5839
+ if (ZERO_WIDTH_MARK.test(character)) return Number.NaN;
5840
+ if (SPACING_MARK.test(character) && LATIN_CJK.test(character)) return Number.NaN;
5841
+ const base = character === " " ? 0.32 : /[A-Z0-9]/.test(character) ? 0.62 : character.codePointAt(0) > 11904 ? 1 : 0.54;
5842
+ const factor = SCRIPT_WIDTH_FACTORS.find(([script]) => script.test(character))?.[1] ?? 1;
5843
+ return base * factor;
5844
+ }
5845
+ var ASCII_UNITS = Array.from({ length: 128 }, (_, code) => characterUnits(String.fromCharCode(code)));
5846
+ var characterUnitCache = /* @__PURE__ */ new Map();
5847
+ var CHARACTER_UNIT_CACHE_LIMIT = 65536;
5635
5848
  function measureText(text2, fontSize) {
5636
5849
  let units = 0;
5637
5850
  for (const character of text2) {
5638
- if (/\p{Mn}|\p{Me}|\u200d|\ufe0f/u.test(character)) continue;
5639
- if (/\p{Mc}/u.test(character) && /\p{Script=Latin}|\p{Script=Han}|\p{Script=Hangul}|\p{Script=Hiragana}|\p{Script=Katakana}/u.test(character)) continue;
5640
- const base = character === " " ? 0.32 : /[A-Z0-9]/.test(character) ? 0.62 : character.codePointAt(0) > 11904 ? 1 : 0.54;
5641
- const factor = SCRIPT_WIDTH_FACTORS.find(([script]) => script.test(character))?.[1] ?? 1;
5642
- units += base * factor;
5851
+ const code = character.charCodeAt(0);
5852
+ let advance = code < 128 ? ASCII_UNITS[code] : characterUnitCache.get(character);
5853
+ if (advance === void 0) {
5854
+ advance = characterUnits(character);
5855
+ if (characterUnitCache.size < CHARACTER_UNIT_CACHE_LIMIT) characterUnitCache.set(character, advance);
5856
+ }
5857
+ if (!Number.isNaN(advance)) units += advance;
5643
5858
  }
5644
5859
  return units * fontSize;
5645
5860
  }
5646
5861
  function wrapText(text2, width, fontSize, measure = measureText) {
5647
5862
  return fitSourceText(text2, { width, height: Number.MAX_VALUE }, fontSize, fontSize, 1, measure, true, false).lines;
5648
5863
  }
5649
- function fitText(text2, box, requestedSize = 25, minFontSize = 16, measure = measureText, direction) {
5864
+ function fitText(text2, box, requestedSize = PROSE_NOMINAL_SIZE, minFontSize = 16, measure = measureText, direction, balance = false) {
5650
5865
  if (![box.width, box.height, requestedSize, minFontSize].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || requestedSize <= 0 || minFontSize <= 0) {
5651
5866
  throw new RangeError("Text dimensions and font sizes must be finite and positive.");
5652
5867
  }
5653
- return fitSourceText(text2, box, Math.max(requestedSize, minFontSize), minFontSize, 1, measure, true, true, direction);
5868
+ return fitSourceText(text2, box, Math.max(requestedSize, minFontSize), minFontSize, 1, measure, true, true, direction, balance);
5654
5869
  }
5655
5870
  var FONT_SIZE_GRID_PER_PX = 75;
5656
5871
  var FONT_GRID_EPSILON = 1e-6;
@@ -5675,11 +5890,11 @@ function fitAtSizes(layout, requested, minimum, step = 1, snap = true) {
5675
5890
  }
5676
5891
  var PHOTO_RATIO = 3;
5677
5892
  var PHOTO_GAP = 0.75;
5678
- function quoteBodyRuns(runs) {
5679
- if (!runs.length) return ['""'];
5893
+ function quoteBodyRuns(runs, marks2) {
5894
+ if (!runs.length) return [marks2.open + marks2.close];
5680
5895
  const last = runs.length - 1;
5681
5896
  return runs.map((run, index) => {
5682
- const before = index === 0 ? '"' : "", after = index === last ? '"' : "";
5897
+ const before = index === 0 ? marks2.open : "", after = index === last ? marks2.close : "";
5683
5898
  return typeof run === "string" ? before + run + after : { ...run, text: before + run.text + after };
5684
5899
  });
5685
5900
  }
@@ -5714,23 +5929,24 @@ function layoutQuote(value, box, options = {}) {
5714
5929
  for (const field of ["attribution", "role", "source"]) {
5715
5930
  const text2 = quote[field];
5716
5931
  if (!text2) continue;
5717
- if (footer) footer += field === "role" ? "\n" : " - ";
5932
+ if (footer) footer += field === "role" ? "\n" : QUOTE_SOURCE_SEPARATOR;
5718
5933
  footerSources.push(source(`${path}.${field}`, text2, footer.length));
5719
5934
  footer += text2;
5720
5935
  }
5721
5936
  const hasPhoto = photoValue !== void 0, hasFooter = footer !== "", hasBlock = hasPhoto || hasFooter;
5722
5937
  const photoPath = `${path}.photo`, rtl = options.direction === "rtl";
5723
5938
  const bodyPath = shorthand ? path : `${path}.text`;
5939
+ const marks2 = quoteMarksFor(options.language);
5724
5940
  const body = add(
5725
5941
  "body",
5726
- `"${quoteText}"`,
5727
- [source(bodyPath, quoteText, 1)],
5942
+ `${marks2.open}${quoteText}${marks2.close}`,
5943
+ [source(bodyPath, quoteText, marks2.open.length)],
5728
5944
  { x: box.x + 18, y: box.y + 18, width: box.width - 36, height: box.height - (hasBlock ? 94 : 36) },
5729
- 28,
5945
+ QUOTE_NOMINAL_SIZE,
5730
5946
  options.fontFamilies?.accent ?? options.fontFamilies?.heading ?? "sans-serif",
5731
5947
  600,
5732
5948
  bodyPath,
5733
- richBody ? quoteBodyRuns(richBody) : void 0
5949
+ richBody ? quoteBodyRuns(richBody, marks2) : void 0
5734
5950
  );
5735
5951
  const attribution = hasFooter ? add(
5736
5952
  "footer",
@@ -5852,7 +6068,7 @@ function layoutMetric(value, box, options = {}) {
5852
6068
  const sourceValue = metric[role];
5853
6069
  if (sourceValue === void 0) continue;
5854
6070
  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;
6071
+ const nominal = (role === "value" ? 76 : role === "description" ? 20 : role === "trend" ? 18 : 23) * scale2;
5856
6072
  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
6073
  const part = {
5858
6074
  role,
@@ -6167,9 +6383,9 @@ function layoutTimeline(value, box, options = {}) {
6167
6383
  function fitCodeText(text2, box, size, minimum, step, measure, direction) {
6168
6384
  return fitSourceText(text2, box, size, minimum, step, measure, false, true, direction);
6169
6385
  }
6170
- function fitSourceText(text2, box, size, minimum, step, measure, prose = false, snap = true, direction) {
6386
+ function fitSourceText(text2, box, size, minimum, step, measure, prose = false, snap = true, direction, balance = false) {
6171
6387
  const directionAt = direction === "rtl" ? paragraphDirectionAt(text2, "rtl") : void 0;
6172
- const layout = (fontSize) => {
6388
+ const layout = (fontSize, forced) => {
6173
6389
  const tabWidth = measure(" ", fontSize) * 4;
6174
6390
  if (!Number.isFinite(tabWidth) || text2.includes(" ") && tabWidth <= 0) throw new RangeError("Text tabs require a positive finite measured space advance.");
6175
6391
  const measureLine = (start2, end2, record8 = false) => {
@@ -6210,6 +6426,7 @@ function fitSourceText(text2, box, size, minimum, step, measure, prose = false,
6210
6426
  push(a, b, "hard");
6211
6427
  continue;
6212
6428
  }
6429
+ if (forced?.has(a) && end > start && /\S/u.test(text2.slice(start, end))) push(a, a, "soft");
6213
6430
  if (width(start, b) <= box.width + WRAP_TOLERANCE) {
6214
6431
  end = b;
6215
6432
  continue;
@@ -6247,7 +6464,22 @@ function fitSourceText(text2, box, size, minimum, step, measure, prose = false,
6247
6464
  overflow: sourceLines.length * lineHeight > box.height + 0.01 || sourceLines.some((line2) => line2.width > box.width + 0.01)
6248
6465
  };
6249
6466
  };
6250
- return fitAtSizes(layout, size, minimum, step, snap);
6467
+ const balanced = (fontSize) => {
6468
+ const greedy = layout(fontSize);
6469
+ if (!balance || greedy.overflow || greedy.sourceLines.length < 2) return greedy;
6470
+ const widthOf = (start, end) => {
6471
+ let x = 0;
6472
+ for (const [index, segment2] of text2.slice(start, end).split(" ").entries()) {
6473
+ if (index) x = (Math.floor(x / (measure(" ", fontSize) * 4) + 1e-9) + 1) * measure(" ", fontSize) * 4;
6474
+ if (segment2) x += measure(segment2, fontSize);
6475
+ }
6476
+ return x;
6477
+ };
6478
+ const forced = balancedBreaks(text2, greedy.sourceLines.map((line2) => ({ start: line2.start, end: line2.end, hard: line2.boundary !== "soft" })), widthOf, box.width);
6479
+ const result = forced ? layout(fontSize, forced) : greedy;
6480
+ return result.sourceLines.length === greedy.sourceLines.length && !result.overflow ? result : greedy;
6481
+ };
6482
+ return fitAtSizes(balance ? balanced : layout, size, minimum, step, snap);
6251
6483
  }
6252
6484
  function layoutCode(value, box, options = {}) {
6253
6485
  const shorthand = typeof value === "string", code = shorthand ? { source: value } : value;
@@ -6270,7 +6502,7 @@ function layoutCode(value, box, options = {}) {
6270
6502
  sources: generated ? [] : [{ path: partPath, start: 0, end: text2.length }],
6271
6503
  generated,
6272
6504
  box: { ...inner },
6273
- requestedFontSize: gridFontSize((role === "body" ? 18 : 14) * scale2, minimum),
6505
+ requestedFontSize: gridFontSize((role === "body" ? CODE_NOMINAL_SIZE : 14) * scale2, minimum),
6274
6506
  minFontSize: minimum,
6275
6507
  requestedStyle,
6276
6508
  style: resolveTextStyle({ ...requestedStyle }, options.textMeasurement)
@@ -6283,7 +6515,16 @@ function layoutCode(value, box, options = {}) {
6283
6515
  if (!parts.length) add("language", "code", path, true);
6284
6516
  const body = add("body", code.source, shorthand ? path : `${path}.source`), headers = parts.filter((part) => part !== body);
6285
6517
  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;
6518
+ const measureCode = textWidthMeasurer(body.style, options.textMeasurement);
6519
+ const fit = (part, area2, size = part.requestedFontSize, floor = minimum) => {
6520
+ if (!usable2(area2)) return void 0;
6521
+ const wanted = fitCodeText(part.text, area2, size, floor, scale2, textWidthMeasurer(part.style, options.textMeasurement));
6522
+ const wraps = (value2) => value2.sourceLines.filter((line2) => line2.boundary === "soft").length;
6523
+ const legacy = gridFontSize(LEGACY_CODE_NOMINAL_SIZE * scale2, minimum);
6524
+ if (part.role !== "body" || part.requestedFontSize <= legacy || !wraps(wanted)) return wanted;
6525
+ const earlier = fitCodeText(part.text, area2, legacy, floor, scale2, measureCode);
6526
+ return !earlier.overflow && wraps(earlier) < wraps(wanted) ? earlier : wanted;
6527
+ };
6287
6528
  if (usable2(inner)) {
6288
6529
  const combinations = headers.reduce((choices, part) => choices.flatMap((choice) => [.../* @__PURE__ */ new Set([part.requestedFontSize, minimum])].map((size) => [...choice, { part, size }])), [[]]);
6289
6530
  let selected;
@@ -6306,6 +6547,7 @@ function layoutCode(value, box, options = {}) {
6306
6547
  part.box = box2;
6307
6548
  part.fit = fit2;
6308
6549
  }
6550
+ if (selected && !selected.overflow && body.fit) body.box = { ...body.box, height: Math.min(body.box.height, body.fit.lines.length * body.fit.lineHeight) };
6309
6551
  }
6310
6552
  const report = (reason, part, roles, message) => diagnostics.push({ code: "text-overflow", reason, path: part.path, parts: roles, message });
6311
6553
  for (const part of parts) {
@@ -6323,9 +6565,11 @@ function layoutCode(value, box, options = {}) {
6323
6565
  }
6324
6566
  }
6325
6567
  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 };
6568
+ const bottom = body.fit && !body.fit.overflow ? body.box.y + body.box.height + 18 : box.y + box.height;
6569
+ const bounds = { x: box.x, y: box.y, width: box.width, height: Math.min(box.height, Math.max(bottom - box.y, 0)) };
6570
+ return { bounds, algorithm: "code-flow-v1", textMeasurement: options.textMeasurement ? "provided" : "estimated", parts, diagnostics, overflow: diagnostics.length > 0 };
6327
6571
  }
6328
- function fitRichText(input, box, requestedSize = 25, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
6572
+ function fitRichText(input, box, requestedSize = PROSE_NOMINAL_SIZE, minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
6329
6573
  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
6574
  const layout = richTextLayouter(input, box, requestedSize, options, minFontSize);
6331
6575
  return fitAtSizes(layout, requestedSize, richMinimum(input, requestedSize, minFontSize));
@@ -6342,7 +6586,7 @@ function richMinimum(input, requested, minimum) {
6342
6586
  return visible ? floor : minimum;
6343
6587
  }
6344
6588
  function richTextLayouter(input, box, requestedSize, options, minimum = 0) {
6345
- let offset = 0;
6589
+ let offset = 0, lastWidth = () => 0, lastRanges = [];
6346
6590
  const source = input.map((value, runIndex) => {
6347
6591
  const run = typeof value === "string" ? { text: value } : value;
6348
6592
  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 +6600,7 @@ function richTextLayouter(input, box, requestedSize, options, minimum = 0) {
6356
6600
  const whole = source.map((entry) => entry.run.text).join("");
6357
6601
  const paint = (raw) => raw >= minimum - 1e-6 ? gridFontSize(raw, minimum) : snapFontSizeDown(raw);
6358
6602
  const directionAt = options.direction === "rtl" ? paragraphDirectionAt(whole, "rtl") : void 0;
6359
- const layout = (rawFontSize) => {
6603
+ const build = (rawFontSize, forced) => {
6360
6604
  const fontSize = paint(rawFontSize), ratio = rawFontSize / requestedSize;
6361
6605
  const fragments = (start, end) => {
6362
6606
  let x = 0;
@@ -6411,6 +6655,7 @@ function richTextLayouter(input, box, requestedSize, options, minimum = 0) {
6411
6655
  lineEnd = end;
6412
6656
  continue;
6413
6657
  }
6658
+ if (forced?.has(start) && lineEnd > lineStart && /\S/u.test(whole.slice(lineStart, lineEnd))) push(start);
6414
6659
  if (width(lineStart, end) <= box.width + WRAP_TOLERANCE) {
6415
6660
  lineEnd = end;
6416
6661
  continue;
@@ -6454,7 +6699,7 @@ function richTextLayouter(input, box, requestedSize, options, minimum = 0) {
6454
6699
  y += height;
6455
6700
  }
6456
6701
  }
6457
- return {
6702
+ const fit = {
6458
6703
  lines: ranges.map((range) => whole.slice(range.start, range.end)),
6459
6704
  fontSize,
6460
6705
  lineHeight: Math.max(fontSize * 1.22, ...richLines.map((line2) => line2.height)),
@@ -6463,12 +6708,22 @@ function richTextLayouter(input, box, requestedSize, options, minimum = 0) {
6463
6708
  overflow: y > box.height + 0.01 || richLines.some((line2) => line2.width > box.width + 0.01),
6464
6709
  ...directionAt ? { directions: ranges.map((range) => directionAt(range.start)) } : {}
6465
6710
  };
6711
+ lastWidth = width;
6712
+ lastRanges = ranges;
6713
+ return fit;
6714
+ };
6715
+ const layout = (rawFontSize) => {
6716
+ const greedy = build(rawFontSize);
6717
+ if (!options.balance || greedy.overflow || greedy.richLines.length < 2) return greedy;
6718
+ 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);
6719
+ const result = forced ? build(rawFontSize, forced) : greedy;
6720
+ return result.richLines.length === greedy.richLines.length && !result.overflow ? result : greedy;
6466
6721
  };
6467
6722
  return layout;
6468
6723
  }
6469
6724
  var PICTURE_BULLET_SCALE = 0.65;
6470
6725
  function contentNominalSize(field) {
6471
- return field === "items" || field === "bullets" ? 32 : 25;
6726
+ return field === "items" || field === "bullets" ? LIST_NOMINAL_SIZE : field === "table" ? TABLE_NOMINAL_SIZE : PROSE_NOMINAL_SIZE;
6472
6727
  }
6473
6728
  function fitList(input, box, requestedSize = contentNominalSize("items"), minFontSize = 16, options = { style: { fontFamily: "sans-serif", fontWeight: 400 } }) {
6474
6729
  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 +6748,11 @@ function fitList(input, box, requestedSize = contentNominalSize("items"), minFon
6493
6748
  const entries = [], lines = [], directions = [];
6494
6749
  const markerStyles = numbers ? source.map((item) => resolveTextStyle(markerStyleFor(item), options.textMeasurement)) : [];
6495
6750
  const markerWidths = numbers ? numbers.map((number, index) => textWidthMeasurer(markerStyles[index], options.textMeasurement)(number.text, fontSize)) : [];
6751
+ const none = options.bullet === "none" && !numbers;
6496
6752
  const hanging = numbers ? Math.max(fontSize * 1.1, Math.max(0, ...markerWidths) + fontSize * 0.3) : fontSize * 1.1;
6497
6753
  for (const item of source) {
6498
6754
  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);
6755
+ 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
6756
  if (offset + indent >= box.width) overflow = true;
6501
6757
  const textBox = { x, y, width, height: box.height };
6502
6758
  const style = { ...options.style, path: item.textPath };
@@ -6517,7 +6773,7 @@ function fitList(input, box, requestedSize = contentNominalSize("items"), minFon
6517
6773
  }
6518
6774
  overflow ||= text2.overflow || !!description?.overflow;
6519
6775
  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;
6776
+ 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
6777
  entries.push({
6522
6778
  index: item.index,
6523
6779
  level: item.level,
@@ -6531,7 +6787,7 @@ function fitList(input, box, requestedSize = contentNominalSize("items"), minFon
6531
6787
  descriptionBox,
6532
6788
  marker,
6533
6789
  ...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 } } : {}
6790
+ ...options.bulletImage && !number && !none ? { bulletImage: options.bulletImage, bulletBox: { x: rtl ? marker.x - side : marker.x, y: marker.y - side, width: side, height: side } } : {}
6535
6791
  });
6536
6792
  if (item.index < source.length - 1) y += fontSize * 0.28;
6537
6793
  }
@@ -6798,14 +7054,15 @@ function composeSlide(input, options = {}) {
6798
7054
  const minSize = snapFontSizeUp((composition.minFontSize ?? 16) * scale2);
6799
7055
  const rasterPadding = (options.textRasterPadding ?? 1) * scale2;
6800
7056
  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");
7057
+ let subtitleInGroup = true;
7058
+ const alignmentFor = (field) => (field === "title" || (field === "tag" || subtitleInGroup && field === "subtitle") && hints.sources.contentAlignment !== "slide" ? hints.titleAlignment : hints.contentAlignment) ?? (field === "metric" ? "center" : "left");
6803
7059
  const styleFor = (field, path2) => resolveTextStyle({
6804
7060
  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
7061
  fontWeight: field === "title" ? 700 : 400,
6806
7062
  path: path2
6807
7063
  }, options.textMeasurement);
6808
7064
  const listBullet = hints.listBullet !== void 0 ? { value: hints.listBullet, path: hints.paths.listBullet } : void 0;
7065
+ const noListBullet = listBullet?.value === "none";
6809
7066
  const bulletImage = listBullet?.value === "image" ? (() => {
6810
7067
  const resolved = resolveLogo(options.presentation, slide, { shape: "icon", onDark: options.darkBackground, slideIndex: options.slideIndex });
6811
7068
  return resolved ? { source: resolved.source, path: resolved.path } : void 0;
@@ -6816,13 +7073,14 @@ function composeSlide(input, options = {}) {
6816
7073
  const richOptions = (style) => ({ style, textMeasurement: options.textMeasurement, ...options.fontFamilies?.code ? { codeFontFamily: options.fontFamilies.code } : {}, ...citationMarker ? { citationMarker } : {}, ...rtl ? { direction: "rtl" } : {} });
6817
7074
  const fitPlacedText = (field, value, text2, box, size, minimum, path2, explicitAlignment) => {
6818
7075
  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);
7076
+ const balance = field === "title" || field === "subtitle", textOptions = balance ? { ...richOptions(style), balance } : richOptions(style);
7077
+ if (!options.textMeasurement?.outlineBounds) return rich ? fitRichText(value, box, size, minimum, textOptions) : fitText(text2, box, size, minimum, textWidthMeasurer(style, options.textMeasurement), textDirection, balance);
6820
7078
  const alignment = explicitAlignment ?? alignmentFor(field);
6821
- const richLayout = rich ? richTextLayouter(value, box, size, richOptions(style), minimum) : void 0;
7079
+ const richLayout = rich ? richTextLayouter(value, box, size, textOptions, minimum) : void 0;
6822
7080
  const measure = textWidthMeasurer(style, options.textMeasurement), floor = snapFontSizeUp(rich ? richMinimum(value, size, minimum) : minimum), start = Math.max(size, floor);
6823
7081
  for (let trial = 0; trial <= 64; trial++) {
6824
7082
  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);
7083
+ const fit = richLayout ? richLayout(fontSize) : fitText(text2, box, fontSize, fontSize, measure, textDirection, balance);
6826
7084
  const richLines = "richLines" in fit ? fit.richLines : void 0;
6827
7085
  const lines = richLines ? richLines.map((line2) => {
6828
7086
  let outline = null;
@@ -6864,7 +7122,7 @@ function composeSlide(input, options = {}) {
6864
7122
  }
6865
7123
  throw new Error("Text placement did not evaluate its bounded floor trial.");
6866
7124
  };
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);
7125
+ 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
7126
  const path = `slides.${options.slideIndex ?? 0}`;
6869
7127
  const annotationFit = (value, box, requestedSize, minFontSize, fitPath, alignment) => fitPlacedText("text", value, annotationText(value), box, requestedSize, minFontSize, fitPath, alignment);
6870
7128
  const annotationOptions = { scale: scale2, minFontSize: minSize, fit: annotationFit, textStyle: (fitPath) => styleFor("text", fitPath) };
@@ -6873,7 +7131,7 @@ function composeSlide(input, options = {}) {
6873
7131
  const backgroundImage = resolveBackgroundImage(slide, options.presentation, options.themeBackground, width, height, path, mirrorSide, imageDiagnostics);
6874
7132
  const regions = Object.keys(slide).filter((key) => regionParts(key)).sort();
6875
7133
  const assets = record7(options.presentation).assets;
6876
- const imageContext = (placement) => ({ fit: hints.imageFit, scale: scale2, mirrorSide, diagnostics: imageDiagnostics, assets, ...placement ? { placement } : {} });
7134
+ const imageContext = (placement, anchor) => ({ fit: hints.imageFit ?? (!placement && autoComposition() ? "contain" : void 0), scale: scale2, mirrorSide, diagnostics: imageDiagnostics, assets, ...placement ? { placement } : {}, ...anchor ? { anchor } : {} });
6877
7135
  const area2 = { left: 0, top: 0, right: width, bottom: height };
6878
7136
  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
7137
  const placed = [], usedEdges = /* @__PURE__ */ new Set();
@@ -6915,8 +7173,9 @@ function composeSlide(input, options = {}) {
6915
7173
  const flowBlocks = Array.isArray(slide.blocks) ? slide.blocks.filter((_, index) => !placedBlocks.has(index)) : slide.blocks;
6916
7174
  const hasBodyPayload = regions.some((key) => !emptyHost(record7(slide[key]))) || !emptyPayload(flowBlocks) || fields.some((field) => !(placedRoot && field === "image") && !emptyPayload(slide[field]));
6917
7175
  const useTemplate = template !== void 0 && !regions.length;
7176
+ const autoComposition = () => !hasLayoutRecord && !regions.length;
6918
7177
  const isCover = !hasBodyPayload && (useTemplate || !hasLayoutRecord);
6919
- coverGroup = isCover;
7178
+ subtitleInGroup = !(useTemplate && template?.subtitle === true);
6920
7179
  let logo;
6921
7180
  if (isCover && options[LAYOUT_MASTER] !== true) {
6922
7181
  const resolved = resolveLogo(options.presentation, slide, { shape: "full", onDark: options.darkBackground, slideIndex: options.slideIndex });
@@ -6931,7 +7190,7 @@ function composeSlide(input, options = {}) {
6931
7190
  const headingItems = [];
6932
7191
  for (const field of useTemplate ? [] : ["tag", "title", "subtitle"]) {
6933
7192
  if (!slide[field] || Array.isArray(slide[field]) && !annotationText(slide[field])) continue;
6934
- const requested = (field === "title" ? 54 : field === "tag" ? 16 : 25) * scale2;
7193
+ const requested = (field === "title" ? 54 : field === "tag" ? 16 : PROSE_NOMINAL_SIZE) * scale2;
6935
7194
  const maxHeight = Math.max(height * (field === "title" ? 0.26 : field === "subtitle" ? 0.12 : 0.045), minSize * 1.22 + 2 * rasterPadding);
6936
7195
  const box = { x: area2.left + padding, y, width: area2.right - area2.left - padding * 2, height: maxHeight };
6937
7196
  const text2 = fitPlacedText(field, slide[field], annotationText(slide[field]), box, requested, minSize, `${path}.${field}`);
@@ -7008,6 +7267,8 @@ function composeSlide(input, options = {}) {
7008
7267
  const regionsInReadingOrder = visualReadingOrder(regions.map((key) => ({ key, box: regionBox(regionParts(key), contentBox, gap) }))).map((entry) => entry.key);
7009
7268
  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
7269
  const groups = [], flows = [];
7270
+ const peerSets = [];
7271
+ const range = (from, to) => Array.from({ length: to - from }, (_, index) => from + index);
7011
7272
  const decisions = options.explain ? [] : void 0;
7012
7273
  const inheritedSettings = (parent, own3 = {}) => ({
7013
7274
  minFontSize: parent.minFontSize,
@@ -7024,6 +7285,11 @@ function composeSlide(input, options = {}) {
7024
7285
  const frame = acceptedBox(box), padding2 = Math.min(12 * scale2, frame.width / 4, frame.height / 4);
7025
7286
  return acceptedBox({ x: frame.x + padding2, y: frame.y + padding2, width: frame.width - 2 * padding2, height: frame.height - 2 * padding2 });
7026
7287
  };
7288
+ let languageTag = null;
7289
+ const deckLanguage = () => {
7290
+ if (languageTag === null) languageTag = quoteLanguage(options.presentation, options.slideIndex);
7291
+ return languageTag;
7292
+ };
7027
7293
  const measureQuote = (node, box, settings) => layoutQuote(node.value, acceptedBox(box), {
7028
7294
  fontFamilies: options.fontFamilies,
7029
7295
  textMeasurement: options.textMeasurement,
@@ -7031,7 +7297,8 @@ function composeSlide(input, options = {}) {
7031
7297
  minFontSize: settings.minFontSize,
7032
7298
  path: node.path,
7033
7299
  direction: textDirection,
7034
- ...citationMarker ? { citationMarker } : {}
7300
+ ...citationMarker ? { citationMarker } : {},
7301
+ ...deckLanguage() !== void 0 ? { language: deckLanguage() } : {}
7035
7302
  });
7036
7303
  const measureCode = (node, box, settings) => layoutCode(node.value, acceptedBox(box), {
7037
7304
  fontFamilies: options.fontFamilies,
@@ -7060,14 +7327,21 @@ function composeSlide(input, options = {}) {
7060
7327
  direction: textDirection
7061
7328
  });
7062
7329
  const captionOf = (node, box) => node.caption !== void 0 && node.captionPath ? layoutCaption(node.caption, box, node.captionPath, annotationOptions) : void 0;
7330
+ const wholePictureAspect = (node) => {
7331
+ const host = node.host;
7332
+ if (node.field !== "image" || !host || !autoComposition() || host.aspectRatio !== void 0 || host.shape === "circle") return void 0;
7333
+ const fit = IMAGE_FITS.includes(host.fit) ? host.fit : hints.imageFit ?? "contain";
7334
+ return fit === "contain" ? intrinsicImageAspect(host.image, assets) : void 0;
7335
+ };
7063
7336
  const leafScore = (node, box, settings, penalties) => {
7064
7337
  box = payloadBox(box);
7065
7338
  const captioned = captionOf(node, box);
7066
7339
  if (captioned) box = captioned.mediaBox;
7067
7340
  const text2 = contentText(node.field, node.value);
7068
- let score = Math.abs(Math.log(box.width / box.height / 1.6)) + (captioned?.overflow ? 1e3 : 0);
7341
+ const proportions = Math.abs(Math.log(box.width / box.height / (wholePictureAspect(node) ?? 1.6)));
7342
+ let score = proportions + (captioned?.overflow ? 1e3 : 0);
7069
7343
  if (penalties) {
7070
- penalties.cellProportions += Math.abs(Math.log(box.width / box.height / 1.6));
7344
+ penalties.cellProportions += proportions;
7071
7345
  if (captioned?.overflow) penalties.textOverflow += 1e3;
7072
7346
  }
7073
7347
  if (node.field === "quote" || node.field === "code" || node.field === "metric" || node.field === "timeline") {
@@ -7079,8 +7353,9 @@ function composeSlide(input, options = {}) {
7079
7353
  penalties.textOverflow += internal.overflow ? 1e3 : 0;
7080
7354
  }
7081
7355
  } 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;
7356
+ const prose = proseMeasure(node, box.width);
7357
+ const fit = fitContent(node.field, node.value, text2, { ...box, width: prose.width }, prose.size, snapFontSizeUp((settings.minFontSize ?? 16) * scale2), node.path, node.payload.numbering);
7358
+ const reduction = Math.max(0, snapFontSizeDown(prose.size) - fit.fontSize) / scale2;
7084
7359
  score += reduction + (fit.overflow ? 1e3 : 0);
7085
7360
  if (penalties) {
7086
7361
  penalties.fontReduction += reduction;
@@ -7109,6 +7384,74 @@ function composeSlide(input, options = {}) {
7109
7384
  const boxes = gridBoxes(slotCount, area3, cols, (own3.gap ?? 1 / 30) * Math.min(box.width, box.height), own3.weights ?? [], modeFor(own3) === "column");
7110
7385
  return node.children.reduce((score, child, index) => score + scoreNode(child, boxes[index], own3, penalties), 0);
7111
7386
  };
7387
+ const contentSized = /* @__PURE__ */ new WeakSet();
7388
+ const stackedBoxes = (nodes, area3, requestedGap, settings, cards = hasCards, anchor = "top") => {
7389
+ const between = Math.max(0, Math.min(requestedGap, area3.width / 2, area3.height / (nodes.length * 2)));
7390
+ const space = Math.max(0, area3.height - between * (nodes.length - 1)), share = space / nodes.length;
7391
+ const elastic = nodes.map((node) => !measuredNode(node)), stretch = elastic.filter(Boolean).length;
7392
+ const needs = nodes.map((node, index) => elastic[index] ? share : naturalHeight(node, area3.width, cards, void 0, settings));
7393
+ const floors = nodes.map((node, index) => elastic[index] ? share : naturalHeight(node, area3.width, cards, void 0, settings, true));
7394
+ let heights = shareHeights(needs, floors, space);
7395
+ const textual = nodes.every((node) => !node.children && node.caption === void 0 && (node.field === "text" || node.field === "items" || node.field === "bullets"));
7396
+ if (textual && nodes.length > 1 && needs.reduce((sum, size) => sum + size, 0) > space + 1e-6) {
7397
+ const floor = snapFontSizeUp((settings.minFontSize ?? 16) * scale2), start = Math.max(...nodes.map((node) => proseMeasure(node, area3.width).size));
7398
+ for (let size = start; ; size -= scale2) {
7399
+ const trial = Math.max(floor, snapFontSizeDown(size));
7400
+ const natural = nodes.map((node) => naturalHeight(node, area3.width, cards, void 0, settings, false, Math.min(trial, proseMeasure(node, area3.width).size)));
7401
+ if (natural.reduce((sum, height2) => sum + height2, 0) <= space + 1e-6) {
7402
+ heights = natural;
7403
+ break;
7404
+ }
7405
+ if (trial <= floor) break;
7406
+ }
7407
+ }
7408
+ if (stretch && heights.some((size, index) => elastic[index] && size < share - 1e-9)) {
7409
+ const measured = nodes.flatMap((_, index) => elastic[index] ? [] : [index]);
7410
+ const rest = shareHeights(measured.map((index) => needs[index]), measured.map((index) => floors[index]), space - stretch * share);
7411
+ heights = nodes.map((_, index) => elastic[index] ? share : rest[measured.indexOf(index)]);
7412
+ }
7413
+ const spare = space - heights.reduce((sum, size) => sum + size, 0);
7414
+ if (stretch && spare > 0) heights = heights.map((size, index) => elastic[index] ? size + spare / stretch : size);
7415
+ const used = heights.reduce((sum, size) => sum + size, 0) + between * (nodes.length - 1);
7416
+ nodes.forEach((node, index) => {
7417
+ if (!node.children && !elastic[index] && heights[index] >= needs[index] - 1e-6) contentSized.add(node);
7418
+ });
7419
+ let y2 = area3.y + Math.max(0, anchor === "middle" ? (area3.height - used) / 2 : anchor === "bottom" ? area3.height - used : 0);
7420
+ return heights.map((size) => {
7421
+ const box = { x: area3.x, y: y2, width: area3.width, height: size };
7422
+ y2 += size + between;
7423
+ return box;
7424
+ });
7425
+ };
7426
+ const WEIGHTED_DEMAND_RATIO = 2.5, WEIGHTED_SHARES = [0.55, 0.6, 0.65, 0.7];
7427
+ const autoRowWeights = (nodes, count, cols, area3, rowGap, settings) => {
7428
+ if (!autoComposition() || settings.weights?.length || count < 2 || count > 3 || cols !== count || nodes.length !== count) return void 0;
7429
+ const equal = gridGeometry(count, area3, cols, rowGap, [], false).boxes;
7430
+ const wholeWidths = nodes.map((node, index) => {
7431
+ const aspect = node.caption === void 0 ? wholePictureAspect(node) : void 0;
7432
+ return aspect === void 0 ? void 0 : Math.min(equal[index].width, equal[index].height * aspect);
7433
+ });
7434
+ const media = wholeWidths.flatMap((width2, index) => width2 !== void 0 && width2 < equal[index].width - scale2 ? [index] : []);
7435
+ const others = nodes.flatMap((_, index) => wholeWidths[index] === void 0 ? [index] : []);
7436
+ if (media.length && others.length) {
7437
+ const freed = media.reduce((sum, index) => sum + equal[index].width - wholeWidths[index], 0);
7438
+ return nodes.map((_, index) => media.includes(index) ? wholeWidths[index] : equal[index].width + freed / others.length);
7439
+ }
7440
+ const prose = (node) => !node.children && node.caption === void 0 && (node.field === "text" || node.field === "items" || node.field === "bullets");
7441
+ if (count !== 2 || !nodes.every(prose)) return void 0;
7442
+ const need = (node, width2) => naturalHeight(node, width2, hasCards, void 0, settings);
7443
+ const heights = nodes.map((node, index) => need(node, equal[index].width));
7444
+ const long = heights[0] >= heights[1] ? 0 : 1, short = 1 - long;
7445
+ if (heights[long] < WEIGHTED_DEMAND_RATIO * heights[short]) return void 0;
7446
+ const total = equal[0].width + equal[1].width, line2 = contentNominalSize(nodes[short].field) * scale2 * 1.3;
7447
+ let best;
7448
+ for (const share of WEIGHTED_SHARES) {
7449
+ const longHeight = need(nodes[long], total * share), shortHeight = need(nodes[short], total * (1 - share));
7450
+ if (shortHeight > heights[short] + line2 || longHeight > heights[long] - line2 * 0.5) continue;
7451
+ if (!best || longHeight < best.height - 1e-6) best = { share, height: longHeight };
7452
+ }
7453
+ return best ? long === 0 ? [best.share, 1 - best.share] : [1 - best.share, best.share] : void 0;
7454
+ };
7112
7455
  const arrange = (nodes, area3, settings, reserved2 = 0, gapOverride, containerPath = path, recorded = true) => {
7113
7456
  const count = Math.max(nodes.length, reserved2);
7114
7457
  if (!count) return;
@@ -7116,7 +7459,12 @@ function composeSlide(input, options = {}) {
7116
7459
  const actualGap = gapOverride ?? (settings === rootSettings ? gap : localGap);
7117
7460
  let cols = defaultColumns(count, area3, settings);
7118
7461
  const hasRegions = nodes.some((node) => node.region);
7462
+ if (!hasRegions && (mode === "auto" || nodes.length === 1)) for (const node of nodes) {
7463
+ node.auto = true;
7464
+ node.lone = nodes.length === 1 && reserved2 === 0;
7465
+ }
7119
7466
  const candidates = decisions && recorded ? [] : void 0;
7467
+ const stacks = !hasRegions && count === nodes.length && count > 1 && !(mode === "column" && settings.weights?.length);
7120
7468
  if (mode === "auto" && !hasRegions) {
7121
7469
  let best = Infinity;
7122
7470
  for (let candidate = 1; candidate <= Math.min(count, settings.columns ?? 6); candidate++) {
@@ -7138,14 +7486,24 @@ function composeSlide(input, options = {}) {
7138
7486
  ...hasRegions ? {} : { selectedColumns: cols },
7139
7487
  candidates: candidates ?? []
7140
7488
  });
7141
- const grid = gridGeometry(count, area3, cols, actualGap, settings.weights ?? [], mode === "column");
7489
+ const grid = gridGeometry(count, area3, cols, actualGap, (mode === "auto" && !hasRegions ? autoRowWeights(nodes, count, cols, area3, actualGap, settings) : void 0) ?? settings.weights ?? [], mode === "column");
7490
+ if (cols === 1 && stacks) {
7491
+ grid.boxes = stackedBoxes(nodes, area3, actualGap, settings);
7492
+ grid.rows = grid.boxes.map((box) => ({ offset: box.y - area3.y, size: box.height }));
7493
+ }
7142
7494
  const mirrored = (box) => rtl ? { ...box, x: area3.x + area3.width - (box.x - area3.x) - box.width } : box;
7143
7495
  const boxes = grid.boxes.map(mirrored);
7144
7496
  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 });
7497
+ const rowItems = /* @__PURE__ */ new Map(), stack = [];
7145
7498
  nodes.forEach((node, index) => {
7146
7499
  const box = node.region ? mirrored(regionBox(node.region, area3, actualGap)) : boxes[index];
7500
+ const first = items.length;
7147
7501
  placeNode(node, box, settings);
7502
+ if (!hasRegions && cols > 1 && !node.children) rowItems.set(Math.floor(index / cols), [...rowItems.get(Math.floor(index / cols)) ?? [], ...range(first, items.length)]);
7503
+ else if (!hasRegions && cols === 1 && stacks && !node.children) stack.push(...range(first, items.length));
7148
7504
  });
7505
+ for (const set2 of rowItems.values()) peerSets.push(set2);
7506
+ if (stack.length > 1) peerSets.push(stack);
7149
7507
  };
7150
7508
  let regionTag, templateCell = false;
7151
7509
  const composeChart = (node, box, settings) => {
@@ -7153,7 +7511,15 @@ function composeSlide(input, options = {}) {
7153
7511
  const valueAxis = chartValueAxis(node.value, resolved, { width: box.width, height: box.height, scale: scale2, minFontSize: settings.minFontSize });
7154
7512
  return valueAxis ? { valueAxis } : void 0;
7155
7513
  };
7156
- const placeNode = (node, box, settings, cards = hasCards, listColumns, metricReferenceHeight) => {
7514
+ const VIDEO_ASPECT = 16 / 9, CHART_MAX_ASPECT_RATIO = 3 / 4;
7515
+ const settleCaption = (caption, frame, anchor) => {
7516
+ if (caption.position === "above" || anchor !== "top") return;
7517
+ const target = round2(frame.y + frame.height + caption.fontSize * 0.4), dy = target - caption.box.y;
7518
+ if (dy >= -0.01) return;
7519
+ caption.box = { ...caption.box, y: target };
7520
+ caption.fit = translateTextFit(caption.fit, dy);
7521
+ };
7522
+ const placeNode = (node, box, settings, cards = hasCards, listColumns, metricReferenceHeight, anchor = "top") => {
7157
7523
  if (node.children) {
7158
7524
  const own3 = inheritedSettings(settings, node.composition), inner = inset(box, own3);
7159
7525
  if (!node.synthetic) groups.push({ path: node.path, box, contentBox: inner, composition: own3 });
@@ -7175,8 +7541,24 @@ function composeSlide(input, options = {}) {
7175
7541
  const timelineLayout = node.field === "timeline" ? measureTimeline(node, box, settings) : void 0;
7176
7542
  const internal = quoteLayout ?? codeLayout ?? metricLayout ?? timelineLayout, body = internal?.parts.find((part) => part.role === "body" || part.role === "value");
7177
7543
  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;
7544
+ const floorSize = snapFontSizeUp((settings.minFontSize ?? 16) * scale2);
7545
+ const prose = textValue !== void 0 && !internal ? proseMeasure(node, box.width) : void 0, wholeBox = box;
7546
+ if (prose && prose.width < box.width - 0.5) {
7547
+ const side = alignmentFor(node.field), atEnd = rtl ? side === "left" : side === "right";
7548
+ box = { ...box, x: side === "center" ? box.x + (box.width - prose.width) / 2 : atEnd ? box.x + box.width - prose.width : box.x, width: prose.width };
7549
+ }
7550
+ 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;
7551
+ if (prose && box !== wholeBox && text2 && textValue !== void 0) {
7552
+ const wide = fitContent(node.field, node.value, textValue, wholeBox, prose.size, floorSize, node.path, node.payload.numbering);
7553
+ if (wide.fontSize > text2.fontSize + 1e-9 || text2.overflow && !wide.overflow) {
7554
+ text2 = wide;
7555
+ box = wholeBox;
7556
+ }
7557
+ }
7558
+ if (node.field === "video") box = roundBox(containedBox(box, VIDEO_ASPECT, anchor));
7559
+ 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 });
7560
+ const image = node.field === "image" ? composeImage(node.host ?? {}, node.hostPath ?? node.path, box, box, imageContext(void 0, anchor)) : void 0;
7561
+ if (caption) settleCaption(caption, image?.box ?? box, anchor);
7180
7562
  const chart = node.field === "chart" ? composeChart(node, box, settings) : void 0;
7181
7563
  items.push({
7182
7564
  path: node.path,
@@ -7184,7 +7566,7 @@ function composeSlide(input, options = {}) {
7184
7566
  type: node.type,
7185
7567
  value: node.value,
7186
7568
  payload: node.payload,
7187
- box: internal ? acceptedBox(box) : box,
7569
+ box: codeLayout && !codeLayout.overflow ? { ...acceptedBox(box), height: Math.min(acceptedBox(box).height, round2(codeLayout.bounds.height)) } : internal ? acceptedBox(box) : box,
7188
7570
  ...image ? { image } : {},
7189
7571
  ...chart ? { chart } : {},
7190
7572
  ...frameBox ? { frameBox } : {},
@@ -7201,7 +7583,7 @@ function composeSlide(input, options = {}) {
7201
7583
  ...regionTag !== void 0 ? { region: regionTag } : {},
7202
7584
  ...columns2 && columns2.length > 1 ? { listColumns: columns2 } : {}
7203
7585
  });
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." });
7586
+ 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
7587
  };
7206
7588
  const listColumnsFor = (node, box, settings, regionMode) => {
7207
7589
  if (node.field !== "items" && node.field !== "bullets") return void 0;
@@ -7212,7 +7594,7 @@ function composeSlide(input, options = {}) {
7212
7594
  if (mode === void 0 || mode === 1) return void 0;
7213
7595
  const start = contentNominalSize(node.field) * scale2, floor = snapFontSizeUp((settings.minFontSize ?? 16) * scale2);
7214
7596
  const numbering = node.payload.numbering;
7215
- const listOptions = { ...richOptions(styleFor(node.field, node.path)), ...numbering !== void 0 ? { numbering } : bulletImage ? { bulletImage } : {} };
7597
+ const listOptions = { ...richOptions(styleFor(node.field, node.path)), ...numbering !== void 0 ? { numbering } : bulletImage ? { bulletImage } : noListBullet ? { bullet: "none" } : {} };
7216
7598
  const columnGap = (count2) => Math.min(gap, box.width / (count2 * 2));
7217
7599
  const columnBoxes = (count2) => {
7218
7600
  const between = columnGap(count2), columnWidth = (box.width - between * (count2 - 1)) / count2;
@@ -7248,7 +7630,7 @@ function composeSlide(input, options = {}) {
7248
7630
  const size = Math.min(...shrunk.map((column) => column.text.fontSize));
7249
7631
  return shrunk.map((column) => ({ ...column, text: renumber(fitList(chunk(column.start, column.end), column.box, size, size, listOptions), column.start) }));
7250
7632
  };
7251
- const LONG_LINES = 6, LONG_CODE_LINES = 12, startSize = 25 * scale2;
7633
+ const LONG_LINES = 6, LONG_CODE_LINES = 12, startSize = PROSE_NOMINAL_SIZE * scale2;
7252
7634
  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
7635
  const isLong = (node, cellWidth) => {
7254
7636
  if (node.children) return node.children.some((child) => isLong(child, cellWidth));
@@ -7272,22 +7654,37 @@ function composeSlide(input, options = {}) {
7272
7654
  if (node.field === "code") return String(typeof node.value === "string" ? node.value : record7(node.value).source ?? "").split("\n").length > LONG_CODE_LINES;
7273
7655
  return false;
7274
7656
  };
7275
- const naturalHeight = (node, cellWidth, cards, metricReferenceHeight, naturalSettings = composition) => {
7657
+ const proseMeasure = (node, cellWidth) => {
7658
+ const role = contentNominalSize(node.field) * scale2;
7659
+ if (node.field !== "text" || node.children || !node.auto) return { size: role, width: cellWidth };
7660
+ const linesAt = (width2, size) => estimatedLines(node.value, width2, size);
7661
+ const measureAt = (size) => Math.min(cellWidth, PROSE_MEASURE_CHARS * AVERAGE_CHARACTER_EM * size);
7662
+ const lead = LEAD_NOMINAL_SIZE * scale2;
7663
+ if (node.lone && lead > role && linesAt(measureAt(lead), lead) <= 2) return { size: lead, width: linesAt(cellWidth, lead) <= 1 ? cellWidth : measureAt(lead) };
7664
+ return { size: role, width: linesAt(cellWidth, role) <= 1 ? cellWidth : measureAt(role) };
7665
+ };
7666
+ const MEASURED_FIELDS = /* @__PURE__ */ new Set(["text", "items", "bullets", "quote", "metric", "code"]);
7667
+ const measuredNode = (node) => node.children ? node.children.length > 0 && node.children.every(measuredNode) : MEASURED_FIELDS.has(node.field);
7668
+ const naturalHeight = (node, cellWidth, cards, metricReferenceHeight, naturalSettings = composition, atFloor = false, sizeOverride) => {
7276
7669
  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;
7670
+ 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;
7671
+ const floorSize = snapFontSizeUp((naturalSettings.minFontSize ?? 16) * scale2);
7278
7672
  let content;
7279
7673
  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;
7674
+ const prose = proseMeasure(node, inner), size = sizeOverride ?? (atFloor ? floorSize : prose.size);
7675
+ 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);
7676
+ content = Math.max("height" in fit && typeof fit.height === "number" ? fit.height : fit.lines.length * fit.lineHeight, fit.placement?.height ?? 0);
7283
7677
  } 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;
7678
+ else if (node.field === "code") {
7679
+ const code = measureCode(node, { x: 0, y: 0, width: inner, height: 1e6 }, naturalSettings), source = code.parts.find((part) => part.role === "body");
7680
+ content = source?.fit ? source.box.y + source.fit.lines.length * source.fit.lineHeight + 18 : 1e6;
7681
+ } 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
7682
  else if (node.field === "timeline") content = 170 * scale2;
7287
7683
  else if (node.field === "table") {
7288
7684
  const value = record7(node.value);
7289
7685
  content = ((Array.isArray(value.rows) ? value.rows.length : 4) + 1) * 36 * scale2;
7290
7686
  } else content = inner / 1.6;
7687
+ if (atFloor && !(node.field === "text" || node.field === "items" || node.field === "bullets")) content *= Math.min(1, floorSize / startSize);
7291
7688
  return content + (node.caption !== void 0 ? 32 * scale2 : 0) + pad;
7292
7689
  };
7293
7690
  const templateRegions = [], headingAreas = [];
@@ -7302,6 +7699,13 @@ function composeSlide(input, options = {}) {
7302
7699
  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
7700
  };
7304
7701
  const contents = new Map(binding.regions.map((entry) => [entry.name, [...entry.blocks, ...entry.overflow].map(unitOf)]));
7702
+ for (const region of plan2.regions) {
7703
+ const units = contents.get(region.name) ?? [];
7704
+ if (region.flow === "auto") for (const unit of units) {
7705
+ unit.auto = true;
7706
+ unit.lone = units.length === 1;
7707
+ }
7708
+ }
7305
7709
  const BELOW = "(below)";
7306
7710
  const belowUnits = binding.below.map(unitOf);
7307
7711
  const belowRegion = { name: BELOW, accepts: [], role: "supporting", flow: "auto", max: belowUnits.length, bleed: false, listColumns: 1, anchor: "top", empty: "collapse" };
@@ -7340,7 +7744,7 @@ function composeSlide(input, options = {}) {
7340
7744
  const list = [];
7341
7745
  let top = y2;
7342
7746
  for (const field of fields2) {
7343
- const requested = (field === "title" ? 54 : field === "tag" ? 16 : 25) * scale2;
7747
+ const requested = (field === "title" ? 54 : field === "tag" ? 16 : PROSE_NOMINAL_SIZE) * scale2;
7344
7748
  const maxHeight = Math.max(height * (field === "title" ? 0.26 : field === "subtitle" ? 0.12 : 0.045), minSize * 1.22 + 2 * rasterPadding);
7345
7749
  const box = { x, y: top, width: areaWidth, height: maxHeight };
7346
7750
  const text2 = fitPlacedText(field, slide[field], annotationText(slide[field]), box, requested, minSize, `${path}.${field}`);
@@ -7361,33 +7765,35 @@ function composeSlide(input, options = {}) {
7361
7765
  const cellWidth = (cell.width - gap2 * (shape.columns - 1)) / shape.columns - (cards ? 2 * Math.min(12 * scale2, cell.width / 4) : 0);
7362
7766
  return units.some((unit) => isLong(unit, cellWidth)) ? "column" : "grid";
7363
7767
  };
7364
- const regionNeed = (name, areaWidth) => {
7768
+ const regionNeed = (name, areaWidth, atFloor = false) => {
7365
7769
  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;
7770
+ 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
7771
  const shape = flowShape(arrangementOf(region, units, { width: areaWidth, height: areaWidth / 4 }), units.length, true);
7368
7772
  const cellWidth = (areaWidth - gap2 * (shape.columns - 1)) / shape.columns, cards = hasCards && !region.bleed;
7369
7773
  let total = 0, index = 0;
7370
7774
  for (const count of shape.perRow) {
7371
7775
  let row = 0;
7372
- for (let k = 0; k < count; k++) row = Math.max(row, naturalHeight(units[index++], cellWidth, cards));
7776
+ for (let k = 0; k < count; k++) row = Math.max(row, naturalHeight(units[index++], cellWidth, cards, void 0, composition, atFloor));
7373
7777
  total += row;
7374
7778
  }
7375
7779
  return total + gap2 * Math.max(0, shape.rows - 1);
7376
7780
  };
7377
7781
  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
7782
  const demands = rowSizes.map((size, row) => {
7379
- let need = 0, heading = false, below = false;
7783
+ let need = 0, floor = 0, heading = false, below = false;
7380
7784
  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;
7785
+ const autoSpan = size === "auto" && rect.rowSpan > 1 && rect.row === row && rowSizes.slice(rect.row, rect.row + rect.rowSpan).every((entry) => entry === "auto");
7786
+ if (size === "auto" ? rect.row !== row || rect.rowSpan !== 1 && !autoSpan : row < rect.row || row >= rect.row + rect.rowSpan) continue;
7382
7787
  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;
7788
+ const value = areaNeed(name, rect) / (autoSpan ? 1 : rect.rowSpan);
7789
+ if (size === "auto" && name !== "title" && name !== "subtitle") floor = Math.max(floor, regionNeed(name, spanWidth(rect), true));
7384
7790
  if (value > need) {
7385
7791
  need = value;
7386
7792
  heading = name === "title" || name === "subtitle";
7387
7793
  below = name === BELOW;
7388
7794
  }
7389
7795
  }
7390
- return { size, need, kind: heading ? "heading" : below ? "below" : "body" };
7796
+ return { size, need, ...size === "auto" && !heading ? { floor } : {}, kind: heading ? "heading" : below ? "below" : "body" };
7391
7797
  });
7392
7798
  const heights = allocateRows(demands, frame.height, gap2);
7393
7799
  const rowY = [];
@@ -7395,9 +7801,11 @@ function composeSlide(input, options = {}) {
7395
7801
  rowY.push(size > 0 || !heights.slice(0, row).some((above) => above > 0) ? y2 : y2 - gap2);
7396
7802
  return size > 0 ? y2 + size + gap2 : y2;
7397
7803
  }, 0);
7804
+ const stackUsed = heights.reduce((sum, size) => sum + size, 0) + gap2 * Math.max(0, heights.filter((size) => size > 0).length - 1);
7805
+ const stackShift = plan2.stack === void 0 ? 0 : Math.max(0, frame.height - stackUsed) * (plan2.stack === "middle" ? 0.5 : 1);
7398
7806
  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) };
7807
+ const last = rect.row + rect.rowSpan - 1, y2 = frame.y + stackShift + rowY[rect.row];
7808
+ 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
7809
  };
7402
7810
  const flip = hints.mirror === true !== rtl;
7403
7811
  const physical = (cell) => flip ? { ...cell, x: frame.x + frame.width - (cell.x - frame.x) - cell.width } : cell;
@@ -7454,18 +7862,8 @@ function composeSlide(input, options = {}) {
7454
7862
  const arrangement = arrangementOf(region, units, cell);
7455
7863
  let boxes = flowCells(flowShape(arrangement, units.length, cell.width >= cell.height), cell, gap2).cells;
7456
7864
  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
- }
7865
+ const stacked = arrangement === "column" && !(units.length === 1 && region.anchor === "top");
7866
+ if (stacked) boxes = stackedBoxes(units, cell, gap2, settings, cards, region.anchor);
7469
7867
  if ((arrangement === "grid" || arrangement === "row") && region.anchor !== "top") {
7470
7868
  const naturals = boxes.map((box, index) => {
7471
7869
  const unit = units[index];
@@ -7480,22 +7878,44 @@ function composeSlide(input, options = {}) {
7480
7878
  });
7481
7879
  }
7482
7880
  if (rtl) boxes = boxes.map((box) => ({ ...box, x: cell.x + cell.width - (box.x - cell.x) - box.width }));
7881
+ const shape = flowShape(arrangement, units.length, cell.width >= cell.height), rowItems = shape.perRow.map(() => []);
7882
+ const rowOf = (index) => {
7883
+ let row = 0;
7884
+ for (let seen = shape.perRow[0] ?? 0; index >= seen; seen += shape.perRow[++row] ?? Infinity) ;
7885
+ return row;
7886
+ };
7887
+ const stack = [];
7483
7888
  units.forEach((unit, index) => {
7484
- placeNode(unit, boxes[index], settings, cards, region.listColumns, region.anchor === "top" ? void 0 : referenceHeights[index]);
7889
+ const first = items.length;
7890
+ placeNode(unit, boxes[index], settings, cards, region.listColumns, region.anchor === "top" || stacked ? void 0 : referenceHeights[index], region.anchor);
7891
+ if (!unit.children) {
7892
+ rowItems[rowOf(index)]?.push(...range(first, items.length));
7893
+ leafItems.push(...range(first, items.length));
7894
+ stack.push(...range(first, items.length));
7895
+ }
7485
7896
  });
7897
+ if (arrangement !== "column") peerSets.push(...rowItems.filter((_, row) => (shape.perRow[row] ?? 0) > 1));
7898
+ else if (stacked && stack.length > 1) peerSets.push(stack);
7486
7899
  return arrangement;
7487
7900
  } finally {
7488
7901
  regionTag = void 0;
7489
7902
  templateCell = false;
7490
7903
  }
7491
7904
  };
7905
+ const bands = /* @__PURE__ */ new Map();
7906
+ let leafItems = [];
7492
7907
  for (const region of plan2.regions) {
7493
7908
  const units = unitsOf(region.name), bound = binding.regions.find((entry) => entry.name === region.name);
7494
7909
  const rect = rects.get(region.name);
7495
7910
  let cell = physical(logical(rect ?? ownRects.get(region.name)));
7496
7911
  const areaBox = region.bleed && rect ? roundBox(cell) : void 0;
7497
7912
  if (region.bleed && rect) cell = bleed(cell);
7913
+ leafItems = [];
7498
7914
  const arrangement = units.length && rect ? placeRegion(region, units, cell) : void 0;
7915
+ if (arrangement && rect) {
7916
+ const band = `${rect.row}:${rect.rowSpan}:${region.role}`;
7917
+ bands.set(band, [...bands.get(band) ?? [], leafItems]);
7918
+ }
7499
7919
  templateRegions.push({
7500
7920
  name: region.name,
7501
7921
  box: roundBox(cell),
@@ -7512,8 +7932,36 @@ function composeSlide(input, options = {}) {
7512
7932
  ...areaBox ? { areaBox } : {}
7513
7933
  });
7514
7934
  }
7935
+ for (const members of bands.values()) if (members.length > 1) peerSets.push(members.flat());
7515
7936
  if (belowUnits.length) placeRegion(belowRegion, belowUnits, physical(logical(rects.get(BELOW))));
7516
7937
  };
7938
+ const sharePeerSizes = () => {
7939
+ const parent = /* @__PURE__ */ new Map();
7940
+ const find = (index) => {
7941
+ let root = index;
7942
+ while (parent.has(root) && parent.get(root) !== root) root = parent.get(root);
7943
+ parent.set(index, root);
7944
+ return root;
7945
+ };
7946
+ for (const set2 of peerSets) for (const index of set2) parent.set(find(index), find(set2[0]));
7947
+ 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;
7948
+ const sets = /* @__PURE__ */ new Map();
7949
+ for (const index of parent.keys()) {
7950
+ const item = items[index], kind2 = item && kindOf(item);
7951
+ if (kind2) sets.set(`${find(index)}:${kind2}`, [...sets.get(`${find(index)}:${kind2}`) ?? [], index]);
7952
+ }
7953
+ for (const members of sets.values()) {
7954
+ if (members.length < 2) continue;
7955
+ const size = Math.min(...members.map((index) => items[index].text.fontSize));
7956
+ for (const index of members) {
7957
+ const item = items[index];
7958
+ if (item.text.fontSize <= size + 1e-9) continue;
7959
+ const text2 = contentText(item.field, item.value) ?? "";
7960
+ const refit = fitContent(item.field, item.value, text2, item.box, size, snapFontSizeUp((item.composition.minFontSize ?? 16) * scale2), item.path, item.payload.numbering);
7961
+ if (!refit.overflow && Math.abs(refit.fontSize - size) < 1e-9) item.text = refit;
7962
+ }
7963
+ }
7964
+ };
7517
7965
  const ownMode = record7(slide.composition).mode;
7518
7966
  const direction = hints.contentDirection;
7519
7967
  const directionMode = direction === "vertical" ? "column" : direction === "horizontal" ? "row" : "auto";
@@ -7529,6 +7977,7 @@ function composeSlide(input, options = {}) {
7529
7977
  const support = { field: "blocks", type: "group", value: rest.map((node) => node.payload), path, payload: {}, children: rest, composition: {}, synthetic: true };
7530
7978
  arrange(firstChart ? [primary, support] : [support, primary], contentBox, rootSettings, 0);
7531
7979
  } else arrange(pending, contentBox, rootSettings, 0);
7980
+ sharePeerSizes();
7532
7981
  if (listBullet?.value === "image" && !bulletImage && items.some((item) => (item.field === "items" || item.field === "bullets") && item.payload.numbering === void 0)) {
7533
7982
  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
7983
  }
@@ -7591,4 +8040,4 @@ function resolveCanvasDimensions(input) {
7591
8040
  return { width, height };
7592
8041
  }
7593
8042
 
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 };
8043
+ 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 };