postext 0.3.34 → 0.3.35
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__/pipeline/calloutOverflow.test.js +3 -1
- package/dist/__tests__/pipeline/calloutOverflow.test.js.map +1 -1
- package/dist/__tests__/pipeline/floatFirstSlot.test.js +34 -3
- package/dist/__tests__/pipeline/floatFirstSlot.test.js.map +1 -1
- package/dist/__tests__/pipeline/spanBlocks.test.js +9 -8
- package/dist/__tests__/pipeline/spanBlocks.test.js.map +1 -1
- package/dist/__tests__/pipeline/tableSplit.test.js +48 -0
- package/dist/__tests__/pipeline/tableSplit.test.js.map +1 -1
- package/dist/defaults/pdfGeneration.d.ts.map +1 -1
- package/dist/defaults/pdfGeneration.js +7 -0
- package/dist/defaults/pdfGeneration.js.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +128 -34
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/floatPlacement.d.ts +8 -0
- package/dist/pipeline/floatPlacement.d.ts.map +1 -1
- package/dist/pipeline/floatPlacement.js.map +1 -1
- package/dist/pipeline/resourceLayout.d.ts.map +1 -1
- package/dist/pipeline/resourceLayout.js +3 -1
- package/dist/pipeline/resourceLayout.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +3 -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
|
@@ -364,9 +364,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
364
364
|
* decides what becomes of the rows left over — a rest float to continue
|
|
365
365
|
* on the next page (`'split'`), nothing (`'clip'`) — or, for `'hide'`,
|
|
366
366
|
* that the table is dropped. Returns null for a figure or a table with
|
|
367
|
-
* nothing to cut, which are force-placed like before.
|
|
367
|
+
* nothing to cut, which are force-placed like before. In the `'strict'`
|
|
368
|
+
* (current-page) mode nothing is forced: `'none'` when not even the
|
|
369
|
+
* smallest slice fits the slot.
|
|
368
370
|
*/
|
|
369
|
-
const splitTableFloat = (f, width, position, targetCols, contentArea, avail) => {
|
|
371
|
+
const splitTableFloat = (f, width, position, targetCols, contentArea, avail, mode) => {
|
|
370
372
|
const rowCount = tableRowCount(f.resourceId);
|
|
371
373
|
if (rowCount === 0)
|
|
372
374
|
return null;
|
|
@@ -390,11 +392,15 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
390
392
|
// `avail` is the grid multiple below it, less the gap.
|
|
391
393
|
const hMax = Math.floor((avail + 0.01) / baselineGrid) * baselineGrid - floatGapPx;
|
|
392
394
|
let end = planTableSlice(metrics, startRow, hMax - overhead);
|
|
393
|
-
// Nothing fits: carry the smallest slice anyway (it overflows, as a
|
|
394
|
-
// dominating figure would) rather than stall the queue.
|
|
395
395
|
const floor = firstBody + 1;
|
|
396
|
-
if (end < floor)
|
|
396
|
+
if (end < floor) {
|
|
397
|
+
// Nothing fits. A fresh page carries the smallest slice anyway (it
|
|
398
|
+
// overflows, as a dominating figure would) rather than stall the
|
|
399
|
+
// queue; a slot of the current page is simply not this table's.
|
|
400
|
+
if (mode === 'strict')
|
|
401
|
+
return 'none';
|
|
397
402
|
end = floor;
|
|
403
|
+
}
|
|
398
404
|
// A last page holding a row or two under a repeated header reads as a
|
|
399
405
|
// stranded tail: give the closing slice at least `MIN_TAIL_ROWS` rows by
|
|
400
406
|
// handing some back from this one (at a breakable edge, not under a
|
|
@@ -429,6 +435,8 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
429
435
|
while (end > floor && !(metrics.breakableAfter[end - 1] ?? true));
|
|
430
436
|
slice = sliceFor(end);
|
|
431
437
|
}
|
|
438
|
+
if (mode === 'strict' && !fits(slice))
|
|
439
|
+
return 'none';
|
|
432
440
|
const rest = slice.continues ? { ...f, startRow: end } : undefined;
|
|
433
441
|
return rest ? { slice, rest } : { slice };
|
|
434
442
|
};
|
|
@@ -483,6 +491,8 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
483
491
|
const { width, xLeft } = probe;
|
|
484
492
|
let { slice, measure, need, y } = probe;
|
|
485
493
|
let rest;
|
|
494
|
+
/** The float was cut to this slot (a table slice). */
|
|
495
|
+
let cut = false;
|
|
486
496
|
if (mode === 'fresh') {
|
|
487
497
|
let minAvail = Infinity;
|
|
488
498
|
let anyReserved = false;
|
|
@@ -496,12 +506,13 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
496
506
|
return 'defer';
|
|
497
507
|
if (need > minAvail + 0.01) {
|
|
498
508
|
// Dominating the band: a table is cut to it.
|
|
499
|
-
const split = splitTableFloat(f, width, position, targetCols, page.contentArea, minAvail);
|
|
509
|
+
const split = splitTableFloat(f, width, position, targetCols, page.contentArea, minAvail, 'fresh');
|
|
500
510
|
if (split === 'skip')
|
|
501
511
|
return 'skip';
|
|
502
|
-
if (split) {
|
|
512
|
+
if (split && split !== 'none') {
|
|
503
513
|
slice = split.slice;
|
|
504
514
|
rest = split.rest;
|
|
515
|
+
cut = true;
|
|
505
516
|
const m = measureFloat(f.resourceId, width, slice);
|
|
506
517
|
if (!m)
|
|
507
518
|
return 'skip';
|
|
@@ -518,10 +529,44 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
518
529
|
return 'defer';
|
|
519
530
|
continue;
|
|
520
531
|
}
|
|
521
|
-
|
|
532
|
+
const hasBand = columnHasFloatBand(page, c);
|
|
533
|
+
if (fitsStrict(need, c, hasBand, minTextPx))
|
|
534
|
+
continue;
|
|
535
|
+
// The head of an empty column: a splittable table is cut to the
|
|
536
|
+
// column and continues in the next slot — the compositor sets a
|
|
537
|
+
// long table beside the text that cites it, not pages later. A
|
|
538
|
+
// table that clips or hides when too tall keeps to fresh pages.
|
|
539
|
+
if (position !== 'top' || pageSpan || c.blocks.length > 0)
|
|
540
|
+
return 'defer';
|
|
541
|
+
if (resolved.tableStyle.overflow !== 'split')
|
|
542
|
+
return 'defer';
|
|
543
|
+
const split = splitTableFloat(f, width, position, targetCols, page.contentArea, c.availableHeight - (hasBand ? minTextPx : 0), 'strict');
|
|
544
|
+
if (!split || split === 'none' || split === 'skip')
|
|
545
|
+
return 'defer';
|
|
546
|
+
slice = split.slice;
|
|
547
|
+
rest = split.rest;
|
|
548
|
+
cut = true;
|
|
549
|
+
const m = measureFloat(f.resourceId, width, slice);
|
|
550
|
+
if (!m)
|
|
551
|
+
return 'skip';
|
|
552
|
+
measure = m;
|
|
553
|
+
({ need, y } = measureFloatBand(position, measure, targetCols, page.contentArea, baselineGrid, floatGapPx, (cc) => trueBottom(cc, uncappedBottoms)));
|
|
554
|
+
if (!fitsStrict(need, c, hasBand, minTextPx))
|
|
522
555
|
return 'defer';
|
|
523
556
|
}
|
|
524
557
|
}
|
|
558
|
+
// A slice cut to the head of a column takes the column whole when the
|
|
559
|
+
// rows leave less than the text minimum under it: a line or two of
|
|
560
|
+
// body text stranded under a table reads worse than an empty foot.
|
|
561
|
+
if (cut && position === 'top') {
|
|
562
|
+
const minAvail = Math.min(...targetCols.map((c) => c.availableHeight));
|
|
563
|
+
if (minAvail - need < minTextPx)
|
|
564
|
+
need = Math.max(need, minAvail);
|
|
565
|
+
}
|
|
566
|
+
// The rest goes on after this slice in reading order, never before.
|
|
567
|
+
if (rest) {
|
|
568
|
+
rest = { ...rest, notBefore: { pageIndex: page.index, columnIndex: targetCols[targetCols.length - 1].index } };
|
|
569
|
+
}
|
|
525
570
|
const built = buildFloatBlock(f.resourceId, xLeft, width, slice);
|
|
526
571
|
if (!built)
|
|
527
572
|
return 'skip';
|
|
@@ -563,35 +608,61 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
563
608
|
return rest ? { rest } : 'placed';
|
|
564
609
|
};
|
|
565
610
|
/** Apply a slot outcome to the queue at `i`: drop a placed float, keep a
|
|
566
|
-
* deferred one, swap in the rest of a split table
|
|
567
|
-
*
|
|
611
|
+
* deferred one, swap in the rest of a split table (offered the next slot
|
|
612
|
+
* right away, so a table cut to one column goes on in the column beside
|
|
613
|
+
* it). Returns the index to continue from. */
|
|
568
614
|
const settle = (i, r) => {
|
|
569
615
|
if (r === 'defer')
|
|
570
616
|
return i + 1;
|
|
571
617
|
if (typeof r === 'object') {
|
|
572
618
|
pendingFloats[i] = r.rest;
|
|
573
|
-
return i
|
|
619
|
+
return i;
|
|
574
620
|
}
|
|
575
621
|
pendingFloats.splice(i, 1);
|
|
576
622
|
return i;
|
|
577
623
|
};
|
|
624
|
+
/** Whether the pending float at `i` must wait: an earlier float of the
|
|
625
|
+
* same numbering sequence (resource type) is still pending. Figures and
|
|
626
|
+
* tables are numbered in first-reference order, and the reader must
|
|
627
|
+
* meet them in that order too — table 3 never lands after table 4, even
|
|
628
|
+
* when 4 would fit a slot 3 does not. Sequences do not hold each other
|
|
629
|
+
* up: a waiting table lets a later figure through. */
|
|
630
|
+
const heldBack = (i) => {
|
|
631
|
+
const typeId = resourceById.get(pendingFloats[i].resourceId)?.typeId;
|
|
632
|
+
for (let j = 0; j < i; j++) {
|
|
633
|
+
if (resourceById.get(pendingFloats[j].resourceId)?.typeId === typeId)
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
636
|
+
return false;
|
|
637
|
+
};
|
|
578
638
|
const positionsFor = (f) => f.position === 'auto' ? ['top', 'bottom'] : [f.position];
|
|
579
639
|
/** Reserve top/bottom bands on a freshly opened page and position as many
|
|
580
640
|
* pending floats as fit, shrinking the affected columns so body text flows
|
|
581
641
|
* around them. Full-width (page-span) floats reserve the outermost bands
|
|
582
642
|
* first, so a later single-column float nests inside the remaining column
|
|
583
|
-
* space rather than overlapping a full-width band
|
|
584
|
-
*
|
|
585
|
-
*
|
|
643
|
+
* space rather than overlapping a full-width band; the passes repeat
|
|
644
|
+
* while they place something, so a page-span float held back behind a
|
|
645
|
+
* column float of its sequence still gets the page's foot once that one
|
|
646
|
+
* is set. A float that does not fit holds up the ones behind it in its
|
|
647
|
+
* numbering sequence (see `heldBack`), never the other sequence. */
|
|
586
648
|
const flushFloatsIntoPage = (page) => {
|
|
587
649
|
if (pendingFloats.length === 0)
|
|
588
650
|
return;
|
|
589
651
|
const textCols = page.columns.filter((c) => c.kind !== 'span');
|
|
590
652
|
if (textCols.length === 0)
|
|
591
653
|
return;
|
|
592
|
-
|
|
593
|
-
|
|
654
|
+
/** The least reserved text column a float may take (the rest of a
|
|
655
|
+
* split table: only columns after its previous slice on this page). */
|
|
656
|
+
const leastReserved = (f) => {
|
|
657
|
+
const after = f.notBefore && f.notBefore.pageIndex === page.index ? f.notBefore.columnIndex : -1;
|
|
658
|
+
let best;
|
|
594
659
|
for (const c of textCols) {
|
|
660
|
+
if (c.index <= after)
|
|
661
|
+
continue;
|
|
662
|
+
if (!best) {
|
|
663
|
+
best = c;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
595
666
|
const rb = reservedOf(best);
|
|
596
667
|
const rc = reservedOf(c);
|
|
597
668
|
if (rc.top + rc.bottom < rb.top + rb.bottom)
|
|
@@ -599,24 +670,36 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
599
670
|
}
|
|
600
671
|
return best;
|
|
601
672
|
};
|
|
602
|
-
for (
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
673
|
+
for (let progress = true; progress;) {
|
|
674
|
+
const before = floatsPlaced;
|
|
675
|
+
for (const pageSpanPass of [true, false]) {
|
|
676
|
+
let i = 0;
|
|
677
|
+
while (i < pendingFloats.length) {
|
|
678
|
+
const f = pendingFloats[i];
|
|
679
|
+
const isPageSpan = f.span === 'page' && textCols.length > 1;
|
|
680
|
+
if (isPageSpan !== pageSpanPass || heldBack(i)) {
|
|
681
|
+
i++;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
// A page-span rest never shares the page of its previous slice.
|
|
685
|
+
if (isPageSpan && f.notBefore?.pageIndex === page.index) {
|
|
686
|
+
i++;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
let r = 'defer';
|
|
690
|
+
for (const pos of positionsFor(f)) {
|
|
691
|
+
const col = isPageSpan ? undefined : leastReserved(f);
|
|
692
|
+
if (!isPageSpan && !col)
|
|
693
|
+
break;
|
|
694
|
+
const cols = isPageSpan ? textCols : [col];
|
|
695
|
+
r = placeFloatInColumns(page, f, cols, pos, isPageSpan, 'fresh');
|
|
696
|
+
if (r !== 'defer')
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
i = settle(i, r);
|
|
617
700
|
}
|
|
618
|
-
i = settle(i, r);
|
|
619
701
|
}
|
|
702
|
+
progress = floatsPlaced > before;
|
|
620
703
|
}
|
|
621
704
|
};
|
|
622
705
|
/** The keep-together box content block `idx` opens, when it is one that
|
|
@@ -686,9 +769,20 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
686
769
|
const page = doc.pages[cursor.pageIndex];
|
|
687
770
|
const box = nextBlockIdx !== undefined ? keepTogetherBoxAt(nextBlockIdx) : null;
|
|
688
771
|
for (let i = 0; i < pendingFloats.length;) {
|
|
772
|
+
if (heldBack(i)) {
|
|
773
|
+
i++;
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
689
776
|
const f = pendingFloats[i];
|
|
690
777
|
let r = 'defer';
|
|
691
778
|
let slots = enumerateCurrentPageSlots(page, cursor.columnIndex, f, capKindOf);
|
|
779
|
+
// The rest of a table cut on this page only takes the slots after
|
|
780
|
+
// its previous slice in reading order (never the foot of the column
|
|
781
|
+
// before it; a page-span rest waits for the next page).
|
|
782
|
+
if (f.notBefore && f.notBefore.pageIndex === page.index) {
|
|
783
|
+
const after = f.notBefore.columnIndex;
|
|
784
|
+
slots = slots.filter((s) => !s.pageSpan && s.cols[0].index > after);
|
|
785
|
+
}
|
|
692
786
|
// A page-span box comes next: the head of an empty column keeps the
|
|
693
787
|
// band cuttable under the float (the box then sits below both the
|
|
694
788
|
// text and the figure), where the referencing column's foot would
|
|
@@ -1212,7 +1306,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1212
1306
|
for (let i = 0; i < pendingFloats.length;) {
|
|
1213
1307
|
const f = pendingFloats[i];
|
|
1214
1308
|
const cols = bandColumns(page, currentBand(page, cursor));
|
|
1215
|
-
if (f.span !== 'page' || cols.length < 2 || !((capActiveHere && !placedAny) || levelForBox(cols))) {
|
|
1309
|
+
if (f.span !== 'page' || cols.length < 2 || heldBack(i) || !((capActiveHere && !placedAny) || levelForBox(cols))) {
|
|
1216
1310
|
i++;
|
|
1217
1311
|
continue;
|
|
1218
1312
|
}
|
|
@@ -1309,7 +1403,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1309
1403
|
// box moving on and the band ending level like a closing one.
|
|
1310
1404
|
// Only a page-span figure is planned for here; column figures keep
|
|
1311
1405
|
// the ordinary slots (their level is the band cap's own business).
|
|
1312
|
-
const first = pendingFloats.find((f) => f.span === 'page');
|
|
1406
|
+
const first = pendingFloats.find((f, i) => f.span === 'page' && !heldBack(i));
|
|
1313
1407
|
if (first && !capActive && cap === undefined && bandStart && registeredBand
|
|
1314
1408
|
&& registeredBand.pageIndex === page.index && registeredBand.band === bandBefore) {
|
|
1315
1409
|
const cols = bandColumns(page, bandBefore).filter((c) => c.bbox.height > 0.5);
|