Commit Graph

400 Commits

Author SHA1 Message Date
SGavrl
6c77eb2383 render: preserve block fill beside floats 2026-07-23 15:13:40 +00:00
SGavrl
bbdb779de9 test: gate opposing float geometry 2026-07-23 15:04:03 +00:00
SGavrl
85613b741d render: place opposing floats in one band 2026-07-23 15:03:50 +00:00
SGavrl
c7438370db test: gate replaced grid geometry 2026-07-23 14:48:36 +00:00
SGavrl
9471591c6c render: preserve responsive image grid geometry 2026-07-23 14:48:27 +00:00
SGavrl
da5cc2cc2a render: honor font shorthand 2026-07-23 14:25:09 +00:00
SGavrl
944db4b104 test: gate float flow geometry 2026-07-23 14:10:02 +00:00
SGavrl
735375ee33 render: preserve float flow bands 2026-07-23 14:09:57 +00:00
SGavrl
1f85dfce6b test: gate flex item geometry 2026-07-23 13:34:19 +00:00
SGavrl
64b628404b render: ignore non-items in flex layout 2026-07-23 13:34:09 +00:00
SGavrl
29628df548 render: resolve contextual CSS math 2026-07-23 12:53:11 +00:00
SGavrl
df9359ec87 render: honor responsive grid track syntax 2026-07-23 12:41:56 +00:00
SGavrl
b13a1e5e71 render: resolve relative box edge lengths 2026-07-23 12:30:44 +00:00
SGavrl
b39ada2fc0 render: resolve block auto margins 2026-07-23 12:19:29 +00:00
SGavrl
7827a6a788 render: honor modern containing-block triggers
Track transform, filter, backdrop-filter, perspective, containment,
will-change, and content-visibility triggers independently. Use the combined
state for absolute and fixed containing-block ancestry.
2026-07-23 12:02:55 +00:00
SGavrl
bbeceb97ad render: honor transform containing blocks
Track every non-none transform as a containing-block trigger, including
unsupported visual transforms. Keep separate nearest-ancestor maps so
positioned boxes capture absolute descendants while only transforms capture
fixed descendants.
2026-07-23 11:47:00 +00:00
SGavrl
6ea1739121 render: position inset boxes against CSS containers
Reparent inset-positioned absolute boxes to the nearest non-static ancestor
and fixed boxes to the initial containing block. Keep auto-axis boxes on the
existing static-position path, distinguish fixed from absolute, and blockify
positioned inline boxes and floats.
2026-07-23 11:34:23 +00:00
SGavrl
5de5d07c24 render: support streamed framework hydration
Make attribute maps live, preserve parser script execution state, and expose
element scrolling used by React commit callbacks. Connect fallback web streams
so React Router and Remix hydration payloads flow through codec transforms.
2026-07-23 10:59:51 +00:00
SGavrl
4979606a2f render: respect CSS box sizing 2026-07-23 10:06:55 +00:00
SGavrl
3d7a8e2582 render: honor important cascade priority 2026-07-22 12:59:55 +00:00
SGavrl
22f33e4dec render: z-index paint layers and clear ending float zones
Paint order was pure tree order, so a dropdown/overlay declared before a
later sibling lost to it and a z:-1 decorative backdrop covered its own
content. A positioned element with a non-zero z-index now lifts its
whole subtree into a separate paint layer: negative layers under the
normal flow, positive above it, each sorted ascending with tree order
preserved within equal z. Nested z roots paint inside their ancestor
root's subtree in tree order, the pragmatic core of CSS stacking
contexts.

clear (left/right/both, previously unparsed) now ends the float zone,
so a clearfix element drops below the float at full width instead of
joining the flow column beside it.

Repro-verified: an earlier z:10 overlay paints over a later sibling, a
later z:-1 backdrop paints under earlier content, and the clear repro
matches Chromium within 3px on every measure. Spot set unchanged or
slightly improved; obstacle course 33/33.
2026-07-16 13:10:28 +00:00
SGavrl
953292563d render: table vertical-align and specified column/row sizing
Cells now vertical-align the way tables actually do: middle by default
(the effective UA behavior), with author top/middle/bottom parsed and
applied to whichever shape the cell took: main-axis alignment on the
flex-column stand-in, line alignment on a promoted inline context, or a
pinned-origin offset for the pure-text leaf path where the shaped text
height is known at finalize.

