summaryrefslogtreecommitdiffstats
path: root/servo/components/style/queries
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--servo/components/style/queries/condition.rs366
-rw-r--r--servo/components/style/queries/feature.rs198
-rw-r--r--servo/components/style/queries/feature_expression.rs764
-rw-r--r--servo/components/style/queries/mod.rs19
-rw-r--r--servo/components/style/queries/values.rs36
5 files changed, 1383 insertions, 0 deletions
diff --git a/servo/components/style/queries/condition.rs b/servo/components/style/queries/condition.rs
new file mode 100644
index 0000000000..e17e6abd2e
--- /dev/null
+++ b/servo/components/style/queries/condition.rs
@@ -0,0 +1,366 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! A query condition:
+//!
+//! https://drafts.csswg.org/mediaqueries-4/#typedef-media-condition
+//! https://drafts.csswg.org/css-contain-3/#typedef-container-condition
+
+use super::{FeatureFlags, FeatureType, QueryFeatureExpression};
+use crate::values::computed;
+use crate::{error_reporting::ContextualParseError, parser::ParserContext};
+use cssparser::{Parser, Token};
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// A binary `and` or `or` operator.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
+#[allow(missing_docs)]
+pub enum Operator {
+ And,
+ Or,
+}
+
+/// Whether to allow an `or` condition or not during parsing.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss)]
+enum AllowOr {
+ Yes,
+ No,
+}
+
+/// https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss)]
+pub enum KleeneValue {
+ /// False
+ False = 0,
+ /// True
+ True = 1,
+ /// Either true or false, but we’re not sure which yet.
+ Unknown,
+}
+
+impl From<bool> for KleeneValue {
+ fn from(b: bool) -> Self {
+ if b {
+ Self::True
+ } else {
+ Self::False
+ }
+ }
+}
+
+impl KleeneValue {
+ /// Turns this Kleene value to a bool, taking the unknown value as an
+ /// argument.
+ pub fn to_bool(self, unknown: bool) -> bool {
+ match self {
+ Self::True => true,
+ Self::False => false,
+ Self::Unknown => unknown,
+ }
+ }
+}
+
+impl std::ops::Not for KleeneValue {
+ type Output = Self;
+
+ fn not(self) -> Self {
+ match self {
+ Self::True => Self::False,
+ Self::False => Self::True,
+ Self::Unknown => Self::Unknown,
+ }
+ }
+}
+
+// Implements the logical and operation.
+impl std::ops::BitAnd for KleeneValue {
+ type Output = Self;
+
+ fn bitand(self, other: Self) -> Self {
+ if self == Self::False || other == Self::False {
+ return Self::False;
+ }
+ if self == Self::Unknown || other == Self::Unknown {
+ return Self::Unknown;
+ }
+ Self::True
+ }
+}
+
+// Implements the logical or operation.
+impl std::ops::BitOr for KleeneValue {
+ type Output = Self;
+
+ fn bitor(self, other: Self) -> Self {
+ if self == Self::True || other == Self::True {
+ return Self::True;
+ }
+ if self == Self::Unknown || other == Self::Unknown {
+ return Self::Unknown;
+ }
+ Self::False
+ }
+}
+
+impl std::ops::BitOrAssign for KleeneValue {
+ fn bitor_assign(&mut self, other: Self) {
+ *self = *self | other;
+ }
+}
+
+impl std::ops::BitAndAssign for KleeneValue {
+ fn bitand_assign(&mut self, other: Self) {
+ *self = *self & other;
+ }
+}
+
+/// Represents a condition.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub enum QueryCondition {
+ /// A simple feature expression, implicitly parenthesized.
+ Feature(QueryFeatureExpression),
+ /// A negation of a condition.
+ Not(Box<QueryCondition>),
+ /// A set of joint operations.
+ Operation(Box<[QueryCondition]>, Operator),
+ /// A condition wrapped in parenthesis.
+ InParens(Box<QueryCondition>),
+ /// [ <function-token> <any-value>? ) ] | [ ( <any-value>? ) ]
+ GeneralEnclosed(String),
+}
+
+impl ToCss for QueryCondition {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match *self {
+ // NOTE(emilio): QueryFeatureExpression already includes the
+ // parenthesis.
+ QueryCondition::Feature(ref f) => f.to_css(dest),
+ QueryCondition::Not(ref c) => {
+ dest.write_str("not ")?;
+ c.to_css(dest)
+ },
+ QueryCondition::InParens(ref c) => {
+ dest.write_char('(')?;
+ c.to_css(dest)?;
+ dest.write_char(')')
+ },
+ QueryCondition::Operation(ref list, op) => {
+ let mut iter = list.iter();
+ iter.next().unwrap().to_css(dest)?;
+ for item in iter {
+ dest.write_char(' ')?;
+ op.to_css(dest)?;
+ dest.write_char(' ')?;
+ item.to_css(dest)?;
+ }
+ Ok(())
+ },
+ QueryCondition::GeneralEnclosed(ref s) => dest.write_str(&s),
+ }
+ }
+}
+
+/// <https://drafts.csswg.org/css-syntax-3/#typedef-any-value>
+fn consume_any_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> {
+ input.expect_no_error_token().map_err(Into::into)
+}
+
+impl QueryCondition {
+ /// Parse a single condition.
+ pub fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, feature_type, AllowOr::Yes)
+ }
+
+ fn visit<F>(&self, visitor: &mut F)
+ where
+ F: FnMut(&Self),
+ {
+ visitor(self);
+ match *self {
+ Self::Feature(..) => {},
+ Self::GeneralEnclosed(..) => {},
+ Self::Not(ref cond) => cond.visit(visitor),
+ Self::Operation(ref conds, _op) => {
+ for cond in conds.iter() {
+ cond.visit(visitor);
+ }
+ },
+ Self::InParens(ref cond) => cond.visit(visitor),
+ }
+ }
+
+ /// Returns the union of all flags in the expression. This is useful for
+ /// container queries.
+ pub fn cumulative_flags(&self) -> FeatureFlags {
+ let mut result = FeatureFlags::empty();
+ self.visit(&mut |condition| {
+ if let Self::Feature(ref f) = condition {
+ result.insert(f.feature_flags())
+ }
+ });
+ result
+ }
+
+ /// Parse a single condition, disallowing `or` expressions.
+ ///
+ /// To be used from the legacy query syntax.
+ pub fn parse_disallow_or<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, feature_type, AllowOr::No)
+ }
+
+ /// https://drafts.csswg.org/mediaqueries-5/#typedef-media-condition or
+ /// https://drafts.csswg.org/mediaqueries-5/#typedef-media-condition-without-or
+ /// (depending on `allow_or`).
+ fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ allow_or: AllowOr,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ if input.try_parse(|i| i.expect_ident_matching("not")).is_ok() {
+ let inner_condition = Self::parse_in_parens(context, input, feature_type)?;
+ return Ok(QueryCondition::Not(Box::new(inner_condition)));
+ }
+
+ let first_condition = Self::parse_in_parens(context, input, feature_type)?;
+ let operator = match input.try_parse(Operator::parse) {
+ Ok(op) => op,
+ Err(..) => return Ok(first_condition),
+ };
+
+ if allow_or == AllowOr::No && operator == Operator::Or {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ let mut conditions = vec![];
+ conditions.push(first_condition);
+ conditions.push(Self::parse_in_parens(context, input, feature_type)?);
+
+ let delim = match operator {
+ Operator::And => "and",
+ Operator::Or => "or",
+ };
+
+ loop {
+ if input.try_parse(|i| i.expect_ident_matching(delim)).is_err() {
+ return Ok(QueryCondition::Operation(
+ conditions.into_boxed_slice(),
+ operator,
+ ));
+ }
+
+ conditions.push(Self::parse_in_parens(context, input, feature_type)?);
+ }
+ }
+
+ fn parse_in_parenthesis_block<'i>(
+ context: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ feature_type: FeatureType,
+ ) -> Result<Self, ParseError<'i>> {
+ // Base case. Make sure to preserve this error as it's more generally
+ // relevant.
+ let feature_error = match input.try_parse(|input| {
+ QueryFeatureExpression::parse_in_parenthesis_block(context, input, feature_type)
+ }) {
+ Ok(expr) => return Ok(Self::Feature(expr)),
+ Err(e) => e,
+ };
+ if let Ok(inner) = Self::parse(context, input, feature_type) {
+ return Ok(Self::InParens(Box::new(inner)));
+ }
+ Err(feature_error)
+ }
+
+ /// Parse a condition in parentheses, or `<general-enclosed>`.
+ ///
+ /// https://drafts.csswg.org/mediaqueries/#typedef-media-in-parens
+ pub fn parse_in_parens<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ ) -> Result<Self, ParseError<'i>> {
+ input.skip_whitespace();
+ let start = input.position();
+ let start_location = input.current_source_location();
+ match *input.next()? {
+ Token::ParenthesisBlock => {
+ let nested = input.try_parse(|input| {
+ input.parse_nested_block(|input| {
+ Self::parse_in_parenthesis_block(context, input, feature_type)
+ })
+ });
+ match nested {
+ Ok(nested) => return Ok(nested),
+ Err(e) => {
+ // We're about to swallow the error in a `<general-enclosed>`
+ // condition, so report it while we can.
+ let loc = e.location;
+ let error =
+ ContextualParseError::InvalidMediaRule(input.slice_from(start), e);
+ context.log_css_error(loc, error);
+ },
+ }
+ },
+ Token::Function(..) => {
+ // TODO: handle `style()` queries, etc.
+ },
+ ref t => return Err(start_location.new_unexpected_token_error(t.clone())),
+ }
+ input.parse_nested_block(consume_any_value)?;
+ Ok(Self::GeneralEnclosed(input.slice_from(start).to_owned()))
+ }
+
+ /// Whether this condition matches the device and quirks mode.
+ /// https://drafts.csswg.org/mediaqueries/#evaluating
+ /// https://drafts.csswg.org/mediaqueries/#typedef-general-enclosed
+ /// Kleene 3-valued logic is adopted here due to the introduction of
+ /// <general-enclosed>.
+ pub fn matches(&self, context: &computed::Context) -> KleeneValue {
+ match *self {
+ QueryCondition::Feature(ref f) => f.matches(context),
+ QueryCondition::GeneralEnclosed(_) => KleeneValue::Unknown,
+ QueryCondition::InParens(ref c) => c.matches(context),
+ QueryCondition::Not(ref c) => !c.matches(context),
+ QueryCondition::Operation(ref conditions, op) => {
+ debug_assert!(!conditions.is_empty(), "We never create an empty op");
+ match op {
+ Operator::And => {
+ let mut result = KleeneValue::True;
+ for c in conditions.iter() {
+ result &= c.matches(context);
+ if result == KleeneValue::False {
+ break;
+ }
+ }
+ result
+ },
+ Operator::Or => {
+ let mut result = KleeneValue::False;
+ for c in conditions.iter() {
+ result |= c.matches(context);
+ if result == KleeneValue::True {
+ break;
+ }
+ }
+ result
+ },
+ }
+ },
+ }
+ }
+}
diff --git a/servo/components/style/queries/feature.rs b/servo/components/style/queries/feature.rs
new file mode 100644
index 0000000000..83ff7e7522
--- /dev/null
+++ b/servo/components/style/queries/feature.rs
@@ -0,0 +1,198 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Query features.
+
+use super::condition::KleeneValue;
+use crate::parser::ParserContext;
+use crate::values::computed::{self, CSSPixelLength, Ratio, Resolution};
+use crate::values::AtomString;
+use crate::Atom;
+use cssparser::Parser;
+use std::fmt;
+use style_traits::ParseError;
+
+/// A generic discriminant for an enum value.
+pub type KeywordDiscriminant = u8;
+
+type QueryFeatureGetter<T> = fn(device: &computed::Context) -> T;
+
+/// Serializes a given discriminant.
+///
+/// FIXME(emilio): we could prevent this allocation if the ToCss code would
+/// generate a method for keywords to get the static string or something.
+pub type KeywordSerializer = fn(KeywordDiscriminant) -> String;
+
+/// Parses a given identifier.
+pub type KeywordParser = for<'a, 'i, 't> fn(
+ context: &'a ParserContext,
+ input: &'a mut Parser<'i, 't>,
+) -> Result<KeywordDiscriminant, ParseError<'i>>;
+
+/// An evaluator for a given feature.
+///
+/// This determines the kind of values that get parsed, too.
+#[allow(missing_docs)]
+pub enum Evaluator {
+ Length(QueryFeatureGetter<CSSPixelLength>),
+ OptionalLength(QueryFeatureGetter<Option<CSSPixelLength>>),
+ Integer(QueryFeatureGetter<i32>),
+ Float(QueryFeatureGetter<f32>),
+ BoolInteger(QueryFeatureGetter<bool>),
+ /// A non-negative number ratio, such as the one from device-pixel-ratio.
+ NumberRatio(QueryFeatureGetter<Ratio>),
+ OptionalNumberRatio(QueryFeatureGetter<Option<Ratio>>),
+ /// A resolution.
+ Resolution(QueryFeatureGetter<Resolution>),
+ String(fn(&computed::Context, Option<&AtomString>) -> KleeneValue),
+ /// A keyword value.
+ Enumerated {
+ /// The parser to get a discriminant given a string.
+ parser: KeywordParser,
+ /// The serializer to get a string from a discriminant.
+ ///
+ /// This is guaranteed to be called with a keyword that `parser` has
+ /// produced.
+ serializer: KeywordSerializer,
+ /// The evaluator itself. This is guaranteed to be called with a
+ /// keyword that `parser` has produced.
+ evaluator: fn(&computed::Context, Option<KeywordDiscriminant>) -> KleeneValue,
+ },
+}
+
+/// A simple helper macro to create a keyword evaluator.
+///
+/// This assumes that keyword feature expressions don't accept ranges, and
+/// asserts if that's not true. As of today there's nothing like that (does that
+/// even make sense?).
+macro_rules! keyword_evaluator {
+ ($actual_evaluator:ident, $keyword_type:ty) => {{
+ fn __parse<'i, 't>(
+ context: &$crate::parser::ParserContext,
+ input: &mut $crate::cssparser::Parser<'i, 't>,
+ ) -> Result<$crate::queries::feature::KeywordDiscriminant, ::style_traits::ParseError<'i>>
+ {
+ let kw = <$keyword_type as $crate::parser::Parse>::parse(context, input)?;
+ Ok(kw as $crate::queries::feature::KeywordDiscriminant)
+ }
+
+ fn __serialize(kw: $crate::queries::feature::KeywordDiscriminant) -> String {
+ // This unwrap is ok because the only discriminants that get
+ // back to us is the ones that `parse` produces.
+ let value: $keyword_type = ::num_traits::cast::FromPrimitive::from_u8(kw).unwrap();
+ <$keyword_type as ::style_traits::ToCss>::to_css_string(&value)
+ }
+
+ fn __evaluate(
+ context: &$crate::values::computed::Context,
+ value: Option<$crate::queries::feature::KeywordDiscriminant>,
+ ) -> $crate::queries::condition::KleeneValue {
+ // This unwrap is ok because the only discriminants that get
+ // back to us is the ones that `parse` produces.
+ let value: Option<$keyword_type> =
+ value.map(|kw| ::num_traits::cast::FromPrimitive::from_u8(kw).unwrap());
+ $crate::queries::condition::KleeneValue::from($actual_evaluator(context, value))
+ }
+
+ $crate::queries::feature::Evaluator::Enumerated {
+ parser: __parse,
+ serializer: __serialize,
+ evaluator: __evaluate,
+ }
+ }};
+}
+
+/// Different flags or toggles that change how a expression is parsed or
+/// evaluated.
+#[derive(Clone, Copy, Debug, ToShmem)]
+pub struct FeatureFlags(u8);
+bitflags! {
+ impl FeatureFlags : u8 {
+ /// The feature should only be parsed in chrome and ua sheets.
+ const CHROME_AND_UA_ONLY = 1 << 0;
+ /// The feature requires a -webkit- prefix.
+ const WEBKIT_PREFIX = 1 << 1;
+ /// The feature requires the inline-axis containment.
+ const CONTAINER_REQUIRES_INLINE_AXIS = 1 << 2;
+ /// The feature requires the block-axis containment.
+ const CONTAINER_REQUIRES_BLOCK_AXIS = 1 << 3;
+ /// The feature requires containment in the physical width axis.
+ const CONTAINER_REQUIRES_WIDTH_AXIS = 1 << 4;
+ /// The feature requires containment in the physical height axis.
+ const CONTAINER_REQUIRES_HEIGHT_AXIS = 1 << 5;
+ /// The feature evaluation depends on the viewport size.
+ const VIEWPORT_DEPENDENT = 1 << 6;
+ }
+}
+
+impl FeatureFlags {
+ /// Returns parsing requirement flags.
+ pub fn parsing_requirements(self) -> Self {
+ self.intersection(Self::CHROME_AND_UA_ONLY | Self::WEBKIT_PREFIX)
+ }
+
+ /// Returns all the container axis flags.
+ pub fn all_container_axes() -> Self {
+ Self::CONTAINER_REQUIRES_INLINE_AXIS |
+ Self::CONTAINER_REQUIRES_BLOCK_AXIS |
+ Self::CONTAINER_REQUIRES_WIDTH_AXIS |
+ Self::CONTAINER_REQUIRES_HEIGHT_AXIS
+ }
+
+ /// Returns our subset of container axis flags.
+ pub fn container_axes(self) -> Self {
+ self.intersection(Self::all_container_axes())
+ }
+}
+
+/// Whether a feature allows ranges or not.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[allow(missing_docs)]
+pub enum AllowsRanges {
+ Yes,
+ No,
+}
+
+/// A description of a feature.
+pub struct QueryFeatureDescription {
+ /// The feature name, in ascii lowercase.
+ pub name: Atom,
+ /// Whether min- / max- prefixes are allowed or not.
+ pub allows_ranges: AllowsRanges,
+ /// The evaluator, which we also use to determine which kind of value to
+ /// parse.
+ pub evaluator: Evaluator,
+ /// Different feature-specific flags.
+ pub flags: FeatureFlags,
+}
+
+impl QueryFeatureDescription {
+ /// Whether this feature allows ranges.
+ #[inline]
+ pub fn allows_ranges(&self) -> bool {
+ self.allows_ranges == AllowsRanges::Yes
+ }
+}
+
+/// A simple helper to construct a `QueryFeatureDescription`.
+macro_rules! feature {
+ ($name:expr, $allows_ranges:expr, $evaluator:expr, $flags:expr,) => {
+ $crate::queries::feature::QueryFeatureDescription {
+ name: $name,
+ allows_ranges: $allows_ranges,
+ evaluator: $evaluator,
+ flags: $flags,
+ }
+ };
+}
+
+impl fmt::Debug for QueryFeatureDescription {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("QueryFeatureDescription")
+ .field("name", &self.name)
+ .field("allows_ranges", &self.allows_ranges)
+ .field("flags", &self.flags)
+ .finish()
+ }
+}
diff --git a/servo/components/style/queries/feature_expression.rs b/servo/components/style/queries/feature_expression.rs
new file mode 100644
index 0000000000..c0171c2058
--- /dev/null
+++ b/servo/components/style/queries/feature_expression.rs
@@ -0,0 +1,764 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Parsing for query feature expressions, like `(foo: bar)` or
+//! `(width >= 400px)`.
+
+use super::feature::{Evaluator, QueryFeatureDescription};
+use super::feature::{FeatureFlags, KeywordDiscriminant};
+use crate::parser::{Parse, ParserContext};
+use crate::queries::condition::KleeneValue;
+use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase};
+use crate::values::computed::{self, Ratio, ToComputedValue};
+use crate::values::specified::{Integer, Length, Number, Resolution};
+use crate::values::{AtomString, CSSFloat};
+use crate::{Atom, Zero};
+use cssparser::{Parser, Token};
+use std::cmp::{Ordering, PartialOrd};
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// Whether we're parsing a media or container query feature.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+pub enum FeatureType {
+ /// We're parsing a media feature.
+ Media,
+ /// We're parsing a container feature.
+ Container,
+}
+
+impl FeatureType {
+ fn features(&self) -> &'static [QueryFeatureDescription] {
+ #[cfg(feature = "gecko")]
+ use crate::gecko::media_features::MEDIA_FEATURES;
+ #[cfg(feature = "servo")]
+ use crate::servo::media_queries::MEDIA_FEATURES;
+
+ use crate::stylesheets::container_rule::CONTAINER_FEATURES;
+
+ match *self {
+ FeatureType::Media => &MEDIA_FEATURES,
+ FeatureType::Container => &CONTAINER_FEATURES,
+ }
+ }
+
+ fn find_feature(&self, name: &Atom) -> Option<(usize, &'static QueryFeatureDescription)> {
+ self.features()
+ .iter()
+ .enumerate()
+ .find(|(_, f)| f.name == *name)
+ }
+}
+
+/// The kind of matching that should be performed on a feature value.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+enum LegacyRange {
+ /// At least the specified value.
+ Min,
+ /// At most the specified value.
+ Max,
+}
+
+/// The operator that was specified in this feature.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+enum Operator {
+ /// =
+ Equal,
+ /// >
+ GreaterThan,
+ /// >=
+ GreaterThanEqual,
+ /// <
+ LessThan,
+ /// <=
+ LessThanEqual,
+}
+
+impl ToCss for Operator {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ dest.write_str(match *self {
+ Self::Equal => "=",
+ Self::LessThan => "<",
+ Self::LessThanEqual => "<=",
+ Self::GreaterThan => ">",
+ Self::GreaterThanEqual => ">=",
+ })
+ }
+}
+
+impl Operator {
+ fn is_compatible_with(self, right_op: Self) -> bool {
+ // Some operators are not compatible with each other in multi-range
+ // context.
+ match self {
+ Self::Equal => false,
+ Self::GreaterThan | Self::GreaterThanEqual => {
+ matches!(right_op, Self::GreaterThan | Self::GreaterThanEqual)
+ },
+ Self::LessThan | Self::LessThanEqual => {
+ matches!(right_op, Self::LessThan | Self::LessThanEqual)
+ },
+ }
+ }
+
+ fn evaluate(&self, cmp: Ordering) -> bool {
+ match *self {
+ Self::Equal => cmp == Ordering::Equal,
+ Self::GreaterThan => cmp == Ordering::Greater,
+ Self::GreaterThanEqual => cmp == Ordering::Equal || cmp == Ordering::Greater,
+ Self::LessThan => cmp == Ordering::Less,
+ Self::LessThanEqual => cmp == Ordering::Equal || cmp == Ordering::Less,
+ }
+ }
+
+ fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let operator = match *input.next()? {
+ Token::Delim('=') => return Ok(Operator::Equal),
+ Token::Delim('>') => Operator::GreaterThan,
+ Token::Delim('<') => Operator::LessThan,
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ };
+
+ // https://drafts.csswg.org/mediaqueries-4/#mq-syntax:
+ //
+ // No whitespace is allowed between the “<” or “>”
+ // <delim-token>s and the following “=” <delim-token>, if it’s
+ // present.
+ //
+ // TODO(emilio): Maybe we should ignore comments as well?
+ // https://github.com/w3c/csswg-drafts/issues/6248
+ let parsed_equal = input
+ .try_parse(|i| {
+ let t = i.next_including_whitespace().map_err(|_| ())?;
+ if !matches!(t, Token::Delim('=')) {
+ return Err(());
+ }
+ Ok(())
+ })
+ .is_ok();
+
+ if !parsed_equal {
+ return Ok(operator);
+ }
+
+ Ok(match operator {
+ Operator::GreaterThan => Operator::GreaterThanEqual,
+ Operator::LessThan => Operator::LessThanEqual,
+ _ => unreachable!(),
+ })
+ }
+}
+
+#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
+enum QueryFeatureExpressionKind {
+ /// Just the media feature name.
+ Empty,
+
+ /// A single value.
+ Single(QueryExpressionValue),
+
+ /// Legacy range syntax (min-*: value) or so.
+ LegacyRange(LegacyRange, QueryExpressionValue),
+
+ /// Modern range context syntax:
+ /// https://drafts.csswg.org/mediaqueries-5/#mq-range-context
+ Range {
+ left: Option<(Operator, QueryExpressionValue)>,
+ right: Option<(Operator, QueryExpressionValue)>,
+ },
+}
+
+impl QueryFeatureExpressionKind {
+ /// Evaluate a given range given an optional query value and a value from
+ /// the browser.
+ fn evaluate<T>(
+ &self,
+ context_value: T,
+ mut compute: impl FnMut(&QueryExpressionValue) -> T,
+ ) -> bool
+ where
+ T: PartialOrd + Zero,
+ {
+ match *self {
+ Self::Empty => return !context_value.is_zero(),
+ Self::Single(ref value) => {
+ let value = compute(value);
+ let cmp = match context_value.partial_cmp(&value) {
+ Some(c) => c,
+ None => return false,
+ };
+ cmp == Ordering::Equal
+ },
+ Self::LegacyRange(ref range, ref value) => {
+ let value = compute(value);
+ let cmp = match context_value.partial_cmp(&value) {
+ Some(c) => c,
+ None => return false,
+ };
+ cmp == Ordering::Equal ||
+ match range {
+ LegacyRange::Min => cmp == Ordering::Greater,
+ LegacyRange::Max => cmp == Ordering::Less,
+ }
+ },
+ Self::Range {
+ ref left,
+ ref right,
+ } => {
+ debug_assert!(left.is_some() || right.is_some());
+ if let Some((ref op, ref value)) = left {
+ let value = compute(value);
+ let cmp = match value.partial_cmp(&context_value) {
+ Some(c) => c,
+ None => return false,
+ };
+ if !op.evaluate(cmp) {
+ return false;
+ }
+ }
+ if let Some((ref op, ref value)) = right {
+ let value = compute(value);
+ let cmp = match context_value.partial_cmp(&value) {
+ Some(c) => c,
+ None => return false,
+ };
+ if !op.evaluate(cmp) {
+ return false;
+ }
+ }
+ true
+ },
+ }
+ }
+
+ /// Non-ranged features only need to compare to one value at most.
+ fn non_ranged_value(&self) -> Option<&QueryExpressionValue> {
+ match *self {
+ Self::Empty => None,
+ Self::Single(ref v) => Some(v),
+ Self::LegacyRange(..) | Self::Range { .. } => {
+ debug_assert!(false, "Unexpected ranged value in non-ranged feature!");
+ None
+ },
+ }
+ }
+}
+
+/// A feature expression contains a reference to the feature, the value the
+/// query contained, and the range to evaluate.
+#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)]
+pub struct QueryFeatureExpression {
+ feature_type: FeatureType,
+ feature_index: usize,
+ kind: QueryFeatureExpressionKind,
+}
+
+impl ToCss for QueryFeatureExpression {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ dest.write_char('(')?;
+
+ match self.kind {
+ QueryFeatureExpressionKind::Empty => self.write_name(dest)?,
+ QueryFeatureExpressionKind::Single(ref v) |
+ QueryFeatureExpressionKind::LegacyRange(_, ref v) => {
+ self.write_name(dest)?;
+ dest.write_str(": ")?;
+ v.to_css(dest, self)?;
+ },
+ QueryFeatureExpressionKind::Range {
+ ref left,
+ ref right,
+ } => {
+ if let Some((ref op, ref val)) = left {
+ val.to_css(dest, self)?;
+ dest.write_char(' ')?;
+ op.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+ self.write_name(dest)?;
+ if let Some((ref op, ref val)) = right {
+ dest.write_char(' ')?;
+ op.to_css(dest)?;
+ dest.write_char(' ')?;
+ val.to_css(dest, self)?;
+ }
+ },
+ }
+ dest.write_char(')')
+ }
+}
+
+fn consume_operation_or_colon<'i>(
+ input: &mut Parser<'i, '_>,
+) -> Result<Option<Operator>, ParseError<'i>> {
+ if input.try_parse(|input| input.expect_colon()).is_ok() {
+ return Ok(None);
+ }
+ Operator::parse(input).map(|op| Some(op))
+}
+
+#[allow(unused_variables)]
+fn disabled_by_pref(feature: &Atom, context: &ParserContext) -> bool {
+ #[cfg(feature = "gecko")]
+ {
+ if *feature == atom!("forced-colors") {
+ // forced-colors is always enabled in the ua and chrome. On
+ // the web it is hidden behind a preference, which is defaulted
+ // to 'true' as of bug 1659511.
+ return !context.chrome_rules_enabled() &&
+ !static_prefs::pref!("layout.css.forced-colors.enabled");
+ }
+ // prefers-contrast is always enabled in the ua and chrome. On
+ // the web it is hidden behind a preference.
+ if *feature == atom!("prefers-contrast") {
+ return !context.chrome_rules_enabled() &&
+ !static_prefs::pref!("layout.css.prefers-contrast.enabled");
+ }
+
+ // prefers-reduced-transparency is always enabled in the ua and chrome. On
+ // the web it is hidden behind a preference (see Bug 1822176).
+ if *feature == atom!("prefers-reduced-transparency") {
+ return !context.chrome_rules_enabled() &&
+ !static_prefs::pref!("layout.css.prefers-reduced-transparency.enabled");
+ }
+
+ // inverted-colors is always enabled in the ua and chrome. On
+ // the web it is hidden behind a preferenc.
+ if *feature == atom!("inverted-colors") {
+ return !context.chrome_rules_enabled() &&
+ !static_prefs::pref!("layout.css.inverted-colors.enabled");
+ }
+ }
+ false
+}
+
+impl QueryFeatureExpression {
+ fn new(
+ feature_type: FeatureType,
+ feature_index: usize,
+ kind: QueryFeatureExpressionKind,
+ ) -> Self {
+ debug_assert!(feature_index < feature_type.features().len());
+ Self {
+ feature_type,
+ feature_index,
+ kind,
+ }
+ }
+
+ fn write_name<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ let feature = self.feature();
+ if feature.flags.contains(FeatureFlags::WEBKIT_PREFIX) {
+ dest.write_str("-webkit-")?;
+ }
+
+ if let QueryFeatureExpressionKind::LegacyRange(range, _) = self.kind {
+ match range {
+ LegacyRange::Min => dest.write_str("min-")?,
+ LegacyRange::Max => dest.write_str("max-")?,
+ }
+ }
+
+ // NB: CssStringWriter not needed, feature names are under control.
+ write!(dest, "{}", feature.name)?;
+
+ Ok(())
+ }
+
+ fn feature(&self) -> &'static QueryFeatureDescription {
+ &self.feature_type.features()[self.feature_index]
+ }
+
+ /// Returns the feature flags for our feature.
+ pub fn feature_flags(&self) -> FeatureFlags {
+ self.feature().flags
+ }
+
+ /// Parse a feature expression of the form:
+ ///
+ /// ```
+ /// (media-feature: media-value)
+ /// ```
+ pub fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_parenthesis_block()?;
+ input.parse_nested_block(|input| {
+ Self::parse_in_parenthesis_block(context, input, feature_type)
+ })
+ }
+
+ fn parse_feature_name<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ ) -> Result<(usize, Option<LegacyRange>), ParseError<'i>> {
+ let mut flags = FeatureFlags::empty();
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+
+ if context.chrome_rules_enabled() {
+ flags.insert(FeatureFlags::CHROME_AND_UA_ONLY);
+ }
+
+ let mut feature_name = &**ident;
+ if starts_with_ignore_ascii_case(feature_name, "-webkit-") {
+ feature_name = &feature_name[8..];
+ flags.insert(FeatureFlags::WEBKIT_PREFIX);
+ }
+
+ let range = if starts_with_ignore_ascii_case(feature_name, "min-") {
+ feature_name = &feature_name[4..];
+ Some(LegacyRange::Min)
+ } else if starts_with_ignore_ascii_case(feature_name, "max-") {
+ feature_name = &feature_name[4..];
+ Some(LegacyRange::Max)
+ } else {
+ None
+ };
+
+ let atom = Atom::from(string_as_ascii_lowercase(feature_name));
+ let (feature_index, feature) = match feature_type.find_feature(&atom) {
+ Some((i, f)) => (i, f),
+ None => {
+ return Err(location.new_custom_error(
+ StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()),
+ ))
+ },
+ };
+
+ if disabled_by_pref(&feature.name, context) ||
+ !flags.contains(feature.flags.parsing_requirements()) ||
+ (range.is_some() && !feature.allows_ranges())
+ {
+ return Err(location.new_custom_error(
+ StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()),
+ ));
+ }
+
+ Ok((feature_index, range))
+ }
+
+ /// Parses the following range syntax:
+ ///
+ /// (feature-value <operator> feature-name)
+ /// (feature-value <operator> feature-name <operator> feature-value)
+ fn parse_multi_range_syntax<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ ) -> Result<Self, ParseError<'i>> {
+ let start = input.state();
+
+ // To parse the values, we first need to find the feature name. We rely
+ // on feature values for ranged features not being able to be top-level
+ // <ident>s, which holds.
+ let feature_index = loop {
+ // NOTE: parse_feature_name advances the input.
+ if let Ok((index, range)) = Self::parse_feature_name(context, input, feature_type) {
+ if range.is_some() {
+ // Ranged names are not allowed here.
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ break index;
+ }
+ if input.is_exhausted() {
+ return Err(start
+ .source_location()
+ .new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ };
+
+ input.reset(&start);
+
+ let feature = &feature_type.features()[feature_index];
+ let left_val = QueryExpressionValue::parse(feature, context, input)?;
+ let left_op = Operator::parse(input)?;
+
+ {
+ let (parsed_index, _) = Self::parse_feature_name(context, input, feature_type)?;
+ debug_assert_eq!(
+ parsed_index, feature_index,
+ "How did we find a different feature?"
+ );
+ }
+
+ let right_op = input.try_parse(Operator::parse).ok();
+ let right = match right_op {
+ Some(op) => {
+ if !left_op.is_compatible_with(op) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Some((op, QueryExpressionValue::parse(feature, context, input)?))
+ },
+ None => None,
+ };
+ Ok(Self::new(
+ feature_type,
+ feature_index,
+ QueryFeatureExpressionKind::Range {
+ left: Some((left_op, left_val)),
+ right,
+ },
+ ))
+ }
+
+ /// Parse a feature expression where we've already consumed the parenthesis.
+ pub fn parse_in_parenthesis_block<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ feature_type: FeatureType,
+ ) -> Result<Self, ParseError<'i>> {
+ let (feature_index, range) =
+ match input.try_parse(|input| Self::parse_feature_name(context, input, feature_type)) {
+ Ok(v) => v,
+ Err(e) => {
+ if let Ok(expr) = Self::parse_multi_range_syntax(context, input, feature_type) {
+ return Ok(expr);
+ }
+ return Err(e);
+ },
+ };
+ let operator = input.try_parse(consume_operation_or_colon);
+ let operator = match operator {
+ Err(..) => {
+ // If there's no colon, this is a query of the form
+ // '(<feature>)', that is, there's no value specified.
+ //
+ // Gecko doesn't allow ranged expressions without a
+ // value, so just reject them here too.
+ if range.is_some() {
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::RangedExpressionWithNoValue)
+ );
+ }
+
+ return Ok(Self::new(
+ feature_type,
+ feature_index,
+ QueryFeatureExpressionKind::Empty,
+ ));
+ },
+ Ok(operator) => operator,
+ };
+
+ let feature = &feature_type.features()[feature_index];
+
+ let value = QueryExpressionValue::parse(feature, context, input).map_err(|err| {
+ err.location
+ .new_custom_error(StyleParseErrorKind::MediaQueryExpectedFeatureValue)
+ })?;
+
+ let kind = match range {
+ Some(range) => {
+ if operator.is_some() {
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator)
+ );
+ }
+ QueryFeatureExpressionKind::LegacyRange(range, value)
+ },
+ None => match operator {
+ Some(operator) => {
+ if !feature.allows_ranges() {
+ return Err(input
+ .new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator));
+ }
+ QueryFeatureExpressionKind::Range {
+ left: None,
+ right: Some((operator, value)),
+ }
+ },
+ None => QueryFeatureExpressionKind::Single(value),
+ },
+ };
+
+ Ok(Self::new(feature_type, feature_index, kind))
+ }
+
+ /// Returns whether this query evaluates to true for the given device.
+ pub fn matches(&self, context: &computed::Context) -> KleeneValue {
+ macro_rules! expect {
+ ($variant:ident, $v:expr) => {
+ match *$v {
+ QueryExpressionValue::$variant(ref v) => v,
+ _ => unreachable!("Unexpected QueryExpressionValue"),
+ }
+ };
+ }
+
+ KleeneValue::from(match self.feature().evaluator {
+ Evaluator::Length(eval) => {
+ let v = eval(context);
+ self.kind
+ .evaluate(v, |v| expect!(Length, v).to_computed_value(context))
+ },
+ Evaluator::OptionalLength(eval) => {
+ let v = match eval(context) {
+ Some(v) => v,
+ None => return KleeneValue::Unknown,
+ };
+ self.kind
+ .evaluate(v, |v| expect!(Length, v).to_computed_value(context))
+ },
+ Evaluator::Integer(eval) => {
+ let v = eval(context);
+ self.kind.evaluate(v, |v| *expect!(Integer, v))
+ },
+ Evaluator::Float(eval) => {
+ let v = eval(context);
+ self.kind.evaluate(v, |v| *expect!(Float, v))
+ },
+ Evaluator::NumberRatio(eval) => {
+ let ratio = eval(context);
+ // A ratio of 0/0 behaves as the ratio 1/0, so we need to call used_value()
+ // to convert it if necessary.
+ // FIXME: we may need to update here once
+ // https://github.com/w3c/csswg-drafts/issues/4954 got resolved.
+ self.kind
+ .evaluate(ratio, |v| expect!(NumberRatio, v).used_value())
+ },
+ Evaluator::OptionalNumberRatio(eval) => {
+ let ratio = match eval(context) {
+ Some(v) => v,
+ None => return KleeneValue::Unknown,
+ };
+ // See above for subtleties here.
+ self.kind
+ .evaluate(ratio, |v| expect!(NumberRatio, v).used_value())
+ },
+ Evaluator::Resolution(eval) => {
+ let v = eval(context).dppx();
+ self.kind.evaluate(v, |v| {
+ expect!(Resolution, v).to_computed_value(context).dppx()
+ })
+ },
+ Evaluator::Enumerated { evaluator, .. } => {
+ let computed = self
+ .kind
+ .non_ranged_value()
+ .map(|v| *expect!(Enumerated, v));
+ return evaluator(context, computed);
+ },
+ Evaluator::String(evaluator) => {
+ let string = self.kind.non_ranged_value().map(|v| expect!(String, v));
+ return evaluator(context, string);
+ },
+ Evaluator::BoolInteger(eval) => {
+ let computed = self
+ .kind
+ .non_ranged_value()
+ .map(|v| *expect!(BoolInteger, v));
+ let boolean = eval(context);
+ computed.map_or(boolean, |v| v == boolean)
+ },
+ })
+ }
+}
+
+/// A value found or expected in a expression.
+///
+/// FIXME(emilio): How should calc() serialize in the Number / Integer /
+/// BoolInteger / NumberRatio case, as computed or as specified value?
+///
+/// If the first, this would need to store the relevant values.
+///
+/// See: https://github.com/w3c/csswg-drafts/issues/1968
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub enum QueryExpressionValue {
+ /// A length.
+ Length(Length),
+ /// An integer.
+ Integer(i32),
+ /// A floating point value.
+ Float(CSSFloat),
+ /// A boolean value, specified as an integer (i.e., either 0 or 1).
+ BoolInteger(bool),
+ /// A single non-negative number or two non-negative numbers separated by '/',
+ /// with optional whitespace on either side of the '/'.
+ NumberRatio(Ratio),
+ /// A resolution.
+ Resolution(Resolution),
+ /// An enumerated value, defined by the variant keyword table in the
+ /// feature's `mData` member.
+ Enumerated(KeywordDiscriminant),
+ /// An arbitrary ident value.
+ String(AtomString),
+}
+
+impl QueryExpressionValue {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>, for_expr: &QueryFeatureExpression) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match *self {
+ QueryExpressionValue::Length(ref l) => l.to_css(dest),
+ QueryExpressionValue::Integer(v) => v.to_css(dest),
+ QueryExpressionValue::Float(v) => v.to_css(dest),
+ QueryExpressionValue::BoolInteger(v) => dest.write_str(if v { "1" } else { "0" }),
+ QueryExpressionValue::NumberRatio(ratio) => ratio.to_css(dest),
+ QueryExpressionValue::Resolution(ref r) => r.to_css(dest),
+ QueryExpressionValue::Enumerated(value) => match for_expr.feature().evaluator {
+ Evaluator::Enumerated { serializer, .. } => dest.write_str(&*serializer(value)),
+ _ => unreachable!(),
+ },
+ QueryExpressionValue::String(ref s) => s.to_css(dest),
+ }
+ }
+
+ fn parse<'i, 't>(
+ for_feature: &QueryFeatureDescription,
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<QueryExpressionValue, ParseError<'i>> {
+ Ok(match for_feature.evaluator {
+ Evaluator::OptionalLength(..) | Evaluator::Length(..) => {
+ let length = Length::parse(context, input)?;
+ QueryExpressionValue::Length(length)
+ },
+ Evaluator::Integer(..) => {
+ let integer = Integer::parse(context, input)?;
+ QueryExpressionValue::Integer(integer.value())
+ },
+ Evaluator::BoolInteger(..) => {
+ let integer = Integer::parse_non_negative(context, input)?;
+ let value = integer.value();
+ if value > 1 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ QueryExpressionValue::BoolInteger(value == 1)
+ },
+ Evaluator::Float(..) => {
+ let number = Number::parse(context, input)?;
+ QueryExpressionValue::Float(number.get())
+ },
+ Evaluator::OptionalNumberRatio(..) | Evaluator::NumberRatio(..) => {
+ use crate::values::specified::Ratio as SpecifiedRatio;
+ let ratio = SpecifiedRatio::parse(context, input)?;
+ QueryExpressionValue::NumberRatio(Ratio::new(ratio.0.get(), ratio.1.get()))
+ },
+ Evaluator::Resolution(..) => {
+ QueryExpressionValue::Resolution(Resolution::parse(context, input)?)
+ },
+ Evaluator::String(..) => {
+ QueryExpressionValue::String(input.expect_string()?.as_ref().into())
+ },
+ Evaluator::Enumerated { parser, .. } => {
+ QueryExpressionValue::Enumerated(parser(context, input)?)
+ },
+ })
+ }
+}
diff --git a/servo/components/style/queries/mod.rs b/servo/components/style/queries/mod.rs
new file mode 100644
index 0000000000..ec11ab3721
--- /dev/null
+++ b/servo/components/style/queries/mod.rs
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Code shared between [media queries][mq] and [container queries][cq].
+//!
+//! [mq]: https://drafts.csswg.org/mediaqueries/
+//! [cq]: https://drafts.csswg.org/css-contain-3/#container-rule
+
+pub mod condition;
+
+#[macro_use]
+pub mod feature;
+pub mod feature_expression;
+pub mod values;
+
+pub use self::condition::QueryCondition;
+pub use self::feature::FeatureFlags;
+pub use self::feature_expression::{FeatureType, QueryFeatureExpression};
diff --git a/servo/components/style/queries/values.rs b/servo/components/style/queries/values.rs
new file mode 100644
index 0000000000..f4934408c4
--- /dev/null
+++ b/servo/components/style/queries/values.rs
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Common feature values between media and container features.
+
+use app_units::Au;
+use euclid::default::Size2D;
+
+/// The orientation media / container feature.
+/// https://drafts.csswg.org/mediaqueries-5/#orientation
+/// https://drafts.csswg.org/css-contain-3/#orientation
+#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum Orientation {
+ Portrait,
+ Landscape,
+}
+
+impl Orientation {
+ /// A helper to evaluate a orientation query given a generic size getter.
+ pub fn eval(size: Size2D<Au>, value: Option<Self>) -> bool {
+ let query_orientation = match value {
+ Some(v) => v,
+ None => return true,
+ };
+
+ // Per spec, square viewports should be 'portrait'
+ let is_landscape = size.width > size.height;
+ match query_orientation {
+ Self::Landscape => is_landscape,
+ Self::Portrait => !is_landscape,
+ }
+ }
+}