ladybird/Libraries/LibWeb/CSS/Parser/MediaParsing.cpp
Sam Atkins f88c093f93 LibWeb/CSS: Maintain rule-parsing context when converting to CSSRules
Specifically we will need to know the parent rules for a nested style
rule inside `@scope` in order to resolve its selectors correctly.
Reusing our existing m_rule_context stack is the simplest option - once
we reach the point of converting rules, this context stack is empty, so
we can populate it as we go.
2026-05-22 10:00:42 +01:00

711 lines
30 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2020-2021, the SerenityOS developers.
* Copyright (c) 2021-2026, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
* Copyright (c) 2022, MacDue <macdue@dueutil.tech>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/CSSFunctionDeclarations.h>
#include <LibWeb/CSS/CSSMediaRule.h>
#include <LibWeb/CSS/CSSNestedDeclarations.h>
#include <LibWeb/CSS/ContainerQuery.h>
#include <LibWeb/CSS/MediaList.h>
#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>
namespace Web::CSS::Parser {
Vector<NonnullRefPtr<MediaQuery>> Parser::parse_as_media_query_list()
{
return parse_a_media_query_list(m_token_stream);
}
template<typename T>
Vector<NonnullRefPtr<MediaQuery>> Parser::parse_a_media_query_list(TokenStream<T>& tokens)
{
// https://www.w3.org/TR/mediaqueries-4/#mq-list
// AD-HOC: Ignore whitespace-only queries
// to make `@media {..}` equivalent to `@media all {..}`
tokens.discard_whitespace();
if (!tokens.has_next_token())
return {};
auto comma_separated_lists = parse_a_comma_separated_list_of_component_values(tokens);
AK::Vector<NonnullRefPtr<MediaQuery>> media_queries;
for (auto& media_query_parts : comma_separated_lists) {
auto stream = TokenStream(media_query_parts);
media_queries.append(parse_media_query(stream));
}
return media_queries;
}
RefPtr<MediaQuery> Parser::parse_as_media_query()
{
// https://www.w3.org/TR/cssom-1/#parse-a-media-query
auto media_query_list = parse_as_media_query_list();
if (media_query_list.is_empty())
return MediaQuery::create_not_all();
if (media_query_list.size() == 1)
return media_query_list.first();
return nullptr;
}
// `<media-query>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-query
NonnullRefPtr<MediaQuery> Parser::parse_media_query(TokenStream<ComponentValue>& tokens)
{
// `<media-query> = <media-condition>
// | [ not | only ]? <media-type> [ and <media-condition-without-or> ]?`
// `[ not | only ]?`, Returns whether to negate the query
auto parse_initial_modifier = [](auto& tokens) -> Optional<bool> {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto& token = tokens.consume_a_token();
if (!token.is(Token::Type::Ident))
return {};
auto ident = token.token().ident();
if (ident.equals_ignoring_ascii_case("not"sv)) {
transaction.commit();
return true;
}
if (ident.equals_ignoring_ascii_case("only"sv)) {
transaction.commit();
return false;
}
return {};
};
auto invalid_media_query = [&](String&& description) {
// "A media query that does not match the grammar in the previous section must be replaced by `not all`
// during parsing." - https://www.w3.org/TR/mediaqueries-5/#error-handling
ErrorReporter::the().report(InvalidQueryError {
.query_type = "@media"_fly_string,
.value_string = tokens.dump_string(),
.description = move(description),
});
return MediaQuery::create_not_all();
};
auto media_query = MediaQuery::create();
tokens.discard_whitespace();
// `<media-condition>`
if (auto media_condition = parse_media_condition(tokens)) {
tokens.discard_whitespace();
if (tokens.has_next_token())
return invalid_media_query("Trailing tokens after <media-condition>"_string);
media_query->m_media_condition = media_condition.release_nonnull();
return media_query;
}
// `[ not | only ]?`
if (auto modifier = parse_initial_modifier(tokens); modifier.has_value()) {
media_query->m_negated = modifier.value();
tokens.discard_whitespace();
}
// `<media-type>`
if (auto media_type = parse_media_type(tokens); media_type.has_value()) {
media_query->m_media_type = media_type.release_value();
tokens.discard_whitespace();
} else {
// https://drafts.csswg.org/mediaqueries-4/#error-handling
// A media query that does not match the grammar in the previous section must be replaced by not all during parsing.
return invalid_media_query("Doesn't match `<media-query>`"_string);
}
if (!tokens.has_next_token())
return media_query;
// `[ 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)) {
// "or" is disallowed at the top level
if (is<BooleanOrExpression>(*media_condition))
return invalid_media_query("Contains top-level `or`"_string);
tokens.discard_whitespace();
if (tokens.has_next_token())
return invalid_media_query("Trailing tokens after `<media-condition-without-or>`"_string);
media_query->m_media_condition = move(media_condition);
return media_query;
}
return invalid_media_query("Missing `<media-condition>` after `and`"_string);
}
return invalid_media_query("Trailing tokens after `<media-query>`"_string);
}
// `<media-condition>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-condition
OwnPtr<BooleanExpression> Parser::parse_media_condition(TokenStream<ComponentValue>& tokens)
{
return parse_boolean_expression(tokens, MatchResult::Unknown, [this](TokenStream<ComponentValue>& outer_tokens) -> OwnPtr<BooleanExpression> {
auto transaction = outer_tokens.begin_transaction();
outer_tokens.discard_whitespace();
if (!(outer_tokens.next_token().is_block() && outer_tokens.next_token().block().is_paren()))
return nullptr;
auto const& block = outer_tokens.consume_a_token().block();
TokenStream inner_tokens { block.value };
if (auto maybe_media_feature = parse_media_feature(inner_tokens)) {
transaction.commit();
return maybe_media_feature;
}
return nullptr;
});
}
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)
{
auto transaction = inner_tokens.begin_transaction();
// `<mf-name> = <ident>`
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 = feature_name_from_string(name); id.has_value()) {
transaction.commit();
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 = feature_name_from_string(adjusted_name); id.has_value() && allows_range_syntax(id.value())) {
transaction.commit();
return FeatureName<FeatureID> {
name.starts_with_bytes("min-"sv, CaseSensitivity::CaseInsensitive) ? FeatureNameType::Min : FeatureNameType::Max,
id.value()
};
}
}
}
return {};
};
auto parse_feature_boolean = [&](auto& tokens) -> OwnPtr<Feature> {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
if (auto maybe_name = parse_feature_name(tokens, false); maybe_name.has_value()) {
tokens.discard_whitespace();
if (!tokens.has_next_token()) {
transaction.commit();
return Feature::boolean(maybe_name->id);
}
}
return {};
};
auto parse_feature_plain = [&](auto& tokens) -> OwnPtr<Feature> {
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
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_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 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-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_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_feature_name(tokens, false); maybe_name.has_value() && allows_range_syntax(maybe_name->id)) {
tokens.discard_whitespace();
if (auto maybe_comparison = parse_feature_comparison(tokens); maybe_comparison.has_value()) {
tokens.discard_whitespace();
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 Feature::half_range(maybe_name->id, maybe_comparison.release_value(), maybe_value.release_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>`
// NOTE: To parse the first value, we need to first find and parse the <mf-name> so we know what value types to parse.
// 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<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_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_feature_name(tokens, false);
break;
}
tokens.discard_a_token();
tokens.discard_whitespace();
}
}
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_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 Feature::half_range(maybe_left_value.release_value(), maybe_left_comparison.release_value(), maybe_name->id);
}
if (auto maybe_right_comparison = parse_feature_comparison(tokens); maybe_right_comparison.has_value()) {
tokens.discard_whitespace();
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 </<=.
// - Neither comparison can be `=`.
// - Neither value can be an ident.
auto left_comparison = maybe_left_comparison.release_value();
auto right_comparison = maybe_right_comparison.release_value();
if (!tokens.has_next_token()
&& feature_comparisons_match(left_comparison, right_comparison)
&& left_comparison != FeatureComparison::Equal
&& !maybe_left_value->is_ident() && !maybe_right_value->is_ident()) {
transaction.commit();
return Feature::range(maybe_left_value.release_value(), left_comparison, maybe_name->id, right_comparison, maybe_right_value.release_value());
}
}
}
}
}
}
return {};
};
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_feature_boolean.release_nonnull();
}
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_feature_plain.release_nonnull();
}
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_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); });
}
// `<size-feature>`, https://drafts.csswg.org/css-conditional-5/#size-container
OwnPtr<SizeFeature> Parser::parse_size_feature(TokenStream<ComponentValue>& inner_tokens)
{
return parse_query_feature<SizeFeature, SizeFeatureID>(
inner_tokens,
[](StringView name) { return size_feature_id_from_string(name); },
[this](SizeFeatureID id, auto& tokens) { return parse_size_feature_value(id, tokens); },
[](SizeFeatureID id) { return size_feature_type_is_range(id); });
}
Optional<MediaQuery::MediaType> Parser::parse_media_type(TokenStream<ComponentValue>& tokens)
{
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
auto const& token = tokens.consume_a_token();
if (!token.is(Token::Type::Ident))
return {};
// https://drafts.csswg.org/mediaqueries-3/#error-handling
// "However, an exception is made for media types ‘layer’, ‘not’, ‘and’, ‘only’, and ‘or’. Even though they do match
// the IDENT production, they must not be treated as unknown media types, but rather trigger the malformed query clause."
if (token.is_ident("layer"sv) || token.is_ident("not"sv) || token.is_ident("and"sv) || token.is_ident("only"sv) || token.is_ident("or"sv))
return {};
transaction.commit();
auto const& ident = token.token().ident();
return MediaQuery::MediaType {
.name = ident,
.known_type = media_type_from_string(ident),
};
}
static bool is_feature_value_token(ComponentValue const& component_value)
{
if (!component_value.is_token())
return true;
switch (component_value.token().type()) {
case Token::Type::Ident:
case Token::Type::Function:
case Token::Type::AtKeyword:
case Token::Type::Hash:
case Token::Type::String:
case Token::Type::BadString:
case Token::Type::Url:
case Token::Type::BadUrl:
case Token::Type::Number:
case Token::Type::Percentage:
case Token::Type::Dimension:
case Token::Type::Whitespace:
case Token::Type::Comma:
return true;
case Token::Type::Delim:
// FIXME: What list of delimiters should we actually allow here?
return !first_is_one_of(component_value.token().delim(), static_cast<u32>('<'), static_cast<u32>('>'), static_cast<u32>('='));
case Token::Type::Invalid:
case Token::Type::EndOfFile:
case Token::Type::CDO:
case Token::Type::CDC:
case Token::Type::Colon:
case Token::Type::Semicolon:
case Token::Type::OpenSquare:
case Token::Type::CloseSquare:
case Token::Type::OpenParen:
case Token::Type::CloseParen:
case Token::Type::OpenCurly:
case Token::Type::CloseCurly:
return false;
}
VERIFY_NOT_REACHED();
}
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 = [&](FeatureID feature, TokenStream<ComponentValue>& tokens) -> Optional<FeatureValue> {
auto context_guard = push_temporary_value_parsing_context(SpecialContext::MediaCondition);
// 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 && feature_accepts_keyword(feature, keyword->to_keyword())) {
transaction.commit();
return FeatureValue(FeatureValue::Type::Ident, keyword.release_nonnull());
}
}
// Boolean (<mq-boolean> in the spec: a 1 or 0)
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 FeatureValue(FeatureValue::Type::Integer, integer.release_nonnull());
}
}
}
// 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 FeatureValue(FeatureValue::Type::Integer, integer.release_nonnull());
}
}
// 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 FeatureValue(FeatureValue::Type::Length, length.release_nonnull());
}
// https://drafts.csswg.org/mediaqueries-5/#typedef-mf-value
// <mf-value> = <number> | <dimension> | <ident> | <ratio>
//
// https://drafts.csswg.org/css-values-4/#lengths
// "For zero lengths the unit identifier is optional"
//
// https://drafts.csswg.org/css-values-4/#zero-value
// "Values of '0' can be written without units, even if the
// value type doesn't allow 'unitless zeroes'."
if (tokens.has_next_token()) {
auto const& token = tokens.next_token();
if (auto calc = parse_calculated_value(token, { .accepted_ranges_by_type = { { ValueType::Number, infinite_range } } }); calc && calc->as_calculated().resolves_to_number()) {
if (auto resolved_number = calc->as_calculated().resolve_number({}); resolved_number.has_value() && *resolved_number == 0) {
tokens.discard_a_token();
transaction.commit();
return FeatureValue(FeatureValue::Type::Length, LengthStyleValue::create(Length::make_px(0)));
}
}
}
}
// 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 FeatureValue(FeatureValue::Type::Ratio, ratio.release_nonnull());
}
}
// 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 FeatureValue(FeatureValue::Type::Resolution, resolution.release_nonnull());
}
}
return {};
}(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_feature_value_token(tokens.next_token())) {
transaction.commit();
return value.release_value();
}
}
}
// Parsing failed somehow, so wrap all the tokens into an "unknown" FeatureValue if possible.
auto transaction = tokens.begin_transaction();
tokens.discard_whitespace();
Vector<ComponentValue> unknown_tokens;
// Consume any tokens that could be part of a value.
while (tokens.has_next_token()) {
if (is_feature_value_token(tokens.next_token())) {
unknown_tokens.append(tokens.consume_a_token());
} else {
break;
}
}
if (!unknown_tokens.is_empty()) {
transaction.commit();
ErrorReporter::the().report(InvalidValueError {
.value_type = "<mf-value>"_fly_string,
.value_string = MUST(String::join(""sv, unknown_tokens)),
.description = "Unrecognized type"_string,
});
// NB: We only use this for serialization so the substitution function presence is irrelevant and we can just
// set it to empty.
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); });
}
Optional<FeatureValue> Parser::parse_size_feature_value(SizeFeatureID feature, TokenStream<ComponentValue>& tokens)
{
auto size_feature_accepts_keyword = [](SizeFeatureID feature, Keyword keyword) {
return feature == SizeFeatureID::Orientation && first_is_one_of(keyword, Keyword::Landscape, Keyword::Portrait);
};
auto size_feature_accepts_type = [](SizeFeatureID feature, QueryValueType type) {
switch (type) {
case QueryValueType::Length:
return first_is_one_of(feature,
SizeFeatureID::BlockSize,
SizeFeatureID::Height,
SizeFeatureID::InlineSize,
SizeFeatureID::Width);
case QueryValueType::Ratio:
return feature == SizeFeatureID::AspectRatio;
default:
return false;
}
};
return parse_feature_value(feature, tokens, size_feature_accepts_keyword, size_feature_accepts_type);
}
template<typename NestedDeclarationsRule>
GC::Ptr<CSSMediaRule> Parser::convert_to_media_rule(AtRule const& rule, Nested nested)
{
m_rule_context.append(RuleContext::AtMedia);
ScopeGuard guard = [&] {
[[maybe_unused]] auto last = m_rule_context.take_last();
VERIFY(last == RuleContext::AtMedia);
};
// https://drafts.csswg.org/css-conditional-3/#at-media
// @media <media-query-list> {
// <rule-list>
// }
if (!rule.is_block_rule) {
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
.rule_name = "@media"_fly_string,
.prelude = MUST(String::join(""sv, rule.prelude)),
.description = "Expected a block."_string,
});
return nullptr;
}
auto media_query_tokens = TokenStream { rule.prelude };
auto media_query_list = parse_a_media_query_list(media_query_tokens);
auto media_list = MediaList::create(realm(), move(media_query_list));
GC::RootVector<GC::Ref<CSSRule>> child_rules;
for (auto const& child : rule.child_rules_and_lists_of_declarations) {
child.visit(
[&](Rule const& rule) {
if (auto child_rule = convert_to_rule<NestedDeclarationsRule>(rule, nested))
child_rules.append(*child_rule);
},
[&](Vector<Declaration> const& declarations) {
child_rules.append(NestedDeclarationsRule::create(realm(), *this, declarations));
});
}
auto rule_list = CSSRuleList::create(realm(), child_rules);
return CSSMediaRule::create(realm(), media_list, rule_list);
}
template GC::Ptr<CSSMediaRule> Parser::convert_to_media_rule<CSSNestedDeclarations>(AtRule const&, Parser::Nested);
template GC::Ptr<CSSMediaRule> Parser::convert_to_media_rule<CSSFunctionDeclarations>(AtRule const&, Parser::Nested);
}