Skip to content

refactor(html): name the boxes a frame anchors against, not every element - #685

Merged
andiwand merged 1 commit into
mainfrom
fix/html-explicit-containing-blocks
Aug 11, 2026
Merged

refactor(html): name the boxes a frame anchors against, not every element#685
andiwand merged 1 commit into
mainfrom
fix/html-explicit-containing-blocks

Conversation

@andiwand

@andiwand andiwand commented Aug 11, 2026

Copy link
Copy Markdown
Member

Follow-up to #684, now rebased on main.

*{position:relative} in the shipped stylesheet made every element a containing block, so every absolutely positioned frame and shape resolved against its direct parent by accident rather than against the box the file actually anchors it to.

Auditing all 1144 reference-output pages for absolutely positioned elements with no positioned ancestor in their own markup, exactly three boxes were doing that work:

container elements relying on it formats
.odr-page-outer 2022 odg, odp, ppt, pptx
td 184 ods, xlsx
x-p 26 odt

Those three state it; the blanket rule goes. Two things it turned out not to be holding up: .odr-sheet-sort, whose header is already position:sticky (the comment claiming otherwise is corrected here), and the PDF frontend, which links no document.css and declares .p{position:relative} itself.

*{margin:0} goes with it. Of the elements this sheet reaches — div, table/tr/td/th/col, img, svg, x-p, x-s, mark, span, br/wbr — only body carries a margin from the browser's own sheet, and body{margin:0} on the next line already covers it. (<p>/<h1> appear only in font_file.cpp, which writes its own inline <style>.)

Being a containing block is also what a page-anchored frame has to get past to reach its page: frame_anchor_type reads all four ODF anchor types, but translate_frame_properties only branches on as_char, so a page-anchored frame lands against its paragraph. That stays unfixable while every ancestor is an anchor, and becomes fixable now.

Verification

Rendered both sides of the whole public corpus in Chrome and compared pixels.

Correction to an earlier claim in this PR. I first verified with htmlcmp, which reported all 332 + 1062 files matching. That result was empty: compare-html short-circuits byte-identical files without rendering them, and this PR changes only CSS, so every html file was identical and nothing was ever rendered. CI's compare step has the same blind spot. Both were re-run with a renderer that screenshots each page from either tree regardless.

Of 228 public pages, 226 are pixel-identical. Two differ:

  • docx/sample1.docx/document.html — 2943 px
  • docx/sample3.docx/document.html — 1891 px

Both are one line of text moving exactly 1 pixel down (ink centroid dx +0.000, dy +0.999, ink identical to the byte). Layout is unchanged: every element's top and height match to 0.01px. Injecting the two dropped rules back one at a time isolates the cause to *{position:relative} alone (*{margin:0} has no effect) — a line whose baseline falls on a fractional pixel snaps to a different whole pixel when its spans are positioned boxes. The new result is consistent with every other unpositioned line on the page.

Reproduced across repeated runs with identical bounding boxes, and a control rendering the same tree twice shows the pdf/xls pages are nondeterministic while these two are not.

🤖 Generated with Claude Code

…ment

`*{position:relative}` made every element a containing block, so an
absolutely positioned frame or shape resolved against its direct parent
by accident. Three boxes were doing that work - the page for a slide's
and a drawing's shapes, a paragraph or a cell for a text document's
frames - so they state it, and the blanket rule goes.

Being a containing block is also what a page-anchored frame has to get
past to reach its page, which no element can while every ancestor is
one.

`*{margin:0}` goes with it: of the elements this sheet reaches, only
`body` carries a margin from the browser's own sheet, and `body{margin:0}`
already covers it.

Rendering is unchanged - htmlcmp compares 332 public and 1062 private
outputs, generated from one build and rendered against either sheet, with
no mismatch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVkpzsGS6YASMqm3NwmWib
@andiwand
andiwand enabled auto-merge (squash) August 11, 2026 18:50
@andiwand
andiwand merged commit 7aefde7 into main Aug 11, 2026
36 checks passed
@andiwand
andiwand deleted the fix/html-explicit-containing-blocks branch August 11, 2026 19:04
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