postext 1.2.1 → 1.2.3

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 (195) hide show
  1. package/dist/__tests__/canvasPageScale.test.d.ts +2 -0
  2. package/dist/__tests__/canvasPageScale.test.d.ts.map +1 -0
  3. package/dist/__tests__/canvasPageScale.test.js +48 -0
  4. package/dist/__tests__/canvasPageScale.test.js.map +1 -0
  5. package/dist/__tests__/columnBalancing.test.js +62 -2
  6. package/dist/__tests__/columnBalancing.test.js.map +1 -1
  7. package/dist/__tests__/columnClip.test.d.ts +2 -0
  8. package/dist/__tests__/columnClip.test.d.ts.map +1 -0
  9. package/dist/__tests__/columnClip.test.js +27 -0
  10. package/dist/__tests__/columnClip.test.js.map +1 -0
  11. package/dist/__tests__/defaults/calloutStyles.test.js +5 -0
  12. package/dist/__tests__/defaults/calloutStyles.test.js.map +1 -1
  13. package/dist/__tests__/defaults/tableStyles.test.d.ts +2 -0
  14. package/dist/__tests__/defaults/tableStyles.test.d.ts.map +1 -0
  15. package/dist/__tests__/defaults/tableStyles.test.js +56 -0
  16. package/dist/__tests__/defaults/tableStyles.test.js.map +1 -0
  17. package/dist/__tests__/design/paintOrder.test.d.ts +2 -0
  18. package/dist/__tests__/design/paintOrder.test.d.ts.map +1 -0
  19. package/dist/__tests__/design/paintOrder.test.js +75 -0
  20. package/dist/__tests__/design/paintOrder.test.js.map +1 -0
  21. package/dist/__tests__/design/slotContainers.test.d.ts +2 -0
  22. package/dist/__tests__/design/slotContainers.test.d.ts.map +1 -0
  23. package/dist/__tests__/design/slotContainers.test.js +70 -0
  24. package/dist/__tests__/design/slotContainers.test.js.map +1 -0
  25. package/dist/__tests__/exports.test.js +12 -0
  26. package/dist/__tests__/exports.test.js.map +1 -1
  27. package/dist/__tests__/html-backend.test.js +23 -0
  28. package/dist/__tests__/html-backend.test.js.map +1 -1
  29. package/dist/__tests__/parts.test.js +89 -0
  30. package/dist/__tests__/parts.test.js.map +1 -1
  31. package/dist/__tests__/pipeline/calloutNested.test.d.ts +2 -0
  32. package/dist/__tests__/pipeline/calloutNested.test.d.ts.map +1 -0
  33. package/dist/__tests__/pipeline/calloutNested.test.js +264 -0
  34. package/dist/__tests__/pipeline/calloutNested.test.js.map +1 -0
  35. package/dist/__tests__/pipeline/calloutOverflow.test.js +39 -3
  36. package/dist/__tests__/pipeline/calloutOverflow.test.js.map +1 -1
  37. package/dist/__tests__/pipeline/calloutSnapToGrid.test.d.ts +2 -0
  38. package/dist/__tests__/pipeline/calloutSnapToGrid.test.d.ts.map +1 -0
  39. package/dist/__tests__/pipeline/calloutSnapToGrid.test.js +72 -0
  40. package/dist/__tests__/pipeline/calloutSnapToGrid.test.js.map +1 -0
  41. package/dist/__tests__/pipeline/calloutTallKeepTogether.test.d.ts +2 -0
  42. package/dist/__tests__/pipeline/calloutTallKeepTogether.test.d.ts.map +1 -0
  43. package/dist/__tests__/pipeline/calloutTallKeepTogether.test.js +166 -0
  44. package/dist/__tests__/pipeline/calloutTallKeepTogether.test.js.map +1 -0
  45. package/dist/__tests__/pipeline/chips.test.d.ts +2 -0
  46. package/dist/__tests__/pipeline/chips.test.d.ts.map +1 -0
  47. package/dist/__tests__/pipeline/chips.test.js +230 -0
  48. package/dist/__tests__/pipeline/chips.test.js.map +1 -0
  49. package/dist/__tests__/pipeline/floatFirstSlot.test.js +53 -0
  50. package/dist/__tests__/pipeline/floatFirstSlot.test.js.map +1 -1
  51. package/dist/__tests__/pipeline/scratchDeepSky.test.d.ts +3 -0
  52. package/dist/__tests__/pipeline/scratchDeepSky.test.d.ts.map +1 -0
  53. package/dist/__tests__/pipeline/scratchDeepSky.test.js +137 -0
  54. package/dist/__tests__/pipeline/scratchDeepSky.test.js.map +1 -0
  55. package/dist/__tests__/pipeline/spanBands.test.js +18 -0
  56. package/dist/__tests__/pipeline/spanBands.test.js.map +1 -1
  57. package/dist/__tests__/pipeline/spanBlocks.test.js +3 -2
  58. package/dist/__tests__/pipeline/spanBlocks.test.js.map +1 -1
  59. package/dist/__tests__/pipeline/tableCellImages.test.js +87 -1
  60. package/dist/__tests__/pipeline/tableCellImages.test.js.map +1 -1
  61. package/dist/__tests__/pipeline/tableStyles.test.d.ts +2 -0
  62. package/dist/__tests__/pipeline/tableStyles.test.d.ts.map +1 -0
  63. package/dist/__tests__/pipeline/tableStyles.test.js +188 -0
  64. package/dist/__tests__/pipeline/tableStyles.test.js.map +1 -0
  65. package/dist/canvas-backend/blockRender.d.ts.map +1 -1
  66. package/dist/canvas-backend/blockRender.js +10 -1
  67. package/dist/canvas-backend/blockRender.js.map +1 -1
  68. package/dist/canvas-backend/chip.d.ts +10 -0
  69. package/dist/canvas-backend/chip.d.ts.map +1 -0
  70. package/dist/canvas-backend/chip.js +49 -0
  71. package/dist/canvas-backend/chip.js.map +1 -0
  72. package/dist/canvas-backend/index.d.ts.map +1 -1
  73. package/dist/canvas-backend/index.js +14 -27
  74. package/dist/canvas-backend/index.js.map +1 -1
  75. package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
  76. package/dist/canvas-backend/renderResourceBlock.js +48 -1
  77. package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
  78. package/dist/columnClip.d.ts +16 -0
  79. package/dist/columnClip.d.ts.map +1 -0
  80. package/dist/columnClip.js +37 -0
  81. package/dist/columnClip.js.map +1 -0
  82. package/dist/defaults/calloutStyles.d.ts +1 -0
  83. package/dist/defaults/calloutStyles.d.ts.map +1 -1
  84. package/dist/defaults/calloutStyles.js +7 -0
  85. package/dist/defaults/calloutStyles.js.map +1 -1
  86. package/dist/defaults/chipStyles.d.ts +28 -0
  87. package/dist/defaults/chipStyles.d.ts.map +1 -0
  88. package/dist/defaults/chipStyles.js +102 -0
  89. package/dist/defaults/chipStyles.js.map +1 -0
  90. package/dist/defaults/index.d.ts +2 -1
  91. package/dist/defaults/index.d.ts.map +1 -1
  92. package/dist/defaults/index.js +18 -2
  93. package/dist/defaults/index.js.map +1 -1
  94. package/dist/defaults/parts.d.ts +1 -0
  95. package/dist/defaults/parts.d.ts.map +1 -1
  96. package/dist/defaults/parts.js +4 -0
  97. package/dist/defaults/parts.js.map +1 -1
  98. package/dist/defaults/shared.d.ts.map +1 -1
  99. package/dist/defaults/shared.js +43 -16
  100. package/dist/defaults/shared.js.map +1 -1
  101. package/dist/defaults/tableStyle.d.ts +17 -1
  102. package/dist/defaults/tableStyle.d.ts.map +1 -1
  103. package/dist/defaults/tableStyle.js +46 -0
  104. package/dist/defaults/tableStyle.js.map +1 -1
  105. package/dist/design/layout.d.ts.map +1 -1
  106. package/dist/design/layout.js +10 -6
  107. package/dist/design/layout.js.map +1 -1
  108. package/dist/html-backend.d.ts.map +1 -1
  109. package/dist/html-backend.js +72 -5
  110. package/dist/html-backend.js.map +1 -1
  111. package/dist/index.d.ts +7 -6
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +4 -3
  114. package/dist/index.js.map +1 -1
  115. package/dist/knuthPlass/richAdapter.d.ts +3 -1
  116. package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
  117. package/dist/knuthPlass/richAdapter.js +7 -4
  118. package/dist/knuthPlass/richAdapter.js.map +1 -1
  119. package/dist/measure/cache.d.ts.map +1 -1
  120. package/dist/measure/cache.js +7 -1
  121. package/dist/measure/cache.js.map +1 -1
  122. package/dist/measure/chipEdges.d.ts +5 -0
  123. package/dist/measure/chipEdges.d.ts.map +1 -0
  124. package/dist/measure/chipEdges.js +20 -0
  125. package/dist/measure/chipEdges.js.map +1 -0
  126. package/dist/measure/rich.d.ts +12 -0
  127. package/dist/measure/rich.d.ts.map +1 -1
  128. package/dist/measure/rich.js +128 -5
  129. package/dist/measure/rich.js.map +1 -1
  130. package/dist/parse/blockParser.d.ts.map +1 -1
  131. package/dist/parse/blockParser.js +10 -7
  132. package/dist/parse/blockParser.js.map +1 -1
  133. package/dist/parse/index.d.ts +3 -3
  134. package/dist/parse/index.d.ts.map +1 -1
  135. package/dist/parse/index.js +1 -1
  136. package/dist/parse/index.js.map +1 -1
  137. package/dist/parse/inlineFormatting.d.ts +28 -0
  138. package/dist/parse/inlineFormatting.d.ts.map +1 -1
  139. package/dist/parse/inlineFormatting.js +52 -0
  140. package/dist/parse/inlineFormatting.js.map +1 -1
  141. package/dist/parse/inlineSnippet.d.ts +2 -1
  142. package/dist/parse/inlineSnippet.d.ts.map +1 -1
  143. package/dist/parse/inlineSnippet.js +6 -4
  144. package/dist/parse/inlineSnippet.js.map +1 -1
  145. package/dist/parse/sourceMapping.d.ts.map +1 -1
  146. package/dist/parse/sourceMapping.js +25 -1
  147. package/dist/parse/sourceMapping.js.map +1 -1
  148. package/dist/parse/types.d.ts +31 -0
  149. package/dist/parse/types.d.ts.map +1 -1
  150. package/dist/pipeline/bandCaps.d.ts.map +1 -1
  151. package/dist/pipeline/bandCaps.js +2 -0
  152. package/dist/pipeline/bandCaps.js.map +1 -1
  153. package/dist/pipeline/build.d.ts.map +1 -1
  154. package/dist/pipeline/build.js +261 -60
  155. package/dist/pipeline/build.js.map +1 -1
  156. package/dist/pipeline/calloutLayout.d.ts +52 -4
  157. package/dist/pipeline/calloutLayout.d.ts.map +1 -1
  158. package/dist/pipeline/calloutLayout.js +161 -26
  159. package/dist/pipeline/calloutLayout.js.map +1 -1
  160. package/dist/pipeline/chips.d.ts +27 -0
  161. package/dist/pipeline/chips.d.ts.map +1 -0
  162. package/dist/pipeline/chips.js +56 -0
  163. package/dist/pipeline/chips.js.map +1 -0
  164. package/dist/pipeline/columnBalancing.d.ts +1 -1
  165. package/dist/pipeline/columnBalancing.d.ts.map +1 -1
  166. package/dist/pipeline/columnBalancing.js +99 -1
  167. package/dist/pipeline/columnBalancing.js.map +1 -1
  168. package/dist/pipeline/config.d.ts.map +1 -1
  169. package/dist/pipeline/config.js +3 -1
  170. package/dist/pipeline/config.js.map +1 -1
  171. package/dist/pipeline/headerFooter.d.ts +0 -4
  172. package/dist/pipeline/headerFooter.d.ts.map +1 -1
  173. package/dist/pipeline/headerFooter.js +42 -15
  174. package/dist/pipeline/headerFooter.js.map +1 -1
  175. package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
  176. package/dist/pipeline/measureContentBlock.js +6 -1
  177. package/dist/pipeline/measureContentBlock.js.map +1 -1
  178. package/dist/pipeline/partPalette.d.ts +6 -0
  179. package/dist/pipeline/partPalette.d.ts.map +1 -0
  180. package/dist/pipeline/partPalette.js +96 -0
  181. package/dist/pipeline/partPalette.js.map +1 -0
  182. package/dist/pipeline/placeholders.d.ts +2 -1
  183. package/dist/pipeline/placeholders.d.ts.map +1 -1
  184. package/dist/pipeline/placeholders.js +11 -1
  185. package/dist/pipeline/placeholders.js.map +1 -1
  186. package/dist/pipeline/resourceLayout.d.ts.map +1 -1
  187. package/dist/pipeline/resourceLayout.js +35 -11
  188. package/dist/pipeline/resourceLayout.js.map +1 -1
  189. package/dist/types.d.ts +141 -6
  190. package/dist/types.d.ts.map +1 -1
  191. package/dist/vdt.d.ts +88 -2
  192. package/dist/vdt.d.ts.map +1 -1
  193. package/dist/vdt.js +17 -0
  194. package/dist/vdt.js.map +1 -1
  195. package/package.json +1 -1
