diff --git a/Base/res/html/misc/media-queries.html b/Base/res/html/misc/media-queries.html index 86c746756e7..acfec62f2bd 100644 --- a/Base/res/html/misc/media-queries.html +++ b/Base/res/html/misc/media-queries.html @@ -41,6 +41,13 @@ } } + @media only all and (width > 399px) { + .size-min-range { + background-color: lime; + border: 1px solid black; + } + } + @media (max-width: 1000px) { .size-max { background-color: lime; @@ -48,6 +55,13 @@ } } + @media (1001px > width) { + .size-max-range { + background-color: lime; + border: 1px solid black; + } + } + @media (min-width: 400px) and (max-width: 1000px) { .size-range { background-color: lime; @@ -55,6 +69,13 @@ } } + @media (400px <= width <= 1000px) { + .size-range-syntax { + background-color: lime; + border: 1px solid black; + } + } + @media (color) { .color { background-color: lime; @@ -90,12 +111,21 @@

This should be green, with a black border and black text, if the window is at least 400px wide.

+

+ This should be green, with a black border and black text, if the window is at least 400px wide, and we understand range syntax. +

This should be green, with a black border and black text, if the window is at most 1000px wide.

+

+ This should be green, with a black border and black text, if the window is at most 1000px wide, and we understand range syntax. +

This should be green, with a black border and black text, if the window is between 400px and 1000px wide.

+

+ This should be green, with a black border and black text, if the window is between 400px and 1000px wide, and we understand range syntax. +

This should be green, with a black border and black text, if we detected the color feature.

diff --git a/Userland/Libraries/LibWeb/CSS/MediaQuery.cpp b/Userland/Libraries/LibWeb/CSS/MediaQuery.cpp index c45a238ef42..f25aa62fdae 100644 --- a/Userland/Libraries/LibWeb/CSS/MediaQuery.cpp +++ b/Userland/Libraries/LibWeb/CSS/MediaQuery.cpp @@ -61,15 +61,36 @@ bool MediaFeatureValue::equals(MediaFeatureValue const& other) const String MediaFeature::to_string() const { + auto comparison_string = [](Comparison comparison) -> StringView { + switch (comparison) { + case Comparison::Equal: + return "="sv; + case Comparison::LessThan: + return "<"sv; + case Comparison::LessThanOrEqual: + return "<="sv; + case Comparison::GreaterThan: + return ">"sv; + case Comparison::GreaterThanOrEqual: + return ">="sv; + } + VERIFY_NOT_REACHED(); + }; + switch (m_type) { case Type::IsTrue: - return m_name; + return serialize_an_identifier(m_name); case Type::ExactValue: - return String::formatted("{}:{}", m_name, m_value->to_string()); + return String::formatted("{}:{}", serialize_an_identifier(m_name), m_value->to_string()); case Type::MinValue: - return String::formatted("min-{}:{}", m_name, m_value->to_string()); + return String::formatted("min-{}:{}", serialize_an_identifier(m_name), m_value->to_string()); case Type::MaxValue: - return String::formatted("max-{}:{}", m_name, m_value->to_string()); + return String::formatted("max-{}:{}", serialize_an_identifier(m_name), m_value->to_string()); + case Type::Range: + if (!m_range->right_comparison.has_value()) + return String::formatted("{} {} {}", m_range->left_value.to_string(), comparison_string(m_range->left_comparison), serialize_an_identifier(m_name)); + + return String::formatted("{} {} {} {} {}", m_range->left_value.to_string(), comparison_string(m_range->left_comparison), serialize_an_identifier(m_name), comparison_string(*m_range->right_comparison), m_range->right_value->to_string()); } VERIFY_NOT_REACHED(); @@ -93,47 +114,79 @@ bool MediaFeature::evaluate(DOM::Window const& window) const return false; case Type::ExactValue: - return queried_value.equals(*m_value); + return compare(*m_value, Comparison::Equal, queried_value); case Type::MinValue: - if (!m_value->is_same_type(queried_value)) - return false; - - if (m_value->is_number()) - return queried_value.number() >= m_value->number(); - - if (m_value->is_length()) { - auto& queried_length = queried_value.length(); - auto& value_length = m_value->length(); - // FIXME: Handle relative lengths. https://www.w3.org/TR/mediaqueries-4/#ref-for-relative-length - if (!value_length.is_absolute()) { - dbgln("Media feature was given a non-absolute length! {}", value_length.to_string()); - return false; - } - return queried_length.absolute_length_to_px() >= value_length.absolute_length_to_px(); - } - - return false; + return compare(queried_value, Comparison::GreaterThanOrEqual, *m_value); case Type::MaxValue: - if (!m_value->is_same_type(queried_value)) + return compare(queried_value, Comparison::LessThanOrEqual, *m_value); + + case Type::Range: + if (!compare(m_range->left_value, m_range->left_comparison, queried_value)) return false; - if (m_value->is_number()) - return queried_value.number() <= m_value->number(); - - if (m_value->is_length()) { - auto& queried_length = queried_value.length(); - auto& value_length = m_value->length(); - // FIXME: Handle relative lengths. https://www.w3.org/TR/mediaqueries-4/#ref-for-relative-length - if (!value_length.is_absolute()) { - dbgln("Media feature was given a non-absolute length! {}", value_length.to_string()); + if (m_range->right_comparison.has_value()) + if (!compare(queried_value, *m_range->right_comparison, *m_range->right_value)) return false; - } - return queried_length.absolute_length_to_px() <= value_length.absolute_length_to_px(); + + return true; + } + + VERIFY_NOT_REACHED(); +} + +bool MediaFeature::compare(MediaFeatureValue left, Comparison comparison, MediaFeatureValue right) +{ + if (!left.is_same_type(right)) + return false; + + if (left.is_ident()) { + if (comparison == Comparison::Equal) + return left.ident().equals_ignoring_case(right.ident()); + return false; + } + + if (left.is_number()) { + switch (comparison) { + case Comparison::Equal: + return left.number() == right.number(); + case Comparison::LessThan: + return left.number() < right.number(); + case Comparison::LessThanOrEqual: + return left.number() <= right.number(); + case Comparison::GreaterThan: + return left.number() > right.number(); + case Comparison::GreaterThanOrEqual: + return left.number() >= right.number(); + } + VERIFY_NOT_REACHED(); + } + + if (left.is_length()) { + // FIXME: Handle relative lengths. https://www.w3.org/TR/mediaqueries-4/#ref-for-relative-length + if (!left.length().is_absolute() || !right.length().is_absolute()) { + dbgln("TODO: Support relative lengths in media queries!"); + return false; } - return false; + auto left_px = left.length().absolute_length_to_px(); + auto right_px = right.length().absolute_length_to_px(); + + switch (comparison) { + case Comparison::Equal: + return left_px == right_px; + case Comparison::LessThan: + return left_px < right_px; + case Comparison::LessThanOrEqual: + return left_px <= right_px; + case Comparison::GreaterThan: + return left_px > right_px; + case Comparison::GreaterThanOrEqual: + return left_px >= right_px; + } + + VERIFY_NOT_REACHED(); } VERIFY_NOT_REACHED(); @@ -333,4 +386,58 @@ String serialize_a_media_query_list(NonnullRefPtrVector const& media return builder.to_string(); } +bool is_media_feature_name(StringView name) +{ + // MEDIAQUERIES-4 - https://www.w3.org/TR/mediaqueries-4/#media-descriptor-table + if (name.equals_ignoring_case("any-hover"sv)) + return true; + if (name.equals_ignoring_case("any-pointer"sv)) + return true; + if (name.equals_ignoring_case("aspect-ratio"sv)) + return true; + if (name.equals_ignoring_case("color"sv)) + return true; + if (name.equals_ignoring_case("color-gamut"sv)) + return true; + if (name.equals_ignoring_case("color-index"sv)) + return true; + if (name.equals_ignoring_case("device-aspect-ratio"sv)) + return true; + if (name.equals_ignoring_case("device-height"sv)) + return true; + if (name.equals_ignoring_case("device-width"sv)) + return true; + if (name.equals_ignoring_case("grid"sv)) + return true; + if (name.equals_ignoring_case("height"sv)) + return true; + if (name.equals_ignoring_case("hover"sv)) + return true; + if (name.equals_ignoring_case("monochrome"sv)) + return true; + if (name.equals_ignoring_case("orientation"sv)) + return true; + if (name.equals_ignoring_case("overflow-block"sv)) + return true; + if (name.equals_ignoring_case("overflow-inline"sv)) + return true; + if (name.equals_ignoring_case("pointer"sv)) + return true; + if (name.equals_ignoring_case("resolution"sv)) + return true; + if (name.equals_ignoring_case("scan"sv)) + return true; + if (name.equals_ignoring_case("update"sv)) + return true; + if (name.equals_ignoring_case("width"sv)) + return true; + + // MEDIAQUERIES-5 - https://www.w3.org/TR/mediaqueries-5/#media-descriptor-table + if (name.equals_ignoring_case("prefers-color-scheme"sv)) + return true; + // FIXME: Add other level 5 feature names + + return false; +} + } diff --git a/Userland/Libraries/LibWeb/CSS/MediaQuery.h b/Userland/Libraries/LibWeb/CSS/MediaQuery.h index ee1abbc55b8..9940f4a6f9d 100644 --- a/Userland/Libraries/LibWeb/CSS/MediaQuery.h +++ b/Userland/Libraries/LibWeb/CSS/MediaQuery.h @@ -72,6 +72,14 @@ private: // https://www.w3.org/TR/mediaqueries-4/#mq-features class MediaFeature { public: + enum class Comparison { + Equal, + LessThan, + LessThanOrEqual, + GreaterThan, + GreaterThanOrEqual, + }; + // Corresponds to `` grammar static MediaFeature boolean(String const& name) { @@ -88,16 +96,40 @@ public: return MediaFeature(Type::ExactValue, move(name), move(value)); } + // Corresponds to `` grammar, with a single comparison + static MediaFeature half_range(MediaFeatureValue value, Comparison comparison, String const& name) + { + MediaFeature feature { Type::Range, name }; + feature.m_range = Range { + .left_value = value, + .left_comparison = comparison, + }; + return feature; + } + + // Corresponds to `` grammar, with two comparisons + static MediaFeature range(MediaFeatureValue left_value, Comparison left_comparison, String const& name, Comparison right_comparison, MediaFeatureValue right_value) + { + MediaFeature feature { Type::Range, name }; + feature.m_range = Range { + .left_value = left_value, + .left_comparison = left_comparison, + .right_comparison = right_comparison, + .right_value = right_value, + }; + return feature; + } + bool evaluate(DOM::Window const&) const; String to_string() const; private: - // FIXME: Implement range syntax: https://www.w3.org/TR/mediaqueries-4/#mq-ranges enum class Type { IsTrue, ExactValue, MinValue, MaxValue, + Range, }; MediaFeature(Type type, FlyString name, Optional value = {}) @@ -107,9 +139,19 @@ private: { } + static bool compare(MediaFeatureValue left, Comparison comparison, MediaFeatureValue right); + + struct Range { + MediaFeatureValue left_value; + Comparison left_comparison; + Optional right_comparison {}; + Optional right_value {}; + }; + Type m_type; FlyString m_name; Optional m_value {}; + Optional m_range {}; }; // https://www.w3.org/TR/mediaqueries-4/#media-conditions @@ -189,6 +231,8 @@ private: String serialize_a_media_query_list(NonnullRefPtrVector const&); +bool is_media_feature_name(StringView name); + } namespace AK { diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp index 7eee3242c56..bff0e7ea125 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -869,9 +869,23 @@ Optional Parser::parse_media_feature(TokenStream = ` - auto parse_mf_name = [](StyleComponentValueRule const& token) -> Optional { - if (token.is(Token::Type::Ident)) - return token.token().ident(); + auto parse_mf_name = [](auto& tokens, bool allow_min_max_prefix) -> Optional { + auto& token = tokens.peek_token(); + if (token.is(Token::Type::Ident)) { + auto name = token.token().ident(); + if (is_media_feature_name(name)) { + tokens.next_token(); + return name; + } + + if (allow_min_max_prefix && (name.starts_with("min-", CaseSensitivity::CaseInsensitive) || name.starts_with("max-", CaseSensitivity::CaseInsensitive))) { + auto adjusted_name = name.substring_view(4); + if (is_media_feature_name(adjusted_name)) { + tokens.next_token(); + return name; + } + } + } return {}; }; @@ -880,7 +894,7 @@ Optional Parser::parse_media_feature(TokenStream Parser::parse_media_feature(TokenStream Parser::parse_media_feature(TokenStream = '<' '='? + // = '>' '='? + // = '=' + // = | | ` + auto parse_comparison = [](auto& tokens) -> Optional { + auto position = tokens.position(); + tokens.skip_whitespace(); + + auto& first = tokens.next_token(); + if (first.is(Token::Type::Delim)) { + auto first_delim = first.token().delim(); + if (first_delim == "="sv) + return MediaFeature::Comparison::Equal; + if (first_delim == "<"sv) { + auto& second = tokens.peek_token(); + if (second.is(Token::Type::Delim) && second.token().delim() == "="sv) { + tokens.next_token(); + return MediaFeature::Comparison::LessThanOrEqual; + } + return MediaFeature::Comparison::LessThan; + } + if (first_delim == ">"sv) { + auto& second = tokens.peek_token(); + if (second.is(Token::Type::Delim) && second.token().delim() == "="sv) { + tokens.next_token(); + return MediaFeature::Comparison::GreaterThanOrEqual; + } + return MediaFeature::Comparison::GreaterThan; + } + } + + tokens.rewind_to_position(position); + 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 position = tokens.position(); + 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()) { + 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(tokens); maybe_value.has_value()) { + tokens.skip_whitespace(); + if (!tokens.has_next_token() && !maybe_value->is_ident()) + return MediaFeature::half_range(maybe_value.release_value(), flip(maybe_comparison.release_value()), maybe_name.release_value()); + } + } + } + + // ` + // | + // | ` + if (auto maybe_left_value = parse_media_feature_value(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(); + if (auto maybe_name = parse_mf_name(tokens, false); maybe_name.has_value()) { + tokens.skip_whitespace(); + + if (!tokens.has_next_token()) + return MediaFeature::half_range(maybe_left_value.release_value(), maybe_left_comparison.release_value(), maybe_name.release_value()); + + 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(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()) { + return MediaFeature::range(maybe_left_value.release_value(), left_comparison, maybe_name.release_value(), right_comparison, maybe_right_value.release_value()); + } + } + } + } + } + } + + tokens.rewind_to_position(position); + 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(); - // FIXME: Implement range syntax + if (auto maybe_mf_range = parse_mf_range(tokens); maybe_mf_range.has_value()) + return maybe_mf_range.release_value(); tokens.rewind_to_position(position); return {};