From 0b49a3701a7717ae9b4662e38bdbe35328394705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Fri, 15 Jun 2018 23:51:07 -0700 Subject: [PATCH] style: Add code to parse media conditions. Still unused. Bug: 1422225 Reviewed-by: xidorn MozReview-Commit-ID: IQfxObw9BV5 --- components/style/gecko/media_queries.rs | 251 +++++++++--------- .../style/media_queries/media_condition.rs | 109 ++++++++ components/style/media_queries/mod.rs | 2 + components/style/servo/media_queries.rs | 43 +-- 4 files changed, 267 insertions(+), 138 deletions(-) create mode 100644 components/style/media_queries/media_condition.rs diff --git a/components/style/gecko/media_queries.rs b/components/style/gecko/media_queries.rs index 02fc864eb09..a6322842f20 100644 --- a/components/style/gecko/media_queries.rs +++ b/components/style/gecko/media_queries.rs @@ -651,141 +651,150 @@ impl MediaFeatureExpression { })?; input.parse_nested_block(|input| { - // FIXME: remove extra indented block when lifetimes are non-lexical - let feature; - let range; + Self::parse_in_parenthesis_block(context, input) + }) + } + + /// Parse a media range expression where we've already consumed the + /// parenthesis. + pub fn parse_in_parenthesis_block<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + // FIXME: remove extra indented block when lifetimes are non-lexical + let feature; + let range; + { + let location = input.current_source_location(); + let ident = input.expect_ident().map_err(|err| { + err.location.new_custom_error(match err.kind { + BasicParseErrorKind::UnexpectedToken(t) => { + StyleParseErrorKind::ExpectedIdentifier(t) + }, + _ => StyleParseErrorKind::UnspecifiedError, + }) + })?; + + let mut flags = 0; + + if context.chrome_rules_enabled() || context.stylesheet_origin == Origin::UserAgent { - let location = input.current_source_location(); - let ident = input.expect_ident().map_err(|err| { - err.location.new_custom_error(match err.kind { - BasicParseErrorKind::UnexpectedToken(t) => { - StyleParseErrorKind::ExpectedIdentifier(t) - }, - _ => StyleParseErrorKind::UnspecifiedError, - }) - })?; - - let mut flags = 0; - - if context.chrome_rules_enabled() || context.stylesheet_origin == Origin::UserAgent - { - flags |= structs::nsMediaFeature_RequirementFlags_eUserAgentAndChromeOnly; - } - - let result = { - let mut feature_name = &**ident; - - if unsafe { structs::StaticPrefs_sVarCache_layout_css_prefixes_webkit } && - starts_with_ignore_ascii_case(feature_name, "-webkit-") - { - feature_name = &feature_name[8..]; - flags |= structs::nsMediaFeature_RequirementFlags_eHasWebkitPrefix; - if unsafe { - structs::StaticPrefs_sVarCache_layout_css_prefixes_device_pixel_ratio_webkit - } { - flags |= structs::nsMediaFeature_RequirementFlags_eWebkitDevicePixelRatioPrefEnabled; - } - } - - 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)); - match find_feature(|f| atom.as_ptr() == unsafe { *f.mName as *mut _ }) { - Some(f) => Ok((f, range)), - None => Err(()), - } - }; - - match result { - Ok((f, r)) => { - feature = f; - range = r; - }, - Err(()) => { - return Err(location.new_custom_error( - StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), - )) - }, - } - - if (feature.mReqFlags & !flags) != 0 { - return Err(location.new_custom_error( - StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), - )); - } - - if range.is_some() && - feature.mRangeType != nsMediaFeature_RangeType::eMinMaxAllowed - { - return Err(location.new_custom_error( - StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), - )); - } + flags |= structs::nsMediaFeature_RequirementFlags_eUserAgentAndChromeOnly; } - let feature_allows_ranges = - feature.mRangeType == nsMediaFeature_RangeType::eMinMaxAllowed; + let result = { + let mut feature_name = &**ident; - let operator = input.try(consume_operation_or_colon); - let operator = match operator { - Err(..) => { - // If there's no colon, this is a media query of the - // form '()', 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 - )); + if unsafe { structs::StaticPrefs_sVarCache_layout_css_prefixes_webkit } && + starts_with_ignore_ascii_case(feature_name, "-webkit-") + { + feature_name = &feature_name[8..]; + flags |= structs::nsMediaFeature_RequirementFlags_eHasWebkitPrefix; + if unsafe { + structs::StaticPrefs_sVarCache_layout_css_prefixes_device_pixel_ratio_webkit + } { + flags |= structs::nsMediaFeature_RequirementFlags_eWebkitDevicePixelRatioPrefEnabled; } - - return Ok(Self::new(feature, None, None)); } - Ok(operator) => operator, + + 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)); + match find_feature(|f| atom.as_ptr() == unsafe { *f.mName as *mut _ }) { + Some(f) => Ok((f, range)), + None => Err(()), + } }; - let range_or_operator = match range { - Some(range) => { - if operator.is_some() { - return Err(input.new_custom_error( - StyleParseErrorKind::MediaQueryExpectedFeatureValue - )); - } - Some(RangeOrOperator::Range(range)) + match result { + Ok((f, r)) => { + feature = f; + range = r; + }, + Err(()) => { + return Err(location.new_custom_error( + StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), + )) + }, + } + + if (feature.mReqFlags & !flags) != 0 { + return Err(location.new_custom_error( + StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), + )); + } + + if range.is_some() && + feature.mRangeType != nsMediaFeature_RangeType::eMinMaxAllowed + { + return Err(location.new_custom_error( + StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), + )); + } + } + + let feature_allows_ranges = + feature.mRangeType == nsMediaFeature_RangeType::eMinMaxAllowed; + + let operator = input.try(consume_operation_or_colon); + let operator = match operator { + Err(..) => { + // If there's no colon, this is a media query of the + // form '()', 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 + )); } - None => { - match operator { - Some(operator) => { - if !feature_allows_ranges { - return Err(input.new_custom_error( - StyleParseErrorKind::MediaQueryExpectedFeatureValue - )); - } - Some(RangeOrOperator::Operator(operator)) + + return Ok(Self::new(feature, None, None)); + } + Ok(operator) => operator, + }; + + let range_or_operator = match range { + Some(range) => { + if operator.is_some() { + return Err(input.new_custom_error( + StyleParseErrorKind::MediaQueryExpectedFeatureValue + )); + } + Some(RangeOrOperator::Range(range)) + } + None => { + match operator { + Some(operator) => { + if !feature_allows_ranges { + return Err(input.new_custom_error( + StyleParseErrorKind::MediaQueryExpectedFeatureValue + )); } - None => None, + Some(RangeOrOperator::Operator(operator)) } + None => None, } - }; + } + }; - let value = - parse_feature_value(feature, feature.mValueType, context, input).map_err(|err| { - err.location - .new_custom_error(StyleParseErrorKind::MediaQueryExpectedFeatureValue) - })?; + let value = + parse_feature_value(feature, feature.mValueType, context, input).map_err(|err| { + err.location + .new_custom_error(StyleParseErrorKind::MediaQueryExpectedFeatureValue) + })?; - Ok(Self::new(feature, Some(value), range_or_operator)) - }) + Ok(Self::new(feature, Some(value), range_or_operator)) } /// Returns whether this media query evaluates to true for the given device. diff --git a/components/style/media_queries/media_condition.rs b/components/style/media_queries/media_condition.rs new file mode 100644 index 00000000000..cb3bd73ee20 --- /dev/null +++ b/components/style/media_queries/media_condition.rs @@ -0,0 +1,109 @@ +/* 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 http://mozilla.org/MPL/2.0/. */ + +//! A media query condition: +//! +//! https://drafts.csswg.org/mediaqueries-4/#typedef-media-condition + +use cssparser::{Parser, Token}; +use parser::ParserContext; +use style_traits::ParseError; + +use super::MediaFeatureExpression; + + +/// A binary `and` or `or` operator. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, Parse, ToCss)] +#[allow(missing_docs)] +pub enum Operator { + And, + Or, +} + +/// Represents a media condition. +pub enum MediaCondition { + /// A simple media feature expression, implicitly parenthesized. + Feature(MediaFeatureExpression), + /// A negation of a condition. + Not(Box), + /// A set of joint operations. + Operation(Box<[MediaCondition]>, Operator), + /// A condition wrapped in parenthesis. + InParens(Box), +} + +impl MediaCondition { + /// Parse a single media condition. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + 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(Operator::parse) { + Ok(op) => op, + Err(..) => return Ok(first_condition), + }; + + 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(|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> { + input.expect_parenthesis_block()?; + Self::parse_paren_block(context, input) + } + + fn parse_paren_block<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + input.parse_nested_block(|input| { + // Base case. + if let Ok(expr) = input.try(|i| MediaFeatureExpression::parse_in_parenthesis_block(context, i)) { + return Ok(MediaCondition::Feature(expr)); + } + + let inner = Self::parse(context, input)?; + Ok(MediaCondition::InParens(Box::new(inner))) + }) + } +} diff --git a/components/style/media_queries/mod.rs b/components/style/media_queries/mod.rs index 8c0722265a7..d27e33cc64c 100644 --- a/components/style/media_queries/mod.rs +++ b/components/style/media_queries/mod.rs @@ -6,9 +6,11 @@ //! //! [mq]: https://drafts.csswg.org/mediaqueries/ +mod media_condition; mod media_list; mod media_query; +pub use self::media_condition::MediaCondition; pub use self::media_list::MediaList; pub use self::media_query::{MediaQuery, MediaQueryType, MediaType, Qualifier}; diff --git a/components/style/servo/media_queries.rs b/components/style/servo/media_queries.rs index 14dec24b261..3a8235aa65c 100644 --- a/components/style/servo/media_queries.rs +++ b/components/style/servo/media_queries.rs @@ -183,34 +183,43 @@ impl MediaFeatureExpression { /// Parse a media expression of the form: /// /// ``` - /// (media-feature: media-value) + /// media-feature: media-value /// ``` /// - /// Only supports width and width ranges for now. + /// Only supports width ranges for now. pub fn parse<'i, 't>( context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { input.expect_parenthesis_block()?; input.parse_nested_block(|input| { - let name = input.expect_ident_cloned()?; - input.expect_colon()?; - // TODO: Handle other media features - Ok(MediaFeatureExpression(match_ignore_ascii_case! { &name, - "min-width" => { - ExpressionKind::Width(Range::Min(specified::Length::parse_non_negative(context, input)?)) - }, - "max-width" => { - ExpressionKind::Width(Range::Max(specified::Length::parse_non_negative(context, input)?)) - }, - "width" => { - ExpressionKind::Width(Range::Eq(specified::Length::parse_non_negative(context, input)?)) - }, - _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name))) - })) + Self::parse_in_parenthesis_block(context, input) }) } + /// Parse a media range expression where we've already consumed the + /// parenthesis. + pub fn parse_in_parenthesis_block<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let name = input.expect_ident_cloned()?; + input.expect_colon()?; + // TODO: Handle other media features + Ok(MediaFeatureExpression(match_ignore_ascii_case! { &name, + "min-width" => { + ExpressionKind::Width(Range::Min(specified::Length::parse_non_negative(context, input)?)) + }, + "max-width" => { + ExpressionKind::Width(Range::Max(specified::Length::parse_non_negative(context, input)?)) + }, + "width" => { + ExpressionKind::Width(Range::Eq(specified::Length::parse_non_negative(context, input)?)) + }, + _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name))) + })) + } + /// Evaluate this expression and return whether it matches the current /// device. pub fn matches(&self, device: &Device, quirks_mode: QuirksMode) -> bool {