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, });