LibWeb: Implement generic boolean logic for media/supports queries

CSS Values 5 now defines a `<boolean-expr[]>` type that is used in place
of the bespoke grammar that previously existed for `@media` and
`@supports` queries. This commit implements some BooleanExpression
types to represent the nodes in a `<boolean-expr[]>`, and reimplements
`@media` and `@supports` queries using this.

The one part of this implementation I'm not convinced on is that the
`evaluate()` methods take a `HTML::Window*`. This is a compromise
because `@media` requires a Window, and `@supports` does not require
anything at all. As more users of `<boolean-expr[]>` get implemented in
the future, it will become clear if this is sufficient, or if we need
to do something smarter.

As a bonus, this actually improves our serialization of media queries!
This commit is contained in:
Sam Atkins 2025-03-14 10:31:27 +00:00
commit 0f5e054f97
Notes: github-actions[bot] 2025-03-17 10:01:42 +00:00
13 changed files with 526 additions and 663 deletions

View file

@ -150,7 +150,7 @@ RefPtr<Supports> Parser::parse_a_supports(TokenStream<T>& tokens)
auto component_values = parse_a_list_of_component_values(tokens);
TokenStream<ComponentValue> token_stream { component_values };
m_rule_context.append(ContextType::SupportsCondition);
auto maybe_condition = parse_supports_condition(token_stream);
auto maybe_condition = parse_boolean_expression(token_stream, MatchResult::False, [this](auto& tokens) { return parse_supports_feature(tokens); });
m_rule_context.take_last();
token_stream.discard_whitespace();
if (maybe_condition && !token_stream.has_next_token())
@ -159,59 +159,66 @@ RefPtr<Supports> Parser::parse_a_supports(TokenStream<T>& tokens)
return {};
}
OwnPtr<Supports::Condition> Parser::parse_supports_condition(TokenStream<ComponentValue>& tokens)
// https://drafts.csswg.org/css-values-5/#typedef-boolean-expr
OwnPtr<BooleanExpression> Parser::parse_boolean_expression(TokenStream<ComponentValue>& tokens, MatchResult result_for_general_enclosed, ParseTest parse_test)
{
// <boolean-expr[ <test> ]> = not <boolean-expr-group> | <boolean-expr-group>
// [ [ and <boolean-expr-group> ]*
// | [ or <boolean-expr-group> ]* ]
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto const& peeked_token = tokens.next_token();
// `not <supports-in-parens>`
// `not <boolean-expr-group>`
if (peeked_token.is_ident("not"sv)) {
tokens.discard_a_token();
tokens.discard_whitespace();
auto child = parse_supports_in_parens(tokens);
if (!child.has_value())
return {};
transaction.commit();
auto condition = make<Supports::Condition>();
condition->type = Supports::Condition::Type::Not;
condition->children.append(child.release_value());
return condition;
if (auto child = parse_boolean_expression_group(tokens, result_for_general_enclosed, parse_test)) {
transaction.commit();
return BooleanNotExpression::create(child.release_nonnull());
}
return {};
}
// ` <supports-in-parens> [ and <supports-in-parens> ]*
// | <supports-in-parens> [ or <supports-in-parens> ]*`
Vector<Supports::InParens> children;
Optional<Supports::Condition::Type> condition_type {};
auto as_condition_type = [](auto& token) -> Optional<Supports::Condition::Type> {
// `<boolean-expr-group>
// [ [ and <boolean-expr-group> ]*
// | [ or <boolean-expr-group> ]* ]`
Vector<NonnullOwnPtr<BooleanExpression>> children;
enum class Combinator : u8 {
And,
Or,
};
Optional<Combinator> combinator;
auto as_combinator = [](auto& token) -> Optional<Combinator> {
if (!token.is(Token::Type::Ident))
return {};
auto ident = token.token().ident();
if (ident.equals_ignoring_ascii_case("and"sv))
return Supports::Condition::Type::And;
return Combinator::And;
if (ident.equals_ignoring_ascii_case("or"sv))
return Supports::Condition::Type::Or;
return Combinator::Or;
return {};
};
while (tokens.has_next_token()) {
if (!children.is_empty()) {
// Expect `and` or `or` here
auto maybe_combination = as_condition_type(tokens.consume_a_token());
if (!maybe_combination.has_value())
auto maybe_combinator = as_combinator(tokens.consume_a_token());
if (!maybe_combinator.has_value())
return {};
if (!condition_type.has_value()) {
condition_type = maybe_combination.value();
} else if (maybe_combination != condition_type) {
if (!combinator.has_value()) {
combinator = maybe_combinator.value();
} else if (maybe_combinator != combinator) {
return {};
}
}
tokens.discard_whitespace();
if (auto in_parens = parse_supports_in_parens(tokens); in_parens.has_value()) {
children.append(in_parens.release_value());
if (auto child = parse_boolean_expression_group(tokens, result_for_general_enclosed, parse_test)) {
children.append(child.release_nonnull());
} else {
return {};
}
@ -223,15 +230,24 @@ OwnPtr<Supports::Condition> Parser::parse_supports_condition(TokenStream<Compone
return {};
transaction.commit();
auto condition = make<Supports::Condition>();
condition->type = condition_type.value_or(Supports::Condition::Type::Or);
condition->children = move(children);
return condition;
if (children.size() == 1)
return children.take_first();
VERIFY(combinator.has_value());
switch (*combinator) {
case Combinator::And:
return BooleanAndExpression::create(move(children));
case Combinator::Or:
return BooleanOrExpression::create(move(children));
}
VERIFY_NOT_REACHED();
}
Optional<Supports::InParens> Parser::parse_supports_in_parens(TokenStream<ComponentValue>& tokens)
OwnPtr<BooleanExpression> Parser::parse_boolean_expression_group(TokenStream<ComponentValue>& tokens, MatchResult result_for_general_enclosed, ParseTest parse_test)
{
// `( <supports-condition> )`
// <boolean-expr-group> = <test> | ( <boolean-expr[ <test> ]> ) | <general-enclosed>
// `( <boolean-expr[ <test> ]> )`
auto const& first_token = tokens.next_token();
if (first_token.is_block() && first_token.block().is_paren()) {
auto transaction = tokens.begin_transaction();
@ -239,54 +255,46 @@ Optional<Supports::InParens> Parser::parse_supports_in_parens(TokenStream<Compon
tokens.discard_whitespace();
TokenStream child_tokens { first_token.block().value };
if (auto condition = parse_supports_condition(child_tokens)) {
if (auto expression = parse_boolean_expression(child_tokens, result_for_general_enclosed, parse_test)) {
if (child_tokens.has_next_token())
return {};
transaction.commit();
return Supports::InParens {
.value = { condition.release_nonnull() }
};
return BooleanExpressionInParens::create(expression.release_nonnull());
}
}
// `<supports-feature>`
if (auto feature = parse_supports_feature(tokens); feature.has_value()) {
return Supports::InParens {
.value = { feature.release_value() }
};
}
// `<test>`
if (auto test = parse_test(tokens))
return test.release_nonnull();
// `<general-enclosed>`
if (auto general_enclosed = parse_general_enclosed(tokens); general_enclosed.has_value()) {
return Supports::InParens {
.value = general_enclosed.release_value()
};
}
if (auto general_enclosed = parse_general_enclosed(tokens, result_for_general_enclosed))
return general_enclosed.release_nonnull();
return {};
}
Optional<Supports::Feature> Parser::parse_supports_feature(TokenStream<ComponentValue>& tokens)
// https://drafts.csswg.org/css-conditional-4/#typedef-supports-feature
OwnPtr<BooleanExpression> Parser::parse_supports_feature(TokenStream<ComponentValue>& tokens)
{
// <supports-feature> = <supports-selector-fn> | <supports-decl>
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto const& first_token = tokens.consume_a_token();
// `<supports-decl>`
// `<supports-decl> = ( <declaration> )`
if (first_token.is_block() && first_token.block().is_paren()) {
TokenStream block_tokens { first_token.block().value };
// FIXME: Parsing and then converting back to a string is weird.
if (auto declaration = consume_a_declaration(block_tokens); declaration.has_value()) {
transaction.commit();
return Supports::Feature {
Supports::Declaration {
declaration->to_string(),
convert_to_style_property(*declaration).has_value() }
};
return Supports::Declaration::create(
declaration->to_string(),
convert_to_style_property(*declaration).has_value());
}
}
// `<supports-selector-fn>`
// `<supports-selector-fn> = selector( <complex-selector> )`
if (first_token.is_function("selector"sv)) {
// FIXME: Parsing and then converting back to a string is weird.
StringBuilder builder;
@ -294,19 +302,18 @@ Optional<Supports::Feature> Parser::parse_supports_feature(TokenStream<Component
builder.append(item.to_string());
transaction.commit();
TokenStream selector_tokens { first_token.function().value };
return Supports::Feature {
Supports::Selector {
builder.to_string_without_validation(),
!parse_a_selector_list(selector_tokens, SelectorType::Standalone).is_error() }
};
return Supports::Selector::create(
builder.to_string_without_validation(),
!parse_a_selector_list(selector_tokens, SelectorType::Standalone).is_error());
}
return {};
}
// https://www.w3.org/TR/mediaqueries-4/#typedef-general-enclosed
Optional<GeneralEnclosed> Parser::parse_general_enclosed(TokenStream<ComponentValue>& tokens)
OwnPtr<GeneralEnclosed> Parser::parse_general_enclosed(TokenStream<ComponentValue>& tokens, MatchResult result)
{
// FIXME: <general-enclosed> syntax changed in MediaQueries-5
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto const& first_token = tokens.consume_a_token();
@ -314,13 +321,13 @@ Optional<GeneralEnclosed> Parser::parse_general_enclosed(TokenStream<ComponentVa
// `[ <function-token> <any-value>? ) ]`
if (first_token.is_function()) {
transaction.commit();
return GeneralEnclosed { first_token.to_string() };
return GeneralEnclosed::create(first_token.to_string(), result);
}
// `( <any-value>? )`
if (first_token.is_block() && first_token.block().is_paren()) {
transaction.commit();
return GeneralEnclosed { first_token.to_string() };
return GeneralEnclosed::create(first_token.to_string(), result);
}
return {};
@ -1641,10 +1648,10 @@ LengthOrCalculated Parser::parse_as_sizes_attribute(DOM::Element const& element,
// 5. Parse the remaining component values in unparsed size as a <media-condition>.
// If it does not parse correctly, or it does parse correctly but the <media-condition> evaluates to false, continue.
TokenStream<ComponentValue> token_stream { unparsed_size };
auto media_condition = parse_media_condition(token_stream, MediaCondition::AllowOr::Yes);
TokenStream token_stream { unparsed_size };
auto media_condition = parse_media_condition(token_stream);
auto const* context_window = window();
if (!media_condition || (context_window && media_condition->evaluate(*context_window) == MatchResult::False)) {
if (!media_condition || (context_window && media_condition->evaluate(context_window) == MatchResult::False)) {
continue;
}