diff --git a/docs/architecture/pagination.md b/docs/architecture/pagination.md index 9f46dfb..a836025 100644 --- a/docs/architecture/pagination.md +++ b/docs/architecture/pagination.md @@ -28,7 +28,7 @@ ping-pong and keeps the current layout; only an edit or a forced recalc forgets rewrites its page numbers on every `pm-pagecount`, so resetting on any document change would leave nothing to catch). -**Keeping blocks together** (`pageBreak.ts` attrs, both read from the direct property *and* the paragraph style): `keepNext` (`w:keepNext`, `fo:keep-with-next`) moves a block down when its successor's first line no longer fits below it — headings do that in Word and LibreOffice anyway, so the attr only marks the others. `keepLines` (`w:keepLines`, `fo:keep-together`) makes an otherwise splittable block paginate atomically, unless it is taller than the page slot, where Word splits it too. +**Keeping blocks together** (`pageBreak.ts` attrs, both read from the direct property *and* the paragraph style): `keepNext` (`w:keepNext`, `fo:keep-with-next`) moves a block down when the successor's first `MIN_KEPT_LINES` no longer fit below it — one line is not enough, because widow-orphan control would carry the second one over and strand the block anyway (measured on the Math Guide: a heading left alone at the foot of page 8) — headings do that in Word and LibreOffice anyway, so the attr only marks the others. `keepLines` (`w:keepLines`, `fo:keep-together`) makes an otherwise splittable block paginate atomically, unless it is taller than the page slot, where Word splits it too. **Line breaking:** `editor.css` overrides prosemirror-view's `white-space: break-spaces` back to `pre-wrap` on `.paper .tiptap`. Under `break-spaces` a line-end space may not hang into the margin and counts toward the line width, so lines break one word earlier than in Word/LibreOffice whenever a line reaches within a space width of the margin. diff --git a/src/lib/editor/extensions/pageBreaks.ts b/src/lib/editor/extensions/pageBreaks.ts index 300bb7c..bdd0347 100644 --- a/src/lib/editor/extensions/pageBreaks.ts +++ b/src/lib/editor/extensions/pageBreaks.ts @@ -505,11 +505,14 @@ export const PageBreaks = Extension.create({ // `minLines` is the widow-orphan minimum, or 1 for a leaf taller than one page // slot — there the rule is unsatisfiable and splitting beats overflowing. - // What a keep-with-next block has to fit below itself: the successor's first - // line, in unscaled doc px. An empty block falls back to its whole height. - function firstLineHeight(el: HTMLElement, scale: number): number { - const first = getLineRects(el)[0]; - return first ? (first.bottom - first.top) / scale : el.offsetHeight; + // What a keep-with-next block has to fit below itself, in unscaled doc px: as + // many lines of the successor as widow-orphan control would keep together + // anyway. An empty block falls back to its whole height. + function firstLinesHeight(el: HTMLElement, scale: number, want: number): number { + const lines = getLineRects(el); + if (!lines.length) return el.offsetHeight; + const last = lines[Math.min(want, lines.length) - 1]; + return (last.bottom - lines[0].top) / scale; } function findLineSplit( @@ -1152,9 +1155,10 @@ export const PageBreaks = Extension.create({ // What the pair needs below this block: its own space below, then the // successor's space above (its padding; an atomic leaf's height has it // already) and its first line. + const wantLines = next.el.getAttribute('data-widow-control') === 'false' ? 1 : MIN_KEPT_LINES; const needed = next.kind === 'atomic' ? Math.min(next.naturalHeight, CONTENT_HEIGHT) - : firstLineHeight(next.el, scale) + (parseFloat(getComputedStyle(next.el).paddingTop) || 0); + : firstLinesHeight(next.el, scale, wantLines) + (parseFloat(getComputedStyle(next.el).paddingTop) || 0); if (effectiveBottom + (leaf.spaceAfter ?? 0) + needed > contentEnd) { const target = pageContentStart(page + 1, vm.top, CYCLE_PX); const { docPos, row } = leafSpacer(leaf);