Pārlūkot izejas kodu

LibWeb: Rewrite media-query parsing to match spec grammar

Past me decided that the grammar was overly verbose and I could do it
better myself. Which seemed fine until the spec changed and I didn't
know how to integrate the changes. Lesson learned! :^)

Rather than have a function for every single part of the grammar, I have
written some as lambdas, and combned `<media-condition>` and
`<media-condition-without-or>` into one function. But otherwise it's
close to the spec, with comments listing the part of the grammar being
parsed, so hopefully it will be easier to make future adjustments!

This does not add any new functionality.
Sam Atkins 3 gadi atpakaļ
vecāks
revīzija
c3bf9e5b79

+ 6 - 0
Userland/Libraries/LibWeb/CSS/MediaQuery.h

@@ -122,6 +122,12 @@ struct MediaCondition {
         GeneralEnclosed,
     };
 
+    // Only used in parsing
+    enum class AllowOr {
+        No = 0,
+        Yes = 1,
+    };
+
     static NonnullOwnPtr<MediaCondition> from_general_enclosed(GeneralEnclosed&&);
     static NonnullOwnPtr<MediaCondition> from_feature(MediaFeature&&);
     static NonnullOwnPtr<MediaCondition> from_not(NonnullOwnPtr<MediaCondition>&&);

+ 179 - 111
Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp

@@ -674,14 +674,22 @@ RefPtr<MediaQuery> Parser::parse_as_media_query()
     return nullptr;
 }
 
