From ba4481a32c897125635be9964ce5964322d22a97 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 22:35:57 +0200 Subject: [PATCH 01/27] LibWeb: Read selector identifiers through live DOM wrappers 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. --- Libraries/LibWeb/CSS/Rust/build.rs | 1 + .../LibWeb/CSS/Rust/src/selector_engine.rs | 149 ++++++++++++++---- Libraries/LibWeb/CSS/SelectorMatching.cpp | 84 ++++++---- Libraries/LibWeb/CSS/SelectorRustBridge.cpp | 2 + .../css/live-selector-identifiers.txt | 8 + .../input/css/live-selector-identifiers.html | 47 ++++++ 6 files changed, 224 insertions(+), 67 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/css/live-selector-identifiers.txt create mode 100644 Tests/LibWeb/Text/input/css/live-selector-identifiers.html diff --git a/Libraries/LibWeb/CSS/Rust/build.rs b/Libraries/LibWeb/CSS/Rust/build.rs index e1f928161dcd6..50bec1b25bce4 100644 --- a/Libraries/LibWeb/CSS/Rust/build.rs +++ b/Libraries/LibWeb/CSS/Rust/build.rs @@ -562,6 +562,7 @@ fn main() -> Result<(), Box> { ("FfiSimpleSelector", "SimpleSelector"), ("FfiCompoundSelector", "CompoundSelector"), ("FfiSelector", "Selector"), + ("FfiElement", "Element"), ("FfiElementAndShadowHost", "ElementAndShadowHost"), ] { selector_config diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index d9a862055022e..e461a5e89d9e0 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -73,6 +73,10 @@ pub struct QualifiedName { #[derive(Clone, Debug, PartialEq, Eq)] pub struct NameSelector { pub name: SelectorString, + /// The one-word identity of the C++ `Utf16FlyString` backing `name`. This is present for + /// selectors compiled from C++ and allows the live DOM wrapper to compare interned names + /// without crossing the FFI. + interned_name: Option, /// See [`QualifiedName::cxx_simple_selector`]. cxx_simple_selector: RetainedCxxPointer, } @@ -1512,6 +1516,7 @@ pub struct FfiStringView { pub struct FfiSimpleSelector { pub selector_type: FfiSimpleSelectorType, pub cxx_simple_selector: *const c_void, + pub interned_name: *const usize, pub namespace_type: NamespaceType, pub namespace: FfiStringView, pub name: FfiStringView, @@ -1558,6 +1563,42 @@ pub struct RustSelector { selector: Rc, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +/// A borrowed view of the element's current selector-relevant identifier data. +/// +/// C++ constructs this immediately before crossing the FFI and after every tree navigation. The +/// pointers must not be retained beyond the matching call. +pub struct FfiElement { + pub pointer: *const c_void, + pub id: *const usize, + pub classes: *const usize, + pub class_count: usize, + pub class_names_are_case_insensitive: bool, +} + +impl FfiElement { + fn is_null(self) -> bool { + self.pointer.is_null() + } + + fn id(self) -> Option { + // SAFETY: The C++ wrapper points at the live element's `Utf16FlyString` storage, which is + // pinned for the duration of the matching call. + unsafe { self.id.as_ref().copied() } + } + + unsafe fn classes<'a>(self) -> &'a [usize] { + if self.class_count == 0 { + return &[]; + } + assert!(!self.classes.is_null()); + // SAFETY: C++ guarantees that `Utf16FlyString` has the size and alignment of `usize` and + // that the element's class vector cannot mutate during this matching call. + unsafe { std::slice::from_raw_parts(self.classes, self.class_count) } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum FfiNodeKind { Element, @@ -1572,6 +1613,7 @@ enum FfiNodeKind { struct FfiNode<'a> { pointer: *const c_void, kind: FfiNodeKind, + element: FfiElement, marker: PhantomData<&'a FfiCallScope>, } @@ -1588,13 +1630,26 @@ impl FfiNode<'_> { assert_eq!(self.kind, FfiNodeKind::Element); self.pointer } + + fn as_element(self) -> FfiElement { + assert_eq!(self.kind, FfiNodeKind::Element); + self.element + } +} + +impl<'a> FfiNode<'a> { + fn classes(self) -> &'a [usize] { + // SAFETY: `FfiNode` cannot outlive the call scope which pins the C++ element and its + // current class storage. + unsafe { self.as_element().classes() } + } } #[derive(Clone, Copy)] #[repr(C)] pub struct FfiElementAndShadowHost { - pub element: *const c_void, - pub shadow_host: *const c_void, + pub element: FfiElement, + pub shadow_host: FfiElement, } unsafe extern "C" { @@ -1609,8 +1664,7 @@ unsafe extern "C" { cxx_simple_selector: *const c_void, matching_mode: TagNameMatchingMode, ) -> bool; - fn selector_ffi_matches_id(element: *const c_void, cxx_simple_selector: *const c_void) -> bool; - fn selector_ffi_matches_class(element: *const c_void, cxx_simple_selector: *const c_void) -> bool; + fn selector_ffi_matches_class_quirks(element: *const c_void, cxx_simple_selector: *const c_void) -> bool; fn selector_ffi_matches_attribute( context: *mut c_void, element: *const c_void, @@ -1622,13 +1676,13 @@ unsafe extern "C" { fn selector_ffi_matches_state(element: *const c_void, cxx_simple_selector: *const c_void) -> bool; fn selector_ffi_matches_heading(element: *const c_void, levels: *const i64, level_count: usize) -> bool; - fn selector_ffi_parent_element(element: *const c_void, shadow_host: *const c_void) -> *const c_void; - fn selector_ffi_parent_element_in_light_tree(element: *const c_void) -> *const c_void; - fn selector_ffi_previous_element_sibling(element: *const c_void) -> *const c_void; - fn selector_ffi_next_element_sibling(element: *const c_void) -> *const c_void; - fn selector_ffi_first_element_child(element: *const c_void) -> *const c_void; - fn selector_ffi_first_element_descendant(element: *const c_void) -> *const c_void; - fn selector_ffi_next_element_descendant(element: *const c_void, root: *const c_void) -> *const c_void; + fn selector_ffi_parent_element(element: *const c_void, shadow_host: *const c_void) -> FfiElement; + fn selector_ffi_parent_element_in_light_tree(element: *const c_void) -> FfiElement; + fn selector_ffi_previous_element_sibling(element: *const c_void) -> FfiElement; + fn selector_ffi_next_element_sibling(element: *const c_void) -> FfiElement; + fn selector_ffi_first_element_child(element: *const c_void) -> FfiElement; + fn selector_ffi_first_element_descendant(element: *const c_void) -> FfiElement; + fn selector_ffi_next_element_descendant(element: *const c_void, root: *const c_void) -> FfiElement; fn selector_ffi_has_no_element_or_nonempty_text_children(element: *const c_void) -> bool; fn selector_ffi_has_same_type(first: *const c_void, second: *const c_void) -> bool; fn selector_ffi_is_document_root(element: *const c_void) -> bool; @@ -1701,13 +1755,27 @@ impl<'a> FfiDom<'a> { (!pointer.is_null()).then_some(FfiNode { pointer, kind, + element: FfiElement { + pointer: std::ptr::null(), + id: std::ptr::null(), + classes: std::ptr::null(), + class_count: 0, + class_names_are_case_insensitive: false, + }, marker: PhantomData, }) } - unsafe fn element(&self, element: *const c_void) -> Option> { - // SAFETY: The caller guarantees that a non-null pointer identifies a live DOM element. - unsafe { self.node(element, FfiNodeKind::Element) } + unsafe fn element(&self, element: FfiElement) -> Option> { + if element.is_null() { + return None; + } + Some(FfiNode { + pointer: element.pointer, + kind: FfiNodeKind::Element, + element, + marker: PhantomData, + }) } unsafe fn scope(&self, scope: *const c_void) -> Option> { @@ -1763,17 +1831,22 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn matches_id_selector(&mut self, element: FfiNode<'a>, id: &NameSelector) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the element and retained simple selector remain valid - // for the duration of matching. - unsafe { selector_ffi_matches_id(element.as_element_pointer(), id.cxx_simple_selector.as_ptr()) } + id.interned_name.is_some_and(|id| element.as_element().id() == Some(id)) } fn matches_class_selector(&mut self, element: FfiNode<'a>, class_name: &NameSelector) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the element and retained simple selector remain valid - // for the duration of matching. - unsafe { selector_ffi_matches_class(element.as_element_pointer(), class_name.cxx_simple_selector.as_ptr()) } + let ffi_element = element.as_element(); + if ffi_element.class_names_are_case_insensitive { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); + // SAFETY: `FfiDom` guarantees that the element and retained simple selector remain + // valid for the duration of matching. + return unsafe { + selector_ffi_matches_class_quirks(element.as_element_pointer(), class_name.cxx_simple_selector.as_ptr()) + }; + } + class_name + .interned_name + .is_some_and(|class_name| element.classes().contains(&class_name)) } fn matches_attribute_selector(&mut self, element: FfiNode<'a>, attribute: &AttributeSelector) -> bool { @@ -2100,6 +2173,12 @@ unsafe fn string_from_ffi(value: FfiStringView) -> SelectorString { unsafe { copy_ffi_slice(value.data, value.length) } } +unsafe fn interned_name_from_ffi(selector: &FfiSimpleSelector) -> Option { + // SAFETY: The caller guarantees that a non-null pointer identifies the one-word storage of a + // live C++ `Utf16FlyString` for the duration of selector compilation. + unsafe { selector.interned_name.as_ref().copied() } +} + unsafe fn qualified_name_from_ffi(selector: &FfiSimpleSelector) -> QualifiedName { QualifiedName { namespace_type: selector.namespace_type, @@ -2132,11 +2211,15 @@ unsafe fn simple_selector_from_ffi(selector: &FfiSimpleSelector) -> SimpleSelect FfiSimpleSelectorType::Id => SimpleSelector::Id(NameSelector { // SAFETY: The caller guarantees that every string view in `selector` is valid. name: unsafe { string_from_ffi(selector.name) }, + // SAFETY: The caller guarantees that all retained C++ selector data is valid. + interned_name: unsafe { interned_name_from_ffi(selector) }, cxx_simple_selector: RetainedCxxPointer::new(selector.cxx_simple_selector), }), FfiSimpleSelectorType::Class => SimpleSelector::Class(NameSelector { // SAFETY: The caller guarantees that every string view in `selector` is valid. name: unsafe { string_from_ffi(selector.name) }, + // SAFETY: The caller guarantees that all retained C++ selector data is valid. + interned_name: unsafe { interned_name_from_ffi(selector) }, cxx_simple_selector: RetainedCxxPointer::new(selector.cxx_simple_selector), }), FfiSimpleSelectorType::Attribute => SimpleSelector::Attribute(AttributeSelector { @@ -2255,8 +2338,8 @@ unsafe fn compiled_selector_from_ffi(selector: &FfiSelector) -> Rc( - element: *const c_void, - shadow_host: *const c_void, + element: FfiElement, + shadow_host: FfiElement, context: *mut c_void, scope: *const c_void, collects_selector_involvement_metadata: bool, @@ -2274,9 +2357,9 @@ unsafe fn with_ffi_dom( inside_has_argument, ) }; - // SAFETY: The caller guarantees that `element` points to a DOM element. + // SAFETY: The caller guarantees that `element` wraps a live DOM element. let element = unsafe { dom.element(element) }.unwrap(); - // SAFETY: The caller guarantees that a non-null `shadow_host` points to a DOM element. + // SAFETY: The caller guarantees that a non-null `shadow_host` wraps a live DOM element. let shadow_host = unsafe { dom.element(shadow_host) }; // SAFETY: The caller guarantees that a non-null `scope` points to a DOM parent node. let scope = unsafe { dom.scope(scope) }; @@ -2330,15 +2413,15 @@ pub unsafe extern "C" fn rust_selector_target_pseudo_element(selector: *const Ru /// # Safety /// The `selector` handle must have been returned by `rust_selector_create`. `element` and a -/// non-null `shadow_host` must point to C++ DOM elements, a non-null `scope` must point to a C++ +/// non-null `shadow_host` must wrap live C++ DOM elements, a non-null `scope` must point to a C++ /// DOM parent node, and `context` must point to a C++ Rust matching context. All referenced objects /// must remain valid for this call. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_selector_matches( selector: *const RustSelector, - element: *const c_void, + element: FfiElement, pseudo_element: u8, - shadow_host: *const c_void, + shadow_host: FfiElement, context: *mut c_void, scope: *const c_void, collects_selector_involvement_metadata: bool, @@ -2378,15 +2461,15 @@ pub unsafe extern "C" fn rust_selector_matches( /// # Safety /// The `selector` handle must have been returned by `rust_selector_create`. `element` and a -/// non-null `shadow_host` must point to C++ DOM elements, a non-null `scope` must point to a C++ +/// non-null `shadow_host` must wrap live C++ DOM elements, a non-null `scope` must point to a C++ /// DOM parent node, and `context` must point to a C++ Rust matching context. All referenced objects /// must remain valid for this call. #[unsafe(no_mangle)] pub unsafe extern "C" fn rust_selector_matches_originating_element( selector: *const RustSelector, pseudo_element: u8, - element: *const c_void, - shadow_host: *const c_void, + element: FfiElement, + shadow_host: FfiElement, context: *mut c_void, scope: *const c_void, collects_selector_involvement_metadata: bool, @@ -2658,6 +2741,7 @@ mod tests { fn class(name: &str) -> SimpleSelector { SimpleSelector::Class(NameSelector { name: name.encode_utf16().collect(), + interned_name: None, cxx_simple_selector: RetainedCxxPointer::default(), }) } @@ -2712,6 +2796,7 @@ mod tests { combinator: *combinator, simple_selectors: vec![SimpleSelector::Id(NameSelector { name: Box::from([b'x' as u16]), + interned_name: None, cxx_simple_selector: RetainedCxxPointer::default(), })] .into_boxed_slice(), diff --git a/Libraries/LibWeb/CSS/SelectorMatching.cpp b/Libraries/LibWeb/CSS/SelectorMatching.cpp index 3ffb444084ad1..12991ea100b4b 100644 --- a/Libraries/LibWeb/CSS/SelectorMatching.cpp +++ b/Libraries/LibWeb/CSS/SelectorMatching.cpp @@ -653,14 +653,34 @@ static bool matches_pseudo_class_state(CSS::PseudoClass pseudo_class, DOM::Eleme VERIFY_NOT_REACHED(); } +static CSS::SelectorFFI::Element element_to_ffi(DOM::Element const* element) +{ + if (!element) + return {}; + + // `Utf16FlyString` is its interned one-word representation. Rust borrows these words directly + // while matching; the DOM and selector trees pin their respective strings for the call. + static_assert(sizeof(Utf16FlyString) == sizeof(uintptr_t)); + static_assert(alignof(Utf16FlyString) == alignof(uintptr_t)); + + auto const& classes = element->class_names(); + return { + .pointer = element, + .id = element->id().has_value() ? reinterpret_cast(&element->id().value()) : nullptr, + .classes = reinterpret_cast(classes.data()), + .class_count = classes.size(), + .class_names_are_case_insensitive = element->document().in_quirks_mode(), + }; +} + bool matches(CSS::Selector const& selector, DOM::AbstractElement const& target, GC::Ptr shadow_host, MatchContext& context, GC::Ptr scope) { return CSS::SelectorFFI::rust_selector_matches( &selector.rust_selector(), - &target.element(), + element_to_ffi(&target.element()), CSS::pseudo_element_to_ffi(target.pseudo_element()), - shadow_host.ptr(), + element_to_ffi(shadow_host.ptr()), &context, scope.ptr(), context.collect_per_element_selector_involvement_metadata, @@ -674,8 +694,8 @@ bool matches_originating_element_for_pseudo_element(CSS::Selector const& selecto return CSS::SelectorFFI::rust_selector_matches_originating_element( &selector.rust_selector(), CSS::pseudo_element_to_ffi(pseudo_element), - &target.element(), - shadow_host.ptr(), + element_to_ffi(&target.element()), + element_to_ffi(shadow_host.ptr()), &context, scope.ptr(), context.collect_per_element_selector_involvement_metadata, @@ -768,8 +788,7 @@ using CSS::SelectorFFI::TagNameMatchingMode; DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_universal); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_tag_name); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_id); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_class); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_class_quirks); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_attribute); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_pseudo_class); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_language); @@ -826,16 +845,11 @@ extern "C" bool selector_ffi_matches_tag_name(void* context, void const* element && matches_namespace(qualified_name, target, match_context.style_sheet_for_rule); } -extern "C" bool selector_ffi_matches_id(void const* element, void const* cxx_simple_selector) -{ - return ffi_element(element).id() == ffi_simple_selector(cxx_simple_selector).id_name(); -} - -extern "C" bool selector_ffi_matches_class(void const* element, void const* cxx_simple_selector) +extern "C" bool selector_ffi_matches_class_quirks(void const* element, void const* cxx_simple_selector) { auto const& target = ffi_element(element); - auto case_sensitivity = target.document().in_quirks_mode() ? CaseSensitivity::CaseInsensitive : CaseSensitivity::CaseSensitive; - return target.has_class(ffi_simple_selector(cxx_simple_selector).class_name(), case_sensitivity); + VERIFY(target.document().in_quirks_mode()); + return target.has_class(ffi_simple_selector(cxx_simple_selector).class_name(), CaseSensitivity::CaseInsensitive); } static bool matches_attribute_value(CSS::Selector::SimpleSelector::Attribute::MatchType match_type, Utf16View selector_value, Utf16View element_value, CaseSensitivity case_sensitivity) @@ -1043,54 +1057,54 @@ extern "C" bool selector_ffi_matches_heading(void const* element, i64 const* lev return ReadonlySpan { levels, level_count }.contains_slow(heading->heading_level()); } -extern "C" void const* selector_ffi_parent_element(void const* element, void const* shadow_host) +extern "C" CSS::SelectorFFI::Element selector_ffi_parent_element(void const* element, void const* shadow_host) { auto const& target = ffi_element(element); if (!shadow_host) - return target.parent_element(); + return element_to_ffi(target.parent_element()); if (element == shadow_host) - return nullptr; - return target.parent_or_shadow_host_element(); + return {}; + return element_to_ffi(target.parent_or_shadow_host_element()); } -extern "C" void const* selector_ffi_parent_element_in_light_tree(void const* element) +extern "C" CSS::SelectorFFI::Element selector_ffi_parent_element_in_light_tree(void const* element) { - return ffi_element(element).parent_element(); + return element_to_ffi(ffi_element(element).parent_element()); } -extern "C" void const* selector_ffi_previous_element_sibling(void const* element) +extern "C" CSS::SelectorFFI::Element selector_ffi_previous_element_sibling(void const* element) { - return ffi_element(element).previous_element_sibling(); + return element_to_ffi(ffi_element(element).previous_element_sibling()); } -extern "C" void const* selector_ffi_next_element_sibling(void const* element) +extern "C" CSS::SelectorFFI::Element selector_ffi_next_element_sibling(void const* element) { - return ffi_element(element).next_element_sibling(); + return element_to_ffi(ffi_element(element).next_element_sibling()); } -extern "C" void const* selector_ffi_first_element_child(void const* element) +extern "C" CSS::SelectorFFI::Element selector_ffi_first_element_child(void const* element) { - return ffi_element(element).first_child_of_type(); + return element_to_ffi(ffi_element(element).first_child_of_type()); } -extern "C" void const* selector_ffi_first_element_descendant(void const* element) +extern "C" CSS::SelectorFFI::Element selector_ffi_first_element_descendant(void const* element) { auto const& root = ffi_element(element); for (auto const* node = root.first_child(); node; node = node->next_in_pre_order(&root)) { if (node->is_element()) - return static_cast(node); + return element_to_ffi(static_cast(node)); } - return nullptr; + return {}; } -extern "C" void const* selector_ffi_next_element_descendant(void const* element, void const* root) +extern "C" CSS::SelectorFFI::Element selector_ffi_next_element_descendant(void const* element, void const* root) { auto const& root_element = ffi_element(root); for (auto const* node = static_cast(&ffi_element(element))->next_in_pre_order(&root_element); node; node = node->next_in_pre_order(&root_element)) { if (node->is_element()) - return static_cast(node); + return element_to_ffi(static_cast(node)); } - return nullptr; + return {}; } extern "C" bool selector_ffi_has_no_element_or_nonempty_text_children(void const* element) @@ -1139,8 +1153,8 @@ extern "C" CSS::SelectorFFI::ElementAndShadowHost selector_ffi_slotted_parent(vo if (slot_shadow_root != match_context.rule_shadow_root) continue; return { - .element = slot, - .shadow_host = slot_shadow_root ? slot_shadow_root->host() : nullptr, + .element = element_to_ffi(slot), + .shadow_host = element_to_ffi(slot_shadow_root ? slot_shadow_root->host() : nullptr), }; } return {}; @@ -1182,7 +1196,7 @@ extern "C" CSS::SelectorFFI::ElementAndShadowHost selector_ffi_part_parent(void* else next_shadow_host = nullptr; } - return { .element = &host, .shadow_host = next_shadow_host }; + return { .element = element_to_ffi(&host), .shadow_host = element_to_ffi(next_shadow_host) }; } return {}; } diff --git a/Libraries/LibWeb/CSS/SelectorRustBridge.cpp b/Libraries/LibWeb/CSS/SelectorRustBridge.cpp index f0bee1fdb3c44..9e466e95a7d1a 100644 --- a/Libraries/LibWeb/CSS/SelectorRustBridge.cpp +++ b/Libraries/LibWeb/CSS/SelectorRustBridge.cpp @@ -173,10 +173,12 @@ class SelectorCompiler { case Selector::SimpleSelector::Type::Id: output.selector_type = SelectorFFI::SimpleSelectorType::Id; output.name = store_string(simple_selector.id_name()); + output.interned_name = reinterpret_cast(&simple_selector.id_name()); break; case Selector::SimpleSelector::Type::Class: output.selector_type = SelectorFFI::SimpleSelectorType::Class; output.name = store_string(simple_selector.class_name()); + output.interned_name = reinterpret_cast(&simple_selector.class_name()); break; case Selector::SimpleSelector::Type::Attribute: { output.selector_type = SelectorFFI::SimpleSelectorType::Attribute; diff --git a/Tests/LibWeb/Text/expected/css/live-selector-identifiers.txt b/Tests/LibWeb/Text/expected/css/live-selector-identifiers.txt new file mode 100644 index 0000000000000..60f5cde935745 --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/live-selector-identifiers.txt @@ -0,0 +1,8 @@ +initial match: true +initial color: rgb(1, 2, 3) +mutated match: true +stale match: false +mutated color: rgb(4, 5, 6) +disconnected match: true +shadow initial match: true +shadow mutated match: true diff --git a/Tests/LibWeb/Text/input/css/live-selector-identifiers.html b/Tests/LibWeb/Text/input/css/live-selector-identifiers.html new file mode 100644 index 0000000000000..ca17872b8b91f --- /dev/null +++ b/Tests/LibWeb/Text/input/css/live-selector-identifiers.html @@ -0,0 +1,47 @@ + + + +
+ +
+
+ From 15be6d80e07a43030b820d0bb262fee06f950954 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 22:46:38 +0200 Subject: [PATCH 02/27] LibWeb: Match common tag selectors through live DOM wrappers 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. --- Libraries/LibWeb/CSS/Rust/build.rs | 2 + .../LibWeb/CSS/Rust/src/selector_engine.rs | 113 ++++++++++++++++-- Libraries/LibWeb/CSS/SelectorMatching.cpp | 33 ++++- Libraries/LibWeb/CSS/SelectorRustBridge.cpp | 2 + 4 files changed, 136 insertions(+), 14 deletions(-) diff --git a/Libraries/LibWeb/CSS/Rust/build.rs b/Libraries/LibWeb/CSS/Rust/build.rs index 50bec1b25bce4..245659627b682 100644 --- a/Libraries/LibWeb/CSS/Rust/build.rs +++ b/Libraries/LibWeb/CSS/Rust/build.rs @@ -563,6 +563,8 @@ fn main() -> Result<(), Box> { ("FfiCompoundSelector", "CompoundSelector"), ("FfiSelector", "Selector"), ("FfiElement", "Element"), + ("FfiDefaultNamespaceType", "DefaultNamespaceType"), + ("FfiNamespaceContext", "NamespaceContext"), ("FfiElementAndShadowHost", "ElementAndShadowHost"), ] { selector_config diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index e461a5e89d9e0..6af1c6143951d 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -65,6 +65,8 @@ pub struct QualifiedName { pub namespace: SelectorString, pub name: SelectorString, pub lowercase_name: SelectorString, + interned_name: Option, + interned_lowercase_name: Option, /// Pointer to the C++ simple selector this was compiled from, so that matching callbacks can /// compare its interned strings without copying. Null in unit tests. cxx_simple_selector: RetainedCxxPointer, @@ -1517,6 +1519,7 @@ pub struct FfiSimpleSelector { pub selector_type: FfiSimpleSelectorType, pub cxx_simple_selector: *const c_void, pub interned_name: *const usize, + pub interned_lowercase_name: *const usize, pub namespace_type: NamespaceType, pub namespace: FfiStringView, pub name: FfiStringView, @@ -1571,10 +1574,14 @@ pub struct RustSelector { /// pointers must not be retained beyond the matching call. pub struct FfiElement { pub pointer: *const c_void, + pub local_name: *const usize, + pub namespace_: *const usize, pub id: *const usize, pub classes: *const usize, pub class_count: usize, pub class_names_are_case_insensitive: bool, + pub namespace_is_null: bool, + pub is_html_element_in_html_document: bool, } impl FfiElement { @@ -1588,6 +1595,16 @@ impl FfiElement { unsafe { self.id.as_ref().copied() } } + fn local_name(self) -> Option { + // SAFETY: The C++ wrapper points at the live element's pinned `Utf16FlyString` storage. + unsafe { self.local_name.as_ref().copied() } + } + + fn namespace(self) -> Option { + // SAFETY: The C++ wrapper points at the live element's pinned `Utf16FlyString` storage. + unsafe { self.namespace_.as_ref().copied() } + } + unsafe fn classes<'a>(self) -> &'a [usize] { if self.class_count == 0 { return &[]; @@ -1599,6 +1616,23 @@ impl FfiElement { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +// NB: Constructed by C++ through the FFI. +#[allow(dead_code)] +pub enum FfiDefaultNamespaceType { + Any, + None, + Named, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct FfiNamespaceContext { + pub default_namespace_type: FfiDefaultNamespaceType, + pub default_namespace: *const usize, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum FfiNodeKind { Element, @@ -1730,6 +1764,7 @@ struct FfiCallScope; struct FfiDom<'a> { context: *mut c_void, + namespace_context: FfiNamespaceContext, marker: PhantomData<&'a mut FfiCallScope>, // NB: Both flags are mirrored here so that the hot matching loops can skip the note_* // callbacks without crossing the FFI; the common case collects no metadata. @@ -1743,9 +1778,11 @@ impl<'a> FfiDom<'a> { _call_scope: &'a mut FfiCallScope, collects_selector_involvement_metadata: bool, inside_has_argument: bool, + namespace_context: FfiNamespaceContext, ) -> Self { Self { context, + namespace_context, marker: PhantomData, collects_selector_involvement_metadata, inside_has_argument, @@ -1757,10 +1794,14 @@ impl<'a> FfiDom<'a> { kind, element: FfiElement { pointer: std::ptr::null(), + local_name: std::ptr::null(), + namespace_: std::ptr::null(), id: std::ptr::null(), classes: std::ptr::null(), class_count: 0, class_names_are_case_insensitive: false, + namespace_is_null: true, + is_html_element_in_html_document: false, }, marker: PhantomData, }) @@ -1799,6 +1840,22 @@ impl<'a> SelectorDom for FfiDom<'a> { type Element = FfiNode<'a>; fn matches_universal_selector(&mut self, element: FfiNode<'a>, name: &QualifiedName) -> bool { + match name.namespace_type { + NamespaceType::Default => match self.namespace_context.default_namespace_type { + FfiDefaultNamespaceType::Any => return true, + FfiDefaultNamespaceType::None => return element.as_element().namespace_is_null, + FfiDefaultNamespaceType::Named => { + // SAFETY: C++ guarantees that the default namespace remains pinned for this + // matching call. + let default_namespace = unsafe { self.namespace_context.default_namespace.as_ref().copied() }; + return default_namespace + .is_some_and(|namespace| element.as_element().namespace() == Some(namespace)); + } + }, + NamespaceType::None => return element.as_element().namespace_is_null, + NamespaceType::Any => return true, + NamespaceType::Named => {} + } crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); // SAFETY: `FfiDom` guarantees that the context, element, and retained simple selector // remain valid for the duration of matching. @@ -1817,6 +1874,18 @@ impl<'a> SelectorDom for FfiDom<'a> { name: &QualifiedName, mode: TagNameMatchingMode, ) -> bool { + let ffi_element = element.as_element(); + if ffi_element.is_html_element_in_html_document || mode == TagNameMatchingMode::Fast { + let interned_name = if ffi_element.is_html_element_in_html_document { + name.interned_lowercase_name + } else { + name.interned_name + }; + if interned_name.is_none_or(|name| ffi_element.local_name() != Some(name)) { + return false; + } + return self.matches_universal_selector(element, name); + } crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); // SAFETY: `FfiDom` guarantees that the context, element, and retained simple selector // remain valid for the duration of matching. @@ -2188,6 +2257,11 @@ unsafe fn qualified_name_from_ffi(selector: &FfiSimpleSelector) -> QualifiedName name: unsafe { string_from_ffi(selector.name) }, // SAFETY: The caller guarantees that every string view in `selector` is valid. lowercase_name: unsafe { string_from_ffi(selector.lowercase_name) }, + // SAFETY: The caller guarantees that all retained C++ selector data is valid. + interned_name: unsafe { interned_name_from_ffi(selector) }, + // SAFETY: The caller guarantees that a non-null pointer identifies retained C++ selector + // data for the duration of selector compilation. + interned_lowercase_name: unsafe { selector.interned_lowercase_name.as_ref().copied() }, cxx_simple_selector: RetainedCxxPointer::new(selector.cxx_simple_selector), } } @@ -2340,10 +2414,8 @@ unsafe fn compiled_selector_from_ffi(selector: &FfiSelector) -> Rc( element: FfiElement, shadow_host: FfiElement, - context: *mut c_void, scope: *const c_void, - collects_selector_involvement_metadata: bool, - inside_has_argument: bool, + configuration: FfiDomConfiguration, callback: impl for<'a> FnOnce(FfiNode<'a>, Option>, Option>, &mut FfiDom<'a>) -> R, ) -> R { let mut call_scope = FfiCallScope; @@ -2351,10 +2423,11 @@ unsafe fn with_ffi_dom( // callback. Borrowing `call_scope` prevents the DOM wrapper and its nodes from escaping it. let mut dom = unsafe { FfiDom::new( - context, + configuration.context, &mut call_scope, - collects_selector_involvement_metadata, - inside_has_argument, + configuration.collects_selector_involvement_metadata, + configuration.inside_has_argument, + configuration.namespace_context, ) }; // SAFETY: The caller guarantees that `element` wraps a live DOM element. @@ -2366,6 +2439,14 @@ unsafe fn with_ffi_dom( callback(element, shadow_host, scope, &mut dom) } +#[derive(Clone, Copy)] +struct FfiDomConfiguration { + context: *mut c_void, + collects_selector_involvement_metadata: bool, + inside_has_argument: bool, + namespace_context: FfiNamespaceContext, +} + /// # Safety /// /// `selector`, every transitively referenced array, string, and `RustSelector` handle must be @@ -2426,6 +2507,7 @@ pub unsafe extern "C" fn rust_selector_matches( scope: *const c_void, collects_selector_involvement_metadata: bool, inside_has_argument: bool, + namespace_context: FfiNamespaceContext, ) -> bool { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMatchEntry); abort_on_panic(|| { @@ -2440,10 +2522,13 @@ pub unsafe extern "C" fn rust_selector_matches( with_ffi_dom( element, shadow_host, - context, scope, - collects_selector_involvement_metadata, - inside_has_argument, + FfiDomConfiguration { + context, + collects_selector_involvement_metadata, + inside_has_argument, + namespace_context, + }, |element, shadow_host, scope, dom| { let target = MatchTarget { element, @@ -2474,6 +2559,7 @@ pub unsafe extern "C" fn rust_selector_matches_originating_element( scope: *const c_void, collects_selector_involvement_metadata: bool, inside_has_argument: bool, + namespace_context: FfiNamespaceContext, ) -> bool { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMatchEntry); abort_on_panic(|| { @@ -2488,10 +2574,13 @@ pub unsafe extern "C" fn rust_selector_matches_originating_element( with_ffi_dom( element, shadow_host, - context, scope, - collects_selector_involvement_metadata, - inside_has_argument, + FfiDomConfiguration { + context, + collects_selector_involvement_metadata, + inside_has_argument, + namespace_context, + }, |element, shadow_host, scope, dom| { matches_originating_element_for_pseudo_element( selector, diff --git a/Libraries/LibWeb/CSS/SelectorMatching.cpp b/Libraries/LibWeb/CSS/SelectorMatching.cpp index 12991ea100b4b..28135a1f2d218 100644 --- a/Libraries/LibWeb/CSS/SelectorMatching.cpp +++ b/Libraries/LibWeb/CSS/SelectorMatching.cpp @@ -666,10 +666,37 @@ static CSS::SelectorFFI::Element element_to_ffi(DOM::Element const* element) auto const& classes = element->class_names(); return { .pointer = element, + .local_name = reinterpret_cast(&element->local_name()), + .namespace_ = element->namespace_uri().has_value() ? reinterpret_cast(&element->namespace_uri().value()) : nullptr, .id = element->id().has_value() ? reinterpret_cast(&element->id().value()) : nullptr, .classes = reinterpret_cast(classes.data()), .class_count = classes.size(), .class_names_are_case_insensitive = element->document().in_quirks_mode(), + .namespace_is_null = !element->namespace_uri().has_value() || element->namespace_uri()->is_empty(), + .is_html_element_in_html_document = element->namespace_uri() == Namespace::HTML + && element->document().document_type() == DOM::Document::Type::HTML, + }; +} + +static CSS::SelectorFFI::NamespaceContext namespace_context_to_ffi(MatchContext const& context) +{ + if (!context.style_sheet_for_rule || !context.style_sheet_for_rule->default_namespace_rule()) { + return { + .default_namespace_type = CSS::SelectorFFI::DefaultNamespaceType::Any, + .default_namespace = nullptr, + }; + } + + auto const& default_namespace = context.style_sheet_for_rule->default_namespace_rule()->namespace_uri(); + if (default_namespace.is_empty()) { + return { + .default_namespace_type = CSS::SelectorFFI::DefaultNamespaceType::None, + .default_namespace = nullptr, + }; + } + return { + .default_namespace_type = CSS::SelectorFFI::DefaultNamespaceType::Named, + .default_namespace = reinterpret_cast(&default_namespace), }; } @@ -684,7 +711,8 @@ bool matches(CSS::Selector const& selector, DOM::AbstractElement const& target, &context, scope.ptr(), context.collect_per_element_selector_involvement_metadata, - context.inside_has_argument_match); + context.inside_has_argument_match, + namespace_context_to_ffi(context)); } bool matches_originating_element_for_pseudo_element(CSS::Selector const& selector, CSS::PseudoElement pseudo_element, DOM::AbstractElement const& target, GC::Ptr shadow_host, MatchContext& context, GC::Ptr scope) @@ -699,7 +727,8 @@ bool matches_originating_element_for_pseudo_element(CSS::Selector const& selecto &context, scope.ptr(), context.collect_per_element_selector_involvement_metadata, - context.inside_has_argument_match); + context.inside_has_argument_match, + namespace_context_to_ffi(context)); } static MatchContext& rust_match_context(void* context) diff --git a/Libraries/LibWeb/CSS/SelectorRustBridge.cpp b/Libraries/LibWeb/CSS/SelectorRustBridge.cpp index 9e466e95a7d1a..75bafc8db4194 100644 --- a/Libraries/LibWeb/CSS/SelectorRustBridge.cpp +++ b/Libraries/LibWeb/CSS/SelectorRustBridge.cpp @@ -152,6 +152,8 @@ class SelectorCompiler { output.namespace_ = store_string(qualified_name.namespace_); output.name = store_string(qualified_name.name.name); output.lowercase_name = store_string(qualified_name.name.lowercase_name); + output.interned_name = reinterpret_cast(&qualified_name.name.name); + output.interned_lowercase_name = reinterpret_cast(&qualified_name.name.lowercase_name); } SelectorFFI::SimpleSelector compile_simple_selector(Selector::SimpleSelector const& simple_selector) From 38d943e946df3962e4d300aa7ceec013b8da5f23 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 22:50:26 +0200 Subject: [PATCH 03/27] LibWeb: Read root selector state from the live DOM wrapper 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. --- Libraries/LibWeb/CSS/Rust/src/selector_engine.rs | 7 +++---- Libraries/LibWeb/CSS/SelectorMatching.cpp | 7 +------ .../LibWeb/Text/expected/css/live-selector-identifiers.txt | 2 ++ Tests/LibWeb/Text/input/css/live-selector-identifiers.html | 4 ++++ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index 6af1c6143951d..3c7340215edcd 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -1582,6 +1582,7 @@ pub struct FfiElement { pub class_names_are_case_insensitive: bool, pub namespace_is_null: bool, pub is_html_element_in_html_document: bool, + pub is_document_root: bool, } impl FfiElement { @@ -1719,7 +1720,6 @@ unsafe extern "C" { fn selector_ffi_next_element_descendant(element: *const c_void, root: *const c_void) -> FfiElement; fn selector_ffi_has_no_element_or_nonempty_text_children(element: *const c_void) -> bool; fn selector_ffi_has_same_type(first: *const c_void, second: *const c_void) -> bool; - fn selector_ffi_is_document_root(element: *const c_void) -> bool; fn selector_ffi_is_shadow_tree_slot(element: *const c_void) -> bool; fn selector_ffi_slotted_parent(context: *mut c_void, element: *const c_void) -> FfiElementAndShadowHost; @@ -1802,6 +1802,7 @@ impl<'a> FfiDom<'a> { class_names_are_case_insensitive: false, namespace_is_null: true, is_html_element_in_html_document: false, + is_document_root: false, }, marker: PhantomData, }) @@ -2059,9 +2060,7 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn is_document_root(&mut self, element: FfiNode<'a>) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); - // SAFETY: `FfiDom` guarantees that the element remains valid for matching. - unsafe { selector_ffi_is_document_root(element.as_element_pointer()) } + element.as_element().is_document_root } fn is_shadow_tree_slot(&mut self, element: FfiNode<'a>) -> bool { diff --git a/Libraries/LibWeb/CSS/SelectorMatching.cpp b/Libraries/LibWeb/CSS/SelectorMatching.cpp index 28135a1f2d218..cb8bd01a9c803 100644 --- a/Libraries/LibWeb/CSS/SelectorMatching.cpp +++ b/Libraries/LibWeb/CSS/SelectorMatching.cpp @@ -675,6 +675,7 @@ static CSS::SelectorFFI::Element element_to_ffi(DOM::Element const* element) .namespace_is_null = !element->namespace_uri().has_value() || element->namespace_uri()->is_empty(), .is_html_element_in_html_document = element->namespace_uri() == Namespace::HTML && element->document().document_type() == DOM::Document::Type::HTML, + .is_document_root = is(*element), }; } @@ -833,7 +834,6 @@ DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_first_element_descendant); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_next_element_descendant); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_has_no_element_or_nonempty_text_children); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_has_same_type); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_is_document_root); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_is_shadow_tree_slot); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_slotted_parent); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_part_parent); @@ -1162,11 +1162,6 @@ extern "C" bool selector_ffi_has_same_type(void const* first, void const* second && first_element.namespace_uri() == second_element.namespace_uri(); } -extern "C" bool selector_ffi_is_document_root(void const* element) -{ - return is(ffi_element(element)); -} - extern "C" bool selector_ffi_is_shadow_tree_slot(void const* element) { auto const* slot = as_if(ffi_element(element)); diff --git a/Tests/LibWeb/Text/expected/css/live-selector-identifiers.txt b/Tests/LibWeb/Text/expected/css/live-selector-identifiers.txt index 60f5cde935745..864c42a1d4efe 100644 --- a/Tests/LibWeb/Text/expected/css/live-selector-identifiers.txt +++ b/Tests/LibWeb/Text/expected/css/live-selector-identifiers.txt @@ -6,3 +6,5 @@ mutated color: rgb(4, 5, 6) disconnected match: true shadow initial match: true shadow mutated match: true +document root: true +disconnected html root: true diff --git a/Tests/LibWeb/Text/input/css/live-selector-identifiers.html b/Tests/LibWeb/Text/input/css/live-selector-identifiers.html index ca17872b8b91f..51a90710a85c0 100644 --- a/Tests/LibWeb/Text/input/css/live-selector-identifiers.html +++ b/Tests/LibWeb/Text/input/css/live-selector-identifiers.html @@ -43,5 +43,9 @@ shadow_target.id = "shadow-new"; shadow_target.className = "shadow-new-class"; println(`shadow mutated match: ${shadow_target.matches("#shadow-new.shadow-new-class")}`); + + const disconnected_html = document.createElement("html"); + println(`document root: ${document.documentElement.matches(":root")}`); + println(`disconnected html root: ${disconnected_html.matches(":root")}`); }); From 0c75a934683d2083bfb683e6cd0fb7cc4ceb1874 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 22:54:06 +0200 Subject: [PATCH 04/27] LibWeb: Compare selector element types through live wrappers 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. --- Libraries/LibWeb/CSS/Rust/src/selector_engine.rs | 7 +++---- Libraries/LibWeb/CSS/SelectorMatching.cpp | 9 --------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index 3c7340215edcd..2d89579f55064 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -1719,7 +1719,6 @@ unsafe extern "C" { fn selector_ffi_first_element_descendant(element: *const c_void) -> FfiElement; fn selector_ffi_next_element_descendant(element: *const c_void, root: *const c_void) -> FfiElement; fn selector_ffi_has_no_element_or_nonempty_text_children(element: *const c_void) -> bool; - fn selector_ffi_has_same_type(first: *const c_void, second: *const c_void) -> bool; fn selector_ffi_is_shadow_tree_slot(element: *const c_void) -> bool; fn selector_ffi_slotted_parent(context: *mut c_void, element: *const c_void) -> FfiElementAndShadowHost; @@ -2054,9 +2053,9 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn has_same_type(&mut self, first: FfiNode<'a>, second: FfiNode<'a>) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); - // SAFETY: `FfiDom` guarantees that both elements remain valid for matching. - unsafe { selector_ffi_has_same_type(first.as_element_pointer(), second.as_element_pointer()) } + let first = first.as_element(); + let second = second.as_element(); + first.local_name() == second.local_name() && first.namespace() == second.namespace() } fn is_document_root(&mut self, element: FfiNode<'a>) -> bool { diff --git a/Libraries/LibWeb/CSS/SelectorMatching.cpp b/Libraries/LibWeb/CSS/SelectorMatching.cpp index cb8bd01a9c803..1680da66d3fd5 100644 --- a/Libraries/LibWeb/CSS/SelectorMatching.cpp +++ b/Libraries/LibWeb/CSS/SelectorMatching.cpp @@ -833,7 +833,6 @@ DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_first_element_child); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_first_element_descendant); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_next_element_descendant); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_has_no_element_or_nonempty_text_children); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_has_same_type); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_is_shadow_tree_slot); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_slotted_parent); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_part_parent); @@ -1154,14 +1153,6 @@ extern "C" bool selector_ffi_has_no_element_or_nonempty_text_children(void const return !has_nonempty_text_child; } -extern "C" bool selector_ffi_has_same_type(void const* first, void const* second) -{ - auto const& first_element = ffi_element(first); - auto const& second_element = ffi_element(second); - return first_element.local_name() == second_element.local_name() - && first_element.namespace_uri() == second_element.namespace_uri(); -} - extern "C" bool selector_ffi_is_shadow_tree_slot(void const* element) { auto const* slot = as_if(ffi_element(element)); From 8cb51e75513a946167754e63b85ecaabdb1c792a Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 23:01:47 +0200 Subject: [PATCH 05/27] LibWeb: Read selector element facts through live accessors 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. --- Libraries/LibWeb/CSS/Rust/build.rs | 2 + Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs | 1 + .../LibWeb/CSS/Rust/src/selector_engine.rs | 128 ++++++++++++------ Libraries/LibWeb/CSS/SelectorMatching.cpp | 74 +++++++--- 4 files changed, 146 insertions(+), 59 deletions(-) diff --git a/Libraries/LibWeb/CSS/Rust/build.rs b/Libraries/LibWeb/CSS/Rust/build.rs index 245659627b682..9dee60f41d434 100644 --- a/Libraries/LibWeb/CSS/Rust/build.rs +++ b/Libraries/LibWeb/CSS/Rust/build.rs @@ -563,6 +563,8 @@ fn main() -> Result<(), Box> { ("FfiCompoundSelector", "CompoundSelector"), ("FfiSelector", "Selector"), ("FfiElement", "Element"), + ("FfiElementQualifiedName", "ElementQualifiedName"), + ("FfiInternedStringList", "InternedStringList"), ("FfiDefaultNamespaceType", "DefaultNamespaceType"), ("FfiNamespaceContext", "NamespaceContext"), ("FfiElementAndShadowHost", "ElementAndShadowHost"), diff --git a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs index 269cfb1a109fd..ff031590b8c32 100644 --- a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs +++ b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs @@ -53,6 +53,7 @@ define_ffi_ops! { StyleGroupCloneEntry => "styleGroupCloneEntries", StyleGroupFreeEntry => "styleGroupFreeEntries", // Callbacks: Rust -> C++. + SelectorDomReadCallback => "selectorDomReadCallbacks", SelectorSimpleSelectorCallback => "selectorSimpleSelectorCallbacks", SelectorTreeNavigationCallback => "selectorTreeNavigationCallbacks", SelectorMetadataCallback => "selectorMetadataCallbacks", diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index 2d89579f55064..cd9409aa6f82b 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -1568,21 +1568,12 @@ pub struct RustSelector { #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(C)] -/// A borrowed view of the element's current selector-relevant identifier data. +/// A DOM element borrowed for one selector-matching call. /// -/// C++ constructs this immediately before crossing the FFI and after every tree navigation. The -/// pointers must not be retained beyond the matching call. +/// This handle contains no DOM-derived state. All facts are read from C++ on demand so mutations +/// observed by later matching calls cannot be hidden by retained data. pub struct FfiElement { pub pointer: *const c_void, - pub local_name: *const usize, - pub namespace_: *const usize, - pub id: *const usize, - pub classes: *const usize, - pub class_count: usize, - pub class_names_are_case_insensitive: bool, - pub namespace_is_null: bool, - pub is_html_element_in_html_document: bool, - pub is_document_root: bool, } impl FfiElement { @@ -1590,33 +1581,84 @@ impl FfiElement { self.pointer.is_null() } + fn qualified_name(self) -> FfiElementQualifiedName { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_qualified_name(self.pointer) } + } + fn id(self) -> Option { - // SAFETY: The C++ wrapper points at the live element's `Utf16FlyString` storage, which is - // pinned for the duration of the matching call. - unsafe { self.id.as_ref().copied() } + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element. A non-null result points at its + // current pinned `Utf16FlyString` storage for this matching call. + unsafe { selector_ffi_element_id(self.pointer).as_ref().copied() } } - fn local_name(self) -> Option { - // SAFETY: The C++ wrapper points at the live element's pinned `Utf16FlyString` storage. - unsafe { self.local_name.as_ref().copied() } + fn class_names_are_case_insensitive(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_class_names_are_case_insensitive(self.pointer) } } - fn namespace(self) -> Option { - // SAFETY: The C++ wrapper points at the live element's pinned `Utf16FlyString` storage. - unsafe { self.namespace_.as_ref().copied() } + fn namespace_is_null(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_namespace_is_null(self.pointer) } + } + + fn is_html_element_in_html_document(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_html_element_in_html_document(self.pointer) } + } + + fn is_document_root(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_document_root(self.pointer) } } unsafe fn classes<'a>(self) -> &'a [usize] { - if self.class_count == 0 { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element. C++ returns its current class storage, + // borrowed for this matching call. + let classes = unsafe { selector_ffi_element_classes(self.pointer) }; + if classes.count == 0 { return &[]; } - assert!(!self.classes.is_null()); + assert!(!classes.data.is_null()); // SAFETY: C++ guarantees that `Utf16FlyString` has the size and alignment of `usize` and // that the element's class vector cannot mutate during this matching call. - unsafe { std::slice::from_raw_parts(self.classes, self.class_count) } + unsafe { std::slice::from_raw_parts(classes.data, classes.count) } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct FfiElementQualifiedName { + pub local_name: *const usize, + pub namespace_: *const usize, +} + +impl FfiElementQualifiedName { + fn local_name(self) -> Option { + // SAFETY: C++ points at the live element's pinned `Utf16FlyString` storage. + unsafe { self.local_name.as_ref().copied() } + } + + fn namespace(self) -> Option { + // SAFETY: C++ points at the live element's pinned `Utf16FlyString` storage. + unsafe { self.namespace_.as_ref().copied() } } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C)] +pub struct FfiInternedStringList { + pub data: *const usize, + pub count: usize, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] // NB: Constructed by C++ through the FFI. @@ -1688,6 +1730,14 @@ pub struct FfiElementAndShadowHost { } unsafe extern "C" { + fn selector_ffi_element_qualified_name(element: *const c_void) -> FfiElementQualifiedName; + fn selector_ffi_element_id(element: *const c_void) -> *const usize; + fn selector_ffi_element_classes(element: *const c_void) -> FfiInternedStringList; + fn selector_ffi_element_class_names_are_case_insensitive(element: *const c_void) -> bool; + fn selector_ffi_element_namespace_is_null(element: *const c_void) -> bool; + fn selector_ffi_element_is_html_element_in_html_document(element: *const c_void) -> bool; + fn selector_ffi_element_is_document_root(element: *const c_void) -> bool; + fn selector_ffi_matches_universal( context: *mut c_void, element: *const c_void, @@ -1793,15 +1843,6 @@ impl<'a> FfiDom<'a> { kind, element: FfiElement { pointer: std::ptr::null(), - local_name: std::ptr::null(), - namespace_: std::ptr::null(), - id: std::ptr::null(), - classes: std::ptr::null(), - class_count: 0, - class_names_are_case_insensitive: false, - namespace_is_null: true, - is_html_element_in_html_document: false, - is_document_root: false, }, marker: PhantomData, }) @@ -1843,16 +1884,16 @@ impl<'a> SelectorDom for FfiDom<'a> { match name.namespace_type { NamespaceType::Default => match self.namespace_context.default_namespace_type { FfiDefaultNamespaceType::Any => return true, - FfiDefaultNamespaceType::None => return element.as_element().namespace_is_null, + FfiDefaultNamespaceType::None => return element.as_element().namespace_is_null(), FfiDefaultNamespaceType::Named => { // SAFETY: C++ guarantees that the default namespace remains pinned for this // matching call. let default_namespace = unsafe { self.namespace_context.default_namespace.as_ref().copied() }; return default_namespace - .is_some_and(|namespace| element.as_element().namespace() == Some(namespace)); + .is_some_and(|namespace| element.as_element().qualified_name().namespace() == Some(namespace)); } }, - NamespaceType::None => return element.as_element().namespace_is_null, + NamespaceType::None => return element.as_element().namespace_is_null(), NamespaceType::Any => return true, NamespaceType::Named => {} } @@ -1875,13 +1916,14 @@ impl<'a> SelectorDom for FfiDom<'a> { mode: TagNameMatchingMode, ) -> bool { let ffi_element = element.as_element(); - if ffi_element.is_html_element_in_html_document || mode == TagNameMatchingMode::Fast { - let interned_name = if ffi_element.is_html_element_in_html_document { + let is_html_element_in_html_document = ffi_element.is_html_element_in_html_document(); + if is_html_element_in_html_document || mode == TagNameMatchingMode::Fast { + let interned_name = if is_html_element_in_html_document { name.interned_lowercase_name } else { name.interned_name }; - if interned_name.is_none_or(|name| ffi_element.local_name() != Some(name)) { + if interned_name.is_none_or(|name| ffi_element.qualified_name().local_name() != Some(name)) { return false; } return self.matches_universal_selector(element, name); @@ -1905,7 +1947,7 @@ impl<'a> SelectorDom for FfiDom<'a> { fn matches_class_selector(&mut self, element: FfiNode<'a>, class_name: &NameSelector) -> bool { let ffi_element = element.as_element(); - if ffi_element.class_names_are_case_insensitive { + if ffi_element.class_names_are_case_insensitive() { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); // SAFETY: `FfiDom` guarantees that the element and retained simple selector remain // valid for the duration of matching. @@ -2053,13 +2095,13 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn has_same_type(&mut self, first: FfiNode<'a>, second: FfiNode<'a>) -> bool { - let first = first.as_element(); - let second = second.as_element(); + let first = first.as_element().qualified_name(); + let second = second.as_element().qualified_name(); first.local_name() == second.local_name() && first.namespace() == second.namespace() } fn is_document_root(&mut self, element: FfiNode<'a>) -> bool { - element.as_element().is_document_root + element.as_element().is_document_root() } fn is_shadow_tree_slot(&mut self, element: FfiNode<'a>) -> bool { diff --git a/Libraries/LibWeb/CSS/SelectorMatching.cpp b/Libraries/LibWeb/CSS/SelectorMatching.cpp index 1680da66d3fd5..5fb65d2f64eb1 100644 --- a/Libraries/LibWeb/CSS/SelectorMatching.cpp +++ b/Libraries/LibWeb/CSS/SelectorMatching.cpp @@ -658,24 +658,8 @@ static CSS::SelectorFFI::Element element_to_ffi(DOM::Element const* element) if (!element) return {}; - // `Utf16FlyString` is its interned one-word representation. Rust borrows these words directly - // while matching; the DOM and selector trees pin their respective strings for the call. - static_assert(sizeof(Utf16FlyString) == sizeof(uintptr_t)); - static_assert(alignof(Utf16FlyString) == alignof(uintptr_t)); - - auto const& classes = element->class_names(); return { .pointer = element, - .local_name = reinterpret_cast(&element->local_name()), - .namespace_ = element->namespace_uri().has_value() ? reinterpret_cast(&element->namespace_uri().value()) : nullptr, - .id = element->id().has_value() ? reinterpret_cast(&element->id().value()) : nullptr, - .classes = reinterpret_cast(classes.data()), - .class_count = classes.size(), - .class_names_are_case_insensitive = element->document().in_quirks_mode(), - .namespace_is_null = !element->namespace_uri().has_value() || element->namespace_uri()->is_empty(), - .is_html_element_in_html_document = element->namespace_uri() == Namespace::HTML - && element->document().document_type() == DOM::Document::Type::HTML, - .is_document_root = is(*element), }; } @@ -816,6 +800,13 @@ using CSS::SelectorFFI::TagNameMatchingMode; #define DECLARE_SELECTOR_FFI_CALLBACK(function) \ extern "C" decltype(CSS::SelectorFFI::function) function +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_qualified_name); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_id); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_classes); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_class_names_are_case_insensitive); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_namespace_is_null); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_html_element_in_html_document); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_document_root); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_universal); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_tag_name); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_class_quirks); @@ -849,6 +840,57 @@ DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_should_reject_has_argument); #undef DECLARE_SELECTOR_FFI_CALLBACK +// `Utf16FlyString` is its interned one-word representation. The accessors below expose pointers +// to live words only for the duration of the synchronous selector-matching call. +static_assert(sizeof(Utf16FlyString) == sizeof(uintptr_t)); +static_assert(alignof(Utf16FlyString) == alignof(uintptr_t)); + +extern "C" CSS::SelectorFFI::ElementQualifiedName selector_ffi_element_qualified_name(void const* element) +{ + auto const& target = ffi_element(element); + return { + .local_name = reinterpret_cast(&target.local_name()), + .namespace_ = target.namespace_uri().has_value() ? reinterpret_cast(&target.namespace_uri().value()) : nullptr, + }; +} + +extern "C" uintptr_t const* selector_ffi_element_id(void const* element) +{ + auto const& id = ffi_element(element).id(); + return id.has_value() ? reinterpret_cast(&id.value()) : nullptr; +} + +extern "C" CSS::SelectorFFI::InternedStringList selector_ffi_element_classes(void const* element) +{ + auto const& classes = ffi_element(element).class_names(); + return { + .data = reinterpret_cast(classes.data()), + .count = classes.size(), + }; +} + +extern "C" bool selector_ffi_element_class_names_are_case_insensitive(void const* element) +{ + return ffi_element(element).document().in_quirks_mode(); +} + +extern "C" bool selector_ffi_element_namespace_is_null(void const* element) +{ + return is_in_null_namespace(ffi_element(element)); +} + +extern "C" bool selector_ffi_element_is_html_element_in_html_document(void const* element) +{ + auto const& target = ffi_element(element); + return target.namespace_uri() == Namespace::HTML + && target.document().document_type() == DOM::Document::Type::HTML; +} + +extern "C" bool selector_ffi_element_is_document_root(void const* element) +{ + return is(ffi_element(element)); +} + extern "C" bool selector_ffi_matches_universal(void* context, void const* element, void const* cxx_simple_selector) { auto& match_context = rust_match_context(context); From e99c1dcc91899de128240f8da71db9d5cf65f86f Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 23:06:49 +0200 Subject: [PATCH 06/27] LibWeb: Resolve selector namespaces through live accessors 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. --- Libraries/LibWeb/CSS/Rust/build.rs | 4 +- .../LibWeb/CSS/Rust/src/selector_engine.rs | 93 ++++++++++--------- Libraries/LibWeb/CSS/SelectorMatching.cpp | 89 ++++++++++++------ 3 files changed, 110 insertions(+), 76 deletions(-) diff --git a/Libraries/LibWeb/CSS/Rust/build.rs b/Libraries/LibWeb/CSS/Rust/build.rs index 9dee60f41d434..0a71b2262b1c5 100644 --- a/Libraries/LibWeb/CSS/Rust/build.rs +++ b/Libraries/LibWeb/CSS/Rust/build.rs @@ -565,8 +565,8 @@ fn main() -> Result<(), Box> { ("FfiElement", "Element"), ("FfiElementQualifiedName", "ElementQualifiedName"), ("FfiInternedStringList", "InternedStringList"), - ("FfiDefaultNamespaceType", "DefaultNamespaceType"), - ("FfiNamespaceContext", "NamespaceContext"), + ("FfiResolvedNamespaceType", "ResolvedNamespaceType"), + ("FfiResolvedNamespace", "ResolvedNamespace"), ("FfiElementAndShadowHost", "ElementAndShadowHost"), ] { selector_config diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index cd9409aa6f82b..1e95af00243bc 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -1663,17 +1663,23 @@ pub struct FfiInternedStringList { #[repr(u8)] // NB: Constructed by C++ through the FFI. #[allow(dead_code)] -pub enum FfiDefaultNamespaceType { - Any, - None, +pub enum FfiResolvedNamespaceType { + Missing, + Null, Named, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(C)] -pub struct FfiNamespaceContext { - pub default_namespace_type: FfiDefaultNamespaceType, - pub default_namespace: *const usize, +pub struct FfiResolvedNamespace { + pub namespace_type: FfiResolvedNamespaceType, + pub namespace_: usize, +} + +impl FfiResolvedNamespace { + fn namespace(self) -> Option { + (self.namespace_type == FfiResolvedNamespaceType::Named).then_some(self.namespace_) + } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -1738,11 +1744,9 @@ unsafe extern "C" { fn selector_ffi_element_is_html_element_in_html_document(element: *const c_void) -> bool; fn selector_ffi_element_is_document_root(element: *const c_void) -> bool; - fn selector_ffi_matches_universal( - context: *mut c_void, - element: *const c_void, - cxx_simple_selector: *const c_void, - ) -> bool; + fn selector_ffi_default_namespace(context: *mut c_void) -> FfiResolvedNamespace; + fn selector_ffi_resolve_namespace(context: *mut c_void, prefix: FfiStringView) -> FfiResolvedNamespace; + fn selector_ffi_matches_tag_name( context: *mut c_void, element: *const c_void, @@ -1813,7 +1817,6 @@ struct FfiCallScope; struct FfiDom<'a> { context: *mut c_void, - namespace_context: FfiNamespaceContext, marker: PhantomData<&'a mut FfiCallScope>, // NB: Both flags are mirrored here so that the hot matching loops can skip the note_* // callbacks without crossing the FFI; the common case collects no metadata. @@ -1827,11 +1830,9 @@ impl<'a> FfiDom<'a> { _call_scope: &'a mut FfiCallScope, collects_selector_involvement_metadata: bool, inside_has_argument: bool, - namespace_context: FfiNamespaceContext, ) -> Self { Self { context, - namespace_context, marker: PhantomData, collects_selector_involvement_metadata, inside_has_argument, @@ -1875,6 +1876,29 @@ impl<'a> FfiDom<'a> { self.element(value.shadow_host) })) } + + fn default_namespace(&self) -> FfiResolvedNamespace { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The matching context remains live for this matching call. + unsafe { selector_ffi_default_namespace(self.context) } + } + + fn resolve_namespace(&self, prefix: &[u16]) -> FfiResolvedNamespace { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The matching context remains live, and the prefix view is borrowed for this + // accessor call only. + unsafe { selector_ffi_resolve_namespace(self.context, ffi_string_view(prefix)) } + } + + fn matches_resolved_namespace(&self, element: FfiNode<'a>, namespace: FfiResolvedNamespace) -> bool { + match namespace.namespace_type { + FfiResolvedNamespaceType::Missing => false, + FfiResolvedNamespaceType::Null => element.as_element().namespace_is_null(), + FfiResolvedNamespaceType::Named => namespace + .namespace() + .is_some_and(|namespace| element.as_element().qualified_name().namespace() == Some(namespace)), + } + } } impl<'a> SelectorDom for FfiDom<'a> { @@ -1882,30 +1906,17 @@ impl<'a> SelectorDom for FfiDom<'a> { fn matches_universal_selector(&mut self, element: FfiNode<'a>, name: &QualifiedName) -> bool { match name.namespace_type { - NamespaceType::Default => match self.namespace_context.default_namespace_type { - FfiDefaultNamespaceType::Any => return true, - FfiDefaultNamespaceType::None => return element.as_element().namespace_is_null(), - FfiDefaultNamespaceType::Named => { - // SAFETY: C++ guarantees that the default namespace remains pinned for this - // matching call. - let default_namespace = unsafe { self.namespace_context.default_namespace.as_ref().copied() }; - return default_namespace - .is_some_and(|namespace| element.as_element().qualified_name().namespace() == Some(namespace)); - } - }, - NamespaceType::None => return element.as_element().namespace_is_null(), - NamespaceType::Any => return true, - NamespaceType::Named => {} - } - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the context, element, and retained simple selector - // remain valid for the duration of matching. - unsafe { - selector_ffi_matches_universal( - self.context, - element.as_element_pointer(), - name.cxx_simple_selector.as_ptr(), - ) + NamespaceType::Default => { + let namespace = self.default_namespace(); + namespace.namespace_type == FfiResolvedNamespaceType::Missing + || self.matches_resolved_namespace(element, namespace) + } + NamespaceType::None => element.as_element().namespace_is_null(), + NamespaceType::Any => true, + NamespaceType::Named => { + let namespace = self.resolve_namespace(&name.namespace); + self.matches_resolved_namespace(element, namespace) + } } } @@ -2467,7 +2478,6 @@ unsafe fn with_ffi_dom( &mut call_scope, configuration.collects_selector_involvement_metadata, configuration.inside_has_argument, - configuration.namespace_context, ) }; // SAFETY: The caller guarantees that `element` wraps a live DOM element. @@ -2484,7 +2494,6 @@ struct FfiDomConfiguration { context: *mut c_void, collects_selector_involvement_metadata: bool, inside_has_argument: bool, - namespace_context: FfiNamespaceContext, } /// # Safety @@ -2547,7 +2556,6 @@ pub unsafe extern "C" fn rust_selector_matches( scope: *const c_void, collects_selector_involvement_metadata: bool, inside_has_argument: bool, - namespace_context: FfiNamespaceContext, ) -> bool { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMatchEntry); abort_on_panic(|| { @@ -2567,7 +2575,6 @@ pub unsafe extern "C" fn rust_selector_matches( context, collects_selector_involvement_metadata, inside_has_argument, - namespace_context, }, |element, shadow_host, scope, dom| { let target = MatchTarget { @@ -2599,7 +2606,6 @@ pub unsafe extern "C" fn rust_selector_matches_originating_element( scope: *const c_void, collects_selector_involvement_metadata: bool, inside_has_argument: bool, - namespace_context: FfiNamespaceContext, ) -> bool { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMatchEntry); abort_on_panic(|| { @@ -2619,7 +2625,6 @@ pub unsafe extern "C" fn rust_selector_matches_originating_element( context, collects_selector_involvement_metadata, inside_has_argument, - namespace_context, }, |element, shadow_host, scope, dom| { matches_originating_element_for_pseudo_element( diff --git a/Libraries/LibWeb/CSS/SelectorMatching.cpp b/Libraries/LibWeb/CSS/SelectorMatching.cpp index 5fb65d2f64eb1..9e69c6fa162bf 100644 --- a/Libraries/LibWeb/CSS/SelectorMatching.cpp +++ b/Libraries/LibWeb/CSS/SelectorMatching.cpp @@ -663,28 +663,6 @@ static CSS::SelectorFFI::Element element_to_ffi(DOM::Element const* element) }; } -static CSS::SelectorFFI::NamespaceContext namespace_context_to_ffi(MatchContext const& context) -{ - if (!context.style_sheet_for_rule || !context.style_sheet_for_rule->default_namespace_rule()) { - return { - .default_namespace_type = CSS::SelectorFFI::DefaultNamespaceType::Any, - .default_namespace = nullptr, - }; - } - - auto const& default_namespace = context.style_sheet_for_rule->default_namespace_rule()->namespace_uri(); - if (default_namespace.is_empty()) { - return { - .default_namespace_type = CSS::SelectorFFI::DefaultNamespaceType::None, - .default_namespace = nullptr, - }; - } - return { - .default_namespace_type = CSS::SelectorFFI::DefaultNamespaceType::Named, - .default_namespace = reinterpret_cast(&default_namespace), - }; -} - bool matches(CSS::Selector const& selector, DOM::AbstractElement const& target, GC::Ptr shadow_host, MatchContext& context, GC::Ptr scope) { @@ -696,8 +674,7 @@ bool matches(CSS::Selector const& selector, DOM::AbstractElement const& target, &context, scope.ptr(), context.collect_per_element_selector_involvement_metadata, - context.inside_has_argument_match, - namespace_context_to_ffi(context)); + context.inside_has_argument_match); } bool matches_originating_element_for_pseudo_element(CSS::Selector const& selector, CSS::PseudoElement pseudo_element, DOM::AbstractElement const& target, GC::Ptr shadow_host, MatchContext& context, GC::Ptr scope) @@ -712,8 +689,7 @@ bool matches_originating_element_for_pseudo_element(CSS::Selector const& selecto &context, scope.ptr(), context.collect_per_element_selector_involvement_metadata, - context.inside_has_argument_match, - namespace_context_to_ffi(context)); + context.inside_has_argument_match); } static MatchContext& rust_match_context(void* context) @@ -807,7 +783,8 @@ DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_class_names_are_case_insensit DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_namespace_is_null); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_html_element_in_html_document); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_document_root); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_universal); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_default_namespace); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_resolve_namespace); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_tag_name); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_class_quirks); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_attribute); @@ -845,6 +822,13 @@ DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_should_reject_has_argument); static_assert(sizeof(Utf16FlyString) == sizeof(uintptr_t)); static_assert(alignof(Utf16FlyString) == alignof(uintptr_t)); +static uintptr_t interned_string_identity(Utf16FlyString const& string) +{ + uintptr_t identity; + __builtin_memcpy(&identity, &string, sizeof(identity)); + return identity; +} + extern "C" CSS::SelectorFFI::ElementQualifiedName selector_ffi_element_qualified_name(void const* element) { auto const& target = ffi_element(element); @@ -891,11 +875,56 @@ extern "C" bool selector_ffi_element_is_document_root(void const* element) return is(ffi_element(element)); } -extern "C" bool selector_ffi_matches_universal(void* context, void const* element, void const* cxx_simple_selector) +extern "C" CSS::SelectorFFI::ResolvedNamespace selector_ffi_default_namespace(void* context) { auto& match_context = rust_match_context(context); - auto const& qualified_name = ffi_simple_selector(cxx_simple_selector).qualified_name(); - return matches_namespace(qualified_name, ffi_element(element), match_context.style_sheet_for_rule); + if (!match_context.style_sheet_for_rule || !match_context.style_sheet_for_rule->default_namespace_rule()) { + return { + .namespace_type = CSS::SelectorFFI::ResolvedNamespaceType::Missing, + .namespace_ = 0, + }; + } + + auto const& namespace_ = match_context.style_sheet_for_rule->default_namespace_rule()->namespace_uri(); + if (namespace_.is_empty()) { + return { + .namespace_type = CSS::SelectorFFI::ResolvedNamespaceType::Null, + .namespace_ = 0, + }; + } + return { + .namespace_type = CSS::SelectorFFI::ResolvedNamespaceType::Named, + .namespace_ = interned_string_identity(namespace_), + }; +} + +extern "C" CSS::SelectorFFI::ResolvedNamespace selector_ffi_resolve_namespace(void* context, StringView prefix) +{ + auto& match_context = rust_match_context(context); + if (!match_context.style_sheet_for_rule) { + return { + .namespace_type = CSS::SelectorFFI::ResolvedNamespaceType::Missing, + .namespace_ = 0, + }; + } + + auto namespace_ = match_context.style_sheet_for_rule->namespace_uri(ffi_string_view(prefix)); + if (!namespace_.has_value()) { + return { + .namespace_type = CSS::SelectorFFI::ResolvedNamespaceType::Missing, + .namespace_ = 0, + }; + } + if (namespace_->is_empty()) { + return { + .namespace_type = CSS::SelectorFFI::ResolvedNamespaceType::Null, + .namespace_ = 0, + }; + } + return { + .namespace_type = CSS::SelectorFFI::ResolvedNamespaceType::Named, + .namespace_ = interned_string_identity(*namespace_), + }; } extern "C" bool selector_ffi_matches_tag_name(void* context, void const* element, void const* cxx_simple_selector, TagNameMatchingMode matching_mode) From c575bb9bbbe0fde498777be86d57d579b77b856d Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 23:13:46 +0200 Subject: [PATCH 07/27] LibWeb: Match tag names through live DOM string views 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. --- Libraries/LibWeb/CSS/Rust/build.rs | 1 + .../LibWeb/CSS/Rust/src/selector_engine.rs | 95 ++++++++++++++----- Libraries/LibWeb/CSS/SelectorMatching.cpp | 79 ++++----------- .../css/selector-engine-characterization.txt | 1 + .../css/selector-engine-characterization.html | 1 + 5 files changed, 96 insertions(+), 81 deletions(-) diff --git a/Libraries/LibWeb/CSS/Rust/build.rs b/Libraries/LibWeb/CSS/Rust/build.rs index 0a71b2262b1c5..932ff6e5f92a7 100644 --- a/Libraries/LibWeb/CSS/Rust/build.rs +++ b/Libraries/LibWeb/CSS/Rust/build.rs @@ -565,6 +565,7 @@ fn main() -> Result<(), Box> { ("FfiElement", "Element"), ("FfiElementQualifiedName", "ElementQualifiedName"), ("FfiInternedStringList", "InternedStringList"), + ("FfiDomStringView", "DomStringView"), ("FfiResolvedNamespaceType", "ResolvedNamespaceType"), ("FfiResolvedNamespace", "ResolvedNamespace"), ("FfiElementAndShadowHost", "ElementAndShadowHost"), diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index 1e95af00243bc..cd41eda202cc1 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -1618,6 +1618,17 @@ impl FfiElement { unsafe { selector_ffi_element_is_document_root(self.pointer) } } + unsafe fn local_name<'a>(self) -> DomStringView<'a> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element. C++ returns a string view borrowed + // from its current local name for this matching call. + DomStringView { + // SAFETY: C++ guarantees that the returned string view remains valid for matching. + view: unsafe { selector_ffi_element_local_name(self.pointer) }, + marker: PhantomData, + } + } + unsafe fn classes<'a>(self) -> &'a [usize] { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: The handle identifies a live DOM element. C++ returns its current class storage, @@ -1659,6 +1670,37 @@ pub struct FfiInternedStringList { pub count: usize, } +#[derive(Clone, Copy)] +#[repr(C)] +pub struct FfiDomStringView { + pub data: *const c_void, + pub length: usize, + pub is_ascii: bool, +} + +#[derive(Clone, Copy)] +struct DomStringView<'a> { + view: FfiDomStringView, + marker: PhantomData<&'a FfiCallScope>, +} + +impl DomStringView<'_> { + fn len(self) -> usize { + self.view.length + } + + fn code_unit_at(self, index: usize) -> u16 { + assert!(index < self.len()); + assert!(!self.view.data.is_null()); + if self.view.is_ascii { + // SAFETY: C++ guarantees that an ASCII view points at `length` bytes. + return u16::from(unsafe { *(self.view.data.cast::().add(index)) }); + } + // SAFETY: C++ guarantees that a UTF-16 view points at `length` aligned code units. + unsafe { *(self.view.data.cast::().add(index)) } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] // NB: Constructed by C++ through the FFI. @@ -1726,6 +1768,12 @@ impl<'a> FfiNode<'a> { // current class storage. unsafe { self.as_element().classes() } } + + fn local_name(self) -> DomStringView<'a> { + // SAFETY: `FfiNode` cannot outlive the call scope which pins the C++ element and its + // current local-name storage. + unsafe { self.as_element().local_name() } + } } #[derive(Clone, Copy)] @@ -1743,16 +1791,11 @@ unsafe extern "C" { fn selector_ffi_element_namespace_is_null(element: *const c_void) -> bool; fn selector_ffi_element_is_html_element_in_html_document(element: *const c_void) -> bool; fn selector_ffi_element_is_document_root(element: *const c_void) -> bool; + fn selector_ffi_element_local_name(element: *const c_void) -> FfiDomStringView; fn selector_ffi_default_namespace(context: *mut c_void) -> FfiResolvedNamespace; fn selector_ffi_resolve_namespace(context: *mut c_void, prefix: FfiStringView) -> FfiResolvedNamespace; - fn selector_ffi_matches_tag_name( - context: *mut c_void, - element: *const c_void, - cxx_simple_selector: *const c_void, - matching_mode: TagNameMatchingMode, - ) -> bool; fn selector_ffi_matches_class_quirks(element: *const c_void, cxx_simple_selector: *const c_void) -> bool; fn selector_ffi_matches_attribute( context: *mut c_void, @@ -1813,6 +1856,22 @@ fn ffi_string_view(string: &[u16]) -> FfiStringView { } } +fn ascii_lowercase(code_unit: u16) -> u16 { + if (u16::from(b'A')..=u16::from(b'Z')).contains(&code_unit) { + code_unit + u16::from(b'a' - b'A') + } else { + code_unit + } +} + +fn utf16_equals_ignoring_ascii_case(first: DomStringView<'_>, second: &[u16]) -> bool { + first.len() == second.len() + && second + .iter() + .enumerate() + .all(|(index, &second)| ascii_lowercase(first.code_unit_at(index)) == ascii_lowercase(second)) +} + struct FfiCallScope; struct FfiDom<'a> { @@ -1928,28 +1987,20 @@ impl<'a> SelectorDom for FfiDom<'a> { ) -> bool { let ffi_element = element.as_element(); let is_html_element_in_html_document = ffi_element.is_html_element_in_html_document(); - if is_html_element_in_html_document || mode == TagNameMatchingMode::Fast { + let name_matches = if is_html_element_in_html_document || mode == TagNameMatchingMode::Fast { let interned_name = if is_html_element_in_html_document { name.interned_lowercase_name } else { name.interned_name }; - if interned_name.is_none_or(|name| ffi_element.qualified_name().local_name() != Some(name)) { - return false; - } - return self.matches_universal_selector(element, name); - } - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the context, element, and retained simple selector - // remain valid for the duration of matching. - unsafe { - selector_ffi_matches_tag_name( - self.context, - element.as_element_pointer(), - name.cxx_simple_selector.as_ptr(), - mode, - ) + interned_name.is_some_and(|name| ffi_element.qualified_name().local_name() == Some(name)) + } else { + utf16_equals_ignoring_ascii_case(element.local_name(), &name.name) + }; + if !name_matches { + return false; } + self.matches_universal_selector(element, name) } fn matches_id_selector(&mut self, element: FfiNode<'a>, id: &NameSelector) -> bool { diff --git a/Libraries/LibWeb/CSS/SelectorMatching.cpp b/Libraries/LibWeb/CSS/SelectorMatching.cpp index 9e69c6fa162bf..fc55d11ccc64b 100644 --- a/Libraries/LibWeb/CSS/SelectorMatching.cpp +++ b/Libraries/LibWeb/CSS/SelectorMatching.cpp @@ -724,46 +724,6 @@ static bool is_in_null_namespace(DOM::Element const& element) return !element.namespace_uri().has_value() || element.namespace_uri()->is_empty(); } -static bool matches_namespace(CSS::Selector::SimpleSelector::QualifiedName const& qualified_name, DOM::Element const& element, GC::Ptr style_sheet_for_rule) -{ - switch (qualified_name.namespace_type) { - case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Default: - // "if no default namespace has been declared for selectors, this is equivalent to *|E." - if (!style_sheet_for_rule || !style_sheet_for_rule->default_namespace_rule()) - return true; - // "Otherwise it is equivalent to ns|E where ns is the default namespace." - if (style_sheet_for_rule->default_namespace_rule()->namespace_uri().is_empty()) - return is_in_null_namespace(element); - - return element.namespace_uri().has_value() - && style_sheet_for_rule->default_namespace_rule()->namespace_uri() == element.namespace_uri()->view(); - case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::None: - // "elements with name E without a namespace" - return is_in_null_namespace(element); - case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Any: - // "elements with name E in any namespace, including those without a namespace" - return true; - case CSS::Selector::SimpleSelector::QualifiedName::NamespaceType::Named: { - // "elements with name E in namespace ns" - // Unrecognized namespace prefixes are invalid, so don't match. - // (We can't detect this at parse time, since a namespace rule may be inserted later.) - // So, if we don't have a context to look up namespaces from, we fail to match. - if (!style_sheet_for_rule) - return false; - auto selector_namespace = style_sheet_for_rule->namespace_uri(qualified_name.namespace_); - // https://www.w3.org/TR/css-namespaces-3/#terminology - // In CSS Namespaces a namespace name consisting of the empty string is taken to represent the null namespace - // or lack of a namespace. - if (selector_namespace.has_value() && selector_namespace->is_empty()) - return is_in_null_namespace(element); - return selector_namespace.has_value() - && element.namespace_uri().has_value() - && *selector_namespace == element.namespace_uri()->view(); - } - } - VERIFY_NOT_REACHED(); -} - using CSS::SelectorFFI::AttributeCaseType; using CSS::SelectorFFI::AttributeMatchType; using CSS::SelectorFFI::Combinator; @@ -771,7 +731,6 @@ using CSS::SelectorFFI::Direction; using CSS::SelectorFFI::HasCacheResult; using CSS::SelectorFFI::NamespaceType; using CSS::SelectorFFI::StringView; -using CSS::SelectorFFI::TagNameMatchingMode; #define DECLARE_SELECTOR_FFI_CALLBACK(function) \ extern "C" decltype(CSS::SelectorFFI::function) function @@ -783,9 +742,9 @@ DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_class_names_are_case_insensit DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_namespace_is_null); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_html_element_in_html_document); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_document_root); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_local_name); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_default_namespace); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_resolve_namespace); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_tag_name); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_class_quirks); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_attribute); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_pseudo_class); @@ -875,6 +834,25 @@ extern "C" bool selector_ffi_element_is_document_root(void const* element) return is(ffi_element(element)); } +extern "C" CSS::SelectorFFI::DomStringView selector_ffi_element_local_name(void const* element) +{ + auto local_name = ffi_element(element).local_name().view(); + if (local_name.has_ascii_storage()) { + auto storage = local_name.ascii_span(); + return { + .data = storage.data(), + .length = storage.size(), + .is_ascii = true, + }; + } + auto storage = local_name.utf16_span(); + return { + .data = storage.data(), + .length = storage.size(), + .is_ascii = false, + }; +} + extern "C" CSS::SelectorFFI::ResolvedNamespace selector_ffi_default_namespace(void* context) { auto& match_context = rust_match_context(context); @@ -927,23 +905,6 @@ extern "C" CSS::SelectorFFI::ResolvedNamespace selector_ffi_resolve_namespace(vo }; } -extern "C" bool selector_ffi_matches_tag_name(void* context, void const* element, void const* cxx_simple_selector, TagNameMatchingMode matching_mode) -{ - auto& match_context = rust_match_context(context); - auto const& target = ffi_element(element); - auto const& qualified_name = ffi_simple_selector(cxx_simple_selector).qualified_name(); - bool const is_html_element_in_html_document = target.namespace_uri() == Namespace::HTML - && target.document().document_type() == DOM::Document::Type::HTML; - auto const& name_to_match = is_html_element_in_html_document ? qualified_name.name.lowercase_name : qualified_name.name.name; - bool name_matches; - if (is_html_element_in_html_document || matching_mode == TagNameMatchingMode::Fast) - name_matches = target.local_name() == name_to_match; - else - name_matches = target.local_name().equals_ignoring_ascii_case(name_to_match); - return name_matches - && matches_namespace(qualified_name, target, match_context.style_sheet_for_rule); -} - extern "C" bool selector_ffi_matches_class_quirks(void const* element, void const* cxx_simple_selector) { auto const& target = ffi_element(element); diff --git a/Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt b/Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt index 9838cf8887d82..bf6dead873096 100644 --- a/Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt +++ b/Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt @@ -12,6 +12,7 @@ relative sibling: true closest: true query selector all: 3 XML tag case: true/false +XML slow tag case: true XML namespace wildcard: true XML attribute namespace wildcard: true host: yes diff --git a/Tests/LibWeb/Text/input/css/selector-engine-characterization.html b/Tests/LibWeb/Text/input/css/selector-engine-characterization.html index abcd5298cbe2c..511cbc2ea8fcb 100644 --- a/Tests/LibWeb/Text/input/css/selector-engine-characterization.html +++ b/Tests/LibWeb/Text/input/css/selector-engine-characterization.html @@ -32,6 +32,7 @@ xml.documentElement.appendChild(child); println(`XML tag case: ${child.matches("Child")}/${child.matches("child")}`); + println(`XML slow tag case: ${child.matches("child:not(.missing)")}`); println(`XML namespace wildcard: ${child.matches("*|Child")}`); println(`XML attribute namespace wildcard: ${child.matches("[*|value]")}`); const host = document.createElement("div"); From 3a652c95f04ff77309342dadb3d114ede5efffc9 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Tue, 21 Jul 2026 23:17:29 +0200 Subject: [PATCH 08/27] LibWeb: Match quirks classes through live DOM views 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. --- .../LibWeb/CSS/Rust/src/selector_engine.rs | 33 +++++++----- Libraries/LibWeb/CSS/SelectorMatching.cpp | 50 ++++++++++--------- ...-mode-case-insensitive-class-selector.html | 2 +- 3 files changed, 48 insertions(+), 37 deletions(-) diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index cd41eda202cc1..309d193c35934 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -79,8 +79,6 @@ pub struct NameSelector { /// selectors compiled from C++ and allows the live DOM wrapper to compare interned names /// without crossing the FFI. interned_name: Option, - /// See [`QualifiedName::cxx_simple_selector`]. - cxx_simple_selector: RetainedCxxPointer, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -1629,6 +1627,17 @@ impl FfiElement { } } + unsafe fn class_name<'a>(self, index: usize) -> DomStringView<'a> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element and `index` identifies one of its + // current classes. C++ returns a view borrowed for this matching call. + DomStringView { + // SAFETY: The caller guarantees that `index` is within the current class list. + view: unsafe { selector_ffi_element_class_name(self.pointer, index) }, + marker: PhantomData, + } + } + unsafe fn classes<'a>(self) -> &'a [usize] { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: The handle identifies a live DOM element. C++ returns its current class storage, @@ -1774,6 +1783,12 @@ impl<'a> FfiNode<'a> { // current local-name storage. unsafe { self.as_element().local_name() } } + + fn class_name(self, index: usize) -> DomStringView<'a> { + // SAFETY: `FfiNode` cannot outlive the call scope which pins the C++ element and its + // current class-name storage. Callers obtain `index` from the same live class list. + unsafe { self.as_element().class_name(index) } + } } #[derive(Clone, Copy)] @@ -1792,11 +1807,11 @@ unsafe extern "C" { fn selector_ffi_element_is_html_element_in_html_document(element: *const c_void) -> bool; fn selector_ffi_element_is_document_root(element: *const c_void) -> bool; fn selector_ffi_element_local_name(element: *const c_void) -> FfiDomStringView; + fn selector_ffi_element_class_name(element: *const c_void, index: usize) -> FfiDomStringView; fn selector_ffi_default_namespace(context: *mut c_void) -> FfiResolvedNamespace; fn selector_ffi_resolve_namespace(context: *mut c_void, prefix: FfiStringView) -> FfiResolvedNamespace; - fn selector_ffi_matches_class_quirks(element: *const c_void, cxx_simple_selector: *const c_void) -> bool; fn selector_ffi_matches_attribute( context: *mut c_void, element: *const c_void, @@ -2010,12 +2025,8 @@ impl<'a> SelectorDom for FfiDom<'a> { fn matches_class_selector(&mut self, element: FfiNode<'a>, class_name: &NameSelector) -> bool { let ffi_element = element.as_element(); if ffi_element.class_names_are_case_insensitive() { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the element and retained simple selector remain - // valid for the duration of matching. - return unsafe { - selector_ffi_matches_class_quirks(element.as_element_pointer(), class_name.cxx_simple_selector.as_ptr()) - }; + return (0..element.classes().len()) + .any(|index| utf16_equals_ignoring_ascii_case(element.class_name(index), &class_name.name)); } class_name .interned_name @@ -2389,14 +2400,12 @@ unsafe fn simple_selector_from_ffi(selector: &FfiSimpleSelector) -> SimpleSelect name: unsafe { string_from_ffi(selector.name) }, // SAFETY: The caller guarantees that all retained C++ selector data is valid. interned_name: unsafe { interned_name_from_ffi(selector) }, - cxx_simple_selector: RetainedCxxPointer::new(selector.cxx_simple_selector), }), FfiSimpleSelectorType::Class => SimpleSelector::Class(NameSelector { // SAFETY: The caller guarantees that every string view in `selector` is valid. name: unsafe { string_from_ffi(selector.name) }, // SAFETY: The caller guarantees that all retained C++ selector data is valid. interned_name: unsafe { interned_name_from_ffi(selector) }, - cxx_simple_selector: RetainedCxxPointer::new(selector.cxx_simple_selector), }), FfiSimpleSelectorType::Attribute => SimpleSelector::Attribute(AttributeSelector { match_type: selector.attribute_match_type, @@ -2927,7 +2936,6 @@ mod tests { SimpleSelector::Class(NameSelector { name: name.encode_utf16().collect(), interned_name: None, - cxx_simple_selector: RetainedCxxPointer::default(), }) } @@ -2982,7 +2990,6 @@ mod tests { simple_selectors: vec![SimpleSelector::Id(NameSelector { name: Box::from([b'x' as u16]), interned_name: None, - cxx_simple_selector: RetainedCxxPointer::default(), })] .into_boxed_slice(), }) diff --git a/Libraries/LibWeb/CSS/SelectorMatching.cpp b/Libraries/LibWeb/CSS/SelectorMatching.cpp index fc55d11ccc64b..21d8f9dbe546c 100644 --- a/Libraries/LibWeb/CSS/SelectorMatching.cpp +++ b/Libraries/LibWeb/CSS/SelectorMatching.cpp @@ -743,9 +743,9 @@ DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_namespace_is_null); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_html_element_in_html_document); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_document_root); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_local_name); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_class_name); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_default_namespace); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_resolve_namespace); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_class_quirks); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_attribute); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_pseudo_class); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_language); @@ -788,6 +788,24 @@ static uintptr_t interned_string_identity(Utf16FlyString const& string) return identity; } +static CSS::SelectorFFI::DomStringView dom_string_view(Utf16View string) +{ + if (string.has_ascii_storage()) { + auto storage = string.ascii_span(); + return { + .data = storage.data(), + .length = storage.size(), + .is_ascii = true, + }; + } + auto storage = string.utf16_span(); + return { + .data = storage.data(), + .length = storage.size(), + .is_ascii = false, + }; +} + extern "C" CSS::SelectorFFI::ElementQualifiedName selector_ffi_element_qualified_name(void const* element) { auto const& target = ffi_element(element); @@ -836,21 +854,14 @@ extern "C" bool selector_ffi_element_is_document_root(void const* element) extern "C" CSS::SelectorFFI::DomStringView selector_ffi_element_local_name(void const* element) { - auto local_name = ffi_element(element).local_name().view(); - if (local_name.has_ascii_storage()) { - auto storage = local_name.ascii_span(); - return { - .data = storage.data(), - .length = storage.size(), - .is_ascii = true, - }; - } - auto storage = local_name.utf16_span(); - return { - .data = storage.data(), - .length = storage.size(), - .is_ascii = false, - }; + return dom_string_view(ffi_element(element).local_name()); +} + +extern "C" CSS::SelectorFFI::DomStringView selector_ffi_element_class_name(void const* element, size_t index) +{ + auto const& classes = ffi_element(element).class_names(); + VERIFY(index < classes.size()); + return dom_string_view(classes[index]); } extern "C" CSS::SelectorFFI::ResolvedNamespace selector_ffi_default_namespace(void* context) @@ -905,13 +916,6 @@ extern "C" CSS::SelectorFFI::ResolvedNamespace selector_ffi_resolve_namespace(vo }; } -extern "C" bool selector_ffi_matches_class_quirks(void const* element, void const* cxx_simple_selector) -{ - auto const& target = ffi_element(element); - VERIFY(target.document().in_quirks_mode()); - return target.has_class(ffi_simple_selector(cxx_simple_selector).class_name(), CaseSensitivity::CaseInsensitive); -} - static bool matches_attribute_value(CSS::Selector::SimpleSelector::Attribute::MatchType match_type, Utf16View selector_value, Utf16View element_value, CaseSensitivity case_sensitivity) { bool const case_insensitive = case_sensitivity == CaseSensitivity::CaseInsensitive; diff --git a/Tests/LibWeb/Text/input/quirks-mode-case-insensitive-class-selector.html b/Tests/LibWeb/Text/input/quirks-mode-case-insensitive-class-selector.html index 810a266601453..9d195c3601775 100644 --- a/Tests/LibWeb/Text/input/quirks-mode-case-insensitive-class-selector.html +++ b/Tests/LibWeb/Text/input/quirks-mode-case-insensitive-class-selector.html @@ -1,6 +1,6 @@ -
+
+ +
+ From c50b63287b2d2a1822dd826ff4d08841d5f4ee58 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 22 Jul 2026 00:29:02 +0200 Subject: [PATCH 20/27] LibWeb: Match remaining states through live DOM facts 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. --- .../LibWeb/CSS/Rust/src/selector_engine.rs | 146 +++++- Libraries/LibWeb/CSS/SelectorMatching.cpp | 457 +++++++----------- 2 files changed, 304 insertions(+), 299 deletions(-) diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index c72edffd5cfd2..4f87339cdb551 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -1479,6 +1479,37 @@ pub enum FfiDirection { Other, } +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +// NB: Constructed by C++ through the FFI. +#[allow(dead_code)] +pub enum FfiMeterValueState { + NotMeter, + EvenLessGood, + Suboptimal, + Optimal, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +// NB: Constructed by C++ through the FFI. +#[allow(dead_code)] +pub enum FfiRequiredState { + NotApplicable, + Optional, + Required, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +// NB: Constructed by C++ through the FFI. +#[allow(dead_code)] +pub enum FfiValidityState { + NotApplicable, + Invalid, + Valid, +} + #[derive(Clone, Copy)] #[repr(u8)] // NB: Constructed by C++ through the FFI. @@ -1780,6 +1811,78 @@ impl FfiElement { unsafe { selector_ffi_element_media_is_stalled(self.pointer) } } + fn is_default(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_default(self.pointer) } + } + + fn meter_value_state(self) -> FfiMeterValueState { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_meter_value_state(self.pointer) } + } + + fn meter_value_is_high(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_meter_value_is_high(self.pointer) } + } + + fn meter_value_is_low(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_meter_value_is_low(self.pointer) } + } + + fn is_hovered(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_hovered(self.pointer) } + } + + fn is_indeterminate(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_indeterminate(self.pointer) } + } + + fn validity_state(self) -> FfiValidityState { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_validity_state(self.pointer) } + } + + fn is_modal(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_modal(self.pointer) } + } + + fn is_open(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_open(self.pointer) } + } + + fn required_state(self) -> FfiRequiredState { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_required_state(self.pointer) } + } + + fn is_read_write(self) -> bool { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_is_read_write(self.pointer) } + } + + fn user_validity_state(self) -> FfiValidityState { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element for the duration of matching. + unsafe { selector_ffi_element_user_validity_state(self.pointer) } + } + unsafe fn local_name<'a>(self) -> DomStringView<'a> { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: The handle identifies a live DOM element. C++ returns a string view borrowed @@ -2061,6 +2164,18 @@ unsafe extern "C" { fn selector_ffi_element_media_is_paused(element: *const c_void) -> bool; fn selector_ffi_element_media_is_seeking(element: *const c_void) -> bool; fn selector_ffi_element_media_is_stalled(element: *const c_void) -> bool; + fn selector_ffi_element_is_default(element: *const c_void) -> bool; + fn selector_ffi_element_meter_value_state(element: *const c_void) -> FfiMeterValueState; + fn selector_ffi_element_meter_value_is_high(element: *const c_void) -> bool; + fn selector_ffi_element_meter_value_is_low(element: *const c_void) -> bool; + fn selector_ffi_element_is_hovered(element: *const c_void) -> bool; + fn selector_ffi_element_is_indeterminate(element: *const c_void) -> bool; + fn selector_ffi_element_validity_state(element: *const c_void) -> FfiValidityState; + fn selector_ffi_element_is_modal(element: *const c_void) -> bool; + fn selector_ffi_element_is_open(element: *const c_void) -> bool; + fn selector_ffi_element_required_state(element: *const c_void) -> FfiRequiredState; + fn selector_ffi_element_is_read_write(element: *const c_void) -> bool; + fn selector_ffi_element_user_validity_state(element: *const c_void) -> FfiValidityState; fn selector_ffi_element_local_name(element: *const c_void) -> FfiDomStringView; fn selector_ffi_element_class_name(element: *const c_void, index: usize) -> FfiDomStringView; fn selector_ffi_element_attribute_count(element: *const c_void) -> usize; @@ -2069,7 +2184,6 @@ unsafe extern "C" { fn selector_ffi_default_namespace(context: *mut c_void) -> FfiResolvedNamespace; fn selector_ffi_resolve_namespace(context: *mut c_void, prefix: FfiStringView) -> FfiResolvedNamespace; - fn selector_ffi_matches_pseudo_class(element: *const c_void, pseudo_class: u8) -> bool; fn selector_ffi_parent_element(element: *const c_void, shadow_host: *const c_void) -> FfiElement; fn selector_ffi_parent_element_in_light_tree(element: *const c_void) -> FfiElement; fn selector_ffi_previous_element_sibling(element: *const c_void) -> FfiElement; @@ -2684,6 +2798,28 @@ impl<'a> SelectorDom for FfiDom<'a> { } PseudoClassType::Seeking => element.as_element().media_is_seeking(), PseudoClassType::Stalled => element.as_element().media_is_stalled(), + PseudoClassType::Default => element.as_element().is_default(), + PseudoClassType::EvenLessGoodValue => { + element.as_element().meter_value_state() == FfiMeterValueState::EvenLessGood + } + PseudoClassType::HighValue => element.as_element().meter_value_is_high(), + PseudoClassType::Hover => element.as_element().is_hovered(), + PseudoClassType::Indeterminate => element.as_element().is_indeterminate(), + PseudoClassType::Invalid => element.as_element().validity_state() == FfiValidityState::Invalid, + PseudoClassType::LowValue => element.as_element().meter_value_is_low(), + PseudoClassType::Modal => element.as_element().is_modal(), + PseudoClassType::Open => element.as_element().is_open(), + PseudoClassType::OptimalValue => element.as_element().meter_value_state() == FfiMeterValueState::Optimal, + PseudoClassType::Optional => element.as_element().required_state() == FfiRequiredState::Optional, + PseudoClassType::ReadOnly => !element.as_element().is_read_write(), + PseudoClassType::ReadWrite => element.as_element().is_read_write(), + PseudoClassType::Required => element.as_element().required_state() == FfiRequiredState::Required, + PseudoClassType::SuboptimalValue => { + element.as_element().meter_value_state() == FfiMeterValueState::Suboptimal + } + PseudoClassType::UserInvalid => element.as_element().user_validity_state() == FfiValidityState::Invalid, + PseudoClassType::UserValid => element.as_element().user_validity_state() == FfiValidityState::Valid, + PseudoClassType::Valid => element.as_element().validity_state() == FfiValidityState::Valid, PseudoClassType::Fullscreen => element.as_element().is_fullscreen(), PseudoClassType::Focus => element.as_element().is_focused(), PseudoClassType::FocusVisible => { @@ -2729,13 +2865,7 @@ impl<'a> SelectorDom for FfiDom<'a> { PseudoClassType::PopoverOpen => { element.as_element().has_popover_attribute() && element.as_element().popover_is_showing() } - _ => { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorSimpleSelectorCallback); - // SAFETY: `FfiDom` guarantees that the element remains valid for matching. - unsafe { - selector_ffi_matches_pseudo_class(element.as_element_pointer(), pseudo_class.pseudo_class as u8) - } - } + _ => unreachable!("structural pseudo-class reached state matching"), } } diff --git a/Libraries/LibWeb/CSS/SelectorMatching.cpp b/Libraries/LibWeb/CSS/SelectorMatching.cpp index 27d7407d504d7..44251fa17d198 100644 --- a/Libraries/LibWeb/CSS/SelectorMatching.cpp +++ b/Libraries/LibWeb/CSS/SelectorMatching.cpp @@ -306,235 +306,6 @@ static bool matches_open_state_pseudo_class(DOM::Element const& element, bool op return false; } -static bool matches_optimal_value_pseudo_class(DOM::Element const& element, HTML::HTMLMeterElement::ValueState desired_state) -{ - if (auto* meter = as_if(element)) - return meter->value_state() == desired_state; - return false; -} - -static bool matches_pseudo_class_state(CSS::PseudoClass pseudo_class, DOM::Element const& element) -{ - switch (pseudo_class) { - case CSS::PseudoClass::Default: { - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-default - if (auto const* form_associated_element = as_if(element)) { - if (form_associated_element->is_submit_button() && form_associated_element->form() && form_associated_element->form()->default_button() == form_associated_element) - return true; - if (auto const* input_element = as_if(form_associated_element)) { - if (input_element->checked_applies() && input_element->has_attribute(HTML::AttributeNames::checked)) - return true; - } - if (auto const* option_element = as_if(form_associated_element)) { - if (option_element->has_attribute(HTML::AttributeNames::selected)) - return true; - } - } - return false; - } - case CSS::PseudoClass::EvenLessGoodValue: - return matches_optimal_value_pseudo_class(element, HTML::HTMLMeterElement::ValueState::EvenLessGood); - case CSS::PseudoClass::HighValue: - if (auto const* meter = as_if(element)) - return meter->value() > meter->high(); - return false; - case CSS::PseudoClass::Hover: - return matches_hover_pseudo_class(element); - case CSS::PseudoClass::Indeterminate: - return matches_indeterminate_pseudo_class(element); - case CSS::PseudoClass::Invalid: { - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-invalid - if (auto form_associated_element = as_if(element)) { - if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) - return true; - } - - if (auto form_element = as_if(element)) { - bool has_invalid_elements = false; - element.for_each_in_subtree([&](auto& node) { - if (auto form_associated_element = as_if(&node)) { - if (form_associated_element->form() == form_element && form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { - has_invalid_elements = true; - return TraversalDecision::Break; - } - } - return TraversalDecision::Continue; - }); - if (has_invalid_elements) - return true; - } - - if (is(element)) { - bool has_invalid_children = false; - element.for_each_in_subtree([&](auto& node) { - if (auto form_associated_element = as_if(&node)) { - if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { - has_invalid_children = true; - return TraversalDecision::Break; - } - } - return TraversalDecision::Continue; - }); - if (has_invalid_children) - return true; - } - return false; - } - case CSS::PseudoClass::LowValue: - if (auto const* meter = as_if(element)) - return meter->value() < meter->low(); - return false; - case CSS::PseudoClass::Modal: - // https://drafts.csswg.org/selectors/#modal-state - if (auto const* dialog_element = as_if(element)) - return dialog_element->is_modal(); - // FIXME: fullscreen elements are also modal. - return false; - case CSS::PseudoClass::Open: - return matches_open_state_pseudo_class(element, true); - case CSS::PseudoClass::OptimalValue: - return matches_optimal_value_pseudo_class(element, HTML::HTMLMeterElement::ValueState::Optimal); - case CSS::PseudoClass::Optional: - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-optional - if (auto const* input_element = as_if(element)) { - if (input_element->required_applies() && !input_element->has_attribute(HTML::AttributeNames::required)) - return true; - // AD-HOC: Chromium and Webkit also match for hidden inputs (and WPT expects this) - // See: https://github.com/whatwg/html/issues/11273 - return input_element->type_state() == HTML::HTMLInputElement::TypeAttributeState::Hidden; - } - if (auto const* select_element = as_if(element)) - return !select_element->has_attribute(HTML::AttributeNames::required); - if (auto const* textarea_element = as_if(element)) - return !textarea_element->has_attribute(HTML::AttributeNames::required); - return false; - case CSS::PseudoClass::ReadOnly: - return !matches_read_write_pseudo_class(element); - case CSS::PseudoClass::ReadWrite: - return matches_read_write_pseudo_class(element); - case CSS::PseudoClass::Required: - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-required - if (auto const* input_element = as_if(element)) - return input_element->required_applies() && input_element->has_attribute(HTML::AttributeNames::required); - if (auto const* select_element = as_if(element)) - return select_element->has_attribute(HTML::AttributeNames::required); - if (auto const* textarea_element = as_if(element)) - return textarea_element->has_attribute(HTML::AttributeNames::required); - return false; - case CSS::PseudoClass::SuboptimalValue: - return matches_optimal_value_pseudo_class(element, HTML::HTMLMeterElement::ValueState::Suboptimal); - case CSS::PseudoClass::UserInvalid: - case CSS::PseudoClass::UserValid: { - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-user-valid - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-user-invalid - bool user_validity = false; - if (auto const* input_element = as_if(element)) - user_validity = input_element->user_validity(); - else if (auto const* select_element = as_if(element)) - user_validity = select_element->user_validity(); - else if (auto const* text_area_element = as_if(element)) - user_validity = text_area_element->user_validity(); - if (!user_validity) - return false; - - auto const& form_associated_element = as(element); - if (!form_associated_element.is_candidate_for_constraint_validation()) - return false; - return pseudo_class == CSS::PseudoClass::UserValid - ? form_associated_element.satisfies_its_constraints() - : !form_associated_element.satisfies_its_constraints(); - } - case CSS::PseudoClass::Valid: { - // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-valid - if (auto form_associated_element = as_if(element)) { - if (form_associated_element->is_candidate_for_constraint_validation() && form_associated_element->satisfies_its_constraints()) - return true; - } - - if (auto form_element = as_if(element)) { - bool has_invalid_elements = false; - element.for_each_in_subtree([&](auto& node) { - if (auto form_associated_element = as_if(&node)) { - if (form_associated_element->form() == form_element && form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { - has_invalid_elements = true; - return TraversalDecision::Break; - } - } - return TraversalDecision::Continue; - }); - if (!has_invalid_elements) - return true; - } - - if (is(element)) { - bool has_invalid_children = false; - element.for_each_in_subtree([&](auto& node) { - if (auto form_associated_element = as_if(&node)) { - if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { - has_invalid_children = true; - return TraversalDecision::Break; - } - } - return TraversalDecision::Continue; - }); - if (!has_invalid_children) - return true; - } - return false; - } - case CSS::PseudoClass::Visited: - case CSS::PseudoClass::VolumeLocked: - case CSS::PseudoClass::__Count: - case CSS::PseudoClass::Active: - case CSS::PseudoClass::AnyLink: - case CSS::PseudoClass::Autofill: - case CSS::PseudoClass::Buffering: - case CSS::PseudoClass::Checked: - case CSS::PseudoClass::Defined: - case CSS::PseudoClass::Dir: - case CSS::PseudoClass::Disabled: - case CSS::PseudoClass::Empty: - case CSS::PseudoClass::Enabled: - case CSS::PseudoClass::FirstChild: - case CSS::PseudoClass::FirstOfType: - case CSS::PseudoClass::Focus: - case CSS::PseudoClass::FocusVisible: - case CSS::PseudoClass::FocusWithin: - case CSS::PseudoClass::Fullscreen: - case CSS::PseudoClass::Has: - case CSS::PseudoClass::Heading: - case CSS::PseudoClass::Host: - case CSS::PseudoClass::Is: - case CSS::PseudoClass::Lang: - case CSS::PseudoClass::LastChild: - case CSS::PseudoClass::LastOfType: - case CSS::PseudoClass::Link: - case CSS::PseudoClass::LocalLink: - case CSS::PseudoClass::Muted: - case CSS::PseudoClass::Not: - case CSS::PseudoClass::NthChild: - case CSS::PseudoClass::NthLastChild: - case CSS::PseudoClass::NthLastOfType: - case CSS::PseudoClass::NthOfType: - case CSS::PseudoClass::OnlyChild: - case CSS::PseudoClass::OnlyOfType: - case CSS::PseudoClass::Paused: - case CSS::PseudoClass::PlaceholderShown: - case CSS::PseudoClass::PopoverOpen: - case CSS::PseudoClass::Playing: - case CSS::PseudoClass::Root: - case CSS::PseudoClass::Scope: - case CSS::PseudoClass::Seeking: - case CSS::PseudoClass::State: - case CSS::PseudoClass::Stalled: - case CSS::PseudoClass::Target: - case CSS::PseudoClass::Unchecked: - case CSS::PseudoClass::Where: - VERIFY_NOT_REACHED(); - } - VERIFY_NOT_REACHED(); -} - static CSS::SelectorFFI::Element element_to_ffi(DOM::Element const* element) { if (!element) @@ -642,13 +413,24 @@ DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_media_is_muted); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_media_is_paused); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_media_is_seeking); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_media_is_stalled); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_default); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_meter_value_state); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_meter_value_is_high); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_meter_value_is_low); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_hovered); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_indeterminate); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_validity_state); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_modal); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_open); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_required_state); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_read_write); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_user_validity_state); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_local_name); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_class_name); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_attribute_count); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_attribute); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_default_namespace); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_resolve_namespace); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_matches_pseudo_class); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_parent_element); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_parent_element_in_light_tree); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_previous_element_sibling); @@ -899,6 +681,160 @@ extern "C" bool selector_ffi_element_media_is_stalled(void const* element) return media_element && media_element->stalled(); } +// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-default +extern "C" bool selector_ffi_element_is_default(void const* element) +{ + auto const& target = ffi_element(element); + auto const* form_associated_element = as_if(target); + if (!form_associated_element) + return false; + if (form_associated_element->is_submit_button() && form_associated_element->form() && form_associated_element->form()->default_button() == form_associated_element) + return true; + if (auto const* input_element = as_if(form_associated_element)) + return input_element->checked_applies() && input_element->has_attribute(HTML::AttributeNames::checked); + if (auto const* option_element = as_if(form_associated_element)) + return option_element->has_attribute(HTML::AttributeNames::selected); + return false; +} + +extern "C" CSS::SelectorFFI::FfiMeterValueState selector_ffi_element_meter_value_state(void const* element) +{ + auto const* meter = as_if(ffi_element(element)); + if (!meter) + return CSS::SelectorFFI::FfiMeterValueState::NotMeter; + switch (meter->value_state()) { + case HTML::HTMLMeterElement::ValueState::EvenLessGood: + return CSS::SelectorFFI::FfiMeterValueState::EvenLessGood; + case HTML::HTMLMeterElement::ValueState::Suboptimal: + return CSS::SelectorFFI::FfiMeterValueState::Suboptimal; + case HTML::HTMLMeterElement::ValueState::Optimal: + return CSS::SelectorFFI::FfiMeterValueState::Optimal; + } + VERIFY_NOT_REACHED(); +} + +extern "C" bool selector_ffi_element_meter_value_is_high(void const* element) +{ + auto const* meter = as_if(ffi_element(element)); + return meter && meter->value() > meter->high(); +} + +extern "C" bool selector_ffi_element_meter_value_is_low(void const* element) +{ + auto const* meter = as_if(ffi_element(element)); + return meter && meter->value() < meter->low(); +} + +extern "C" bool selector_ffi_element_is_hovered(void const* element) +{ + return matches_hover_pseudo_class(ffi_element(element)); +} + +extern "C" bool selector_ffi_element_is_indeterminate(void const* element) +{ + return matches_indeterminate_pseudo_class(ffi_element(element)); +} + +// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-invalid +// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-valid +extern "C" CSS::SelectorFFI::FfiValidityState selector_ffi_element_validity_state(void const* element) +{ + auto const& target = ffi_element(element); + if (auto const* form_associated_element = as_if(target)) { + if (form_associated_element->is_candidate_for_constraint_validation()) { + return form_associated_element->satisfies_its_constraints() + ? CSS::SelectorFFI::FfiValidityState::Valid + : CSS::SelectorFFI::FfiValidityState::Invalid; + } + } + + auto const* form_element = as_if(target); + if (!form_element && !is(target)) + return CSS::SelectorFFI::FfiValidityState::NotApplicable; + + bool has_invalid_elements = false; + target.for_each_in_subtree([&](auto& node) { + auto const* form_associated_element = as_if(&node); + if (!form_associated_element) + return TraversalDecision::Continue; + if (form_element && form_associated_element->form() != form_element) + return TraversalDecision::Continue; + if (form_associated_element->is_candidate_for_constraint_validation() && !form_associated_element->satisfies_its_constraints()) { + has_invalid_elements = true; + return TraversalDecision::Break; + } + return TraversalDecision::Continue; + }); + return has_invalid_elements + ? CSS::SelectorFFI::FfiValidityState::Invalid + : CSS::SelectorFFI::FfiValidityState::Valid; +} + +// https://drafts.csswg.org/selectors/#modal-state +extern "C" bool selector_ffi_element_is_modal(void const* element) +{ + auto const* dialog_element = as_if(ffi_element(element)); + // FIXME: Fullscreen elements are also modal. + return dialog_element && dialog_element->is_modal(); +} + +extern "C" bool selector_ffi_element_is_open(void const* element) +{ + return matches_open_state_pseudo_class(ffi_element(element), true); +} + +// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-optional +// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-required +extern "C" CSS::SelectorFFI::FfiRequiredState selector_ffi_element_required_state(void const* element) +{ + auto const& target = ffi_element(element); + if (auto const* input_element = as_if(target)) { + if (input_element->required_applies()) + return input_element->has_attribute(HTML::AttributeNames::required) + ? CSS::SelectorFFI::FfiRequiredState::Required + : CSS::SelectorFFI::FfiRequiredState::Optional; + // AD-HOC: Chromium and WebKit also match :optional for hidden inputs. + return input_element->type_state() == HTML::HTMLInputElement::TypeAttributeState::Hidden + ? CSS::SelectorFFI::FfiRequiredState::Optional + : CSS::SelectorFFI::FfiRequiredState::NotApplicable; + } + if (is(target) || is(target)) + return target.has_attribute(HTML::AttributeNames::required) + ? CSS::SelectorFFI::FfiRequiredState::Required + : CSS::SelectorFFI::FfiRequiredState::Optional; + return CSS::SelectorFFI::FfiRequiredState::NotApplicable; +} + +extern "C" bool selector_ffi_element_is_read_write(void const* element) +{ + return matches_read_write_pseudo_class(ffi_element(element)); +} + +// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-user-valid +// https://html.spec.whatwg.org/multipage/semantics-other.html#selector-user-invalid +extern "C" CSS::SelectorFFI::FfiValidityState selector_ffi_element_user_validity_state(void const* element) +{ + auto const& target = ffi_element(element); + bool user_validity = false; + if (auto const* input_element = as_if(target)) + user_validity = input_element->user_validity(); + else if (auto const* select_element = as_if(target)) + user_validity = select_element->user_validity(); + else if (auto const* text_area_element = as_if(target)) + user_validity = text_area_element->user_validity(); + else + return CSS::SelectorFFI::FfiValidityState::NotApplicable; + if (!user_validity) + return CSS::SelectorFFI::FfiValidityState::NotApplicable; + + auto const& form_associated_element = as(target); + if (!form_associated_element.is_candidate_for_constraint_validation()) + return CSS::SelectorFFI::FfiValidityState::NotApplicable; + return form_associated_element.satisfies_its_constraints() + ? CSS::SelectorFFI::FfiValidityState::Valid + : CSS::SelectorFFI::FfiValidityState::Invalid; +} + extern "C" CSS::SelectorFFI::DomStringView selector_ffi_element_local_name(void const* element) { return dom_string_view(ffi_element(element).local_name()); @@ -984,67 +920,6 @@ extern "C" CSS::SelectorFFI::ResolvedNamespace selector_ffi_resolve_namespace(vo }; } -static bool is_rust_matched_pseudo_class(CSS::PseudoClass pseudo_class) -{ - return first_is_one_of( - pseudo_class, - CSS::PseudoClass::Active, - CSS::PseudoClass::AnyLink, - CSS::PseudoClass::Autofill, - CSS::PseudoClass::Buffering, - CSS::PseudoClass::Checked, - CSS::PseudoClass::Defined, - CSS::PseudoClass::Disabled, - CSS::PseudoClass::Empty, - CSS::PseudoClass::Enabled, - CSS::PseudoClass::FirstChild, - CSS::PseudoClass::FirstOfType, - CSS::PseudoClass::Focus, - CSS::PseudoClass::FocusVisible, - CSS::PseudoClass::FocusWithin, - CSS::PseudoClass::Fullscreen, - CSS::PseudoClass::Has, - CSS::PseudoClass::Host, - CSS::PseudoClass::Is, - CSS::PseudoClass::LastChild, - CSS::PseudoClass::LastOfType, - CSS::PseudoClass::Link, - CSS::PseudoClass::LocalLink, - CSS::PseudoClass::Muted, - CSS::PseudoClass::Not, - CSS::PseudoClass::NthChild, - CSS::PseudoClass::NthLastChild, - CSS::PseudoClass::NthLastOfType, - CSS::PseudoClass::NthOfType, - CSS::PseudoClass::OnlyChild, - CSS::PseudoClass::OnlyOfType, - CSS::PseudoClass::Paused, - CSS::PseudoClass::PlaceholderShown, - CSS::PseudoClass::PopoverOpen, - CSS::PseudoClass::Playing, - CSS::PseudoClass::Root, - CSS::PseudoClass::Scope, - CSS::PseudoClass::Seeking, - CSS::PseudoClass::Where, - CSS::PseudoClass::Dir, - CSS::PseudoClass::Heading, - CSS::PseudoClass::Lang, - CSS::PseudoClass::State, - CSS::PseudoClass::Stalled, - CSS::PseudoClass::Target, - CSS::PseudoClass::Unchecked, - CSS::PseudoClass::Visited, - CSS::PseudoClass::VolumeLocked); -} - -extern "C" bool selector_ffi_matches_pseudo_class(void const* element, u8 pseudo_class_value) -{ - auto pseudo_class = static_cast(pseudo_class_value); - VERIFY(pseudo_class < CSS::PseudoClass::__Count); - VERIFY(!is_rust_matched_pseudo_class(pseudo_class)); - return matches_pseudo_class_state(pseudo_class, ffi_element(element)); -} - extern "C" CSS::SelectorFFI::Element selector_ffi_parent_element(void const* element, void const* shadow_host) { auto const& target = ffi_element(element); From e50a76df0777115e8eaf2f659657634bffdc5d6b Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 22 Jul 2026 00:33:08 +0200 Subject: [PATCH 21/27] LibWeb: Treat selector navigation as live DOM reads 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. --- Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs | 1 - .../LibWeb/CSS/Rust/src/selector_engine.rs | 32 +++++++++++-------- Libraries/LibWeb/CSS/SelectorMatching.cpp | 19 +++++------ 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs index ff031590b8c32..9c88cf51d2c09 100644 --- a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs +++ b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs @@ -55,7 +55,6 @@ define_ffi_ops! { // Callbacks: Rust -> C++. SelectorDomReadCallback => "selectorDomReadCallbacks", SelectorSimpleSelectorCallback => "selectorSimpleSelectorCallbacks", - SelectorTreeNavigationCallback => "selectorTreeNavigationCallbacks", SelectorMetadataCallback => "selectorMetadataCallbacks", CascadePropertyDisallowedCallback => "cascadePropertyDisallowedCallbacks", CascadeResolveUnresolvedCallback => "cascadeResolveUnresolvedCallbacks", diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index 4f87339cdb551..f47ac71c16726 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -2191,7 +2191,8 @@ unsafe extern "C" { fn selector_ffi_first_element_child(element: *const c_void) -> FfiElement; fn selector_ffi_first_element_descendant(element: *const c_void) -> FfiElement; fn selector_ffi_next_element_descendant(element: *const c_void, root: *const c_void) -> FfiElement; - fn selector_ffi_has_no_element_or_nonempty_text_children(element: *const c_void) -> bool; + fn selector_ffi_element_has_element_child(element: *const c_void) -> bool; + fn selector_ffi_element_has_nonempty_text_child(element: *const c_void) -> bool; fn selector_ffi_is_shadow_tree_slot(element: *const c_void) -> bool; fn selector_ffi_slotted_parent(context: *mut c_void, element: *const c_void) -> FfiElementAndShadowHost; @@ -2870,7 +2871,7 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn parent_element(&mut self, element: FfiNode<'a>, shadow_host: Option>) -> Option> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the input handles remain valid. The callback returns // either null or another live element borrowed for the same lifetime. unsafe { @@ -2882,42 +2883,42 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn parent_element_in_light_tree(&mut self, element: FfiNode<'a>) -> Option> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_parent_element_in_light_tree(element.as_element_pointer())) } } fn previous_element_sibling(&mut self, element: FfiNode<'a>) -> Option> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_previous_element_sibling(element.as_element_pointer())) } } fn next_element_sibling(&mut self, element: FfiNode<'a>) -> Option> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_next_element_sibling(element.as_element_pointer())) } } fn first_element_child(&mut self, element: FfiNode<'a>) -> Option> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_first_element_child(element.as_element_pointer())) } } fn first_element_descendant(&mut self, element: FfiNode<'a>) -> Option> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the input remains valid. The callback returns either // null or another live element borrowed for the same lifetime. unsafe { self.element(selector_ffi_first_element_descendant(element.as_element_pointer())) } } fn next_element_descendant(&mut self, element: FfiNode<'a>, root: FfiNode<'a>) -> Option> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that both element handles remain valid for this call. The // callback returns either null or another live element borrowed for the same lifetime. unsafe { @@ -2929,9 +2930,14 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn has_no_element_or_nonempty_text_children(&mut self, element: FfiNode<'a>) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the element remains valid for matching. - unsafe { selector_ffi_has_no_element_or_nonempty_text_children(element.as_element_pointer()) } + if unsafe { selector_ffi_element_has_element_child(element.as_element_pointer()) } { + return false; + } + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: `FfiDom` guarantees that the element remains valid for matching. + !unsafe { selector_ffi_element_has_nonempty_text_child(element.as_element_pointer()) } } fn has_same_type(&mut self, first: FfiNode<'a>, second: FfiNode<'a>) -> bool { @@ -2945,13 +2951,13 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn is_shadow_tree_slot(&mut self, element: FfiNode<'a>) -> bool { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the element remains valid for matching. unsafe { selector_ffi_is_shadow_tree_slot(element.as_element_pointer()) } } fn slotted_parent(&mut self, element: FfiNode<'a>) -> Option<(FfiNode<'a>, Option>)> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: `FfiDom` guarantees that the context and element remain valid. The callback // returns null or live elements borrowed for the same lifetime. unsafe { self.element_and_shadow_host(selector_ffi_slotted_parent(self.context, element.as_element_pointer())) } @@ -2964,7 +2970,7 @@ impl<'a> SelectorDom for FfiDom<'a> { allow_same_shadow_root_scope: bool, shadow_host: Option>, ) -> Option<(FfiNode<'a>, Option>)> { - crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorTreeNavigationCallback); + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); let identifiers = identifiers .iter() .map(|identifier| ffi_string_view(identifier)) diff --git a/Libraries/LibWeb/CSS/SelectorMatching.cpp b/Libraries/LibWeb/CSS/SelectorMatching.cpp index 44251fa17d198..d84693f623917 100644 --- a/Libraries/LibWeb/CSS/SelectorMatching.cpp +++ b/Libraries/LibWeb/CSS/SelectorMatching.cpp @@ -438,7 +438,8 @@ DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_next_element_sibling); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_first_element_child); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_first_element_descendant); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_next_element_descendant); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_has_no_element_or_nonempty_text_children); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_has_element_child); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_has_nonempty_text_child); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_is_shadow_tree_slot); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_slotted_parent); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_part_parent); @@ -970,22 +971,22 @@ extern "C" CSS::SelectorFFI::Element selector_ffi_next_element_descendant(void c return {}; } -extern "C" bool selector_ffi_has_no_element_or_nonempty_text_children(void const* element) +extern "C" bool selector_ffi_element_has_element_child(void const* element) +{ + return ffi_element(element).first_child_of_type(); +} + +extern "C" bool selector_ffi_element_has_nonempty_text_child(void const* element) { - auto const& target = ffi_element(element); - if (!target.has_children()) - return true; - if (target.first_child_of_type()) - return false; bool has_nonempty_text_child = false; - target.for_each_child_of_type([&](auto const& text) { + ffi_element(element).for_each_child_of_type([&](auto const& text) { if (!text.data().is_empty()) { has_nonempty_text_child = true; return IterationDecision::Break; } return IterationDecision::Continue; }); - return !has_nonempty_text_child; + return has_nonempty_text_child; } extern "C" bool selector_ffi_is_shadow_tree_slot(void const* element) From 5ad695493d10fab5e9b6e3c50b6b825304c83abd Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 22 Jul 2026 00:53:05 +0200 Subject: [PATCH 22/27] LibWeb: Remove the retired selector callback counter --- Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs index 9c88cf51d2c09..73eec87212f42 100644 --- a/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs +++ b/Libraries/LibWeb/CSS/Rust/src/ffi_stats.rs @@ -54,7 +54,6 @@ define_ffi_ops! { StyleGroupFreeEntry => "styleGroupFreeEntries", // Callbacks: Rust -> C++. SelectorDomReadCallback => "selectorDomReadCallbacks", - SelectorSimpleSelectorCallback => "selectorSimpleSelectorCallbacks", SelectorMetadataCallback => "selectorMetadataCallbacks", CascadePropertyDisallowedCallback => "cascadePropertyDisallowedCallbacks", CascadeResolveUnresolvedCallback => "cascadeResolveUnresolvedCallbacks", From a9b1d477a3935057c4f7644bbf1d363deb8e9e26 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 22 Jul 2026 00:56:12 +0200 Subject: [PATCH 23/27] LibWeb: Remove redundant selector DOM wrapper state 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. --- .../LibWeb/CSS/Rust/src/selector_engine.rs | 52 ++++++------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index f47ac71c16726..2d63db631c8e4 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -1601,10 +1601,6 @@ pub struct FfiElement { } impl FfiElement { - fn is_null(self) -> bool { - self.pointer.is_null() - } - fn qualified_name(self) -> FfiElementQualifiedName { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: The handle identifies a live DOM element for the duration of matching. @@ -2043,12 +2039,6 @@ pub struct FfiResolvedNamespace { pub namespace_: usize, } -impl FfiResolvedNamespace { - fn namespace(self) -> Option { - (self.namespace_type == FfiResolvedNamespaceType::Named).then_some(self.namespace_) - } -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum FfiNodeKind { Element, @@ -2063,7 +2053,6 @@ enum FfiNodeKind { struct FfiNode<'a> { pointer: *const c_void, kind: FfiNodeKind, - element: FfiElement, marker: PhantomData<&'a FfiCallScope>, } @@ -2083,7 +2072,7 @@ impl FfiNode<'_> { fn as_element(self) -> FfiElement { assert_eq!(self.kind, FfiNodeKind::Element); - self.element + FfiElement { pointer: self.pointer } } } @@ -2591,23 +2580,13 @@ impl<'a> FfiDom<'a> { (!pointer.is_null()).then_some(FfiNode { pointer, kind, - element: FfiElement { - pointer: std::ptr::null(), - }, marker: PhantomData, }) } unsafe fn element(&self, element: FfiElement) -> Option> { - if element.is_null() { - return None; - } - Some(FfiNode { - pointer: element.pointer, - kind: FfiNodeKind::Element, - element, - marker: PhantomData, - }) + // SAFETY: The caller guarantees that a non-null pointer identifies a live DOM element. + unsafe { self.node(element.pointer, FfiNodeKind::Element) } } unsafe fn scope(&self, scope: *const c_void) -> Option> { @@ -2643,9 +2622,9 @@ impl<'a> FfiDom<'a> { match namespace.namespace_type { FfiResolvedNamespaceType::Missing => false, FfiResolvedNamespaceType::Null => element.as_element().namespace_is_null(), - FfiResolvedNamespaceType::Named => namespace - .namespace() - .is_some_and(|namespace| element.as_element().qualified_name().namespace() == Some(namespace)), + FfiResolvedNamespaceType::Named => { + element.as_element().qualified_name().namespace() == Some(namespace.namespace_) + } } } } @@ -2760,13 +2739,16 @@ impl<'a> SelectorDom for FfiDom<'a> { // "|attr"). NamespaceType::Default | NamespaceType::None => dom_attribute.namespace().is_none(), NamespaceType::Any => true, - NamespaceType::Named => match resolved_namespace.unwrap().namespace_type { - FfiResolvedNamespaceType::Missing => false, - FfiResolvedNamespaceType::Null => dom_attribute.namespace().is_none(), - FfiResolvedNamespaceType::Named => { - dom_attribute.namespace() == resolved_namespace.unwrap().namespace() + NamespaceType::Named => { + let resolved_namespace = resolved_namespace.unwrap(); + match resolved_namespace.namespace_type { + FfiResolvedNamespaceType::Missing => false, + FfiResolvedNamespaceType::Null => dom_attribute.namespace().is_none(), + FfiResolvedNamespaceType::Named => { + dom_attribute.namespace() == Some(resolved_namespace.namespace_) + } } - }, + } }; namespace_matches && matches_attribute_value( @@ -3394,7 +3376,7 @@ pub unsafe extern "C" fn rust_selector_matches( crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMatchEntry); abort_on_panic(|| { assert!(!selector.is_null()); - assert!(!element.is_null()); + assert!(!element.pointer.is_null()); assert!(!context.is_null()); // SAFETY: The caller guarantees that the selector handle remains valid for this call. let selector = unsafe { &(*selector).selector }; @@ -3444,7 +3426,7 @@ pub unsafe extern "C" fn rust_selector_matches_originating_element( crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorMatchEntry); abort_on_panic(|| { assert!(!selector.is_null()); - assert!(!element.is_null()); + assert!(!element.pointer.is_null()); assert!(!context.is_null()); // SAFETY: The caller guarantees that the selector handle remains valid for this call. let selector = unsafe { &(*selector).selector }; From f71e80bd8855735da4988fdbcdf1b7a9dfc18634 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 22 Jul 2026 00:58:29 +0200 Subject: [PATCH 24/27] LibWeb: Remove the unused open-state parameter --- Libraries/LibWeb/CSS/SelectorMatching.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Libraries/LibWeb/CSS/SelectorMatching.cpp b/Libraries/LibWeb/CSS/SelectorMatching.cpp index d84693f623917..67af22bb170c3 100644 --- a/Libraries/LibWeb/CSS/SelectorMatching.cpp +++ b/Libraries/LibWeb/CSS/SelectorMatching.cpp @@ -285,7 +285,7 @@ static bool matches_read_write_pseudo_class(DOM::Element const& element) } // https://drafts.csswg.org/selectors-4/#open-state -static bool matches_open_state_pseudo_class(DOM::Element const& element, bool open) +static bool matches_open_state_pseudo_class(DOM::Element const& element) { // The :open pseudo-class represents an element that has both “open” and “closed” states, // and which is currently in the “open” state. @@ -295,13 +295,13 @@ static bool matches_open_state_pseudo_class(DOM::Element const& element, bool op // - details elements that have an open attribute // - dialog elements that have an open attribute if (is(element) || is(element)) - return open == element.has_attribute(HTML::AttributeNames::open); + return element.has_attribute(HTML::AttributeNames::open); // - select elements that are a drop-down box and whose drop-down boxes are open if (auto const* select = as_if(element)) - return open == select->is_open(); + return select->is_open(); // - input elements that support a picker and whose pickers are open if (auto const* input = as_if(element)) - return open == (input->supports_a_picker() && input->is_open()); + return input->supports_a_picker() && input->is_open(); return false; } @@ -781,7 +781,7 @@ extern "C" bool selector_ffi_element_is_modal(void const* element) extern "C" bool selector_ffi_element_is_open(void const* element) { - return matches_open_state_pseudo_class(ffi_element(element), true); + return matches_open_state_pseudo_class(ffi_element(element)); } // https://html.spec.whatwg.org/multipage/semantics-other.html#selector-optional From 94681bc2495703eaf74d0608a73324cd97d24c3e Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 22 Jul 2026 02:09:13 +0200 Subject: [PATCH 25/27] LibWeb: Match foreign type selectors case-sensitively 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. --- Libraries/LibWeb/CSS/Rust/src/selector_engine.rs | 14 +++++++++++++- .../css/selector-engine-characterization.txt | 2 +- ...l-mixed-case-element-name-selector-matching.txt | 1 + ...-mixed-case-element-name-selector-matching.html | 4 ++++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index 2d63db631c8e4..f59269f2f4068 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -2238,6 +2238,14 @@ fn utf16_equals_ignoring_ascii_case(first: DomStringView<'_>, second: &[u16]) -> .all(|(index, &second)| ascii_lowercase(first.code_unit_at(index)) == ascii_lowercase(second)) } +fn utf16_equals(first: DomStringView<'_>, second: &[u16]) -> bool { + first.len() == second.len() + && second + .iter() + .enumerate() + .all(|(index, &second)| first.code_unit_at(index) == second) +} + struct SelectorSubtags<'a> { value: &'a [u16], position: usize, @@ -2654,6 +2662,10 @@ impl<'a> SelectorDom for FfiDom<'a> { name: &QualifiedName, mode: TagNameMatchingMode, ) -> bool { + // https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors + // The same selector when compared to other elements must be compared according to its + // original case. In both cases, to match, the values must be identical to each other (and + // therefore the comparison is case sensitive). let ffi_element = element.as_element(); let is_html_element_in_html_document = ffi_element.is_html_element_in_html_document(); let name_matches = if is_html_element_in_html_document || mode == TagNameMatchingMode::Fast { @@ -2664,7 +2676,7 @@ impl<'a> SelectorDom for FfiDom<'a> { }; interned_name.is_some_and(|name| ffi_element.qualified_name().local_name() == Some(name)) } else { - utf16_equals_ignoring_ascii_case(element.local_name(), &name.name) + utf16_equals(element.local_name(), &name.name) }; if !name_matches { return false; diff --git a/Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt b/Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt index f591e422a6c93..1308d20c07531 100644 --- a/Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt +++ b/Tests/LibWeb/Text/expected/css/selector-engine-characterization.txt @@ -13,7 +13,7 @@ relative sibling: true closest: true query selector all: 3 XML tag case: true/false -XML slow tag case: true +XML slow tag case: false XML namespace wildcard: true XML attribute namespace wildcard: true host: yes diff --git a/Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt b/Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt index ff6e3f2858fa9..dfb05c596f350 100644 --- a/Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt +++ b/Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt @@ -2,3 +2,4 @@ ✅ Pass: Selector match for SVG element clipPath. ✅ Pass: Selector match for SVG element foreignObject. ✅ Pass: Selector match for SVG element radialGradient. +✅ Pass: Selector matching for SVG element linearGradient is case-sensitive. diff --git a/Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html b/Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html index f22888e8d0eaa..6cd9ac1bf2822 100644 --- a/Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html +++ b/Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html @@ -26,5 +26,9 @@ println("✅ Pass: Selector match for SVG element radialGradient."); else println("❌ Fail: No selector match for SVG element radialGradient."); + if (!document.querySelector("lineargradient:not(.missing)")) + println("✅ Pass: Selector matching for SVG element linearGradient is case-sensitive."); + else + println("❌ Fail: Selector matching for SVG element linearGradient is not case-sensitive."); }); From 5f9a9c2efe57df80d689dac053b3ad6509ca236c Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 22 Jul 2026 02:11:57 +0200 Subject: [PATCH 26/27] LibWeb: Match ID selectors case-insensitively in quirks mode 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. --- .../LibWeb/CSS/Rust/src/selector_engine.rs | 32 ++++++++++++++++--- Libraries/LibWeb/CSS/SelectorMatching.cpp | 11 +++++-- ...irks-mode-case-insensitive-id-selector.txt | 1 + ...rks-mode-case-insensitive-id-selector.html | 9 ++++++ 4 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/quirks-mode-case-insensitive-id-selector.txt create mode 100644 Tests/LibWeb/Text/input/quirks-mode-case-insensitive-id-selector.html diff --git a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs index f59269f2f4068..a39718494f8dd 100644 --- a/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs +++ b/Libraries/LibWeb/CSS/Rust/src/selector_engine.rs @@ -1614,10 +1614,21 @@ impl FfiElement { unsafe { selector_ffi_element_id(self.pointer).as_ref().copied() } } - fn class_names_are_case_insensitive(self) -> bool { + unsafe fn id_value<'a>(self) -> DomStringView<'a> { + crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); + // SAFETY: The handle identifies a live DOM element. C++ returns its current ID with + // backing storage owned by the element for this matching call. + DomStringView { + // SAFETY: C++ guarantees that the returned string view remains valid for matching. + view: unsafe { selector_ffi_element_id_value(self.pointer) }, + marker: PhantomData, + } + } + + fn id_and_class_names_are_case_insensitive(self) -> bool { crate::ffi_stats::bump(crate::ffi_stats::FfiOp::SelectorDomReadCallback); // SAFETY: The handle identifies a live DOM element for the duration of matching. - unsafe { selector_ffi_element_class_names_are_case_insensitive(self.pointer) } + unsafe { selector_ffi_element_id_and_class_names_are_case_insensitive(self.pointer) } } fn namespace_is_null(self) -> bool { @@ -2077,6 +2088,12 @@ impl FfiNode<'_> { } impl<'a> FfiNode<'a> { + fn id_value(self) -> DomStringView<'a> { + // SAFETY: `FfiNode` cannot outlive the call scope which pins the C++ element and its + // current ID storage. + unsafe { self.as_element().id_value() } + } + fn classes(self) -> &'a [usize] { // SAFETY: `FfiNode` cannot outlive the call scope which pins the C++ element and its // current class storage. @@ -2122,8 +2139,9 @@ pub struct FfiElementAndShadowHost { unsafe extern "C" { fn selector_ffi_element_qualified_name(element: *const c_void) -> FfiElementQualifiedName; fn selector_ffi_element_id(element: *const c_void) -> *const usize; + fn selector_ffi_element_id_value(element: *const c_void) -> FfiDomStringView; fn selector_ffi_element_classes(element: *const c_void) -> FfiInternedStringList; - fn selector_ffi_element_class_names_are_case_insensitive(element: *const c_void) -> bool; + fn selector_ffi_element_id_and_class_names_are_case_insensitive(element: *const c_void) -> bool; fn selector_ffi_element_namespace_is_null(element: *const c_void) -> bool; fn selector_ffi_element_is_html_element_in_html_document(element: *const c_void) -> bool; fn selector_ffi_element_is_document_root(element: *const c_void) -> bool; @@ -2685,12 +2703,16 @@ impl<'a> SelectorDom for FfiDom<'a> { } fn matches_id_selector(&mut self, element: FfiNode<'a>, id: &NameSelector) -> bool { - id.interned_name.is_some_and(|id| element.as_element().id() == Some(id)) + let ffi_element = element.as_element(); + if ffi_element.id_and_class_names_are_case_insensitive() { + return utf16_equals_ignoring_ascii_case(element.id_value(), &id.name); + } + id.interned_name.is_some_and(|id| ffi_element.id() == Some(id)) } fn matches_class_selector(&mut self, element: FfiNode<'a>, class_name: &NameSelector) -> bool { let ffi_element = element.as_element(); - if ffi_element.class_names_are_case_insensitive() { + if ffi_element.id_and_class_names_are_case_insensitive() { return (0..element.classes().len()) .any(|index| utf16_equals_ignoring_ascii_case(element.class_name(index), &class_name.name)); } diff --git a/Libraries/LibWeb/CSS/SelectorMatching.cpp b/Libraries/LibWeb/CSS/SelectorMatching.cpp index 67af22bb170c3..a61640d202660 100644 --- a/Libraries/LibWeb/CSS/SelectorMatching.cpp +++ b/Libraries/LibWeb/CSS/SelectorMatching.cpp @@ -382,8 +382,9 @@ using CSS::SelectorFFI::StringView; DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_qualified_name); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_id); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_id_value); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_classes); -DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_class_names_are_case_insensitive); +DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_id_and_class_names_are_case_insensitive); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_namespace_is_null); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_html_element_in_html_document); DECLARE_SELECTOR_FFI_CALLBACK(selector_ffi_element_is_document_root); @@ -501,6 +502,12 @@ extern "C" uintptr_t const* selector_ffi_element_id(void const* element) return id.has_value() ? reinterpret_cast(&id.value()) : nullptr; } +extern "C" CSS::SelectorFFI::DomStringView selector_ffi_element_id_value(void const* element) +{ + auto const& id = ffi_element(element).id(); + return id.has_value() ? dom_string_view(id.value()) : CSS::SelectorFFI::DomStringView {}; +} + extern "C" CSS::SelectorFFI::InternedStringList selector_ffi_element_classes(void const* element) { auto const& classes = ffi_element(element).class_names(); @@ -510,7 +517,7 @@ extern "C" CSS::SelectorFFI::InternedStringList selector_ffi_element_classes(voi }; } -extern "C" bool selector_ffi_element_class_names_are_case_insensitive(void const* element) +extern "C" bool selector_ffi_element_id_and_class_names_are_case_insensitive(void const* element) { return ffi_element(element).document().in_quirks_mode(); } diff --git a/Tests/LibWeb/Text/expected/quirks-mode-case-insensitive-id-selector.txt b/Tests/LibWeb/Text/expected/quirks-mode-case-insensitive-id-selector.txt new file mode 100644 index 0000000000000..7ab628d28f76a --- /dev/null +++ b/Tests/LibWeb/Text/expected/quirks-mode-case-insensitive-id-selector.txt @@ -0,0 +1 @@ +ParentNode.querySelector matches ID selectors case-insensitively in quirks mode: true diff --git a/Tests/LibWeb/Text/input/quirks-mode-case-insensitive-id-selector.html b/Tests/LibWeb/Text/input/quirks-mode-case-insensitive-id-selector.html new file mode 100644 index 0000000000000..ff77c9358a380 --- /dev/null +++ b/Tests/LibWeb/Text/input/quirks-mode-case-insensitive-id-selector.html @@ -0,0 +1,9 @@ + + +
+ From c1a7fd18f7d5680a881935db16665fc447594e48 Mon Sep 17 00:00:00 2001 From: Andreas Kling Date: Wed, 22 Jul 2026 02:31:11 +0200 Subject: [PATCH 27/27] LibWeb: Strengthen foreign type selector coverage Check exact-case compound matching before the wrong-case spelling. This prevents the negative assertion from passing when both selectors fail. --- .../non-html-mixed-case-element-name-selector-matching.txt | 1 + .../non-html-mixed-case-element-name-selector-matching.html | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt b/Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt index dfb05c596f350..74bca0e144853 100644 --- a/Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt +++ b/Tests/LibWeb/Text/expected/non-html-mixed-case-element-name-selector-matching.txt @@ -2,4 +2,5 @@ ✅ Pass: Selector match for SVG element clipPath. ✅ Pass: Selector match for SVG element foreignObject. ✅ Pass: Selector match for SVG element radialGradient. +✅ Pass: Exact-case selector match for SVG element linearGradient. ✅ Pass: Selector matching for SVG element linearGradient is case-sensitive. diff --git a/Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html b/Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html index 6cd9ac1bf2822..3dc4db2305c39 100644 --- a/Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html +++ b/Tests/LibWeb/Text/input/non-html-mixed-case-element-name-selector-matching.html @@ -26,6 +26,10 @@ println("✅ Pass: Selector match for SVG element radialGradient."); else println("❌ Fail: No selector match for SVG element radialGradient."); + if (document.querySelector("linearGradient:not(.missing)")) + println("✅ Pass: Exact-case selector match for SVG element linearGradient."); + else + println("❌ Fail: No exact-case selector match for SVG element linearGradient."); if (!document.querySelector("lineargradient:not(.missing)")) println("✅ Pass: Selector matching for SVG element linearGradient is case-sensitive."); else