mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-02 14:19:48 +00:00
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:
parent
84a695c958
commit
0f5e054f97
Notes:
github-actions[bot]
2025-03-17 10:01:42 +00:00
Author: https://github.com/AtkinsSJ
Commit: 0f5e054f97
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/3941
13 changed files with 526 additions and 663 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue