diff options
Diffstat (limited to '')
-rw-r--r-- | servo/components/style/media_queries/media_condition.rs | 185 | ||||
-rw-r--r-- | servo/components/style/media_queries/media_feature.rs | 180 | ||||
-rw-r--r-- | servo/components/style/media_queries/media_feature_expression.rs | 522 | ||||
-rw-r--r-- | servo/components/style/media_queries/media_list.rs | 138 | ||||
-rw-r--r-- | servo/components/style/media_queries/media_query.rs | 180 | ||||
-rw-r--r-- | servo/components/style/media_queries/mod.rs | 24 |
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; |