@@ -24,6 +24,7 @@ import { computeFloatPlan, floatedResourceIds, } from './floatPlacement';
24
24
  import { enumerateCurrentPageSlots, measureFloatBand, measureSideStack, columnHasFloatBand, fitsStrict, trueBottom, } from './floatSlots';
25
25
  import { computeHeadingContext, computeResourceNumbering, } from './resourceNumbering';
26
26
  import { defaultResourceTypes } from '../defaults/resourceTypes';
27
+ import { pickTableStyle } from '../defaults/tableStyle';
27
28
  import { buildHeadersAndFooters, measureHeadingAdvancedDesignHeight } from './headerFooter';
28
29
  import { proposeBalanceLines, collectColumnGaps, firstDivergentColumn, gapLinesIn, pageSegments, MAX_BALANCING_PASSES, MAX_BALANCING_PASSES_PER_DOCUMENT, balanceKey } from './columnBalancing';
29
30
  import { applyBandCap, uncapBand, columnBottom, bandCapLines, bandTop, resolveBandCapsGen, drainPasses, resolveTrailingCapsGen, bandCapLinesAroundZone, } from './bandCaps';
@@ -371,6 +372,9 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
371
372
  : undefined;
372
373
  /** Row count of a table resource (0 for anything else). */
373
374
  const tableRowCount = (resourceId) => resourceById.get(resourceId)?.table?.model.rows.length ?? 0;
