Skip to content

LibWeb: Match selectors against the live DOM from Rust - #10795

Merged
awesomekling merged 27 commits into
LadybirdBrowser:masterfrom
awesomekling:keepgoing
Jul 22, 2026
Merged

LibWeb: Match selectors against the live DOM from Rust#10795
awesomekling merged 27 commits into
LadybirdBrowser:masterfrom
awesomekling:keepgoing

Conversation

@awesomekling

Copy link
Copy Markdown
Member

The Rust selector engine was already the authoritative matcher, but DOM-sensitive selectors still delegated their decisions to C++ callbacks. Replace those callbacks with Stylo-like, lifetime-bound wrappers over the live DOM. Rust now reads current names, namespaces, identifiers, attributes, tree relationships, and element states through small accessors and performs the matching itself.

The wrappers borrow existing interned identifiers and string storage, without copying, caching, or snapshotting DOM state. Live reads are important because focus, validity, media state, attributes, shadow relationships, and other CSS inputs may change independently. This removes the semantic simple-selector and tree-navigation callback surfaces while preserving the existing invalidation and :has() bookkeeping. Regression coverage exercises mutations, disconnected elements, shadow trees, XML namespaces, and stateful selectors.

Replace id and standards-mode class callbacks with a lifetime-bound
element wrapper that borrows current interned identifier storage from
the DOM. Return fresh wrappers from tree navigation so ancestor and
sibling matching reads live data.

Keep quirks-mode class matching in C++ for its case-insensitive
comparison. Cover identifier mutation, ancestor matching, disconnected
elements, and shadow trees.
Borrow each element's current interned local name and namespace through
the lifetime-bound selector wrapper. Borrow the stylesheet's current
default namespace once per match call.

Match HTML and fast-path tag names plus default, null, and wildcard
namespaces in Rust. Keep C++ callbacks for named namespaces and the
case-insensitive XML slow path.
Expose the current HTML root-type fact on the lifetime-bound element
wrapper and read it directly for :root matching. Remove the per-selector
callback while preserving existing behavior for disconnected HTML
elements.

Extend the live selector test to cover connected and disconnected roots.
Compare borrowed interned local-name and namespace identities directly
for typed siblings. Remove the C++ callback while preserving the exact
distinction between absent and empty namespaces.
Reduce FfiElement to a lifetime-bound opaque DOM handle. Query names,
IDs, classes, document mode, and element-type facts through minimal C++
accessors at the point Rust needs each value.

Count live DOM reads separately from semantic matcher callbacks. Drop
captured pointers and booleans so no element state survives in the
wrapper.
Remove the per-match namespace context snapshot. Query the current
default or named namespace through the live matching context only when a
selector needs it.

Move namespace-kind and element-identity decisions into Rust. Delete
the semantic universal-selector callback.
Expose each element's current local name as a lifetime-bound view over
its existing ASCII or UTF-16 storage. Compare the XML slow path in Rust
without copying the name.

Delete the semantic tag-name callback and keep namespace matching on
the live context accessors.
Expose each current class name as a lifetime-bound view over its
existing ASCII or UTF-16 storage. Perform quirks-mode case folding and
comparison in Rust without copying the class list.

Delete the semantic quirks-mode class callback and the retained C++
pointer that only supported it.
Expose the current attribute count and each attribute's interned name,
namespace, and lifetime-bound value view. Implement namespace, value,
and case-sensitivity decisions in Rust without copying DOM strings.

Delete the semantic attribute callback and the retained C++ selector
pointer that only supported it. Cover mutations and non-leading
namespaced attributes.
Expose the element's current link status as a live read and select the
:link and :any-link behavior in Rust. Move the currently unsupported
autofill, visited, and volume-locked outcomes into Rust as well.