Specified column widths finally apply. <col>/<colgroup> widths (style
or attribute, with span) and colspan-1 cell widths are recorded per
column at build time and pinned inside the column-balancing pass: px
directly, percent against the table's content width, with each column's
content min-content still flooring it. The cell's own width is
neutralized once it has fed the column, so the cell box fills its grid
area instead of shrinking to a fraction of its own already-sized track
(a width:50% cell previously ended up at 25 percent). Unspecified
columns keep content sizing but stretch to fill a definite table width
instead of leaving a dead strip of table background. Row and cell
heights act as row minimums.

Verified against Chromium on a 5-case repro (column from cell width,
row backgrounds, vertical alignment, col element width, percent cell in
a fixed table): all match. Wikipedia and the prose spot set unchanged;
obstacle course 33/33.
2026-07-16 12:47:47 +00:00
SGavrl
1d96c316ca render: clip image rasters to their visible bound, browser-like alt fallback
paint_image clipped an object-fit:cover overflow to the element box but
ignored the ancestor overflow:hidden clip entirely, so a partially
visible image (a half-scrolled carousel slide, a hero cropped by its
section) painted its full raster past the viewport edge. Pass the
already clip-intersected visible rect down and mask against it whenever
the destination exceeds it on any side.

Broken images now fall back the way browsers do: a non-empty alt renders
as text in place of the image with no placeholder box, alt="" renders
nothing (declared decorative), and only a missing alt keeps the neutral
grey placeholder.

Verified with a repro covering the half-clipped cover slide and all
three alt variants; no movement on the image-heavy spot set; obstacle
course 33/33.
2026-07-16 12:23:17 +00:00
SGavrl
c29c64fc57 render: scale image intrinsic size by the selected srcset density
The srcset picker already normalized every candidate to a pixel density
(x descriptor, or w / source-size) to choose one, then threw the density
away, so layout sized the image from the file's raw pixels. Every 2x
responsive asset occupied twice its design size.

Return the picked candidate's density alongside its URL (img srcset,
data-srcset, and picture source paths alike) and divide the fetched
intrinsic dimensions by it, so layout sees CSS px.

Verified: srcset "x.png 2x" of a 648px file lays out at 324 CSS px and
"648w" with sizes=300px at 300, while a bare src stays at raw size; no
regressions on the image-heavy spot set (a bbc delta traced to homepage
content drift against a days-old baseline, 8.0 vs 7.8 percent when
recaptured same-time); obstacle course 33/33.
2026-07-16 12:08:19 +00:00
SGavrl
35cbbff9b2 render: real block layout for mixed content, owner-space overflow clips
Two structural fixes to the layout and paint model.

Mixed inline+block content: a block holding both was promoted wholesale
to a flex-row-wrap container to approximate the inline formatting
context. That collapsed contentless block children to width 0 (a
position:relative hero wrapper whose only child is an absolute fill
image vanished with it) and let text share lines with sibling blocks.
Now the parent stays a real block, block-level children stay direct
children (auto width fills the containing block), and each maximal run
of consecutive inline-level siblings becomes one anonymous block-level
box carrying the IFC. A run of pure text and foldable inline wrappers
collapses to a single shaped cosmic-text leaf (one taffy node per run
instead of one per word); runs with atomic inline boxes fall back to a
flex-wrap wrapper. Absolutely positioned children neither join nor
break runs and no longer count as inline content, so their containing
block keeps its real used width and taffy resolves width:100%/inset:0
fills against it.

Overflow clips: an inherited overflow:hidden clip was shifted by the
painted node's own accumulated translate, so a carousel track's
translateX dragged the viewport's clip along with every slide and all
slides stayed visible stacked. Clips are now stored in the clip owner's
coordinate space (owner translate baked in at resolve time) and no
longer move with descendants, for boxes, per-word text, and shaped
inline items alike. The same walk records per-node accumulated
translates once, replacing the per-painted-node ancestor walks. Also
parse translate3d (Swiper-style carousels position their track
exclusively through it) and make transform:none reset a lower-priority
transform.

Verified: abs-fill repros that rendered 0x0 now fill across
block/flex/grid parents; carousel repro shows exactly one slide;
danluu.com mismatch vs Chromium 13.0 to 5.7 percent; no regressions
across the prose and framework spot sets; obstacle course 33/33; 36
percent faster on a layout-bound synthetic page with neutral real-page
memory.
2026-07-16 11:58:10 +00:00
SGavrl
8fa387faed render: respect stylesheet gating (disabled/media) and follow @import
Skip <link rel=stylesheet> sheets that are disabled or gated to
print/dark, and resolve @import chains inside fetched sheets so the
imported CSS reaches the cascade.

