diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp index 5599f2d5bd..1ef678fd5d 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -672,13 +672,283 @@ RefPtr Parser::parse_as_media_query() return nullptr; } -NonnullRefPtr Parser::parse_media_query(TokenStream&) +NonnullRefPtr Parser::parse_media_query(TokenStream& tokens) { - // "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 + // Returns whether to negate the query + auto consume_initial_modifier = [](auto& tokens) -> Optional { + auto& token = tokens.next_token(); - // FIXME: Implement media queries! - return MediaQuery::create_not_all(); + if (!token.is(Token::Type::Ident)) { + tokens.reconsume_current_input_token(); + return {}; + } + + auto ident = token.token().ident(); + if (ident.equals_ignoring_case("not")) { + return true; + } else if (ident.equals_ignoring_case("only")) { + return false; + } + tokens.reconsume_current_input_token(); + 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(); + + // Only `` + if (auto media_condition = consume_media_condition(tokens)) { + tokens.skip_whitespace(); + if (tokens.has_next_token()) + return invalid_media_query(); + media_query->m_media_condition = move(media_condition); + return media_query; + } + + // Optional `"only" | "not"` + if (auto modifier = consume_initial_modifier(tokens); modifier.has_value()) { + media_query->m_negated = modifier.value(); + tokens.skip_whitespace(); + } + + // `` + if (auto media_type = consume_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; + + // Optional "and " + if (auto maybe_and = tokens.next_token(); maybe_and.is(Token::Type::Ident) && maybe_and.token().ident().equals_ignoring_case("and")) { + if (auto media_condition = consume_media_condition(tokens)) { + 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(); +} + +OwnPtr Parser::consume_media_condition(TokenStream& tokens) +{ + // "not " + auto position = tokens.position(); + auto& first_token = tokens.peek_token(); + if (first_token.is(Token::Type::Ident) && first_token.token().ident().equals_ignoring_case("not"sv)) { + tokens.next_token(); + + auto condition = new MediaQuery::MediaCondition; + condition->type = MediaQuery::MediaCondition::Type::Not; + + if (auto child_condition = consume_media_condition(tokens)) { + condition->conditions.append(child_condition.release_nonnull()); + return adopt_own(*condition); + } + + tokens.rewind_to_position(position); + return {}; + } + + // " ([and | or] )*" + NonnullOwnPtrVector child_conditions; + Optional condition_type {}; + auto as_condition_type = [](auto& token) -> Optional { + if (!token.is(Token::Type::Ident)) + return {}; + auto ident = token.token().ident(); + if (ident.equals_ignoring_case("and")) + return MediaQuery::MediaCondition::Type::And; + if (ident.equals_ignoring_case("or")) + return MediaQuery::MediaCondition::Type::Or; + return {}; + }; + + bool is_invalid = false; + tokens.skip_whitespace(); + while (tokens.has_next_token()) { + if (!child_conditions.is_empty()) { + // Expect an "and" or "or" here + auto maybe_combination = as_condition_type(tokens.next_token()); + if (!maybe_combination.has_value()) { + is_invalid = true; + break; + } + if (!condition_type.has_value()) { + condition_type = maybe_combination.value(); + } else if (maybe_combination != condition_type) { + is_invalid = true; + break; + } + } + + tokens.skip_whitespace(); + + if (auto child_feature = consume_media_feature(tokens); child_feature.has_value()) { + auto child = new MediaQuery::MediaCondition; + child->type = MediaQuery::MediaCondition::Type::Single; + child->feature = child_feature.value(); + child_conditions.append(adopt_own(*child)); + } else { + auto& token = tokens.next_token(); + if (!token.is_block() || !token.block().is_paren()) { + is_invalid = true; + break; + } + auto block_tokens = TokenStream { token.block().values() }; + if (auto child = consume_media_condition(block_tokens)) { + child_conditions.append(child.release_nonnull()); + } else { + is_invalid = true; + break; + } + } + + tokens.skip_whitespace(); + } + + if (!is_invalid && !child_conditions.is_empty()) { + if (child_conditions.size() == 1) + return move(child_conditions.ptr_at(0)); + + auto condition = new MediaQuery::MediaCondition; + condition->type = condition_type.value(); + condition->conditions = move(child_conditions); + return adopt_own(*condition); + } + + // "" + tokens.rewind_to_position(position); + if (auto feature = consume_media_feature(tokens); feature.has_value()) { + auto condition = new MediaQuery::MediaCondition; + condition->type = MediaQuery::MediaCondition::Type::Single; + condition->feature = feature.value(); + return adopt_own(*condition); + } + + tokens.rewind_to_position(position); + return {}; +} + +Optional Parser::consume_media_feature(TokenStream& outer_tokens) +{ + outer_tokens.skip_whitespace(); + + auto invalid_feature = [&]() -> Optional { + outer_tokens.reconsume_current_input_token(); + return {}; + }; + + auto& block_token = outer_tokens.next_token(); + if (block_token.is_block() && block_token.block().is_paren()) { + TokenStream tokens { block_token.block().values() }; + + tokens.skip_whitespace(); + auto& name_token = tokens.next_token(); + + // FIXME: Range syntax allows a value to come before the name + // https://www.w3.org/TR/mediaqueries-4/#mq-range-context + if (!name_token.is(Token::Type::Ident)) + return invalid_feature(); + + auto feature_name = name_token.token().ident(); + tokens.skip_whitespace(); + + if (!tokens.has_next_token()) { + return MediaQuery::MediaFeature { + .type = MediaQuery::MediaFeature::Type::IsTrue, + .name = feature_name, + }; + } + + if (!tokens.next_token().is(Token::Type::Colon)) + return invalid_feature(); + tokens.skip_whitespace(); + + auto value = parse_css_value(PropertyID::Custom, tokens); + if (value.is_error()) + return invalid_feature(); + + if (tokens.has_next_token()) + return invalid_feature(); + + if (feature_name.starts_with("min-", CaseSensitivity::CaseInsensitive)) { + return MediaQuery::MediaFeature { + .type = MediaQuery::MediaFeature::Type::MinValue, + .name = feature_name.substring_view(4), + .value = value.release_value(), + }; + } else if (feature_name.starts_with("max-", CaseSensitivity::CaseInsensitive)) { + return MediaQuery::MediaFeature { + .type = MediaQuery::MediaFeature::Type::MaxValue, + .name = feature_name.substring_view(4), + .value = value.release_value(), + }; + } + + return MediaQuery::MediaFeature { + .type = MediaQuery::MediaFeature::Type::ExactValue, + .name = feature_name, + .value = value.release_value(), + }; + } + + return invalid_feature(); +} + +Optional Parser::consume_media_type(TokenStream& tokens) +{ + auto& token = tokens.next_token(); + + if (!token.is(Token::Type::Ident)) { + tokens.reconsume_current_input_token(); + return {}; + } + + auto ident = token.token().ident(); + if (ident.equals_ignoring_case("all")) { + return MediaQuery::MediaType::All; + } else if (ident.equals_ignoring_case("aural")) { + return MediaQuery::MediaType::Aural; + } else if (ident.equals_ignoring_case("braille")) { + return MediaQuery::MediaType::Braille; + } else if (ident.equals_ignoring_case("embossed")) { + return MediaQuery::MediaType::Embossed; + } else if (ident.equals_ignoring_case("handheld")) { + return MediaQuery::MediaType::Handheld; + } else if (ident.equals_ignoring_case("print")) { + return MediaQuery::MediaType::Print; + } else if (ident.equals_ignoring_case("projection")) { + return MediaQuery::MediaType::Projection; + } else if (ident.equals_ignoring_case("screen")) { + return MediaQuery::MediaType::Screen; + } else if (ident.equals_ignoring_case("speech")) { + return MediaQuery::MediaType::Speech; + } else if (ident.equals_ignoring_case("tty")) { + return MediaQuery::MediaType::TTY; + } else if (ident.equals_ignoring_case("tv")) { + return MediaQuery::MediaType::TV; + } + + tokens.reconsume_current_input_token(); + return {}; } NonnullRefPtrVector Parser::consume_a_list_of_rules(bool top_level) diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h index 0eace6cfc2..d3b55de014 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h @@ -227,7 +227,10 @@ private: Optional parse_selector_combinator(TokenStream&); Result parse_simple_selector(TokenStream&); - static NonnullRefPtr parse_media_query(TokenStream&); + NonnullRefPtr parse_media_query(TokenStream&); + OwnPtr consume_media_condition(TokenStream&); + Optional consume_media_feature(TokenStream&); + Optional consume_media_type(TokenStream&); static bool has_ignored_vendor_prefix(StringView const&);