fix: every leaf's natural top is reconstructed the same way

Only the ordinary-block push site added the space the page-top rule had
dropped above it; the index, text box, columns and table-row sites subtracted
the spacers alone, so each of those measured too high by whatever the pages
above had swallowed and got an equally oversized spacer — a fixture's figure
frames landed 16mm down their page. One naturalTopOf() now serves all six.
This commit is contained in:
Steffen Becker committed 2026-08-10 14:00:12 +00:00
1 parent 7a4174cd07
commit 0712d44ebf
2 files changed
+17 -7

No files matched your search

+3
View File
@@ -49,6 +49,9 @@ same document opened at 59 or 65 pages from one load to the next. `collectLeaves
space back (`spaceAbove`, and a running `cumulativeDropped` for the leaves below), so what it
measures is the document; the placement loop then takes the drop off `cumulativeShift` itself. The
rule is general — anything the pass decides has to be reconstructed out of the next pass's input.
Every leaf is therefore born through `naturalTopOf`: four of the six push sites (index, text box,
columns fragment, table row) once subtracted the spacers but not the dropped space, and a figure
frame below a chapter heading landed 16mm down its page.
**A justified line fits more in LibreOffice than in a browser:** LibreOffice compresses the
inter-word spaces to squeeze one more word onto a justified line, CSS `text-align: justify` only
+14 -7
View File
@@ -628,6 +628,13 @@ export const PageBreaks = Extension.create({
return top;
}
// A leaf's top in the undecorated document: the rendered top with the spacers
// above it taken out and the space they swallowed put back, so a pass never
// measures its own previous answer. Every leaf must be born through this.
function naturalTopOf(el: HTMLElement): number {
return topWithin(el) - cumulativeSpacerHeight + cumulativeDropped;
}
// Emit one atomic leaf per table row so the table breaks between rows across
// pages (a whole table is usually taller than a page). TipTap renders tables as
// <div class="tableWrapper"><table><colgroup><tbody>…, so walk the tbody rows.
@@ -645,7 +652,7 @@ export const PageBreaks = Extension.create({
leaves.push({
el: wrapperEl,
kind: 'atomic',
naturalTop: topWithin(wrapperEl) - cumulativeSpacerHeight,
naturalTop: naturalTopOf(wrapperEl),
naturalHeight: wrapperEl.offsetHeight,
});
return;
@@ -665,7 +672,7 @@ export const PageBreaks = Extension.create({
leaves.push({
el: tr,
kind: 'atomic',
naturalTop: topWithin(tr) - cumulativeSpacerHeight,
naturalTop: naturalTopOf(tr),
naturalHeight: rowHeight,
tableRow: { columns, wrapperEl, isFirstRow: !seenRealRow },
});
@@ -732,7 +739,7 @@ export const PageBreaks = Extension.create({
leaves.push({
el: child,
kind: 'atomic',
naturalTop: topWithin(child) - cumulativeSpacerHeight,
naturalTop: naturalTopOf(child),
naturalHeight: child.offsetHeight,
inTableCell,
});
@@ -744,7 +751,7 @@ export const PageBreaks = Extension.create({
leaves.push({
el: child,
kind: 'atomic',
naturalTop: topWithin(child) - cumulativeSpacerHeight,
naturalTop: naturalTopOf(child),
naturalHeight: child.offsetHeight,
inTableCell,
});
@@ -764,7 +771,7 @@ export const PageBreaks = Extension.create({
leaves.push({
el: child,
kind: 'atomic',
naturalTop: topWithin(child) - cumulativeSpacerHeight,
naturalTop: naturalTopOf(child),
naturalHeight: child.offsetHeight,
inTableCell,
columnsFragment: {
@@ -816,7 +823,7 @@ export const PageBreaks = Extension.create({
leaves.push({
el: child,
kind: isAtomic ? 'atomic' : 'splittable',
naturalTop: topWithin(child) - cumulativeSpacerHeight + cumulativeDropped,
naturalTop: naturalTopOf(child),
naturalHeight: Math.max(child.offsetHeight, floatBottom) - intraSpacerHeight + dropped,
spaceAfter: inTableCell ? 0 : parseFloat(cs.marginBottom) || 0,
spaceAbove,
@@ -847,7 +854,7 @@ export const PageBreaks = Extension.create({
leaves.push({
el: child,
kind: 'splittable',
naturalTop: topWithin(child) - cumulativeSpacerHeight,
naturalTop: naturalTopOf(child),
naturalHeight: child.offsetHeight,
inTableCell,
});