- disabled links (JS-toggled alternate themes) no longer paint pages
  dark under a light Chromium
- media="(prefers-color-scheme: dark)" link sheets are skipped
- @import url(...) is fetched recursively; Sphinx docs pull their layout
  from basic.css via classic.css and it was being dropped

Pixel fidelity vs headless Chromium: docs.python 95.5 to 10.2 percent,
web.dev 91.1 to 10.5, developer.chrome 88.0 to 18.9. No regressions on
the good-site set; obstacle course still 33/33.
2026-07-16 11:56:28 +00:00
SGavrl
ea486a8900 render: blockify grid items so inline children lay out correctly
A direct child of a grid container is blockified per CSS Display 3, so an
inline <a>/<time>/<span> becomes its own block-level grid item instead of
being flattened into loose words in the container (which shattered grid
lists like jvns.ca's .article-list of alternating <time>/<a> into one word
per cell). Grid only, not flex, since flex-column is also our block
stand-in for <td>.
2026-07-11 22:17:33 +00:00
SGavrl
e559dc2b64 render: resolve protocol-relative image URLs
An <img src="//host/path"> (ubiquitous on Wikipedia and CDN-hosted media)
now inherits the document scheme, defaulting to https for non-network bases
so a file:// page does not turn it into file://host. Previously url::join
against a file base produced an unfetchable URL.
2026-07-11 22:01:14 +00:00
SGavrl
281a22e247 render: match Chrome font metrics with Liberation Sans/Serif/Mono
Chrome's default sans-serif on this class of host is Liberation Sans, not
DejaVu (measured: sans-serif == Arial == Helvetica == Liberation width,
DejaVu ~12% wider). Forcing DejaVu drifted all text wider and shifted
every line. Bundle Liberation Sans/Serif/Mono (DejaVu kept as glyph
fallback), default to Liberation Sans, and resolve font-family to the
right generic (monospace/code -> Mono, serif -> Serif, else -> Sans).
A controlled text page drops from ~15-20% to 3.5% pixel mismatch and
every line lands within 1px of Chrome.
2026-07-11 21:49:33 +00:00
SGavrl
23594b1f96 render: percentage padding, box-shadow, external-sprite svg, gradient text
percentage padding/margin: top/bottom percentages resolve against the
containing-block width (the responsive aspect-ratio-box trick, e.g.
padding-top:56.25%), resolved to px in the top-down layout pass so card
media boxes reserve space instead of collapsing.
box-shadow: parse and paint an outset shadow (rounded, offset, spread,
soft-edge approximation) so cards/buttons/menus have depth.
external-sprite svg: resolve <use href="url#id"> to an external sprite
file, inject the referenced symbol so inline-svg icon systems rasterize.
background-clip:text: paint transparent-with-gradient text (the common
gradient hero/button label) instead of leaving it invisible.
2026-07-11 21:02:57 +00:00
SGavrl
58dee6330e browser: skip print-only stylesheets when fetching linked CSS
A <link rel=stylesheet media="print"> must not apply on screen; fetching
and injecting it let print rules override the screen layout (e.g. the
Guardian's print sheet). Skip links whose media is print-only.
2026-07-11 20:36:58 +00:00
SGavrl
e27a3bc5b0 render: object-fit, transform translate/scale, inline-svg rasterization
object-fit: contain/cover/scale-down/none for <img> so an image whose
aspect ratio differs from its box is fit (letterbox/crop) instead of
stretched.
transform: parse translate()/translateX/translateY (and scale); apply
translate as a paint-time offset to the element and its subtree, so
translate(-50%,-50%) centering and off-screen skip-links land correctly.
inline-svg: serialize and rasterize inline <svg> subtrees via resvg so
icon sprites and brand logos paint instead of showing blank.
2026-07-11 20:29:57 +00:00
SGavrl
2652ee64a4 render: real-world CSS rendering parity
Colors: oklch()/oklab() and color-mix(); linear-gradient backgrounds.
Grid: named-line placement, repeat() expansion, drop whitespace-only
grid items; upgrade taffy 0.7 -> 0.12 so aspect-ratio images in
auto-height rows no longer collapse to slivers.
CSS: @layer, nesting, @property, :is()/:where(), clamp(), @media not all;
source/track/param/area and svg desc/metadata as display:none.
Images: webp + intrinsic sizing, srcset/picture, browser UA + Accept for
CDN fetches, grey placeholder on failure; width/height HTML attributes
rank below author CSS with aspect-ratio preserved for auto axes.
2026-07-11 19:52:36 +00:00
SGavrl
88313178b2 Split text nodes into per-word layout leaves so text actually wraps
A whole DOM text node was one indivisible taffy leaf. Flex-wrap only
  breaks between items, never inside one, so a long run of plain text
  with no inline element breaking it up (an ordinary sentence with no
  link in it) either fit on one line or ran straight past the
  container's edge instead of wrapping. Short runs interrupted by links
  every few words (extremely common in Wikipedia's markup) happened to
  be narrow enough to wrap on their own, which is why paragraphs looked
  fine but any long plain-text stretch overflowed straight over the
  infobox and the History-section image.

  Text nodes now split into one taffy leaf per word, flattened into the
  same child list as sibling elements so words from different text nodes
  and inline elements interleave and wrap together like real inline
  flow. This changes the geometry data model: a text node's content is
  now a list of (box, word) pairs (DomLayout::text_runs) instead of a
  single box, since it can span several lines. Updated painting to draw
  each word at its own position, and fixed a bold-text measurement
  mismatch (layout width and drawn width disagreed by 1px/char, invisible
  before as one blob, visible as gaps between every word once each word
  is independently positioned).

  Added a regression test that fails against the old single-leaf
  behavior.
2026-07-03 12:59:40 +00:00
SGavrl
55e6787b0b Fix :enabled/:disabled/:checked pseudo-classes matching real DOM state
These were bundled in with the truly dynamic, interaction-only
  pseudo-classes (:hover/:active/:focus) and always resolved to false.
  That's right for :hover et al (there's no live cursor against a static
  snapshot) but wrong for :enabled/:disabled/:checked: those reflect
  real, static DOM state (the disabled/checked attributes), not user
  interaction, and modern component systems (Codex, Material, Bootstrap)
  lean on :enabled constantly for their base button/input styling. With
  it always false, every such base rule was silently inert, which is why
  the Wikipedia TOC toggle button (a <button> with no disabled
  attribute, so :enabled should be true) kept its default black border
  instead of the transparent one its :enabled.cdx-button--weight-quiet
  rule sets.

  :enabled now matches a form control (input/button/select/textarea/
  optgroup/option/fieldset) without a disabled attribute, :disabled the
  opposite, and :checked matches the checked/selected attribute.
2026-07-03 12:41:48 +00:00
SGavrl
ca4a4ef135 Fix icon rendering, calc()/var() length resolution, and approximate CSS float
Icons were rendering as either overlapping solid squares or not at all,
  text ran under the infobox instead of beside it, and general.css parsing
  had two more silent-corruption bugs on top of the ones fixed earlier:

  - A CSS declaration list was split on every literal semicolon, so any
    value containing one inside a quoted string or url() (a data: URI
    with a MIME parameter, e.g. url(data:image/svg+xml;utf8,<svg...>),
    extremely common for small inline icon SVGs) got torn into two
    malformed halves and silently dropped. This is why mask-image never
    applied to the TOC toggle icons at all.
  - data: URIs were only decoded as base64. Inline SVGs are just as
    commonly written as data:image/svg+xml;utf8,<percent-escaped text>,
    which needs percent-decoding instead; added that path.
  - top/left/right/bottom/width/height only handled bare lengths and a
    flat calc(a +/- b). Real values nest var(--x, fallback), calc(), and
    min()/max() inside each other (Wikipedia's icon offset math is
    literally calc(-1 * 22px / 2), and its icon sizing is
    calc(max(calc(var(--font-size-medium,1rem) + 4px),10px))). Replaced
    the flat evaluator with a small recursive resolver that handles all
    three, nested arbitrarily. This is what was placing the TOC toggle
    button's icon on top of the section text instead of in the left
    margin where it belongs.

  Added actual feature support once the parsing was correct:

  - mask-image/-webkit-mask-image: the pattern behind most scalable
    colored icons on the modern web (an SVG or image used purely as an
    alpha stencil, tinted by background-color/color). Previously every
    masked icon rendered as an unmasked solid-color square.
  - float: left/right, approximated without a full per-line reflow
    engine (which taffy's block/flex/grid modes don't provide): a
    floated element is grouped into a row alongside the flow siblings
    that follow it, up to whichever comes first of the next heading or
    another float, then everything from there on reverts to normal
    full-width flow. This isn't general CSS float, but it directly
    targets the overwhelmingly common real shape (an infobox or image
    floated near the top of an article, with the rest of the content
    starting at the next section heading), and is what makes article
    text actually wrap beside an infobox instead of running underneath
    it.

  Refactored paint.rs's image fetch/decode/cache logic into shared
  helpers (fetch_bytes, raster_to_pixmap) reused by plain <img> painting,
  background-image, and the new mask-image path.
2026-07-03 12:34:27 +00:00
SGavrl
920977776c Fix render engine: CSS grid, cascade correctness, SVG, inline flow
The render layer had no CSS Grid support, so Vector 2022's page layout
  (three nested grids) collapsed to a vertical stack with the article
  landing tens of thousands of pixels down the page. Wikipedia rendered
  as a near-blank page and Hacker News only worked through per-site
  hardcoded CSS overrides.

  Removed the HN-specific hardcoding and fixed the general engine instead:

  - CSS Grid layout (grid-template-areas/columns/rows via taffy's grid mode)
  - Indexed, specificity-correct cascade in a new css.rs, replacing the
    old test-every-selector-against-every-node scan. Cascade time on
    Wikipedia went from 9.5s to under 200ms.
  - Ancestor bloom-filter fast-rejection for selector matching in
    obscura-dom, needed to make the indexed cascade actually fast on
    descendant combinators.
  - :link/:visited pseudo-classes were previously unparseable, so any
    rule using them (a:link{color:...}, extremely common) silently
    dropped the whole rule from the cascade.
  - overflow:hidden clipping, fixing the pervasive visually-hidden-but-
    accessible pattern used for skip links and screen-reader-only labels.
  - @media parser robustness: it did a literal substring search for
    max-width: and silently dropped the constraint on max-width : 750px
    (space before colon) or max-width:calc(750px - 1px), treating
    mobile-only or narrow-viewport rules as always active. The calc()
    case was hiding Wikipedia's TOC sidebar via a rule meant only for
    narrow screens.
  - Table auto-layout approximation: the trailing auto-width td in a row
    absorbs leftover space, so a width:100% table's unsized last column
    reaches the right edge instead of shrinking to content.
  - Inline formatting context approximation: a block holding inline-level
    children (a paragraph or list item with links) is promoted to a
    wrapping flex row so text and links read as continuous prose instead
    of one fragment per line.
  - Whitespace-only text nodes no longer get a full line-height box. They
    used to stack up as blank rows, turning Wikipedia's collapsed TOC
    subsections and the page header into hundreds of pixels of dead space.
  - SVG rendering via resvg/usvg, plus resolving relative image URLs
    against the page's base URL.
  - background-image support including background-size/background-position
    for correctly sized and positioned icons.
  - Per-screenshot image fetch cache.
  - Fixed the layout-only (no paint feature) build, which didn't compile
    due to an unconditional call into paint-gated code.
2026-07-03 12:04:03 +00:00
SGavrl
cd1ee2dcc5 Hide head, script, and style tags from render tree 2026-07-02 22:21:19 +00:00
SGavrl
740caaea06 Fix layout text width estimates and alignment for hn header 2026-07-02 22:12:03 +00:00
SGavrl
e3d072f0b7 Implement Hacker News specific styles and drawing rules 2026-07-02 21:52:46 +00:00
SGavrl
ff622c00a0 Fix table rendering layout and alignment 2026-07-02 21:43:36 +00:00
SGavrl
f53d60e061 fix(render): implement top-down style inheritance and fix logo heuristic 2026-07-02 20:41:19 +00:00
SGavrl
cd3571ddc0 fix(render): render logo and parse named colors 2026-07-02 20:34:32 +00:00
SGavrl
bd4845b52c fix(render): correct center tag alignment and percentage dimensions 2026-07-02 20:26:55 +00:00
SGavrl
42874989eb feat(render): parse presentational attributes and font sizes for beautiful layout 2026-07-02 20:20:26 +00:00
SGavrl
9c8d62e13b fix(render): lay out text nodes in taffy and implement inline flow 2026-07-02 20:02:07 +00:00
SGavrl
193e30deb5 feat(render): implement basic CSS cascade matching from <style> blocks 2026-07-02 19:47:08 +00:00
SGavrl
bed6d6f619 feat(render): implement full image rendering for screenshots 2026-07-02 19:42:41 +00:00
SGavrl
6d33783ddf perf(render): optimize text rendering & feat(render): add border support 2026-07-02 19:38:00 +00:00
SGavrl
dec56fa42f feat(render): implement text rendering using ab_glyph 2026-07-02 19:34:52 +00:00