375
+ /** What a table resource does when taller than the page — its own
376
+ * (named) style's `overflow`. */
377
+ const tableOverflow = (resourceId) => pickTableStyle(resolved, resourceById.get(resourceId)?.table?.styleId).overflow;
374
378
  /** The slice a pending float stands for: the whole resource, or — for the
375
379
  * rest of a split table — its remaining rows, laid out as the closing
376
380
  * slice (no marker; the note). */
@@ -505,7 +509,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
505
509
  * The slice of a table float that fits a fresh band of `avail` px, when
506
510
  * the whole (rest of the) table does not: the leading rows within the
507
511
  * band, cut where `planTableSlice` allows, verified against the band
508
- * geometry and shrunk row by row until it fits. `tableStyle.overflow`
512
+ * geometry and shrunk row by row until it fits. The table style's `overflow`
509
513
  * decides what becomes of the rows left over — a rest float to continue
510
514
  * on the next page (`'split'`), nothing (`'clip'`) — or, for `'hide'`,
511
515
  * that the table is dropped. Returns null for a figure or a table with
@@ -517,7 +521,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
517
521
  const rowCount = tableRowCount(f.resourceId);
518
522
  if (rowCount === 0)
519
523
  return null;
520
- const overflow = resolved.tableStyle.overflow;
524
+ const overflow = tableOverflow(f.resourceId);
521
525
  if (overflow === 'hide')
522
526
  return 'skip';
523
527
  const metrics = tableMetrics(f.resourceId, width, rotated);
@@ -814,7 +818,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
814
818
  // table that clips or hides when too tall keeps to fresh pages.
815
819
  if (position !== 'top' || pageSpan || c.blocks.length > 0)
816
820
  return 'defer';
817
- if (resolved.tableStyle.overflow !== 'split')
821
+ if (tableOverflow(f.resourceId) !== 'split')
818
822
  return 'defer';
819
823
  const split = splitTableFloat(f, width, position, targetCols, page.contentArea, c.availableHeight - (hasBand ? minTextPx : 0), 'strict');
820
824
  if (!split || split === 'none' || split === 'skip')
@@ -1097,7 +1101,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1097
1101
  * block is placed, so a float lands in the first gap after its reference.
1098
1102
  * `nextBlockIdx` is that block: a keep-together box it opens holds the
1099
1103
  * slots that would starve it (see `slotStarvesBox`). */
