feat(html): let a host put a floor under the margin a view keeps - #748
Merged
Conversation
`HtmlConfig::min_content_margin` states, per side, the least distance the generated content keeps from the view's border. Every view already insets its content by some amount it names itself — 3mm for a reflowed text document, 16px for the page column and the source views, 8px for a font specimen, nothing for an image or a file listing — and no single number could stand in for all of them, so the config is a floor rather than a value: an unset side leaves the view exactly where it was, a set one only ever raises it. The arithmetic is css's. Each inset becomes `max(own, var(--odr-min-margin-*, 0px))`, so the view's own measure stays in the stylesheet and no unit has to be converted to state it. `write_content_margin_style` declares the variables per view and writes nothing at all when no side is set, which is what keeps the shipped stylesheets independent of the config — they are shared under one name when the config links rather than embeds them. One place still needs the number: the gutter around the page column is part of the width the view is fitted to, so `page_column_gutter_pixels` reads the config and a side css can read but `css_pixels` cannot convert keeps the built-in gutter — the margin still applies, only the fit cannot count it. A unit css could not read at all is dropped rather than written into the style element it would otherwise close. A sheet and the media view are not inset: the one meets the edge by design, the other is full-bleed. Bound in python, jni, wasm (`minContentMargin`, css lengths as strings) and apple, where `ODRMeasure` and `ODRDirectionalMeasure` gain the initialisers a caller needs to state one. Rendering with the default config is unchanged. Where the stylesheet is linked the document is byte-identical; where it is embedded — the image, pdf and font views — the reference output restates the same insets as `max(own, var(...))`, which with the variables unset is the number it always was, and the shipped stylesheets render pixel-identically under `compare-html`. The reference-output pins are advanced to match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UoWywNazPio5vphzB59U5g
andiwand
force-pushed
the
feat-html-min-content-margin
branch
from
August 26, 2026 19:15
44cc7c7 to
2c08c53
Compare
`is_css_unit` accepted any alphabetic unit, so a value css cannot resolve was written into `--odr-min-margin-*` and voided the whole shorthand that floors an inset against it - the opposite of raising it - while the fit kept counting a gutter the page no longer had. One predicate now decides both paths: a finite, positive magnitude with a css length unit, matched case-insensitively. Everything else keeps the built-in inset. `css_pixels` folds case and drops a non-finite result too, which makes it a subset again, so the fit calls it directly. Cover it a level down, in `html_common`, and render every view that writes the declaration rather than only the odt document view. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GzAvJtjXwfvy7c5yMxsJpu
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Generated with Claude Code
HtmlConfig::min_content_marginstates, per side, the least distance the generated content keeps from the view's border.Why a floor and not a value
Every view already insets its content by an amount it names itself —
3mmfor a reflowed text document,16pxfor the page column and the source views,8pxfor a font specimen, nothing for an image or a file listing. No single default could stand in for all of them, so the config is a floor: an unset side leaves the view exactly where it is today, a set side only ever raises it.How
The arithmetic is css's. Each inset becomes
max(own, var(--odr-min-margin-*, 0px)), so the view's own measure stays in the stylesheet and nothing has to be converted to state it:write_content_margin_styledeclares the variables per view and writes nothing at all when no side is set — that is what keeps the shipped stylesheets independent of the config, which they must be: withembed_shipped_resources=falsethey are shared under one name across documents.One place still needs the number in pixels: the gutter around the page column is part of the width the view is fitted to, so
page_column_gutter_pixelsnow reads the config. A side css can read butcss_pixelscannot convert (em,%) keeps the built-in gutter — the margin still applies, only the fit cannot count it. A unit css could not read at all is dropped rather than written into the<style>element it would otherwise close.3mmmax(3mm, …)16pxmax(16px, …), and the fit follows16px16pxmax(16px, …)0by default8pxmax(8px, …)A sheet and the media view are not inset: the one meets the edge by design (its ruler is sticky against the scrollport), the other is full-bleed.
Bindings
DirectionalStyle<Measure>was already mirrored everywhere, so the surface is small: pythonmin_content_margin, jniminContentMargin(DirectionalMeasure), wasmminContentMargin(css lengths as strings), appleminContentMargin— whereODRMeasureandODRDirectionalMeasuregain the initialisers a caller needs to state one, having been read-only style mirrors until now.Verification
odr_test: 951 non-output tests and 278 output tests pass; three newhtml.*tests cover written/not-written, the dropped unit, and the fit.minContentMarginReachesTheHtml, a pytest, a wasmnode --testcase and an XCTest cover each binding.compare-html --driver chrome— 410/411 public and 1383/1403 private matched, and every mismatch reproduces when the reference output is compared against a byte-changed copy of itself, i.e. they are screenshot flakes in the pdf and svg views, not this change.Not in this PR
A
marginreference-output variant.compare-htmlfails on an output directory the reference repo does not have, so pinning the floor visually needs the reference output regenerated andtest/data.cmakeadvanced — say the word and it goes on this branch.