+// `<media-query>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-query
 NonnullRefPtr<MediaQuery> Parser::parse_media_query(TokenStream<StyleComponentValueRule>& tokens)
 {
-    // Returns whether to negate the query
-    auto consume_initial_modifier = [](auto& tokens) -> Optional<bool> {
+    // `<media-query> = <media-condition>
+    //                | [ not | only ]? <media-type> [ and <media-condition-without-or> ]?`
+    auto position = tokens.position();
+    tokens.skip_whitespace();
+
+    // `[ not | only ]?`, Returns whether to negate the query
+    auto parse_initial_modifier = [](auto& tokens) -> Optional<bool> {
+        auto position = tokens.position();
+        tokens.skip_whitespace();
         auto& token = tokens.next_token();
 
         if (!token.is(Token::Type::Ident)) {
-            tokens.reconsume_current_input_token();
+            tokens.rewind_to_position(position);
             return {};
         }
 
@@ -691,7 +699,7 @@ NonnullRefPtr<MediaQuery> Parser::parse_media_query(TokenStream<StyleComponentVa
         } else if (ident.equals_ignoring_case("only")) {
             return false;
         }
-        tokens.reconsume_current_input_token();
+        tokens.rewind_to_position(position);
         return {};
     };
 
@@ -702,14 +710,15 @@ NonnullRefPtr<MediaQuery> Parser::parse_media_query(TokenStream<StyleComponentVa
             dbgln("Invalid media query:");
             tokens.dump_all_tokens();
         }
+        tokens.rewind_to_position(position);
         return MediaQuery::create_not_all();
     };
 
     auto media_query = MediaQuery::create();
     tokens.skip_whitespace();
 
-    // Only `<media-condition>`
-    if (auto media_condition = consume_media_condition(tokens)) {
+    // `<media-condition>`
+    if (auto media_condition = parse_media_condition(tokens, MediaCondition::AllowOr::Yes)) {
         tokens.skip_whitespace();
         if (tokens.has_next_token())
             return invalid_media_query();
@@ -717,14 +726,14 @@ NonnullRefPtr<MediaQuery> Parser::parse_media_query(TokenStream<StyleComponentVa
         return media_query;
     }
 
-    // Optional `"only" | "not"`
-    if (auto modifier = consume_initial_modifier(tokens); modifier.has_value()) {
+    // `[ not | only ]?`
+    if (auto modifier = parse_initial_modifier(tokens); modifier.has_value()) {
         media_query->m_negated = modifier.value();
         tokens.skip_whitespace();
     }
 
     // `<media-type>`
-    if (auto media_type = consume_media_type(tokens); media_type.has_value()) {
+    if (auto media_type = parse_media_type(tokens); media_type.has_value()) {
         media_query->m_media_type = media_type.value();
         tokens.skip_whitespace();
     } else {
@@ -734,9 +743,9 @@ NonnullRefPtr<MediaQuery> Parser::parse_media_query(TokenStream<StyleComponentVa
     if (!tokens.has_next_token())
         return media_query;
 
-    // Optional "and <media-condition>"
+    // `[ and <media-condition-without-or> ]?`
     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)) {
+        if (auto media_condition = parse_media_condition(tokens, MediaCondition::AllowOr::No)) {
             tokens.skip_whitespace();
             if (tokens.has_next_token())
                 return invalid_media_query();
@@ -749,150 +758,180 @@ NonnullRefPtr<MediaQuery> Parser::parse_media_query(TokenStream<StyleComponentVa
     return invalid_media_query();
 }
 
-OwnPtr<MediaCondition> Parser::consume_media_condition(TokenStream<StyleComponentValueRule>& tokens)
+// `<media-condition>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-condition
+// `<media-condition-widthout-or>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-condition-without-or
+// (We distinguish between these two with the `allow_or` parameter.)
+OwnPtr<MediaCondition> Parser::parse_media_condition(TokenStream<StyleComponentValueRule>& tokens, MediaCondition::AllowOr allow_or)
 {
-    // "not <media-condition>"
-    // ( `<media-not>` in the grammar )
+    // `<media-not> | <media-in-parens> [ <media-and>* | <media-or>* ]`
     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();
+    tokens.skip_whitespace();
 
-        if (auto child_condition = consume_media_condition(tokens))
-            return MediaCondition::from_not(child_condition.release_nonnull());
+    // `<media-not> = not <media-in-parens>`
+    auto parse_media_not = [&](auto& tokens) -> OwnPtr<MediaCondition> {
+        auto position = tokens.position();
+        tokens.skip_whitespace();
+
+        auto& first_token = tokens.next_token();
+        if (first_token.is(Token::Type::Ident) && first_token.token().ident().equals_ignoring_case("not"sv)) {
+            if (auto child_condition = parse_media_condition(tokens, MediaCondition::AllowOr::Yes))
+                return MediaCondition::from_not(child_condition.release_nonnull());
+        }
 
         tokens.rewind_to_position(position);
         return {};
-    }
+    };
 
-    // "<media-condition> ([and | or] <media-condition>)*"
-    // ( `<media-in-parens> [ <media-and>* | <media-or>* ]` in the grammar )
-    NonnullOwnPtrVector<MediaCondition> child_conditions;
-    Optional<MediaCondition::Type> condition_type {};
-    auto as_condition_type = [](auto& token) -> Optional<MediaCondition::Type> {
-        if (!token.is(Token::Type::Ident))
-            return {};
-        auto ident = token.token().ident();
-        if (ident.equals_ignoring_case("and"))
-            return MediaCondition::Type::And;
-        if (ident.equals_ignoring_case("or"))
-            return MediaCondition::Type::Or;
+    auto parse_media_with_combinator = [&](auto& tokens, StringView combinator) -> OwnPtr<MediaCondition> {
+        auto position = tokens.position();
+        tokens.skip_whitespace();
+
+        auto& first = tokens.next_token();
+        if (first.is(Token::Type::Ident) && first.token().ident().equals_ignoring_case(combinator)) {
+            tokens.skip_whitespace();
+            if (auto media_in_parens = parse_media_in_parens(tokens))
+                return media_in_parens;
+        }
+
+        tokens.rewind_to_position(position);
         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;
-            }
-        }
+    // `<media-and> = and <media-in-parens>`
+    auto parse_media_and = [&](auto& tokens) { return parse_media_with_combinator(tokens, "and"sv); };
+    // `<media-or> = or <media-in-parens>`
+    auto parse_media_or = [&](auto& tokens) { return parse_media_with_combinator(tokens, "or"sv); };
 
+    // `<media-not>`
+    if (auto maybe_media_not = parse_media_not(tokens))
+        return maybe_media_not.release_nonnull();
+
+    // `<media-in-parens> [ <media-and>* | <media-or>* ]`
+    if (auto maybe_media_in_parens = parse_media_in_parens(tokens)) {
         tokens.skip_whitespace();
+        // Only `<media-in-parens>`
+        if (!tokens.has_next_token())
+            return maybe_media_in_parens.release_nonnull();
 
-        if (auto child_feature = consume_media_feature(tokens); child_feature.has_value()) {
-            child_conditions.append(MediaCondition::from_feature(child_feature.release_value()));
-        } 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;
-            }
-        }
+        NonnullOwnPtrVector<MediaCondition> child_conditions;
+        child_conditions.append(maybe_media_in_parens.release_nonnull());
 
-        tokens.skip_whitespace();
-    }
+        // `<media-and>*`
+        if (auto media_and = parse_media_and(tokens)) {
+            child_conditions.append(media_and.release_nonnull());
 
-    if (!is_invalid && !child_conditions.is_empty()) {
-        if (child_conditions.size() == 1)
-            return move(child_conditions.ptr_at(0));
+            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!
+                tokens.rewind_to_position(position);
+                return {};
+            }
 
-        if (condition_type.value() == MediaCondition::Type::And)
             return MediaCondition::from_and_list(move(child_conditions));
-        return MediaCondition::from_or_list(move(child_conditions));
-    }
+        }
 
-    // `<media-feature>`
-    tokens.rewind_to_position(position);
-    if (auto feature = consume_media_feature(tokens); feature.has_value())
-        return MediaCondition::from_feature(feature.release_value());
+        // `<media-or>*`
+        if (allow_or == MediaCondition::AllowOr::Yes) {
+            if (auto media_or = parse_media_or(tokens)) {
+                child_conditions.append(media_or.release_nonnull());
 
-    // `<general-enclosed>`
-    tokens.rewind_to_position(position);
-    if (auto general_enclosed = parse_general_enclosed(tokens); general_enclosed.has_value())
-        return MediaCondition::from_general_enclosed(general_enclosed.release_value());
+                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!
+                    tokens.rewind_to_position(position);
+                    return {};
+                }
+
+                return MediaCondition::from_or_list(move(child_conditions));
+            }
+        }
+    }
 
     tokens.rewind_to_position(position);
     return {};
 }
 
-Optional<MediaFeature> Parser::consume_media_feature(TokenStream<StyleComponentValueRule>& outer_tokens)
+// `<media-feature>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-feature
+Optional<MediaFeature> Parser::parse_media_feature(TokenStream<StyleComponentValueRule>& tokens)
 {
-    outer_tokens.skip_whitespace();
+    // `[ <mf-plain> | <mf-boolean> | <mf-range> ]`
+    auto position = tokens.position();
+    tokens.skip_whitespace();
 
-    auto invalid_feature = [&]() -> Optional<MediaFeature> {
-        outer_tokens.reconsume_current_input_token();
+    // `<mf-name> = <ident>`
+    auto parse_mf_name = [](StyleComponentValueRule const& token) -> Optional<String> {
+        if (token.is(Token::Type::Ident))
+            return token.token().ident();
         return {};
     };
 
-    auto& block_token = outer_tokens.next_token();
-    if (block_token.is_block() && block_token.block().is_paren()) {
-        TokenStream tokens { block_token.block().values() };
-
+    // `<mf-boolean> = <mf-name>`
+    auto parse_mf_boolean = [&](auto& tokens) -> Optional<MediaFeature> {
+        auto position = tokens.position();
         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 maybe_name = parse_mf_name(tokens.next_token());
+        if (maybe_name.has_value()) {
+            tokens.skip_whitespace();
+            if (!tokens.has_next_token())
+                return MediaFeature::boolean(maybe_name.release_value());
+        }
+
+        tokens.rewind_to_position(position);
+        return {};
+    };
 
-        auto feature_name = name_token.token().ident();
+    // `<mf-plain> = <mf-name> : <mf-value>`
+    auto parse_mf_plain = [&](auto& tokens) -> Optional<MediaFeature> {
+        auto position = tokens.position();
         tokens.skip_whitespace();
 
-        if (!tokens.has_next_token())
-            return MediaFeature::boolean(feature_name);
+        if (auto maybe_name = parse_mf_name(tokens.next_token()); 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(tokens); maybe_value.has_value()) {
+                    tokens.skip_whitespace();
+                    if (!tokens.has_next_token())
+                        return MediaFeature::plain(maybe_name.release_value(), maybe_value.release_value());
+                }
+            }
+        }
 
-        if (!tokens.next_token().is(Token::Type::Colon))
-            return invalid_feature();
-        tokens.skip_whitespace();
+        tokens.rewind_to_position(position);
+        return {};
+    };
 
-        auto value = parse_media_feature_value(tokens);
-        if (!value.has_value())
-            return invalid_feature();
+    if (auto maybe_mf_boolean = parse_mf_boolean(tokens); maybe_mf_boolean.has_value())
+        return maybe_mf_boolean.release_value();
 
-        if (tokens.has_next_token())
-            return invalid_feature();
+    if (auto maybe_mf_plain = parse_mf_plain(tokens); maybe_mf_plain.has_value())
+        return maybe_mf_plain.release_value();
 
-        return MediaFeature::plain(feature_name, value.release_value());
-    }
+    // FIXME: Implement range syntax
 
-    return invalid_feature();
+    tokens.rewind_to_position(position);
+    return {};
 }
 
