@stll/folio-core 0.25.0 → 0.25.1
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.
|
@@ -560,20 +560,39 @@ function layoutTable(block, measure, paginator, footnoteHeightById) {
|
|
|
560
560
|
const splittableRow = rows[currentRowIndex];
|
|
561
561
|
if (canSplitRow(currentRowIndex)) {
|
|
562
562
|
let consumed = 0;
|
|
563
|
+
const renderedBreakHints = tableRowHasTrackedChanges(block, currentRowIndex) ? [] : breakInfo.renderedBreakHints[currentRowIndex] ?? [];
|
|
564
|
+
let renderedBreakHintIndex = 0;
|
|
565
|
+
const discardPassedRenderedBreakHints = () => {
|
|
566
|
+
while ((renderedBreakHints[renderedBreakHintIndex]?.offset ?? Infinity) <= consumed) renderedBreakHintIndex += 1;
|
|
567
|
+
};
|
|
568
|
+
const advanceAfterNaturalFlowBreak = (previousPageNumber) => {
|
|
569
|
+
discardPassedRenderedBreakHints();
|
|
570
|
+
if (paginator.getCurrentState().page.number > previousPageNumber && renderedBreakHintIndex < renderedBreakHints.length) renderedBreakHintIndex += 1;
|
|
571
|
+
};
|
|
563
572
|
while (consumed < splittableRow.height) {
|
|
564
573
|
const sliceState = paginator.getCurrentState();
|
|
565
574
|
const repeatHeaderRows = shouldRepeatHeaderRows(currentRowIndex, consumed, sliceState);
|
|
566
575
|
const headerOverhead = repeatHeaderRows ? headerRowsHeight : 0;
|
|
567
576
|
const sliceAvail = paginator.getAvailableHeight() - headerOverhead - (consumed === 0 ? sliceState.trailingSpacing : 0);
|
|
568
577
|
let slice = snapRowBreak(breakInfo, currentRowIndex, consumed, sliceAvail);
|
|
578
|
+
let forceRenderedPageBreak = false;
|
|
569
579
|
if (slice <= 0) {
|
|
570
580
|
if (!(sliceState.cursorY === sliceState.topMargin && sliceState.page.fragments.length === 0)) {
|
|
581
|
+
const previousPageNumber = sliceState.page.number;
|
|
571
582
|
paginator.forceColumnBreak();
|
|
583
|
+
advanceAfterNaturalFlowBreak(previousPageNumber);
|
|
572
584
|
continue;
|
|
573
585
|
}
|
|
574
586
|
const from = consumed;
|
|
575
587
|
slice = (breakInfo.breakOffsets[currentRowIndex]?.find((o) => o > from) ?? splittableRow.height) - consumed;
|
|
576
588
|
}
|
|
589
|
+
discardPassedRenderedBreakHints();
|
|
590
|
+
const renderedBreakHint = renderedBreakHints[renderedBreakHintIndex];
|
|
591
|
+
if (renderedBreakHint && renderedBreakHint.offset <= consumed + slice && sliceAvail - (renderedBreakHint.offset - consumed) <= renderedBreakHint.lineAdvance * RENDERED_BREAK_REFLOW_TOLERANCE_LINES) {
|
|
592
|
+
slice = renderedBreakHint.offset - consumed;
|
|
593
|
+
renderedBreakHintIndex += 1;
|
|
594
|
+
forceRenderedPageBreak = true;
|
|
595
|
+
}
|
|
577
596
|
const sliceBottom = consumed + slice;
|
|
578
597
|
const continuationSkip = sliceBottom < splittableRow.height ? getRowContinuationSkip(breakInfo, currentRowIndex, sliceBottom) : 0;
|
|
579
598
|
const nextConsumed = Math.min(splittableRow.height, sliceBottom + continuationSkip);
|
|
@@ -601,7 +620,12 @@ function layoutTable(block, measure, paginator, footnoteHeightById) {
|
|
|
601
620
|
sliceFragment.y = sliceResult.y;
|
|
602
621
|
sliceFragment.x = computeTableX(sliceResult.state.columnIndex);
|
|
603
622
|
consumed = nextConsumed;
|
|
604
|
-
if (consumed < splittableRow.height) paginator.
|
|
623
|
+
if (consumed < splittableRow.height) if (forceRenderedPageBreak) paginator.forcePageBreak();
|
|
624
|
+
else {
|
|
625
|
+
const previousPageNumber = paginator.getCurrentState().page.number;
|
|
626
|
+
paginator.forceColumnBreak();
|
|
627
|
+
advanceAfterNaturalFlowBreak(previousPageNumber);
|
|
628
|
+
}
|
|
605
629
|
}
|
|
606
630
|
currentRowIndex += 1;
|
|
607
631
|
continue;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { TableBlock, TableMeasure } from "./types.js";
|
|
2
2
|
//#region src/layout-engine/tableRowBreak.d.ts
|
|
3
|
+
type RenderedRowBreakHint = {
|
|
4
|
+
offset: number;
|
|
5
|
+
lineAdvance: number;
|
|
6
|
+
};
|
|
3
7
|
/** Precomputed break geometry for a table. */
|
|
4
8
|
type TableRowBreakInfo = {
|
|
5
9
|
/** Cumulative y of the top of each row; `rowTops[rows.length]` is the table height. */
|
|
@@ -10,6 +14,8 @@ type TableRowBreakInfo = {
|
|
|
10
14
|
* final boundary.
|
|
11
15
|
*/
|
|
12
16
|
breakOffsets: number[][];
|
|
17
|
+
/** Safe cached page boundaries within each row, relative to the row top. */
|
|
18
|
+
renderedBreakHints: RenderedRowBreakHint[][];
|
|
13
19
|
/** Suppressible leading paragraph whitespace after each matching break offset. */
|
|
14
20
|
continuationSkips: number[][];
|
|
15
21
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { measuredLineAdvance } from "./lineFlow.js";
|
|
1
2
|
import { measureParagraph } from "./measure/measureParagraph.js";
|
|
2
3
|
import { buildTableCellFloatingZones, getTableCellContentWidth, getTableCellFloatingImages } from "./measure/tableCellFloating.js";
|
|
3
4
|
import { createTableCellFlowState, placeTableCellBlock } from "./measure/tableCellFlow.js";
|
|
@@ -32,6 +33,10 @@ function shiftCellGeometry(geometry, offset) {
|
|
|
32
33
|
if (offset <= 0) return geometry;
|
|
33
34
|
return {
|
|
34
35
|
bottoms: geometry.bottoms.map((bottom) => bottom + offset),
|
|
36
|
+
renderedBreakHints: geometry.renderedBreakHints.map((hint) => ({
|
|
37
|
+
offset: hint.offset + offset,
|
|
38
|
+
lineAdvance: hint.lineAdvance
|
|
39
|
+
})),
|
|
35
40
|
unsafeRanges: geometry.unsafeRanges.map((range) => ({
|
|
36
41
|
top: range.top + offset,
|
|
37
42
|
bottom: range.bottom + offset
|
|
@@ -45,6 +50,7 @@ function shiftCellGeometry(geometry, offset) {
|
|
|
45
50
|
/** Cumulative break geometry within a single cell's content. */
|
|
46
51
|
function cellBreakGeometry(cell, measure) {
|
|
47
52
|
const bottoms = [];
|
|
53
|
+
const renderedBreakHints = [];
|
|
48
54
|
const unsafeRanges = [];
|
|
49
55
|
const suppressibleLeadingRanges = [];
|
|
50
56
|
const cellBlocks = cell?.blocks;
|
|
@@ -71,6 +77,10 @@ function cellBreakGeometry(cell, measure) {
|
|
|
71
77
|
for (const line of blockMeasure.lines) {
|
|
72
78
|
y += line.floatSkipBefore ?? 0;
|
|
73
79
|
const top = y;
|
|
80
|
+
if (line.renderedPageBreakBefore === true) renderedBreakHints.push({
|
|
81
|
+
offset: top,
|
|
82
|
+
lineAdvance: measuredLineAdvance(line)
|
|
83
|
+
});
|
|
74
84
|
y += line.lineHeight;
|
|
75
85
|
unsafeRanges.push({
|
|
76
86
|
top,
|
|
@@ -97,10 +107,23 @@ function cellBreakGeometry(cell, measure) {
|
|
|
97
107
|
}
|
|
98
108
|
return {
|
|
99
109
|
bottoms,
|
|
110
|
+
renderedBreakHints,
|
|
100
111
|
unsafeRanges,
|
|
101
112
|
suppressibleLeadingRanges
|
|
102
113
|
};
|
|
103
114
|
}
|
|
115
|
+
const mergeRenderedBreakHints = (hints) => {
|
|
116
|
+
const merged = [];
|
|
117
|
+
for (const hint of hints.sort((a, b) => a.offset - b.offset)) {
|
|
118
|
+
const previous = merged.at(-1);
|
|
119
|
+
if (previous && Math.abs(previous.offset - hint.offset) <= BREAK_OFFSET_EPSILON) {
|
|
120
|
+
previous.lineAdvance = Math.max(previous.lineAdvance, hint.lineAdvance);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
merged.push({ ...hint });
|
|
124
|
+
}
|
|
125
|
+
return merged;
|
|
126
|
+
};
|
|
104
127
|
const continuationSkipForCell = (geometry, offset) => {
|
|
105
128
|
if (!geometry.unsafeRanges.some(({ bottom }) => bottom > offset + BREAK_OFFSET_EPSILON)) return;
|
|
106
129
|
const leadingRange = geometry.suppressibleLeadingRanges.find(({ top, bottom }) => top <= offset + BREAK_OFFSET_EPSILON && bottom > offset + BREAK_OFFSET_EPSILON);
|
|
@@ -128,6 +151,7 @@ function buildTableRowBreakInfo(block, measure) {
|
|
|
128
151
|
}
|
|
129
152
|
rowTops.push(acc);
|
|
130
153
|
const breakOffsets = [];
|
|
154
|
+
const renderedBreakHints = [];
|
|
131
155
|
const continuationSkips = [];
|
|
132
156
|
for (let r = 0; r < rowCount; r++) {
|
|
133
157
|
const rowHeight = measure.rows[r]?.height ?? 0;
|
|
@@ -146,11 +170,13 @@ function buildTableRowBreakInfo(block, measure) {
|
|
|
146
170
|
}
|
|
147
171
|
const sortedOffsets = [...offsets].filter((offset) => offset === rowHeight || cellGeometries.every((geometry) => geometry.unsafeRanges.every((range) => !isInsideRange(offset, range)))).sort((a, b) => a - b);
|
|
148
172
|
breakOffsets.push(sortedOffsets);
|
|
173
|
+
renderedBreakHints.push(mergeRenderedBreakHints(cellGeometries.flatMap((geometry) => geometry.renderedBreakHints).filter(({ offset }) => offset > BREAK_OFFSET_EPSILON && offset < rowHeight - BREAK_OFFSET_EPSILON && cellGeometries.every((geometry) => geometry.unsafeRanges.every((range) => !isInsideRange(offset, range))))));
|
|
149
174
|
continuationSkips.push(sortedOffsets.map((offset) => continuationSkipAfter(cellGeometries, offset)));
|
|
150
175
|
}
|
|
151
176
|
return {
|
|
152
177
|
rowTops,
|
|
153
178
|
breakOffsets,
|
|
179
|
+
renderedBreakHints,
|
|
154
180
|
continuationSkips
|
|
155
181
|
};
|
|
156
182
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.1",
|
|
4
4
|
"description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"document-model",
|