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
parent 84a695c958
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

@ -55,6 +55,7 @@ set(SOURCES
Crypto/SubtleCrypto.cpp
CSS/Angle.cpp
CSS/AnimationEvent.cpp
CSS/BooleanExpression.cpp
CSS/CalculatedOr.cpp
CSS/Clip.cpp
CSS/CountersSet.cpp

View file

@ -0,0 +1,136 @@
/*
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringBuilder.h>
#include <LibWeb/CSS/BooleanExpression.h>
namespace Web::CSS {
bool BooleanExpression::evaluate_to_boolean(HTML::Window const* window) const
{
return evaluate(window) == MatchResult::True;
}
void BooleanExpression::indent(StringBuilder& builder, int levels)
{
builder.append_repeated(" "sv, levels);
}
void GeneralEnclosed::dump(StringBuilder& builder, int indent_levels) const
{
indent(builder, indent_levels);
builder.appendff("GeneralEnclosed: {}\n", to_string());
}
MatchResult BooleanNotExpression::evaluate(HTML::Window const* window) const
{
// https://drafts.csswg.org/css-values-5/#boolean-logic
// `not test` evaluates to true if its contained test is false, false if its true, and unknown if its unknown.
switch (m_child->evaluate(window)) {
case MatchResult::False:
return MatchResult::True;
case MatchResult::True:
return MatchResult::False;
case MatchResult::Unknown:
return MatchResult::Unknown;
}
VERIFY_NOT_REACHED();
}
String BooleanNotExpression::to_string() const
{
return MUST(String::formatted("not {}", m_child->to_string()));
}
void BooleanNotExpression::dump(StringBuilder& builder, int indent_levels) const
{
indent(builder, indent_levels);
builder.append("NOT:\n"sv);
m_child->dump(builder, indent_levels + 1);
}
MatchResult BooleanExpressionInParens::evaluate(HTML::Window const* window) const
{
return m_child->evaluate(window);
}
String BooleanExpressionInParens::to_string() const
{
return MUST(String::formatted("({})", m_child->to_string()));
}
void BooleanExpressionInParens::dump(StringBuilder& builder, int indent_levels) const
{
indent(builder, indent_levels);
builder.append("(\n"sv);
m_child->dump(builder, indent_levels + 1);
indent(builder, indent_levels);
builder.append(")\n"sv);
}
MatchResult BooleanAndExpression::evaluate(HTML::Window const* window) const
{
// https://drafts.csswg.org/css-values-5/#boolean-logic
// Multiple tests connected with `and` evaluate to true if all of those tests are true, false if any of them are
// false, and unknown otherwise (i.e. if at least one unknown, but no false).
size_t true_results = 0;
for (auto const& child : m_children) {
auto child_match = child->evaluate(window);
if (child_match == MatchResult::False)
return MatchResult::False;
if (child_match == MatchResult::True)
true_results++;
}
if (true_results == m_children.size())
return MatchResult::True;
return MatchResult::Unknown;
}
String BooleanAndExpression::to_string() const
{
return MUST(String::join(" and "sv, m_children));
}
void BooleanAndExpression::dump(StringBuilder& builder, int indent_levels) const
{
indent(builder, indent_levels);
builder.append("AND:\n"sv);
for (auto const& child : m_children)
child->dump(builder, indent_levels + 1);
}
MatchResult BooleanOrExpression::evaluate(HTML::Window const* window) const
{
// https://drafts.csswg.org/css-values-5/#boolean-logic
// Multiple tests connected with `or` evaluate to true if any of those tests are true, false if all of them are
// false, and unknown otherwise (i.e. at least one unknown, but no true).
size_t false_results = 0;
for (auto const& child : m_children) {
auto child_match = child->evaluate(window);
if (child_match == MatchResult::True)
return MatchResult::True;
if (child_match == MatchResult::False)
false_results++;
}
if (false_results == m_children.size())
return MatchResult::False;
return MatchResult::Unknown;
}
String BooleanOrExpression::to_string() const
{
return MUST(String::join(" or "sv, m_children));
}
void BooleanOrExpression::dump(StringBuilder& builder, int indent_levels) const
{
indent(builder, indent_levels);
builder.append("OR:\n"sv);
for (auto const& child : m_children)
child->dump(builder, indent_levels + 1);
}
}

View file

@ -0,0 +1,171 @@
/*
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullOwnPtr.h>
#include <AK/String.h>
#include <AK/Vector.h>
#include <LibWeb/Forward.h>
namespace Web::CSS {
// Corresponds to Kleene 3-valued logic.
enum class MatchResult {
False,
True,
Unknown,
};
inline MatchResult as_match_result(bool value)
{
return value ? MatchResult::True : MatchResult::False;
}
inline MatchResult negate(MatchResult value)
{
switch (value) {
case MatchResult::False:
return MatchResult::True;
case MatchResult::True:
return MatchResult::False;
case MatchResult::Unknown:
return MatchResult::Unknown;
}
VERIFY_NOT_REACHED();
}
// The contents of this file implement the `<boolean-expr>` concept.
// https://drafts.csswg.org/css-values-5/#typedef-boolean-expr
class BooleanExpression {
public:
virtual ~BooleanExpression() = default;
bool evaluate_to_boolean(HTML::Window const*) const;
static void indent(StringBuilder& builder, int levels);
virtual MatchResult evaluate(HTML::Window const*) const = 0;
virtual String to_string() const = 0;
virtual void dump(StringBuilder&, int indent_levels = 0) const = 0;
};
// https://www.w3.org/TR/mediaqueries-4/#typedef-general-enclosed
class GeneralEnclosed final : public BooleanExpression {
public:
static NonnullOwnPtr<GeneralEnclosed> create(String serialized_contents, MatchResult matches = MatchResult::Unknown)
{
return adopt_own(*new GeneralEnclosed(move(serialized_contents), matches));
}
virtual ~GeneralEnclosed() override = default;
virtual MatchResult evaluate(HTML::Window const*) const override { return m_matches; }
virtual String to_string() const override { return m_serialized_contents; }
virtual void dump(StringBuilder&, int indent_levels = 0) const override;
private:
GeneralEnclosed(String serialized_contents, MatchResult matches)
: m_serialized_contents(move(serialized_contents))
, m_matches(matches)
{
}
String m_serialized_contents;
MatchResult m_matches;
};
class BooleanNotExpression final : public BooleanExpression {
public:
static NonnullOwnPtr<BooleanNotExpression> create(NonnullOwnPtr<BooleanExpression>&& child)
{
return adopt_own(*new BooleanNotExpression(move(child)));
}
virtual ~BooleanNotExpression() override = default;
virtual MatchResult evaluate(HTML::Window const*) const override;
virtual String to_string() const override;
virtual void dump(StringBuilder&, int indent_levels = 0) const override;
private:
BooleanNotExpression(NonnullOwnPtr<BooleanExpression>&& child)
: m_child(move(child))
{
}
NonnullOwnPtr<BooleanExpression> m_child;
};
class BooleanExpressionInParens final : public BooleanExpression {
public:
static NonnullOwnPtr<BooleanExpressionInParens> create(NonnullOwnPtr<BooleanExpression>&& child)
{
return adopt_own(*new BooleanExpressionInParens(move(child)));
}
virtual ~BooleanExpressionInParens() override = default;
virtual MatchResult evaluate(HTML::Window const*) const override;
virtual String to_string() const override;
virtual void dump(StringBuilder&, int indent_levels = 0) const override;
private:
BooleanExpressionInParens(NonnullOwnPtr<BooleanExpression>&& child)
: m_child(move(child))
{
}
NonnullOwnPtr<BooleanExpression> m_child;
};
class BooleanAndExpression final : public BooleanExpression {
public:
static NonnullOwnPtr<BooleanAndExpression> create(Vector<NonnullOwnPtr<BooleanExpression>>&& children)
{
return adopt_own(*new BooleanAndExpression(move(children)));
}
virtual ~BooleanAndExpression() override = default;
virtual MatchResult evaluate(HTML::Window const*) const override;
virtual String to_string() const override;
virtual void dump(StringBuilder&, int indent_levels = 0) const override;
private:
BooleanAndExpression(Vector<NonnullOwnPtr<BooleanExpression>>&& children)
: m_children(move(children))
{
}
Vector<NonnullOwnPtr<BooleanExpression>> m_children;
};
class BooleanOrExpression final : public BooleanExpression {
public:
static NonnullOwnPtr<BooleanOrExpression> create(Vector<NonnullOwnPtr<BooleanExpression>>&& children)
{
return adopt_own(*new BooleanOrExpression(move(children)));
}
virtual ~BooleanOrExpression() override = default;
virtual MatchResult evaluate(HTML::Window const*) const override;
virtual String to_string() const override;
virtual void dump(StringBuilder&, int indent_levels = 0) const override;
private:
BooleanOrExpression(Vector<NonnullOwnPtr<BooleanExpression>>&& children)
: m_children(move(children))
{
}
Vector<NonnullOwnPtr<BooleanExpression>> m_children;
};
}
template<>
struct AK::Formatter<Web::CSS::BooleanExpression> : AK::Formatter<StringView> {
ErrorOr<void> format(FormatBuilder& builder, Web::CSS::BooleanExpression const& expression)
{
return Formatter<StringView>::format(builder, expression.to_string());
}
};

View file

@ -1,85 +0,0 @@
/*
* Copyright (c) 2021, Sam Atkins <atkinssj@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/String.h>
namespace Web::CSS {
// Corresponds to Kleene 3-valued logic.
// https://www.w3.org/TR/mediaqueries-4/#evaluating
enum class MatchResult {
False,
True,
Unknown,
};
inline MatchResult as_match_result(bool value)
{
return value ? MatchResult::True : MatchResult::False;
}
inline MatchResult negate(MatchResult value)
{
switch (value) {
case MatchResult::False:
return MatchResult::True;
case MatchResult::True:
return MatchResult::False;
case MatchResult::Unknown:
return MatchResult::Unknown;
}
VERIFY_NOT_REACHED();
}
template<typename Collection, typename Evaluate>
inline MatchResult evaluate_and(Collection& collection, Evaluate evaluate)
{
size_t true_results = 0;
for (auto& item : collection) {
auto item_match = evaluate(item);
if (item_match == MatchResult::False)
return MatchResult::False;
if (item_match == MatchResult::True)
true_results++;
}
if (true_results == collection.size())
return MatchResult::True;
return MatchResult::Unknown;
}
template<typename Collection, typename Evaluate>
inline MatchResult evaluate_or(Collection& collection, Evaluate evaluate)
{
size_t false_results = 0;
for (auto& item : collection) {
auto item_match = evaluate(item);
if (item_match == MatchResult::True)
return MatchResult::True;
if (item_match == MatchResult::False)
false_results++;
}
if (false_results == collection.size())
return MatchResult::False;
return MatchResult::Unknown;
}
// https://www.w3.org/TR/mediaqueries-4/#typedef-general-enclosed
class GeneralEnclosed {
public:
GeneralEnclosed(String serialized_contents)
: m_serialized_contents(move(serialized_contents))
{
}
MatchResult evaluate() const { return MatchResult::Unknown; }
String const& to_string() const { return m_serialized_contents; }
private:
String m_serialized_contents;
};
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -83,57 +83,58 @@ String MediaFeature::to_string() const
VERIFY_NOT_REACHED();
}
bool MediaFeature::evaluate(HTML::Window const& window) const
MatchResult MediaFeature::evaluate(HTML::Window const* window) const
{
auto maybe_queried_value = window.query_media_feature(m_id);
VERIFY(window);
auto maybe_queried_value = window->query_media_feature(m_id);
if (!maybe_queried_value.has_value())
return false;
return MatchResult::False;
auto queried_value = maybe_queried_value.release_value();
CalculationResolutionContext calculation_context {
.length_resolution_context = Length::ResolutionContext::for_window(window),
.length_resolution_context = Length::ResolutionContext::for_window(*window),
};
switch (m_type) {
case Type::IsTrue:
if (queried_value.is_integer())
return queried_value.integer().resolved(calculation_context) != 0;
return as_match_result(queried_value.integer().resolved(calculation_context) != 0);
if (queried_value.is_length()) {
auto length = queried_value.length().resolved(calculation_context);
return length->raw_value() != 0;
return as_match_result(length->raw_value() != 0);
}
// FIXME: I couldn't figure out from the spec how ratios should be evaluated in a boolean context.
if (queried_value.is_ratio())
return !queried_value.ratio().is_degenerate();
return as_match_result(!queried_value.ratio().is_degenerate());
if (queried_value.is_resolution())
return queried_value.resolution().resolved(calculation_context).map([](auto& it) { return it.to_dots_per_pixel(); }).value_or(0) != 0;
return as_match_result(queried_value.resolution().resolved(calculation_context).map([](auto& it) { return it.to_dots_per_pixel(); }).value_or(0) != 0);
if (queried_value.is_ident()) {
// NOTE: It is not technically correct to always treat `no-preference` as false, but every
// media-feature that accepts it as a value treats it as false, so good enough. :^)
// If other features gain this property for other keywords in the future, we can
// add more robust handling for them then.
return queried_value.ident() != Keyword::None
&& queried_value.ident() != Keyword::NoPreference;
return as_match_result(queried_value.ident() != Keyword::None
&& queried_value.ident() != Keyword::NoPreference);
}
return false;
return MatchResult::False;
case Type::ExactValue:
return compare(window, *m_value, Comparison::Equal, queried_value);
return as_match_result(compare(*window, *m_value, Comparison::Equal, queried_value));
case Type::MinValue:
return compare(window, queried_value, Comparison::GreaterThanOrEqual, *m_value);
return as_match_result(compare(*window, queried_value, Comparison::GreaterThanOrEqual, *m_value));
case Type::MaxValue:
return compare(window, queried_value, Comparison::LessThanOrEqual, *m_value);
return as_match_result(compare(*window, queried_value, Comparison::LessThanOrEqual, *m_value));
case Type::Range:
if (!compare(window, m_range->left_value, m_range->left_comparison, queried_value))
return false;
if (!compare(*window, m_range->left_value, m_range->left_comparison, queried_value))
return MatchResult::False;
if (m_range->right_comparison.has_value())
if (!compare(window, queried_value, *m_range->right_comparison, *m_range->right_value))
return false;
if (!compare(*window, queried_value, *m_range->right_comparison, *m_range->right_value))
return MatchResult::False;
return true;
return MatchResult::True;
}
VERIFY_NOT_REACHED();
@ -247,92 +248,10 @@ bool MediaFeature::compare(HTML::Window const& window, MediaFeatureValue left, C
VERIFY_NOT_REACHED();
}
NonnullOwnPtr<MediaCondition> MediaCondition::from_general_enclosed(GeneralEnclosed&& general_enclosed)
void MediaFeature::dump(StringBuilder& builder, int indent_levels) const
{
auto result = new MediaCondition;
result->type = Type::GeneralEnclosed;
result->general_enclosed = move(general_enclosed);
return adopt_own(*result);
}
NonnullOwnPtr<MediaCondition> MediaCondition::from_feature(MediaFeature&& feature)
{
auto result = new MediaCondition;
result->type = Type::Single;
result->feature = move(feature);
return adopt_own(*result);
}
NonnullOwnPtr<MediaCondition> MediaCondition::from_not(NonnullOwnPtr<MediaCondition>&& condition)
{
auto result = new MediaCondition;
result->type = Type::Not;
result->conditions.append(move(condition));
return adopt_own(*result);
}
NonnullOwnPtr<MediaCondition> MediaCondition::from_and_list(Vector<NonnullOwnPtr<MediaCondition>>&& conditions)
{
auto result = new MediaCondition;
result->type = Type::And;
result->conditions = move(conditions);
return adopt_own(*result);
}
NonnullOwnPtr<MediaCondition> MediaCondition::from_or_list(Vector<NonnullOwnPtr<MediaCondition>>&& conditions)
{
auto result = new MediaCondition;
result->type = Type::Or;
result->conditions = move(conditions);
return adopt_own(*result);
}
String MediaCondition::to_string() const
{
StringBuilder builder;
builder.append('(');
switch (type) {
case Type::Single:
builder.append(feature->to_string());
break;
case Type::Not:
builder.append("not "sv);
builder.append(conditions.first()->to_string());
break;
case Type::And:
builder.join(" and "sv, conditions);
break;
case Type::Or:
builder.join(" or "sv, conditions);
break;
case Type::GeneralEnclosed:
builder.append(general_enclosed->to_string());
break;
}
builder.append(')');
return MUST(builder.to_string());
}
MatchResult MediaCondition::evaluate(HTML::Window const& window) const
{
switch (type) {
case Type::Single:
return as_match_result(feature->evaluate(window));
case Type::Not:
return negate(conditions.first()->evaluate(window));
case Type::And:
return evaluate_and(conditions, [&](auto& child) { return child->evaluate(window); });
case Type::Or:
return evaluate_or(conditions, [&](auto& child) { return child->evaluate(window); });
case Type::GeneralEnclosed:
return general_enclosed->evaluate();
}
VERIFY_NOT_REACHED();
indent(builder, indent_levels);
builder.appendff("MediaFeature: {}", to_string());
}
String MediaQuery::to_string() const
@ -386,7 +305,7 @@ bool MediaQuery::evaluate(HTML::Window const& window)
MatchResult result = matches_media(m_media_type);
if ((result == MatchResult::True) && m_media_condition)
result = m_media_condition->evaluate(window);
result = m_media_condition->evaluate(&window);
if (m_negated)
result = negate(result);

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021-2023, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -10,8 +10,8 @@
#include <AK/Optional.h>
#include <AK/OwnPtr.h>
#include <AK/RefCounted.h>
#include <LibWeb/CSS/BooleanExpression.h>
#include <LibWeb/CSS/CalculatedOr.h>
#include <LibWeb/CSS/GeneralEnclosed.h>
#include <LibWeb/CSS/MediaFeatureID.h>
#include <LibWeb/CSS/Ratio.h>
@ -94,7 +94,7 @@ private:
};
// https://www.w3.org/TR/mediaqueries-4/#mq-features
class MediaFeature {
class MediaFeature final : public BooleanExpression {
public:
enum class Comparison {
Equal,
@ -105,51 +105,52 @@ public:
};
// Corresponds to `<mf-boolean>` grammar
static MediaFeature boolean(MediaFeatureID id)
static NonnullOwnPtr<MediaFeature> boolean(MediaFeatureID id)
{
return MediaFeature(Type::IsTrue, id);
return adopt_own(*new MediaFeature(Type::IsTrue, id));
}
// Corresponds to `<mf-plain>` grammar
static MediaFeature plain(MediaFeatureID id, MediaFeatureValue value)
static NonnullOwnPtr<MediaFeature> plain(MediaFeatureID id, MediaFeatureValue value)
{
return MediaFeature(Type::ExactValue, move(id), move(value));
return adopt_own(*new MediaFeature(Type::ExactValue, move(id), move(value)));
}
static MediaFeature min(MediaFeatureID id, MediaFeatureValue value)
static NonnullOwnPtr<MediaFeature> min(MediaFeatureID id, MediaFeatureValue value)
{
return MediaFeature(Type::MinValue, id, move(value));
return adopt_own(*new MediaFeature(Type::MinValue, id, move(value)));
}
static MediaFeature max(MediaFeatureID id, MediaFeatureValue value)
static NonnullOwnPtr<MediaFeature> max(MediaFeatureID id, MediaFeatureValue value)
{
return MediaFeature(Type::MaxValue, id, move(value));
return adopt_own(*new MediaFeature(Type::MaxValue, id, move(value)));
}
// Corresponds to `<mf-range>` grammar, with a single comparison
static MediaFeature half_range(MediaFeatureValue value, Comparison comparison, MediaFeatureID id)
static NonnullOwnPtr<MediaFeature> half_range(MediaFeatureValue value, Comparison comparison, MediaFeatureID id)
{
MediaFeature feature { Type::Range, id };
feature.m_range = Range {
.left_value = value,
auto feature = adopt_own(*new MediaFeature(Type::Range, id));
feature->m_range = Range {
.left_value = move(value),
.left_comparison = comparison,
};
return feature;
}
// Corresponds to `<mf-range>` grammar, with two comparisons
static MediaFeature range(MediaFeatureValue left_value, Comparison left_comparison, MediaFeatureID id, Comparison right_comparison, MediaFeatureValue right_value)
static NonnullOwnPtr<MediaFeature> range(MediaFeatureValue left_value, Comparison left_comparison, MediaFeatureID id, Comparison right_comparison, MediaFeatureValue right_value)
{
MediaFeature feature { Type::Range, id };
feature.m_range = Range {
.left_value = left_value,
auto feature = adopt_own(*new MediaFeature(Type::Range, id));
feature->m_range = Range {
.left_value = move(left_value),
.left_comparison = left_comparison,
.right_comparison = right_comparison,
.right_value = right_value,
.right_value = move(right_value),
};
return feature;
}
bool evaluate(HTML::Window const&) const;
String to_string() const;
virtual MatchResult evaluate(HTML::Window const*) const override;
virtual String to_string() const override;
virtual void dump(StringBuilder&, int indent_levels = 0) const override;
private:
enum class Type {
@ -182,39 +183,6 @@ private:
Optional<Range> m_range {};
};
// https://www.w3.org/TR/mediaqueries-4/#media-conditions
struct MediaCondition {
enum class Type {
Single,
And,
Or,
Not,
GeneralEnclosed,
};
// Only used in parsing
enum class AllowOr {
No = 0,
Yes = 1,
};
static NonnullOwnPtr<MediaCondition> from_general_enclosed(GeneralEnclosed&&);
static NonnullOwnPtr<MediaCondition> from_feature(MediaFeature&&);
static NonnullOwnPtr<MediaCondition> from_not(NonnullOwnPtr<MediaCondition>&&);
static NonnullOwnPtr<MediaCondition> from_and_list(Vector<NonnullOwnPtr<MediaCondition>>&&);
static NonnullOwnPtr<MediaCondition> from_or_list(Vector<NonnullOwnPtr<MediaCondition>>&&);
MatchResult evaluate(HTML::Window const&) const;
String to_string() const;
private:
MediaCondition() = default;
Type type;
Optional<MediaFeature> feature;
Vector<NonnullOwnPtr<MediaCondition>> conditions;
Optional<GeneralEnclosed> general_enclosed;
};
class MediaQuery : public RefCounted<MediaQuery> {
friend class Parser::Parser;
@ -252,7 +220,7 @@ private:
// https://www.w3.org/TR/mediaqueries-4/#mq-not
bool m_negated { false };
MediaType m_media_type { MediaType::All };
OwnPtr<MediaCondition> m_media_condition { nullptr };
OwnPtr<BooleanExpression> m_media_condition { nullptr };
// Cached value, updated by evaluate()
bool m_matches { false };
@ -275,14 +243,6 @@ struct Formatter<Web::CSS::MediaFeature> : Formatter<StringView> {
}
};
template<>
struct Formatter<Web::CSS::MediaCondition> : Formatter<StringView> {
ErrorOr<void> format(FormatBuilder& builder, Web::CSS::MediaCondition const& media_condition)
{
return Formatter<StringView>::format(builder, media_condition.to_string());
}
};
template<>
struct Formatter<Web::CSS::MediaQuery> : Formatter<StringView> {
ErrorOr<void> format(FormatBuilder& builder, Web::CSS::MediaQuery const& media_query)

View file

@ -96,11 +96,11 @@ NonnullRefPtr<MediaQuery> Parser::parse_media_query(TokenStream<ComponentValue>&
tokens.discard_whitespace();
// `<media-condition>`
if (auto media_condition = parse_media_condition(tokens, MediaCondition::AllowOr::Yes)) {
if (auto media_condition = parse_media_condition(tokens)) {
tokens.discard_whitespace();
if (tokens.has_next_token())
return invalid_media_query();
media_query->m_media_condition = move(media_condition);
media_query->m_media_condition = media_condition.release_nonnull();
return media_query;
}
@ -127,7 +127,11 @@ NonnullRefPtr<MediaQuery> Parser::parse_media_query(TokenStream<ComponentValue>&
// `[ and <media-condition-without-or> ]?`
if (auto const& maybe_and = tokens.consume_a_token(); maybe_and.is_ident("and"sv)) {
if (auto media_condition = parse_media_condition(tokens, MediaCondition::AllowOr::No)) {
if (auto media_condition = parse_media_condition(tokens)) {
// "or" is disallowed at the top level
if (is<BooleanOrExpression>(*media_condition))
return invalid_media_query();
tokens.discard_whitespace();
if (tokens.has_next_token())
return invalid_media_query();
@ -141,115 +145,15 @@ NonnullRefPtr<MediaQuery> Parser::parse_media_query(TokenStream<ComponentValue>&
}
// `<media-condition>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-condition
// `<media-condition-widthout-or>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-condition-without-or
// (We distinguish between these two with the `allow_or` parameter.)
OwnPtr<MediaCondition> Parser::parse_media_condition(TokenStream<ComponentValue>& tokens, MediaCondition::AllowOr allow_or)
OwnPtr<BooleanExpression> Parser::parse_media_condition(TokenStream<ComponentValue>& tokens)
{
// `<media-not> | <media-in-parens> [ <media-and>* | <media-or>* ]`
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
// `<media-not> = not <media-in-parens>`
auto parse_media_not = [&](auto& tokens) -> OwnPtr<MediaCondition> {
auto local_transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto& first_token = tokens.consume_a_token();
if (first_token.is_ident("not"sv)) {
if (auto child_condition = parse_media_condition(tokens, MediaCondition::AllowOr::Yes)) {
local_transaction.commit();
return MediaCondition::from_not(child_condition.release_nonnull());
}
}
return {};
};
auto parse_media_with_combinator = [&](auto& tokens, StringView combinator) -> OwnPtr<MediaCondition> {
auto local_transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto& first = tokens.consume_a_token();
if (first.is_ident(combinator)) {
tokens.discard_whitespace();
if (auto media_in_parens = parse_media_in_parens(tokens)) {
local_transaction.commit();
return media_in_parens;
}
}
return {};
};
// `<media-and> = and <media-in-parens>`
auto parse_media_and = [&](auto& tokens) { return parse_media_with_combinator(tokens, "and"sv); };
// `<media-or> = or <media-in-parens>`
auto parse_media_or = [&](auto& tokens) { return parse_media_with_combinator(tokens, "or"sv); };
// `<media-not>`
if (auto maybe_media_not = parse_media_not(tokens)) {
transaction.commit();
return maybe_media_not.release_nonnull();
}
// `<media-in-parens> [ <media-and>* | <media-or>* ]`
if (auto maybe_media_in_parens = parse_media_in_parens(tokens)) {
tokens.discard_whitespace();
// Only `<media-in-parens>`
if (!tokens.has_next_token()) {
transaction.commit();
return maybe_media_in_parens.release_nonnull();
}
Vector<NonnullOwnPtr<MediaCondition>> child_conditions;
child_conditions.append(maybe_media_in_parens.release_nonnull());
// `<media-and>*`
if (auto media_and = parse_media_and(tokens)) {
child_conditions.append(media_and.release_nonnull());
tokens.discard_whitespace();
while (tokens.has_next_token()) {
if (auto next_media_and = parse_media_and(tokens)) {
child_conditions.append(next_media_and.release_nonnull());
tokens.discard_whitespace();
continue;
}
// We failed - invalid syntax!
return {};
}
transaction.commit();
return MediaCondition::from_and_list(move(child_conditions));
}
// `<media-or>*`
if (allow_or == MediaCondition::AllowOr::Yes) {
if (auto media_or = parse_media_or(tokens)) {
child_conditions.append(media_or.release_nonnull());
tokens.discard_whitespace();
while (tokens.has_next_token()) {
if (auto next_media_or = parse_media_or(tokens)) {
child_conditions.append(next_media_or.release_nonnull());
tokens.discard_whitespace();
continue;
}
// We failed - invalid syntax!
return {};
}
transaction.commit();
return MediaCondition::from_or_list(move(child_conditions));
}
}
}
return {};
return parse_boolean_expression(tokens, MatchResult::Unknown, [this](TokenStream<ComponentValue>& tokens) -> OwnPtr<BooleanExpression> {
return parse_media_feature(tokens);
});
}
// `<media-feature>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-feature
Optional<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>& tokens)
OwnPtr<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>& tokens)
{
// `[ <mf-plain> | <mf-boolean> | <mf-range> ]`
tokens.discard_whitespace();
@ -288,7 +192,7 @@ Optional<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>&
};
// `<mf-boolean> = <mf-name>`
auto parse_mf_boolean = [&](auto& tokens) -> Optional<MediaFeature> {
auto parse_mf_boolean = [&](auto& tokens) -> OwnPtr<MediaFeature> {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
@ -304,7 +208,7 @@ Optional<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>&
};
// `<mf-plain> = <mf-name> : <mf-value>`
auto parse_mf_plain = [&](auto& tokens) -> Optional<MediaFeature> {
auto parse_mf_plain = [&](auto& tokens) -> OwnPtr<MediaFeature> {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
@ -406,7 +310,7 @@ Optional<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>&
// | <mf-value> <mf-comparison> <mf-name>
// | <mf-value> <mf-lt> <mf-name> <mf-lt> <mf-value>
// | <mf-value> <mf-gt> <mf-name> <mf-gt> <mf-value>`
auto parse_mf_range = [&](auto& tokens) -> Optional<MediaFeature> {
auto parse_mf_range = [&](auto& tokens) -> OwnPtr<MediaFeature> {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
@ -490,14 +394,14 @@ Optional<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>&
return {};
};
if (auto maybe_mf_boolean = parse_mf_boolean(tokens); maybe_mf_boolean.has_value())
return maybe_mf_boolean.release_value();
if (auto maybe_mf_boolean = parse_mf_boolean(tokens))
return maybe_mf_boolean.release_nonnull();
if (auto maybe_mf_plain = parse_mf_plain(tokens); maybe_mf_plain.has_value())
return maybe_mf_plain.release_value();
if (auto maybe_mf_plain = parse_mf_plain(tokens))
return maybe_mf_plain.release_nonnull();
if (auto maybe_mf_range = parse_mf_range(tokens); maybe_mf_range.has_value())
return maybe_mf_range.release_value();
if (auto maybe_mf_range = parse_mf_range(tokens))
return maybe_mf_range.release_nonnull();
return {};
}
@ -517,41 +421,6 @@ Optional<MediaQuery::MediaType> Parser::parse_media_type(TokenStream<ComponentVa
return media_type_from_string(ident);
}
// `<media-in-parens>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-in-parens
OwnPtr<MediaCondition> Parser::parse_media_in_parens(TokenStream<ComponentValue>& tokens)
{
// `<media-in-parens> = ( <media-condition> ) | ( <media-feature> ) | <general-enclosed>`
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
// `( <media-condition> ) | ( <media-feature> )`
auto const& first_token = tokens.next_token();
if (first_token.is_block() && first_token.block().is_paren()) {
TokenStream inner_token_stream { first_token.block().value };
if (auto maybe_media_condition = parse_media_condition(inner_token_stream, MediaCondition::AllowOr::Yes)) {
tokens.discard_a_token();
transaction.commit();
return maybe_media_condition.release_nonnull();
}
if (auto maybe_media_feature = parse_media_feature(inner_token_stream); maybe_media_feature.has_value()) {
tokens.discard_a_token();
transaction.commit();
return MediaCondition::from_feature(maybe_media_feature.release_value());
}
}
// `<general-enclosed>`
// FIXME: We should only be taking this branch if the grammar doesn't match the above options.
// Currently we take it if the above fail to parse, which is different.
// eg, `@media (min-width: 76yaks)` is valid grammar, but does not parse because `yaks` isn't a unit.
if (auto maybe_general_enclosed = parse_general_enclosed(tokens); maybe_general_enclosed.has_value()) {
transaction.commit();
return MediaCondition::from_general_enclosed(maybe_general_enclosed.release_value());
}
return {};
}
// `<mf-value>`, https://www.w3.org/TR/mediaqueries-4/#typedef-mf-value
Optional<MediaFeatureValue> Parser::parse_media_feature_value(MediaFeatureID media_feature, TokenStream<ComponentValue>& tokens)
{

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;
}

View file

@ -13,9 +13,9 @@
#include <AK/Vector.h>
#include <LibGC/Ptr.h>
#include <LibGfx/Font/UnicodeRange.h>
#include <LibWeb/CSS/BooleanExpression.h>
#include <LibWeb/CSS/CSSStyleDeclaration.h>
#include <LibWeb/CSS/CSSStyleValue.h>
#include <LibWeb/CSS/GeneralEnclosed.h>
#include <LibWeb/CSS/MediaQuery.h>
#include <LibWeb/CSS/ParsedFontFace.h>
#include <LibWeb/CSS/Parser/ComponentValue.h>
@ -215,7 +215,7 @@ private:
void consume_a_function_and_do_nothing(TokenStream<Token>&);
// TODO: consume_a_unicode_range_value()
Optional<GeneralEnclosed> parse_general_enclosed(TokenStream<ComponentValue>&);
OwnPtr<GeneralEnclosed> parse_general_enclosed(TokenStream<ComponentValue>&, MatchResult);
template<typename T>
Vector<ParsedFontFace::Source> parse_font_face_src(TokenStream<T>&);
@ -434,15 +434,16 @@ private:
ParseErrorOr<Optional<Selector::SimpleSelector>> parse_simple_selector(TokenStream<ComponentValue>&);
NonnullRefPtr<MediaQuery> parse_media_query(TokenStream<ComponentValue>&);
OwnPtr<MediaCondition> parse_media_condition(TokenStream<ComponentValue>&, MediaCondition::AllowOr allow_or);
Optional<MediaFeature> parse_media_feature(TokenStream<ComponentValue>&);
OwnPtr<BooleanExpression> parse_media_condition(TokenStream<ComponentValue>&);
OwnPtr<MediaFeature> parse_media_feature(TokenStream<ComponentValue>&);
Optional<MediaQuery::MediaType> parse_media_type(TokenStream<ComponentValue>&);
OwnPtr<MediaCondition> parse_media_in_parens(TokenStream<ComponentValue>&);
Optional<MediaFeatureValue> parse_media_feature_value(MediaFeatureID, TokenStream<ComponentValue>&);
OwnPtr<Supports::Condition> parse_supports_condition(TokenStream<ComponentValue>&);
Optional<Supports::InParens> parse_supports_in_parens(TokenStream<ComponentValue>&);
Optional<Supports::Feature> parse_supports_feature(TokenStream<ComponentValue>&);
using ParseTest = AK::Function<OwnPtr<BooleanExpression>(TokenStream<ComponentValue>&)> const&;
OwnPtr<BooleanExpression> parse_boolean_expression(TokenStream<ComponentValue>&, MatchResult result_for_general_enclosed, ParseTest parse_test);
OwnPtr<BooleanExpression> parse_boolean_expression_group(TokenStream<ComponentValue>&, MatchResult result_for_general_enclosed, ParseTest parse_test);
OwnPtr<BooleanExpression> parse_supports_feature(TokenStream<ComponentValue>&);
NonnullRefPtr<CSSStyleValue> resolve_unresolved_style_value(DOM::Element&, Optional<Selector::PseudoElement::Type>, PropertyID, UnresolvedStyleValue const&);
bool expand_variables(DOM::Element&, Optional<Selector::PseudoElement::Type>, FlyString const& property_name, HashMap<FlyString, NonnullRefPtr<PropertyDependencyNode>>& dependencies, TokenStream<ComponentValue>& source, Vector<ComponentValue>& dest);

View file

@ -9,150 +9,47 @@
namespace Web::CSS {
static void indent(StringBuilder& builder, int levels)
{
for (int i = 0; i < levels; i++)
builder.append(" "sv);
}
Supports::Supports(NonnullOwnPtr<Condition>&& condition)
Supports::Supports(NonnullOwnPtr<BooleanExpression>&& condition)
: m_condition(move(condition))
{
m_matches = m_condition->evaluate();
m_matches = m_condition->evaluate_to_boolean(nullptr);
}
bool Supports::Condition::evaluate() const
MatchResult Supports::Declaration::evaluate(HTML::Window const*) const
{
switch (type) {
case Type::Not:
return !children.first().evaluate();
case Type::And:
for (auto& child : children) {
if (!child.evaluate())
return false;
}
return true;
case Type::Or:
for (auto& child : children) {
if (child.evaluate())
return true;
}
return false;
}
VERIFY_NOT_REACHED();
}
bool Supports::InParens::evaluate() const
{
return value.visit(
[&](NonnullOwnPtr<Condition> const& condition) {
return condition->evaluate();
},
[&](Feature const& feature) {
return feature.evaluate();
},
[&](GeneralEnclosed const&) {
return false;
});
}
bool Supports::Feature::evaluate() const
{
return value.visit(
[&](Declaration const& declaration) {
return declaration.evaluate();
},
[&](Selector const& selector) {
return selector.evaluate();
});
return as_match_result(m_matches);
}
String Supports::Declaration::to_string() const
{
return MUST(String::formatted("({})", declaration));
}
String Supports::Selector::to_string() const
{
return MUST(String::formatted("selector({})", selector));
}
String Supports::Feature::to_string() const
{
return value.visit([](auto& it) { return it.to_string(); });
}
String Supports::InParens::to_string() const
{
return value.visit(
[](NonnullOwnPtr<Condition> const& condition) { return MUST(String::formatted("({})", condition->to_string())); },
[](Supports::Feature const& it) { return it.to_string(); },
[](GeneralEnclosed const& it) { return it.to_string(); });
}
String Supports::Condition::to_string() const
{
switch (type) {
case Type::Not:
return MUST(String::formatted("not {}", children.first().to_string()));
case Type::And:
return MUST(String::join(" and "sv, children));
case Type::Or:
return MUST(String::join(" or "sv, children));
}
VERIFY_NOT_REACHED();
}
String Supports::to_string() const
{
return m_condition->to_string();
return MUST(String::formatted("({})", m_declaration));
}
void Supports::Declaration::dump(StringBuilder& builder, int indent_levels) const
{
indent(builder, indent_levels);
builder.appendff("Declaration: {}\n", declaration);
builder.appendff("Declaration: `{}`, matches={}\n", m_declaration, m_matches);
}
MatchResult Supports::Selector::evaluate(HTML::Window const*) const
{
return as_match_result(m_matches);
}
String Supports::Selector::to_string() const
{
return MUST(String::formatted("selector({})", m_selector));
}
void Supports::Selector::dump(StringBuilder& builder, int indent_levels) const
{
indent(builder, indent_levels);
builder.appendff("Selector: {}\n", selector);
builder.appendff("Selector: `{}` matches={}\n", m_selector, m_matches);
}
void Supports::Feature::dump(StringBuilder& builder, int indent_levels) const
String Supports::to_string() const
{
value.visit([&](auto& it) { it.dump(builder, indent_levels); });
}
void Supports::InParens::dump(StringBuilder& builder, int indent_levels) const
{
value.visit(
[&](NonnullOwnPtr<Condition> const& condition) { condition->dump(builder, indent_levels); },
[&](Supports::Feature const& it) { it.dump(builder, indent_levels); },
[&](GeneralEnclosed const& it) {
indent(builder, indent_levels);
builder.appendff("GeneralEnclosed: {}\n", it.to_string());
});
}
void Supports::Condition::dump(StringBuilder& builder, int indent_levels) const
{
indent(builder, indent_levels);
StringView type_name = [](Type type) {
switch (type) {
case Type::And:
return "AND"sv;
case Type::Or:
return "OR"sv;
case Type::Not:
return "NOT"sv;
}
VERIFY_NOT_REACHED();
}(type);
builder.appendff("Condition: {}\n", type_name);
for (auto const& child : children)
child.dump(builder, indent_levels + 1);
return m_condition->to_string();
}
void Supports::dump(StringBuilder& builder, int indent_levels) const

View file

@ -9,62 +9,58 @@
#include <AK/NonnullOwnPtr.h>
#include <AK/RefCounted.h>
#include <AK/String.h>
#include <AK/Variant.h>
#include <AK/Vector.h>
#include <LibWeb/CSS/GeneralEnclosed.h>
#include <LibWeb/CSS/BooleanExpression.h>
namespace Web::CSS {
// https://www.w3.org/TR/css-conditional-3/#at-supports
class Supports final : public RefCounted<Supports> {
public:
struct Declaration {
String declaration;
bool matches;
[[nodiscard]] bool evaluate() const { return matches; }
String to_string() const;
void dump(StringBuilder&, int indent_levels = 0) const;
class Declaration final : public BooleanExpression {
public:
static NonnullOwnPtr<Declaration> create(String declaration, bool matches)
{
return adopt_own(*new Declaration(move(declaration), matches));
}
virtual ~Declaration() override = default;
virtual MatchResult evaluate(HTML::Window const*) const override;
virtual String to_string() const override;
virtual void dump(StringBuilder&, int indent_levels = 0) const override;
private:
Declaration(String declaration, bool matches)
: m_declaration(move(declaration))
, m_matches(matches)
{
}
String m_declaration;
bool m_matches;
};
struct Selector {
String selector;
bool matches;
[[nodiscard]] bool evaluate() const { return matches; }
String to_string() const;
void dump(StringBuilder&, int indent_levels = 0) const;
class Selector final : public BooleanExpression {
public:
static NonnullOwnPtr<Selector> create(String selector, bool matches)
{
return adopt_own(*new Selector(move(selector), matches));
}
virtual ~Selector() override = default;
virtual MatchResult evaluate(HTML::Window const*) const override;
virtual String to_string() const override;
virtual void dump(StringBuilder&, int indent_levels = 0) const override;
private:
Selector(String selector, bool matches)
: m_selector(move(selector))
, m_matches(matches)
{
}
String m_selector;
bool m_matches;
};
struct Feature {
Variant<Declaration, Selector> value;
[[nodiscard]] bool evaluate() const;
String to_string() const;
void dump(StringBuilder&, int indent_levels = 0) const;
};
struct Condition;
struct InParens {
Variant<NonnullOwnPtr<Condition>, Feature, GeneralEnclosed> value;
[[nodiscard]] bool evaluate() const;
String to_string() const;
void dump(StringBuilder&, int indent_levels = 0) const;
};
struct Condition {
enum class Type {
Not,
And,
Or,
};
Type type;
Vector<InParens> children;
[[nodiscard]] bool evaluate() const;
String to_string() const;
void dump(StringBuilder&, int indent_levels = 0) const;
};
static NonnullRefPtr<Supports> create(NonnullOwnPtr<Condition>&& condition)
static NonnullRefPtr<Supports> create(NonnullOwnPtr<BooleanExpression>&& condition)
{
return adopt_ref(*new Supports(move(condition)));
}
@ -75,18 +71,10 @@ public:
void dump(StringBuilder&, int indent_levels = 0) const;
private:
Supports(NonnullOwnPtr<Condition>&&);
Supports(NonnullOwnPtr<BooleanExpression>&&);
NonnullOwnPtr<Condition> m_condition;
NonnullOwnPtr<BooleanExpression> m_condition;
bool m_matches { false };
};
}
template<>
struct AK::Formatter<Web::CSS::Supports::InParens> : AK::Formatter<StringView> {
ErrorOr<void> format(FormatBuilder& builder, Web::CSS::Supports::InParens const& in_parens)
{
return Formatter<StringView>::format(builder, in_parens.to_string());
}
};

View file

@ -1,6 +1,6 @@
@media screen {
}
@media screen and ((min-width: 20px) and (max-width: 40px)) {
@media screen and (min-width: 20px) and (max-width: 40px) {
}
@media screen and (min-resolution: 1dppx) {
}

View file

@ -2,8 +2,7 @@ Harness status: OK
Found 12 tests
10 Pass
2 Fail
12 Pass
Pass empty media query list
Pass type - no features
Pass type - no features - negation
@ -11,8 +10,8 @@ Pass type - no features - character case normalization
Pass type - omission of all
Pass type - inclusion of negated all
Pass features - character case normalization
Fail features - preservation of overspecified features
Fail features - no lexicographical sorting
Pass features - preservation of overspecified features
Pass features - no lexicographical sorting
Pass media query list
Pass one rule
Pass many rules