postext 0.3.28 → 0.3.29

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 (37) hide show
  1. package/dist/__tests__/columnBalancing.test.js +78 -1
  2. package/dist/__tests__/columnBalancing.test.js.map +1 -1
  3. package/dist/__tests__/pipeline/colonBalancing.test.d.ts +2 -0
  4. package/dist/__tests__/pipeline/colonBalancing.test.d.ts.map +1 -0
  5. package/dist/__tests__/pipeline/colonBalancing.test.js +124 -0
  6. package/dist/__tests__/pipeline/colonBalancing.test.js.map +1 -0
  7. package/dist/__tests__/pipeline/floatFirstSlot.test.js +33 -0
  8. package/dist/__tests__/pipeline/floatFirstSlot.test.js.map +1 -1
  9. package/dist/__tests__/pipeline/headingShortBand.test.d.ts +2 -0
  10. package/dist/__tests__/pipeline/headingShortBand.test.d.ts.map +1 -0
  11. package/dist/__tests__/pipeline/headingShortBand.test.js +89 -0
  12. package/dist/__tests__/pipeline/headingShortBand.test.js.map +1 -0
  13. package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
  14. package/dist/canvas-backend/headerFooter.js +2 -6
  15. package/dist/canvas-backend/headerFooter.js.map +1 -1
  16. package/dist/canvas-backend/index.d.ts +1 -1
  17. package/dist/canvas-backend/index.d.ts.map +1 -1
  18. package/dist/canvas-backend/renderResourceBlock.d.ts +15 -1
  19. package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
  20. package/dist/canvas-backend/renderResourceBlock.js +104 -8
  21. package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
  22. package/dist/defaults/headings.d.ts +2 -0
  23. package/dist/defaults/headings.d.ts.map +1 -1
  24. package/dist/defaults/headings.js +14 -0
  25. package/dist/defaults/headings.js.map +1 -1
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/pipeline/build.d.ts.map +1 -1
  29. package/dist/pipeline/build.js +177 -17
  30. package/dist/pipeline/build.js.map +1 -1
  31. package/dist/pipeline/columnBalancing.d.ts +30 -5
  32. package/dist/pipeline/columnBalancing.d.ts.map +1 -1
  33. package/dist/pipeline/columnBalancing.js +70 -4
  34. package/dist/pipeline/columnBalancing.js.map +1 -1
  35. package/dist/types.d.ts +8 -0
  36. package/dist/types.d.ts.map +1 -1
  37. package/package.json +1 -1
@@ -22,7 +22,7 @@ import { enumerateCurrentPageSlots, measureFloatBand, columnHasFloatBand, fitsSt
22
22
  import { computeHeadingContext, computeResourceNumbering, } from './resourceNumbering';
23
23
  import { defaultResourceTypes } from '../defaults/resourceTypes';
24
24
  import { buildHeadersAndFooters, measureHeadingAdvancedDesignHeight } from './headerFooter';
25
- import { totalGapLines, proposeBalanceLines, MAX_BALANCING_PASSES } from './columnBalancing';
25
+ import { totalGapLines, proposeBalanceLines, collectColumnGaps, firstDivergentColumn, MAX_BALANCING_PASSES } from './columnBalancing';
26
26
  import { applyBandCap, uncapBand, columnBottom, bandCapLines, bandTop, resolveBandCaps, resolveTrailingCaps, bandCapLinesAroundZone, } from './bandCaps';
27
27
  import { raggedUrlLines } from './raggedUrl';
28
28
  export class BuildCancelledError extends Error {
@@ -303,6 +303,9 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
303
303
  * sends single-column floats to the least reserved column). */
304
304
  const floatReserved = new Map();
305
305
  const reservedOf = (col) => floatReserved.get(col) ?? { top: 0, bottom: 0 };
306
+ /** Content index of the block that first referenced the latest float
307
+ * reserved at the head of each column. */
308
+ const topFloatRefOf = new Map();
306
309
  /** Kind of cap the column is under (`undefined` when uncapped). A cap that
307
310
  * cannot be attributed to the active band is treated as a span cap — the
308
311
  * conservative reading, which keeps the column's bottom off the slot list. */
@@ -366,6 +369,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
366
369
  col.bbox.height = Math.max(0, col.bbox.height - need);
367
370
  col.availableHeight = Math.max(0, col.availableHeight - need);
368
371
  r.top += need;
372
+ topFloatRefOf.set(col, Math.max(topFloatRefOf.get(col) ?? -1, f.firstBlockIdx));
369
373
  }
