Skip to content

paginate: cut a laid-out document into pages under CSS fragmentation - #142

Merged
tannevaled merged 1 commit into
mainfrom
paginate
Sep 6, 2026
Merged

tannevaled merged 1 commit into
mainfrom
paginate

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

What

New package paginate (pure logic, 100 % covered, gated at 100): Breaks(root, pageH) / Paginate(root, Options{PageHeight, FirstPageHeight}) return where each page after the first starts. Atoms are text lines, whole table rows (layout-table wrapper rows descended into) and leaf boxes with height — html2pdf's rule, moved here. It now honours the document's CSS fragmentation (#141): forced breaks (page/left/right, page-break-* aliases), break-inside: avoid (kept whole when the box fits a page), break-before/after: avoid (keep with next/previous), orphans/widows, with css-break-3 §5.4's relaxation order when a page cannot be cut otherwise. Documented limit: margins at an unforced break are not truncated.

Measured against Chrome

html2pdf's corpus/fixtures/breaks.html (sections with break-before: page, a table and a figure with break-inside: avoid placed to straddle a page end, an h2 with break-after: avoid, an eight-line paragraph with orphans/widows 3; @page { size: A5; margin: 15mm }) printed with headless Chrome 141 gave the marker → page key. Laid out at the A5 page area (118 × 180 mm) with the engine's fonts, paginate puts every marker on Chrome's page and produces Chrome's nine pages (the orphans paragraph is cut with ≥ 3 lines each side, as the constraint says; Chrome cuts 4 + 4).

Tests

Synthetic trees for plain fill, forced breaks (short page), avoid-inside kept whole / cut when taller than a page, keep-with-next in both forms, orphans/widows (3/3, 4/4, unsatisfiable 5/5 relaxed), a chain of avoids longer than a page, tall atoms overflowing, first-page height, wrapper rows, nil/empty — plus the Chrome answer key.

🤖 Generated with Claude Code

A new pure-logic package: Breaks(root, pageH) / Paginate(root, Options)
return the document y at which each page after the first starts. It
cuts only between atoms — a text line, a whole table row (a layout-table
wrapper row is descended into), a leaf box with height — the rule
go-pdfkit/html2pdf paginated with, moved here so every consumer of the
layout tree paginates alike; and it now reads what the document asks
(css #141): forced breaks (break-before/after: page, left, right and the
page-break-* aliases), break-inside: avoid kept whole when the box can
fit a page, break-before/after: avoid (keep with the next or previous
content), orphans and widows — relaxing orphans/widows first and the
avoid rules second when a page cannot otherwise be cut, as css-break-3
§5.4 prescribes. Margins adjoining an unforced break are not truncated
(documented).

The answer key is Chrome: html2pdf's fixture (sections, a straddling
table and figure, a heading kept with its paragraph, an eight-line
paragraph with orphans/widows 3, on A5 with 15 mm margins) printed with
headless Chrome gave marker → page; laid out at the A5 page area with
the engine's own fonts, every marker lands on Chrome's page and the
count is Chrome's nine. Synthetic trees cover each rule and each
relaxation. 100 % covered and added to the coverage gate at 100.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 2e0350d into main Sep 6, 2026
7 checks passed
@tannevaled
tannevaled deleted the paginate branch September 6, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant