Skip to content

css: fragmentation properties, @page, and the absolute length units - #141

Merged
tannevaled merged 2 commits into
mainfrom
css-fragmentation
Sep 6, 2026
Merged

tannevaled merged 2 commits into
mainfrom
css-fragmentation

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Two items, both for paged media (go-pdfkit/html2pdf and the paginate package consume the css package through the names below, which are a contract).

1. CSS Fragmentation 3 properties and @page

  • css.Style gains BreakBefore, BreakAfter css.Break (BreakAuto, BreakAvoid, BreakPage, BreakLeft, BreakRight), BreakInside css.BreakInside (BreakInsideAuto, BreakInsideAvoid) — not inherited, initial auto — and Orphans, Widows int — inherited, initial 2. Every break-before/break-after/break-inside value parses; column/region-only values map to auto (they say nothing about pages); always/all/recto/verso are a forced page break; page-break-* are aliases. orphans/widows accept positive integers only (0, negatives, 2.5 ignored). inherit/initial/unset per CSS Cascade.
  • css/page.go: PageRule, ParsePageRules(src, m Media) []PageRule, PageSpec{Width, Height float64; Margin [4]float64; MarginSet [4]bool}, ResolvePage(rules, selector) PageSpec, DocumentPage(root, externalSheets, m, selector) PageSpec. Selectors :first/:left/:right/:blank, named pages, selector lists; size (auto, 1–2 lengths, the ten named sizes, landscape/portrait either order); margin + longhands. @page inside @media print applies under css.Print only; @layer unwrapped. Nested margin boxes (@top-center { … }) are skipped and the declarations around them kept.
  • The element cascade is unchanged: parseRules already skipped @page as an unknown at-rule; styleElementText is factored out of collectAuthorRulesFrom so DocumentPage reads the same <style> text.

2. Absolute length units in parseLength

Measured: <div style="height:40mm"> laid out with H = 0 (4cm, 1.5in, 100pt, 10pc likewise); height:151px gave 151. parseLength knew only px/em/rem/%/vw/vh. Added in, cm, mm, Q, pt, pc per CSS Values 4 §6.2 in the one function every property resolves a length through (font-size, calc() terms, line-height, flex basis — hasUnit updated too). A unit is split off as trailing letters and the rest must be a number, so thin never reads as a length. @page sizes/margins use the same table.

Tests

Every value of every property through both spellings; cascade inheritance (orphans/widows inherited, break-* not); inherit/initial/unset; the unit table (40mm → 151.18px, 12pt → 16px); a layout test that height:40mm is 151.18px tall; and the @page contract cases (A5 + 15mm → 559.37×793.70 with 56.69px margins; 100mm 200mm; letter landscape both orders; 10cm square; margin: 1in 2in; :first overriding only the top; print-only @page; nested @top-center with surrounding margin still read; DocumentPage over two <style>s plus an external sheet, external first, later wins).

Coverage: css 99.5% → 99.6% (floor 99.5); layout 100%. Cross-builds for riscv64/loong64 checked locally. FIDELITY.md round 59.

🤖 Generated with Claude Code

tannevaled and others added 2 commits September 6, 2026 20:16
…te length units

CSS Fragmentation 3 properties on Style (BreakBefore/BreakAfter as Break,
BreakInside, Orphans/Widows — inherited, initial 2) with the legacy
page-break-* aliases; ParsePageRules/ResolvePage/DocumentPage for @page
(selectors, size with the named sizes and orientation, margins, @media
print honoured, nested margin boxes skipped without losing neighbours).

parseLength now understands in/cm/mm/Q/pt/pc (CSS Values 4 §6.2): a
`height:40mm` laid out as 0 before. @page goes through the same table.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 05b7f9a into main Sep 6, 2026
7 checks passed
@tannevaled
tannevaled deleted the css-fragmentation branch September 6, 2026 18:18
tannevaled added a commit that referenced this pull request Sep 6, 2026
…142)

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>
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