diff --git a/layout/reftests/bugs/1499386-ref.html b/layout/reftests/bugs/1499386-ref.html new file mode 100644 index 000000000000..30475270df85 --- /dev/null +++ b/layout/reftests/bugs/1499386-ref.html @@ -0,0 +1,3 @@ + diff --git a/layout/reftests/bugs/1499386.html b/layout/reftests/bugs/1499386.html new file mode 100644 index 000000000000..e84642a79fce --- /dev/null +++ b/layout/reftests/bugs/1499386.html @@ -0,0 +1,6 @@ + + diff --git a/layout/reftests/bugs/reftest.list b/layout/reftests/bugs/reftest.list index c453ef8b42f3..7183fa0ce322 100644 --- a/layout/reftests/bugs/reftest.list +++ b/layout/reftests/bugs/reftest.list @@ -2087,3 +2087,5 @@ fuzzy(0-1,0-625) == 1466638-1.html 1466638-1-ref.html test-pref(layout.css.contain.enabled,true) == 1483946.html 1483946-ref.html test-pref(layout.css.visited_links_enabled,false) == 1488155.html 1488155-ref.html == 1492660-1.html 1492660-1-ref.html +pref(layout.css.supports-selector.enabled,true) == 1499386.html 1499386-ref.html +pref(layout.css.supports-selector.enabled,false) != 1499386.html 1499386-ref.html diff --git a/modules/libpref/init/StaticPrefList.h b/modules/libpref/init/StaticPrefList.h index f088725d4f49..963101644707 100644 --- a/modules/libpref/init/StaticPrefList.h +++ b/modules/libpref/init/StaticPrefList.h @@ -712,6 +712,18 @@ VARCACHE_PREF( bool, false ) +#ifdef NIGHTLY_BUILD +# define PREF_VALUE true +#else +# define PREF_VALUE false +#endif +VARCACHE_PREF( + "layout.css.supports-selector.enabled", + layout_css_supports_selector_enabled, + bool, PREF_VALUE +) +#undef PREF_VALUE + // Pref to control whether @-moz-document url-prefix() is parsed in content // pages. Only effective when layout.css.moz-document.content.enabled is false. #ifdef EARLY_BETA_OR_EARLIER diff --git a/servo/components/selectors/parser.rs b/servo/components/selectors/parser.rs index 8e26ff63d8ac..fc4b26bb3a32 100644 --- a/servo/components/selectors/parser.rs +++ b/servo/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/servo/components/style/stylesheets/mod.rs b/servo/components/style/stylesheets/mod.rs index 5058ad94e806..81726950665e 100644 --- a/servo/components/style/stylesheets/mod.rs +++ b/servo/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/servo/components/style/stylesheets/rule_parser.rs b/servo/components/style/stylesheets/rule_parser.rs index 683378b9b96b..aa449c70e934 100644 --- a/servo/components/style/stylesheets/rule_parser.rs +++ b/servo/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/servo/components/style/stylesheets/stylesheet.rs b/servo/components/style/stylesheets/stylesheet.rs index e1359cb3722d..ac1d810a05bb 100644 --- a/servo/components/style/stylesheets/stylesheet.rs +++ b/servo/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/servo/components/style/stylesheets/supports_rule.rs b/servo/components/style/stylesheets/supports_rule.rs index 2851d8914dd1..a91a3ea3d171 100644 --- a/servo/components/style/stylesheets/supports_rule.rs +++ b/servo/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() } } diff --git a/servo/ports/geckolib/glue.rs b/servo/ports/geckolib/glue.rs index 0569ecd199ad..63fe0f7759aa 100644 --- a/servo/ports/geckolib/glue.rs +++ b/servo/ports/geckolib/glue.rs @@ -4499,24 +4499,26 @@ pub extern "C" fn Servo_CSSSupports(cond: *const nsACString) -> bool { let condition = unsafe { cond.as_ref().unwrap().as_str_unchecked() }; let mut input = ParserInput::new(&condition); let mut input = Parser::new(&mut input); - let cond = input.parse_entirely(|i| parse_condition_or_declaration(i)); - if let Ok(cond) = cond { - let url_data = unsafe { dummy_url_data() }; - // NOTE(emilio): The supports API is not associated to any stylesheet, - // so the fact that there is no namespace map here is fine. - let context = ParserContext::new_for_cssom( - url_data, - Some(CssRuleType::Style), - ParsingMode::DEFAULT, - QuirksMode::NoQuirks, - None, - None, - ); + let cond = match input.parse_entirely(parse_condition_or_declaration) { + Ok(c) => c, + Err(..) => return false, + }; - cond.eval(&context) - } else { - false - } + let url_data = unsafe { dummy_url_data() }; + + // NOTE(emilio): The supports API is not associated to any stylesheet, + // so the fact that there is no namespace map here is fine. + let context = ParserContext::new_for_cssom( + url_data, + Some(CssRuleType::Style), + ParsingMode::DEFAULT, + QuirksMode::NoQuirks, + None, + None, + ); + + let namespaces = Default::default(); + cond.eval(&context, &namespaces) } #[no_mangle] diff --git a/testing/web-platform/tests/css/css-conditional/at-supports-040.html b/testing/web-platform/tests/css/css-conditional/at-supports-040.html new file mode 100644 index 000000000000..6c4a58f346ba --- /dev/null +++ b/testing/web-platform/tests/css/css-conditional/at-supports-040.html @@ -0,0 +1,18 @@ + +CSS Conditional Test: @supports selector() with pseudo-elements. + + + + + +

