@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.forceColumnBreak();
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.0",
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",