This halves semantic simple-selector callbacks on the baseline
workloads while preserving dynamic link invalidation.
Expose current focus, focus indication, and focus-within facts through
small DOM accessors. Select :focus, :focus-visible, and :focus-within
in Rust without retaining state between matching calls.
Expose the current fullscreen flag through a small DOM accessor. Match
:fullscreen in Rust without retaining state between matching calls.
Expose the current heading level through a small DOM accessor. Perform
:heading and :heading() matching in Rust without retaining DOM state.
Expose the popover attribute and current visibility through small DOM
accessors. Match :popover-open in Rust without retaining DOM state.
Expose the current element directionality through a small DOM accessor.
Compare :dir() arguments in Rust without retaining DOM state.
Copy the interned custom-state identifier identity during selector
compilation and query the live CustomStateSet from Rust matching. Stop
retaining a C++ SimpleSelector pointer solely for :state() matching.
Lend element-owned language storage to Rust for each matching call.
Implement RFC 4647 extended filtering in Rust and remove the semantic
:lang() callback without retaining state in the selector bridge.
Expose current activation, control, definition, location, and
placeholder facts through lifetime-bound DOM handles. Dispatch these
pseudo-classes in Rust without retaining selector-derived state.
Expose the current media element flags through lifetime-bound DOM
handles. Let Rust derive :playing from the live paused state, and add
coverage for media and non-media elements plus live mutation.
Replace the generic pseudo-class callback with lifetime-bound live DOM
queries. Return typed meter, requiredness, and validity states so Rust
can make paired selector decisions without retaining derived state.
Count parent, sibling, descendant, slot, and part traversal with the
lifetime-bound DOM view instead of a separate callback category. Compose
:empty in Rust from live element-child and text-child facts.
Store each borrowed DOM node pointer only once. Reconstruct the
lightweight element handle after checking the node kind, and compare
resolved namespace identities directly after confirming that the value
is named.
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 317ada5b-ed89-458d-8530-124f74aeb48f

📥 Commits

Reviewing files that changed from the base of the PR and between 5f9a9c2 and c1a7fd1.

📒 Files selected for processing (2)
  • Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt
  • Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html
🚧 Files skipped from review as they are similar to previous changes (2)
  • Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt
  • Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html

📝 Walkthrough

Walkthrough

Selector matching now uses Rust-side DOM evaluation through typed FFI element handles, interned selector identities, expanded DOM callbacks, and updated namespace, attribute, language, pseudo-class, and traversal logic. C++ adds :has() fast rejection, while selector tests cover live state and mutation behavior.

Changes

Selector matching FFI migration

Layer / File(s) Summary
FFI contracts and interned selector data
Libraries/LibWeb/CSS/Rust/..., Libraries/LibWeb/CSS/SelectorRustBridge.cpp, Libraries/LibWeb/DOM/Element.*
FFI layouts and generated renames now expose typed elements, interned selector identities, namespace data, form-state enums, and a language string view.
C++ DOM adapter and callbacks
Libraries/LibWeb/CSS/SelectorMatching.cpp
C++ provides typed element conversion, DOM accessors, namespace resolution, traversal callbacks, updated :open handling, and :has() fast rejection.
Rust matching engine and entry points
Libraries/LibWeb/CSS/Rust/src/selector_engine.rs, Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs
Rust performs selector predicates, string and language matching, namespace handling, traversal, and exported matching through FfiElement handles.
Selector behavior coverage
Tests/LibWeb/Text/input/css/*, Tests/LibWeb/Text/expected/css/*, Tests/LibWeb/Text/input/*selector*
Tests cover live media states, identifier mutations, disconnected and shadow-root elements, attribute mutations, XML case handling, and quirks-mode matching.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant SelectorMatching
  participant rust_selector_matches
  participant FfiDom
  participant DOMCallbacks
  SelectorMatching->>rust_selector_matches: pass FfiElement handles
  rust_selector_matches->>FfiDom: construct selector DOM adapter
  FfiDom->>DOMCallbacks: read element properties and traverse DOM
  DOMCallbacks-->>FfiDom: return element state and navigation data
  FfiDom-->>rust_selector_matches: return selector match result
Loading
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description accurately matches the PR’s Rust selector engine refactor, live DOM wrappers, removed callbacks, and added regression coverage.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Libraries/LibWeb/CSS/Rust/src/selector_engine.rs`:
- Around line 2675-2677: Update matches_id_selector to apply the same
quirks-mode ASCII-insensitive comparison used by class matching, while
preserving exact matching outside quirks mode and the existing interned-name
handling.
- Around line 2657-2668: Update the non-HTML fallback in the selector matching
logic around is_html_element_in_html_document and name_matches to use
case-sensitive local-name comparison. Preserve ASCII-insensitive matching only
for HTML elements in HTML documents, while retaining the existing fast-mode
interned-name path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 195fccfc-4bee-47dd-bd6b-cca987a51fb9

📥 Commits

Reviewing files that changed from the base of the PR and between 5a8ea0d and f71e80b.

📒 Files selected for processing (14)
  • Libraries/LibWeb/CSS/Rust/build.rs
  • Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs
  • Libraries/LibWeb/CSS/Rust/src/selector_engine.rs
  • Libraries/LibWeb/CSS/SelectorMatching.cpp
  • Libraries/LibWeb/CSS/SelectorRustBridge.cpp
  • Libraries/LibWeb/DOM/Element.cpp
  • Libraries/LibWeb/DOM/Element.h
  • Tests/LibWeb/Text/expected/css/live-media-state-selectors.txt
  • Tests/LibWeb/Text/expected/css/live-selector-identifiers.txt
  • Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt
  • Tests/LibWeb/Text/input/css/live-media-state-selectors.html
  • Tests/LibWeb/Text/input/css/live-selector-identifiers.html
  • Tests/LibWeb/Text/input/css/selector-engine-characterization.html
  • Tests/LibWeb/Text/input/quirks-mode-case-insensitive-class-selector.html

Comment thread Libraries/LibWeb/CSS/Rust/src/selector_engine.rs
Comment thread Libraries/LibWeb/CSS/Rust/src/selector_engine.rs
The normal selector path compared all non-HTML element names without
regard to ASCII case. This let wrong-case compound selectors match SVG
and XML elements even though their names must retain their original
case.

Compare borrowed local names exactly outside the HTML fast path. Extend
the SVG test with a wrong-case compound selector and correct the XML
characterization.
ID selector matching always compared interned names exactly. Class
matching already honored ASCII-insensitive behavior in quirks mode, so
mixed-case IDs alone remained unmatched on legacy pages.

Expose the live ID value only for the quirks-mode fallback. Retain the
interned identity fast path elsewhere and share the case-sensitivity
accessor with class matching. Add focused querySelector coverage.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html`:
- Around line 29-32: Strengthen the selector test around the existing
linearGradient query by also asserting that
document.querySelector("linearGradient") returns the SVG element. Update the
pass/fail output so the test verifies exact-case matching while retaining the
lowercase non-match assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a561aeee-ca44-41d8-9101-7baf2e58d30b

📥 Commits

Reviewing files that changed from the base of the PR and between f71e80b and 5f9a9c2.

📒 Files selected for processing (7)
  • Libraries/LibWeb/CSS/Rust/src/selector_engine.rs
  • Libraries/LibWeb/CSS/SelectorMatching.cpp
  • Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt
  • Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt
  • Tests/LibWeb/Text/expected/quirks-mode-case-insensitive-id-selector.txt
  • Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html
  • Tests/LibWeb/Text/input/quirks-mode-case-insensitive-id-selector.html
🚧 Files skipped from review as they are similar to previous changes (3)
  • Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt
  • Libraries/LibWeb/CSS/SelectorMatching.cpp
  • Libraries/LibWeb/CSS/Rust/src/selector_engine.rs

Check exact-case compound matching before the wrong-case spelling. This
prevents the negative assertion from passing when both selectors fail.
@awesomekling
awesomekling merged commit 9b6432a into LadybirdBrowser:master Jul 22, 2026
14 checks passed
@awesomekling
awesomekling deleted the keepgoing branch July 22, 2026 01:37
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