From 1d6c2cb2877b8be40c62c4eac431d734fcdde79d Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Thu, 17 Aug 2023 14:56:00 +0100 Subject: [PATCH] LibWeb: Move media-query parsing code to separate file Parser.cpp is big and complicated enough to make CLion sluggish and unhappy, so let's move some code out of it. --- Userland/Libraries/LibWeb/CMakeLists.txt | 1 + .../LibWeb/CSS/Parser/MediaParsing.cpp | 636 ++++++++++++++++++ .../Libraries/LibWeb/CSS/Parser/Parser.cpp | 621 +---------------- Userland/Libraries/LibWeb/CSS/Parser/Parser.h | 2 + 4 files changed, 644 insertions(+), 616 deletions(-) create mode 100644 Userland/Libraries/LibWeb/CSS/Parser/MediaParsing.cpp diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index d1d755f5f6e..30bd9ad0bb0 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -54,6 +54,7 @@ set(SOURCES CSS/Parser/DeclarationOrAtRule.cpp CSS/Parser/Function.cpp CSS/Parser/Helpers.cpp + CSS/Parser/MediaParsing.cpp CSS/Parser/Parser.cpp CSS/Parser/ParsingContext.cpp CSS/Parser/Rule.cpp diff --git a/Userland/Libraries/LibWeb/CSS/Parser/MediaParsing.cpp b/Userland/Libraries/LibWeb/CSS/Parser/MediaParsing.cpp new file mode 100644 index 00000000000..3ad57a575da --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/Parser/MediaParsing.cpp @@ -0,0 +1,636 @@ +/* + * Copyright (c) 2018-2022, Andreas Kling + * Copyright (c) 2020-2021, the SerenityOS developers. + * Copyright (c) 2021-2023, Sam Atkins + * Copyright (c) 2021, Tobias Christiansen + * Copyright (c) 2022, MacDue + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +namespace Web::CSS::Parser { + +Vector> Parser::parse_as_media_query_list() +{ + return parse_a_media_query_list(m_token_stream); +} + +template +Vector> Parser::parse_a_media_query_list(TokenStream& tokens) +{ + // https://www.w3.org/TR/mediaqueries-4/#mq-list + + auto comma_separated_lists = parse_a_comma_separated_list_of_component_values(tokens); + + AK::Vector> 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 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; +} + +// ``, https://www.w3.org/TR/mediaqueries-4/#typedef-media-query +NonnullRefPtr Parser::parse_media_query(TokenStream& tokens) +{ + // ` = + // | [ not | only ]? [ and ]?` + + // `[ not | only ]?`, Returns whether to negate the query + auto parse_initial_modifier = [](auto& tokens) -> Optional { + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + auto& token = tokens.next_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 = [&]() { + // "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 + if constexpr (CSS_PARSER_DEBUG) { + dbgln("Invalid media query:"); + tokens.dump_all_tokens(); + } + return MediaQuery::create_not_all(); + }; + + auto media_query = MediaQuery::create(); + tokens.skip_whitespace(); + + // `` + if (auto media_condition = parse_media_condition(tokens, MediaCondition::AllowOr::Yes)) { + tokens.skip_whitespace(); + if (tokens.has_next_token()) + return invalid_media_query(); + media_query->m_media_condition = move(media_condition); + return media_query; + } + + // `[ not | only ]?` + if (auto modifier = parse_initial_modifier(tokens); modifier.has_value()) { + media_query->m_negated = modifier.value(); + tokens.skip_whitespace(); + } + + // `` + if (auto media_type = parse_media_type(tokens); media_type.has_value()) { + media_query->m_media_type = media_type.value(); + tokens.skip_whitespace(); + } else { + return invalid_media_query(); + } + + if (!tokens.has_next_token()) + return media_query; + + // `[ and ]?` + if (auto maybe_and = tokens.next_token(); maybe_and.is(Token::Type::Ident) && maybe_and.token().ident().equals_ignoring_ascii_case("and"sv)) { + if (auto media_condition = parse_media_condition(tokens, MediaCondition::AllowOr::No)) { + tokens.skip_whitespace(); + if (tokens.has_next_token()) + return invalid_media_query(); + media_query->m_media_condition = move(media_condition); + return media_query; + } + return invalid_media_query(); + } + + return invalid_media_query(); +} + +// ``, https://www.w3.org/TR/mediaqueries-4/#typedef-media-condition +// ``, https://www.w3.org/TR/mediaqueries-4/#typedef-media-condition-without-or +// (We distinguish between these two with the `allow_or` parameter.) +OwnPtr Parser::parse_media_condition(TokenStream& tokens, MediaCondition::AllowOr allow_or) +{ + // ` | [ * | * ]` + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + + // ` = not ` + auto parse_media_not = [&](auto& tokens) -> OwnPtr { + auto local_transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + + auto& first_token = tokens.next_token(); + if (first_token.is(Token::Type::Ident) && first_token.token().ident().equals_ignoring_ascii_case("not"sv)) { + if (auto child_condition = parse_media_condition(tokens, MediaCondition::AllowOr::Yes)) { + local_transaction.commit(); + return MediaCondition::from_not(child_condition.release_nonnull()); + } + } + + return {}; + }; + + auto parse_media_with_combinator = [&](auto& tokens, StringView combinator) -> OwnPtr { + auto local_transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + + auto& first = tokens.next_token(); + if (first.is(Token::Type::Ident) && first.token().ident().equals_ignoring_ascii_case(combinator)) { + tokens.skip_whitespace(); + if (auto media_in_parens = parse_media_in_parens(tokens)) { + local_transaction.commit(); + return media_in_parens; + } + } + + return {}; + }; + + // ` = and ` + auto parse_media_and = [&](auto& tokens) { return parse_media_with_combinator(tokens, "and"sv); }; + // ` = or ` + auto parse_media_or = [&](auto& tokens) { return parse_media_with_combinator(tokens, "or"sv); }; + + // `` + if (auto maybe_media_not = parse_media_not(tokens)) { + transaction.commit(); + return maybe_media_not.release_nonnull(); + } + + // ` [ * | * ]` + if (auto maybe_media_in_parens = parse_media_in_parens(tokens)) { + tokens.skip_whitespace(); + // Only `` + if (!tokens.has_next_token()) { + transaction.commit(); + return maybe_media_in_parens.release_nonnull(); + } + + Vector> child_conditions; + child_conditions.append(maybe_media_in_parens.release_nonnull()); + + // `*` + if (auto media_and = parse_media_and(tokens)) { + child_conditions.append(media_and.release_nonnull()); + + tokens.skip_whitespace(); + while (tokens.has_next_token()) { + if (auto next_media_and = parse_media_and(tokens)) { + child_conditions.append(next_media_and.release_nonnull()); + tokens.skip_whitespace(); + continue; + } + // We failed - invalid syntax! + return {}; + } + + transaction.commit(); + return MediaCondition::from_and_list(move(child_conditions)); + } + + // `*` + if (allow_or == MediaCondition::AllowOr::Yes) { + if (auto media_or = parse_media_or(tokens)) { + child_conditions.append(media_or.release_nonnull()); + + tokens.skip_whitespace(); + while (tokens.has_next_token()) { + if (auto next_media_or = parse_media_or(tokens)) { + child_conditions.append(next_media_or.release_nonnull()); + tokens.skip_whitespace(); + continue; + } + // We failed - invalid syntax! + return {}; + } + + transaction.commit(); + return MediaCondition::from_or_list(move(child_conditions)); + } + } + } + + return {}; +} + +// ``, https://www.w3.org/TR/mediaqueries-4/#typedef-media-feature +Optional Parser::parse_media_feature(TokenStream& tokens) +{ + // `[ | | ]` + tokens.skip_whitespace(); + + // ` = ` + struct MediaFeatureName { + enum Type { + Normal, + Min, + Max + } type; + MediaFeatureID id; + }; + auto parse_mf_name = [](auto& tokens, bool allow_min_max_prefix) -> Optional { + auto transaction = tokens.begin_transaction(); + auto& token = tokens.next_token(); + if (token.is(Token::Type::Ident)) { + auto name = token.token().ident(); + if (auto id = media_feature_id_from_string(name); id.has_value()) { + transaction.commit(); + return MediaFeatureName { MediaFeatureName::Type::Normal, id.value() }; + } + + if (allow_min_max_prefix && (name.starts_with("min-"sv, CaseSensitivity::CaseInsensitive) || name.starts_with("max-"sv, CaseSensitivity::CaseInsensitive))) { + auto adjusted_name = name.substring_view(4); + if (auto id = media_feature_id_from_string(adjusted_name); id.has_value() && media_feature_type_is_range(id.value())) { + transaction.commit(); + return MediaFeatureName { + name.starts_with("min-"sv, CaseSensitivity::CaseInsensitive) ? MediaFeatureName::Type::Min : MediaFeatureName::Type::Max, + id.value() + }; + } + } + } + return {}; + }; + + // ` = ` + auto parse_mf_boolean = [&](auto& tokens) -> Optional { + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + + if (auto maybe_name = parse_mf_name(tokens, false); maybe_name.has_value()) { + tokens.skip_whitespace(); + if (!tokens.has_next_token()) { + transaction.commit(); + return MediaFeature::boolean(maybe_name->id); + } + } + + return {}; + }; + + // ` = : ` + auto parse_mf_plain = [&](auto& tokens) -> Optional { + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + + if (auto maybe_name = parse_mf_name(tokens, true); maybe_name.has_value()) { + tokens.skip_whitespace(); + if (tokens.next_token().is(Token::Type::Colon)) { + tokens.skip_whitespace(); + if (auto maybe_value = parse_media_feature_value(maybe_name->id, tokens); maybe_value.has_value()) { + tokens.skip_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()); + } + VERIFY_NOT_REACHED(); + } + } + } + } + return {}; + }; + + // ` = '<' '='? + // = '>' '='? + // = '=' + // = | | ` + auto parse_comparison = [](auto& tokens) -> Optional { + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + + auto& first = tokens.next_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.peek_token(); + if (second.is_delim('=')) { + tokens.next_token(); + transaction.commit(); + return MediaFeature::Comparison::LessThanOrEqual; + } + transaction.commit(); + return MediaFeature::Comparison::LessThan; + } + if (first_delim == '>') { + auto& second = tokens.peek_token(); + if (second.is_delim('=')) { + tokens.next_token(); + transaction.commit(); + return MediaFeature::Comparison::GreaterThanOrEqual; + } + transaction.commit(); + return MediaFeature::Comparison::GreaterThan; + } + } + + return {}; + }; + + auto flip = [](MediaFeature::Comparison comparison) { + switch (comparison) { + case MediaFeature::Comparison::Equal: + return MediaFeature::Comparison::Equal; + case MediaFeature::Comparison::LessThan: + return MediaFeature::Comparison::GreaterThan; + case MediaFeature::Comparison::LessThanOrEqual: + return MediaFeature::Comparison::GreaterThanOrEqual; + case MediaFeature::Comparison::GreaterThan: + return MediaFeature::Comparison::LessThan; + case MediaFeature::Comparison::GreaterThanOrEqual: + return MediaFeature::Comparison::LessThanOrEqual; + } + VERIFY_NOT_REACHED(); + }; + + 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(); + }; + + // ` = + // | + // | + // | ` + auto parse_mf_range = [&](auto& tokens) -> Optional { + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + + // ` ` + // NOTE: We have to check for first, since all s will also parse as . + if (auto maybe_name = parse_mf_name(tokens, false); maybe_name.has_value() && media_feature_type_is_range(maybe_name->id)) { + tokens.skip_whitespace(); + if (auto maybe_comparison = parse_comparison(tokens); maybe_comparison.has_value()) { + tokens.skip_whitespace(); + if (auto maybe_value = parse_media_feature_value(maybe_name->id, tokens); maybe_value.has_value()) { + tokens.skip_whitespace(); + if (!tokens.has_next_token() && !maybe_value->is_ident()) { + transaction.commit(); + return MediaFeature::half_range(maybe_value.release_value(), flip(maybe_comparison.release_value()), maybe_name->id); + } + } + } + } + + // ` + // | + // | ` + // NOTE: To parse the first value, we need to first find and parse the so we know what value types to parse. + // To allow for 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 , which should be correct as long as they don't add a value + // type that can include a comparison in it. :^) + Optional 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()) { + // We found a comparison, so the next non-whitespace token should be the + tokens.skip_whitespace(); + maybe_name = parse_mf_name(tokens, false); + break; + } + tokens.next_token(); + tokens.skip_whitespace(); + } + } + + // Now, we can parse the range properly. + if (maybe_name.has_value() && media_feature_type_is_range(maybe_name->id)) { + if (auto maybe_left_value = parse_media_feature_value(maybe_name->id, tokens); maybe_left_value.has_value()) { + tokens.skip_whitespace(); + if (auto maybe_left_comparison = parse_comparison(tokens); maybe_left_comparison.has_value()) { + tokens.skip_whitespace(); + tokens.next_token(); // The which we already parsed above. + tokens.skip_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); + } + + if (auto maybe_right_comparison = parse_comparison(tokens); maybe_right_comparison.has_value()) { + tokens.skip_whitespace(); + if (auto maybe_right_value = parse_media_feature_value(maybe_name->id, tokens); maybe_right_value.has_value()) { + tokens.skip_whitespace(); + // For this to be valid, the following must be true: + // - Comparisons must either both be >/>= or both be 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 {}; + }; + + if (auto maybe_mf_boolean = parse_mf_boolean(tokens); maybe_mf_boolean.has_value()) + return maybe_mf_boolean.release_value(); + + if (auto maybe_mf_plain = parse_mf_plain(tokens); maybe_mf_plain.has_value()) + return maybe_mf_plain.release_value(); + + if (auto maybe_mf_range = parse_mf_range(tokens); maybe_mf_range.has_value()) + return maybe_mf_range.release_value(); + + return {}; +} + +Optional Parser::parse_media_type(TokenStream& tokens) +{ + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + auto const& token = tokens.next_token(); + + if (!token.is(Token::Type::Ident)) + return {}; + + transaction.commit(); + + auto ident = token.token().ident(); + return media_type_from_string(ident); +} + +// ``, https://www.w3.org/TR/mediaqueries-4/#typedef-media-in-parens +OwnPtr Parser::parse_media_in_parens(TokenStream& tokens) +{ + // ` = ( ) | ( ) | ` + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + + // `( ) | ( )` + auto const& first_token = tokens.peek_token(); + if (first_token.is_block() && first_token.block().is_paren()) { + TokenStream inner_token_stream { first_token.block().values() }; + if (auto maybe_media_condition = parse_media_condition(inner_token_stream, MediaCondition::AllowOr::Yes)) { + tokens.next_token(); + transaction.commit(); + return maybe_media_condition.release_nonnull(); + } + if (auto maybe_media_feature = parse_media_feature(inner_token_stream); maybe_media_feature.has_value()) { + tokens.next_token(); + transaction.commit(); + return MediaCondition::from_feature(maybe_media_feature.release_value()); + } + } + + // `` + // FIXME: We should only be taking this branch if the grammar doesn't match the above options. + // Currently we take it if the above fail to parse, which is different. + // eg, `@media (min-width: 76yaks)` is valid grammar, but does not parse because `yaks` isn't a unit. + if (auto maybe_general_enclosed = parse_general_enclosed(tokens); maybe_general_enclosed.has_value()) { + transaction.commit(); + return MediaCondition::from_general_enclosed(maybe_general_enclosed.release_value()); + } + + return {}; +} + +// ``, https://www.w3.org/TR/mediaqueries-4/#typedef-mf-value +Optional Parser::parse_media_feature_value(MediaFeatureID media_feature, TokenStream& tokens) +{ + // Identifiers + if (tokens.peek_token().is(Token::Type::Ident)) { + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + auto ident = value_id_from_string(tokens.next_token().token().ident()); + if (ident.has_value() && media_feature_accepts_identifier(media_feature, ident.value())) { + transaction.commit(); + return MediaFeatureValue(ident.value()); + } + } + + // One branch for each member of the MediaFeatureValueType enum: + + // Boolean ( in the spec: a 1 or 0) + if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Boolean)) { + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + auto const& first = tokens.next_token(); + if (first.is(Token::Type::Number) && first.token().number().is_integer() + && (first.token().number_value() == 0 || first.token().number_value() == 1)) { + transaction.commit(); + return MediaFeatureValue(first.token().number_value()); + } + } + + // Integer + if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Integer)) { + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + auto const& first = tokens.next_token(); + if (first.is(Token::Type::Number) && first.token().number().is_integer()) { + transaction.commit(); + return MediaFeatureValue(first.token().number_value()); + } + } + + // Length + if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Length)) { + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + auto const& first = tokens.next_token(); + if (auto length = parse_length(first); length.has_value()) { + transaction.commit(); + return MediaFeatureValue(length.release_value()); + } + } + + // Ratio + if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Ratio)) { + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + if (auto ratio = parse_ratio(tokens); ratio.has_value()) { + transaction.commit(); + return MediaFeatureValue(ratio.release_value()); + } + } + + // Resolution + if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Resolution)) { + auto transaction = tokens.begin_transaction(); + tokens.skip_whitespace(); + auto const& first = tokens.next_token(); + if (auto resolution = parse_dimension(first); resolution.has_value() && resolution->is_resolution()) { + transaction.commit(); + return MediaFeatureValue(resolution->resolution()); + } + } + + return {}; +} + +CSSMediaRule* Parser::convert_to_media_rule(NonnullRefPtr rule) +{ + auto media_query_tokens = TokenStream { rule->prelude() }; + auto media_query_list = parse_a_media_query_list(media_query_tokens); + if (media_query_list.is_empty() || !rule->block()) + return {}; + + auto child_tokens = TokenStream { rule->block()->values() }; + auto parser_rules = parse_a_list_of_rules(child_tokens); + JS::MarkedVector child_rules(m_context.realm().heap()); + for (auto& raw_rule : parser_rules) { + if (auto* child_rule = convert_to_rule(raw_rule)) + child_rules.append(child_rule); + } + auto media_list = MediaList::create(m_context.realm(), move(media_query_list)); + auto rule_list = CSSRuleList::create(m_context.realm(), child_rules); + return CSSMediaRule::create(m_context.realm(), media_list, rule_list); +} + +} diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp index ae0929cc19d..f965ccb2f84 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -795,604 +795,6 @@ Parser::ParseErrorOr> Parser::parse_simple_se return ParseError::SyntaxError; } -Vector> Parser::parse_as_media_query_list() -{ - return parse_a_media_query_list(m_token_stream); -} - -template -Vector> Parser::parse_a_media_query_list(TokenStream& tokens) -{ - // https://www.w3.org/TR/mediaqueries-4/#mq-list - - auto comma_separated_lists = parse_a_comma_separated_list_of_component_values(tokens); - - AK::Vector> 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 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; -} - -// ``, https://www.w3.org/TR/mediaqueries-4/#typedef-media-query -NonnullRefPtr Parser::parse_media_query(TokenStream& tokens) -{ - // ` = - // | [ not | only ]? [ and ]?` - - // `[ not | only ]?`, Returns whether to negate the query - auto parse_initial_modifier = [](auto& tokens) -> Optional { - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - auto& token = tokens.next_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 = [&]() { - // "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 - if constexpr (CSS_PARSER_DEBUG) { - dbgln("Invalid media query:"); - tokens.dump_all_tokens(); - } - return MediaQuery::create_not_all(); - }; - - auto media_query = MediaQuery::create(); - tokens.skip_whitespace(); - - // `` - if (auto media_condition = parse_media_condition(tokens, MediaCondition::AllowOr::Yes)) { - tokens.skip_whitespace(); - if (tokens.has_next_token()) - return invalid_media_query(); - media_query->m_media_condition = move(media_condition); - return media_query; - } - - // `[ not | only ]?` - if (auto modifier = parse_initial_modifier(tokens); modifier.has_value()) { - media_query->m_negated = modifier.value(); - tokens.skip_whitespace(); - } - - // `` - if (auto media_type = parse_media_type(tokens); media_type.has_value()) { - media_query->m_media_type = media_type.value(); - tokens.skip_whitespace(); - } else { - return invalid_media_query(); - } - - if (!tokens.has_next_token()) - return media_query; - - // `[ and ]?` - if (auto maybe_and = tokens.next_token(); maybe_and.is(Token::Type::Ident) && maybe_and.token().ident().equals_ignoring_ascii_case("and"sv)) { - if (auto media_condition = parse_media_condition(tokens, MediaCondition::AllowOr::No)) { - tokens.skip_whitespace(); - if (tokens.has_next_token()) - return invalid_media_query(); - media_query->m_media_condition = move(media_condition); - return media_query; - } - return invalid_media_query(); - } - - return invalid_media_query(); -} - -// ``, https://www.w3.org/TR/mediaqueries-4/#typedef-media-condition -// ``, https://www.w3.org/TR/mediaqueries-4/#typedef-media-condition-without-or -// (We distinguish between these two with the `allow_or` parameter.) -OwnPtr Parser::parse_media_condition(TokenStream& tokens, MediaCondition::AllowOr allow_or) -{ - // ` | [ * | * ]` - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - - // ` = not ` - auto parse_media_not = [&](auto& tokens) -> OwnPtr { - auto local_transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - - auto& first_token = tokens.next_token(); - if (first_token.is(Token::Type::Ident) && first_token.token().ident().equals_ignoring_ascii_case("not"sv)) { - if (auto child_condition = parse_media_condition(tokens, MediaCondition::AllowOr::Yes)) { - local_transaction.commit(); - return MediaCondition::from_not(child_condition.release_nonnull()); - } - } - - return {}; - }; - - auto parse_media_with_combinator = [&](auto& tokens, StringView combinator) -> OwnPtr { - auto local_transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - - auto& first = tokens.next_token(); - if (first.is(Token::Type::Ident) && first.token().ident().equals_ignoring_ascii_case(combinator)) { - tokens.skip_whitespace(); - if (auto media_in_parens = parse_media_in_parens(tokens)) { - local_transaction.commit(); - return media_in_parens; - } - } - - return {}; - }; - - // ` = and ` - auto parse_media_and = [&](auto& tokens) { return parse_media_with_combinator(tokens, "and"sv); }; - // ` = or ` - auto parse_media_or = [&](auto& tokens) { return parse_media_with_combinator(tokens, "or"sv); }; - - // `` - if (auto maybe_media_not = parse_media_not(tokens)) { - transaction.commit(); - return maybe_media_not.release_nonnull(); - } - - // ` [ * | * ]` - if (auto maybe_media_in_parens = parse_media_in_parens(tokens)) { - tokens.skip_whitespace(); - // Only `` - if (!tokens.has_next_token()) { - transaction.commit(); - return maybe_media_in_parens.release_nonnull(); - } - - Vector> child_conditions; - child_conditions.append(maybe_media_in_parens.release_nonnull()); - - // `*` - if (auto media_and = parse_media_and(tokens)) { - child_conditions.append(media_and.release_nonnull()); - - tokens.skip_whitespace(); - while (tokens.has_next_token()) { - if (auto next_media_and = parse_media_and(tokens)) { - child_conditions.append(next_media_and.release_nonnull()); - tokens.skip_whitespace(); - continue; - } - // We failed - invalid syntax! - return {}; - } - - transaction.commit(); - return MediaCondition::from_and_list(move(child_conditions)); - } - - // `*` - if (allow_or == MediaCondition::AllowOr::Yes) { - if (auto media_or = parse_media_or(tokens)) { - child_conditions.append(media_or.release_nonnull()); - - tokens.skip_whitespace(); - while (tokens.has_next_token()) { - if (auto next_media_or = parse_media_or(tokens)) { - child_conditions.append(next_media_or.release_nonnull()); - tokens.skip_whitespace(); - continue; - } - // We failed - invalid syntax! - return {}; - } - - transaction.commit(); - return MediaCondition::from_or_list(move(child_conditions)); - } - } - } - - return {}; -} - -// ``, https://www.w3.org/TR/mediaqueries-4/#typedef-media-feature -Optional Parser::parse_media_feature(TokenStream& tokens) -{ - // `[ | | ]` - tokens.skip_whitespace(); - - // ` = ` - struct MediaFeatureName { - enum Type { - Normal, - Min, - Max - } type; - MediaFeatureID id; - }; - auto parse_mf_name = [](auto& tokens, bool allow_min_max_prefix) -> Optional { - auto transaction = tokens.begin_transaction(); - auto& token = tokens.next_token(); - if (token.is(Token::Type::Ident)) { - auto name = token.token().ident(); - if (auto id = media_feature_id_from_string(name); id.has_value()) { - transaction.commit(); - return MediaFeatureName { MediaFeatureName::Type::Normal, id.value() }; - } - - if (allow_min_max_prefix && (name.starts_with("min-"sv, CaseSensitivity::CaseInsensitive) || name.starts_with("max-"sv, CaseSensitivity::CaseInsensitive))) { - auto adjusted_name = name.substring_view(4); - if (auto id = media_feature_id_from_string(adjusted_name); id.has_value() && media_feature_type_is_range(id.value())) { - transaction.commit(); - return MediaFeatureName { - name.starts_with("min-"sv, CaseSensitivity::CaseInsensitive) ? MediaFeatureName::Type::Min : MediaFeatureName::Type::Max, - id.value() - }; - } - } - } - return {}; - }; - - // ` = ` - auto parse_mf_boolean = [&](auto& tokens) -> Optional { - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - - if (auto maybe_name = parse_mf_name(tokens, false); maybe_name.has_value()) { - tokens.skip_whitespace(); - if (!tokens.has_next_token()) { - transaction.commit(); - return MediaFeature::boolean(maybe_name->id); - } - } - - return {}; - }; - - // ` = : ` - auto parse_mf_plain = [&](auto& tokens) -> Optional { - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - - if (auto maybe_name = parse_mf_name(tokens, true); maybe_name.has_value()) { - tokens.skip_whitespace(); - if (tokens.next_token().is(Token::Type::Colon)) { - tokens.skip_whitespace(); - if (auto maybe_value = parse_media_feature_value(maybe_name->id, tokens); maybe_value.has_value()) { - tokens.skip_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()); - } - VERIFY_NOT_REACHED(); - } - } - } - } - return {}; - }; - - // ` = '<' '='? - // = '>' '='? - // = '=' - // = | | ` - auto parse_comparison = [](auto& tokens) -> Optional { - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - - auto& first = tokens.next_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.peek_token(); - if (second.is_delim('=')) { - tokens.next_token(); - transaction.commit(); - return MediaFeature::Comparison::LessThanOrEqual; - } - transaction.commit(); - return MediaFeature::Comparison::LessThan; - } - if (first_delim == '>') { - auto& second = tokens.peek_token(); - if (second.is_delim('=')) { - tokens.next_token(); - transaction.commit(); - return MediaFeature::Comparison::GreaterThanOrEqual; - } - transaction.commit(); - return MediaFeature::Comparison::GreaterThan; - } - } - - return {}; - }; - - auto flip = [](MediaFeature::Comparison comparison) { - switch (comparison) { - case MediaFeature::Comparison::Equal: - return MediaFeature::Comparison::Equal; - case MediaFeature::Comparison::LessThan: - return MediaFeature::Comparison::GreaterThan; - case MediaFeature::Comparison::LessThanOrEqual: - return MediaFeature::Comparison::GreaterThanOrEqual; - case MediaFeature::Comparison::GreaterThan: - return MediaFeature::Comparison::LessThan; - case MediaFeature::Comparison::GreaterThanOrEqual: - return MediaFeature::Comparison::LessThanOrEqual; - } - VERIFY_NOT_REACHED(); - }; - - 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(); - }; - - // ` = - // | - // | - // | ` - auto parse_mf_range = [&](auto& tokens) -> Optional { - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - - // ` ` - // NOTE: We have to check for first, since all s will also parse as . - if (auto maybe_name = parse_mf_name(tokens, false); maybe_name.has_value() && media_feature_type_is_range(maybe_name->id)) { - tokens.skip_whitespace(); - if (auto maybe_comparison = parse_comparison(tokens); maybe_comparison.has_value()) { - tokens.skip_whitespace(); - if (auto maybe_value = parse_media_feature_value(maybe_name->id, tokens); maybe_value.has_value()) { - tokens.skip_whitespace(); - if (!tokens.has_next_token() && !maybe_value->is_ident()) { - transaction.commit(); - return MediaFeature::half_range(maybe_value.release_value(), flip(maybe_comparison.release_value()), maybe_name->id); - } - } - } - } - - // ` - // | - // | ` - // NOTE: To parse the first value, we need to first find and parse the so we know what value types to parse. - // To allow for 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 , which should be correct as long as they don't add a value - // type that can include a comparison in it. :^) - Optional 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()) { - // We found a comparison, so the next non-whitespace token should be the - tokens.skip_whitespace(); - maybe_name = parse_mf_name(tokens, false); - break; - } - tokens.next_token(); - tokens.skip_whitespace(); - } - } - - // Now, we can parse the range properly. - if (maybe_name.has_value() && media_feature_type_is_range(maybe_name->id)) { - if (auto maybe_left_value = parse_media_feature_value(maybe_name->id, tokens); maybe_left_value.has_value()) { - tokens.skip_whitespace(); - if (auto maybe_left_comparison = parse_comparison(tokens); maybe_left_comparison.has_value()) { - tokens.skip_whitespace(); - tokens.next_token(); // The which we already parsed above. - tokens.skip_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); - } - - if (auto maybe_right_comparison = parse_comparison(tokens); maybe_right_comparison.has_value()) { - tokens.skip_whitespace(); - if (auto maybe_right_value = parse_media_feature_value(maybe_name->id, tokens); maybe_right_value.has_value()) { - tokens.skip_whitespace(); - // For this to be valid, the following must be true: - // - Comparisons must either both be >/>= or both be 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 {}; - }; - - if (auto maybe_mf_boolean = parse_mf_boolean(tokens); maybe_mf_boolean.has_value()) - return maybe_mf_boolean.release_value(); - - if (auto maybe_mf_plain = parse_mf_plain(tokens); maybe_mf_plain.has_value()) - return maybe_mf_plain.release_value(); - - if (auto maybe_mf_range = parse_mf_range(tokens); maybe_mf_range.has_value()) - return maybe_mf_range.release_value(); - - return {}; -} - -Optional Parser::parse_media_type(TokenStream& tokens) -{ - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - auto const& token = tokens.next_token(); - - if (!token.is(Token::Type::Ident)) - return {}; - - transaction.commit(); - - auto ident = token.token().ident(); - return media_type_from_string(ident); -} - -// ``, https://www.w3.org/TR/mediaqueries-4/#typedef-media-in-parens -OwnPtr Parser::parse_media_in_parens(TokenStream& tokens) -{ - // ` = ( ) | ( ) | ` - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - - // `( ) | ( )` - auto const& first_token = tokens.peek_token(); - if (first_token.is_block() && first_token.block().is_paren()) { - TokenStream inner_token_stream { first_token.block().values() }; - if (auto maybe_media_condition = parse_media_condition(inner_token_stream, MediaCondition::AllowOr::Yes)) { - tokens.next_token(); - transaction.commit(); - return maybe_media_condition.release_nonnull(); - } - if (auto maybe_media_feature = parse_media_feature(inner_token_stream); maybe_media_feature.has_value()) { - tokens.next_token(); - transaction.commit(); - return MediaCondition::from_feature(maybe_media_feature.release_value()); - } - } - - // `` - // FIXME: We should only be taking this branch if the grammar doesn't match the above options. - // Currently we take it if the above fail to parse, which is different. - // eg, `@media (min-width: 76yaks)` is valid grammar, but does not parse because `yaks` isn't a unit. - if (auto maybe_general_enclosed = parse_general_enclosed(tokens); maybe_general_enclosed.has_value()) { - transaction.commit(); - return MediaCondition::from_general_enclosed(maybe_general_enclosed.release_value()); - } - - return {}; -} - -// ``, https://www.w3.org/TR/mediaqueries-4/#typedef-mf-value -Optional Parser::parse_media_feature_value(MediaFeatureID media_feature, TokenStream& tokens) -{ - // Identifiers - if (tokens.peek_token().is(Token::Type::Ident)) { - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - auto ident = value_id_from_string(tokens.next_token().token().ident()); - if (ident.has_value() && media_feature_accepts_identifier(media_feature, ident.value())) { - transaction.commit(); - return MediaFeatureValue(ident.value()); - } - } - - // One branch for each member of the MediaFeatureValueType enum: - - // Boolean ( in the spec: a 1 or 0) - if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Boolean)) { - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - auto const& first = tokens.next_token(); - if (first.is(Token::Type::Number) && first.token().number().is_integer() - && (first.token().number_value() == 0 || first.token().number_value() == 1)) { - transaction.commit(); - return MediaFeatureValue(first.token().number_value()); - } - } - - // Integer - if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Integer)) { - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - auto const& first = tokens.next_token(); - if (first.is(Token::Type::Number) && first.token().number().is_integer()) { - transaction.commit(); - return MediaFeatureValue(first.token().number_value()); - } - } - - // Length - if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Length)) { - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - auto const& first = tokens.next_token(); - if (auto length = parse_length(first); length.has_value()) { - transaction.commit(); - return MediaFeatureValue(length.release_value()); - } - } - - // Ratio - if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Ratio)) { - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - if (auto ratio = parse_ratio(tokens); ratio.has_value()) { - transaction.commit(); - return MediaFeatureValue(ratio.release_value()); - } - } - - // Resolution - if (media_feature_accepts_type(media_feature, MediaFeatureValueType::Resolution)) { - auto transaction = tokens.begin_transaction(); - tokens.skip_whitespace(); - auto const& first = tokens.next_token(); - if (auto resolution = parse_dimension(first); resolution.has_value() && resolution->is_resolution()) { - transaction.commit(); - return MediaFeatureValue(resolution->resolution()); - } - } - - return {}; -} - RefPtr Parser::parse_as_supports() { return parse_a_supports(m_token_stream); @@ -2221,6 +1623,8 @@ Vector> Parser::parse_a_list_of_rules(TokenStream& tokens // 3. Return the returned list. return list_of_rules; } +template Vector> Parser::parse_a_list_of_rules(TokenStream& tokens); +template Vector> Parser::parse_a_list_of_rules(TokenStream& tokens); Optional Parser::parse_as_supports_condition() { @@ -2373,8 +1777,8 @@ Vector> Parser::parse_a_comma_separated_list_of_component // 4. Return list of cvls. return list_of_component_value_lists; } - template Vector> Parser::parse_a_comma_separated_list_of_component_values(TokenStream&); +template Vector> Parser::parse_a_comma_separated_list_of_component_values(TokenStream&); ElementInlineCSSStyleDeclaration* Parser::parse_as_style_attribute(DOM::Element& element) { @@ -3173,23 +2577,8 @@ CSSRule* Parser::convert_to_rule(NonnullRefPtr rule) dbgln_if(CSS_PARSER_DEBUG, "Unable to parse url from @import rule"); return {}; } - if (rule->at_rule_name().equals_ignoring_ascii_case("media"sv)) { - auto media_query_tokens = TokenStream { rule->prelude() }; - auto media_query_list = parse_a_media_query_list(media_query_tokens); - if (media_query_list.is_empty() || !rule->block()) - return {}; - - auto child_tokens = TokenStream { rule->block()->values() }; - auto parser_rules = parse_a_list_of_rules(child_tokens); - JS::MarkedVector child_rules(m_context.realm().heap()); - for (auto& raw_rule : parser_rules) { - if (auto* child_rule = convert_to_rule(raw_rule)) - child_rules.append(child_rule); - } - auto media_list = MediaList::create(m_context.realm(), move(media_query_list)); - auto rule_list = CSSRuleList::create(m_context.realm(), child_rules); - return CSSMediaRule::create(m_context.realm(), media_list, rule_list); - } + if (rule->at_rule_name().equals_ignoring_ascii_case("media"sv)) + return convert_to_media_rule(rule); if (rule->at_rule_name().equals_ignoring_ascii_case("supports"sv)) { auto supports_tokens = TokenStream { rule->prelude() }; auto supports = parse_a_supports(supports_tokens); diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h index a5021cd068b..ca600584e7e 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h @@ -185,6 +185,8 @@ private: Vector parse_font_face_src(TokenStream&); CSSRule* convert_to_rule(NonnullRefPtr); + CSSMediaRule* convert_to_media_rule(NonnullRefPtr); + PropertyOwningCSSStyleDeclaration* convert_to_style_declaration(Vector const& declarations); Optional convert_to_style_property(Declaration const&);