-Optional<MediaQuery::MediaType> Parser::consume_media_type(TokenStream<StyleComponentValueRule>& tokens)
+Optional<MediaQuery::MediaType> Parser::parse_media_type(TokenStream<StyleComponentValueRule>& tokens)
 {
+    auto position = tokens.position();
+    tokens.skip_whitespace();
     auto& token = tokens.next_token();
 
     if (!token.is(Token::Type::Ident)) {
-        tokens.reconsume_current_input_token();
+        tokens.rewind_to_position(position);
         return {};
     }
 
@@ -921,7 +960,36 @@ Optional<MediaQuery::MediaType> Parser::consume_media_type(TokenStream<StyleComp
         return MediaQuery::MediaType::TV;
     }
 
-    tokens.reconsume_current_input_token();
+    tokens.rewind_to_position(position);
+    return {};
+}
+
+// `<media-in-parens>`, https://www.w3.org/TR/mediaqueries-4/#typedef-media-in-parens
+OwnPtr<MediaCondition> Parser::parse_media_in_parens(TokenStream<StyleComponentValueRule>& tokens)
+{
+    // `<media-in-parens> = ( <media-condition> ) | ( <media-feature> ) | <general-enclosed>`
+    auto position = tokens.position();
+    tokens.skip_whitespace();
+
+    // `( <media-condition> ) | ( <media-feature> )`
+    auto& 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();
+            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();
+            return MediaCondition::from_feature(maybe_media_feature.release_value());
+        }
+    }
+
+    // `<general-enclosed>`
+    if (auto maybe_general_enclosed = parse_general_enclosed(tokens); maybe_general_enclosed.has_value())
+        return MediaCondition::from_general_enclosed(maybe_general_enclosed.release_value());
+
+    tokens.rewind_to_position(position);
     return {};
 }
 

