Unify CSS feature query truth

This commit is contained in:
SGavrl committed 2026-08-03 13:11:28 +00:00
1 parent 33d94eaf85
commit d41a4b88e6
5 files changed
+749 -29

No files matched your search

+80 -12
View File
@@ -2517,13 +2517,13 @@ pub(crate) fn supports_condition_applies(condition: &str) -> bool {
.or_else(|| condition.strip_prefix("supports"))
.unwrap_or(condition)
.trim();
eval_supports_condition(condition)
eval_supports_condition(condition).unwrap_or(false)
}
fn eval_supports_condition(condition: &str) -> bool {
fn eval_supports_condition(condition: &str) -> Option<bool> {
let condition = condition.trim();
if condition.is_empty() {
return false;
if condition.is_empty() || !balanced_supports_syntax(condition) {
return None;
}
if let Some(inner) = enclosing_parenthesized(condition) {
return eval_supports_condition(inner);
@@ -2536,23 +2536,76 @@ fn eval_supports_condition(condition: &str) -> bool {
.get(3)
.is_some_and(u8::is_ascii_whitespace)
{
return !eval_supports_condition(condition[3..].trim());
return eval_supports_condition(condition[3..].trim()).map(|result| !result);
}
if let Some(parts) = split_supports_operator(condition, "or") {
return parts.into_iter().any(eval_supports_condition);
let or_parts = split_supports_operator(condition, "or");
let and_parts = split_supports_operator(condition, "and");
// CSS Conditional Rules does not permit `and` and `or` at the same
// nesting level. Treat the entire condition as invalid rather than
// inventing JavaScript-like precedence.
if or_parts.is_some() && and_parts.is_some() {
return None;
}
if let Some(parts) = split_supports_operator(condition, "and") {
return parts.into_iter().all(eval_supports_condition);
if let Some(parts) = or_parts {
let results = parts
.into_iter()
.map(eval_supports_condition)
.collect::<Option<Vec<_>>>()?;
return Some(results.into_iter().any(|result| result));
}
if let Some(parts) = and_parts {
let results = parts
.into_iter()
.map(eval_supports_condition)
.collect::<Option<Vec<_>>>()?;
return Some(results.into_iter().all(|result| result));
}
let lower = condition.to_ascii_lowercase();
if lower.starts_with("selector(") && condition.ends_with(')') {
let inner = &condition["selector(".len()..condition.len() - 1];
return obscura_dom::selector::parse_selector(inner.trim()).is_ok();
return Some(obscura_dom::selector::parse_selector(inner.trim()).is_ok());
}
let Some((name, value)) = condition.split_once(':') else {
return false;
// A syntactically balanced unknown functional condition is a valid
// general-enclosed leaf whose result is false. Everything else is a
// malformed condition, important for `not`: invalid syntax must not
// become true merely because it was negated.
return condition
.find('(')
.filter(|index| *index > 0 && condition.ends_with(')'))
.map(|_| false);
};
crate::style::supports_declaration(name, value)
Some(crate::style::supports_declaration(name, value))
}
fn balanced_supports_syntax(condition: &str) -> bool {
let mut stack = Vec::new();
let mut quote = None;
let mut escaped = false;
for character in condition.chars() {
if escaped {
escaped = false;
continue;
}
if character == '\\' {
escaped = true;
continue;
}
if let Some(active) = quote {
if character == active {
quote = None;
}
continue;
}
match character {
'\'' | '"' => quote = Some(character),
'(' => stack.push(')'),
'[' => stack.push(']'),
')' | ']' if stack.pop() != Some(character) => return false,
_ => {}
}
}
!escaped && quote.is_none() && stack.is_empty()
}
/// Return the contents when one outer parenthesis pair encloses the complete
@@ -4671,6 +4724,21 @@ mod tests {
);
}
#[test]
fn supports_conditions_reject_invalid_boolean_grammar() {
assert!(supports_condition_applies("(display:grid) and (word-break:break-all)"));
assert!(supports_condition_applies("(display:grid) or (unknown:value)"));
assert!(!supports_condition_applies(
"(display:grid) and (word-break:break-all) or (display:flex)"
));
assert!(!supports_condition_applies(
"not ((display:grid) and (word-break:break-all) or (display:flex))"
));
assert!(!supports_condition_applies("not ()"));
assert!(!supports_condition_applies("(display:grid;)"));
assert!(!supports_condition_applies("not (display:grid"));
}
#[test]
fn supports_polygon_clip_path_activates_only_painted_shape_subset() {
let css = "@supports (clip-path:polygon(0 0,100% 0,50% 100%)){\