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 <=.
+ // - 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()
+ && comparisons_match(left_comparison, right_comparison)
+ && left_comparison != MediaFeature::Comparison::Equal
+ && !maybe_left_value->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 {};