LibWeb/CSS: Extract base class from MediaFeature

`<media-feature>` and the upcoming `<size-feature>` from `@container`,
share the same syntax and almost all of their behaviour. To avoid a lot
of duplication, pull as much as possible into a FeatureQuery template
class that they will both inherit from.

MediaFeatureValue is renamed FeatureValue as it's also shared by both.

No behaviour change.
This commit is contained in:
Sam Atkins 2026-05-14 11:22:48 +01:00
parent 34382a2aca
commit 9996403e73
13 changed files with 642 additions and 541 deletions

View file

@ -315,12 +315,11 @@ The definitions here are like a simplified version of the `Properties.json` defi
| `false-keywords` | Array of strings. These are any keywords that should be considered false when the media feature is evaluated as `@media (foo)`. Generally this will be a single value, such as `"none"`. |
The generated code provides:
- A `MediaFeatureValueType` enum listing the possible value types
- A `MediaFeatureID` enum, listing each media-feature
- `Optional<MediaFeatureID> media_feature_id_from_string(StringView)` to convert a string to a `MediaFeatureID`
- `StringView string_from_media_feature_id(MediaFeatureID)` to convert a `MediaFeatureID` back to a string
- `bool media_feature_type_is_range(MediaFeatureID)` returns whether the media feature is a `range` type, as opposed to a `discrete` type
- `bool media_feature_accepts_type(MediaFeatureID, MediaFeatureValueType)` returns whether the media feature will accept values of this type
- `bool media_feature_accepts_type(MediaFeatureID, QueryValueType)` returns whether the media feature will accept values of this type
- `bool media_feature_accepts_keyword(MediaFeatureID, Keyword)` returns whether the media feature accepts this keyword
- `bool media_feature_keyword_is_falsey(MediaFeatureID, Keyword)` returns whether the given keyword is considered false when the media-feature is evaluated in a boolean context. (Like `@media (foo)`)

View file

