fix(selectors): accept webkit compatibility pseudos

This commit is contained in:
ldm0
2026-09-09 06:38:21 +08:00
parent 1b28950b1c
commit ff063fd6b7
5 changed files with 140 additions and 14 deletions
@@ -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
@@ -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
+81 -5
View File
@@ -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<String> {
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<KnownPseudoElementKind> {
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)"
));
+24
View File
@@ -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]
+33 -7
View File
@@ -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(|_| ())
}