postext 0.3.29 → 0.3.30
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__/exports.test.js +2 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/pipeline/calloutOverflow.test.d.ts +2 -0
- package/dist/__tests__/pipeline/calloutOverflow.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/calloutOverflow.test.js +189 -0
- package/dist/__tests__/pipeline/calloutOverflow.test.js.map +1 -0
- package/dist/__tests__/pipeline/spanBoxHeadAfterCut.test.d.ts +2 -0
- package/dist/__tests__/pipeline/spanBoxHeadAfterCut.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/spanBoxHeadAfterCut.test.js +96 -0
- package/dist/__tests__/pipeline/spanBoxHeadAfterCut.test.js.map +1 -0
- package/dist/__tests__/pipeline/spanFloatAtCut.test.d.ts +2 -0
- package/dist/__tests__/pipeline/spanFloatAtCut.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/spanFloatAtCut.test.js +155 -0
- package/dist/__tests__/pipeline/spanFloatAtCut.test.js.map +1 -0
- package/dist/__tests__/pipeline/tableCellImages.test.d.ts +2 -0
- package/dist/__tests__/pipeline/tableCellImages.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/tableCellImages.test.js +166 -0
- package/dist/__tests__/pipeline/tableCellImages.test.js.map +1 -0
- package/dist/__tests__/pipeline/tableCellLists.test.d.ts +2 -0
- package/dist/__tests__/pipeline/tableCellLists.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/tableCellLists.test.js +91 -0
- package/dist/__tests__/pipeline/tableCellLists.test.js.map +1 -0
- package/dist/__tests__/pipeline/tableSlice.test.d.ts +2 -0
- package/dist/__tests__/pipeline/tableSlice.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/tableSlice.test.js +226 -0
- package/dist/__tests__/pipeline/tableSlice.test.js.map +1 -0
- package/dist/__tests__/pipeline/tableSplit.test.d.ts +2 -0
- package/dist/__tests__/pipeline/tableSplit.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/tableSplit.test.js +154 -0
- package/dist/__tests__/pipeline/tableSplit.test.js.map +1 -0
- package/dist/__tests__/pipeline/trailingCalloutBalance.test.d.ts +2 -0
- package/dist/__tests__/pipeline/trailingCalloutBalance.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/trailingCalloutBalance.test.js +60 -0
- package/dist/__tests__/pipeline/trailingCalloutBalance.test.js.map +1 -0
- package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.js +13 -1
- package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
- package/dist/defaults/calloutStyles.d.ts +1 -0
- package/dist/defaults/calloutStyles.d.ts.map +1 -1
- package/dist/defaults/calloutStyles.js +6 -0
- package/dist/defaults/calloutStyles.js.map +1 -1
- package/dist/defaults/index.d.ts +1 -1
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +1 -1
- package/dist/defaults/index.js.map +1 -1
- package/dist/defaults/tableStyle.d.ts +7 -1
- package/dist/defaults/tableStyle.d.ts.map +1 -1
- package/dist/defaults/tableStyle.js +35 -1
- package/dist/defaults/tableStyle.js.map +1 -1
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +32 -19
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/knuthPlass/breakpoints.d.ts.map +1 -1
- package/dist/knuthPlass/breakpoints.js +6 -2
- package/dist/knuthPlass/breakpoints.js.map +1 -1
- package/dist/knuthPlass/constants.d.ts +3 -1
- package/dist/knuthPlass/constants.d.ts.map +1 -1
- package/dist/knuthPlass/constants.js +3 -1
- package/dist/knuthPlass/constants.js.map +1 -1
- package/dist/pipeline/bandCaps.d.ts +4 -2
- package/dist/pipeline/bandCaps.d.ts.map +1 -1
- package/dist/pipeline/bandCaps.js +5 -3
- package/dist/pipeline/bandCaps.js.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +547 -104
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/calloutLayout.d.ts +7 -0
- package/dist/pipeline/calloutLayout.d.ts.map +1 -1
- package/dist/pipeline/calloutLayout.js +36 -6
- package/dist/pipeline/calloutLayout.js.map +1 -1
- package/dist/pipeline/columnBalancing.d.ts +17 -1
- package/dist/pipeline/columnBalancing.d.ts.map +1 -1
- package/dist/pipeline/columnBalancing.js +72 -2
- package/dist/pipeline/columnBalancing.js.map +1 -1
- package/dist/pipeline/config.js +1 -1
- package/dist/pipeline/config.js.map +1 -1
- package/dist/pipeline/floatPlacement.d.ts +4 -0
- package/dist/pipeline/floatPlacement.d.ts.map +1 -1
- package/dist/pipeline/floatPlacement.js.map +1 -1
- package/dist/pipeline/resourceLayout.d.ts +55 -1
- package/dist/pipeline/resourceLayout.d.ts.map +1 -1
- package/dist/pipeline/resourceLayout.js +394 -30
- package/dist/pipeline/resourceLayout.js.map +1 -1
- package/dist/table/model.d.ts +7 -1
- package/dist/table/model.d.ts.map +1 -1
- package/dist/table/model.js +22 -0
- package/dist/table/model.js.map +1 -1
- package/dist/types.d.ts +48 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +55 -0
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js.map +1 -1
- package/package.json +1 -1
package/dist/pipeline/build.js
CHANGED
|
@@ -16,13 +16,13 @@ import { measureContentBlock } from './measureContentBlock';
|
|
|
16
16
|
import { planParagraphContainers } from './paragraphContainers';
|
|
17
17
|
import { planParts, derivePartMeasureContext } from './parts';
|
|
18
18
|
import { layoutCallout, offsetCalloutToAbsolute, pickCalloutStyle, planCallouts, resolveCalloutAttrs, } from './calloutLayout';
|
|
19
|
-
import { layoutResourceBlock } from './resourceLayout';
|
|
19
|
+
import { layoutResourceBlock, planTableSlice } from './resourceLayout';
|
|
20
20
|
import { computeFloatPlan, floatedResourceIds, } from './floatPlacement';
|
|
21
21
|
import { enumerateCurrentPageSlots, measureFloatBand, columnHasFloatBand, fitsStrict, trueBottom, } from './floatSlots';
|
|
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, collectColumnGaps, firstDivergentColumn, MAX_BALANCING_PASSES } from './columnBalancing';
|
|
25
|
+
import { totalGapLines, proposeBalanceLines, collectColumnGaps, firstDivergentColumn, MAX_BALANCING_PASSES, balanceKey } 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 {
|
|
@@ -207,6 +207,8 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
207
207
|
};
|
|
208
208
|
const floatGapPx = bodyStyle.lineHeightPx;
|
|
209
209
|
const minTextPx = bodyStyle.lineHeightPx * 3;
|
|
210
|
+
/** Fewest body rows the closing slice of a split table carries. */
|
|
211
|
+
const MIN_TAIL_ROWS = 3;
|
|
210
212
|
/** Offset a resolved resource block's caption/table geometry from
|
|
211
213
|
* block-relative to absolute page coordinates (mirrors inline placement). */
|
|
212
214
|
const offsetResourceBlockToAbsolute = (rb, ox, oy) => {
|
|
@@ -220,6 +222,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
220
222
|
ln.bbox.y += oy;
|
|
221
223
|
ln.baseline += oy;
|
|
222
224
|
}
|
|
225
|
+
for (const ln of rb.continuesLines) {
|
|
226
|
+
ln.bbox.x += ox;
|
|
227
|
+
ln.bbox.y += oy;
|
|
228
|
+
ln.baseline += oy;
|
|
229
|
+
}
|
|
223
230
|
if (rb.captionBar) {
|
|
224
231
|
rb.captionBar.rect.x += ox;
|
|
225
232
|
rb.captionBar.rect.y += oy;
|
|
@@ -228,6 +235,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
228
235
|
for (const cell of rb.table.cells) {
|
|
229
236
|
cell.rect.x += ox;
|
|
230
237
|
cell.rect.y += oy;
|
|
238
|
+
if (cell.image) {
|
|
239
|
+
cell.image.rect.x += ox;
|
|
240
|
+
cell.image.rect.y += oy;
|
|
241
|
+
}
|
|
231
242
|
for (const cl of cell.lines) {
|
|
232
243
|
cl.bbox.x += ox;
|
|
233
244
|
cl.bbox.y += oy;
|
|
@@ -236,7 +247,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
236
247
|
}
|
|
237
248
|
}
|
|
238
249
|
};
|
|
239
|
-
const layoutFloat = (resourceId, width) => {
|
|
250
|
+
const layoutFloat = (resourceId, width, slice) => {
|
|
240
251
|
const resource = resourceById.get(resourceId);
|
|
241
252
|
if (!resource)
|
|
242
253
|
return null;
|
|
@@ -249,17 +260,38 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
249
260
|
resourceNumbering,
|
|
250
261
|
resourceTypes,
|
|
251
262
|
resources,
|
|
263
|
+
...(slice ? { slice } : {}),
|
|
252
264
|
});
|
|
253
265
|
};
|
|
266
|
+
/** Row count of a table resource (0 for anything else). */
|
|
267
|
+
const tableRowCount = (resourceId) => resourceById.get(resourceId)?.table?.model.rows.length ?? 0;
|
|
268
|
+
/** The slice a pending float stands for: the whole resource, or — for the
|
|
269
|
+
* rest of a split table — its remaining rows, laid out as the closing
|
|
270
|
+
* slice (no marker; the note). */
|
|
271
|
+
const sliceOf = (f) => f.startRow !== undefined && f.startRow > 0
|
|
272
|
+
? { startRow: f.startRow, endRow: tableRowCount(f.resourceId), continues: false }
|
|
273
|
+
: undefined;
|
|
274
|
+
const sliceKey = (slice) => slice ? `:${slice.startRow}-${slice.endRow}${slice.continues ? '+' : ''}` : '';
|
|
275
|
+
/** Row metrics of a table float laid out in full at a width, memoised. */
|
|
276
|
+
const tableMetricsMemo = new Map();
|
|
277
|
+
const tableMetrics = (resourceId, width) => {
|
|
278
|
+
const key = `${resourceId}:${width.toFixed(2)}`;
|
|
279
|
+
const memo = tableMetricsMemo.get(key);
|
|
280
|
+
if (memo !== undefined)
|
|
281
|
+
return memo;
|
|
282
|
+
const m = layoutFloat(resourceId, width)?.tableRows ?? null;
|
|
283
|
+
tableMetricsMemo.set(key, m);
|
|
284
|
+
return m;
|
|
285
|
+
};
|
|
254
286
|
/** Height (and caption baseline) of a float at a given width, memoised —
|
|
255
287
|
* fit checks run for every pending float on every loop iteration. */
|
|
256
288
|
const floatMeasureMemo = new Map();
|
|
257
|
-
const measureFloat = (resourceId, width) => {
|
|
258
|
-
const key = `${resourceId}:${width.toFixed(2)}`;
|
|
289
|
+
const measureFloat = (resourceId, width, slice) => {
|
|
290
|
+
const key = `${resourceId}:${width.toFixed(2)}${sliceKey(slice)}`;
|
|
259
291
|
const memo = floatMeasureMemo.get(key);
|
|
260
292
|
if (memo !== undefined)
|
|
261
293
|
return memo;
|
|
262
|
-
const laid = layoutFloat(resourceId, width);
|
|
294
|
+
const laid = layoutFloat(resourceId, width, slice);
|
|
263
295
|
let m = null;
|
|
264
296
|
if (laid) {
|
|
265
297
|
// A bottom band aligns the float's LAST text line to the grid: the
|
|
@@ -269,7 +301,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
269
301
|
const rb = laid.block;
|
|
270
302
|
const bodyBottom = rb.bodyRect.y + rb.bodyRect.height;
|
|
271
303
|
let lastBaseline;
|
|
272
|
-
for (const ln of [...rb.captionLines, ...rb.noteLines]) {
|
|
304
|
+
for (const ln of [...rb.captionLines, ...rb.noteLines, ...rb.continuesLines]) {
|
|
273
305
|
if (ln.bbox.y < bodyBottom - 0.5)
|
|
274
306
|
continue;
|
|
275
307
|
if (lastBaseline === undefined || ln.baseline > lastBaseline)
|
|
@@ -285,12 +317,13 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
285
317
|
};
|
|
286
318
|
/** Measure + build a float block at horizontal offset `x` (y = 0), or null
|
|
287
319
|
* when the resource id is unknown. Caller offsets it to its final `y`. */
|
|
288
|
-
const buildFloatBlock = (resourceId, x, width) => {
|
|
289
|
-
const laid = layoutFloat(resourceId, width);
|
|
320
|
+
const buildFloatBlock = (resourceId, x, width, slice) => {
|
|
321
|
+
const laid = layoutFloat(resourceId, width, slice);
|
|
290
322
|
if (!laid)
|
|
291
323
|
return null;
|
|
292
324
|
const { block: rb, totalHeight } = laid;
|
|
293
|
-
const
|
|
325
|
+
const id = slice && slice.startRow > 0 ? `float-${resourceId}-cont-${slice.startRow}` : `float-${resourceId}`;
|
|
326
|
+
const blk = createVDTBlock(id, 'resource', bodyStyle.fontString, bodyStyle.color, bodyStyle.textAlign);
|
|
294
327
|
blk.resourceBlock = rb;
|
|
295
328
|
blk.dirty = false;
|
|
296
329
|
blk.snappedToGrid = false;
|
|
@@ -317,24 +350,136 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
317
350
|
}
|
|
318
351
|
return 'span';
|
|
319
352
|
};
|
|
353
|
+
/** Floats (or slices) placed so far — the progress signal of the drain
|
|
354
|
+
* loop, which a split does not shorten the queue for. */
|
|
355
|
+
let floatsPlaced = 0;
|
|
356
|
+
/**
|
|
357
|
+
* The slice of a table float that fits a fresh band of `avail` px, when
|
|
358
|
+
* the whole (rest of the) table does not: the leading rows within the
|
|
359
|
+
* band, cut where `planTableSlice` allows, verified against the band
|
|
360
|
+
* geometry and shrunk row by row until it fits. `tableStyle.overflow`
|
|
361
|
+
* decides what becomes of the rows left over — a rest float to continue
|
|
362
|
+
* on the next page (`'split'`), nothing (`'clip'`) — or, for `'hide'`,
|
|
363
|
+
* that the table is dropped. Returns null for a figure or a table with
|
|
364
|
+
* nothing to cut, which are force-placed like before.
|
|
365
|
+
*/
|
|
366
|
+
const splitTableFloat = (f, width, position, targetCols, contentArea, avail) => {
|
|
367
|
+
const rowCount = tableRowCount(f.resourceId);
|
|
368
|
+
if (rowCount === 0)
|
|
369
|
+
return null;
|
|
370
|
+
const overflow = resolved.tableStyle.overflow;
|
|
371
|
+
if (overflow === 'hide')
|
|
372
|
+
return 'skip';
|
|
373
|
+
const metrics = tableMetrics(f.resourceId, width);
|
|
374
|
+
if (!metrics)
|
|
375
|
+
return null;
|
|
376
|
+
const startRow = f.startRow ?? 0;
|
|
377
|
+
const firstBody = startRow > 0 ? Math.max(startRow, metrics.headerRowCount) : metrics.headerRowCount;
|
|
378
|
+
if (firstBody >= rowCount)
|
|
379
|
+
return null;
|
|
380
|
+
// Overhead of a continuing slice at this width — caption, gaps, marker —
|
|
381
|
+
// from a one-row probe; the row heights come from the metrics.
|
|
382
|
+
const probe = layoutFloat(f.resourceId, width, { startRow, endRow: firstBody + 1, continues: true });
|
|
383
|
+
if (!probe)
|
|
384
|
+
return null;
|
|
385
|
+
const overhead = probe.totalHeight - probe.block.bodyRect.height;
|
|
386
|
+
// A top band rounds up to the grid: the tallest float it holds within
|
|
387
|
+
// `avail` is the grid multiple below it, less the gap.
|
|
388
|
+
const hMax = Math.floor((avail + 0.01) / baselineGrid) * baselineGrid - floatGapPx;
|
|
389
|
+
let end = planTableSlice(metrics, startRow, hMax - overhead);
|
|
390
|
+
// Nothing fits: carry the smallest slice anyway (it overflows, as a
|
|
391
|
+
// dominating figure would) rather than stall the queue.
|
|
392
|
+
const floor = firstBody + 1;
|
|
393
|
+
if (end < floor)
|
|
394
|
+
end = floor;
|
|
395
|
+
// A last page holding a row or two under a repeated header reads as a
|
|
396
|
+
// stranded tail: give the closing slice at least `MIN_TAIL_ROWS` rows by
|
|
397
|
+
// handing some back from this one (at a breakable edge, not under a
|
|
398
|
+
// group head), when this slice can spare them.
|
|
399
|
+
const tail = rowCount - end;
|
|
400
|
+
if (overflow === 'split' && tail > 0 && tail < MIN_TAIL_ROWS) {
|
|
401
|
+
let e = rowCount - MIN_TAIL_ROWS;
|
|
402
|
+
while (e > floor && (!(metrics.breakableAfter[e - 1] ?? true) || metrics.groupHeaderRow[e - 1]))
|
|
403
|
+
e--;
|
|
404
|
+
if (e >= floor && e >= end - MIN_TAIL_ROWS)
|
|
405
|
+
end = e;
|
|
406
|
+
}
|
|
407
|
+
const fits = (slice) => {
|
|
408
|
+
const measure = measureFloat(f.resourceId, width, slice);
|
|
409
|
+
if (!measure)
|
|
410
|
+
return true;
|
|
411
|
+
const { need } = measureFloatBand(position, measure, targetCols, contentArea, baselineGrid, floatGapPx, (c) => trueBottom(c, uncappedBottoms));
|
|
412
|
+
return need <= avail + 0.01;
|
|
413
|
+
};
|
|
414
|
+
const sliceFor = (e) => ({
|
|
415
|
+
startRow,
|
|
416
|
+
endRow: e,
|
|
417
|
+
continues: e < rowCount && overflow === 'split',
|
|
418
|
+
});
|
|
419
|
+
let slice = sliceFor(end);
|
|
420
|
+
// The caption may wrap differently with its suffix, the closing slice
|
|
421
|
+
// carries the note instead of the marker: verify, backing off a row at
|
|
422
|
+
// a time (over breakable edges) when the band still overflows.
|
|
423
|
+
for (let guard = 0; guard < 8 && end > floor && !fits(slice); guard++) {
|
|
424
|
+
do
|
|
425
|
+
end--;
|
|
426
|
+
while (end > floor && !(metrics.breakableAfter[end - 1] ?? true));
|
|
427
|
+
slice = sliceFor(end);
|
|
428
|
+
}
|
|
429
|
+
const rest = slice.continues ? { ...f, startRow: end } : undefined;
|
|
430
|
+
return rest ? { slice, rest } : { slice };
|
|
431
|
+
};
|
|
320
432
|
/**
|
|
321
433
|
* Reserve a float band on `targetCols` (one column, or every text column
|
|
322
434
|
* of the band for a page-span float) and position the float there.
|
|
323
435
|
* `'fresh'` is the freshly-opened-page rule: keep three lines of text room
|
|
324
436
|
* once a band already holds a float, but force-place a dominating float
|
|
325
|
-
* on an all-text band so the queue always progresses
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
437
|
+
* on an all-text band so the queue always progresses — a table is cut to
|
|
438
|
+
* the band instead and continues on the next page (see
|
|
439
|
+
* {@link splitTableFloat}). `'strict'` is the current-page rule: the band
|
|
440
|
+
* must fit in each column's remaining height (below its content), keeping
|
|
441
|
+
* the text room only next to another band. Only mutates page geometry
|
|
442
|
+
* when it places.
|
|
329
443
|
*/
|
|
330
|
-
|
|
444
|
+
/** The band a float would take in a slot — its height (`need`) and the
|
|
445
|
+
* float's `y` — without reserving it. `null` when the float cannot be
|
|
446
|
+
* measured. */
|
|
447
|
+
const probeFloatBand = (page, f, targetCols, position, pageSpan, anchorToCap) => {
|
|
331
448
|
const first = targetCols[0];
|
|
332
449
|
const width = pageSpan ? page.contentArea.width : first.bbox.width;
|
|
333
450
|
const xLeft = pageSpan ? page.contentArea.x : first.bbox.x;
|
|
334
|
-
const
|
|
451
|
+
const slice = sliceOf(f);
|
|
452
|
+
const measure = measureFloat(f.resourceId, width, slice);
|
|
335
453
|
if (!measure)
|
|
454
|
+
return null;
|
|
455
|
+
// A bottom band normally anchors to the column's true foot (under a
|
|
456
|
+
// trailing cap, the page bottom — the closing-page figure). Before a
|
|
457
|
+
// page-span box the cap IS the band's foot: the figure hugs the text
|
|
458
|
+
// and the box follows both.
|
|
459
|
+
const bottomOf = (c) => anchorToCap ? c.bbox.y + c.bbox.height : trueBottom(c, uncappedBottoms);
|
|
460
|
+
const { need, y } = measureFloatBand(position, measure, targetCols, page.contentArea, baselineGrid, floatGapPx, bottomOf);
|
|
461
|
+
return { need, y, measure, slice, width, xLeft };
|
|
462
|
+
};
|
|
463
|
+
/** Free height `col` keeps for the flow once a float band `probe` is
|
|
464
|
+
* reserved in it at `position` (mirrors the reservation arithmetic of
|
|
465
|
+
* `placeFloatInColumns`; a capped column absorbs a bottom band in its
|
|
466
|
+
* remembered true bottom, so its free height does not change). */
|
|
467
|
+
const availableAfterBand = (col, position, probe, anchorToCap) => {
|
|
468
|
+
if (position === 'top')
|
|
469
|
+
return Math.max(0, col.availableHeight - probe.need);
|
|
470
|
+
if (uncappedBottoms.has(col) && !anchorToCap)
|
|
471
|
+
return col.availableHeight;
|
|
472
|
+
const newHeight = Math.max(0, probe.y - floatGapPx - col.bbox.y);
|
|
473
|
+
return Math.max(0, col.availableHeight - (col.bbox.height - newHeight));
|
|
474
|
+
};
|
|
475
|
+
const placeFloatInColumns = (page, f, targetCols, position, pageSpan, mode, anchorToCap = false) => {
|
|
476
|
+
const first = targetCols[0];
|
|
477
|
+
const probe = probeFloatBand(page, f, targetCols, position, pageSpan, anchorToCap);
|
|
478
|
+
if (!probe)
|
|
336
479
|
return 'skip';
|
|
337
|
-
const {
|
|
480
|
+
const { width, xLeft } = probe;
|
|
481
|
+
let { slice, measure, need, y } = probe;
|
|
482
|
+
let rest;
|
|
338
483
|
if (mode === 'fresh') {
|
|
339
484
|
let minAvail = Infinity;
|
|
340
485
|
let anyReserved = false;
|
|
@@ -346,10 +491,25 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
346
491
|
}
|
|
347
492
|
if (need > minAvail - minTextPx && anyReserved)
|
|
348
493
|
return 'defer';
|
|
494
|
+
if (need > minAvail + 0.01) {
|
|
495
|
+
// Dominating the band: a table is cut to it.
|
|
496
|
+
const split = splitTableFloat(f, width, position, targetCols, page.contentArea, minAvail);
|
|
497
|
+
if (split === 'skip')
|
|
498
|
+
return 'skip';
|
|
499
|
+
if (split) {
|
|
500
|
+
slice = split.slice;
|
|
501
|
+
rest = split.rest;
|
|
502
|
+
const m = measureFloat(f.resourceId, width, slice);
|
|
503
|
+
if (!m)
|
|
504
|
+
return 'skip';
|
|
505
|
+
measure = m;
|
|
506
|
+
({ need, y } = measureFloatBand(position, measure, targetCols, page.contentArea, baselineGrid, floatGapPx, (c) => trueBottom(c, uncappedBottoms)));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
349
509
|
}
|
|
350
510
|
else {
|
|
351
511
|
for (const c of targetCols) {
|
|
352
|
-
if (position === 'bottom' && uncappedBottoms.has(c)) {
|
|
512
|
+
if (position === 'bottom' && uncappedBottoms.has(c) && !anchorToCap) {
|
|
353
513
|
// Trailing cap: the band must lie entirely below the level cut.
|
|
354
514
|
if (y - floatGapPx < c.bbox.y + c.bbox.height - 0.01)
|
|
355
515
|
return 'defer';
|
|
@@ -359,7 +519,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
359
519
|
return 'defer';
|
|
360
520
|
}
|
|
361
521
|
}
|
|
362
|
-
const built = buildFloatBlock(f.resourceId, xLeft, width);
|
|
522
|
+
const built = buildFloatBlock(f.resourceId, xLeft, width, slice);
|
|
363
523
|
if (!built)
|
|
364
524
|
return 'skip';
|
|
365
525
|
for (const col of targetCols) {
|
|
@@ -373,7 +533,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
373
533
|
}
|
|
374
534
|
else {
|
|
375
535
|
const capped = uncappedBottoms.get(col);
|
|
376
|
-
if (capped !== undefined) {
|
|
536
|
+
if (capped !== undefined && !anchorToCap) {
|
|
377
537
|
uncappedBottoms.set(col, capped - need);
|
|
378
538
|
}
|
|
379
539
|
else {
|
|
@@ -381,6 +541,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
381
541
|
const reserved = col.bbox.height - newHeight;
|
|
382
542
|
col.bbox.height = newHeight;
|
|
383
543
|
col.availableHeight = Math.max(0, col.availableHeight - reserved);
|
|
544
|
+
// Anchored to a cap: the band is the column's content down to
|
|
545
|
+
// the cut; the true bottom shrinks by it too, so a span box
|
|
546
|
+
// measuring its room never cuts across the figure.
|
|
547
|
+
if (capped !== undefined)
|
|
548
|
+
uncappedBottoms.set(col, capped - need);
|
|
384
549
|
}
|
|
385
550
|
r.bottom += need;
|
|
386
551
|
}
|
|
@@ -391,7 +556,21 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
391
556
|
built.block.pageIndex = page.index;
|
|
392
557
|
built.block.columnIndex = first.index;
|
|
393
558
|
(page.floats ??= []).push(built.block);
|
|
394
|
-
|
|
559
|
+
floatsPlaced++;
|
|
560
|
+
return rest ? { rest } : 'placed';
|
|
561
|
+
};
|
|
562
|
+
/** Apply a slot outcome to the queue at `i`: drop a placed float, keep a
|
|
563
|
+
* deferred one, swap in the rest of a split table. Returns the index to
|
|
564
|
+
* continue from. */
|
|
565
|
+
const settle = (i, r) => {
|
|
566
|
+
if (r === 'defer')
|
|
567
|
+
return i + 1;
|
|
568
|
+
if (typeof r === 'object') {
|
|
569
|
+
pendingFloats[i] = r.rest;
|
|
570
|
+
return i + 1;
|
|
571
|
+
}
|
|
572
|
+
pendingFloats.splice(i, 1);
|
|
573
|
+
return i;
|
|
395
574
|
};
|
|
396
575
|
const positionsFor = (f) => f.position === 'auto' ? ['top', 'bottom'] : [f.position];
|
|
397
576
|
/** Reserve top/bottom bands on a freshly opened page and position as many
|
|
@@ -433,21 +612,76 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
433
612
|
if (r !== 'defer')
|
|
434
613
|
break;
|
|
435
614
|
}
|
|
436
|
-
|
|
437
|
-
i++;
|
|
438
|
-
else
|
|
439
|
-
pendingFloats.splice(i, 1);
|
|
615
|
+
i = settle(i, r);
|
|
440
616
|
}
|
|
441
617
|
}
|
|
442
618
|
};
|
|
619
|
+
/** The keep-together box content block `idx` opens, when it is one that
|
|
620
|
+
* lays out inline in a column of the current band (`span: 'column'`,
|
|
621
|
+
* placement `here`): its height at a column width, so the floats placed
|
|
622
|
+
* right before it can tell whether a slot would starve it. */
|
|
623
|
+
const keepTogetherBoxAt = (idx) => {
|
|
624
|
+
const b = contentBlocks[idx];
|
|
625
|
+
if (!b || b.type !== 'containerStart' || b.containerName !== 'callout')
|
|
626
|
+
return null;
|
|
627
|
+
const plan = calloutPlan.get(idx);
|
|
628
|
+
const style = plan ? pickCalloutStyle(resolved.calloutStyles, plan.attrs.type) : undefined;
|
|
629
|
+
if (!plan || !style || !style.keepTogether)
|
|
630
|
+
return null;
|
|
631
|
+
const { span, placement } = resolveCalloutAttrs(style, plan.attrs);
|
|
632
|
+
if (placement !== 'here')
|
|
633
|
+
return null;
|
|
634
|
+
const page = doc.pages[cursor.pageIndex];
|
|
635
|
+
if (span === 'page' && bandColumns(page, currentBand(page, cursor)).length > 1)
|
|
636
|
+
return null;
|
|
637
|
+
const L = makeCalloutLayouter(idx, plan, style);
|
|
638
|
+
const heights = new Map();
|
|
639
|
+
return {
|
|
640
|
+
heightAt: (width) => {
|
|
641
|
+
const key = Math.round(width * 100);
|
|
642
|
+
let h = heights.get(key);
|
|
643
|
+
if (h === undefined) {
|
|
644
|
+
const r = L.layoutRange(CUT_START, L.end, width, 'probe', false);
|
|
645
|
+
h = r.totalHeight + Math.max(pendingSpacing, r.marginTopPx);
|
|
646
|
+
heights.set(key, h);
|
|
647
|
+
}
|
|
648
|
+
return h;
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
};
|
|
652
|
+
/** Whether reserving float `f` in `slot` would leave the keep-together
|
|
653
|
+
* box that comes next with no column of the current band to land in —
|
|
654
|
+
* when, without the float, one of them (the cursor's, or an empty one
|
|
655
|
+
* after it) still holds it. A float yields to such a box, as a
|
|
656
|
+
* compositor would: the box stays in flow and the figure takes the next
|
|
657
|
+
* slot (usually the next page) instead of pushing the box off the page
|
|
658
|
+
* and leaving the column with the figure alone. */
|
|
659
|
+
const slotStarvesBox = (page, f, slot, box, anchorToCap) => {
|
|
660
|
+
const cursorCol = page.columns[cursor.columnIndex];
|
|
661
|
+
if (!cursorCol)
|
|
662
|
+
return false;
|
|
663
|
+
const candidates = bandColumns(page, currentBand(page, cursor))
|
|
664
|
+
.filter((c) => c.kind !== 'span' && c.index >= cursorCol.index && (c === cursorCol || c.blocks.length === 0));
|
|
665
|
+
const fitsIn = (c, available) => available >= box.heightAt(c.bbox.width) - 0.01;
|
|
666
|
+
if (!candidates.some((c) => fitsIn(c, c.availableHeight)))
|
|
667
|
+
return false;
|
|
668
|
+
const probe = probeFloatBand(page, f, slot.cols, slot.position, slot.pageSpan, anchorToCap);
|
|
669
|
+
if (!probe)
|
|
670
|
+
return false;
|
|
671
|
+
const after = (c) => slot.cols.includes(c) ? availableAfterBand(c, slot.position, probe, anchorToCap) : c.availableHeight;
|
|
672
|
+
return !candidates.some((c) => fitsIn(c, after(c)));
|
|
673
|
+
};
|
|
443
674
|
/** Offer every pending float the free slots of the current page after the
|
|
444
675
|
* cursor (bottom of the referencing column, top / bottom of the next
|
|
445
676
|
* empty columns; the band bottom for page-span floats). Runs before each
|
|
446
|
-
* block is placed, so a float lands in the first gap after its reference.
|
|
447
|
-
|
|
677
|
+
* block is placed, so a float lands in the first gap after its reference.
|
|
678
|
+
* `nextBlockIdx` is that block: a keep-together box it opens holds the
|
|
679
|
+
* slots that would starve it (see `slotStarvesBox`). */
|
|
680
|
+
const tryPlacePendingFloatsOnCurrentPage = (preferTop = false, nextBlockIdx) => {
|
|
448
681
|
if (pendingFloats.length === 0)
|
|
449
682
|
return;
|
|
450
683
|
const page = doc.pages[cursor.pageIndex];
|
|
684
|
+
const box = nextBlockIdx !== undefined ? keepTogetherBoxAt(nextBlockIdx) : null;
|
|
451
685
|
for (let i = 0; i < pendingFloats.length;) {
|
|
452
686
|
const f = pendingFloats[i];
|
|
453
687
|
let r = 'defer';
|
|
@@ -455,18 +689,24 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
455
689
|
// A page-span box comes next: the head of an empty column keeps the
|
|
456
690
|
// band cuttable under the float (the box then sits below both the
|
|
457
691
|
// text and the figure), where the referencing column's foot would
|
|
458
|
-
// wall the box off the page.
|
|
692
|
+
// wall the box off the page. A page-span figure waits for the box
|
|
693
|
+
// itself, which cuts the band under the text and sets the figure
|
|
694
|
+
// there, above the box (`placeSpanFloatsAtCut`) — its only slot here
|
|
695
|
+
// would be the band's foot, under the box.
|
|
696
|
+
if (preferTop && f.span === 'page' && bandColumns(page, currentBand(page, cursor)).length > 1) {
|
|
697
|
+
i++;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
459
700
|
if (preferTop)
|
|
460
701
|
slots = [...slots.filter((s) => s.position === 'top'), ...slots.filter((s) => s.position !== 'top')];
|
|
461
702
|
for (const slot of slots) {
|
|
462
|
-
|
|
703
|
+
if (box && slotStarvesBox(page, f, slot, box, preferTop))
|
|
704
|
+
continue;
|
|
705
|
+
r = placeFloatInColumns(page, f, slot.cols, slot.position, slot.pageSpan, 'strict', preferTop);
|
|
463
706
|
if (r !== 'defer')
|
|
464
707
|
break;
|
|
465
708
|
}
|
|
466
|
-
|
|
467
|
-
i++;
|
|
468
|
-
else
|
|
469
|
-
pendingFloats.splice(i, 1);
|
|
709
|
+
i = settle(i, r);
|
|
470
710
|
}
|
|
471
711
|
};
|
|
472
712
|
/** Reserve floats on each freshly opened content page. Passed only to the
|
|
@@ -481,7 +721,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
481
721
|
return false;
|
|
482
722
|
const plan = calloutPlan.get(idx);
|
|
483
723
|
const style = plan ? pickCalloutStyle(resolved.calloutStyles, plan.attrs.type) : undefined;
|
|
484
|
-
if (!style
|
|
724
|
+
if (!style)
|
|
725
|
+
return false;
|
|
726
|
+
const { span, placement } = resolveCalloutAttrs(style, plan.attrs);
|
|
727
|
+
if (span !== 'page' || placement === 'fixed')
|
|
485
728
|
return false;
|
|
486
729
|
const page = doc.pages[cursor.pageIndex];
|
|
487
730
|
return bandColumns(page, currentBand(page, cursor)).length > 1;
|
|
@@ -494,12 +737,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
494
737
|
tryPlacePendingFloatsOnCurrentPage();
|
|
495
738
|
let guard = 0;
|
|
496
739
|
while (pendingFloats.length > 0 && guard++ < 1000) {
|
|
497
|
-
const before =
|
|
740
|
+
const before = floatsPlaced;
|
|
498
741
|
const startPageIndex = cursor.pageIndex;
|
|
499
742
|
do {
|
|
500
743
|
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
501
744
|
} while (cursor.pageIndex === startPageIndex);
|
|
502
|
-
if (
|
|
745
|
+
if (floatsPlaced === before)
|
|
503
746
|
break; // safety: no progress
|
|
504
747
|
}
|
|
505
748
|
};
|
|
@@ -637,11 +880,22 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
637
880
|
const aroundZone = zone ? bandCapLinesAroundZone(cols, baselineGrid, zone, (c) => columnBottom(c, uncappedBottoms)) : null;
|
|
638
881
|
if (aroundZone === null && Math.max(...bottoms) - Math.min(...bottoms) <= baselineGrid + 0.5)
|
|
639
882
|
return;
|
|
883
|
+
// Float bands reserved at the columns' feet are the band's content too:
|
|
884
|
+
// the level cut must leave them room under the text (a figure placed
|
|
885
|
+
// before a span box then anchors to the cut, see `anchorToCap`).
|
|
886
|
+
const footBands = cols.reduce((sum, c) => sum + reservedOf(c).bottom, 0);
|
|
887
|
+
// A figure heading an otherwise empty column keeps its slot: the cut
|
|
888
|
+
// goes no higher than the band it takes, or the capped pass evicts it
|
|
889
|
+
// to a page of its own.
|
|
890
|
+
const top = bandTop(cols);
|
|
891
|
+
const floatHeads = cols
|
|
892
|
+
.filter((c) => c.blocks.length === 0 && reservedOf(c).top > 0)
|
|
893
|
+
.map((c) => Math.ceil((c.bbox.y - top - 0.01) / baselineGrid));
|
|
640
894
|
bandCapProposals.set(boundaryIndex, {
|
|
641
895
|
kind: 'trailing',
|
|
642
896
|
startContentIndex: bandStart.contentIndex,
|
|
643
897
|
startPart: bandStart.part,
|
|
644
|
-
lines: aroundZone ?? bandCapLines(cols, baselineGrid),
|
|
898
|
+
lines: aroundZone ?? Math.max(bandCapLines(cols, baselineGrid, footBands), ...floatHeads),
|
|
645
899
|
retries: 0,
|
|
646
900
|
...(aroundZone !== null ? { zone } : {}),
|
|
647
901
|
});
|
|
@@ -734,6 +988,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
734
988
|
doc.blocks.push(child);
|
|
735
989
|
}
|
|
736
990
|
};
|
|
991
|
+
const CUT_START = { child: 0, line: 0 };
|
|
737
992
|
const makeCalloutLayouter = (startIdx, plan, style) => {
|
|
738
993
|
const children = contentBlocks.slice(startIdx + 1, plan.endIdx);
|
|
739
994
|
const realAt = [];
|
|
@@ -743,12 +998,15 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
743
998
|
});
|
|
744
999
|
const layoutRange = (from, to, width, frameId, continuation) => {
|
|
745
1000
|
let n = 0;
|
|
1001
|
+
// A cut inside child `to.child` includes that child (its first
|
|
1002
|
+
// `to.line` lines); a cut at a child's head excludes it.
|
|
1003
|
+
const toChild = to.line > 0 ? to.child + 1 : to.child;
|
|
746
1004
|
return layoutCallout({
|
|
747
1005
|
style,
|
|
748
1006
|
attrs: plan.attrs,
|
|
749
1007
|
continuation,
|
|
750
|
-
children: children.slice(from,
|
|
751
|
-
childStartIdx: startIdx + 1 + from,
|
|
1008
|
+
children: children.slice(from.child, toChild),
|
|
1009
|
+
childStartIdx: startIdx + 1 + from.child,
|
|
752
1010
|
width,
|
|
753
1011
|
ctx: measureCtx,
|
|
754
1012
|
resolved,
|
|
@@ -756,49 +1014,64 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
756
1014
|
frameId,
|
|
757
1015
|
nextChildId: () => `${frameId}-c${n++}`,
|
|
758
1016
|
paragraphStyleFor: (idx) => paragraphContainers.byBlock[idx]?.style,
|
|
1017
|
+
...(from.line > 0 ? { lineFrom: from.line } : {}),
|
|
1018
|
+
...(to.line > 0 ? { lineTo: to.line } : {}),
|
|
759
1019
|
});
|
|
760
1020
|
};
|
|
761
|
-
return { children, childBase: startIdx + 1, realAt, layoutRange };
|
|
1021
|
+
return { children, childBase: startIdx + 1, realAt, end: { child: children.length, line: 0 }, layoutRange };
|
|
762
1022
|
};
|
|
763
|
-
/** The longest leading fragment of the
|
|
764
|
-
* is at most `roomPx` tall
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const full = L.layoutRange(from, L.children.length, width, frameId, continuation);
|
|
1023
|
+
/** The longest leading fragment of the box from cut `from` on whose box
|
|
1024
|
+
* is at most `roomPx` tall. Cuts fall between children or between the
|
|
1025
|
+
* lines of a text child, and every fragment keeps at least the style's
|
|
1026
|
+
* `splitMinLines` lines on its side of the cut (a figure or a display
|
|
1027
|
+
* formula counts as one line). The full layout's geometry ranks the
|
|
1028
|
+
* candidates (box bottom = content bottom at the cut + the box's tail
|
|
1029
|
+
* below its last child); the deepest candidate that fits is laid out
|
|
1030
|
+
* for real and taken when it truly fits. `null` when none does. */
|
|
1031
|
+
const splitCalloutFragment = (L, from, width, roomPx, frameId, continuation, minLines) => {
|
|
1032
|
+
const full = L.layoutRange(from, L.end, width, frameId, continuation);
|
|
774
1033
|
const lastChild = full.children[full.children.length - 1];
|
|
775
1034
|
if (!lastChild)
|
|
776
1035
|
return null;
|
|
777
1036
|
const tail = full.totalHeight - (lastChild.bbox.y + lastChild.bbox.height);
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1037
|
+
const totalLines = full.children.reduce((n, c) => n + Math.max(1, c.lines.length), 0);
|
|
1038
|
+
/** Candidate cuts with the head's content bottom and line count. */
|
|
1039
|
+
const candidates = [];
|
|
1040
|
+
let linesBefore = 0;
|
|
1041
|
+
for (let i = 0; i < full.children.length; i++) {
|
|
1042
|
+
const c = full.children[i];
|
|
1043
|
+
const k = (c.contentIndex ?? L.childBase) - L.childBase;
|
|
1044
|
+
const cuttable = c.type !== 'resource' && c.type !== 'mathDisplay' && c.lines.length > 1;
|
|
1045
|
+
// The first laid-out child of a continuation opens after `from.line`
|
|
1046
|
+
// lines: cuts inside it are counted from the child's own head.
|
|
1047
|
+
const lineBase = k === from.child ? from.line : 0;
|
|
1048
|
+
if (cuttable) {
|
|
1049
|
+
for (let l = 1; l < c.lines.length; l++) {
|
|
1050
|
+
const line = c.lines[l - 1];
|
|
1051
|
+
candidates.push({ cut: { child: k, line: lineBase + l }, bottom: line.bbox.y + line.bbox.height, headLines: linesBefore + l });
|
|
784
1052
|
}
|
|
785
1053
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1054
|
+
linesBefore += Math.max(1, c.lines.length);
|
|
1055
|
+
if (i < full.children.length - 1) {
|
|
1056
|
+
candidates.push({ cut: { child: k + 1, line: 0 }, bottom: c.bbox.y + c.bbox.height, headLines: linesBefore });
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const min = Math.max(1, minLines);
|
|
1060
|
+
const viable = candidates
|
|
1061
|
+
.filter((c) => c.headLines >= min && totalLines - c.headLines >= min)
|
|
1062
|
+
.sort((a, b) => b.bottom - a.bottom);
|
|
1063
|
+
for (const c of viable) {
|
|
1064
|
+
if (c.bottom + tail > roomPx + 0.01)
|
|
1065
|
+
continue;
|
|
1066
|
+
const result = L.layoutRange(from, c.cut, width, frameId, continuation);
|
|
794
1067
|
if (result.totalHeight <= roomPx + 0.01)
|
|
795
|
-
return { to, result };
|
|
1068
|
+
return { to: c.cut, result };
|
|
796
1069
|
}
|
|
797
1070
|
return null;
|
|
798
1071
|
};
|
|
799
|
-
/** Absolute content indices of the children
|
|
800
|
-
* the fragment's source range. */
|
|
801
|
-
const fragmentRange = (L, from, to) => ({ firstChildIdx: L.childBase + from, lastChildIdx: L.childBase + to - 1 });
|
|
1072
|
+
/** Absolute content indices of the children a fragment from cut `from`
|
|
1073
|
+
* to cut `to` of `L` touches, for the fragment's source range. */
|
|
1074
|
+
const fragmentRange = (L, from, to) => ({ firstChildIdx: L.childBase + from.child, lastChildIdx: L.childBase + (to.line > 0 ? to.child : to.child - 1) });
|
|
802
1075
|
const markFragment = (result, part, continued) => {
|
|
803
1076
|
if (result.frame.callout) {
|
|
804
1077
|
result.frame.callout.part = part;
|
|
@@ -879,9 +1152,8 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
879
1152
|
* referenced by the very block before the box, so a cut that spills
|
|
880
1153
|
* that block into the figure's column leaves the figure no slot after
|
|
881
1154
|
* 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
|
|
883
|
-
*
|
|
884
|
-
* earlier keeps the cap route, which flows text under it. */
|
|
1155
|
+
* box then cuts under the text and the figure alike; a figure
|
|
1156
|
+
* referenced earlier keeps the cap route, which flows text under it. */
|
|
885
1157
|
const lastBlockBefore = () => {
|
|
886
1158
|
let j = startIdx - 1;
|
|
887
1159
|
while (j >= 0 && isMarkerBlock(contentBlocks[j]))
|
|
@@ -897,13 +1169,69 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
897
1169
|
return false;
|
|
898
1170
|
if (!isBandLevel(textCols))
|
|
899
1171
|
return false;
|
|
900
|
-
const textBottom = bandUsedBottom(textCols);
|
|
901
1172
|
const before = lastBlockBefore();
|
|
902
|
-
|
|
1173
|
+
// However tall the figure is: the text before the box is all placed,
|
|
1174
|
+
// so no cut could level the columns any better — the box goes under
|
|
1175
|
+
// both, the slack under the text being the compositor's trade.
|
|
1176
|
+
return floatOnly.every((c) => topFloatRefOf.get(c) === before);
|
|
1177
|
+
};
|
|
1178
|
+
/**
|
|
1179
|
+
* A page-span figure referenced before the box takes the cut first: the
|
|
1180
|
+
* band is closed level under the text, the figure spans the page right
|
|
1181
|
+
* there (where the text ended, as the compositor reads it), and the box
|
|
1182
|
+
* goes on below — or to the next page when it no longer fits. Only when
|
|
1183
|
+
* the band can be cut for the box (level, or capped for it) and the
|
|
1184
|
+
* figure fits between the cut and the band bottom; otherwise the figure
|
|
1185
|
+
* stays pending for the ordinary slots. Returns whether any was set.
|
|
1186
|
+
*/
|
|
1187
|
+
const placeSpanFloatsAtCut = (page, capActiveHere) => {
|
|
1188
|
+
let placedAny = false;
|
|
1189
|
+
for (let i = 0; i < pendingFloats.length;) {
|
|
1190
|
+
const f = pendingFloats[i];
|
|
1191
|
+
const cols = bandColumns(page, currentBand(page, cursor));
|
|
1192
|
+
if (f.span !== 'page' || cols.length < 2 || !((capActiveHere && !placedAny) || levelForBox(cols))) {
|
|
1193
|
+
i++;
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
const width = page.contentArea.width;
|
|
1197
|
+
const slice = sliceOf(f);
|
|
1198
|
+
const measure = measureFloat(f.resourceId, width, slice);
|
|
1199
|
+
if (!measure) {
|
|
1200
|
+
i++;
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
const cutY = gridUp(page, bandUsedBottom(cols));
|
|
1204
|
+
const spacing = cols.some((c) => c.blocks.length > 0) ? floatGapPx : 0;
|
|
1205
|
+
const need = needFor(spacing, measure.height, floatGapPx);
|
|
1206
|
+
const bandBottom = Math.min(...cols.map((c) => columnBottom(c, uncappedBottoms)));
|
|
1207
|
+
if (cutY + need > bandBottom + 0.01) {
|
|
1208
|
+
i++;
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
const built = buildFloatBlock(f.resourceId, page.contentArea.x, width, slice);
|
|
1212
|
+
if (!built) {
|
|
1213
|
+
i++;
|
|
1214
|
+
continue;
|
|
1215
|
+
}
|
|
1216
|
+
if (capActiveHere && !placedAny) {
|
|
1217
|
+
uncapBand(cols, uncappedBottoms);
|
|
1218
|
+
spanPlacedInBand.add(startIdx);
|
|
1219
|
+
}
|
|
1220
|
+
closeBandAndInsertSpan(page, cols, cutY, built.block, need, cursor, spacing, built.height);
|
|
1221
|
+
// The float was built at (x, 0): move its inner geometry down to
|
|
1222
|
+
// where the span column put it.
|
|
1223
|
+
offsetResourceBlockToAbsolute(built.block.resourceBlock, 0, built.block.bbox.y);
|
|
1224
|
+
built.block.contentIndex = f.firstBlockIdx;
|
|
1225
|
+
doc.blocks.push(built.block);
|
|
1226
|
+
floatsPlaced++;
|
|
1227
|
+
placedAny = true;
|
|
1228
|
+
pendingFloats.splice(i, 1);
|
|
1229
|
+
}
|
|
1230
|
+
return placedAny;
|
|
903
1231
|
};
|
|
904
|
-
/**
|
|
905
|
-
*
|
|
906
|
-
let from =
|
|
1232
|
+
/** Cut the fragment to place starts at, and its 0-based index among
|
|
1233
|
+
* the fragments (0 = the box, or its head). */
|
|
1234
|
+
let from = CUT_START;
|
|
907
1235
|
let part = 0;
|
|
908
1236
|
let frameId = firstFrameId;
|
|
909
1237
|
/** The box already moved to a fresh page (or sits on an empty one):
|
|
@@ -912,7 +1240,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
912
1240
|
for (;;) {
|
|
913
1241
|
let page = doc.pages[cursor.pageIndex];
|
|
914
1242
|
const continuation = part > 0;
|
|
915
|
-
const layoutAt = (width) => L.layoutRange(from, L.
|
|
1243
|
+
const layoutAt = (width) => L.layoutRange(from, L.end, width, frameId, continuation);
|
|
916
1244
|
const result = layoutAt(page.contentArea.width);
|
|
917
1245
|
/** Where the box would cut the current band, and whether it fits (room
|
|
918
1246
|
* is measured against the columns' TRUE bottoms — a capped band keeps
|
|
@@ -936,11 +1264,70 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
936
1264
|
// Is this band capped for this very box? Then it cuts at the band's
|
|
937
1265
|
// used bottom (at most the cap) even when the last column is short.
|
|
938
1266
|
const cap = part === 0 ? bandCaps?.get(startIdx) : undefined;
|
|
939
|
-
|
|
1267
|
+
let capActive = cap !== undefined
|
|
940
1268
|
&& activeCap !== null
|
|
941
1269
|
&& activeCap.spanIndex === startIdx
|
|
942
1270
|
&& activeCap.pageIndex === page.index
|
|
943
1271
|
&& activeCap.band === currentBand(page, cursor);
|
|
1272
|
+
// A page-span figure referenced before the box cuts the band first
|
|
1273
|
+
// and the box measures the fresh band under it. A barrier box then
|
|
1274
|
+
// takes every other pending float — the current page's free slots,
|
|
1275
|
+
// else pages opened ahead — before it lands.
|
|
1276
|
+
if (part === 0) {
|
|
1277
|
+
if (placeSpanFloatsAtCut(page, capActive))
|
|
1278
|
+
capActive = false;
|
|
1279
|
+
if (style.floatBarrier && pendingFloats.length > 0) {
|
|
1280
|
+
const pageBefore = cursor.pageIndex;
|
|
1281
|
+
const bandBefore = currentBand(page, cursor);
|
|
1282
|
+
// An uneven, uncapped band the figures are about to leave behind
|
|
1283
|
+
// gets the cap that levels it: a span cap when a page-span figure
|
|
1284
|
+
// would fit under the level cut — the capped pass sets it there
|
|
1285
|
+
// and the box follows — else a trailing cap, the figure and the
|
|
1286
|
+
// box moving on and the band ending level like a closing one.
|
|
1287
|
+
// Only a page-span figure is planned for here; column figures keep
|
|
1288
|
+
// the ordinary slots (their level is the band cap's own business).
|
|
1289
|
+
const first = pendingFloats.find((f) => f.span === 'page');
|
|
1290
|
+
if (first && !capActive && cap === undefined && bandStart && registeredBand
|
|
1291
|
+
&& registeredBand.pageIndex === page.index && registeredBand.band === bandBefore) {
|
|
1292
|
+
const cols = bandColumns(page, bandBefore).filter((c) => c.bbox.height > 0.5);
|
|
1293
|
+
if (cols.length > 1 && !isBandLevel(cols) && cols.some((c) => c.blocks.length > 0)) {
|
|
1294
|
+
const lines = bandCapLines(cols, baselineGrid);
|
|
1295
|
+
const capBottom = bandTop(cols) + lines * baselineGrid;
|
|
1296
|
+
const bandBottom = Math.min(...cols.map((c) => columnBottom(c, uncappedBottoms)));
|
|
1297
|
+
const m = measureFloat(first.resourceId, page.contentArea.width, sliceOf(first));
|
|
1298
|
+
const figureFits = m !== null && capBottom + needFor(floatGapPx, m.height, floatGapPx) <= bandBottom + 0.01;
|
|
1299
|
+
if (figureFits) {
|
|
1300
|
+
bandCapProposals.set(startIdx, {
|
|
1301
|
+
kind: 'span',
|
|
1302
|
+
startContentIndex: bandStart.contentIndex,
|
|
1303
|
+
startPart: bandStart.part,
|
|
1304
|
+
lines,
|
|
1305
|
+
retries: 0,
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
else {
|
|
1309
|
+
proposeTrailingCap(startIdx);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
tryPlacePendingFloatsOnCurrentPage();
|
|
1314
|
+
drainPendingFloats();
|
|
1315
|
+
if (cursor.pageIndex !== pageBefore || currentBand(doc.pages[cursor.pageIndex], cursor) !== bandBefore) {
|
|
1316
|
+
// A figure took a page ahead and the box follows it there. A
|
|
1317
|
+
// band cut level for it (trailing cap) counts as delivered —
|
|
1318
|
+
// the columns stay cut — and balancing never stretches the
|
|
1319
|
+
// page's last column back to the bottom. A span cap that could
|
|
1320
|
+
// not seat the figure is left undelivered for the driver to
|
|
1321
|
+
// grow or drop.
|
|
1322
|
+
if (capActive && cap?.kind === 'trailing')
|
|
1323
|
+
spanPlacedInBand.add(startIdx);
|
|
1324
|
+
if (doc.pages[pageBefore].columns.some((c) => c.blocks.length > 0))
|
|
1325
|
+
forcedBreakPages.add(pageBefore);
|
|
1326
|
+
}
|
|
1327
|
+
capActive = false;
|
|
1328
|
+
}
|
|
1329
|
+
page = doc.pages[cursor.pageIndex];
|
|
1330
|
+
}
|
|
944
1331
|
const fit = forceHere ? (measureBand(true) ?? measureBand(false)) : measureBand(!capActive);
|
|
945
1332
|
let action = null;
|
|
946
1333
|
if (fit) {
|
|
@@ -960,21 +1347,30 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
960
1347
|
band = measureBand(false);
|
|
961
1348
|
}
|
|
962
1349
|
if (band) {
|
|
963
|
-
const fragment = splitCalloutFragment(L, from, page.contentArea.width, band.roomPx - band.spacing, frameId, continuation);
|
|
1350
|
+
const fragment = splitCalloutFragment(L, from, page.contentArea.width, band.roomPx - band.spacing, frameId, continuation, style.splitMinLines);
|
|
964
1351
|
if (fragment) {
|
|
965
1352
|
action = { kind: 'split', fit: band, need: needFor(band.spacing, fragment.result.totalHeight, 0), fragment };
|
|
966
1353
|
}
|
|
967
1354
|
}
|
|
968
1355
|
}
|
|
969
|
-
if (!action && forceHere && fit)
|
|
1356
|
+
if (!action && forceHere && fit) {
|
|
970
1357
|
action = { kind: 'whole', fit, need: fit.need, result };
|
|
1358
|
+
(doc.warnings ??= []).push({
|
|
1359
|
+
kind: 'calloutOverflow',
|
|
1360
|
+
pageIndex: cursor.pageIndex,
|
|
1361
|
+
columnIndex: cursor.columnIndex,
|
|
1362
|
+
sourceStart: contentBlocks[startIdx].sourceStart + bodyOffset,
|
|
1363
|
+
sourceEnd: contentBlocks[plan.endIdx].sourceEnd + bodyOffset,
|
|
1364
|
+
overflowPx: Math.max(0, fit.spacing + result.totalHeight - fit.roomPx),
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
971
1367
|
if (action) {
|
|
972
1368
|
if (capActive) {
|
|
973
1369
|
uncapBand(action.fit.cols, uncappedBottoms);
|
|
974
1370
|
spanPlacedInBand.add(startIdx);
|
|
975
1371
|
}
|
|
976
1372
|
const placed = action.kind === 'whole' ? action.result : action.fragment.result;
|
|
977
|
-
const to = action.kind === 'whole' ? L.
|
|
1373
|
+
const to = action.kind === 'whole' ? L.end : action.fragment.to;
|
|
978
1374
|
if (part > 0 || action.kind === 'split')
|
|
979
1375
|
markFragment(placed, part, action.kind === 'split');
|
|
980
1376
|
const spanCol = closeBandAndInsertSpan(page, action.fit.cols, action.fit.cutY, placed.frame, action.need, cursor, action.fit.spacing, placed.totalHeight);
|
|
@@ -1011,13 +1407,23 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1011
1407
|
// (or, for a splittable box, for the whole box flush with the
|
|
1012
1408
|
// band bottom).
|
|
1013
1409
|
const cols = bandColumns(page, currentBand(page, cursor));
|
|
1014
|
-
|
|
1015
|
-
|
|
1410
|
+
// A column holding only a figure at its head must keep that slot
|
|
1411
|
+
// in the capped pass: the cut can go no higher than the band the
|
|
1412
|
+
// figure takes, or the figure falls off the page and the box after it.
|
|
1413
|
+
const top = bandTop(cols);
|
|
1414
|
+
const floatHeads = cols
|
|
1415
|
+
.filter((c) => c.blocks.length === 0 && reservedOf(c).top > 0)
|
|
1416
|
+
.map((c) => Math.ceil((c.bbox.y - top - 0.01) / baselineGrid));
|
|
1417
|
+
const lines = Math.max(bandCapLines(cols, baselineGrid), ...floatHeads);
|
|
1418
|
+
const capBottom = top + lines * baselineGrid;
|
|
1016
1419
|
const spacing = Math.max(pendingSpacing, result.marginTopPx);
|
|
1017
1420
|
const need = needFor(spacing, result.totalHeight, result.marginBottomPx);
|
|
1018
1421
|
const bandBottom = Math.min(...cols.map((c) => columnBottom(c, uncappedBottoms)));
|
|
1422
|
+
// A splittable box is worth the cut when its head fits flush with
|
|
1423
|
+
// the band bottom, the rest going on to the next page.
|
|
1019
1424
|
const fitsAfterCut = capBottom + need + minRoomPx <= bandBottom + 0.01
|
|
1020
|
-
|| (splittable && capBottom + needFor(spacing, result.totalHeight, 0) <= bandBottom + 0.01)
|
|
1425
|
+
|| (splittable && capBottom + needFor(spacing, result.totalHeight, 0) <= bandBottom + 0.01)
|
|
1426
|
+
|| (splittable && splitCalloutFragment(L, from, page.contentArea.width, bandBottom - capBottom - spacing, frameId, continuation, style.splitMinLines) !== null);
|
|
1021
1427
|
if (fitsAfterCut) {
|
|
1022
1428
|
bandCapProposals.set(startIdx, {
|
|
1023
1429
|
kind: 'span',
|
|
@@ -1036,7 +1442,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1036
1442
|
proposeTrailingCap(startIdx);
|
|
1037
1443
|
markForcedBreak();
|
|
1038
1444
|
}
|
|
1039
|
-
else if (capActive && cap
|
|
1445
|
+
else if (capActive && cap?.kind === 'trailing') {
|
|
1040
1446
|
// Reached inside the band cut level for it: delivered even though
|
|
1041
1447
|
// the box moves on — the columns stay cut.
|
|
1042
1448
|
spanPlacedInBand.add(startIdx);
|
|
@@ -1261,7 +1667,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1261
1667
|
const firstFrameId = `block-${blockIdCounter++}`;
|
|
1262
1668
|
const { span, placement } = resolveCalloutAttrs(style, plan.attrs);
|
|
1263
1669
|
const L = makeCalloutLayouter(startIdx, plan, style);
|
|
1264
|
-
const layoutAt = (width) => L.layoutRange(
|
|
1670
|
+
const layoutAt = (width) => L.layoutRange(CUT_START, L.end, width, firstFrameId, false);
|
|
1265
1671
|
// Fixed boxes leave the flow entirely.
|
|
1266
1672
|
if (placement === 'fixed') {
|
|
1267
1673
|
placeCalloutFixed(startIdx, plan, style, layoutAt);
|
|
@@ -1279,22 +1685,42 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1279
1685
|
}
|
|
1280
1686
|
}
|
|
1281
1687
|
const splittable = !style.keepTogether;
|
|
1282
|
-
let from =
|
|
1688
|
+
let from = CUT_START;
|
|
1283
1689
|
let part = 0;
|
|
1284
1690
|
let frameId = firstFrameId;
|
|
1691
|
+
/** Times the box left an EMPTY short column (see below) — bounded. */
|
|
1692
|
+
let shortColumnMoves = 0;
|
|
1285
1693
|
for (;;) {
|
|
1286
1694
|
let curCol = currentColumn(doc, cursor);
|
|
1287
1695
|
const continuation = part > 0;
|
|
1288
|
-
const result = L.layoutRange(from, L.
|
|
1289
|
-
|
|
1696
|
+
const result = L.layoutRange(from, L.end, curCol.bbox.width, frameId, continuation);
|
|
1697
|
+
// Column balancing: a box closing its column takes the column's gap
|
|
1698
|
+
// above it (the trailing-callout lever), so its foot lands on the
|
|
1699
|
+
// last grid slot — level with the column beside it. Any fragment
|
|
1700
|
+
// qualifies, as long as something sits above it to push down from:
|
|
1701
|
+
// text, or the float band at the head of an otherwise empty column.
|
|
1702
|
+
const balanceBefore = curCol.blocks.length > 0 || reservedOf(curCol).top > 0
|
|
1703
|
+
? (balanceExtraPx?.get(balanceKey(startIdx, part)) ?? 0)
|
|
1704
|
+
: 0;
|
|
1705
|
+
const spacing = (curCol.blocks.length === 0 ? 0 : Math.max(pendingSpacing, result.marginTopPx)) + balanceBefore;
|
|
1290
1706
|
const roomPx = curCol.availableHeight - spacing;
|
|
1291
1707
|
let fragment = null;
|
|
1292
1708
|
if (result.totalHeight > roomPx + 0.01) {
|
|
1293
1709
|
// The (rest of the) box does not fit the column: a splittable box
|
|
1294
1710
|
// leaves the head that fits here…
|
|
1295
1711
|
if (splittable)
|
|
1296
|
-
fragment = splitCalloutFragment(L, from, curCol.bbox.width, roomPx, frameId, continuation);
|
|
1297
|
-
|
|
1712
|
+
fragment = splitCalloutFragment(L, from, curCol.bbox.width, roomPx, frameId, continuation, style.splitMinLines);
|
|
1713
|
+
// …otherwise it moves whole to the next column — also out of an
|
|
1714
|
+
// EMPTY column that float bands or a band cap have cut short, when
|
|
1715
|
+
// a full column would hold it (bounded, so a run of short columns
|
|
1716
|
+
// cannot make it wander forever). Only a box taller than a full
|
|
1717
|
+
// column is placed anyway, overflowing (a layout warning says so).
|
|
1718
|
+
const shortColumn = shortColumnMoves < 4
|
|
1719
|
+
&& curCol.bbox.height < contentArea.height - baselineGrid
|
|
1720
|
+
&& result.totalHeight <= contentArea.height + 0.01;
|
|
1721
|
+
if (!fragment && (curCol.blocks.length > 0 || shortColumn)) {
|
|
1722
|
+
if (curCol.blocks.length === 0)
|
|
1723
|
+
shortColumnMoves++;
|
|
1298
1724
|
// …otherwise it moves whole to the next column. Keep-with-next: a
|
|
1299
1725
|
// run of headings at the column's tail travels with the box.
|
|
1300
1726
|
// Skipped when the column holds nothing else (rolling back again
|
|
@@ -1315,13 +1741,21 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1315
1741
|
return (rolledBack[0].contentIndex ?? startIdx - rolledBack.length) - 1;
|
|
1316
1742
|
}
|
|
1317
1743
|
advanceToNextColumn(doc, cursor, resolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
|
|
1318
|
-
|
|
1319
|
-
//
|
|
1320
|
-
|
|
1321
|
-
continue;
|
|
1322
|
-
}
|
|
1744
|
+
// Try the next column afresh: it may be short too (a float band
|
|
1745
|
+
// reserved on the page it opened), or of another width (oneAndHalf).
|
|
1746
|
+
continue;
|
|
1323
1747
|
}
|
|
1324
1748
|
// An empty column that is still too short: placed anyway, overflowing.
|
|
1749
|
+
if (!fragment && curCol.blocks.length === 0 && result.totalHeight > curCol.availableHeight - spacing + 0.01) {
|
|
1750
|
+
(doc.warnings ??= []).push({
|
|
1751
|
+
kind: 'calloutOverflow',
|
|
1752
|
+
pageIndex: cursor.pageIndex,
|
|
1753
|
+
columnIndex: cursor.columnIndex,
|
|
1754
|
+
sourceStart: contentBlocks[startIdx].sourceStart + bodyOffset,
|
|
1755
|
+
sourceEnd: contentBlocks[plan.endIdx].sourceEnd + bodyOffset,
|
|
1756
|
+
overflowPx: result.totalHeight + spacing - curCol.availableHeight,
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1325
1759
|
}
|
|
1326
1760
|
// Floats first-referenced inside the box still enqueue in reading order
|
|
1327
1761
|
// (only once the box is committed, so a keep-with-next replay does not
|
|
@@ -1330,10 +1764,17 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1330
1764
|
for (let i = startIdx + 1; i <= plan.endIdx; i++)
|
|
1331
1765
|
enqueueFloatsFor(i);
|
|
1332
1766
|
const placed = fragment ? fragment.result : result;
|
|
1333
|
-
const to = fragment ? fragment.to : L.
|
|
1767
|
+
const to = fragment ? fragment.to : L.end;
|
|
1334
1768
|
if (part > 0 || fragment)
|
|
1335
1769
|
markFragment(placed, part, fragment !== null);
|
|
1336
|
-
const spacingBefore = curCol.blocks.length === 0 ? 0 : Math.max(pendingSpacing, placed.marginTopPx);
|
|
1770
|
+
const spacingBefore = (curCol.blocks.length === 0 ? 0 : Math.max(pendingSpacing, placed.marginTopPx)) + balanceBefore;
|
|
1771
|
+
if (balanceBefore > 0)
|
|
1772
|
+
addBalanceExtra(curCol, balanceBefore);
|
|
1773
|
+
// Spacing collapses at a column top, so the lever's push under a
|
|
1774
|
+
// float band is consumed here: the box then opens that far down.
|
|
1775
|
+
if (balanceBefore > 0 && curCol.blocks.length === 0) {
|
|
1776
|
+
curCol.availableHeight = Math.max(0, curCol.availableHeight - balanceBefore);
|
|
1777
|
+
}
|
|
1337
1778
|
enterBand(startIdx, 0);
|
|
1338
1779
|
placeAtomicBlock(placed.frame, placed.totalHeight, spacingBefore, cursor, doc, resolved, contentArea, pageWidthPx, pageHeightPx);
|
|
1339
1780
|
enterBand(startIdx, 0);
|
|
@@ -1367,7 +1808,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1367
1808
|
// order. Then enqueue the floats first-referenced in this block, so the
|
|
1368
1809
|
// next page opened while placing it (or any later block) reserves their
|
|
1369
1810
|
// band and the next iteration offers them the slots that follow.
|
|
1370
|
-
tryPlacePendingFloatsOnCurrentPage(spanBoxAt(blockIdx));
|
|
1811
|
+
tryPlacePendingFloatsOnCurrentPage(spanBoxAt(blockIdx), blockIdx);
|
|
1371
1812
|
enqueueFloatsFor(blockIdx);
|
|
1372
1813
|
// --- Directives ----------------------------------------------------
|
|
1373
1814
|
if (rawBlock.type === 'directive') {
|
|
@@ -1486,8 +1927,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1486
1927
|
// takes every pending float first — in the current page's free
|
|
1487
1928
|
// slots, else on pages opened ahead of it — so no float escapes
|
|
1488
1929
|
// past it. The page stays balanceable (no forced break).
|
|
1489
|
-
|
|
1490
|
-
|
|
1930
|
+
// A page-span barrier box drains inside `placeCalloutSpan`, once
|
|
1931
|
+
// the page-span figures before it have taken the band cut.
|
|
1932
|
+
if (pickCalloutStyle(resolved.calloutStyles, plan.attrs.type).floatBarrier && !spanBoxAt(blockIdx)) {
|
|
1933
|
+
tryPlacePendingFloatsOnCurrentPage();
|
|
1491
1934
|
drainPendingFloats();
|
|
1492
1935
|
}
|
|
1493
1936
|
// `span: 'page'` boxes in multi-column layouts branch to the
|
|
@@ -2242,12 +2685,12 @@ export function buildDocument(content, config, cache, options) {
|
|
|
2242
2685
|
};
|
|
2243
2686
|
const inColumn = gaps
|
|
2244
2687
|
.filter((g) => g.pageIndex === div.pageIndex && g.columnIndex === div.columnIndex)
|
|
2245
|
-
.flatMap((g) => g.candidates.map((c) => c.contentIndex));
|
|
2688
|
+
.flatMap((g) => g.candidates.map((c) => balanceKey(c.contentIndex, c.part ?? 0)));
|
|
2246
2689
|
if (blacklist(new Set(inColumn)))
|
|
2247
2690
|
return true;
|
|
2248
2691
|
const onPage = gaps
|
|
2249
2692
|
.filter((g) => g.pageIndex === div.pageIndex)
|
|
2250
|
-
.flatMap((g) => g.candidates.map((c) => c.contentIndex));
|
|
2693
|
+
.flatMap((g) => g.candidates.map((c) => balanceKey(c.contentIndex, c.part ?? 0)));
|
|
2251
2694
|
if (blacklist(new Set(onPage)))
|
|
2252
2695
|
return true;
|
|
2253
2696
|
return blacklist(null);
|