From 0712d44ebf92fbb86f982b4965f92125dca1ee51 Mon Sep 17 00:00:00 2001 From: Steffen Becker Date: Mon, 10 Aug 2026 14:00:12 +0000 Subject: [PATCH] fix: every leaf's natural top is reconstructed the same way MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/architecture/pagination.md | 3 +++ src/lib/editor/extensions/pageBreaks.ts | 21 ++++++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/architecture/pagination.md b/docs/architecture/pagination.md index 638de75..9f46dfb 100644 --- a/docs/architecture/pagination.md +++ b/docs/architecture/pagination.md @@ -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 diff --git a/src/lib/editor/extensions/pageBreaks.ts b/src/lib/editor/extensions/pageBreaks.ts index 94639e4..300bb7c 100644 --- a/src/lib/editor/extensions/pageBreaks.ts +++ b/src/lib/editor/extensions/pageBreaks.ts @@ -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 //
…, 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, });