summaryrefslogtreecommitdiffstats
path: root/servo/components/style/media_queries
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--servo/components/style/media_queries/media_condition.rs185
-rw-r--r--servo/components/style/media_queries/media_feature.rs180
-rw-r--r--servo/components/style/media_queries/media_feature_expression.rs522
-rw-r--r--servo/components/style/media_queries/media_list.rs138
-rw-r--r--servo/components/style/media_queries/media_query.rs180
-rw-r--r--servo/components/style/media_queries/mod.rs24
6 files changed, 1229 insertions, 0 deletions
diff --git a/servo/components/style/media_queries/media_condition.rs b/servo/components/style/media_queries/media_condition.rs
new file mode 100644
index 0000000000..f735704556
--- /dev/null
+++ b/servo/components/style/media_queries/media_condition.rs
@@ -0,0 +1,185 @@
+/* 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 media query condition:
+//!
+//! https://drafts.csswg.org/mediaqueries-4/#typedef-media-condition
+
+use super::{Device, MediaFeatureExpression};
+use crate::context::QuirksMode;
+use crate::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,
+}
+
+/// Represents a media condition.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub enum MediaCondition {
+ /// A simple media feature expression, implicitly parenthesized.
+ Feature(MediaFeatureExpression),
+ /// A negation of a condition.
+ Not(Box<MediaCondition>),
+ /// A set of joint operations.
+ Operation(Box<[MediaCondition]>, Operator),
+ /// A condition wrapped in parenthesis.
+ InParens(Box<MediaCondition>),
+}
+
+impl ToCss for MediaCondition {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match *self {
+ // NOTE(emilio): MediaFeatureExpression already includes the
+ // parenthesis.
+ MediaCondition::Feature(ref f) => f.to_css(dest),
+ MediaCondition::Not(ref c) => {
+ dest.write_str("not ")?;
+ c.to_css(dest)
+ },
+ MediaCondition::InParens(ref c) => {
+ dest.write_char('(')?;
+ c.to_css(dest)?;
+ dest.write_char(')')
+ },
+ MediaCondition::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(())
+ },
+ }
+ }
+}
+
+impl MediaCondition {
+ /// Parse a single media condition.
+ pub fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, AllowOr::Yes)
+ }
+
+ /// Parse a single media condition, disallowing `or` expressions.
+ ///
+ /// To be used from the legacy media query syntax.
+ pub fn parse_disallow_or<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, AllowOr::No)
+ }
+
+ fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_or: AllowOr,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+
+ // FIXME(emilio): This can be cleaner with nll.
+ let is_negation = match *input.next()? {
+ Token::ParenthesisBlock => false,
+ Token::Ident(ref ident) if ident.eq_ignore_ascii_case("not") => true,
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ };
+
+ if is_negation {
+ let inner_condition = Self::parse_in_parens(context, input)?;
+ return Ok(MediaCondition::Not(Box::new(inner_condition)));
+ }
+
+ // ParenthesisBlock.
+ let first_condition = Self::parse_paren_block(context, input)?;
+ 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)?);
+
+ 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(MediaCondition::Operation(
+ conditions.into_boxed_slice(),
+ operator,
+ ));
+ }
+
+ conditions.push(Self::parse_in_parens(context, input)?);
+ }
+ }
+
+ /// Parse a media condition in parentheses.
+ pub fn parse_in_parens<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_parenthesis_block()?;
+ Self::parse_paren_block(context, input)
+ }
+
+ fn parse_paren_block<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.parse_nested_block(|input| {
+ // Base case.
+ if let Ok(inner) = input.try_parse(|i| Self::parse(context, i)) {
+ return Ok(MediaCondition::InParens(Box::new(inner)));
+ }
+ let expr = MediaFeatureExpression::parse_in_parenthesis_block(context, input)?;
+ Ok(MediaCondition::Feature(expr))
+ })
+ }
+
+ /// Whether this condition matches the device and quirks mode.
+ pub fn matches(&self, device: &Device, quirks_mode: QuirksMode) -> bool {
+ match *self {
+ MediaCondition::Feature(ref f) => f.matches(device, quirks_mode),
+ MediaCondition::InParens(ref c) => c.matches(device, quirks_mode),
+ MediaCondition::Not(ref c) => !c.matches(device, quirks_mode),
+ MediaCondition::Operation(ref conditions, op) => {
+ let mut iter = conditions.iter();
+ match op {
+ Operator::And => iter.all(|c| c.matches(device, quirks_mode)),
+ Operator::Or => iter.any(|c| c.matches(device, quirks_mode)),
+ }
+ },
+ }
+ }
+}
diff --git a/servo/components/style/media_queries/media_feature.rs b/servo/components/style/media_queries/media_feature.rs
new file mode 100644
index 0000000000..cad695413c
--- /dev/null
+++ b/servo/components/style/media_queries/media_feature.rs
@@ -0,0 +1,180 @@
+/* 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/. */
+
+//! Media features.
+
+use super::media_feature_expression::RangeOrOperator;
+use super::Device;
+use crate::parser::ParserContext;
+use crate::values::computed::position::Ratio;
+use crate::values::computed::{CSSPixelLength, Resolution};
+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 MediaFeatureEvaluator<T> = fn(
+ device: &Device,
+ // null == no value was given in the query.
+ value: Option<T>,
+ range_or_operator: Option<RangeOrOperator>,
+) -> bool;
+
+/// 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 media feature.
+///
+/// This determines the kind of values that get parsed, too.
+#[allow(missing_docs)]
+pub enum Evaluator {
+ Length(MediaFeatureEvaluator<CSSPixelLength>),
+ Integer(MediaFeatureEvaluator<u32>),
+ Float(MediaFeatureEvaluator<f32>),
+ BoolInteger(MediaFeatureEvaluator<bool>),
+ /// A non-negative number ratio, such as the one from device-pixel-ratio.
+ NumberRatio(MediaFeatureEvaluator<Ratio>),
+ /// A resolution.
+ Resolution(MediaFeatureEvaluator<Resolution>),
+ /// 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: MediaFeatureEvaluator<KeywordDiscriminant>,
+ },
+ Ident(MediaFeatureEvaluator<Atom>),
+}
+
+/// 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::media_queries::media_feature::KeywordDiscriminant,
+ ::style_traits::ParseError<'i>,
+ > {
+ let kw = <$keyword_type as $crate::parser::Parse>::parse(context, input)?;
+ Ok(kw as $crate::media_queries::media_feature::KeywordDiscriminant)
+ }
+
+ fn __serialize(kw: $crate::media_queries::media_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(
+ device: &$crate::media_queries::Device,
+ value: Option<$crate::media_queries::media_feature::KeywordDiscriminant>,
+ range_or_operator: Option<
+ $crate::media_queries::media_feature_expression::RangeOrOperator,
+ >,
+ ) -> bool {
+ debug_assert!(
+ range_or_operator.is_none(),
+ "Since when do keywords accept ranges?"
+ );
+ // 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());
+ $actual_evaluator(device, value)
+ }
+
+ $crate::media_queries::media_feature::Evaluator::Enumerated {
+ parser: __parse,
+ serializer: __serialize,
+ evaluator: __evaluate,
+ }
+ }};
+}
+
+bitflags! {
+ /// Different requirements or toggles that change how a expression is
+ /// parsed.
+ pub struct ParsingRequirements: 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;
+ }
+}
+
+/// Whether a media feature allows ranges or not.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+#[allow(missing_docs)]
+pub enum AllowsRanges {
+ Yes,
+ No,
+}
+
+/// A description of a media feature.
+pub struct MediaFeatureDescription {
+ /// The media 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 requirements that need to hold for the feature to be
+ /// successfully parsed.
+ pub requirements: ParsingRequirements,
+}
+
+impl MediaFeatureDescription {
+ /// Whether this media feature allows ranges.
+ #[inline]
+ pub fn allows_ranges(&self) -> bool {
+ self.allows_ranges == AllowsRanges::Yes
+ }
+}
+
+/// A simple helper to construct a `MediaFeatureDescription`.
+macro_rules! feature {
+ ($name:expr, $allows_ranges:expr, $evaluator:expr, $reqs:expr,) => {
+ $crate::media_queries::media_feature::MediaFeatureDescription {
+ name: $name,
+ allows_ranges: $allows_ranges,
+ evaluator: $evaluator,
+ requirements: $reqs,
+ }
+ };
+}
+
+impl fmt::Debug for MediaFeatureDescription {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("MediaFeatureExpression")
+ .field("name", &self.name)
+ .field("allows_ranges", &self.allows_ranges)
+ .field("requirements", &self.requirements)
+ .finish()
+ }
+}
diff --git a/servo/components/style/media_queries/media_feature_expression.rs b/servo/components/style/media_queries/media_feature_expression.rs
new file mode 100644
index 0000000000..08b13136ef
--- /dev/null
+++ b/servo/components/style/media_queries/media_feature_expression.rs
@@ -0,0 +1,522 @@
+/* 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 media feature expressions, like `(foo: bar)` or
+//! `(width >= 400px)`.
+
+use super::media_feature::{Evaluator, MediaFeatureDescription};
+use super::media_feature::{KeywordDiscriminant, ParsingRequirements};
+use super::Device;
+use crate::context::QuirksMode;
+#[cfg(feature = "gecko")]
+use crate::gecko::media_features::MEDIA_FEATURES;
+use crate::parser::{Parse, ParserContext};
+#[cfg(feature = "servo")]
+use crate::servo::media_queries::MEDIA_FEATURES;
+use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase};
+use crate::values::computed::position::Ratio;
+use crate::values::computed::{self, ToComputedValue};
+use crate::values::specified::{Integer, Length, Number, Resolution};
+use crate::values::{serialize_atom_identifier, 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};
+
+/// The kind of matching that should be performed on a media feature value.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+pub enum Range {
+ /// At least the specified value.
+ Min,
+ /// At most the specified value.
+ Max,
+}
+
+/// The operator that was specified in this media feature.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+pub 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 {
+ Operator::Equal => "=",
+ Operator::LessThan => "<",
+ Operator::LessThanEqual => "<=",
+ Operator::GreaterThan => ">",
+ Operator::GreaterThanEqual => ">=",
+ })
+ }
+}
+
+/// Either a `Range` or an `Operator`.
+///
+/// Ranged media features are not allowed with operations (that'd make no
+/// sense).
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+pub enum RangeOrOperator {
+ /// A `Range`.
+ Range(Range),
+ /// An `Operator`.
+ Operator(Operator),
+}
+
+impl RangeOrOperator {
+ /// Evaluate a given range given an optional query value and a value from
+ /// the browser.
+ pub fn evaluate<T>(range_or_op: Option<Self>, query_value: Option<T>, value: T) -> bool
+ where
+ T: PartialOrd + Zero,
+ {
+ match query_value {
+ Some(v) => Self::evaluate_with_query_value(range_or_op, v, value),
+ None => !value.is_zero(),
+ }
+ }
+
+ /// Evaluate a given range given a non-optional query value and a value from
+ /// the browser.
+ pub fn evaluate_with_query_value<T>(range_or_op: Option<Self>, query_value: T, value: T) -> bool
+ where
+ T: PartialOrd,
+ {
+ let cmp = match value.partial_cmp(&query_value) {
+ Some(c) => c,
+ None => return false,
+ };
+
+ let range_or_op = match range_or_op {
+ Some(r) => r,
+ None => return cmp == Ordering::Equal,
+ };
+
+ match range_or_op {
+ RangeOrOperator::Range(range) => {
+ cmp == Ordering::Equal ||
+ match range {
+ Range::Min => cmp == Ordering::Greater,
+ Range::Max => cmp == Ordering::Less,
+ }
+ },
+ RangeOrOperator::Operator(op) => match op {
+ Operator::Equal => cmp == Ordering::Equal,
+ Operator::GreaterThan => cmp == Ordering::Greater,
+ Operator::GreaterThanEqual => cmp == Ordering::Equal || cmp == Ordering::Greater,
+ Operator::LessThan => cmp == Ordering::Less,
+ Operator::LessThanEqual => cmp == Ordering::Equal || cmp == Ordering::Less,
+ },
+ }
+ }
+}
+
+/// A feature expression contains a reference to the media feature, the value
+/// the media query contained, and the range to evaluate.
+#[derive(Clone, Debug, MallocSizeOf, ToShmem)]
+pub struct MediaFeatureExpression {
+ feature_index: usize,
+ value: Option<MediaExpressionValue>,
+ range_or_operator: Option<RangeOrOperator>,
+}
+
+impl PartialEq for MediaFeatureExpression {
+ fn eq(&self, other: &Self) -> bool {
+ self.feature_index == other.feature_index &&
+ self.value == other.value &&
+ self.range_or_operator == other.range_or_operator
+ }
+}
+
+impl ToCss for MediaFeatureExpression {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ dest.write_str("(")?;
+
+ let feature = self.feature();
+
+ if feature
+ .requirements
+ .contains(ParsingRequirements::WEBKIT_PREFIX)
+ {
+ dest.write_str("-webkit-")?;
+ }
+
+ if let Some(RangeOrOperator::Range(range)) = self.range_or_operator {
+ match range {
+ Range::Min => dest.write_str("min-")?,
+ Range::Max => dest.write_str("max-")?,
+ }
+ }
+
+ // NB: CssStringWriter not needed, feature names are under control.
+ write!(dest, "{}", feature.name)?;
+
+ if let Some(RangeOrOperator::Operator(op)) = self.range_or_operator {
+ dest.write_char(' ')?;
+ op.to_css(dest)?;
+ dest.write_char(' ')?;
+ } else if self.value.is_some() {
+ dest.write_str(": ")?;
+ }
+
+ if let Some(ref val) = self.value {
+ val.to_css(dest, self)?;
+ }
+
+ dest.write_str(")")
+ }
+}
+
+/// Consumes an operation or a colon, or returns an error.
+fn consume_operation_or_colon(input: &mut Parser) -> Result<Option<Operator>, ()> {
+ let first_delim = {
+ let next_token = match input.next() {
+ Ok(t) => t,
+ Err(..) => return Err(()),
+ };
+
+ match *next_token {
+ Token::Colon => return Ok(None),
+ Token::Delim(oper) => oper,
+ _ => return Err(()),
+ }
+ };
+ Ok(Some(match first_delim {
+ '=' => Operator::Equal,
+ '>' => {
+ if input.try_parse(|i| i.expect_delim('=')).is_ok() {
+ Operator::GreaterThanEqual
+ } else {
+ Operator::GreaterThan
+ }
+ },
+ '<' => {
+ if input.try_parse(|i| i.expect_delim('=')).is_ok() {
+ Operator::LessThanEqual
+ } else {
+ Operator::LessThan
+ }
+ },
+ _ => return Err(()),
+ }))
+}
+
+#[allow(unused_variables)]
+fn disabled_by_pref(feature: &Atom, context: &ParserContext) -> bool {
+ #[cfg(feature = "gecko")]
+ {
+ if *feature == atom!("forced-colors") {
+ return !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.in_ua_or_chrome_sheet() &&
+ !static_prefs::pref!("layout.css.prefers-contrast.enabled");
+ }
+ }
+ false
+}
+
+impl MediaFeatureExpression {
+ fn new(
+ feature_index: usize,
+ value: Option<MediaExpressionValue>,
+ range_or_operator: Option<RangeOrOperator>,
+ ) -> Self {
+ debug_assert!(feature_index < MEDIA_FEATURES.len());
+ Self {
+ feature_index,
+ value,
+ range_or_operator,
+ }
+ }
+
+ fn feature(&self) -> &'static MediaFeatureDescription {
+ &MEDIA_FEATURES[self.feature_index]
+ }
+
+ /// Parse a media expression of the form:
+ ///
+ /// ```
+ /// (media-feature: media-value)
+ /// ```
+ pub fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_parenthesis_block()?;
+ input.parse_nested_block(|input| Self::parse_in_parenthesis_block(context, input))
+ }
+
+ /// Parse a media feature expression where we've already consumed the
+ /// parenthesis.
+ pub fn parse_in_parenthesis_block<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut requirements = ParsingRequirements::empty();
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+
+ if context.in_ua_or_chrome_sheet() {
+ requirements.insert(ParsingRequirements::CHROME_AND_UA_ONLY);
+ }
+
+ let mut feature_name = &**ident;
+
+ if starts_with_ignore_ascii_case(feature_name, "-webkit-") {
+ feature_name = &feature_name[8..];
+ requirements.insert(ParsingRequirements::WEBKIT_PREFIX);
+ }
+
+ let range = if starts_with_ignore_ascii_case(feature_name, "min-") {
+ feature_name = &feature_name[4..];
+ Some(Range::Min)
+ } else if starts_with_ignore_ascii_case(feature_name, "max-") {
+ feature_name = &feature_name[4..];
+ Some(Range::Max)
+ } else {
+ None
+ };
+
+ let atom = Atom::from(string_as_ascii_lowercase(feature_name));
+
+ let (feature_index, feature) = match MEDIA_FEATURES
+ .iter()
+ .enumerate()
+ .find(|(_, f)| f.name == atom)
+ {
+ Some((i, f)) => (i, f),
+ None => {
+ return Err(location.new_custom_error(
+ StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()),
+ ))
+ },
+ };
+
+ if disabled_by_pref(&feature.name, context) ||
+ !requirements.contains(feature.requirements) ||
+ (range.is_some() && !feature.allows_ranges())
+ {
+ return Err(location.new_custom_error(
+ StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()),
+ ));
+ }
+
+ let operator = input.try_parse(consume_operation_or_colon);
+ let operator = match operator {
+ Err(..) => {
+ // If there's no colon, this is a media 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_index, None, None));
+ },
+ Ok(operator) => operator,
+ };
+
+ let range_or_operator = match range {
+ Some(range) => {
+ if operator.is_some() {
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator)
+ );
+ }
+ Some(RangeOrOperator::Range(range))
+ },
+ None => match operator {
+ Some(operator) => {
+ if !feature.allows_ranges() {
+ return Err(input
+ .new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator));
+ }
+ Some(RangeOrOperator::Operator(operator))
+ },
+ None => None,
+ },
+ };
+
+ let value = MediaExpressionValue::parse(feature, context, input).map_err(|err| {
+ err.location
+ .new_custom_error(StyleParseErrorKind::MediaQueryExpectedFeatureValue)
+ })?;
+
+ Ok(Self::new(feature_index, Some(value), range_or_operator))
+ }
+
+ /// Returns whether this media query evaluates to true for the given device.
+ pub fn matches(&self, device: &Device, quirks_mode: QuirksMode) -> bool {
+ let value = self.value.as_ref();
+
+ macro_rules! expect {
+ ($variant:ident) => {
+ value.map(|value| match *value {
+ MediaExpressionValue::$variant(ref v) => v,
+ _ => unreachable!("Unexpected MediaExpressionValue"),
+ })
+ };
+ }
+
+ match self.feature().evaluator {
+ Evaluator::Length(eval) => {
+ let computed = expect!(Length).map(|specified| {
+ computed::Context::for_media_query_evaluation(device, quirks_mode, |context| {
+ specified.to_computed_value(context)
+ })
+ });
+ eval(device, computed, self.range_or_operator)
+ },
+ Evaluator::Integer(eval) => {
+ eval(device, expect!(Integer).cloned(), self.range_or_operator)
+ },
+ Evaluator::Float(eval) => eval(device, expect!(Float).cloned(), self.range_or_operator),
+ Evaluator::NumberRatio(eval) => eval(
+ device,
+ expect!(NumberRatio).cloned(),
+ self.range_or_operator,
+ ),
+ Evaluator::Resolution(eval) => {
+ let computed = expect!(Resolution).map(|specified| {
+ computed::Context::for_media_query_evaluation(device, quirks_mode, |context| {
+ specified.to_computed_value(context)
+ })
+ });
+ eval(device, computed, self.range_or_operator)
+ },
+ Evaluator::Enumerated { evaluator, .. } => {
+ evaluator(device, expect!(Enumerated).cloned(), self.range_or_operator)
+ },
+ Evaluator::Ident(eval) => eval(device, expect!(Ident).cloned(), self.range_or_operator),
+ Evaluator::BoolInteger(eval) => eval(
+ device,
+ expect!(BoolInteger).cloned(),
+ self.range_or_operator,
+ ),
+ }
+ }
+}
+
+/// A value found or expected in a media 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 MediaExpressionValue {
+ /// A length.
+ Length(Length),
+ /// A (non-negative) integer.
+ Integer(u32),
+ /// 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 identifier.
+ Ident(Atom),
+}
+
+impl MediaExpressionValue {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>, for_expr: &MediaFeatureExpression) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match *self {
+ MediaExpressionValue::Length(ref l) => l.to_css(dest),
+ MediaExpressionValue::Integer(v) => v.to_css(dest),
+ MediaExpressionValue::Float(v) => v.to_css(dest),
+ MediaExpressionValue::BoolInteger(v) => dest.write_str(if v { "1" } else { "0" }),
+ MediaExpressionValue::NumberRatio(ratio) => ratio.to_css(dest),
+ MediaExpressionValue::Resolution(ref r) => r.to_css(dest),
+ MediaExpressionValue::Ident(ref ident) => serialize_atom_identifier(ident, dest),
+ MediaExpressionValue::Enumerated(value) => match for_expr.feature().evaluator {
+ Evaluator::Enumerated { serializer, .. } => dest.write_str(&*serializer(value)),
+ _ => unreachable!(),
+ },
+ }
+ }
+
+ fn parse<'i, 't>(
+ for_feature: &MediaFeatureDescription,
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<MediaExpressionValue, ParseError<'i>> {
+ Ok(match for_feature.evaluator {
+ Evaluator::Length(..) => {
+ let length = Length::parse_non_negative(context, input)?;
+ MediaExpressionValue::Length(length)
+ },
+ Evaluator::Integer(..) => {
+ let integer = Integer::parse_non_negative(context, input)?;
+ MediaExpressionValue::Integer(integer.value() as u32)
+ },
+ 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));
+ }
+ MediaExpressionValue::BoolInteger(value == 1)
+ },
+ Evaluator::Float(..) => {
+ let number = Number::parse(context, input)?;
+ MediaExpressionValue::Float(number.get())
+ },
+ Evaluator::NumberRatio(..) => {
+ use crate::values::generics::position::Ratio as GenericRatio;
+ use crate::values::generics::NonNegative;
+ use crate::values::specified::position::Ratio;
+
+ let ratio = Ratio::parse(context, input)?;
+ MediaExpressionValue::NumberRatio(GenericRatio(
+ NonNegative(ratio.0.get()),
+ NonNegative(ratio.1.get()),
+ ))
+ },
+ Evaluator::Resolution(..) => {
+ MediaExpressionValue::Resolution(Resolution::parse(context, input)?)
+ },
+ Evaluator::Enumerated { parser, .. } => {
+ MediaExpressionValue::Enumerated(parser(context, input)?)
+ },
+ Evaluator::Ident(..) => {
+ MediaExpressionValue::Ident(Atom::from(input.expect_ident()?.as_ref()))
+ },
+ })
+ }
+}
diff --git a/servo/components/style/media_queries/media_list.rs b/servo/components/style/media_queries/media_list.rs
new file mode 100644
index 0000000000..5d150f7db9
--- /dev/null
+++ b/servo/components/style/media_queries/media_list.rs
@@ -0,0 +1,138 @@
+/* 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 media query list:
+//!
+//! https://drafts.csswg.org/mediaqueries/#typedef-media-query-list
+
+use super::{Device, MediaQuery, Qualifier};
+use crate::context::QuirksMode;
+use crate::error_reporting::ContextualParseError;
+use crate::parser::ParserContext;
+use cssparser::{Delimiter, Parser};
+use cssparser::{ParserInput, Token};
+
+/// A type that encapsulates a media query list.
+#[css(comma, derive_debug)]
+#[derive(Clone, MallocSizeOf, ToCss, ToShmem)]
+pub struct MediaList {
+ /// The list of media queries.
+ #[css(iterable)]
+ pub media_queries: Vec<MediaQuery>,
+}
+
+impl MediaList {
+ /// Parse a media query list from CSS.
+ ///
+ /// Always returns a media query list. If any invalid media query is
+ /// found, the media query list is only filled with the equivalent of
+ /// "not all", see:
+ ///
+ /// <https://drafts.csswg.org/mediaqueries/#error-handling>
+ pub fn parse(context: &ParserContext, input: &mut Parser) -> Self {
+ if input.is_exhausted() {
+ return Self::empty();
+ }
+
+ let mut media_queries = vec![];
+ loop {
+ let start_position = input.position();
+ match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse(context, i)) {
+ Ok(mq) => {
+ media_queries.push(mq);
+ },
+ Err(err) => {
+ media_queries.push(MediaQuery::never_matching());
+ let location = err.location;
+ let error = ContextualParseError::InvalidMediaRule(
+ input.slice_from(start_position),
+ err,
+ );
+ context.log_css_error(location, error);
+ },
+ }
+
+ match input.next() {
+ Ok(&Token::Comma) => {},
+ Ok(_) => unreachable!(),
+ Err(_) => break,
+ }
+ }
+
+ MediaList { media_queries }
+ }
+
+ /// Create an empty MediaList.
+ pub fn empty() -> Self {
+ MediaList {
+ media_queries: vec![],
+ }
+ }
+
+ /// Evaluate a whole `MediaList` against `Device`.
+ pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> bool {
+ // Check if it is an empty media query list or any queries match.
+ // https://drafts.csswg.org/mediaqueries-4/#mq-list
+ self.media_queries.is_empty() ||
+ self.media_queries.iter().any(|mq| {
+ let media_match = mq.media_type.matches(device.media_type());
+
+ // Check if the media condition match.
+ let query_match = media_match &&
+ mq.condition
+ .as_ref()
+ .map_or(true, |c| c.matches(device, quirks_mode));
+
+ // Apply the logical NOT qualifier to the result
+ match mq.qualifier {
+ Some(Qualifier::Not) => !query_match,
+ _ => query_match,
+ }
+ })
+ }
+
+ /// Whether this `MediaList` contains no media queries.
+ pub fn is_empty(&self) -> bool {
+ self.media_queries.is_empty()
+ }
+
+ /// Append a new media query item to the media list.
+ /// <https://drafts.csswg.org/cssom/#dom-medialist-appendmedium>
+ ///
+ /// Returns true if added, false if fail to parse the medium string.
+ pub fn append_medium(&mut self, context: &ParserContext, new_medium: &str) -> bool {
+ let mut input = ParserInput::new(new_medium);
+ let mut parser = Parser::new(&mut input);
+ let new_query = match MediaQuery::parse(&context, &mut parser) {
+ Ok(query) => query,
+ Err(_) => {
+ return false;
+ },
+ };
+ // This algorithm doesn't actually matches the current spec,
+ // but it matches the behavior of Gecko and Edge.
+ // See https://github.com/w3c/csswg-drafts/issues/697
+ self.media_queries.retain(|query| query != &new_query);
+ self.media_queries.push(new_query);
+ true
+ }
+
+ /// Delete a media query from the media list.
+ /// <https://drafts.csswg.org/cssom/#dom-medialist-deletemedium>
+ ///
+ /// Returns true if found and deleted, false otherwise.
+ pub fn delete_medium(&mut self, context: &ParserContext, old_medium: &str) -> bool {
+ let mut input = ParserInput::new(old_medium);
+ let mut parser = Parser::new(&mut input);
+ let old_query = match MediaQuery::parse(context, &mut parser) {
+ Ok(query) => query,
+ Err(_) => {
+ return false;
+ },
+ };
+ let old_len = self.media_queries.len();
+ self.media_queries.retain(|query| query != &old_query);
+ old_len != self.media_queries.len()
+ }
+}
diff --git a/servo/components/style/media_queries/media_query.rs b/servo/components/style/media_queries/media_query.rs
new file mode 100644
index 0000000000..83e34c5037
--- /dev/null
+++ b/servo/components/style/media_queries/media_query.rs
@@ -0,0 +1,180 @@
+/* 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 media query:
+//!
+//! https://drafts.csswg.org/mediaqueries/#typedef-media-query
+
+use super::media_condition::MediaCondition;
+use crate::parser::ParserContext;
+use crate::str::string_as_ascii_lowercase;
+use crate::values::CustomIdent;
+use crate::Atom;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// <https://drafts.csswg.org/mediaqueries/#mq-prefix>
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
+pub enum Qualifier {
+ /// Hide a media query from legacy UAs:
+ /// <https://drafts.csswg.org/mediaqueries/#mq-only>
+ Only,
+ /// Negate a media query:
+ /// <https://drafts.csswg.org/mediaqueries/#mq-not>
+ Not,
+}
+
+/// <https://drafts.csswg.org/mediaqueries/#media-types>
+#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+pub struct MediaType(pub CustomIdent);
+
+impl MediaType {
+ /// The `screen` media type.
+ pub fn screen() -> Self {
+ MediaType(CustomIdent(atom!("screen")))
+ }
+
+ /// The `print` media type.
+ pub fn print() -> Self {
+ MediaType(CustomIdent(atom!("print")))
+ }
+
+ fn parse(name: &str) -> Result<Self, ()> {
+ // From https://drafts.csswg.org/mediaqueries/#mq-syntax:
+ //
+ // The <media-type> production does not include the keywords not, or, and, and only.
+ //
+ // Here we also perform the to-ascii-lowercase part of the serialization
+ // algorithm: https://drafts.csswg.org/cssom/#serializing-media-queries
+ match_ignore_ascii_case! { name,
+ "not" | "or" | "and" | "only" => Err(()),
+ _ => Ok(MediaType(CustomIdent(Atom::from(string_as_ascii_lowercase(name))))),
+ }
+ }
+}
+
+/// A [media query][mq].
+///
+/// [mq]: https://drafts.csswg.org/mediaqueries/
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub struct MediaQuery {
+ /// The qualifier for this query.
+ pub qualifier: Option<Qualifier>,
+ /// The media type for this query, that can be known, unknown, or "all".
+ pub media_type: MediaQueryType,
+ /// The condition that this media query contains. This cannot have `or`
+ /// in the first level.
+ pub condition: Option<MediaCondition>,
+}
+
+impl ToCss for MediaQuery {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if let Some(qual) = self.qualifier {
+ qual.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+
+ match self.media_type {
+ MediaQueryType::All => {
+ // We need to print "all" if there's a qualifier, or there's
+ // just an empty list of expressions.
+ //
+ // Otherwise, we'd serialize media queries like "(min-width:
+ // 40px)" in "all (min-width: 40px)", which is unexpected.
+ if self.qualifier.is_some() || self.condition.is_none() {
+ dest.write_str("all")?;
+ }
+ },
+ MediaQueryType::Concrete(MediaType(ref desc)) => desc.to_css(dest)?,
+ }
+
+ let condition = match self.condition {
+ Some(ref c) => c,
+ None => return Ok(()),
+ };
+
+ if self.media_type != MediaQueryType::All || self.qualifier.is_some() {
+ dest.write_str(" and ")?;
+ }
+
+ condition.to_css(dest)
+ }
+}
+
+impl MediaQuery {
+ /// Return a media query that never matches, used for when we fail to parse
+ /// a given media query.
+ pub fn never_matching() -> Self {
+ Self {
+ qualifier: Some(Qualifier::Not),
+ media_type: MediaQueryType::All,
+ condition: None,
+ }
+ }
+
+ /// Parse a media query given css input.
+ ///
+ /// Returns an error if any of the expressions is unknown.
+ pub fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let (qualifier, explicit_media_type) = input
+ .try_parse(|input| -> Result<_, ()> {
+ let qualifier = input.try_parse(Qualifier::parse).ok();
+ let ident = input.expect_ident().map_err(|_| ())?;
+ let media_type = MediaQueryType::parse(&ident)?;
+ Ok((qualifier, Some(media_type)))
+ })
+ .unwrap_or_default();
+
+ let condition = if explicit_media_type.is_none() {
+ Some(MediaCondition::parse(context, input)?)
+ } else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() {
+ Some(MediaCondition::parse_disallow_or(context, input)?)
+ } else {
+ None
+ };
+
+ let media_type = explicit_media_type.unwrap_or(MediaQueryType::All);
+ Ok(Self {
+ qualifier,
+ media_type,
+ condition,
+ })
+ }
+}
+
+/// <http://dev.w3.org/csswg/mediaqueries-3/#media0>
+#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+pub enum MediaQueryType {
+ /// A media type that matches every device.
+ All,
+ /// A specific media type.
+ Concrete(MediaType),
+}
+
+impl MediaQueryType {
+ fn parse(ident: &str) -> Result<Self, ()> {
+ match_ignore_ascii_case! { ident,
+ "all" => return Ok(MediaQueryType::All),
+ _ => (),
+ };
+
+ // If parseable, accept this type as a concrete type.
+ MediaType::parse(ident).map(MediaQueryType::Concrete)
+ }
+
+ /// Returns whether this media query type matches a MediaType.
+ pub fn matches(&self, other: MediaType) -> bool {
+ match *self {
+ MediaQueryType::All => true,
+ MediaQueryType::Concrete(ref known_type) => *known_type == other,
+ }
+ }
+}
diff --git a/servo/components/style/media_queries/mod.rs b/servo/components/style/media_queries/mod.rs
new file mode 100644
index 0000000000..ca38585748
--- /dev/null
+++ b/servo/components/style/media_queries/mod.rs
@@ -0,0 +1,24 @@
+/* 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/. */
+
+//! [Media queries][mq].
+//!
+//! [mq]: https://drafts.csswg.org/mediaqueries/
+
+mod media_condition;
+mod media_list;
+mod media_query;
+#[macro_use]
+pub mod media_feature;
+pub mod media_feature_expression;
+
+pub use self::media_condition::MediaCondition;
+pub use self::media_feature_expression::MediaFeatureExpression;
+pub use self::media_list::MediaList;
+pub use self::media_query::{MediaQuery, MediaQueryType, MediaType, Qualifier};
+
+#[cfg(feature = "gecko")]
+pub use crate::gecko::media_queries::Device;
+#[cfg(feature = "servo")]
+pub use crate::servo::media_queries::Device;