Test passes if there is a filled green square and no red.

+
diff --git a/testing/web-platform/tests/css/css-conditional/at-supports-041.html b/testing/web-platform/tests/css/css-conditional/at-supports-041.html new file mode 100644 index 000000000000..804fb3c38353 --- /dev/null +++ b/testing/web-platform/tests/css/css-conditional/at-supports-041.html @@ -0,0 +1,18 @@ + +CSS Conditional Test: @supports selector() with -webkit- unknown pseudo-elements and negation. + + + + + +

Test passes if there is a filled green square and no red.

+
diff --git a/testing/web-platform/tests/css/css-conditional/at-supports-042.html b/testing/web-platform/tests/css/css-conditional/at-supports-042.html new file mode 100644 index 000000000000..47241f37a357 --- /dev/null +++ b/testing/web-platform/tests/css/css-conditional/at-supports-042.html @@ -0,0 +1,18 @@ + +CSS Conditional Test: @supports selector() with multiple selectors doesn't work. + + + + + +

Test passes if there is a filled green square and no red.

+
diff --git a/testing/web-platform/tests/css/cssom/CSS.html b/testing/web-platform/tests/css/cssom/CSS.html index fd2966a104c0..7d558f04466f 100644 --- a/testing/web-platform/tests/css/cssom/CSS.html +++ b/testing/web-platform/tests/css/cssom/CSS.html @@ -35,4 +35,12 @@ assert_equals(CSS.supports("width", "blah"), false, "CSS.supports: two argument form fails for invalid value"); assert_equals(CSS.supports("--foo", "blah"), true, "CSS.supports: two argument form succeeds for custom property"); }, "CSS.supports, two argument form"); + test(function () { + assert_equals(CSS.supports("selector(div)"), true, "CSS.supports: selector() function accepts a selector"); + assert_equals(CSS.supports("selector(div, div)"), false, "CSS.supports: selector() function doesn't accept a selector list"); + assert_equals(CSS.supports("selector(::-webkit-unknown-pseudo-element)"), false, "CSS.supports: selector() function rejects unknown webkit pseudo-elements."); + assert_equals(CSS.supports("selector(::before)"), true, "CSS.supports: selector() function accepts known pseudo-elements"); + assert_equals(CSS.supports("selector(div + .c)"), true, "CSS.supports: selector() with simple combinators"); + assert_equals(CSS.supports("selector(div | .c)"), false, "CSS.supports: selector() with unknown combinators"); + }, "CSS.supports, selector function");