From edc40ce320c562aaeb974e78950e4e57d9e48482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Wed, 17 Oct 2018 12:08:14 +0000 Subject: [PATCH] style: Implement @supports selector() syntax. This implements the selector() syntax for @supports. See https://github.com/w3c/csswg-drafts/issues/3207 for explainer and discussion. Probably would should wait for that to be sorted out to land this, or maybe we should put it behind a pref to get the code landed and change our implementation if the discussion there leads to a change. Differential Revision: https://phabricator.services.mozilla.com/D8864 --- components/selectors/parser.rs | 17 +- components/style/stylesheets/mod.rs | 1 - components/style/stylesheets/rule_parser.rs | 11 +- components/style/stylesheets/stylesheet.rs | 1 - components/style/stylesheets/supports_rule.rs | 192 +++++++++++++----- 5 files changed, 157 insertions(+), 65 deletions(-) diff --git a/components/selectors/parser.rs b/components/selectors/parser.rs index 8e26ff63d8a..fc4b26bb3a3 100644 --- a/components/selectors/parser.rs +++ b/components/selectors/parser.rs @@ -259,8 +259,13 @@ where Impl: SelectorImpl, { let location = input.current_source_location(); - let selector = Selector::parse(parser, input)?; - // Ensure they're actually all compound selectors. + let selector = parse_selector(parser, input)?; + + // Ensure they're actually all compound selectors without pseudo-elements. + if selector.has_pseudo_element() { + return Err(location.new_custom_error(SelectorParseErrorKind::PseudoElementInComplexSelector)); + } + if selector.iter_raw_match_order().any(|s| s.is_combinator()) { return Err(location.new_custom_error(SelectorParseErrorKind::NonCompoundSelector)); } @@ -1397,6 +1402,7 @@ where impl Selector { /// Parse a selector, without any pseudo-element. + #[inline] pub fn parse<'i, 't, P>( parser: &P, input: &mut CssParser<'i, 't>, @@ -1404,12 +1410,7 @@ impl Selector { where P: Parser<'i, Impl = Impl>, { - let selector = parse_selector(parser, input)?; - if selector.has_pseudo_element() { - let e = SelectorParseErrorKind::PseudoElementInComplexSelector; - return Err(input.new_custom_error(e)); - } - Ok(selector) + parse_selector(parser, input) } } diff --git a/components/style/stylesheets/mod.rs b/components/style/stylesheets/mod.rs index 5058ad94e80..81726950665 100644 --- a/components/style/stylesheets/mod.rs +++ b/components/style/stylesheets/mod.rs @@ -277,7 +277,6 @@ impl CssRule { // nested rules are in the body state let mut rule_parser = TopLevelRuleParser { - stylesheet_origin: parent_stylesheet_contents.origin, context, shared_lock: &shared_lock, loader, diff --git a/components/style/stylesheets/rule_parser.rs b/components/style/stylesheets/rule_parser.rs index 683378b9b96..aa449c70e93 100644 --- a/components/style/stylesheets/rule_parser.rs +++ b/components/style/stylesheets/rule_parser.rs @@ -19,7 +19,7 @@ use servo_arc::Arc; use shared_lock::{Locked, SharedRwLock}; use str::starts_with_ignore_ascii_case; use style_traits::{ParseError, StyleParseErrorKind}; -use stylesheets::{CssRule, CssRuleType, CssRules, Origin, RulesMutateError, StylesheetLoader}; +use stylesheets::{CssRule, CssRuleType, CssRules, RulesMutateError, StylesheetLoader}; use stylesheets::{DocumentRule, FontFeatureValuesRule, KeyframesRule, MediaRule}; use stylesheets::{NamespaceRule, PageRule, StyleRule, SupportsRule, ViewportRule}; use stylesheets::document_rule::DocumentCondition; @@ -41,8 +41,6 @@ pub struct InsertRuleContext<'a> { /// The parser for the top-level rules in a stylesheet. pub struct TopLevelRuleParser<'a> { - /// The origin of the stylesheet we're parsing. - pub stylesheet_origin: Origin, /// A reference to the lock we need to use to create rules. pub shared_lock: &'a SharedRwLock, /// A reference to a stylesheet loader if applicable, for `@import` rules. @@ -69,7 +67,6 @@ pub struct TopLevelRuleParser<'a> { impl<'b> TopLevelRuleParser<'b> { fn nested<'a: 'b>(&'a self) -> NestedRuleParser<'a, 'b> { NestedRuleParser { - stylesheet_origin: self.stylesheet_origin, shared_lock: self.shared_lock, context: &self.context, namespaces: &self.namespaces, @@ -325,7 +322,6 @@ impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser<'a> { #[derive(Clone)] // shallow, relatively cheap .clone struct NestedRuleParser<'a, 'b: 'a> { - stylesheet_origin: Origin, shared_lock: &'a SharedRwLock, context: &'a ParserContext<'b>, namespaces: &'a Namespaces, @@ -340,7 +336,6 @@ impl<'a, 'b> NestedRuleParser<'a, 'b> { let context = ParserContext::new_with_rule_type(self.context, rule_type, self.namespaces); let nested_parser = NestedRuleParser { - stylesheet_origin: self.stylesheet_origin, shared_lock: self.shared_lock, context: &context, namespaces: self.namespaces, @@ -501,7 +496,7 @@ impl<'a, 'b, 'i> AtRuleParser<'i> for NestedRuleParser<'a, 'b> { self.namespaces, ); - let enabled = condition.eval(&eval_context); + let enabled = condition.eval(&eval_context, self.namespaces); Ok(CssRule::Supports(Arc::new(self.shared_lock.wrap( SupportsRule { condition, @@ -577,7 +572,7 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'b> { input: &mut Parser<'i, 't>, ) -> Result> { let selector_parser = SelectorParser { - stylesheet_origin: self.stylesheet_origin, + stylesheet_origin: self.context.stylesheet_origin, namespaces: self.namespaces, url_data: Some(self.context.url_data), }; diff --git a/components/style/stylesheets/stylesheet.rs b/components/style/stylesheets/stylesheet.rs index e1359cb3722..ac1d810a05b 100644 --- a/components/style/stylesheets/stylesheet.rs +++ b/components/style/stylesheets/stylesheet.rs @@ -374,7 +374,6 @@ impl Stylesheet { ); let rule_parser = TopLevelRuleParser { - stylesheet_origin: origin, shared_lock, loader: stylesheet_loader, context, diff --git a/components/style/stylesheets/supports_rule.rs b/components/style/stylesheets/supports_rule.rs index 2851d8914dd..a91a3ea3d17 100644 --- a/components/style/stylesheets/supports_rule.rs +++ b/components/style/stylesheets/supports_rule.rs @@ -12,6 +12,8 @@ use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; use parser::ParserContext; use properties::{PropertyDeclaration, PropertyId, SourcePropertyDeclaration}; use selectors::parser::SelectorParseErrorKind; +use selector_parser::{SelectorImpl, SelectorParser}; +use selectors::parser::Selector; use servo_arc::Arc; use shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; use shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; @@ -19,8 +21,8 @@ use std::ffi::{CStr, CString}; use std::fmt::{self, Write}; use std::str; use str::CssStringWriter; -use style_traits::{CssWriter, ParseError, ToCss}; -use stylesheets::{CssRuleType, CssRules}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; +use stylesheets::{CssRuleType, CssRules, Namespaces}; /// An [`@supports`][supports] rule. /// @@ -87,6 +89,8 @@ pub enum SupportsCondition { Or(Vec), /// `property-ident: value` (value can be any tokens) Declaration(Declaration), + /// A `selector()` function. + Selector(RawSelector), /// `-moz-bool-pref("pref-name")` /// Since we need to pass it through FFI to get the pref value, /// we store it as CString directly. @@ -99,8 +103,8 @@ impl SupportsCondition { /// Parse a condition /// /// - pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { - if let Ok(_) = input.try(|i| i.expect_ident_matching("not")) { + pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { + if input.try(|i| i.expect_ident_matching("not")).is_ok() { let inner = SupportsCondition::parse_in_parens(input)?; return Ok(SupportsCondition::Not(Box::new(inner))); } @@ -109,10 +113,8 @@ impl SupportsCondition { let location = input.current_source_location(); let (keyword, wrapper) = match input.next() { - Err(_) => { - // End of input - return Ok(in_parens); - }, + // End of input + Err(..) => return Ok(in_parens), Ok(&Token::Ident(ref ident)) => { match_ignore_ascii_case! { &ident, "and" => ("and", SupportsCondition::And as fn(_) -> _), @@ -132,17 +134,48 @@ impl SupportsCondition { .is_err() { // Did not find the expected keyword. - // If we found some other token, - // it will be rejected by `Parser::parse_entirely` somewhere up the stack. + // If we found some other token, it will be rejected by + // `Parser::parse_entirely` somewhere up the stack. return Ok(wrapper(conditions)); } } } + /// Parses a functional supports condition. + fn parse_functional<'i, 't>( + function: &str, + input: &mut Parser<'i, 't>, + ) -> Result> { + match_ignore_ascii_case!{ function, + // Although this is an internal syntax, it is not necessary + // to check parsing context as far as we accept any + // unexpected token as future syntax, and evaluate it to + // false when not in chrome / ua sheet. + // See https://drafts.csswg.org/css-conditional-3/#general_enclosed + "-moz-bool-pref" => { + let name = { + let name = input.expect_string()?; + CString::new(name.as_bytes()) + }.map_err(|_| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))?; + Ok(SupportsCondition::MozBoolPref(name)) + } + "selector" => { + let pos = input.position(); + consume_any_value(input)?; + Ok(SupportsCondition::Selector(RawSelector( + input.slice_from(pos).to_owned() + ))) + } + _ => { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } + } + /// fn parse_in_parens<'i, 't>( input: &mut Parser<'i, 't>, - ) -> Result> { + ) -> Result> { // Whitespace is normally taken care of in `Parser::next`, // but we want to not include it in `pos` for the SupportsCondition::FutureSyntax cases. while input.try(Parser::expect_whitespace).is_ok() {} @@ -151,46 +184,45 @@ impl SupportsCondition { // FIXME: remove clone() when lifetimes are non-lexical match input.next()?.clone() { Token::ParenthesisBlock => { - let nested = input - .try(|input| input.parse_nested_block(|i| parse_condition_or_declaration(i))); + let nested = input.try(|input| { + input.parse_nested_block(parse_condition_or_declaration) + }); if nested.is_ok() { return nested; } }, Token::Function(ident) => { - // Although this is an internal syntax, it is not necessary to check - // parsing context as far as we accept any unexpected token as future - // syntax, and evaluate it to false when not in chrome / ua sheet. - // See https://drafts.csswg.org/css-conditional-3/#general_enclosed - if ident.eq_ignore_ascii_case("-moz-bool-pref") { - if let Ok(name) = input.try(|i| { - i.parse_nested_block(|i| { - i.expect_string() - .map(|s| s.to_string()) - .map_err(CssParseError::<()>::from) - }).and_then(|s| CString::new(s).map_err(|_| location.new_custom_error(()))) - }) { - return Ok(SupportsCondition::MozBoolPref(name)); - } + let nested = input.try(|input| { + input.parse_nested_block(|input| { + SupportsCondition::parse_functional(&ident, input) + }) + }); + if nested.is_ok() { + return nested; } }, t => return Err(location.new_unexpected_token_error(t)), } - input.parse_nested_block(|i| consume_any_value(i))?; + input.parse_nested_block(consume_any_value)?; Ok(SupportsCondition::FutureSyntax( input.slice_from(pos).to_owned(), )) } /// Evaluate a supports condition - pub fn eval(&self, cx: &ParserContext) -> bool { + pub fn eval( + &self, + cx: &ParserContext, + namespaces: &Namespaces, + ) -> bool { match *self { - SupportsCondition::Not(ref cond) => !cond.eval(cx), - SupportsCondition::Parenthesized(ref cond) => cond.eval(cx), - SupportsCondition::And(ref vec) => vec.iter().all(|c| c.eval(cx)), - SupportsCondition::Or(ref vec) => vec.iter().any(|c| c.eval(cx)), + SupportsCondition::Not(ref cond) => !cond.eval(cx, namespaces), + SupportsCondition::Parenthesized(ref cond) => cond.eval(cx, namespaces), + SupportsCondition::And(ref vec) => vec.iter().all(|c| c.eval(cx, namespaces)), + SupportsCondition::Or(ref vec) => vec.iter().any(|c| c.eval(cx, namespaces)), SupportsCondition::Declaration(ref decl) => decl.eval(cx), SupportsCondition::MozBoolPref(ref name) => eval_moz_bool_pref(name, cx), + SupportsCondition::Selector(ref selector) => selector.eval(cx, namespaces), SupportsCondition::FutureSyntax(_) => false, } } @@ -265,6 +297,11 @@ impl ToCss for SupportsCondition { decl.to_css(dest)?; dest.write_str(")") }, + SupportsCondition::Selector(ref selector) => { + dest.write_str("selector(")?; + selector.to_css(dest)?; + dest.write_str(")") + } SupportsCondition::MozBoolPref(ref name) => { dest.write_str("-moz-bool-pref(")?; let name = @@ -277,6 +314,68 @@ impl ToCss for SupportsCondition { } } +#[derive(Clone, Debug)] +/// A possibly-invalid CSS selector. +pub struct RawSelector(pub String); + +impl ToCss for RawSelector { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + dest.write_str(&self.0) + } +} + +impl RawSelector { + /// Tries to evaluate a `selector()` function. + pub fn eval( + &self, + context: &ParserContext, + namespaces: &Namespaces, + ) -> bool { + #[cfg(feature = "gecko")] + { + if unsafe { !::gecko_bindings::structs::StaticPrefs_sVarCache_layout_css_supports_selector_enabled } { + return false; + } + } + + let mut input = ParserInput::new(&self.0); + let mut input = Parser::new(&mut input); + input.parse_entirely(|input| -> Result<(), CssParseError<()>> { + let parser = SelectorParser { + namespaces, + stylesheet_origin: context.stylesheet_origin, + url_data: Some(context.url_data), + }; + + let selector = Selector::::parse(&parser, input) + .map_err(|_| input.new_custom_error(()))?; + + #[cfg(feature = "gecko")] + { + use selectors::parser::Component; + use selector_parser::PseudoElement; + + let has_any_unknown_webkit_pseudo = + selector.has_pseudo_element() && + selector.iter_raw_match_order().any(|component| { + matches!( + *component, + Component::PseudoElement(PseudoElement::UnknownWebkit(..)) + ) + }); + if has_any_unknown_webkit_pseudo { + return Err(input.new_custom_error(())); + } + } + + Ok(()) + }).is_ok() + } +} + #[derive(Clone, Debug)] /// A possibly-invalid property declaration pub struct Declaration(pub String); @@ -313,21 +412,20 @@ impl Declaration { let mut input = ParserInput::new(&self.0); let mut input = Parser::new(&mut input); - input - .parse_entirely(|input| -> Result<(), CssParseError<()>> { - let prop = input.expect_ident_cloned().unwrap(); - input.expect_colon().unwrap(); + input.parse_entirely(|input| -> Result<(), CssParseError<()>> { + let prop = input.expect_ident_cloned().unwrap(); + input.expect_colon().unwrap(); - let id = - PropertyId::parse(&prop, context).map_err(|_| input.new_custom_error(()))?; + let id = + PropertyId::parse(&prop, context).map_err(|_| input.new_custom_error(()))?; - let mut declarations = SourcePropertyDeclaration::new(); - input.parse_until_before(Delimiter::Bang, |input| { - PropertyDeclaration::parse_into(&mut declarations, id, &context, input) - .map_err(|_| input.new_custom_error(())) - })?; - let _ = input.try(parse_important); - Ok(()) - }).is_ok() + let mut declarations = SourcePropertyDeclaration::new(); + input.parse_until_before(Delimiter::Bang, |input| { + PropertyDeclaration::parse_into(&mut declarations, id, &context, input) + .map_err(|_| input.new_custom_error(())) + })?; + let _ = input.try(parse_important); + Ok(()) + }).is_ok() } }