+ 4 - 3
Userland/Libraries/LibWeb/CSS/Parser/Parser.h

@@ -235,9 +235,10 @@ private:
     Result<Selector::SimpleSelector, ParsingResult> parse_simple_selector(TokenStream<StyleComponentValueRule>&);
 
     NonnullRefPtr<MediaQuery> parse_media_query(TokenStream<StyleComponentValueRule>&);
-    OwnPtr<MediaCondition> consume_media_condition(TokenStream<StyleComponentValueRule>&);
-    Optional<MediaFeature> consume_media_feature(TokenStream<StyleComponentValueRule>&);
-    Optional<MediaQuery::MediaType> consume_media_type(TokenStream<StyleComponentValueRule>&);
+    OwnPtr<MediaCondition> parse_media_condition(TokenStream<StyleComponentValueRule>&, MediaCondition::AllowOr allow_or);
+    Optional<MediaFeature> parse_media_feature(TokenStream<StyleComponentValueRule>&);
+    Optional<MediaQuery::MediaType> parse_media_type(TokenStream<StyleComponentValueRule>&);
+    OwnPtr<MediaCondition> parse_media_in_parens(TokenStream<StyleComponentValueRule>&);
     Optional<MediaFeatureValue> parse_media_feature_value(TokenStream<StyleComponentValueRule>&);
 
     OwnPtr<Supports::Condition> parse_supports_condition(TokenStream<StyleComponentValueRule>&);