Skip to content

fix(parse): keep <body> and <html> style attributes - #36

Open
leshchenko1979 wants to merge 1 commit into
Macawls:mainfrom
leshchenko1979:fix/body-style
Open

leshchenko1979 wants to merge 1 commit into
Macawls:mainfrom
leshchenko1979:fix/body-style

Conversation

@leshchenko1979

Copy link
Copy Markdown
Contributor

Fixes #34.

Problem

html.ParseFragment ignores <html>, <head> and <body> start tags together with their attributes (HTML standard: the fragment is parsed as if it were the body's children). Parse returned those children, so the <body> element never existed in the tree and its style was lost without a warning.

Every stylesheet-driven page and every email template puts the page font, base colour and background on <body>.

Before / after

Case Before After Chromium
<body style="background:#ff0000">, pixel (150,150) (255,255,255) (255,0,0) (255,0,0)
font-family:'DejaVu Sans' on <body>, family passed to Resolve "sans-serif" (built-in fallback) "DejaVu Sans" "DejaVu Sans"
ПАРИТЕТ ПАРИТЕТ 123, weight 700, 20px, ink width 231 px 263 px 263 px
Parity Parity 123, weight 700 154 px 187 px 188 px
ПАРИТЕТ ПАРИТЕТ 123, weight 400 229 px 234 px 235 px

Bold Cyrillic was 12 % narrower than the browser — which looks exactly like a hinting or font-metrics bug and sends you hunting through font/. It was not: the bold face was never used.

Fix

parse/parse.go: when the input is a full document that declares attributes on <body>, parse it with html.Parse and convert the <body> element itself, attributes intact. Fragments, and documents whose <body> carries no attributes, keep the existing fragment path unchanged — so no existing output moves.

Tests

  • parse/body_style_test.go — body style survives; fragment path unchanged; <body> without attributes adds no wrapper.
  • ogre_body_style_e2e_test.go — through the public Render: body background is painted; text declared on <body> measures identically to the same declaration on a <div>.

Both fail on main (root background = "", want "#ff0000", background pixel = (0,0,0)) and pass with the fix.

Comparison harness (21 Chromium cases, DejaVu Sans)

Nothing regressed; text cases improved because they now use the font the document asks for:

case before after
03-letter-spacing 0.23 0.14
04-line-height 0.52 0.49
13-inherit-font 0.52 0.37
14-wrap-lh115 1.03 0.77
15-wrap-lh200 1.16 0.72
21-boxsizing-content 1.09 0.00
17-table-div 0.60 1.20

17-table-div is the one case that reads worse, and it is not a regression of this change: that fixture relies on box-sizing: content-box (600 px content box inside 56 px padding). Ogre's default is border-box (Satori compatibility, see #23), so the wrapper is 600 px wide in both engines; once the real font arrives the cells are laid out at their true widths and the difference becomes visible. Adding an explicit box-sizing: content-box to that fixture drops it to 0.07 %:

17-table-div variant diff
as committed (implicit default) 1.20 %
with explicit box-sizing: content-box 0.07 %

Suggest updating that fixture rather than changing the renderer default.

@Macawls

Macawls commented Oct 28, 2026

Copy link
Copy Markdown
Owner

Thanks for digging into this, but I can't merge this one as it stands, for two reasons.

  1. It's stacked on the other branches. This PR contains 12 commits: merges of fix(style): honour line-height instead of forcing 1.2 x font-size #24, fix(render): pass letter-spacing through to the glyph path builder #25, fix(layout): honour box-sizing: content-box #26, fix(render): interpolate gradients in sRGB and honour premultiplied alpha #31, fix(render): match browser box-shadow falloff and premultiplied alpha #32 and feat(layout): lay out display:table, table-row and table-cell #33 plus the actual change (96aa967). Merging it would merge all of them at once, and some of those still need changes. If this goes ahead, it would need to be a single commit on top of main.

  2. It conflicts with the input contract. Ogre takes an HTML fragment with a single root element, matching Satori. A pending change makes this explicit: <!DOCTYPE>, <html>, <head> and <body> are ignored with their attributes dropped, <script>/<style>/<link>/<meta>/<title> are stripped, and the CLI warns about both (or fails with --strict). There is a test asserting that <body class="..."> is dropped. With this PR applied on top of that, 4 tests fail (TestBodyAttributesDropped, TestFullDocumentRendersBodyContent, TestRenderFullDocument, TestRunFullDocumentMatchesFragment). Keeping <body> attributes only when they are present also makes the behaviour depend on whether the body has attributes, which is harder to explain than "pass the body contents".

The fix for the case in #34 is to move the <body> styles onto the root element of the fragment. If you think the contract itself should change, I'm happy to discuss it on #34.

html.ParseFragment ignores <html>, <head> and <body> start tags together
with their attributes, so a full document that declares its font, colour
or background on <body> rendered with built-in defaults and no background.
Stylesheet-driven pages and email templates put exactly those properties
there, so the loss was silent and universal.

Detect a full document that declares attributes on <body> and parse it with
html.Parse, then convert the <body> element itself. Fragments and <body>
without attributes keep the existing fragment path.

Visible as text rendering in the built-in font instead of the declared one:
bold Cyrillic came out 12% narrower than Chromium, which reads like a font
metrics bug but is not.

Fixes Macawls#34
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.

body/html style attribute is silently ignored: ParseFragment drops the <body> element and its attributes

2 participants