1100
- const tryPlacePendingFloatsOnCurrentPage = (preferTop = false, nextBlockIdx) => {
1104
+ const tryPlacePendingFloatsOnCurrentPage = (preferTop = false, nextBlockIdx, anyPosition = false) => {
1101
1105
  if (pendingFloats.length === 0)
1102
1106
  return;
1103
1107
  const page = doc.pages[cursor.pageIndex];
@@ -1109,7 +1113,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1109
1113
  }
1110
1114
  const f = pendingFloats[i];
1111
1115
  let r = 'defer';
1112
- let slots = enumerateCurrentPageSlots(page, cursor.columnIndex, f, capKindOf);
1116
+ // `anyPosition`: a figure or table may take a slot its position would
1117
+ // refuse (a head-of-page float offered the foot of the current page);
1118
+ // floated callouts keep their placement.
1119
+ const asked = anyPosition && !f.rotate && !f.callout && f.position !== 'auto' ? { ...f, position: 'auto' } : f;
1120
+ let slots = enumerateCurrentPageSlots(page, cursor.columnIndex, asked, capKindOf);
1113
1121
  // The rest of a table cut on this page only takes the slots after
1114
1122
  // its previous slice in reading order (never the foot of the column
1115
1123
  // before it; a page-span rest waits for the next page).
@@ -1171,6 +1179,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1171
1179
  * float page so the boundary's own page break opens AFTER them. */
1172
1180
  const drainPendingFloats = () => {
1173
1181
  tryPlacePendingFloatsOnCurrentPage();
1182
+ // A float that would otherwise take a page of its own before the
1183
+ // boundary settles for a free slot of the current page — the foot of
1184
+ // its closing columns — whatever position it asked for: a chapter's
1185
+ // last page with room under its text beats a page holding one table.
1186
+ tryPlacePendingFloatsOnCurrentPage(false, undefined, true);
1174
1187
  let guard = 0;
1175
1188
  while ((pendingFloats.length > 0 || pendingSideBoxes.length > 0) && guard++ < 1000) {
1176
1189
  const before = floatsPlaced;
@@ -1307,6 +1320,8 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1307
1320
  const bandCapProposals = new Map();
1308
1321
  const spanPlacedInBand = new Set();
1309
1322
  const bandCapsApplied = new Set();
1323
+ /** Columns of the band each cap was last applied to. */
1324
+ const cappedBandColumns = new Map();
1310
1325
  const enterBand = (contentIndex, part) => {
1311
1326
  const page = doc.pages[cursor.pageIndex];
1312
1327
  const band = currentBand(page, cursor);
@@ -1320,7 +1335,17 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1320
1335
  for (const [spanIndex, cap] of bandCaps) {
1321
1336
  if (cap.startContentIndex !== contentIndex || cap.startPart !== part)
1322
1337
  continue;
1323
- applyBandCap(bandColumns(page, band), cap.lines * baselineGrid, uncappedBottoms, cap.zone, cap.kind === 'trailing');
1338
+ // A cap whose last band took nothing (its opening block is taller
1339
+ // than the cut — a heading's leading over a one-line cap) would cut
1340
+ // the next band just as short, and the flow would open empty pages
1341
+ // forever. Leave the band whole: the cap stays undelivered and the
1342
+ // driver grows or drops it.
1343
+ const prev = cappedBandColumns.get(spanIndex);
1344
+ if (prev && prev.every((c) => c.blocks.length === 0))
1345
+ continue;
1346
+ const cols = bandColumns(page, band);
1347
+ applyBandCap(cols, cap.lines * baselineGrid, uncappedBottoms, cap.zone, cap.kind === 'trailing');
1348
+ cappedBandColumns.set(spanIndex, cols);
1324
1349
  activeCap = { spanIndex, pageIndex: page.index, band };
1325
1350
  bandCapsApplied.add(spanIndex);
1326
1351
  break;
@@ -1411,11 +1436,47 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1411
1436
  tryPlacePendingFloatsOnCurrentPage();
1412
1437
  proposeTrailingCap(boundaryIndex);
1413
1438
  markForcedBreak();
1439
+ hugClosingText();
1414
1440
  drainPendingFloats();
1415
1441
  };
1442
+ /** On the closing page of a chapter nothing follows the page-span floats
1443
+ * set at the foot of its last band: they move up to sit right under the
1444
+ * band's text (one float gap below its grid-rounded bottom, stacked in
1445
+ * their order) instead of leaving a gap between the text and a table at
1446
+ * the page foot. */
1447
+ const hugClosingText = () => {
1448
+ const page = doc.pages[cursor.pageIndex];
1449
+ const floats = page.floats ?? [];
1450
+ // A side column holds its own band under a page-span float: leave it.
1451
+ if (floats.length === 0 || page.partInfo || sideColumns(page).length > 0)
1452
+ return;
1453
+ const cols = bandColumns(page, currentBand(page, cursor)).filter((c) => c.kind !== 'span' && c.kind !== 'side' && c.bbox.height > 0.5);
1454
+ if (cols.length === 0 || !cols.some((c) => c.blocks.length > 0))
1455
+ return;
1456
+ const textBottom = Math.max(...cols.map((c) => c.bbox.y + (c.bbox.height - c.availableHeight)));
1457
+ const spanWidth = page.contentArea.width - 0.5;
1458
+ const below = floats.filter((b) => b.bbox.y >= textBottom - 0.5).sort((a, b) => a.bbox.y - b.bbox.y);
1459
+ let nextY = page.contentArea.y + Math.ceil((textBottom - page.contentArea.y - 0.01) / baselineGrid) * baselineGrid + floatGapPx;
1460
+ for (const b of below) {
1461
+ const rb = b.resourceBlock;
1462
+ // Only resource floats across the page move; anything else below the
1463
+ // text (a fixed or floated box) keeps its place and ends the run.
1464
+ if (!rb || rb.rotation || b.bbox.width < spanWidth)
1465
+ break;
1466
+ const dy = nextY - b.bbox.y;
1467
+ if (dy < -0.5) {
1468
+ b.bbox.y += dy;
1469
+ offsetResourceBlockToAbsolute(rb, 0, dy);
1470
+ }
1471
+ nextY = b.bbox.y + b.bbox.height + floatGapPx;
1472
+ }
1473
+ };
1416
1474
  /** Parity of the page break a closed `:::part` still owes (applied before
1417
1475
  * the next placed block). */
1418
1476
  let pendingPartBreak = null;
1477
+ /** Content index of the closing fence of a part set without a page
1478
+ * (`parts.page: false`): blocks up to it are skipped. */
1479
+ let skipPartUntil = null;
1419
1480
  /** `advanceToNextPageBoundary`, except that an empty part page is left
1420
1481
  * behind too (its opener design is content). No floats are reserved on
1421
1482
  * the page opened this way — parity padding may still follow it. */
@@ -1538,6 +1599,24 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1538
1599
  // A cut inside child `to.child` includes that child (its first
1539
1600
  // `to.line` lines); a cut at a child's head excludes it.
1540
1601
  const toChild = to.line > 0 ? to.child + 1 : to.child;
1602
+ // Nested boxes still open at `from` (their closing markers leading
1603
+ // the range close them): the fragment opens inside them.
1604
+ const open = [];
1605
+ for (let k = 0; k < from.child; k++) {
1606
+ const c = children[k];
1607
+ if (c.containerName !== 'callout')
1608
+ continue;
1609
+ if (c.type === 'containerStart')
1610
+ open.push({ idx: startIdx + 1 + k, block: c });
1611
+ else if (c.type === 'containerEnd')
1612
+ open.pop();
1613
+ }
1614
+ for (let k = from.child; k < children.length && open.length > 0; k++) {
1615
+ const c = children[k];
1616
+ if (c.type !== 'containerEnd' || c.containerId !== open[open.length - 1].block.containerId)
1617
+ break;
1618
+ open.pop();
1619
+ }
1541
1620
  return layoutCallout({
1542
1621
  style,
1543
1622
  attrs: plan.attrs,
@@ -1553,6 +1632,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1553
1632
  paragraphStyleFor: (idx) => paragraphContainers.byBlock[idx]?.style,
1554
1633
  ...(from.line > 0 ? { lineFrom: from.line } : {}),
1555
1634
  ...(to.line > 0 ? { lineTo: to.line } : {}),
1635
+ ...(open.length > 0 ? { openNested: open } : {}),
1556
1636
  mirrored,
1557
1637
  });
1558
1638
  };
@@ -1570,44 +1650,93 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1570
1650
  };
1571
1651
  /** The longest leading fragment of the box from cut `from` on whose box
1572
1652
  * is at most `roomPx` tall. Cuts fall between children or between the
1573
- * lines of a text child, and every fragment keeps at least the style's
1574
- * `splitMinLines` lines on its side of the cut (a figure or a display
1575
- * formula counts as one line). The full layout's geometry ranks the
1576
- * candidates (box bottom = content bottom at the cut + the box's tail
1577
- * below its last child); the deepest candidate that fits is laid out
1578
- * for real and taken when it truly fits. `null` when none does. */
1653
+ * lines of a text child. `splitMinLines` guards text: a cut between
1654
+ * children leaves on each side at least that many text lines or at least
1655
+ * one indivisible block (a figure, table, display formula or nested
1656
+ * box); a cut inside a text child counts every line on its side, a block
1657
+ * as one line, as before. A nested box that may split itself
1658
+ * (`keepTogether: false`, or taller than a full column) offers the cuts
1659
+ * inside it too, by its own `splitMinLines` over its own children; the
1660
+ * fragments on both sides redraw its frame. The full layout's geometry
1661
+ * ranks the candidates (box bottom = content bottom at the cut + the
1662
+ * tails of the boxes closed below it); the deepest candidate that fits
1663
+ * is laid out for real and taken when it truly fits. `null` when none
1664
+ * does. */
1579
1665
  const splitCalloutFragment = (L, from, width, roomPx, frameId, continuation, minLines, mirrored = false) => {
1580
1666
  const full = L.layoutRange(from, L.end, width, frameId, continuation, mirrored);
1581
- const lastChild = full.children[full.children.length - 1];
1582
- if (!lastChild)
1667
+ /** Figures, tables and display formulas: never cut, not text lines. */
1668
+ const indivisible = (c) => c.type === 'resource' || c.type === 'mathDisplay';
1669
+ const unitBottom = (u) => {
1670
+ const b = u.kind === 'block' ? u.block.bbox : u.box.result.frame.bbox;
1671
+ return b.y + b.height;
1672
+ };
1673
+ /** Room a box keeps under its last item (padding, border, icon growth). */
1674
+ const tailOf = (r) => {
1675
+ const last = r.units[r.units.length - 1];
1676
+ return last ? r.frame.bbox.y + r.totalHeight - unitBottom(last) : 0;
1677
+ };
1678
+ if (full.units.length === 0)
1583
1679
  return null;
1584
- const tail = full.totalHeight - (lastChild.bbox.y + lastChild.bbox.height);
1585
- const totalLines = full.children.reduce((n, c) => n + Math.max(1, c.lines.length), 0);
1586
- /** Candidate cuts with the head's content bottom and line count. */
1680
+ const tail = tailOf(full);
1681
+ /** Candidate cuts with the head's content bottom. */
1587
1682
  const candidates = [];
1588
- let linesBefore = 0;
1589
- for (let i = 0; i < full.children.length; i++) {
1590
- const c = full.children[i];
1591
- const k = (c.contentIndex ?? L.childBase) - L.childBase;
1592
- const cuttable = c.type !== 'resource' && c.type !== 'mathDisplay' && c.lines.length > 1;
1593
- // The first laid-out child of a continuation opens after `from.line`
1594
- // lines: cuts inside it are counted from the child's own head.
1595
- const lineBase = k === from.child ? from.line : 0;
1596
- if (cuttable) {
1597
- for (let l = 1; l < c.lines.length; l++) {
1598
- const line = c.lines[l - 1];
1599
- candidates.push({ cut: { child: k, line: lineBase + l }, bottom: line.bbox.y + line.bbox.height, headLines: linesBefore + l });
1600
- }
1601
- }
1602
- linesBefore += Math.max(1, c.lines.length);
1603
- if (i < full.children.length - 1) {
1604
- candidates.push({ cut: { child: k + 1, line: 0 }, bottom: c.bbox.y + c.bbox.height, headLines: linesBefore });
1605
- }
1606
- }
1607
- const min = Math.max(1, minLines);
1683
+ /** Collect the cuts among `units` (one box's items; `below` = the tails
1684
+ * of the nested boxes around them), each side of a cut holding enough
1685
+ * by the box's `min` lines: a block counts as one line, a nested box
1686
+ * as one indivisible block. */
1687
+ const collect = (units, minLines, below) => {
1688
+ const min = Math.max(1, minLines);
1689
+ const lineCount = (u) => (u.kind === 'block' ? Math.max(1, u.block.lines.length) : 1);
1690
+ const isBlock = (u) => u.kind === 'box' || indivisible(u.block);
1691
+ const totalLines = units.reduce((n, u) => n + lineCount(u), 0);
1692
+ const totalBlocks = units.filter(isBlock).length;
1693
+ /** A side of a cut holds enough: `lines` (blocks included) of which
1694
+ * `blocks` are indivisible. Between children, one block suffices or
1695
+ * the text lines alone meet the minimum; inside a text child, the
1696
+ * lines do. */
1697
+ const holds = (lines, blocks, betweenChildren) => betweenChildren ? blocks > 0 || lines - blocks >= min : lines >= min;
1698
+ const bothHold = (lines, blocks, betweenChildren) => holds(lines, blocks, betweenChildren) && holds(totalLines - lines, totalBlocks - blocks, betweenChildren);
1699
+ let linesBefore = 0;
1700
+ let blocksBefore = 0;
1701
+ units.forEach((u, i) => {
1702
+ let next;
1703
+ if (u.kind === 'block') {
1704
+ const c = u.block;
1705
+ const k = (c.contentIndex ?? L.childBase) - L.childBase;
1706
+ next = k + 1;
1707
+ if (!indivisible(c) && c.lines.length > 1) {
1708
+ // The first laid-out child of a continuation opens after
1709
+ // `from.line` lines: cuts inside it are counted from the
1710
+ // child's own head.
1711
+ const lineBase = k === from.child ? from.line : 0;
1712
+ for (let l = 1; l < c.lines.length; l++) {
1713
+ const line = c.lines[l - 1];
1714
+ if (bothHold(linesBefore + l, blocksBefore, false)) {
1715
+ candidates.push({ cut: { child: k, line: lineBase + l }, bottom: line.bbox.y + line.bbox.height + below });
1716
+ }
1717
+ }
1718
+ }
1719
+ }
1720
+ else {
1721
+ const { box } = u;
1722
+ if (box.endIdx !== undefined)
1723
+ next = box.endIdx + 1 - L.childBase;
1724
+ if (!box.style.keepTogether || tallerThanColumn(box.result)) {
1725
+ collect(box.result.units, box.style.splitMinLines, below + tailOf(box.result));
1726
+ }
1727
+ }
1728
+ linesBefore += lineCount(u);
1729
+ if (isBlock(u))
1730
+ blocksBefore++;
1731
+ if (i < units.length - 1 && next !== undefined && bothHold(linesBefore, blocksBefore, true)) {
1732
+ candidates.push({ cut: { child: next, line: 0 }, bottom: unitBottom(u) + below });
1733
+ }
1734
+ });
1735
+ };
1736
+ collect(full.units, minLines, 0);
1608
1737
  const insideGroup = (cut) => L.groups.some(([gs, ge]) => (cut.line === 0 ? gs < cut.child && cut.child <= ge : gs < cut.child && cut.child < ge));
1609
1738
  const viable = candidates
1610
- .filter((c) => c.headLines >= min && totalLines - c.headLines >= min && !insideGroup(c.cut))
1739
+ .filter((c) => !insideGroup(c.cut))
1611
1740
  .sort((a, b) => b.bottom - a.bottom);
1612
1741
  for (const c of viable) {
1613
1742
  if (c.bottom + tail > roomPx + 0.01)
@@ -1621,6 +1750,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1621
1750
  /** Absolute content indices of the children a fragment from cut `from`
1622
1751
  * to cut `to` of `L` touches, for the fragment's source range. */
1623
1752
  const fragmentRange = (L, from, to) => ({ firstChildIdx: L.childBase + from.child, lastChildIdx: L.childBase + (to.line > 0 ? to.child : to.child - 1) });
1753
+ /** The (rest of the) box is taller than an empty, full-height column —
1754
+ * a whole page's content area for a page-span box: no column could
1755
+ * hold it, so even a keep-together box splits (with the rules of a
1756
+ * `keepTogether: false` one) rather than overflow. */
1757
+ const tallerThanColumn = (result) => result.totalHeight > contentArea.height + 0.01;
1624
1758
  const markFragment = (result, part, continued) => {
1625
1759
  if (result.frame.callout) {
1626
1760
  result.frame.callout.part = part;
@@ -1665,7 +1799,8 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1665
1799
  * and the cut columns stay cut (the polish round fills a column ending a
1666
1800
  * line under the cap).
1667
1801
  *
1668
- * Splitting (`keepTogether: false`): a box that does not fit a level (or
1802
+ * Splitting (`keepTogether: false`, or a keep-together box taller than
1803
+ * the page's content area): a box that does not fit a level (or
1669
1804
  * capped, or nearly level — within one grid line) band breaks between
1670
1805
  * its children: the longest fragment that fits closes the page flush
1671
1806
  * with the band bottom, the rest opens the next page in a box without
@@ -1684,7 +1819,6 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1684
1819
  * / bottom of the page's columns, so a span column never overlaps a float.
1685
1820
  */
1686
1821
  const placeCalloutSpan = (startIdx, plan, style, L, firstFrameId) => {
1687
- const splittable = !style.keepTogether;
1688
1822
  const balancingCfg = resolved.headings.balancing;
1689
1823
  const levelBefore = balancingCfg.enabled && balancingCfg.beforeSpan;
1690
1824
  const minLines = resolved.bodyText.avoidWidows ? Math.max(1, resolved.bodyText.widowMinLines) : 1;
@@ -1791,6 +1925,9 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1791
1925
  const continuation = part > 0;
1792
1926
  const layoutAt = (width) => L.layoutRange(from, L.end, width, frameId, continuation, mirroredOf(page));
1793
1927
  const result = layoutAt(page.contentArea.width);
1928
+ // A keep-together box (or the rest of it) taller than a whole page
1929
+ // splits like a `keepTogether: false` one instead of overflowing.
1930
+ const splittable = !style.keepTogether || tallerThanColumn(result);
1794
1931
  /** Where the box would cut the current band, and whether it fits (room
1795
1932
  * is measured against the columns' TRUE bottoms — a capped band keeps
1796
1933
  * its slack below the cap). Null when the band is not level (or the
@@ -2295,13 +2432,15 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2295
2432
  * Place a `:::callout` inline at the current column width as one atomic
2296
2433
  * unit: the frame block followed by its children in the same column. The
2297
2434
  * box's `marginTop` collapses with the pending spacing; `marginBottom` is
2298
- * baked into the post-box grid snap. A box that does not fit moves to the
2435
+ * baked into the post-box grid snap (or, with `snapToGrid: false`, left
2436
+ * exact as pending spacing). A box that does not fit moves to the
2299
2437
  * next column/page (like a resource), pulling a run of trailing headings
2300
- * along (keep-with-next); a box taller than an empty column is placed
2301
- * anyway and overflows (the sandbox warns). A splittable box
2302
- * (`keepTogether: false`) instead leaves the longest run of its children
2303
- * that fits in the column and continues — in a box of its own, without
2304
- * the title or icon — at the top of the next one, splitting again if needed.
2438
+ * along (keep-with-next). A splittable box (`keepTogether: false`, or a
2439
+ * keep-together one taller than a full column) instead leaves the
2440
+ * longest run of its children that fits in the column and continues — in
2441
+ * a box of its own, without the title or icon — at the top of the next
2442
+ * one, splitting again if needed; only a box no cut can split is placed
2443
+ * anyway in an empty column, overflowing (the sandbox warns).
2305
2444
  * Returns the content index to rewind the main loop to when headings
2306
2445
  * were rolled back, else `undefined`.
2307
2446
  *
@@ -2324,17 +2463,27 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2324
2463
  // Floated boxes leave it too: they take the first free band after
2325
2464
  // this point (the foot of the current page, or the head / foot of a
2326
2465
  // page the flow opens later) and the text after them fills the page.
2327
- // A side box always stacks beside the text it interrupts.
2328
- if ((placement === 'auto' || placement === 'top' || placement === 'bottom') && span !== 'side') {
2329
- enqueueCalloutFloat(startIdx, plan, style, L, placement, span);
2330
- return undefined;
2466
+ // A side box always stacks beside the text it interrupts. A box taller
2467
+ // than a full column, which a cut can split, does not float (no band
2468
+ // holds it whole): it stays in the flow where it occurs, like a `here`
2469
+ // box, and splits there. One no cut can split still floats whole.
2470
+ const floating = (placement === 'auto' || placement === 'top' || placement === 'bottom') && span !== 'side';
2471
+ if (floating) {
2472
+ const page = doc.pages[cursor.pageIndex];
2473
+ const width = span === 'page' ? page.contentArea.width : currentColumn(doc, cursor).bbox.width;
2474
+ const inFlow = tallerThanColumn(L.layoutRange(CUT_START, L.end, width, 'float-probe', false))
2475
+ && splitCalloutFragment(L, CUT_START, width, contentArea.height, 'float-probe', false, style.splitMinLines) !== null;
2476
+ if (!inFlow) {
2477
+ enqueueCalloutFloat(startIdx, plan, style, L, placement, span);
2478
+ return undefined;
2479
+ }
2331
2480
  }
2332
2481
  // Page-span boxes split a multi-column page into column bands (stage 1
2333
- // of span blocks). Floating placements keep the inline fallback.
2482
+ // of span blocks).
2334
2483
  {
2335
2484
  const page = doc.pages[cursor.pageIndex];
2336
2485
  if (span === 'page'
2337
- && placement === 'here'
2486
+ && (placement === 'here' || floating)
2338
2487
  && multiColumnBand(page)
2339
2488
  && placeCalloutSpan(startIdx, plan, style, L, firstFrameId)) {
2340
2489
  return undefined;
@@ -2345,7 +2494,6 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2345
2494
  return undefined;
2346
2495
  }
2347
2496
  }
2348
- const splittable = !style.keepTogether;
2349
2497
  let from = CUT_START;
2350
2498
  let part = 0;
2351
2499
  let frameId = firstFrameId;
@@ -2368,20 +2516,34 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2368
2516
  let fragment = null;
2369
2517
  if (result.totalHeight > roomPx + 0.01) {
2370
2518
  // The (rest of the) box does not fit the column: a splittable box
2371
- // leaves the head that fits here…
2519
+ // leaves the head that fits here — a keep-together one too once
2520
+ // no full column could hold it whole…
2521
+ const splittable = !style.keepTogether || tallerThanColumn(result);
2372
2522
  if (splittable)
2373
2523
  fragment = splitCalloutFragment(L, from, curCol.bbox.width, roomPx, frameId, continuation, style.splitMinLines, mirroredOf(doc.pages[cursor.pageIndex]));
2374
2524
  // …otherwise it moves whole to the next column — also out of an
2375
2525
  // EMPTY column that float bands or a band cap have cut short, when
2376
2526
  // a full column would hold it (bounded, so a run of short columns
2377
2527
  // cannot make it wander forever). Only a box taller than a full
2378
- // column is placed anyway, overflowing (a layout warning says so).
2528
+ // column that no cut can split is placed anyway, overflowing (a
2529
+ // layout warning says so).
2379
2530
  const shortColumn = shortColumnMoves < 4
2380
2531
  && curCol.bbox.height < contentArea.height - baselineGrid
2381
2532
  && result.totalHeight <= contentArea.height + 0.01;
2382
2533
  if (!fragment && (curCol.blocks.length > 0 || shortColumn)) {
2383
2534
  if (curCol.blocks.length === 0)
2384
2535
  shortColumnMoves++;
2536
+ // A box that leaves the last column of its band for the next page
2537
+ // leaves that column short while the ones before it run full: the
2538
+ // band is cut level, as a closing band is, so its columns end
2539
+ // together and share the room the box left (keyed by the box —
2540
+ // in the capped pass it reaches the cut band and moves on again).
2541
+ if (part === 0 && curCol.blocks.length > 0) {
2542
+ const page = doc.pages[cursor.pageIndex];
2543
+ const cols = bandColumns(page, currentBand(page, cursor));
2544
+ if (cols[cols.length - 1] === curCol)
2545
+ proposeTrailingCap(startIdx);
2546
+ }
2385
2547
  // …otherwise it moves whole to the next column. Keep-with-next: a
2386
2548
  // run of headings at the column's tail travels with the box.
2387
2549
  // Skipped when the column holds nothing else (rolling back again
@@ -2443,13 +2605,17 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2443
2605
  commitCallout(placed, startIdx, plan, curCol, part > 0 || fragment ? fragmentRange(L, from, to) : undefined);
2444
2606
  // Snap the flow after the box to the baseline grid, baking in at least
2445
2607
  // `marginBottom` (grid wins, margin is a minimum — the resource rule).
2446
- {
2608
+ // An off-grid style (`snapToGrid: false`) keeps its exact margin
2609
+ // instead, as pending spacing that collapses with the next block's
2610
+ // top margin: the flow stays off the grid until the next snap point
2611
+ // (a snapped heading, a list tail), like after an unsnapped heading.
2612
+ if (style.snapToGrid) {
2447
2613
  const usedHeight = curCol.bbox.height - curCol.availableHeight;
2448
2614
  const naturalBottom = usedHeight + placed.marginBottomPx;
2449
2615
  const snappedBottom = Math.ceil((naturalBottom - 0.01) / baselineGrid) * baselineGrid;
2450
2616
  curCol.availableHeight = Math.max(0, curCol.bbox.height - snappedBottom);
2451
2617
  }
2452
- pendingSpacing = 0;
2618
+ pendingSpacing = style.snapToGrid ? 0 : placed.marginBottomPx;
2453
2619
  if (!fragment)
2454
2620
  return undefined;
2455
2621
  // The rest continues at the top of the next column.
@@ -2542,6 +2708,24 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2542
2708
  // the next chapter's own parity rule) starts clean. A part page counts
2543
2709
  // as content even with an empty body — its opener design fills it — so
2544
2710
  // consecutive parts never share a page.
2711
+ // `parts.page: false`: a part opens no page and its body is not set —
2712
+ // it only takes effect (running heads, palette) from the next content.
2713
+ if (skipPartUntil !== null) {
2714
+ if (blockIdx >= skipPartUntil)
2715
+ skipPartUntil = null;
2716
+ continue;
2717
+ }
2718
+ if (!resolved.parts.page && rawBlock.type === 'containerStart' && rawBlock.containerName === 'part') {
2719
+ const plan = partPlan.byStart.get(blockIdx);
2720
+ if (plan) {
2721
+ const end = [...partPlan.byEnd].find(([, p]) => p === plan)?.[0] ?? blockIdx;
2722
+ const marks = (doc.partMarks ??= []);
2723
+ if (!marks.some((m) => m.afterContentIndex === end))
2724
+ marks.push({ afterContentIndex: end, number: plan.number, title: plan.title, ...(plan.palette && Object.keys(plan.palette).length > 0 ? { palette: plan.palette } : {}) });
2725
+ skipPartUntil = end;
2726
+ continue;
2727
+ }
2728
+ }
2545
2729
  if (rawBlock.type === 'containerStart' && rawBlock.containerName === 'part') {
2546
2730
  const plan = partPlan.byStart.get(blockIdx);
2547
2731
  if (plan) {
@@ -2921,7 +3105,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2921
3105
  addBalanceExtra(curCol, looseLines * style.lineHeightPx);
2922
3106
  }
2923
3107
  const effectiveAvailable = curCol.availableHeight - spacingBefore;
2924
- const linesPerAvailable = Math.floor(effectiveAvailable / style.lineHeightPx);
3108
+ // A hair of tolerance: room of exactly N lines (grid arithmetic in
3109
+ // floats — a column cut by a band cap, a balancing extra line) must
3110
+ // hold N lines, not N - 1.
3111
+ const linesPerAvailable = Math.floor((effectiveAvailable + 0.01) / style.lineHeightPx);
2925
3112
  // Math display blocks carry their natural pixel height on the single
2926
3113
  // VDTLine; text blocks use the uniform body lineHeightPx per line.
2927
3114
  const totalRemainHeight = vdtType === 'mathDisplay'
@@ -3181,14 +3368,22 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3181
3368
  // Page-spanning heading: reserve the same vertical band in every
3182
3369
  // other column on this page so body text under the opener band
3183
3370
  // starts below it in ALL columns, not just the one it was placed in.
3371
+ // A column still untouched moves its head below the band (as a top
3372
+ // float does), so a float offered that column's head lands under
3373
+ // the opener instead of over it; the text starts where it did.
3184
3374
  if (vdtType === 'heading' && headingLevel !== undefined) {
3185
3375
  const lvl = headingLevels.forBlock(rawBlock);
3186
3376
  if (lvl?.span === 'page') {
3187
3377
  const page = doc.pages[cursor.pageIndex];
3188
3378
  for (const otherCol of page.columns) {
3189
- if (otherCol !== curCol) {
3190
- otherCol.availableHeight = Math.max(0, otherCol.availableHeight - h);
3379
+ if (otherCol === curCol)
3380
+ continue;
3381
+ if (otherCol.blocks.length === 0 && otherCol.availableHeight >= otherCol.bbox.height - 0.01) {
3382
+ const shift = Math.min(h, otherCol.bbox.height);
3383
+ otherCol.bbox.y += shift;
3384
+ otherCol.bbox.height -= shift;
3191
3385
  }
3386
+ otherCol.availableHeight = Math.max(0, otherCol.availableHeight - h);
3192
3387
  }
3193
3388
  }
3194
3389
  }
@@ -3834,6 +4029,12 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0) {
3834
4029
  best = trailing.result;
3835
4030
  for (const [i, cap] of trailing.caps)
3836
4031
  bandCaps.set(i, cap);
4032
+ // The capped layout is a new problem: the polish round gets its own
4033
+ // pass budget and fresh segments — the first round may have spent
4034
+ // every attempt, or blacklisted the very lever (a heading, a formula)
4035
+ // that now fills the line the cut left short.
4036
+ balancingPasses = 0;
4037
+ segments = [];
3837
4038
  resetSegments();
3838
4039
  yield* balance();
3839
4040
  }