From dae2fde161cacea2dba8dda1e3dbe732bb229c2f Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 2 Sep 2026 10:30:45 +0800 Subject: [PATCH] fix(selectors): use full validity state in DOM queries --- .../src/document_runtime/query_facade.rs | 179 +++++++++++++++--- .../src/native_bridge/element/forms.rs | 4 +- .../native_bridge/element/forms/validation.rs | 29 ++- .../src/native_bridge/element/query.rs | 81 ++++++-- .../src/script_vm/tests/dom_xhr/forms.rs | 55 ++++++ moli-selector/src/lib.rs | 84 +++++++- moli-selector/src/stylo.rs | 87 +++++++-- moli-selector/src/stylo/query.rs | 21 ++ moli-selector/src/stylo/query/element.rs | 12 ++ moli-selector/src/stylo/query/pseudo.rs | 11 +- 10 files changed, 494 insertions(+), 69 deletions(-) diff --git a/moli-renderer-v8/src/document_runtime/query_facade.rs b/moli-renderer-v8/src/document_runtime/query_facade.rs index 1cc46afc02..74c2b52837 100644 --- a/moli-renderer-v8/src/document_runtime/query_facade.rs +++ b/moli-renderer-v8/src/document_runtime/query_facade.rs @@ -20,26 +20,72 @@ impl DocumentRuntime { &self, root: Option, selector: &str, + ) -> Result, SelectorError> { + self.query_selector_with_optional_validity_states(root, selector, None) + } + + pub(crate) fn query_selector_with_validity_states( + &self, + root: Option, + selector: &str, + validity_states: &HashMap, + ) -> Result, SelectorError> { + self.query_selector_with_optional_validity_states(root, selector, Some(validity_states)) + } + + fn query_selector_with_optional_validity_states( + &self, + root: Option, + selector: &str, + validity_states: Option<&HashMap>, ) -> Result, SelectorError> { self.selector_debug.record_query_selector(); trace_hot_selector_api("querySelector", root, selector); match root { Some(root) => { if self.dom_host.node(root).is_some_and(Node::is_document) { - let handles = self.selector_engine.query_selector_all_in_host( - &self.dom_host, - root, - selector, - )?; + let handles = match validity_states { + Some(states) => self + .selector_engine + .query_selector_all_in_host_with_validity_states( + &self.dom_host, + root, + selector, + states, + )?, + None => self.selector_engine.query_selector_all_in_host( + &self.dom_host, + root, + selector, + )?, + }; Ok(handles.into_iter().next()) } else { - self.selector_engine - .query_selector_in_host(&self.dom_host, root, selector) + match validity_states { + Some(states) => self + .selector_engine + .query_selector_in_host_with_validity_states( + &self.dom_host, + root, + selector, + states, + ), + None => self.selector_engine.query_selector_in_host( + &self.dom_host, + root, + selector, + ), + } } } - None => self - .selector_engine - .query_selector_host(&self.dom_host, selector), + None => match validity_states { + Some(states) => self + .selector_engine + .query_selector_host_with_validity_states(&self.dom_host, selector, states), + None => self + .selector_engine + .query_selector_host(&self.dom_host, selector), + }, } } @@ -47,40 +93,123 @@ impl DocumentRuntime { &self, root: Option, selector: &str, + ) -> Result, SelectorError> { + self.query_selector_all_with_optional_validity_states(root, selector, None) + } + + pub(crate) fn query_selector_all_with_validity_states( + &self, + root: Option, + selector: &str, + validity_states: &HashMap, + ) -> Result, SelectorError> { + self.query_selector_all_with_optional_validity_states(root, selector, Some(validity_states)) + } + + fn query_selector_all_with_optional_validity_states( + &self, + root: Option, + selector: &str, + validity_states: Option<&HashMap>, ) -> Result, SelectorError> { self.selector_debug.record_query_selector_all(); trace_hot_selector_api("querySelectorAll", root, selector); match root { - Some(root) => { - let handles = self.selector_engine.query_selector_all_in_host( - &self.dom_host, - root, - selector, - )?; - Ok(handles) - } - None => self - .selector_engine - .query_selector_all_host(&self.dom_host, selector), + Some(root) => match validity_states { + Some(states) => self + .selector_engine + .query_selector_all_in_host_with_validity_states( + &self.dom_host, + root, + selector, + states, + ), + None => { + self.selector_engine + .query_selector_all_in_host(&self.dom_host, root, selector) + } + }, + None => match validity_states { + Some(states) => self + .selector_engine + .query_selector_all_host_with_validity_states(&self.dom_host, selector, states), + None => self + .selector_engine + .query_selector_all_host(&self.dom_host, selector), + }, } } pub(crate) fn matches(&self, node: DomHandle, selector: &str) -> Result { + self.matches_with_optional_validity_states(node, selector, None) + } + + pub(crate) fn matches_with_validity_states( + &self, + node: DomHandle, + selector: &str, + validity_states: &HashMap, + ) -> Result { + self.matches_with_optional_validity_states(node, selector, Some(validity_states)) + } + + fn matches_with_optional_validity_states( + &self, + node: DomHandle, + selector: &str, + validity_states: Option<&HashMap>, + ) -> Result { self.selector_debug.record_matches(); trace_hot_selector_api("matches", Some(node), selector); - self.selector_engine - .matches_host(&self.dom_host, node, selector) + match validity_states { + Some(states) => self.selector_engine.matches_host_with_validity_states( + &self.dom_host, + node, + selector, + states, + ), + None => self + .selector_engine + .matches_host(&self.dom_host, node, selector), + } } pub(crate) fn closest( &self, node: DomHandle, selector: &str, + ) -> Result, SelectorError> { + self.closest_with_optional_validity_states(node, selector, None) + } + + pub(crate) fn closest_with_validity_states( + &self, + node: DomHandle, + selector: &str, + validity_states: &HashMap, + ) -> Result, SelectorError> { + self.closest_with_optional_validity_states(node, selector, Some(validity_states)) + } + + fn closest_with_optional_validity_states( + &self, + node: DomHandle, + selector: &str, + validity_states: Option<&HashMap>, ) -> Result, SelectorError> { self.selector_debug.record_closest(); trace_hot_selector_api("closest", Some(node), selector); - self.selector_engine - .closest_host(&self.dom_host, node, selector) + match validity_states { + Some(states) => self.selector_engine.closest_host_with_validity_states( + &self.dom_host, + node, + selector, + states, + ), + None => self + .selector_engine + .closest_host(&self.dom_host, node, selector), + } } pub(crate) fn selector_debug_snapshot(&self) -> SelectorDebugSnapshot { diff --git a/moli-renderer-v8/src/native_bridge/element/forms.rs b/moli-renderer-v8/src/native_bridge/element/forms.rs index 6d395bfbb8..52ae47f6af 100644 --- a/moli-renderer-v8/src/native_bridge/element/forms.rs +++ b/moli-renderer-v8/src/native_bridge/element/forms.rs @@ -253,8 +253,8 @@ pub(in crate::native_bridge) use self::validation::{ control_check_validity_callback, control_matches_validity_pseudo, control_report_validity_callback, control_set_custom_validity_callback, control_validation_message_getter_function, control_validity_getter_function, - control_will_validate_getter_function, dispatch_invalid_event, form_check_validity_callback, - form_report_validity_callback, form_validate_for_submission, + control_validity_pseudo_state, control_will_validate_getter_function, dispatch_invalid_event, + form_check_validity_callback, form_report_validity_callback, form_validate_for_submission, }; #[derive(strum::EnumString, strum::IntoStaticStr)] diff --git a/moli-renderer-v8/src/native_bridge/element/forms/validation.rs b/moli-renderer-v8/src/native_bridge/element/forms/validation.rs index d02925516f..9202d527e3 100644 --- a/moli-renderer-v8/src/native_bridge/element/forms/validation.rs +++ b/moli-renderer-v8/src/native_bridge/element/forms/validation.rs @@ -240,6 +240,20 @@ pub(in crate::native_bridge) fn control_matches_validity_pseudo( if !matches!(selector, ":valid" | ":invalid") { return None; } + let invalid = control_validity_pseudo_state(scope, runtime_ptr, handle)?; + Some(if selector == ":valid" { + !invalid + } else { + invalid + }) +} + +pub(in crate::native_bridge) fn control_validity_pseudo_state( + scope: &mut v8::PinScope<'_, '_>, + runtime_ptr: *mut JsContextHost, + handle: DomHandle, +) -> Option { + let runtime = unsafe { &*runtime_ptr }; if runtime .dom_host() .node(handle) @@ -250,24 +264,21 @@ pub(in crate::native_bridge) fn control_matches_validity_pseudo( let valid = controls .into_iter() .all(|control| control_satisfies_constraints(scope, runtime_ptr, control)); - return Some(if selector == ":valid" { valid } else { !valid }); + return Some(!valid); } - let Some(element) = runtime.dom_host().node(handle).and_then(Node::as_element) else { - return Some(false); - }; + let element = runtime.dom_host().node(handle).and_then(Node::as_element)?; if is_form_associated_custom_element_handle(runtime, handle) { if !control_will_validate(runtime, handle) { - return Some(false); + return None; } - let valid = control_validity(scope, runtime_ptr, handle).valid(); - return Some(if selector == ":valid" { valid } else { !valid }); + return Some(!control_validity(scope, runtime_ptr, handle).valid()); } if !element_matches_validity_pseudo(runtime, handle, element) { - return Some(false); + return None; } let valid = control_is_readonly_barred_from_constraint_validation(element) || control_validity(scope, runtime_ptr, handle).valid(); - Some(if selector == ":valid" { valid } else { !valid }) + Some(!valid) } pub(in crate::native_bridge) fn control_validation_message_getter_function<'s>( diff --git a/moli-renderer-v8/src/native_bridge/element/query.rs b/moli-renderer-v8/src/native_bridge/element/query.rs index 7321161405..e8ed22f8dd 100644 --- a/moli-renderer-v8/src/native_bridge/element/query.rs +++ b/moli-renderer-v8/src/native_bridge/element/query.rs @@ -1,5 +1,6 @@ use super::super::{ - CollectionKind, LiveCollectionQueryKind, collections, encode_tag_name_ns_query, + CollectionKind, DomHandle, JsContextHost, LiveCollectionQueryKind, collections, + encode_tag_name_ns_query, node::{ node_is_document, node_runtime_and_handle_from_args, node_runtime_and_handle_from_args_or_detached, @@ -8,9 +9,12 @@ use super::super::{ throw_incompatible_method_receiver, throw_native_selector_error_for_selector, }, }; -use super::forms::control_matches_validity_pseudo; +use std::collections::HashMap; + +use super::forms::{control_matches_validity_pseudo, control_validity_pseudo_state}; use crate::native_bridge::set_wrapped_handle_or_null_for_receiver; use crate::{ + dom::native::Node, util::{ call_object_method, object_number_property, object_property_as_object, v8_string, v8str, }, @@ -119,14 +123,17 @@ pub(in crate::native_bridge) fn node_query_selector_callback<'s>( let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - match unsafe { &*runtime_ptr }.query_selector(Some(handle), &parsed.selectors) { - Ok(handle) => set_wrapped_handle_or_null_for_receiver( - scope, - &mut rv, - runtime_ptr, - args.this(), - handle, + let validity_states = selector_validity_states(scope, runtime_ptr, &parsed.selectors); + let result = match validity_states.as_ref() { + Some(states) => unsafe { &*runtime_ptr }.query_selector_with_validity_states( + Some(handle), + &parsed.selectors, + states, ), + None => unsafe { &*runtime_ptr }.query_selector(Some(handle), &parsed.selectors), + }; + match result { + Ok(handle) => set_wrapped_handle_or_null_for_receiver(scope, &mut rv, runtime_ptr, args.this(), handle), Err(error) => throw_native_selector_error_for_selector(scope, &parsed.selectors, &error), } } @@ -175,7 +182,16 @@ pub(in crate::native_bridge) fn node_query_selector_all_callback<'s>( let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - match unsafe { &*runtime_ptr }.query_selector_all(Some(handle), &parsed.selectors) { + let validity_states = selector_validity_states(scope, runtime_ptr, &parsed.selectors); + let result = match validity_states.as_ref() { + Some(states) => unsafe { &*runtime_ptr }.query_selector_all_with_validity_states( + Some(handle), + &parsed.selectors, + states, + ), + None => unsafe { &*runtime_ptr }.query_selector_all(Some(handle), &parsed.selectors), + }; + match result { Ok(handles) => { let Some(context) = args.this().get_creation_context(scope) else { rv.set_null(); @@ -234,8 +250,13 @@ pub(in crate::native_bridge) fn node_matches_callback<'s>( rv.set_bool(is_match); return; } + let validity_states = selector_validity_states(scope, runtime_ptr, &parsed.selectors); let runtime = unsafe { &*runtime_ptr }; - match runtime.matches(handle, &parsed.selectors) { + let result = match validity_states.as_ref() { + Some(states) => runtime.matches_with_validity_states(handle, &parsed.selectors, states), + None => runtime.matches(handle, &parsed.selectors), + }; + match result { Ok(true) => rv.set_bool(true), Ok(false) if node_matches_needs_owner_document_query(scope, args.this()) => rv.set_bool( node_matches_owner_document_query(scope, args.this(), &parsed.selectors), @@ -320,7 +341,14 @@ pub(in crate::native_bridge) fn node_closest_callback<'s>( let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; - match unsafe { &*runtime_ptr }.closest(handle, &parsed.selectors) { + let validity_states = selector_validity_states(scope, runtime_ptr, &parsed.selectors); + let result = match validity_states.as_ref() { + Some(states) => { + unsafe { &*runtime_ptr }.closest_with_validity_states(handle, &parsed.selectors, states) + } + None => unsafe { &*runtime_ptr }.closest(handle, &parsed.selectors), + }; + match result { Ok(Some(handle)) if receiver_is_detached => { match super::super::document::detached_native_object_for_handle( scope, @@ -342,6 +370,35 @@ pub(in crate::native_bridge) fn node_closest_callback<'s>( } } +fn selector_validity_states( + scope: &mut v8::PinScope<'_, '_>, + runtime_ptr: *mut JsContextHost, + selector: &str, +) -> Option> { + if !crate::selector::dom_api_selector_uses_validity_pseudo( + unsafe { &*runtime_ptr }.dom_host(), + selector, + ) { + return None; + } + let handles = unsafe { &*runtime_ptr } + .dom_host() + .dom() + .nodes() + .filter(|node| node.is_element()) + .map(Node::id) + .collect::>(); + Some( + handles + .into_iter() + .filter_map(|handle| { + control_validity_pseudo_state(scope, runtime_ptr, handle) + .map(|invalid| (handle, invalid)) + }) + .collect(), + ) +} + pub(in crate::native_bridge) fn node_get_elements_by_tag_name_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs index 1ddf7097a7..fd0366d3a6 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/forms.rs @@ -540,6 +540,61 @@ fn live_input_pattern_uses_native_regexp_after_global_mutation() { assert_eq!(result, "false/true|true/false|false/true"); } + +#[test] +fn live_input_pattern_drives_dom_selector_apis() { + let mut vm = new_storage_test_vm("https://live-pattern-selectors.test/"); + + let result = vm + .eval( + r#" +(() => { + const root = document.createElement('div'); + root.id = 'patterns'; + root.innerHTML = + '' + + ''; + const ids = selector => + [...root.querySelectorAll(selector)].map(element => element.id).join(','); + const bad = root.querySelector('#bad'); + const good = root.querySelector('#good'); + const initial = [ + ids(':valid'), + ids(String.raw`:\76 alid`), + ids(':invalid'), + root.querySelector('input:invalid').id, + bad.matches('input#bad:invalid'), + good.matches('#good:valid'), + bad.closest('div:has(> input#bad:invalid)').id + ]; + + bad.value = '0BBB'; + good.value = 'BBB'; + + const advanced = document.createElement('input'); + advanced.id = 'advanced'; + advanced.pattern = '([a-z])\\1'; + advanced.value = 'ab'; + root.append(advanced); + + return [ + ...initial, + ids(':valid'), + ids(':invalid'), + advanced.matches('#advanced:invalid'), + root.querySelector('#advanced:invalid').id + ].join('|'); +})() +"#, + ) + .expect("live input patterns should drive DOM selector APIs"); + + assert_eq!( + result, + "good|good|bad|bad|true|true|patterns|bad|good,advanced|true|advanced" + ); +} + #[test] fn detached_select_and_option_track_selection_state() { let mut vm = new_storage_test_vm("https://detached-select-state.test/path/page.html"); diff --git a/moli-selector/src/lib.rs b/moli-selector/src/lib.rs index e6af35c4db..c560b7d458 100644 --- a/moli-selector/src/lib.rs +++ b/moli-selector/src/lib.rs @@ -56,7 +56,8 @@ pub use stylo::{ StyloSourceStyleInvalidationTargetResultRecord, StyloStateInvalidationRoot, StyloStyleInvalidationQuery, StyloStyleInvalidationSnapshot, StyloStyleInvalidationSnapshotAttribute, StyloStyleSourceScope, - StyloStylesheetSourceScopeFallbackInput, is_svg_presentation_attribute_name, + StyloStylesheetSourceScopeFallbackInput, dom_api_selector_uses_validity_pseudo, + is_svg_presentation_attribute_name, stylo_attribute_change_can_skip_fallback_without_dependency, stylo_attribute_change_can_use_retained_invalidator, stylo_element_dependency_snapshot, stylo_fallback_roots_plan, stylo_flat_tree_heading_descendants, @@ -80,7 +81,7 @@ pub use stylo::{ use moli_dom::native::DomHost; use moli_dom::{NodeId, native::NativeDom}; -use std::sync::OnceLock; +use std::{collections::HashMap, sync::OnceLock}; // Temporary self-alias so the extracted module tree can keep compiling while // `moli` keeps its historical `crate::selector::*` references. @@ -147,7 +148,17 @@ impl QueryEngine { host: &DomHost, selector: &str, ) -> Result, SelectorError> { - self.adapter().query_selector(host, selector) + self.adapter().query_selector(host, selector, None) + } + + pub fn query_selector_host_with_validity_states( + &self, + host: &DomHost, + selector: &str, + validity_states: &HashMap, + ) -> Result, SelectorError> { + self.adapter() + .query_selector(host, selector, Some(validity_states)) } pub fn query_selector_all_host( @@ -155,7 +166,17 @@ impl QueryEngine { host: &DomHost, selector: &str, ) -> Result, SelectorError> { - self.adapter().query_selector_all(host, selector) + self.adapter().query_selector_all(host, selector, None) + } + + pub fn query_selector_all_host_with_validity_states( + &self, + host: &DomHost, + selector: &str, + validity_states: &HashMap, + ) -> Result, SelectorError> { + self.adapter() + .query_selector_all(host, selector, Some(validity_states)) } pub fn query_selector_in_host( @@ -164,7 +185,18 @@ impl QueryEngine { root: NodeId, selector: &str, ) -> Result, SelectorError> { - self.adapter().query_selector_in(host, root, selector) + self.adapter().query_selector_in(host, root, selector, None) + } + + pub fn query_selector_in_host_with_validity_states( + &self, + host: &DomHost, + root: NodeId, + selector: &str, + validity_states: &HashMap, + ) -> Result, SelectorError> { + self.adapter() + .query_selector_in(host, root, selector, Some(validity_states)) } pub fn query_selector_all_in_host( @@ -173,7 +205,19 @@ impl QueryEngine { root: NodeId, selector: &str, ) -> Result, SelectorError> { - self.adapter().query_selector_all_in(host, root, selector) + self.adapter() + .query_selector_all_in(host, root, selector, None) + } + + pub fn query_selector_all_in_host_with_validity_states( + &self, + host: &DomHost, + root: NodeId, + selector: &str, + validity_states: &HashMap, + ) -> Result, SelectorError> { + self.adapter() + .query_selector_all_in(host, root, selector, Some(validity_states)) } pub fn matches_host( @@ -182,7 +226,18 @@ impl QueryEngine { node_id: NodeId, selector: &str, ) -> Result { - self.adapter().matches(host, node_id, selector) + self.adapter().matches(host, node_id, selector, None) + } + + pub fn matches_host_with_validity_states( + &self, + host: &DomHost, + node_id: NodeId, + selector: &str, + validity_states: &HashMap, + ) -> Result { + self.adapter() + .matches(host, node_id, selector, Some(validity_states)) } pub fn matches_with_scope_host( @@ -193,7 +248,7 @@ impl QueryEngine { scope_root: NodeId, ) -> Result { self.adapter() - .matches_with_scope(host, node_id, selector, scope_root) + .matches_with_scope(host, node_id, selector, scope_root, None) } pub fn closest_host( @@ -202,7 +257,18 @@ impl QueryEngine { start: NodeId, selector: &str, ) -> Result, SelectorError> { - self.adapter().closest(host, start, selector) + self.adapter().closest(host, start, selector, None) + } + + pub fn closest_host_with_validity_states( + &self, + host: &DomHost, + start: NodeId, + selector: &str, + validity_states: &HashMap, + ) -> Result, SelectorError> { + self.adapter() + .closest(host, start, selector, Some(validity_states)) } pub fn query_selector( diff --git a/moli-selector/src/stylo.rs b/moli-selector/src/stylo.rs index 3074300cff..f41a1d172f 100644 --- a/moli-selector/src/stylo.rs +++ b/moli-selector/src/stylo.rs @@ -1,9 +1,17 @@ -use selectors::{Element as SelectorsElement, matching::MatchingContext}; +use std::collections::HashMap; + +use selectors::{ + Element as SelectorsElement, + matching::MatchingContext, + parser::{Component, RelativeSelector}, + visitor::SelectorVisitor, +}; use style::{ dom::TDocument, dom_apis::{ MayUseInvalidation, QueryAll, QueryFirst, element_closest, element_matches, query_selector, }, + selector_parser::{NonTSPseudoClass, SelectorImpl}, shared_lock::SharedRwLock, }; @@ -21,6 +29,45 @@ pub fn stylo_flat_tree_heading_descendants(host: &DomHost, root: NodeId) -> Vec< query::flat_tree_heading_descendants(host, root) } +/// Reports whether a DOM API selector contains `:valid` or `:invalid`, +/// including escaped names and pseudos nested inside functional selectors. +pub fn dom_api_selector_uses_validity_pseudo(host: &DomHost, selector: &str) -> bool { + let Ok(ParsedDomApiSelectorList::Parsed(selectors)) = + parse_dom_api_selector_list(host, selector) + else { + return false; + }; + let mut visitor = ValidityPseudoVisitor { found: false }; + for selector in selectors.slice() { + if !selector.visit(&mut visitor) { + break; + } + } + visitor.found +} + +struct ValidityPseudoVisitor { + found: bool, +} + +impl SelectorVisitor for ValidityPseudoVisitor { + type Impl = SelectorImpl; + + fn visit_simple_selector(&mut self, component: &Component) -> bool { + self.found = matches!( + component, + Component::NonTSPseudoClass(NonTSPseudoClass::Valid | NonTSPseudoClass::Invalid) + ); + !self.found + } + + fn visit_relative_selector_list(&mut self, selectors: &[RelativeSelector]) -> bool { + selectors + .iter() + .all(|selector| selector.selector.visit(self)) + } +} + mod atoms; mod invalidation; mod presentation; @@ -136,6 +183,7 @@ impl StyloDomApiAdapter { &'a self, host: &'a DomHost, atom_cache: &'a QueryAtomCache, + validity_states: Option<&'a HashMap>, ) -> QueryDocument<'a> { QueryDocument::new( host, @@ -143,6 +191,7 @@ impl StyloDomApiAdapter { &self.shared_lock, None, atom_cache, + validity_states, ) } @@ -151,15 +200,25 @@ impl StyloDomApiAdapter { host: &'a DomHost, handle: NodeId, atom_cache: &'a QueryAtomCache, + validity_states: Option<&'a HashMap>, ) -> Option> { - host.node(handle) - .map(|_| QueryNode::new(host, handle, &self.shared_lock, None, atom_cache)) + host.node(handle).map(|_| { + QueryNode::new( + host, + handle, + &self.shared_lock, + None, + atom_cache, + validity_states, + ) + }) } pub(super) fn query_selector_all( &self, host: &DomHost, selector: &str, + validity_states: Option<&HashMap>, ) -> Result, SelectorError> { let atom_cache = QueryAtomCache::default(); let selector_list = match parse_dom_api_selector_list(host, selector)? { @@ -169,7 +228,7 @@ impl StyloDomApiAdapter { let mut results = >>::Output::default(); query_selector::, QueryAll>( - self.document(host, &atom_cache).as_node(), + self.document(host, &atom_cache, validity_states).as_node(), &selector_list, &mut results, MayUseInvalidation::No, @@ -182,6 +241,7 @@ impl StyloDomApiAdapter { host: &DomHost, root: NodeId, selector: &str, + validity_states: Option<&HashMap>, ) -> Result, SelectorError> { if host.node(root).is_none() { return Ok(Vec::new()); @@ -191,7 +251,7 @@ impl StyloDomApiAdapter { ParsedDomApiSelectorList::EmptyKnownPseudoElement => return Ok(Vec::new()), ParsedDomApiSelectorList::Parsed(selector_list) => selector_list, }; - let Some(root) = self.node(host, root, &atom_cache) else { + let Some(root) = self.node(host, root, &atom_cache, validity_states) else { return Ok(Vec::new()); }; let mut results = @@ -209,6 +269,7 @@ impl StyloDomApiAdapter { &self, host: &DomHost, selector: &str, + validity_states: Option<&HashMap>, ) -> Result, SelectorError> { let atom_cache = QueryAtomCache::default(); let selector_list = match parse_dom_api_selector_list(host, selector)? { @@ -218,7 +279,7 @@ impl StyloDomApiAdapter { let mut result = >>::Output::default(); query_selector::, QueryFirst>( - self.document(host, &atom_cache).as_node(), + self.document(host, &atom_cache, validity_states).as_node(), &selector_list, &mut result, MayUseInvalidation::No, @@ -231,6 +292,7 @@ impl StyloDomApiAdapter { host: &DomHost, root: NodeId, selector: &str, + validity_states: Option<&HashMap>, ) -> Result, SelectorError> { if host.node(root).is_none() { return Ok(None); @@ -240,7 +302,7 @@ impl StyloDomApiAdapter { ParsedDomApiSelectorList::EmptyKnownPseudoElement => return Ok(None), ParsedDomApiSelectorList::Parsed(selector_list) => selector_list, }; - let Some(root) = self.node(host, root, &atom_cache) else { + let Some(root) = self.node(host, root, &atom_cache, validity_states) else { return Ok(None); }; let mut result = @@ -259,6 +321,7 @@ impl StyloDomApiAdapter { host: &DomHost, handle: NodeId, selector: &str, + validity_states: Option<&HashMap>, ) -> Result { let atom_cache = QueryAtomCache::default(); let selector_list = match parse_dom_api_selector_list(host, selector)? { @@ -266,7 +329,7 @@ impl StyloDomApiAdapter { ParsedDomApiSelectorList::Parsed(selector_list) => selector_list, }; let Some(element) = self - .node(host, handle, &atom_cache) + .node(host, handle, &atom_cache, validity_states) .and_then(QueryNode::as_element) else { return Ok(false); @@ -284,6 +347,7 @@ impl StyloDomApiAdapter { handle: NodeId, selector: &str, scope_root: NodeId, + validity_states: Option<&HashMap>, ) -> Result { let selector_list = match parse_dom_api_selector_list(host, selector)? { ParsedDomApiSelectorList::EmptyKnownPseudoElement => return Ok(false), @@ -291,13 +355,13 @@ impl StyloDomApiAdapter { }; let atom_cache = QueryAtomCache::default(); let Some(element) = self - .node(host, handle, &atom_cache) + .node(host, handle, &atom_cache, validity_states) .and_then(QueryNode::as_element) else { return Ok(false); }; let Some(scope_element) = self - .node(host, scope_root, &atom_cache) + .node(host, scope_root, &atom_cache, validity_states) .and_then(QueryNode::as_element) else { return Ok(false); @@ -325,6 +389,7 @@ impl StyloDomApiAdapter { host: &DomHost, handle: NodeId, selector: &str, + validity_states: Option<&HashMap>, ) -> Result, SelectorError> { let atom_cache = QueryAtomCache::default(); let selector_list = match parse_dom_api_selector_list(host, selector)? { @@ -332,7 +397,7 @@ impl StyloDomApiAdapter { ParsedDomApiSelectorList::Parsed(selector_list) => selector_list, }; let Some(element) = self - .node(host, handle, &atom_cache) + .node(host, handle, &atom_cache, validity_states) .and_then(QueryNode::as_element) else { return Ok(None); diff --git a/moli-selector/src/stylo/query.rs b/moli-selector/src/stylo/query.rs index b49e98021e..2b46c21e31 100644 --- a/moli-selector/src/stylo/query.rs +++ b/moli-selector/src/stylo/query.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, fmt, hash::{Hash, Hasher}, ptr::NonNull, @@ -44,6 +45,7 @@ pub(in crate::stylo) struct QueryNode<'a> { shared_lock: &'a SharedRwLock, style_data: Option<&'a StyloElementDataStore>, atom_cache: &'a QueryAtomCache, + validity_states: Option<&'a HashMap>, } impl fmt::Debug for QueryNode<'_> { @@ -74,6 +76,7 @@ impl<'a> QueryNode<'a> { shared_lock: &'a SharedRwLock, style_data: Option<&'a StyloElementDataStore>, atom_cache: &'a QueryAtomCache, + validity_states: Option<&'a HashMap>, ) -> Self { Self { host, @@ -81,6 +84,7 @@ impl<'a> QueryNode<'a> { shared_lock, style_data, atom_cache, + validity_states, } } @@ -100,6 +104,7 @@ impl<'a> QueryNode<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -111,6 +116,7 @@ impl<'a> QueryNode<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -123,6 +129,7 @@ impl<'a> QueryNode<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -140,6 +147,7 @@ pub(in crate::stylo) struct QueryElement<'a> { shared_lock: &'a SharedRwLock, style_data: Option<&'a StyloElementDataStore>, atom_cache: &'a QueryAtomCache, + validity_states: Option<&'a HashMap>, } impl fmt::Debug for QueryElement<'_> { @@ -170,6 +178,7 @@ pub(in crate::stylo) struct QueryDocument<'a> { shared_lock: &'a SharedRwLock, style_data: Option<&'a StyloElementDataStore>, atom_cache: &'a QueryAtomCache, + validity_states: Option<&'a HashMap>, } impl fmt::Debug for QueryDocument<'_> { @@ -200,6 +209,7 @@ impl<'a> QueryDocument<'a> { shared_lock: &'a SharedRwLock, style_data: Option<&'a StyloElementDataStore>, atom_cache: &'a QueryAtomCache, + validity_states: Option<&'a HashMap>, ) -> Self { Self { host, @@ -207,6 +217,7 @@ impl<'a> QueryDocument<'a> { shared_lock, style_data, atom_cache, + validity_states, } } @@ -226,6 +237,7 @@ pub(in crate::stylo) struct QueryShadowRoot<'a> { shared_lock: &'a SharedRwLock, style_data: Option<&'a StyloElementDataStore>, atom_cache: &'a QueryAtomCache, + validity_states: Option<&'a HashMap>, } impl fmt::Debug for QueryShadowRoot<'_> { @@ -269,6 +281,7 @@ impl<'a> TDocument for QueryDocument<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, } } @@ -300,6 +313,7 @@ impl<'a> TNode for QueryNode<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -310,6 +324,7 @@ impl<'a> TNode for QueryNode<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -320,6 +335,7 @@ impl<'a> TNode for QueryNode<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -330,6 +346,7 @@ impl<'a> TNode for QueryNode<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -340,6 +357,7 @@ impl<'a> TNode for QueryNode<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -354,6 +372,7 @@ impl<'a> TNode for QueryNode<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, } } @@ -401,6 +420,7 @@ impl<'a> TShadowRoot for QueryShadowRoot<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, } } @@ -415,6 +435,7 @@ impl<'a> TShadowRoot for QueryShadowRoot<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, } } diff --git a/moli-selector/src/stylo/query/element.rs b/moli-selector/src/stylo/query/element.rs index ace6c3283c..f657e1bc91 100644 --- a/moli-selector/src/stylo/query/element.rs +++ b/moli-selector/src/stylo/query/element.rs @@ -53,6 +53,7 @@ impl<'a> QueryElement<'a> { shared_lock, style_data, atom_cache, + validity_states: None, } } @@ -148,6 +149,7 @@ impl<'a> TElement for QueryElement<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, } } @@ -161,6 +163,7 @@ impl<'a> TElement for QueryElement<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) .collect::>(); LayoutIterator(children.into_iter()) @@ -349,6 +352,7 @@ impl<'a> TElement for QueryElement<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -361,6 +365,7 @@ impl<'a> TElement for QueryElement<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -395,6 +400,7 @@ impl<'a> TElement for QueryElement<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) }); } @@ -462,6 +468,7 @@ impl SelectorsElement for QueryElement<'_> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -480,6 +487,7 @@ impl SelectorsElement for QueryElement<'_> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -496,6 +504,7 @@ impl SelectorsElement for QueryElement<'_> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -508,6 +517,7 @@ impl SelectorsElement for QueryElement<'_> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -520,6 +530,7 @@ impl SelectorsElement for QueryElement<'_> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } @@ -534,6 +545,7 @@ impl SelectorsElement for QueryElement<'_> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) } diff --git a/moli-selector/src/stylo/query/pseudo.rs b/moli-selector/src/stylo/query/pseudo.rs index 9f21d4b35d..17119f5808 100644 --- a/moli-selector/src/stylo/query/pseudo.rs +++ b/moli-selector/src/stylo/query/pseudo.rs @@ -130,6 +130,7 @@ impl<'a> QueryElement<'a> { shared_lock: self.shared_lock, style_data: self.style_data, atom_cache: self.atom_cache, + validity_states: self.validity_states, }) .is_locally_invalid() { @@ -140,6 +141,12 @@ impl<'a> QueryElement<'a> { } pub(super) fn is_invalid(self) -> bool { + if let Some(invalid) = self + .validity_states + .and_then(|states| states.get(&self.handle)) + { + return *invalid; + } if self.is_locally_invalid() { return true; } @@ -434,7 +441,9 @@ impl<'a> QueryElement<'a> { } pub(super) fn matches_validity_pseudo(self) -> bool { - matches!(self.element().local_name(), "form" | "fieldset") + self.validity_states + .is_some_and(|states| states.contains_key(&self.handle)) + || matches!(self.element().local_name(), "form" | "fieldset") || self.is_constraint_validation_candidate() }