fix(cssom): preserve invalid forgiving selectors

This commit is contained in:
ldm0
2026-09-27 07:56:04 +08:00
parent aa3505d120
commit 68a1b0a7f0
4 changed files with 241 additions and 3 deletions
+226
View File
@@ -2,6 +2,7 @@ use cssparser::{
BasicParseErrorKind, ParseError, Parser, ParserInput, Token, parse_nth, serialize_identifier,
serialize_string,
};
use std::ops::Range;
use moli_css_parse::serialize_component_values_single_line;
@@ -147,6 +148,191 @@ pub(crate) fn serialize_cssom_selector_text(
))
}
pub(crate) fn serialize_cssom_selector_text_preserving_invalid_forgiving_items(
selector_text: &str,
has_default_namespace: bool,
prefixes_matching_default_namespace: &[String],
is_strictly_valid: impl Fn(&str) -> bool,
) -> Option<String> {
let (protected_selector, preserved_items) =
protect_invalid_forgiving_selector_items(selector_text, is_strictly_valid)?;
let mut serialized = serialize_cssom_selector_text(
&protected_selector,
has_default_namespace,
prefixes_matching_default_namespace,
)?;
for (marker, item) in preserved_items {
if serialized.matches(&marker).count() != 1 {
return None;
}
serialized = serialized.replacen(&marker, &item, 1);
}
Some(serialized)
}
fn protect_invalid_forgiving_selector_items(
selector_text: &str,
is_strictly_valid: impl Fn(&str) -> bool,
) -> Option<(String, Vec<(String, String)>)> {
let argument_ranges = outermost_forgiving_selector_argument_ranges(selector_text)?;
if argument_ranges.is_empty() || is_strictly_valid(selector_text) {
return Some((selector_text.to_owned(), Vec::new()));
}
let baseline =
selector_with_forgiving_argument_replacements(selector_text, &argument_ranges, None);
let can_classify_items = is_strictly_valid(&baseline);
let mut invalid_item_ranges = Vec::new();
for (argument_index, argument_range) in argument_ranges.iter().enumerate() {
let argument = selector_text.get(argument_range.clone())?;
for item_range in top_level_comma_separated_selector_item_ranges(argument)? {
let item = argument.get(item_range.clone())?;
let item_is_valid = can_classify_items
&& is_strictly_valid(&selector_with_forgiving_argument_replacements(
selector_text,
&argument_ranges,
Some((argument_index, item)),
));
if !item_is_valid {
invalid_item_ranges.push(
argument_range.start + item_range.start..argument_range.start + item_range.end,
);
}
}
}
let collision_text = serialize_component_values_single_line(selector_text)?;
let mut protected_selector = selector_text.to_owned();
let mut preserved_items = Vec::with_capacity(invalid_item_ranges.len());
for (index, range) in invalid_item_ranges.into_iter().enumerate().rev() {
let item = selector_text.get(range.clone())?;
let marker = unused_forgiving_selector_marker(selector_text, &collision_text, index);
protected_selector.replace_range(range, &marker);
preserved_items.push((marker, trim_css_whitespace(item).to_owned()));
}
Some((protected_selector, preserved_items))
}
fn selector_with_forgiving_argument_replacements(
selector_text: &str,
argument_ranges: &[Range<usize>],
selected_item: Option<(usize, &str)>,
) -> String {
let mut selector = selector_text.to_owned();
for (index, range) in argument_ranges.iter().enumerate().rev() {
let replacement = selected_item
.filter(|(selected_index, _)| *selected_index == index)
.map_or(":hover", |(_, item)| trim_css_whitespace(item));
selector.replace_range(range.clone(), replacement);
}
selector
}
fn unused_forgiving_selector_marker(
selector_text: &str,
collision_text: &str,
index: usize,
) -> String {
let mut attempt = 0_u32;
loop {
let marker = format!(".__moli_cssom_invalid_forgiving_{index}_{attempt}");
if !selector_text.contains(&marker) && !collision_text.contains(&marker) {
return marker;
}
attempt = attempt
.checked_add(1)
.expect("forgiving selector marker search exhausted");
}
}
fn outermost_forgiving_selector_argument_ranges(selector_text: &str) -> Option<Vec<Range<usize>>> {
let mut input = ParserInput::new(selector_text);
let mut parser = Parser::new(&mut input);
let mut ranges = Vec::new();
collect_outermost_forgiving_selector_argument_ranges(&mut parser, &mut ranges)?;
Some(ranges)
}
fn collect_outermost_forgiving_selector_argument_ranges(
input: &mut Parser<'_, '_>,
ranges: &mut Vec<Range<usize>>,
) -> Option<()> {
let mut previous_was_colon = false;
while let Ok(token) = input.next_including_whitespace_and_comments().cloned() {
if token.is_parse_error() {
return None;
}
match token {
Token::Colon => previous_was_colon = true,
Token::Comment(_) if previous_was_colon => {}
Token::Function(name) => {
let is_forgiving_selector = previous_was_colon
&& (name.eq_ignore_ascii_case("is") || name.eq_ignore_ascii_case("where"));
let argument_start = input.position().byte_index();
let mut argument_end = None;
let nested: Result<(), ParseError<'_, ()>> = input.parse_nested_block(|input| {
if is_forgiving_selector {
consume_component_values(input)?;
argument_end = Some(input.position().byte_index());
Ok(())
} else {
collect_outermost_forgiving_selector_argument_ranges(input, ranges)
.ok_or_else(|| input.new_custom_error::<(), ()>(()))
}
});
nested.ok()?;
if let Some(argument_end) = argument_end {
ranges.push(argument_start..argument_end);
}
previous_was_colon = false;
}
Token::ParenthesisBlock | Token::CurlyBracketBlock => {
let nested: Result<(), ParseError<'_, ()>> = input.parse_nested_block(|input| {
collect_outermost_forgiving_selector_argument_ranges(input, ranges)
.ok_or_else(|| input.new_custom_error::<(), ()>(()))
});
nested.ok()?;
previous_was_colon = false;
}
Token::SquareBracketBlock => {
consume_nested_component_value(input, &token).ok()?;
previous_was_colon = false;
}
_ => previous_was_colon = false,
}
}
Some(())
}
fn top_level_comma_separated_selector_item_ranges(argument: &str) -> Option<Vec<Range<usize>>> {
let mut input = ParserInput::new(argument);
let mut parser = Parser::new(&mut input);
let mut ranges = Vec::new();
let mut item_start = 0;
while !parser.is_exhausted() {
let token_start = parser.position().byte_index();
let token = parser
.next_including_whitespace_and_comments()
.ok()?
.clone();
if token.is_parse_error() {
return None;
}
if matches!(token, Token::Comma) {
ranges.push(item_start..token_start);
item_start = parser.position().byte_index();
} else {
consume_nested_component_value(&mut parser, &token).ok()?;
}
}
ranges.push(item_start..argument.len());
Some(ranges)
}
fn trim_css_whitespace(value: &str) -> &str {
value.trim_matches(['\t', '\n', '\x0c', '\r', ' '])
}
fn serialize_cssom_selector_text_without_default_namespace(selector_text: &str) -> Option<String> {
serialize_cssom_selector_text(selector_text, false, &[])
}
@@ -1073,6 +1259,46 @@ mod tests {
);
}
#[test]
fn preserves_only_invalid_forgiving_selector_items() {
let serialize = |selector| {
serialize_cssom_selector_text_preserving_invalid_forgiving_items(
selector,
false,
&[],
|candidate| crate::stylo::validate_supports_selector_list(candidate).is_ok(),
)
.unwrap()
};
assert_eq!(
serialize("::part(foo):is([attr='value'])"),
"::part(foo):is([attr='value'])"
);
assert_eq!(
serialize("::part(foo):where([attr='value'])"),
"::part(foo):where([attr='value'])"
);
assert_eq!(
serialize(":is([att=val], ::before:HOVER )"),
r#":is([att="val"], ::before:HOVER)"#
);
assert_eq!(serialize(":is(,,, )"), ":is(, , , )");
assert_eq!(serialize(":host(:is(,,,))"), ":host(:is(, , , ))");
assert_eq!(
serialize(":host(:is(.a, .b+.c, .d))"),
":host(:is(.a, .b+.c, .d))"
);
assert_eq!(
serialize(":has(:is(:has(.a+.b)))"),
":has(:is(:has(.a+.b)))"
);
assert_eq!(
serialize(":is([att='valid'], .other)"),
r#":is([att="valid"], .other)"#
);
}
#[test]
fn parses_get_computed_style_pseudo_element_argument() {
assert_eq!(
+5 -1
View File
@@ -432,10 +432,14 @@ fn serialize_cssom_style_rule_selector_text(
) -> String {
let prefixes_matching_default_namespace =
namespace_context.prefixes_matching_default_namespace();
cssom_selector::serialize_cssom_selector_text(
cssom_selector::serialize_cssom_selector_text_preserving_invalid_forgiving_items(
selector,
namespace_context.has_default_namespace(),
&prefixes_matching_default_namespace,
|candidate| {
stylo::validate_supports_selector_list_with_namespaces(candidate, namespace_context)
.is_ok()
},
)
.unwrap_or_else(|| selector.to_owned())
}
+1 -1
View File
@@ -143,7 +143,7 @@ pub(crate) use selector_parse::{
normalize_scope_style_rule_selector_list_with_namespaces, parse_dom_api_selector_list,
parse_dom_api_selector_list_for_url, validate_style_rule_selector_list,
validate_style_rule_selector_list_with_namespaces,
validate_supports_selector_condition_argument,
validate_supports_selector_condition_argument, validate_supports_selector_list_with_namespaces,
};
pub use style_traversal::{
StyloDocument, StyloDomHostBinding, StyloDomStyleAdapter, StyloElement, StyloElementDataStore,
+9 -1
View File
@@ -187,9 +187,17 @@ pub(crate) fn validate_style_rule_selector_list(selector: &str) -> Result<(), Se
}
pub(crate) fn validate_supports_selector_list(selector: &str) -> Result<(), SelectorError> {
parse_style_rule_selector_list_for_url(
validate_supports_selector_list_with_namespaces(selector, &StyleRuleNamespaceContext::default())
}
pub(crate) fn validate_supports_selector_list_with_namespaces(
selector: &str,
namespace_context: &StyleRuleNamespaceContext,
) -> Result<(), SelectorError> {
parse_style_rule_selector_list_for_url_and_namespaces(
selector,
Url::parse("about:blank").expect("about:blank is valid"),
namespace_context,
true,
)
.map(|_| ())