diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index 3bdd166db8..85958da1e4 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -2549,7 +2549,6 @@ css/selectors/parsing/parse-is-where.html css/selectors/parsing/parse-part.html css/selectors/parsing/parse-slotted.html css/selectors/selectors-4/lang-singleton-subtag-matching.html -css/selectors/webkit-pseudo-element.html custom-elements/parser/parser-uses-registry-of-owner-document.html custom-elements/perform-microtask-checkpoint-before-construction.html custom-elements/reactions/Document.html @@ -3233,7 +3232,6 @@ html/semantics/scripting-1/the-template-element/template-element/node-document-c html/semantics/scripting-1/the-template-element/template-element/template-content-hierarcy.html html/semantics/scripting-1/the-template-element/template-element/template-content-node-document.html html/semantics/sections/headingoffset-and-headingreset.html -html/semantics/selectors/pseudo-classes/autofill.html html/semantics/selectors/pseudo-classes/dir.html html/semantics/selectors/pseudo-classes/readwrite-readonly.html html/semantics/selectors/pseudo-classes/valid-invalid.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 83c54f8b73..4a072d0801 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -3303,6 +3303,7 @@ css/selectors/selector-after-font-family.html css/selectors/selector-placeholder-shown-emptify-placeholder.html css/selectors/selectors-attr-many-2.html css/selectors/selectors-case-sensitive-001.html +css/selectors/webkit-pseudo-element.html css/selectors/x-pseudo-element.html custom-elements/CustomElementRegistry-constructor-and-callbacks-are-held-strongly.html custom-elements/CustomElementRegistry-getName.html @@ -6254,6 +6255,7 @@ html/semantics/scripting-1/the-template-element/template-element/template-descen html/semantics/scripting-1/the-template-element/template-element/template-descendant-frameset.html html/semantics/scripting-1/the-template-element/template-element/template-descendant-head.html html/semantics/sections/headingoffset-and-headingreset.html +html/semantics/selectors/pseudo-classes/autofill.html html/semantics/selectors/pseudo-classes/checked-type-change.html html/semantics/selectors/pseudo-classes/checked.html html/semantics/selectors/pseudo-classes/default.html diff --git a/moli-selector/src/cssom_selector.rs b/moli-selector/src/cssom_selector.rs index e7b8d108a7..4eee47261d 100644 --- a/moli-selector/src/cssom_selector.rs +++ b/moli-selector/src/cssom_selector.rs @@ -668,6 +668,43 @@ pub(crate) fn dom_api_selector_list_contains_known_pseudo_element(selector_text: false } +pub(crate) fn webkit_compat_pseudo_element_validation_selector( + selector_text: &str, +) -> Option { + let mut input = ParserInput::new(selector_text); + let mut parser = Parser::new(&mut input); + let mut replacements = Vec::new(); + + while !parser.is_exhausted() { + let Ok(token) = parser.next_including_whitespace_and_comments().cloned() else { + return None; + }; + if !matches!(token, Token::Colon) { + continue; + } + let Ok(Token::Colon) = parser.next_including_whitespace_and_comments().cloned() else { + continue; + }; + let name_start = parser.position().byte_index(); + let Ok(Token::Ident(name)) = parser.next_including_whitespace_and_comments().cloned() + else { + continue; + }; + if is_unknown_webkit_pseudo_element_name(&name) { + replacements.push((name_start, parser.position().byte_index())); + } + } + + if replacements.is_empty() { + return None; + } + let mut selector = selector_text.to_owned(); + for (start, end) in replacements.into_iter().rev() { + selector.replace_range(start..end, "part(webkit-compat)"); + } + Some(selector) +} + pub(crate) fn selector_list_has_invalid_terminal_pseudo_element_chain(selector_text: &str) -> bool { let mut input = ParserInput::new(selector_text); let mut parser = Parser::new(&mut input); @@ -721,22 +758,23 @@ fn parse_known_pseudo_element_after_colon( parser: &mut Parser<'_, '_>, ) -> Option { let state = parser.state(); - if !matches!( + let has_double_colon = matches!( parser .next_including_whitespace_and_comments() .cloned() .ok()?, Token::Colon - ) { + ); + if !has_double_colon { parser.reset(&state); } let Ok(token) = parser.next_including_whitespace_and_comments().cloned() else { return None; }; match token { - Token::Ident(name) => { - is_known_terminal_pseudo_element_name(&name).then_some(KnownPseudoElementKind::Terminal) - } + Token::Ident(name) => (is_known_terminal_pseudo_element_name(&name) + || (has_double_colon && is_unknown_webkit_pseudo_element_name(&name))) + .then_some(KnownPseudoElementKind::Terminal), Token::Function(name) => { let kind = match name.to_ascii_lowercase().as_str() { "cue" | "highlight" => KnownPseudoElementKind::Terminal, @@ -750,6 +788,12 @@ fn parse_known_pseudo_element_after_colon( } } +fn is_unknown_webkit_pseudo_element_name(name: &str) -> bool { + const WEBKIT_PREFIX: &str = "-webkit-"; + name.get(..WEBKIT_PREFIX.len()) + .is_some_and(|prefix| prefix.eq_ignore_ascii_case(WEBKIT_PREFIX)) +} + fn is_known_terminal_pseudo_element_name(name: &str) -> bool { matches!( name.to_ascii_lowercase().as_str(), @@ -1157,6 +1201,12 @@ mod tests { assert!(dom_api_selector_list_has_only_known_pseudo_elements( "#target::highlight(foo)" )); + assert!(dom_api_selector_list_has_only_known_pseudo_elements( + "span::-WeBkIt-something-invalid" + )); + assert!(!dom_api_selector_list_has_only_known_pseudo_elements( + "input:-webkit-autofill" + )); } #[test] @@ -1178,6 +1228,23 @@ mod tests { )); } + #[test] + fn rewrites_webkit_compat_pseudo_elements_only_for_stylo_validation() { + assert_eq!( + webkit_compat_pseudo_element_validation_selector( + "span::-WeBkIt-something-invalid:active" + ) + .as_deref(), + Some("span::part(webkit-compat):active") + ); + assert_eq!( + webkit_compat_pseudo_element_validation_selector( + r#"[data-value='::-webkit-something-invalid']"# + ), + None + ); + } + #[test] fn detects_invalid_terminal_pseudo_element_chains() { assert!(selector_list_has_invalid_terminal_pseudo_element_chain( @@ -1189,6 +1256,15 @@ mod tests { assert!(selector_list_has_invalid_terminal_pseudo_element_chain( "::highlight(foo)::part(label)" )); + assert!(selector_list_has_invalid_terminal_pseudo_element_chain( + "span::-webkit-something-invalid::before" + )); + assert!(!selector_list_has_invalid_terminal_pseudo_element_chain( + "span::-webkit-something-invalid:active" + )); + assert!(!selector_list_has_invalid_terminal_pseudo_element_chain( + "input:-webkit-autofill::before" + )); assert!(!selector_list_has_invalid_terminal_pseudo_element_chain( "::part(label)::highlight(foo)" )); diff --git a/moli-selector/src/lib.rs b/moli-selector/src/lib.rs index 4332ff757f..a49d314ac4 100644 --- a/moli-selector/src/lib.rs +++ b/moli-selector/src/lib.rs @@ -406,6 +406,12 @@ mod tests { assert!(validate_style_rule_selector_list(".one, main > .two").is_ok()); assert!(validate_style_rule_selector_list("::part(mypart):lang(en)").is_ok()); assert!(validate_style_rule_selector_list("::part(mypart):dir(ltr)").is_ok()); + assert!( + validate_style_rule_selector_list("span::-webkit-something-invalid:active").is_ok() + ); + assert!( + validate_style_rule_selector_list("span::-webkit-something-invalid::before").is_err() + ); assert!(validate_style_rule_selector_list("").is_err()); assert!(validate_style_rule_selector_list("div[").is_err()); assert!(validate_style_rule_selector_list(".one,").is_err()); @@ -419,6 +425,7 @@ mod tests { assert!(validate_supports_selector_list("::part(mypart):is(:hover)").is_ok()); assert!(validate_supports_selector_list("::part(mypart):is(:first-child)").is_err()); assert!(validate_supports_selector_list("::part(mypart):where(:first-child)").is_err()); + assert!(validate_supports_selector_list("span::-webkit-something-invalid").is_err()); } #[test] @@ -1276,6 +1283,23 @@ mod tests { .query_selector_host(&host, "invalid# ::before") .is_err() ); + assert_eq!( + engine + .query_selector_host(&host, "span::-webkit-something-invalid") + .unwrap(), + None + ); + assert_eq!( + engine + .query_selector_host(&host, "span::-webkit-something-invalid:active") + .unwrap(), + None + ); + assert!( + engine + .query_selector_host(&host, "span::-webkit-something-invalid::before") + .is_err() + ); } #[test] diff --git a/moli-selector/src/stylo/selector_parse.rs b/moli-selector/src/stylo/selector_parse.rs index 3cfa6863b2..3feec410c1 100644 --- a/moli-selector/src/stylo/selector_parse.rs +++ b/moli-selector/src/stylo/selector_parse.rs @@ -3,6 +3,7 @@ use crate::cssom_selector::{ dom_api_selector_list_has_only_known_pseudo_elements, dom_api_selector_text_with_trailing_attribute_recovery, selector_list_has_invalid_terminal_pseudo_element_chain, + webkit_compat_pseudo_element_validation_selector, }; use cssparser::ToCss; use selectors::parser::ParseRelative; @@ -83,12 +84,20 @@ fn parse_dom_api_selector_list_text_for_url( } pub(crate) fn validate_style_rule_selector_list(selector: &str) -> Result<(), SelectorError> { - parse_style_rule_selector_list_for_url( - selector, - Url::parse("about:blank").expect("about:blank is valid"), - false, - ) - .map(|_| ()) + if selector_list_has_invalid_terminal_pseudo_element_chain(selector) { + return Err(SelectorError::syntax( + "terminal pseudo-elements cannot be chained", + )); + } + let url = Url::parse("about:blank").expect("about:blank is valid"); + parse_style_rule_selector_list_for_url(selector, url.clone(), false) + .or_else(|error| { + let Some(selector) = webkit_compat_pseudo_element_validation_selector(selector) else { + return Err(error); + }; + parse_style_rule_selector_list_for_url(&selector, url, false) + }) + .map(|_| ()) } pub(crate) fn validate_supports_selector_list(selector: &str) -> Result<(), SelectorError> { @@ -121,12 +130,29 @@ pub(crate) fn validate_style_rule_selector_list_with_namespaces( selector: &str, namespace_context: &StyleRuleNamespaceContext, ) -> Result<(), SelectorError> { + if selector_list_has_invalid_terminal_pseudo_element_chain(selector) { + return Err(SelectorError::syntax( + "terminal pseudo-elements cannot be chained", + )); + } + let url = Url::parse("about:blank").expect("about:blank is valid"); parse_style_rule_selector_list_for_url_and_namespaces( selector, - Url::parse("about:blank").expect("about:blank is valid"), + url.clone(), namespace_context, false, ) + .or_else(|error| { + let Some(selector) = webkit_compat_pseudo_element_validation_selector(selector) else { + return Err(error); + }; + parse_style_rule_selector_list_for_url_and_namespaces( + &selector, + url, + namespace_context, + false, + ) + }) .map(|_| ()) }