@ -165,6 +165,7 @@ set(SOURCES
CSS/Display.cpp
CSS/EasingFunction.cpp
CSS/EdgeRect.cpp
CSS/FeatureQuery.cpp
CSS/Fetch.cpp
CSS/Filter.cpp
CSS/Flex.cpp

View file

@ -0,0 +1,141 @@
/*
* Copyright (c) 2021-2026, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/FeatureQuery.h>
namespace Web::CSS {
String FeatureValue::to_string(SerializationMode mode) const
{
StringBuilder builder;
m_value->serialize(builder, mode);
return MUST(builder.to_string());
}
StringView string_from_feature_comparison(FeatureComparison comparison)
{
switch (comparison) {
case FeatureComparison::Equal:
return "="sv;
case FeatureComparison::LessThan:
return "<"sv;
case FeatureComparison::LessThanOrEqual:
return "<="sv;
case FeatureComparison::GreaterThan:
return ">"sv;
case FeatureComparison::GreaterThanOrEqual:
return ">="sv;
}
VERIFY_NOT_REACHED();
}
bool feature_comparisons_match(FeatureComparison a, FeatureComparison b)
{
switch (a) {
case FeatureComparison::Equal:
return b == FeatureComparison::Equal;
case FeatureComparison::LessThan:
case FeatureComparison::LessThanOrEqual:
return b == FeatureComparison::LessThan || b == FeatureComparison::LessThanOrEqual;
case FeatureComparison::GreaterThan:
case FeatureComparison::GreaterThanOrEqual:
return b == FeatureComparison::GreaterThan || b == FeatureComparison::GreaterThanOrEqual;
}
VERIFY_NOT_REACHED();
}
MatchResult compare_feature_values(FeatureValue const& left, FeatureComparison comparison, FeatureValue const& right, ComputationContext const& computation_context)
{
if (left.is_unknown() || right.is_unknown())
return MatchResult::Unknown;
if (!left.is_same_type(right))
return MatchResult::False;
if (left.is_ident()) {
if (comparison == FeatureComparison::Equal)
return as_match_result(left.ident() == right.ident());
return MatchResult::False;
}
if (left.is_integer()) {
switch (comparison) {
case FeatureComparison::Equal:
return as_match_result(left.integer(computation_context) == right.integer(computation_context));
case FeatureComparison::LessThan:
return as_match_result(left.integer(computation_context) < right.integer(computation_context));
case FeatureComparison::LessThanOrEqual:
return as_match_result(left.integer(computation_context) <= right.integer(computation_context));
case FeatureComparison::GreaterThan:
return as_match_result(left.integer(computation_context) > right.integer(computation_context));
case FeatureComparison::GreaterThanOrEqual:
return as_match_result(left.integer(computation_context) >= right.integer(computation_context));
}
VERIFY_NOT_REACHED();
}
if (left.is_length()) {
auto left_px = left.length(computation_context).absolute_length_to_px();
auto right_px = right.length(computation_context).absolute_length_to_px();
switch (comparison) {
case FeatureComparison::Equal:
return as_match_result(left_px == right_px);
case FeatureComparison::LessThan:
return as_match_result(left_px < right_px);
case FeatureComparison::LessThanOrEqual:
return as_match_result(left_px <= right_px);
case FeatureComparison::GreaterThan:
return as_match_result(left_px > right_px);
case FeatureComparison::GreaterThanOrEqual:
return as_match_result(left_px >= right_px);
}
VERIFY_NOT_REACHED();
}
if (left.is_ratio()) {
auto left_decimal = left.ratio(computation_context).value();
auto right_decimal = right.ratio(computation_context).value();
switch (comparison) {
case FeatureComparison::Equal:
return as_match_result(left_decimal == right_decimal);
case FeatureComparison::LessThan:
return as_match_result(left_decimal < right_decimal);
case FeatureComparison::LessThanOrEqual:
return as_match_result(left_decimal <= right_decimal);
case FeatureComparison::GreaterThan:
return as_match_result(left_decimal > right_decimal);
case FeatureComparison::GreaterThanOrEqual:
return as_match_result(left_decimal >= right_decimal);
}
VERIFY_NOT_REACHED();
}
if (left.is_resolution()) {
auto left_dppx = left.resolution(computation_context).to_dots_per_pixel();
auto right_dppx = right.resolution(computation_context).to_dots_per_pixel();
switch (comparison) {
case FeatureComparison::Equal:
return as_match_result(left_dppx == right_dppx);
case FeatureComparison::LessThan:
return as_match_result(left_dppx < right_dppx);
case FeatureComparison::LessThanOrEqual:
return as_match_result(left_dppx <= right_dppx);
case FeatureComparison::GreaterThan:
return as_match_result(left_dppx > right_dppx);
case FeatureComparison::GreaterThanOrEqual:
return as_match_result(left_dppx >= right_dppx);
}
VERIFY_NOT_REACHED();
}
VERIFY_NOT_REACHED();
}
}

View file

@ -0,0 +1,264 @@
/*
* Copyright (c) 2021-2026, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullOwnPtr.h>
#include <AK/NonnullRefPtr.h>
#include <AK/Optional.h>
#include <AK/StringBuilder.h>
#include <AK/Variant.h>
#include <LibWeb/CSS/BooleanExpression.h>
#include <LibWeb/CSS/Ratio.h>
#include <LibWeb/CSS/Resolution.h>
#include <LibWeb/CSS/StyleValues/ComputationContext.h>
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/CSS/StyleValues/RatioStyleValue.h>
#include <LibWeb/CSS/StyleValues/StyleValue.h>
namespace Web::CSS {
// https://drafts.csswg.org/mediaqueries-5/#typedef-mf-value
class FeatureValue {
public:
enum class Type : u8 {
Ident,
Integer,
Length,
Ratio,
Resolution,
Unknown,
};
explicit FeatureValue(Type type, NonnullRefPtr<StyleValue const> value)
: m_type(type)
, m_value(move(value))
{
}
String to_string(SerializationMode mode) const;
bool is_ident() const { return m_type == Type::Ident; }
bool is_length() const { return m_type == Type::Length; }
bool is_integer() const { return m_type == Type::Integer; }
bool is_ratio() const { return m_type == Type::Ratio; }
bool is_resolution() const { return m_type == Type::Resolution; }
bool is_unknown() const { return m_type == Type::Unknown; }
bool is_same_type(FeatureValue const& other) const { return m_type == other.m_type; }
Keyword ident() const
{
VERIFY(is_ident());
return m_value->to_keyword();
}
Length length(ComputationContext const& computation_context) const
{
VERIFY(is_length());
return Length::from_style_value(m_value->absolutized(computation_context), {});
}
Ratio ratio(ComputationContext const& computation_context) const
{
VERIFY(is_ratio());
return m_value->absolutized(computation_context)->as_ratio().resolved();
}
Resolution resolution(ComputationContext const& computation_context) const
{
VERIFY(is_resolution());
return Resolution::from_style_value(m_value->absolutized(computation_context));
}
i32 integer(ComputationContext const& computation_context) const
{
VERIFY(is_integer());
return int_from_style_value(m_value->absolutized(computation_context));
}
private:
Type m_type;
NonnullRefPtr<StyleValue const> m_value;
};
enum class FeatureComparison : u8 {
Equal,
LessThan,
LessThanOrEqual,
GreaterThan,
GreaterThanOrEqual,
};
StringView string_from_feature_comparison(FeatureComparison);
bool feature_comparisons_match(FeatureComparison, FeatureComparison);
MatchResult compare_feature_values(FeatureValue const& left, FeatureComparison comparison, FeatureValue const& right, ComputationContext const&);
template<typename Derived, typename FeatureID>
class FeatureQuery : public BooleanExpression {
public:
enum class Type : u8 {
IsTrue,
ExactValue,
MinValue,
MaxValue,
Range,
};
struct Range {
Optional<FeatureValue> left_value {};
Optional<FeatureComparison> left_comparison {};
Optional<FeatureComparison> right_comparison {};
Optional<FeatureValue> right_value {};
};
static NonnullOwnPtr<Derived> boolean(FeatureID id)
{
return adopt_own(*new Derived(Type::IsTrue, id));
}
static NonnullOwnPtr<Derived> plain(FeatureID id, FeatureValue&& value)
{
return adopt_own(*new Derived(Type::ExactValue, id, move(value)));
}
static NonnullOwnPtr<Derived> min(FeatureID id, FeatureValue&& value)
{
return adopt_own(*new Derived(Type::MinValue, id, move(value)));
}
static NonnullOwnPtr<Derived> max(FeatureID id, FeatureValue&& value)
{
return adopt_own(*new Derived(Type::MaxValue, id, move(value)));
}
static NonnullOwnPtr<Derived> half_range(FeatureValue&& value, FeatureComparison comparison, FeatureID id)
{
return adopt_own(*new Derived(Type::Range, id,
Range {
.left_value = move(value),
.left_comparison = comparison,
}));
}
static NonnullOwnPtr<Derived> half_range(FeatureID id, FeatureComparison comparison, FeatureValue&& value)
{
return adopt_own(*new Derived(Type::Range, id,
Range {
.right_comparison = comparison,
.right_value = move(value),
}));
}
static NonnullOwnPtr<Derived> range(FeatureValue&& left_value, FeatureComparison left_comparison, FeatureID id, FeatureComparison right_comparison, FeatureValue&& right_value)
{
return adopt_own(*new Derived(Type::Range, id,
Range {
.left_value = move(left_value),
.left_comparison = left_comparison,
.right_comparison = right_comparison,
.right_value = move(right_value),
}));
}
Type type() const { return m_type; }
FeatureID id() const { return m_id; }
FeatureValue const& value() const { return m_value.template get<FeatureValue>(); }
Range const& range() const { return m_value.template get<Range>(); }
virtual String to_string() const override
{
// NB: Even though the surrounding boolean-expression grammar owns the parentheses, feature serialization
// includes them so callers do not need a wrapper node just for serialization.
switch (m_type) {
case Type::IsTrue:
return MUST(String::formatted("({})", Derived::serialize_feature_id(m_id)));
case Type::ExactValue:
return MUST(String::formatted("({}: {})", Derived::serialize_feature_id(m_id), value().to_string(SerializationMode::Normal)));
case Type::MinValue:
return MUST(String::formatted("(min-{}: {})", Derived::serialize_feature_id(m_id), value().to_string(SerializationMode::Normal)));
case Type::MaxValue:
return MUST(String::formatted("(max-{}: {})", Derived::serialize_feature_id(m_id), value().to_string(SerializationMode::Normal)));
case Type::Range: {
auto& range = this->range();
StringBuilder builder;
builder.append('(');
if (range.left_comparison.has_value())
builder.appendff("{} {} ", range.left_value->to_string(SerializationMode::Normal), string_from_feature_comparison(*range.left_comparison));
builder.append(Derived::serialize_feature_id(m_id));
if (range.right_comparison.has_value())
builder.appendff(" {} {}", string_from_feature_comparison(*range.right_comparison), range.right_value->to_string(SerializationMode::Normal));
builder.append(')');
return builder.to_string_without_validation();
}
}
VERIFY_NOT_REACHED();
}
protected:
FeatureQuery(Type type, FeatureID id, Variant<Empty, FeatureValue, Range> value = {})
: m_type(type)
, m_id(id)
, m_value(move(value))
{
}
MatchResult evaluate_internal(FeatureValue const& queried_value, ComputationContext const& computation_context) const
{
switch (type()) {
case Type::IsTrue:
if (queried_value.is_integer())
return as_match_result(queried_value.integer(computation_context) != 0);
if (queried_value.is_length()) {
auto length = queried_value.length(computation_context);
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 as_match_result(!queried_value.ratio(computation_context).is_degenerate());
if (queried_value.is_resolution())
return as_match_result(queried_value.resolution(computation_context).to_dots_per_pixel() != 0);
if (queried_value.is_ident()) {
if (Derived::keyword_is_falsey(id(), queried_value.ident()))
return MatchResult::False;
return MatchResult::True;
}
return MatchResult::False;
case Type::ExactValue:
return compare_feature_values(value(), FeatureComparison::Equal, queried_value, computation_context);
case Type::MinValue:
return compare_feature_values(queried_value, FeatureComparison::GreaterThanOrEqual, value(), computation_context);
case Type::MaxValue:
return compare_feature_values(queried_value, FeatureComparison::LessThanOrEqual, value(), computation_context);
case Type::Range: {
auto const& range = this->range();
if (range.left_comparison.has_value()) {
if (auto const left_result = compare_feature_values(*range.left_value, *range.left_comparison, queried_value, computation_context); left_result != MatchResult::True)
return left_result;
}
if (range.right_comparison.has_value()) {
if (auto const right_result = compare_feature_values(queried_value, *range.right_comparison, *range.right_value, computation_context); right_result != MatchResult::True)
return right_result;
}
return MatchResult::True;
}
}
VERIFY_NOT_REACHED();
}
Type m_type;
FeatureID m_id;
Variant<Empty, FeatureValue, Range> m_value {};
};
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2021-2026, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -10,7 +10,6 @@
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Dump.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Page/Page.h>
namespace Web::CSS {
@ -26,58 +25,14 @@ NonnullRefPtr<MediaQuery> MediaQuery::create_not_all()
return adopt_ref(*media_query);
}
String MediaFeatureValue::to_string(SerializationMode mode) const
StringView MediaFeature::serialize_feature_id(MediaFeatureID id)
{
StringBuilder builder;
m_value->serialize(builder, mode);
return MUST(builder.to_string());
return string_from_media_feature_id(id);
}
String MediaFeature::to_string() const
bool MediaFeature::keyword_is_falsey(MediaFeatureID id, Keyword keyword)
{
auto comparison_string = [](Comparison comparison) -> StringView {
switch (comparison) {
case Comparison::Equal:
return "="sv;
case Comparison::LessThan:
return "<"sv;
case Comparison::LessThanOrEqual:
return "<="sv;
case Comparison::GreaterThan:
return ">"sv;
case Comparison::GreaterThanOrEqual:
return ">="sv;
}
VERIFY_NOT_REACHED();
};
// NB: Even though we parse the parentheses as part of <media-in-parens> rather than <media-feature>, we serialize
// them as part of <media-feature> to avoid having to create a whole MediaInParens class just for serialization.
switch (m_type) {
case Type::IsTrue:
return MUST(String::formatted("({})", string_from_media_feature_id(m_id)));
case Type::ExactValue:
return MUST(String::formatted("({}: {})", string_from_media_feature_id(m_id), value().to_string(SerializationMode::Normal)));
case Type::MinValue:
return MUST(String::formatted("(min-{}: {})", string_from_media_feature_id(m_id), value().to_string(SerializationMode::Normal)));
case Type::MaxValue:
return MUST(String::formatted("(max-{}: {})", string_from_media_feature_id(m_id), value().to_string(SerializationMode::Normal)));
case Type::Range: {
auto& range = this->range();
StringBuilder builder;
builder.append('(');
if (range.left_comparison.has_value())
builder.appendff("{} {} ", range.left_value->to_string(SerializationMode::Normal), comparison_string(*range.left_comparison));
builder.append(string_from_media_feature_id(m_id));
if (range.right_comparison.has_value())
builder.appendff(" {} {}", comparison_string(*range.right_comparison), range.right_value->to_string(SerializationMode::Normal));
builder.append(')');
return builder.to_string_without_validation();
}
}
VERIFY_NOT_REACHED();
return media_feature_keyword_is_falsey(id, keyword);
}
MatchResult MediaFeature::evaluate(BooleanExpressionEvaluationContext const& context) const
@ -90,157 +45,14 @@ MatchResult MediaFeature::evaluate(BooleanExpressionEvaluationContext const& con
if (!document->window())
return MatchResult::False;
auto maybe_queried_value = document->window()->query_media_feature(m_id);
if (!maybe_queried_value.has_value())
auto queried_value = document->window()->query_media_feature(id());
if (!queried_value.has_value())
return MatchResult::False;
auto queried_value = maybe_queried_value.release_value();
ComputationContext computation_context {
.length_resolution_context = Length::ResolutionContext::for_document(*document),
};
switch (m_type) {
case Type::IsTrue:
if (queried_value.is_integer())
return as_match_result(queried_value.integer(computation_context) != 0);
if (queried_value.is_length()) {
auto length = queried_value.length(computation_context);
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 as_match_result(!queried_value.ratio(computation_context).is_degenerate());
if (queried_value.is_resolution())
return as_match_result(queried_value.resolution(computation_context).to_dots_per_pixel() != 0);
if (queried_value.is_ident()) {
if (media_feature_keyword_is_falsey(m_id, queried_value.ident()))
return MatchResult::False;
return MatchResult::True;
}
return MatchResult::False;
case Type::ExactValue:
return compare(*document, value(), Comparison::Equal, queried_value);
case Type::MinValue:
return compare(*document, queried_value, Comparison::GreaterThanOrEqual, value());
case Type::MaxValue:
return compare(*document, queried_value, Comparison::LessThanOrEqual, value());
case Type::Range: {
auto const& range = this->range();
if (range.left_comparison.has_value()) {
if (auto const left_result = compare(*document, *range.left_value, *range.left_comparison, queried_value); left_result != MatchResult::True)
return left_result;
}
if (range.right_comparison.has_value()) {
if (auto const right_result = compare(*document, queried_value, *range.right_comparison, *range.right_value); right_result != MatchResult::True)
return right_result;
}
return MatchResult::True;
}
}
VERIFY_NOT_REACHED();
}
MatchResult MediaFeature::compare(DOM::Document const& document, MediaFeatureValue const& left, Comparison comparison, MediaFeatureValue const& right)
{
if (left.is_unknown() || right.is_unknown())
return MatchResult::Unknown;
if (!left.is_same_type(right))
return MatchResult::False;
if (left.is_ident()) {
if (comparison == Comparison::Equal)
return as_match_result(left.ident() == right.ident());
return MatchResult::False;
}
auto length_resolution_context = Length::ResolutionContext::for_document(document);
ComputationContext computation_context {
.length_resolution_context = length_resolution_context,
};
if (left.is_integer()) {
switch (comparison) {
case Comparison::Equal:
return as_match_result(left.integer(computation_context) == right.integer(computation_context));
case Comparison::LessThan:
return as_match_result(left.integer(computation_context) < right.integer(computation_context));
case Comparison::LessThanOrEqual:
return as_match_result(left.integer(computation_context) <= right.integer(computation_context));
case Comparison::GreaterThan:
return as_match_result(left.integer(computation_context) > right.integer(computation_context));
case Comparison::GreaterThanOrEqual:
return as_match_result(left.integer(computation_context) >= right.integer(computation_context));
}
VERIFY_NOT_REACHED();
}
if (left.is_length()) {
auto left_px = left.length(computation_context).absolute_length_to_px();
auto right_px = right.length(computation_context).absolute_length_to_px();
switch (comparison) {
case Comparison::Equal:
return as_match_result(left_px == right_px);
case Comparison::LessThan:
return as_match_result(left_px < right_px);
case Comparison::LessThanOrEqual:
return as_match_result(left_px <= right_px);
case Comparison::GreaterThan:
return as_match_result(left_px > right_px);
case Comparison::GreaterThanOrEqual:
return as_match_result(left_px >= right_px);
}
VERIFY_NOT_REACHED();
}
if (left.is_ratio()) {
auto left_decimal = left.ratio(computation_context).value();
auto right_decimal = right.ratio(computation_context).value();
switch (comparison) {
case Comparison::Equal:
return as_match_result(left_decimal == right_decimal);
case Comparison::LessThan:
return as_match_result(left_decimal < right_decimal);
case Comparison::LessThanOrEqual:
return as_match_result(left_decimal <= right_decimal);
case Comparison::GreaterThan:
return as_match_result(left_decimal > right_decimal);
case Comparison::GreaterThanOrEqual:
return as_match_result(left_decimal >= right_decimal);
}
VERIFY_NOT_REACHED();
}
if (left.is_resolution()) {
auto left_dppx = left.resolution(computation_context).to_dots_per_pixel();
auto right_dppx = right.resolution(computation_context).to_dots_per_pixel();
switch (comparison) {
case Comparison::Equal:
return as_match_result(left_dppx == right_dppx);
case Comparison::LessThan:
return as_match_result(left_dppx < right_dppx);
case Comparison::LessThanOrEqual:
return as_match_result(left_dppx <= right_dppx);
case Comparison::GreaterThan:
return as_match_result(left_dppx > right_dppx);
case Comparison::GreaterThanOrEqual:
return as_match_result(left_dppx >= right_dppx);
}
VERIFY_NOT_REACHED();
}
VERIFY_NOT_REACHED();
return evaluate_internal(queried_value.value(), computation_context);
}
void MediaFeature::dump(StringBuilder& builder, int indent_levels) const

View file

@ -12,172 +12,30 @@
#include <AK/OwnPtr.h>
#include <AK/RefCounted.h>
#include <LibWeb/CSS/BooleanExpression.h>
#include <LibWeb/CSS/FeatureQuery.h>
#include <LibWeb/CSS/MediaFeatureID.h>
#include <LibWeb/CSS/Parser/ComponentValue.h>
#include <LibWeb/CSS/Ratio.h>
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
#include <LibWeb/CSS/StyleValues/RatioStyleValue.h>
#include <LibWeb/CSS/StyleValues/ResolutionStyleValue.h>
namespace Web::CSS {
// https://www.w3.org/TR/mediaqueries-4/#typedef-mf-value
class MediaFeatureValue {
public:
enum class Type : u8 {
Ident,
Length,
Ratio,
Resolution,
Integer,
Unknown,
};
explicit MediaFeatureValue(Type type, NonnullRefPtr<StyleValue const> value)
: m_type(type)
, m_value(move(value))
{
}
String to_string(SerializationMode mode) const;
bool is_ident() const { return m_type == Type::Ident; }
bool is_length() const { return m_type == Type::Length; }
bool is_integer() const { return m_type == Type::Integer; }
bool is_ratio() const { return m_type == Type::Ratio; }
bool is_resolution() const { return m_type == Type::Resolution; }
bool is_unknown() const { return m_type == Type::Unknown; }
bool is_same_type(MediaFeatureValue const& other) const { return m_type == other.m_type; }
Keyword ident() const
{
VERIFY(is_ident());
return m_value->to_keyword();
}
Length length(ComputationContext const& computation_context) const
{
VERIFY(is_length());
return Length::from_style_value(m_value->absolutized(computation_context), {});
}
Ratio ratio(ComputationContext const& computation_context) const
{
VERIFY(is_ratio());
return m_value->absolutized(computation_context)->as_ratio().resolved();
}
Resolution resolution(ComputationContext const& computation_context) const
{
VERIFY(is_resolution());
return Resolution::from_style_value(m_value->absolutized(computation_context));
}
i32 integer(ComputationContext const& computation_context) const
{
VERIFY(is_integer());
return int_from_style_value(m_value->absolutized(computation_context));
}
private:
Type m_type;
NonnullRefPtr<StyleValue const> m_value;
};
// https://www.w3.org/TR/mediaqueries-4/#mq-features
class MediaFeature final : public BooleanExpression {
class MediaFeature final : public FeatureQuery<MediaFeature, MediaFeatureID> {
public:
enum class Comparison : u8 {
Equal,
LessThan,
LessThanOrEqual,
GreaterThan,
GreaterThanOrEqual,
};
// Corresponds to `<mf-boolean>` grammar
static NonnullOwnPtr<MediaFeature> boolean(MediaFeatureID id)
{
return adopt_own(*new MediaFeature(Type::IsTrue, id));
}
// Corresponds to `<mf-plain>` grammar
static NonnullOwnPtr<MediaFeature> plain(MediaFeatureID id, MediaFeatureValue value)
{
return adopt_own(*new MediaFeature(Type::ExactValue, move(id), move(value)));
}
static NonnullOwnPtr<MediaFeature> min(MediaFeatureID id, MediaFeatureValue value)
{
return adopt_own(*new MediaFeature(Type::MinValue, id, move(value)));
}
static NonnullOwnPtr<MediaFeature> max(MediaFeatureID id, MediaFeatureValue value)
{
return adopt_own(*new MediaFeature(Type::MaxValue, id, move(value)));
}
static NonnullOwnPtr<MediaFeature> half_range(MediaFeatureValue value, Comparison comparison, MediaFeatureID id)
{
return adopt_own(*new MediaFeature(Type::Range, id,
Range {
.left_value = move(value),
.left_comparison = comparison,
}));
}
static NonnullOwnPtr<MediaFeature> half_range(MediaFeatureID id, Comparison comparison, MediaFeatureValue value)
{
return adopt_own(*new MediaFeature(Type::Range, id,
Range {
.right_comparison = comparison,
.right_value = move(value),
}));
}
// Corresponds to `<mf-range>` grammar, with two comparisons
static NonnullOwnPtr<MediaFeature> range(MediaFeatureValue left_value, Comparison left_comparison, MediaFeatureID id, Comparison right_comparison, MediaFeatureValue right_value)
{
return adopt_own(*new MediaFeature(Type::Range, id,
Range {
.left_value = move(left_value),
.left_comparison = left_comparison,
.right_comparison = right_comparison,
.right_value = move(right_value),
}));
}
using Base = FeatureQuery<MediaFeature, MediaFeatureID>;
virtual MatchResult evaluate(BooleanExpressionEvaluationContext const&) const override;
virtual String to_string() const override;
virtual void dump(StringBuilder&, int indent_levels = 0) const override;
static StringView serialize_feature_id(MediaFeatureID);
static bool keyword_is_falsey(MediaFeatureID, Keyword);
private:
enum class Type : u8 {
IsTrue,
ExactValue,
MinValue,
MaxValue,
Range,
};
friend Base;
struct Range {
Optional<MediaFeatureValue> left_value {};
Optional<Comparison> left_comparison {};
Optional<Comparison> right_comparison {};
Optional<MediaFeatureValue> right_value {};
};
MediaFeature(Type type, MediaFeatureID id, Variant<Empty, MediaFeatureValue, Range> value = {})
: m_type(type)
, m_id(move(id))
, m_value(move(value))
MediaFeature(Type type, MediaFeatureID id, Variant<Empty, FeatureValue, Range> value = {})
: Base(type, id, move(value))
{
}
static MatchResult compare(DOM::Document const& document, MediaFeatureValue const& left, Comparison comparison, MediaFeatureValue const& right);
MediaFeatureValue const& value() const { return m_value.get<MediaFeatureValue>(); }
Range const& range() const { return m_value.get<Range>(); }
Type m_type;
MediaFeatureID m_id;
Variant<Empty, MediaFeatureValue, Range> m_value {};
};
class MediaQuery : public RefCounted<MediaQuery> {

View file

@ -1,7 +1,7 @@
/*
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2021-2026, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
* Copyright (c) 2022, MacDue <macdue@dueutil.tech>
*
@ -15,6 +15,7 @@
#include <LibWeb/CSS/MediaQuery.h>
#include <LibWeb/CSS/Parser/ErrorReporter.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/QueryValueType.h>
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
#include <LibWeb/CSS/StyleValues/LengthStyleValue.h>
#include <LibWeb/CSS/StyleValues/UnresolvedStyleValue.h>
@ -170,37 +171,81 @@ OwnPtr<BooleanExpression> Parser::parse_media_condition(TokenStream<ComponentVal
});
}
// `<media-feature>`, https://drafts.csswg.org/mediaqueries-5/#typedef-media-feature
OwnPtr<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>& inner_tokens)
enum class FeatureNameType : u8 {
Normal,
Min,
Max,
};
template<typename FeatureID>
struct FeatureName {
FeatureNameType type;
FeatureID id;
};
// `<mf-lt> = '<' '='?
// <mf-gt> = '>' '='?
// <mf-eq> = '='
// <mf-comparison> = <mf-lt> | <mf-gt> | <mf-eq>`
static Optional<FeatureComparison> parse_feature_comparison(TokenStream<ComponentValue>& tokens)
{
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto& first = tokens.consume_a_token();
if (first.is(Token::Type::Delim)) {
auto first_delim = first.token().delim();
if (first_delim == '=') {
transaction.commit();
return FeatureComparison::Equal;
}
if (first_delim == '<') {
auto& second = tokens.next_token();
if (second.is_delim('=')) {
tokens.discard_a_token();
transaction.commit();
return FeatureComparison::LessThanOrEqual;
}
transaction.commit();
return FeatureComparison::LessThan;
}
if (first_delim == '>') {
auto& second = tokens.next_token();
if (second.is_delim('=')) {
tokens.discard_a_token();
transaction.commit();
return FeatureComparison::GreaterThanOrEqual;
}
transaction.commit();
return FeatureComparison::GreaterThan;
}
}
return {};
}
template<typename Feature, typename FeatureID, typename FeatureNameFromString, typename ParseFeatureValue, typename AllowsRangeSyntax>
static OwnPtr<Feature> parse_query_feature(TokenStream<ComponentValue>& inner_tokens, FeatureNameFromString feature_name_from_string, ParseFeatureValue parse_feature_value, AllowsRangeSyntax allows_range_syntax)
{
// `<media-feature> = [ <mf-plain> | <mf-boolean> | <mf-range> ]`
auto transaction = inner_tokens.begin_transaction();
// `<mf-name> = <ident>`
struct MediaFeatureName {
enum Type {
Normal,
Min,
Max
} type;
MediaFeatureID id;
};
auto parse_mf_name = [](auto& tokens, bool allow_min_max_prefix) -> Optional<MediaFeatureName> {
auto parse_feature_name = [&](auto& tokens, bool allow_min_max_prefix) -> Optional<FeatureName<FeatureID>> {
auto transaction = tokens.begin_transaction();
auto& token = tokens.consume_a_token();
if (token.is(Token::Type::Ident)) {
auto name = token.token().ident();
if (auto id = media_feature_id_from_string(name); id.has_value()) {
if (auto id = feature_name_from_string(name); id.has_value()) {
transaction.commit();
return MediaFeatureName { MediaFeatureName::Type::Normal, id.value() };
return FeatureName<FeatureID> { FeatureNameType::Normal, id.value() };
}
if (allow_min_max_prefix && (name.starts_with_bytes("min-"sv, CaseSensitivity::CaseInsensitive) || name.starts_with_bytes("max-"sv, CaseSensitivity::CaseInsensitive))) {
auto adjusted_name = name.bytes_as_string_view().substring_view(4);
if (auto id = media_feature_id_from_string(adjusted_name); id.has_value() && media_feature_type_is_range(id.value())) {
if (auto id = feature_name_from_string(adjusted_name); id.has_value() && allows_range_syntax(id.value())) {
transaction.commit();
return MediaFeatureName {
name.starts_with_bytes("min-"sv, CaseSensitivity::CaseInsensitive) ? MediaFeatureName::Type::Min : MediaFeatureName::Type::Max,
return FeatureName<FeatureID> {
name.starts_with_bytes("min-"sv, CaseSensitivity::CaseInsensitive) ? FeatureNameType::Min : FeatureNameType::Max,
id.value()
};
}
@ -209,124 +254,69 @@ OwnPtr<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>& in
return {};
};
// `<mf-boolean> = <mf-name>`
auto parse_mf_boolean = [&](auto& tokens) -> OwnPtr<MediaFeature> {
auto parse_feature_boolean = [&](auto& tokens) -> OwnPtr<Feature> {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
if (auto maybe_name = parse_mf_name(tokens, false); maybe_name.has_value()) {
if (auto maybe_name = parse_feature_name(tokens, false); maybe_name.has_value()) {
tokens.discard_whitespace();
if (!tokens.has_next_token()) {
transaction.commit();
return MediaFeature::boolean(maybe_name->id);
return Feature::boolean(maybe_name->id);
}
}
return {};
};
// `<mf-plain> = <mf-name> : <mf-value>`
auto parse_mf_plain = [&](auto& tokens) -> OwnPtr<MediaFeature> {
auto parse_feature_plain = [&](auto& tokens) -> OwnPtr<Feature> {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
if (auto maybe_name = parse_mf_name(tokens, true); maybe_name.has_value()) {
if (auto maybe_name = parse_feature_name(tokens, true); maybe_name.has_value()) {
tokens.discard_whitespace();
if (tokens.consume_a_token().is(Token::Type::Colon)) {
tokens.discard_whitespace();
if (auto maybe_value = parse_media_feature_value(maybe_name->id, tokens); maybe_value.has_value()) {
if (auto maybe_value = parse_feature_value(maybe_name->id, tokens); maybe_value.has_value()) {
tokens.discard_whitespace();
if (!tokens.has_next_token()) {
transaction.commit();
switch (maybe_name->type) {
case MediaFeatureName::Type::Normal:
return MediaFeature::plain(maybe_name->id, maybe_value.release_value());
case MediaFeatureName::Type::Min:
return MediaFeature::min(maybe_name->id, maybe_value.release_value());
case MediaFeatureName::Type::Max:
return MediaFeature::max(maybe_name->id, maybe_value.release_value());
case FeatureNameType::Normal:
return Feature::plain(maybe_name->id, maybe_value.release_value());
case FeatureNameType::Min:
return Feature::min(maybe_name->id, maybe_value.release_value());
case FeatureNameType::Max:
return Feature::max(maybe_name->id, maybe_value.release_value());
}
VERIFY_NOT_REACHED();
}
}
}
}
return {};
};
// `<mf-lt> = '<' '='?
// <mf-gt> = '>' '='?
// <mf-eq> = '='
// <mf-comparison> = <mf-lt> | <mf-gt> | <mf-eq>`
auto parse_comparison = [](auto& tokens) -> Optional<MediaFeature::Comparison> {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto& first = tokens.consume_a_token();
if (first.is(Token::Type::Delim)) {
auto first_delim = first.token().delim();
if (first_delim == '=') {
transaction.commit();
return MediaFeature::Comparison::Equal;
}
if (first_delim == '<') {
auto& second = tokens.next_token();
if (second.is_delim('=')) {
tokens.discard_a_token();
transaction.commit();
return MediaFeature::Comparison::LessThanOrEqual;
}
transaction.commit();
return MediaFeature::Comparison::LessThan;
}
if (first_delim == '>') {
auto& second = tokens.next_token();
if (second.is_delim('=')) {
tokens.discard_a_token();
transaction.commit();
return MediaFeature::Comparison::GreaterThanOrEqual;
}
transaction.commit();
return MediaFeature::Comparison::GreaterThan;
}
}
return {};
};
auto comparisons_match = [](MediaFeature::Comparison a, MediaFeature::Comparison b) -> bool {
switch (a) {
case MediaFeature::Comparison::Equal:
return b == MediaFeature::Comparison::Equal;
case MediaFeature::Comparison::LessThan:
case MediaFeature::Comparison::LessThanOrEqual:
return b == MediaFeature::Comparison::LessThan || b == MediaFeature::Comparison::LessThanOrEqual;
case MediaFeature::Comparison::GreaterThan:
case MediaFeature::Comparison::GreaterThanOrEqual:
return b == MediaFeature::Comparison::GreaterThan || b == MediaFeature::Comparison::GreaterThanOrEqual;
}
VERIFY_NOT_REACHED();
};
// `<mf-range> = <mf-name> <mf-comparison> <mf-value>
// | <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) -> OwnPtr<MediaFeature> {
auto parse_feature_range = [&](auto& tokens) -> OwnPtr<Feature> {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
// `<mf-name> <mf-comparison> <mf-value>`
// NOTE: We have to check for <mf-name> first, since all <mf-name>s will also parse as <mf-value>.
if (auto maybe_name = parse_mf_name(tokens, false); maybe_name.has_value()) {
if (auto maybe_name = parse_feature_name(tokens, false); maybe_name.has_value() && allows_range_syntax(maybe_name->id)) {
tokens.discard_whitespace();
if (auto maybe_comparison = parse_comparison(tokens); maybe_comparison.has_value()) {
if (auto maybe_comparison = parse_feature_comparison(tokens); maybe_comparison.has_value()) {
tokens.discard_whitespace();
if (auto maybe_value = parse_media_feature_value(maybe_name->id, tokens); maybe_value.has_value()) {
if (auto maybe_value = parse_feature_value(maybe_name->id, tokens); maybe_value.has_value()) {
tokens.discard_whitespace();
if (!tokens.has_next_token() && !maybe_value->is_ident()) {
transaction.commit();
return MediaFeature::half_range(maybe_name->id, maybe_comparison.release_value(), maybe_value.release_value());
return Feature::half_range(maybe_name->id, maybe_comparison.release_value(), maybe_value.release_value());
}
}
}
@ -339,15 +329,15 @@ OwnPtr<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>& in
// To allow for <mf-value> to be any number of tokens long, we scan forward until we find a comparison, and then
// treat the next non-whitespace token as the <mf-name>, which should be correct as long as they don't add a value
// type that can include a comparison in it. :^)
Optional<MediaFeatureName> maybe_name;
Optional<FeatureName<FeatureID>> maybe_name;
{
// This transaction is never committed, we just use it to rewind automatically.
auto temp_transaction = tokens.begin_transaction();
while (tokens.has_next_token() && !maybe_name.has_value()) {
if (auto maybe_comparison = parse_comparison(tokens); maybe_comparison.has_value()) {
if (auto maybe_comparison = parse_feature_comparison(tokens); maybe_comparison.has_value()) {
// We found a comparison, so the next non-whitespace token should be the <mf-name>
tokens.discard_whitespace();
maybe_name = parse_mf_name(tokens, false);
maybe_name = parse_feature_name(tokens, false);
break;
}
tokens.discard_a_token();
@ -355,23 +345,22 @@ OwnPtr<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>& in
}
}
// Now, we can parse the range properly.
if (maybe_name.has_value()) {
if (auto maybe_left_value = parse_media_feature_value(maybe_name->id, tokens); maybe_left_value.has_value()) {
if (maybe_name.has_value() && allows_range_syntax(maybe_name->id)) {
if (auto maybe_left_value = parse_feature_value(maybe_name->id, tokens); maybe_left_value.has_value()) {
tokens.discard_whitespace();
if (auto maybe_left_comparison = parse_comparison(tokens); maybe_left_comparison.has_value()) {
if (auto maybe_left_comparison = parse_feature_comparison(tokens); maybe_left_comparison.has_value()) {
tokens.discard_whitespace();
tokens.discard_a_token(); // The <mf-name> which we already parsed above.
tokens.discard_whitespace();
if (!tokens.has_next_token()) {
transaction.commit();
return MediaFeature::half_range(maybe_left_value.release_value(), maybe_left_comparison.release_value(), maybe_name->id);
return Feature::half_range(maybe_left_value.release_value(), maybe_left_comparison.release_value(), maybe_name->id);
}
if (auto maybe_right_comparison = parse_comparison(tokens); maybe_right_comparison.has_value()) {
if (auto maybe_right_comparison = parse_feature_comparison(tokens); maybe_right_comparison.has_value()) {
tokens.discard_whitespace();
if (auto maybe_right_value = parse_media_feature_value(maybe_name->id, tokens); maybe_right_value.has_value()) {
if (auto maybe_right_value = parse_feature_value(maybe_name->id, tokens); maybe_right_value.has_value()) {
tokens.discard_whitespace();
// For this to be valid, the following must be true:
// - Comparisons must either both be >/>= or both be </<=.
@ -381,11 +370,11 @@ OwnPtr<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>& in
auto right_comparison = maybe_right_comparison.release_value();
if (!tokens.has_next_token()
&& comparisons_match(left_comparison, right_comparison)
&& left_comparison != MediaFeature::Comparison::Equal
&& feature_comparisons_match(left_comparison, right_comparison)
&& left_comparison != FeatureComparison::Equal
&& !maybe_left_value->is_ident() && !maybe_right_value->is_ident()) {
transaction.commit();
return MediaFeature::range(maybe_left_value.release_value(), left_comparison, maybe_name->id, right_comparison, maybe_right_value.release_value());
return Feature::range(maybe_left_value.release_value(), left_comparison, maybe_name->id, right_comparison, maybe_right_value.release_value());
}
}
}
@ -396,33 +385,43 @@ OwnPtr<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>& in
return {};
};
if (auto maybe_mf_boolean = parse_mf_boolean(inner_tokens)) {
if (auto maybe_feature_boolean = parse_feature_boolean(inner_tokens)) {
inner_tokens.discard_whitespace();
if (inner_tokens.has_next_token())
return nullptr;
transaction.commit();
return maybe_mf_boolean.release_nonnull();
return maybe_feature_boolean.release_nonnull();
}
if (auto maybe_mf_plain = parse_mf_plain(inner_tokens)) {
if (auto maybe_feature_plain = parse_feature_plain(inner_tokens)) {
inner_tokens.discard_whitespace();
if (inner_tokens.has_next_token())
return nullptr;
transaction.commit();
return maybe_mf_plain.release_nonnull();
return maybe_feature_plain.release_nonnull();
}
if (auto maybe_mf_range = parse_mf_range(inner_tokens)) {
if (auto maybe_feature_range = parse_feature_range(inner_tokens)) {
inner_tokens.discard_whitespace();
if (inner_tokens.has_next_token())
return nullptr;
transaction.commit();
return maybe_mf_range.release_nonnull();
return maybe_feature_range.release_nonnull();
}
return {};
}
// `<media-feature>`, https://drafts.csswg.org/mediaqueries-5/#typedef-media-feature
OwnPtr<MediaFeature> Parser::parse_media_feature(TokenStream<ComponentValue>& inner_tokens)
{
return parse_query_feature<MediaFeature, MediaFeatureID>(
inner_tokens,
[](StringView name) { return media_feature_id_from_string(name); },
[this](MediaFeatureID id, auto& tokens) { return parse_media_feature_value(id, tokens); },
[](MediaFeatureID id) { return media_feature_type_is_range(id); });
}
Optional<MediaQuery::MediaType> Parser::parse_media_type(TokenStream<ComponentValue>& tokens)
{
auto transaction = tokens.begin_transaction();
@ -447,7 +446,7 @@ Optional<MediaQuery::MediaType> Parser::parse_media_type(TokenStream<ComponentVa
};
}
static bool is_media_feature_value_token(ComponentValue const& component_value)
static bool is_feature_value_token(ComponentValue const& component_value)
{
if (!component_value.is_token())
return true;
@ -486,54 +485,54 @@ static bool is_media_feature_value_token(ComponentValue const& component_value)
VERIFY_NOT_REACHED();
}
// `<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)
template<typename FeatureID, typename FeatureAcceptsKeyword, typename FeatureAcceptsType>
Optional<FeatureValue> Parser::parse_feature_value(FeatureID feature, TokenStream<ComponentValue>& tokens, FeatureAcceptsKeyword feature_accepts_keyword, FeatureAcceptsType feature_accepts_type)
{
{
auto transaction = tokens.begin_transaction();
auto value = [this](MediaFeatureID media_feature, TokenStream<ComponentValue>& tokens) -> Optional<MediaFeatureValue> {
auto value = [&](FeatureID feature, TokenStream<ComponentValue>& tokens) -> Optional<FeatureValue> {
auto context_guard = push_temporary_value_parsing_context(SpecialContext::MediaCondition);
// One branch for each member of the MediaFeatureValueType enum:
// One branch for each member of the QueryValueType enum:
// Identifiers
if (tokens.next_token().is(Token::Type::Ident)) {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto keyword = parse_keyword_value(tokens);
if (keyword && media_feature_accepts_keyword(media_feature, keyword->to_keyword())) {
if (keyword && feature_accepts_keyword(feature, keyword->to_keyword())) {
transaction.commit();
return MediaFeatureValue(MediaFeatureValue::Type::Ident, keyword.release_nonnull());
return FeatureValue(FeatureValue::Type::Ident, keyword.release_nonnull());
}
}
// Boolean (<mq-boolean> in the spec: a 1 or 0)
if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Boolean)) {
if (feature_accepts_type(feature, QueryValueType::Boolean)) {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
if (auto integer = parse_integer_value(tokens, infinite_integer_range)) {
if (integer->is_calculated() || first_is_one_of(integer->as_integer().integer(), 0, 1)) {
transaction.commit();
return MediaFeatureValue(MediaFeatureValue::Type::Integer, integer.release_nonnull());
return FeatureValue(FeatureValue::Type::Integer, integer.release_nonnull());
}
}
}
// Integer
if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Integer)) {
if (feature_accepts_type(feature, QueryValueType::Integer)) {
auto transaction = tokens.begin_transaction();
if (auto integer = parse_integer_value(tokens, infinite_integer_range)) {
transaction.commit();
return MediaFeatureValue(MediaFeatureValue::Type::Integer, integer.release_nonnull());
return FeatureValue(FeatureValue::Type::Integer, integer.release_nonnull());
}
}
// Length
if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Length)) {
if (feature_accepts_type(feature, QueryValueType::Length)) {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
if (auto length = parse_length_value(tokens, infinite_range)) {
transaction.commit();
return MediaFeatureValue(MediaFeatureValue::Type::Length, length.release_nonnull());
return FeatureValue(FeatureValue::Type::Length, length.release_nonnull());
}
// https://drafts.csswg.org/mediaqueries-5/#typedef-mf-value
@ -551,48 +550,48 @@ Optional<MediaFeatureValue> Parser::parse_media_feature_value(MediaFeatureID med
if (auto resolved_number = calc->as_calculated().resolve_number({}); resolved_number.has_value() && *resolved_number == 0) {
tokens.discard_a_token();
transaction.commit();
return MediaFeatureValue(MediaFeatureValue::Type::Length, LengthStyleValue::create(Length::make_px(0)));
return FeatureValue(FeatureValue::Type::Length, LengthStyleValue::create(Length::make_px(0)));
}
}
}
}
// Ratio
if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Ratio)) {
if (feature_accepts_type(feature, QueryValueType::Ratio)) {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
if (auto ratio = parse_ratio_value(tokens)) {
transaction.commit();
return MediaFeatureValue(MediaFeatureValue::Type::Ratio, ratio.release_nonnull());
return FeatureValue(FeatureValue::Type::Ratio, ratio.release_nonnull());
}
}
// Resolution
if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Resolution)) {
if (feature_accepts_type(feature, QueryValueType::Resolution)) {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
if (auto resolution = parse_resolution_value(tokens, infinite_range)) {
transaction.commit();
return MediaFeatureValue(MediaFeatureValue::Type::Resolution, resolution.release_nonnull());
return FeatureValue(FeatureValue::Type::Resolution, resolution.release_nonnull());
}
}
return {};
}(media_feature, tokens);
}(feature, tokens);
if (value.has_value()) {
tokens.discard_whitespace();
// Only returned the value if there are no trailing tokens.
// Otherwise, the transaction gets reverted and we consume all the value tokens below.
if (!is_media_feature_value_token(tokens.next_token())) {
if (!is_feature_value_token(tokens.next_token())) {
transaction.commit();
return value.release_value();
}
}
}
// Parsing failed somehow, so wrap all the tokens into an "unknown" MediaFeatureValue if possible.
// Parsing failed somehow, so wrap all the tokens into an "unknown" FeatureValue if possible.
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
@ -600,7 +599,7 @@ Optional<MediaFeatureValue> Parser::parse_media_feature_value(MediaFeatureID med
// Consume any tokens that could be part of a value.
while (tokens.has_next_token()) {
if (is_media_feature_value_token(tokens.next_token())) {
if (is_feature_value_token(tokens.next_token())) {
unknown_tokens.append(tokens.consume_a_token());
} else {
break;
@ -616,12 +615,22 @@ Optional<MediaFeatureValue> Parser::parse_media_feature_value(MediaFeatureID med
});
// NB: We only use this for serialization so the substitution function presence is irrelevant and we can just
// set it to empty.
return MediaFeatureValue(MediaFeatureValue::Type::Unknown, move(UnresolvedStyleValue::create(move(unknown_tokens), {})));
return FeatureValue(FeatureValue::Type::Unknown, move(UnresolvedStyleValue::create(move(unknown_tokens), {})));
}
return {};
}
// `<mf-value>`, https://www.w3.org/TR/mediaqueries-4/#typedef-mf-value
Optional<FeatureValue> Parser::parse_media_feature_value(MediaFeatureID feature, TokenStream<ComponentValue>& tokens)
{
return parse_feature_value(
feature,
tokens,
[](MediaFeatureID feature, Keyword keyword) { return media_feature_accepts_keyword(feature, keyword); },
[](MediaFeatureID feature, QueryValueType type) { return media_feature_accepts_type(feature, type); });
}
template<typename NestedDeclarationsRule>
GC::Ptr<CSSMediaRule> Parser::convert_to_media_rule(AtRule const& rule, Nested nested)
{

View file

@ -1,6 +1,6 @@
/*
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2021-2026, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -592,7 +592,10 @@ private:
OwnPtr<BooleanExpression> parse_media_condition(TokenStream<ComponentValue>&);
OwnPtr<MediaFeature> parse_media_feature(TokenStream<ComponentValue>&);
Optional<MediaQuery::MediaType> parse_media_type(TokenStream<ComponentValue>&);
Optional<MediaFeatureValue> parse_media_feature_value(MediaFeatureID, TokenStream<ComponentValue>&);
Optional<FeatureValue> parse_media_feature_value(MediaFeatureID, TokenStream<ComponentValue>&);
template<typename FeatureID, typename FeatureAcceptsKeyword, typename FeatureAcceptsType>
Optional<FeatureValue> parse_feature_value(FeatureID, TokenStream<ComponentValue>&, FeatureAcceptsKeyword, FeatureAcceptsType);
using ParseTest = AK::Function<OwnPtr<BooleanExpression>(TokenStream<ComponentValue>&)> const&;
OwnPtr<BooleanExpression> parse_boolean_expression(TokenStream<ComponentValue>&, MatchResult result_for_general_enclosed, ParseTest parse_test);

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2026, Sam Atkins <sam@samatkins.co.uk>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Types.h>
namespace Web::CSS {
enum class QueryValueType : u8 {
Boolean,
Integer,
Length,
Ratio,
Resolution,
};
}

View file

@ -312,6 +312,7 @@ class EasingStyleValue;
class EdgeStyleValue;
class EmptyOptionalStyleValue;
class ExplicitGridTrack;
class FeatureValue;
class FilterValueListStyleValue;
class Flex;
class FlexStyleValue;
@ -347,7 +348,6 @@ class LengthPercentage;
class LengthPercentageOrAuto;
class LengthStyleValue;
class LinearGradientStyleValue;
class MediaFeatureValue;
class MediaList;
class MediaQuery;
class MediaQueryList;

View file

@ -345,69 +345,69 @@ Page const& Window::page() const
return associated_document().page();
}
Optional<CSS::MediaFeatureValue> Window::query_media_feature(CSS::MediaFeatureID media_feature) const
Optional<CSS::FeatureValue> Window::query_media_feature(CSS::MediaFeatureID media_feature) const
{
// FIXME: Many of these should be dependent on the hardware
// https://www.w3.org/TR/mediaqueries-5/#media-descriptor-table
switch (media_feature) {
case CSS::MediaFeatureID::AnyHover:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Hover));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Hover));
case CSS::MediaFeatureID::AnyPointer:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Fine));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Fine));
case CSS::MediaFeatureID::AspectRatio:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ratio, CSS::RatioStyleValue::create(CSS::NumberStyleValue::create(inner_width()), CSS::NumberStyleValue::create(inner_height())));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ratio, CSS::RatioStyleValue::create(CSS::NumberStyleValue::create(inner_width()), CSS::NumberStyleValue::create(inner_height())));
case CSS::MediaFeatureID::Color:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Integer, CSS::IntegerStyleValue::create(8));
return CSS::FeatureValue(CSS::FeatureValue::Type::Integer, CSS::IntegerStyleValue::create(8));
case CSS::MediaFeatureID::ColorGamut:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Srgb));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Srgb));
case CSS::MediaFeatureID::ColorIndex:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Integer, CSS::IntegerStyleValue::create(0));
return CSS::FeatureValue(CSS::FeatureValue::Type::Integer, CSS::IntegerStyleValue::create(0));
case CSS::MediaFeatureID::DeviceAspectRatio: {
auto screen_area = page().client().screen_rect();
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ratio, CSS::RatioStyleValue::create(CSS::NumberStyleValue::create(screen_area.width().value()), CSS::NumberStyleValue::create(screen_area.height().value())));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ratio, CSS::RatioStyleValue::create(CSS::NumberStyleValue::create(screen_area.width().value()), CSS::NumberStyleValue::create(screen_area.height().value())));
}
case CSS::MediaFeatureID::DeviceHeight:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Length, CSS::LengthStyleValue::create(CSS::Length::make_px(page().web_exposed_screen_area().height())));
return CSS::FeatureValue(CSS::FeatureValue::Type::Length, CSS::LengthStyleValue::create(CSS::Length::make_px(page().web_exposed_screen_area().height())));
case CSS::MediaFeatureID::DeviceWidth:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Length, CSS::LengthStyleValue::create(CSS::Length::make_px(page().web_exposed_screen_area().width())));
return CSS::FeatureValue(CSS::FeatureValue::Type::Length, CSS::LengthStyleValue::create(CSS::Length::make_px(page().web_exposed_screen_area().width())));
case CSS::MediaFeatureID::DisplayMode:
// FIXME: Detect if window is fullscreen
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Browser));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Browser));
case CSS::MediaFeatureID::DynamicRange:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Standard));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Standard));
case CSS::MediaFeatureID::EnvironmentBlending:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Opaque));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Opaque));
case CSS::MediaFeatureID::ForcedColors:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::None));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::None));
case CSS::MediaFeatureID::Grid:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Integer, CSS::IntegerStyleValue::create(0));
return CSS::FeatureValue(CSS::FeatureValue::Type::Integer, CSS::IntegerStyleValue::create(0));
case CSS::MediaFeatureID::Height:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Length, CSS::LengthStyleValue::create(CSS::Length::make_px(inner_height())));
return CSS::FeatureValue(CSS::FeatureValue::Type::Length, CSS::LengthStyleValue::create(CSS::Length::make_px(inner_height())));
case CSS::MediaFeatureID::HorizontalViewportSegments:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Integer, CSS::IntegerStyleValue::create(1));
return CSS::FeatureValue(CSS::FeatureValue::Type::Integer, CSS::IntegerStyleValue::create(1));
case CSS::MediaFeatureID::Hover:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Hover));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Hover));
case CSS::MediaFeatureID::InvertedColors:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::None));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::None));
case CSS::MediaFeatureID::Monochrome:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Integer, CSS::IntegerStyleValue::create(0));
return CSS::FeatureValue(CSS::FeatureValue::Type::Integer, CSS::IntegerStyleValue::create(0));
case CSS::MediaFeatureID::NavControls:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Back));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Back));
case CSS::MediaFeatureID::Orientation:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(inner_height() >= inner_width() ? CSS::Keyword::Portrait : CSS::Keyword::Landscape));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(inner_height() >= inner_width() ? CSS::Keyword::Portrait : CSS::Keyword::Landscape));
case CSS::MediaFeatureID::OverflowBlock:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Scroll));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Scroll));
case CSS::MediaFeatureID::OverflowInline:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Scroll));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Scroll));
case CSS::MediaFeatureID::Pointer:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Fine));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Fine));
case CSS::MediaFeatureID::PrefersColorScheme: {
switch (page().preferred_color_scheme()) {
case CSS::PreferredColorScheme::Light:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Light));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Light));
case CSS::PreferredColorScheme::Dark:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Dark));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Dark));
default:
VERIFY_NOT_REACHED();
}
@ -415,52 +415,52 @@ Optional<CSS::MediaFeatureValue> Window::query_media_feature(CSS::MediaFeatureID
case CSS::MediaFeatureID::PrefersContrast:
switch (page().preferred_contrast()) {
case CSS::PreferredContrast::Less:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Less));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Less));
case CSS::PreferredContrast::More:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::More));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::More));
case CSS::PreferredContrast::NoPreference:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::NoPreference));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::NoPreference));
case CSS::PreferredContrast::Auto:
default:
// FIXME: Fallback to system settings
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::NoPreference));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::NoPreference));
}
case CSS::MediaFeatureID::PrefersReducedData:
// FIXME: Make this a preference
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::NoPreference));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::NoPreference));
case CSS::MediaFeatureID::PrefersReducedMotion:
switch (page().preferred_motion()) {
case CSS::PreferredMotion::NoPreference:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::NoPreference));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::NoPreference));
case CSS::PreferredMotion::Reduce:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Reduce));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Reduce));
case CSS::PreferredMotion::Auto:
default:
// FIXME: Fallback to system settings
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::NoPreference));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::NoPreference));
}
case CSS::MediaFeatureID::PrefersReducedTransparency:
// FIXME: Make this a preference
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::NoPreference));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::NoPreference));
case CSS::MediaFeatureID::Resolution:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Resolution, CSS::ResolutionStyleValue::create(CSS::Resolution::make_dots_per_pixel(device_pixel_ratio())));
return CSS::FeatureValue(CSS::FeatureValue::Type::Resolution, CSS::ResolutionStyleValue::create(CSS::Resolution::make_dots_per_pixel(device_pixel_ratio())));
case CSS::MediaFeatureID::Scan:
// FIXME: Detect this from the display, if we can. Most displays aren't scanning and should return None.
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::None));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::None));
case CSS::MediaFeatureID::Scripting:
if (associated_document().is_scripting_enabled())
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Enabled));
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::None));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Enabled));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::None));
case CSS::MediaFeatureID::Update:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Fast));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Fast));
case CSS::MediaFeatureID::VerticalViewportSegments:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Integer, CSS::IntegerStyleValue::create(1));
return CSS::FeatureValue(CSS::FeatureValue::Type::Integer, CSS::IntegerStyleValue::create(1));
case CSS::MediaFeatureID::VideoColorGamut:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Srgb));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Srgb));
case CSS::MediaFeatureID::VideoDynamicRange:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Standard));
return CSS::FeatureValue(CSS::FeatureValue::Type::Ident, CSS::KeywordStyleValue::create(CSS::Keyword::Standard));
case CSS::MediaFeatureID::Width:
return CSS::MediaFeatureValue(CSS::MediaFeatureValue::Type::Length, CSS::LengthStyleValue::create(CSS::Length::make_px(inner_width())));
return CSS::FeatureValue(CSS::FeatureValue::Type::Length, CSS::LengthStyleValue::create(CSS::Length::make_px(inner_width())));
default:
break;

View file

@ -124,7 +124,7 @@ public:
DOM::Event const* current_event() const { return m_current_event.ptr(); }
void set_current_event(DOM::Event* event);
Optional<CSS::MediaFeatureValue> query_media_feature(CSS::MediaFeatureID) const;
Optional<CSS::FeatureValue> query_media_feature(CSS::MediaFeatureID) const;
void fire_a_page_transition_event(FlyString const& event_name, bool persisted);

View file

@ -33,17 +33,10 @@ def write_header_file(out: TextIO, media_feature_data: dict) -> None:
#include <AK/StringView.h>
#include <AK/Traits.h>
#include <LibWeb/CSS/Keyword.h>
#include <LibWeb/CSS/QueryValueType.h>
namespace Web::CSS {{
enum class MediaFeatureValueType {{
Boolean,
Integer,
Length,
Ratio,
Resolution,
}};
enum class MediaFeatureID : {underlying_type} {{""")
for name in media_feature_data:
@ -57,7 +50,7 @@ Optional<MediaFeatureID> media_feature_id_from_string(StringView);
StringView string_from_media_feature_id(MediaFeatureID);
bool media_feature_type_is_range(MediaFeatureID);
bool media_feature_accepts_type(MediaFeatureID, MediaFeatureValueType);
bool media_feature_accepts_type(MediaFeatureID, QueryValueType);
bool media_feature_accepts_keyword(MediaFeatureID, Keyword);
bool media_feature_keyword_is_falsey(MediaFeatureID, Keyword);
@ -115,7 +108,7 @@ bool media_feature_type_is_range(MediaFeatureID media_feature_id)
VERIFY_NOT_REACHED();
}
bool media_feature_accepts_type(MediaFeatureID media_feature_id, MediaFeatureValueType value_type)
bool media_feature_accepts_type(MediaFeatureID media_feature_id, QueryValueType value_type)
{
switch (media_feature_id) {""")
@ -138,7 +131,7 @@ bool media_feature_accepts_type(MediaFeatureID media_feature_id, MediaFeatureVal
have_output_value_type_switch = True
value_type = VALUE_TYPE_NAMES[type_name]
out.write(f"""
case MediaFeatureValueType::{value_type}:
case QueryValueType::{value_type}:
return true;""")
if have_output_value_type_switch: