LibWeb: Match selectors against the live DOM from Rust - #10795
Conversation
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.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughSelector 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 ChangesSelector matching FFI migration
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
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (14)
Libraries/LibWeb/CSS/Rust/build.rsLibraries/LibWeb/CSS/Rust/src/ffi_stats.rsLibraries/LibWeb/CSS/Rust/src/selector_engine.rsLibraries/LibWeb/CSS/SelectorMatching.cppLibraries/LibWeb/CSS/SelectorRustBridge.cppLibraries/LibWeb/DOM/Element.cppLibraries/LibWeb/DOM/Element.hTests/LibWeb/Text/expected/css/live-media-state-selectors.txtTests/LibWeb/Text/expected/css/live-selector-identifiers.txtTests/LibWeb/Text/expected/css/selector-engine-characterization.txtTests/LibWeb/Text/input/css/live-media-state-selectors.htmlTests/LibWeb/Text/input/css/live-selector-identifiers.htmlTests/LibWeb/Text/input/css/selector-engine-characterization.htmlTests/LibWeb/Text/input/quirks-mode-case-insensitive-class-selector.html
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.
There was a problem hiding this comment.
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
📒 Files selected for processing (7)
Libraries/LibWeb/CSS/Rust/src/selector_engine.rsLibraries/LibWeb/CSS/SelectorMatching.cppTests/LibWeb/Text/expected/css/selector-engine-characterization.txtTests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txtTests/LibWeb/Text/expected/quirks-mode-case-insensitive-id-selector.txtTests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.htmlTests/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.
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.