Skip to content

0.20.0 — The corpus campaign closes: 14 of 15, attribute selectors, CSS tables, half-leading

Choose a tag to compare

@danmolitor danmolitor released this 05 Sep 20:51
· 76 commits to main since this release

Closing the corpus campaign: the 15 real production templates from the compat experiment now measure 14 of 15 rendering correctly, 1 degrading legibly with its cause named in warnings, 0 broken (11/4/0 at 0.19.0; 3/8/4 when the experiment started). This release also lands the vertical-centering work, with one global typographic change every user should read first.

⚠️ Behavior change: half-leading — every text baseline moves down

Glyph baselines previously sat exactly font-size below the line-box top; the leading now splits evenly above and below the glyph block, per the CSS line box model (and every browser). Every baseline moves down by (line-height − font-size) / 2 — +2.4pt at the default 1.4 ratio on 12pt text. Line boxes do not move or resize: layout geometry, page breaks, and page counts are unchanged — only the ink inside each line box shifts, closer to where Chrome puts it. This is what makes the pre-flexbox centering idiom (line-height matched to a box height) actually center. Fixed-height flex boxes with align-items: center also now truly center (CSS 9.4.8 — the flex line honors the container's definite cross size).

⚠️ Behavior change: position: fixed renders as position: absolute

A paged renderer's viewport is the page, so fixed anchors to its containing block on the page where it occurs — not repeated on every page (margin boxes remain the running-content mechanism; a warning names the difference). The wkhtmltopdf print-footer idiom #footer { position: fixed; bottom: 0 } now sits flush at the page bottom.

⚠️ Behavior change: @media width measures the page box

Media Queries Level 4 defines width in paged media as the width of the page box — A4 = 794 CSS px, so (min-width: 768px) is true on A4, matching Chrome print. Earlier versions measured the content box (a documented spec misreading, now corrected in the README). Only queries with thresholds between your content-box and page-box widths change outcome.

HTML input path

  • Attribute selectors — all seven operators ([attr], =, ~=, |=, ^=, $=, *=) plus the i case flag, with spec specificity. [class*="span"] is Bootstrap 2's entire grid, so BS2 templates get their columns back.
  • CSS tables on divsdisplay: table / table-row / table-cell, the pre-flexbox equal-height-columns idiom. Multi-row CSS tables use the native table machinery; a single-row one (the columns idiom) becomes a breakable flex row. Known boundary, reported via the render-defect channel: a flex row that splits across pages lays its children sequentially, not as parallel columns.
  • body { overflow-x: hidden } is a page-level clip — the print equivalent of a browser suppressing horizontal overflow; off-viewport-parked furniture (right: -230px admin-shell sidebars) disappears instead of smearing into the margin. Engine-side: PageConfig.clipContentX, available to JSON callers too.
  • rowspan occupancy — cells after a rowspan land in their correct columns (table_column_offsets, one pure function shared by layout, measurement, and column counting).
  • position: running() suppression, Bootstrap print-stylesheet display values no longer produce warning noise, and the wkhtmltopdf migration guide documents the fixed-footer semantics.

Engine

  • Visual regression suite is now a live CI gate (Linux-canonical references, 1% threshold; a missing rasterizer fails the job rather than passing vacuously).
  • New render defects: sequential flex-row splits and over-tall atomic table rows report themselves.

Full details in engine/CHANGELOG.md and packages/html/CHANGELOG.md. All packages in lockstep at 0.20.0: npm (@formepdf/*), crates.io (forme-pdf), PyPI (formepdf), Go (v0.20.0).