370
374
  else {
371
375
  const capped = uncappedBottoms.get(col);
@@ -440,14 +444,21 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
440
444
  * cursor (bottom of the referencing column, top / bottom of the next
441
445
  * empty columns; the band bottom for page-span floats). Runs before each
442
446
  * block is placed, so a float lands in the first gap after its reference. */
443
- const tryPlacePendingFloatsOnCurrentPage = () => {
447
+ const tryPlacePendingFloatsOnCurrentPage = (preferTop = false) => {
444
448
  if (pendingFloats.length === 0)
445
449
  return;
446
450
  const page = doc.pages[cursor.pageIndex];
447
451
  for (let i = 0; i < pendingFloats.length;) {
448
452
  const f = pendingFloats[i];
449
453
  let r = 'defer';
450
- for (const slot of enumerateCurrentPageSlots(page, cursor.columnIndex, f, capKindOf)) {
454
+ let slots = enumerateCurrentPageSlots(page, cursor.columnIndex, f, capKindOf);
455
+ // A page-span box comes next: the head of an empty column keeps the
456
+ // band cuttable under the float (the box then sits below both the
457
+ // text and the figure), where the referencing column's foot would
458
+ // wall the box off the page.
459
+ if (preferTop)
460
+ slots = [...slots.filter((s) => s.position === 'top'), ...slots.filter((s) => s.position !== 'top')];
461
+ for (const slot of slots) {
451
462
  r = placeFloatInColumns(page, f, slot.cols, slot.position, slot.pageSpan, 'strict');
452
463
  if (r !== 'defer')
453
464
  break;
@@ -461,6 +472,20 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
461
472
  /** Reserve floats on each freshly opened content page. Passed only to the
462
473
  * content-flow column advances — parity / force-blank pages never get it. */
463
474
  const onNewPage = (page) => flushFloatsIntoPage(page);
475
+ /** Whether content block `idx` opens a callout that will span the page
476
+ * in the current (multi-column) band — the floats placed right before
477
+ * it prefer the head of an empty column. */
478
+ const spanBoxAt = (idx) => {
479
+ const b = contentBlocks[idx];
480
+ if (!b || b.type !== 'containerStart' || b.containerName !== 'callout')
481
+ return false;
482
+ const plan = calloutPlan.get(idx);
483
+ const style = plan ? pickCalloutStyle(resolved.calloutStyles, plan.attrs.type) : undefined;
484
+ if (!style || style.span !== 'page' || style.placement === 'fixed')
485
+ return false;
486
+ const page = doc.pages[cursor.pageIndex];
487
+ return bandColumns(page, currentBand(page, cursor)).length > 1;
488
+ };
464
489
  /** Chapter barrier: place every pending float before the boundary — in
465
490
  * the current page's free slots, then on fresh pages opened ahead of it
466
491
  * (each force-places at least one float). The cursor is left on the last
@@ -531,6 +556,19 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
531
556
  let activeCap = null;
532
557
  /** True bottoms of capped columns (restored when the span block cuts). */
533
558
  const uncappedBottoms = new Map();
559
+ /** Column balancing: height (px) the levers added inside each column so
560
+ * far — extra grid lines above headings / list ends and lines gained by
561
+ * loose paragraphs. Balancing only ever fills a column's bottom gap, so
562
+ * a placement rule that needs slack *after* a block (keep-colon-with-
563
+ * list) must see the column as it was before the levers filled it:
564
+ * otherwise the block that closed the column in the plain pass moves to
565
+ * the next column, the flow shifts on every later page, and the pass is
566
+ * discarded as a regression. */
567
+ const balanceExtraInColumn = new Map();
568
+ const addBalanceExtra = (col, px) => {
569
+ if (px > 0)
570
+ balanceExtraInColumn.set(col, (balanceExtraInColumn.get(col) ?? 0) + px);
571
+ };
534
572
  const bandCapProposals = new Map();
535
573
  const spanPlacedInBand = new Set();
536
574
  const bandCapsApplied = new Set();
@@ -835,6 +873,34 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
835
873
  const bottoms = cols.map((c) => c.bbox.y + (c.bbox.height - c.availableHeight));
836
874
  return Math.max(...bottoms) - Math.min(...bottoms) <= baselineGrid + 0.5;
837
875
  };
876
+ /** Level for the box: the text columns end level, and a column holding
877
+ * only a float band (a figure at its head, no text yet) counts as level
878
+ * when a band cap could not level it either — the figure was first
879
+ * referenced by the very block before the box, so a cut that spills
880
+ * that block into the figure's column leaves the figure no slot after
881
+ * its reference (it would fall off the page, and the box with it). The
882
+ * box then cuts under the text and the figure alike, the slack under
883
+ * the figure being the compositor's usual trade; a figure referenced
884
+ * earlier keeps the cap route, which flows text under it. */
885
+ const lastBlockBefore = () => {
886
+ let j = startIdx - 1;
887
+ while (j >= 0 && isMarkerBlock(contentBlocks[j]))
888
+ j--;
889
+ return j;
890
+ };
891
+ const levelForBox = (cols) => {
892
+ if (isBandLevel(cols))
893
+ return true;
894
+ const textCols = cols.filter((c) => c.blocks.length > 0);
895
+ const floatOnly = cols.filter((c) => c.blocks.length === 0 && reservedOf(c).top > 0);
896
+ if (textCols.length === 0 || textCols.length + floatOnly.length !== cols.length)
897
+ return false;
898
+ if (!isBandLevel(textCols))
899
+ return false;
900
+ const textBottom = bandUsedBottom(textCols);
901
+ const before = lastBlockBefore();
902
+ return floatOnly.every((c) => c.bbox.y <= textBottom + baselineGrid + 0.5 && topFloatRefOf.get(c) === before);
903
+ };
838
904
  /** Position in the children the fragment to place starts at, and its
839
905
  * 0-based index among the fragments (0 = the box, or its head). */
840
906
  let from = 0;
@@ -855,7 +921,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
855
921
  * `requireLevel` is set. */
856
922
  const measureBand = (requireLevel) => {
857
923
  const cols = bandColumns(page, currentBand(page, cursor));
858
- if (cols.length === 0 || (requireLevel && !isBandLevel(cols)))
924
+ if (cols.length === 0 || (requireLevel && !levelForBox(cols)))
859
925
  return null;
860
926
  const cutY = gridUp(page, bandUsedBottom(cols));
861
927
  const bandHasContent = cols.some((c) => c.blocks.length > 0);
@@ -1301,7 +1367,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1301
1367
  // order. Then enqueue the floats first-referenced in this block, so the
1302
1368
  // next page opened while placing it (or any later block) reserves their
1303
1369
  // band and the next iteration offers them the slots that follow.
1304
- tryPlacePendingFloatsOnCurrentPage();
1370
+ tryPlacePendingFloatsOnCurrentPage(spanBoxAt(blockIdx));
1305
1371
  enqueueFloatsFor(blockIdx);
1306
1372
  // --- Directives ----------------------------------------------------
1307
1373
  if (rawBlock.type === 'directive') {
@@ -1421,7 +1487,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1421
1487
  // slots, else on pages opened ahead of it — so no float escapes
1422
1488
  // past it. The page stays balanceable (no forced break).
1423
1489
  if (pickCalloutStyle(resolved.calloutStyles, plan.attrs.type).floatBarrier) {
1424
- tryPlacePendingFloatsOnCurrentPage();
1490
+ tryPlacePendingFloatsOnCurrentPage(spanBoxAt(blockIdx));
1425
1491
  drainPendingFloats();
1426
1492
  }
1427
1493
  // `span: 'page'` boxes in multi-column layouts branch to the
@@ -1614,6 +1680,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1614
1680
  // A justified line a link leaves with too few spaces is set ragged.
1615
1681
  let remainingLines = [...raggedUrlLines(measured.lines, style.textAlign, rawBlock.text)];
1616
1682
  let partIndex = 0;
1683
+ /** Times this block left an EMPTY short column (see `shortColumn`) —
1684
+ * bounded so a page whose columns are all short (footnotes, design
1685
+ * bands) cannot make it wander forever. */
1686
+ let shortColumnMoves = 0;
1617
1687
  // "Keep with next" for colon-introduced lists: a paragraph ending in `:`
1618
1688
  // followed directly by a list acts as a lead-in title — the colon-bearing
1619
1689
  // line must share a column with the first list item. Only checked for the
@@ -1642,8 +1712,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1642
1712
  // page top.
1643
1713
  if (vdtType === 'heading') {
1644
1714
  const extraPx = balanceExtraPx?.get(blockIdx);
1645
- if (extraPx)
1715
+ if (extraPx) {
1646
1716
  spacingBefore += extraPx;
1717
+ addBalanceExtra(curCol, extraPx);
1718
+ }
1647
1719
  }
1648
1720
  }
1649
1721
  else if (vdtType === 'listItem') {
@@ -1657,10 +1729,26 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1657
1729
  // handled inside the heading branch above (after margin collapsing).
1658
1730
  if (vdtType !== 'heading' && vdtType !== 'mathDisplay' && partIndex === 0) {
1659
1731
  const extraPx = balanceExtraPx?.get(blockIdx);
1660
- if (extraPx)
1732
+ if (extraPx) {
1661
1733
  spacingBefore += extraPx;
1734
+ addBalanceExtra(curCol, extraPx);
1735
+ }
1736
+ }
1737
+ }
1738
+ else if (partIndex === 0 && reservedOf(curCol).top > 0) {
1739
+ // Column balancing: extra grid lines between the float band at the
1740
+ // head of this column and its first block (the after-float lever).
1741
+ const extraPx = balanceExtraPx?.get(blockIdx);
1742
+ if (extraPx) {
1743
+ spacingBefore += extraPx;
1744
+ addBalanceExtra(curCol, extraPx);
1662
1745
  }
1663
1746
  }
1747
+ // A loose paragraph's extra line is balancing height too (it lands
1748
+ // whole in this column — loose candidates are never split parts).
1749
+ if (partIndex === 0 && tryLoose && looseLines !== undefined && typeof looseOutcome.get(blockIdx) === 'number') {
1750
+ addBalanceExtra(curCol, looseLines * style.lineHeightPx);
1751
+ }
1664
1752
  const effectiveAvailable = curCol.availableHeight - spacingBefore;
1665
1753
  const linesPerAvailable = Math.floor(effectiveAvailable / style.lineHeightPx);
1666
1754
  // Math display blocks carry their natural pixel height on the single
@@ -1704,7 +1792,9 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1704
1792
  && curCol.blocks.length > 0) {
1705
1793
  const usedHeight = (curCol.bbox.height - curCol.availableHeight) + spacingBefore;
1706
1794
  const paragraphBottom = usedHeight + totalRemainHeight;
1707
- const availableAfter = curCol.bbox.height - paragraphBottom;
1795
+ // Slack after the paragraph as the plain pass saw it: the height
1796
+ // balancing added above in this column is not room the list lost.
1797
+ const availableAfter = curCol.bbox.height - paragraphBottom + (balanceExtraInColumn.get(curCol) ?? 0);
1708
1798
  const nextListKind = nextBlock?.listKind ?? 'unordered';
1709
1799
  const nextListMarginDim = nextListKind === 'ordered'
1710
1800
  ? resolved.orderedLists.marginTop
@@ -1784,6 +1874,16 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1784
1874
  // headingRunCount === curCol.blocks.length: fall through to place.
1785
1875
  }
1786
1876
  }
1877
+ // A block opening a column normally stays (no column can take it any
1878
+ // better), except in a column at least a line shorter than the content
1879
+ // area — the band under a page-span box, or a column cut by a float:
1880
+ // a heading that fits there alone, or a block that does not fit there
1881
+ // but would fit a full column, opens the next column instead (the move
1882
+ // count is bounded besides). A column cut by a band cap is short on
1883
+ // purpose — its content is meant to end at the cut.
1884
+ const shortColumn = shortColumnMoves < 4
1885
+ && !uncappedBottoms.has(curCol)
1886
+ && curCol.bbox.height < contentArea.height - baselineGrid;
1787
1887
  // Block fits in current column
1788
1888
  if (effectiveRemainHeight <= effectiveAvailable) {
1789
1889
  // Heading keep-with-next: never leave a heading as the last block of a
@@ -1800,7 +1900,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1800
1900
  && resolved.headings.keepWithNext
1801
1901
  && !nextIsHeading
1802
1902
  && nextBlock !== null
1803
- && curCol.blocks.length > 0) {
1903
+ && (curCol.blocks.length > 0 || shortColumn)) {
1804
1904
  const wouldUsedHeight = (curCol.bbox.height - curCol.availableHeight) + spacingBefore;
1805
1905
  const naturalBottom = wouldUsedHeight + effectiveRemainHeight + style.marginBottomPx;
1806
1906
  const snappedBottom = shouldSnapToGrid
@@ -1812,6 +1912,8 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1812
1912
  : 1;
1813
1913
  const minSpaceAfter = minLinesNeeded * bodyStyle.lineHeightPx;
1814
1914
  if (remainAfterHeading < minSpaceAfter) {
1915
+ if (curCol.blocks.length === 0)
1916
+ shortColumnMoves++;
1815
1917
  // Roll back any immediately-preceding heading blocks in this
1816
1918
  // column so they travel with this one.
1817
1919
  const rolledBack = rollbackTrailingBlocks(curCol, doc.blocks, isFreeHeading);
@@ -1975,8 +2077,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1975
2077
  }
1976
2078
  // choice.splitAt === 0: fall through to push whole paragraph to next column
1977
2079
  }
1978
- // Cannot split — advance to next column if current has content
1979
- if (curCol.blocks.length > 0) {
2080
+ // Cannot split — advance to next column if current has content (or
2081
+ // the column is a short band that cannot hold the block at all).
2082
+ if (curCol.blocks.length > 0 || (shortColumn && effectiveRemainHeight <= contentArea.height)) {
2083
+ if (curCol.blocks.length === 0)
2084
+ shortColumnMoves++;
1980
2085
  // Heading keep-with-next (no-fit variant): when a heading can't fit
1981
2086
  // in the current column and the column's tail is a run of headings,
1982
2087
  // pull those headings along so they don't remain stranded as orphans
@@ -2099,17 +2204,67 @@ export function buildDocument(content, config, cache, options) {
2099
2204
  let bestScore = totalGapLines(best.doc, best.forcedBreakPages);
2100
2205
  let applied = { lines: new Map(), loose: new Map() };
2101
2206
  const failedLoose = new Set();
2207
+ const failedLines = new Set();
2102
2208
  let converged = bestScore === 0;
2209
+ /**
2210
+ * A rejected pass moved content across a column break somewhere (a
2211
+ * split paragraph whose head no longer fits, a float that lost its slot,
2212
+ * a lead-in that left with its list…): every page after that point is
2213
+ * re-flowed, gaps open elsewhere and a span cap may miss its band. The
2214
+ * levers are meant to be local, so contain the damage: find the first
2215
+ * column whose content changed and blacklist the levers this pass newly
2216
+ * applied there (failing that, on its page; failing that, everywhere), so
2217
+ * the next proposal keeps the working levers before it and tries again
2218
+ * without the one that cascaded. Returns whether anything was blacklisted.
2219
+ */
2220
+ const containCascade = (next, proposal) => {
2221
+ const div = firstDivergentColumn(best.doc, next.doc);
2222
+ if (!div)
2223
+ return false;
2224
+ const newLines = [...proposal.lines].filter(([k, n]) => n > (applied.lines.get(k) ?? 0)).map(([k]) => k);
2225
+ const newLoose = [...proposal.loose.keys()].filter((k) => !applied.loose.has(k));
2226
+ if (newLines.length === 0 && newLoose.length === 0)
2227
+ return false;
2228
+ const gaps = collectColumnGaps(best.doc, best.forcedBreakPages);
2229
+ const blacklist = (cands) => {
2230
+ let hit = false;
2231
+ for (const k of newLines)
2232
+ if (!cands || cands.has(k)) {
2233
+ failedLines.add(k);
2234
+ hit = true;
2235
+ }
2236
+ for (const k of newLoose)
2237
+ if (!cands || cands.has(k)) {
2238
+ failedLoose.add(k);
2239
+ hit = true;
2240
+ }
2241
+ return hit;
2242
+ };
2243
+ const inColumn = gaps
2244
+ .filter((g) => g.pageIndex === div.pageIndex && g.columnIndex === div.columnIndex)
2245
+ .flatMap((g) => g.candidates.map((c) => c.contentIndex));
2246
+ if (blacklist(new Set(inColumn)))
2247
+ return true;
2248
+ const onPage = gaps
2249
+ .filter((g) => g.pageIndex === div.pageIndex)
2250
+ .flatMap((g) => g.candidates.map((c) => c.contentIndex));
2251
+ if (blacklist(new Set(onPage)))
2252
+ return true;
2253
+ return blacklist(null);
2254
+ };
2103
2255
  const balance = () => {
2104
2256
  while (!converged && passCount < MAX_BALANCING_PASSES) {
2105
2257
  const proposal = proposeBalanceLines(best.doc, best.forcedBreakPages, applied, {
2106
2258
  maxLinesPerHeading: balancing.maxLinesPerHeading,
2107
2259
  stretchAfterLists: balancing.stretchAfterLists,
2108
2260
  maxLinesAfterList: balancing.maxLinesAfterList,
2261
+ stretchAfterFloats: balancing.stretchAfterFloats,
2262
+ maxLinesAfterFloat: balancing.maxLinesAfterFloat,
2109
2263
  looseParagraphs: balancing.looseParagraphs,
2110
2264
  maxLooseParagraphs: balancing.maxLooseParagraphs,
2111
2265
  optimalLineBreaking: best.doc.config.bodyText.optimalLineBreaking,
2112
2266
  failedLoose,
2267
+ failedLines,
2113
2268
  });
2114
2269
  if (!proposal.changed) {
2115
2270
  // No stretch point can absorb the remaining gaps — stable.
@@ -2153,11 +2308,16 @@ export function buildDocument(content, config, cache, options) {
2153
2308
  converged = score === 0;
2154
2309
  }
2155
2310
  else {
2156
- // Plateau or regression. Retry when a loose candidate was just
2157
- // blacklisted (the proposer falls through to the next one), or when
2158
- // the new loose paragraphs gained their lines yet the layout did not
2159
- // improve (the gain landed elsewhere — drop them too). A pure spacing
2160
- // plateau means we're done: keep the best layout found so far.
2311
+ // Plateau or regression. First contain a cascade: a lever that
2312
+ // moved content across a column break is blacklisted and the loop
2313
+ // retries without it. Otherwise retry when a loose candidate was
2314
+ // just blacklisted (the proposer falls through to the next one), or
2315
+ // when the new loose paragraphs gained their lines yet the layout
2316
+ // did not improve (the gain landed elsewhere — drop them too). A
2317
+ // pure spacing plateau means we're done: keep the best layout found
2318
+ // so far.
2319
+ if (containCascade(next, proposal))
2320
+ continue;
2161
2321
  if (looseFailed.length > 0 || looseWon.length > 0) {
2162
2322
  for (const k of looseWon)
2163
2323
  failedLoose.add(k);