diff options
Diffstat (limited to 'servo/components/style/media_queries/media_feature_expression.rs')
-rw-r--r-- | servo/components/style/media_queries/media_feature_expression.rs | 522 |
1 files changed, 522 insertions, 0 deletions
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())) + }, + }) + } +} |