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.
- package/dist/__tests__/canvasPageScale.test.d.ts +2 -0
- package/dist/__tests__/canvasPageScale.test.d.ts.map +1 -0
- package/dist/__tests__/canvasPageScale.test.js +48 -0
- package/dist/__tests__/canvasPageScale.test.js.map +1 -0
- package/dist/__tests__/columnBalancing.test.js +62 -2
- package/dist/__tests__/columnBalancing.test.js.map +1 -1
- package/dist/__tests__/columnClip.test.d.ts +2 -0
- package/dist/__tests__/columnClip.test.d.ts.map +1 -0
- package/dist/__tests__/columnClip.test.js +27 -0
- package/dist/__tests__/columnClip.test.js.map +1 -0
- package/dist/__tests__/defaults/calloutStyles.test.js +5 -0
- package/dist/__tests__/defaults/calloutStyles.test.js.map +1 -1
- package/dist/__tests__/defaults/tableStyles.test.d.ts +2 -0
- package/dist/__tests__/defaults/tableStyles.test.d.ts.map +1 -0
- package/dist/__tests__/defaults/tableStyles.test.js +56 -0
- package/dist/__tests__/defaults/tableStyles.test.js.map +1 -0
- package/dist/__tests__/design/paintOrder.test.d.ts +2 -0
- package/dist/__tests__/design/paintOrder.test.d.ts.map +1 -0
- package/dist/__tests__/design/paintOrder.test.js +75 -0
- package/dist/__tests__/design/paintOrder.test.js.map +1 -0
- package/dist/__tests__/design/slotContainers.test.d.ts +2 -0
- package/dist/__tests__/design/slotContainers.test.d.ts.map +1 -0
- package/dist/__tests__/design/slotContainers.test.js +70 -0
- package/dist/__tests__/design/slotContainers.test.js.map +1 -0
- package/dist/__tests__/exports.test.js +12 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/html-backend.test.js +23 -0
- package/dist/__tests__/html-backend.test.js.map +1 -1
- package/dist/__tests__/parts.test.js +89 -0
- package/dist/__tests__/parts.test.js.map +1 -1
- package/dist/__tests__/pipeline/calloutNested.test.d.ts +2 -0
- package/dist/__tests__/pipeline/calloutNested.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/calloutNested.test.js +264 -0
- package/dist/__tests__/pipeline/calloutNested.test.js.map +1 -0
- package/dist/__tests__/pipeline/calloutOverflow.test.js +39 -3
- package/dist/__tests__/pipeline/calloutOverflow.test.js.map +1 -1
- package/dist/__tests__/pipeline/calloutSnapToGrid.test.d.ts +2 -0
- package/dist/__tests__/pipeline/calloutSnapToGrid.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/calloutSnapToGrid.test.js +72 -0
- package/dist/__tests__/pipeline/calloutSnapToGrid.test.js.map +1 -0
- package/dist/__tests__/pipeline/calloutTallKeepTogether.test.d.ts +2 -0
- package/dist/__tests__/pipeline/calloutTallKeepTogether.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/calloutTallKeepTogether.test.js +166 -0
- package/dist/__tests__/pipeline/calloutTallKeepTogether.test.js.map +1 -0
- package/dist/__tests__/pipeline/chips.test.d.ts +2 -0
- package/dist/__tests__/pipeline/chips.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/chips.test.js +230 -0
- package/dist/__tests__/pipeline/chips.test.js.map +1 -0
- package/dist/__tests__/pipeline/floatFirstSlot.test.js +53 -0
- package/dist/__tests__/pipeline/floatFirstSlot.test.js.map +1 -1
- package/dist/__tests__/pipeline/scratchDeepSky.test.d.ts +3 -0
- package/dist/__tests__/pipeline/scratchDeepSky.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/scratchDeepSky.test.js +137 -0
- package/dist/__tests__/pipeline/scratchDeepSky.test.js.map +1 -0
- package/dist/__tests__/pipeline/spanBands.test.js +18 -0
- package/dist/__tests__/pipeline/spanBands.test.js.map +1 -1
- package/dist/__tests__/pipeline/spanBlocks.test.js +3 -2
- package/dist/__tests__/pipeline/spanBlocks.test.js.map +1 -1
- package/dist/__tests__/pipeline/tableCellImages.test.js +87 -1
- package/dist/__tests__/pipeline/tableCellImages.test.js.map +1 -1
- package/dist/__tests__/pipeline/tableStyles.test.d.ts +2 -0
- package/dist/__tests__/pipeline/tableStyles.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/tableStyles.test.js +188 -0
- package/dist/__tests__/pipeline/tableStyles.test.js.map +1 -0
- package/dist/canvas-backend/blockRender.d.ts.map +1 -1
- package/dist/canvas-backend/blockRender.js +10 -1
- package/dist/canvas-backend/blockRender.js.map +1 -1
- package/dist/canvas-backend/chip.d.ts +10 -0
- package/dist/canvas-backend/chip.d.ts.map +1 -0
- package/dist/canvas-backend/chip.js +49 -0
- package/dist/canvas-backend/chip.js.map +1 -0
- package/dist/canvas-backend/index.d.ts.map +1 -1
- package/dist/canvas-backend/index.js +14 -27
- package/dist/canvas-backend/index.js.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.js +48 -1
- package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
- package/dist/columnClip.d.ts +16 -0
- package/dist/columnClip.d.ts.map +1 -0
- package/dist/columnClip.js +37 -0
- package/dist/columnClip.js.map +1 -0
- package/dist/defaults/calloutStyles.d.ts +1 -0
- package/dist/defaults/calloutStyles.d.ts.map +1 -1
- package/dist/defaults/calloutStyles.js +7 -0
- package/dist/defaults/calloutStyles.js.map +1 -1
- package/dist/defaults/chipStyles.d.ts +28 -0
- package/dist/defaults/chipStyles.d.ts.map +1 -0
- package/dist/defaults/chipStyles.js +102 -0
- package/dist/defaults/chipStyles.js.map +1 -0
- package/dist/defaults/index.d.ts +2 -1
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +18 -2
- package/dist/defaults/index.js.map +1 -1
- package/dist/defaults/parts.d.ts +1 -0
- package/dist/defaults/parts.d.ts.map +1 -1
- package/dist/defaults/parts.js +4 -0
- package/dist/defaults/parts.js.map +1 -1
- package/dist/defaults/shared.d.ts.map +1 -1
- package/dist/defaults/shared.js +43 -16
- package/dist/defaults/shared.js.map +1 -1
- package/dist/defaults/tableStyle.d.ts +17 -1
- package/dist/defaults/tableStyle.d.ts.map +1 -1
- package/dist/defaults/tableStyle.js +46 -0
- package/dist/defaults/tableStyle.js.map +1 -1
- package/dist/design/layout.d.ts.map +1 -1
- package/dist/design/layout.js +10 -6
- package/dist/design/layout.js.map +1 -1
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +72 -5
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/knuthPlass/richAdapter.d.ts +3 -1
- package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
- package/dist/knuthPlass/richAdapter.js +7 -4
- package/dist/knuthPlass/richAdapter.js.map +1 -1
- package/dist/measure/cache.d.ts.map +1 -1
- package/dist/measure/cache.js +7 -1
- package/dist/measure/cache.js.map +1 -1
- package/dist/measure/chipEdges.d.ts +5 -0
- package/dist/measure/chipEdges.d.ts.map +1 -0
- package/dist/measure/chipEdges.js +20 -0
- package/dist/measure/chipEdges.js.map +1 -0
- package/dist/measure/rich.d.ts +12 -0
- package/dist/measure/rich.d.ts.map +1 -1
- package/dist/measure/rich.js +128 -5
- package/dist/measure/rich.js.map +1 -1
- package/dist/parse/blockParser.d.ts.map +1 -1
- package/dist/parse/blockParser.js +10 -7
- package/dist/parse/blockParser.js.map +1 -1
- package/dist/parse/index.d.ts +3 -3
- package/dist/parse/index.d.ts.map +1 -1
- package/dist/parse/index.js +1 -1
- package/dist/parse/index.js.map +1 -1
- package/dist/parse/inlineFormatting.d.ts +28 -0
- package/dist/parse/inlineFormatting.d.ts.map +1 -1
- package/dist/parse/inlineFormatting.js +52 -0
- package/dist/parse/inlineFormatting.js.map +1 -1
- package/dist/parse/inlineSnippet.d.ts +2 -1
- package/dist/parse/inlineSnippet.d.ts.map +1 -1
- package/dist/parse/inlineSnippet.js +6 -4
- package/dist/parse/inlineSnippet.js.map +1 -1
- package/dist/parse/sourceMapping.d.ts.map +1 -1
- package/dist/parse/sourceMapping.js +25 -1
- package/dist/parse/sourceMapping.js.map +1 -1
- package/dist/parse/types.d.ts +31 -0
- package/dist/parse/types.d.ts.map +1 -1
- package/dist/pipeline/bandCaps.d.ts.map +1 -1
- package/dist/pipeline/bandCaps.js +2 -0
- package/dist/pipeline/bandCaps.js.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +261 -60
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/calloutLayout.d.ts +52 -4
- package/dist/pipeline/calloutLayout.d.ts.map +1 -1
- package/dist/pipeline/calloutLayout.js +161 -26
- package/dist/pipeline/calloutLayout.js.map +1 -1
- package/dist/pipeline/chips.d.ts +27 -0
- package/dist/pipeline/chips.d.ts.map +1 -0
- package/dist/pipeline/chips.js +56 -0
- package/dist/pipeline/chips.js.map +1 -0
- package/dist/pipeline/columnBalancing.d.ts +1 -1
- package/dist/pipeline/columnBalancing.d.ts.map +1 -1
- package/dist/pipeline/columnBalancing.js +99 -1
- package/dist/pipeline/columnBalancing.js.map +1 -1
- package/dist/pipeline/config.d.ts.map +1 -1
- package/dist/pipeline/config.js +3 -1
- package/dist/pipeline/config.js.map +1 -1
- package/dist/pipeline/headerFooter.d.ts +0 -4
- package/dist/pipeline/headerFooter.d.ts.map +1 -1
- package/dist/pipeline/headerFooter.js +42 -15
- package/dist/pipeline/headerFooter.js.map +1 -1
- package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
- package/dist/pipeline/measureContentBlock.js +6 -1
- package/dist/pipeline/measureContentBlock.js.map +1 -1
- package/dist/pipeline/partPalette.d.ts +6 -0
- package/dist/pipeline/partPalette.d.ts.map +1 -0
- package/dist/pipeline/partPalette.js +96 -0
- package/dist/pipeline/partPalette.js.map +1 -0
- package/dist/pipeline/placeholders.d.ts +2 -1
- package/dist/pipeline/placeholders.d.ts.map +1 -1
- package/dist/pipeline/placeholders.js +11 -1
- package/dist/pipeline/placeholders.js.map +1 -1
- package/dist/pipeline/resourceLayout.d.ts.map +1 -1
- package/dist/pipeline/resourceLayout.js +35 -11
- package/dist/pipeline/resourceLayout.js.map +1 -1
- package/dist/types.d.ts +141 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +88 -2
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js +17 -0
- package/dist/vdt.js.map +1 -1
- package/package.json +1 -1
package/dist/pipeline/build.js
CHANGED
|
@@ -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. `
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1574
|
-
*
|
|
1575
|
-
*
|
|
1576
|
-
*
|
|
1577
|
-
*
|
|
1578
|
-
*
|
|
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
|
-
|
|
1582
|
-
|
|
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
|
|
1585
|
-
|
|
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
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
const
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
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) =>
|
|
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
|
|
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
|
|
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)
|
|
2301
|
-
*
|
|
2302
|
-
*
|
|
2303
|
-
*
|
|
2304
|
-
*
|
|
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
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
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).
|
|
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
|
|
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
|
-
|
|
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
|
|
3190
|
-
|
|
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
|
}
|