style: Implement @supports selector() syntax.

This implements the selector(<complex-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
This commit is contained in:
Emilio Cobos Álvarez 2018-10-17 12:08:14 +00:00
parent 4e356b4bb9
commit edc40ce320
No known key found for this signature in database
GPG key ID: 056B727BB9C1027C
5 changed files with 157 additions and 65 deletions

View file

@ -259,8 +259,13 @@ where
Impl: SelectorImpl, Impl: SelectorImpl,
{ {
let location = input.current_source_location(); let location = input.current_source_location();
let selector = Selector::parse(parser, input)?; let selector = parse_selector(parser, input)?;
// Ensure they're actually all compound selectors.
// 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()) { if selector.iter_raw_match_order().any(|s| s.is_combinator()) {
return Err(location.new_custom_error(SelectorParseErrorKind::NonCompoundSelector)); return Err(location.new_custom_error(SelectorParseErrorKind::NonCompoundSelector));
} }
@ -1397,6 +1402,7 @@ where
impl<Impl: SelectorImpl> Selector<Impl> { impl<Impl: SelectorImpl> Selector<Impl> {
/// Parse a selector, without any pseudo-element. /// Parse a selector, without any pseudo-element.
#[inline]
pub fn parse<'i, 't, P>( pub fn parse<'i, 't, P>(
parser: &P, parser: &P,
input: &mut CssParser<'i, 't>, input: &mut CssParser<'i, 't>,
@ -1404,12 +1410,7 @@ impl<Impl: SelectorImpl> Selector<Impl> {
where where
P: Parser<'i, Impl = Impl>, P: Parser<'i, Impl = Impl>,
{ {
let selector = parse_selector(parser, input)?; parse_selector(parser, input)
if selector.has_pseudo_element() {
let e = SelectorParseErrorKind::PseudoElementInComplexSelector;
return Err(input.new_custom_error(e));
}
Ok(selector)
} }
} }

View file

@ -277,7 +277,6 @@ impl CssRule {
// nested rules are in the body state // nested rules are in the body state
let mut rule_parser = TopLevelRuleParser { let mut rule_parser = TopLevelRuleParser {
stylesheet_origin: parent_stylesheet_contents.origin,
context, context,
shared_lock: &shared_lock, shared_lock: &shared_lock,
loader, loader,

View file

@ -19,7 +19,7 @@ use servo_arc::Arc;
use shared_lock::{Locked, SharedRwLock}; use shared_lock::{Locked, SharedRwLock};
use str::starts_with_ignore_ascii_case; use str::starts_with_ignore_ascii_case;
use style_traits::{ParseError, StyleParseErrorKind}; 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::{DocumentRule, FontFeatureValuesRule, KeyframesRule, MediaRule};
use stylesheets::{NamespaceRule, PageRule, StyleRule, SupportsRule, ViewportRule}; use stylesheets::{NamespaceRule, PageRule, StyleRule, SupportsRule, ViewportRule};
use stylesheets::document_rule::DocumentCondition; use stylesheets::document_rule::DocumentCondition;
@ -41,8 +41,6 @@ pub struct InsertRuleContext<'a> {
/// The parser for the top-level rules in a stylesheet. /// The parser for the top-level rules in a stylesheet.
pub struct TopLevelRuleParser<'a> { 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. /// A reference to the lock we need to use to create rules.
pub shared_lock: &'a SharedRwLock, pub shared_lock: &'a SharedRwLock,
/// A reference to a stylesheet loader if applicable, for `@import` rules. /// A reference to a stylesheet loader if applicable, for `@import` rules.
@ -69,7 +67,6 @@ pub struct TopLevelRuleParser<'a> {
impl<'b> TopLevelRuleParser<'b> { impl<'b> TopLevelRuleParser<'b> {
fn nested<'a: 'b>(&'a self) -> NestedRuleParser<'a, 'b> { fn nested<'a: 'b>(&'a self) -> NestedRuleParser<'a, 'b> {
NestedRuleParser { NestedRuleParser {
stylesheet_origin: self.stylesheet_origin,
shared_lock: self.shared_lock, shared_lock: self.shared_lock,
context: &self.context, context: &self.context,
namespaces: &self.namespaces, namespaces: &self.namespaces,
@ -325,7 +322,6 @@ impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser<'a> {
#[derive(Clone)] // shallow, relatively cheap .clone #[derive(Clone)] // shallow, relatively cheap .clone
struct NestedRuleParser<'a, 'b: 'a> { struct NestedRuleParser<'a, 'b: 'a> {
stylesheet_origin: Origin,
shared_lock: &'a SharedRwLock, shared_lock: &'a SharedRwLock,
context: &'a ParserContext<'b>, context: &'a ParserContext<'b>,
namespaces: &'a Namespaces, 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 context = ParserContext::new_with_rule_type(self.context, rule_type, self.namespaces);
let nested_parser = NestedRuleParser { let nested_parser = NestedRuleParser {
stylesheet_origin: self.stylesheet_origin,
shared_lock: self.shared_lock, shared_lock: self.shared_lock,
context: &context, context: &context,
namespaces: self.namespaces, namespaces: self.namespaces,
@ -501,7 +496,7 @@ impl<'a, 'b, 'i> AtRuleParser<'i> for NestedRuleParser<'a, 'b> {
self.namespaces, 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( Ok(CssRule::Supports(Arc::new(self.shared_lock.wrap(
SupportsRule { SupportsRule {
condition, condition,
@ -577,7 +572,7 @@ impl<'a, 'b, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'b> {
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
) -> Result<Self::Prelude, ParseError<'i>> { ) -> Result<Self::Prelude, ParseError<'i>> {
let selector_parser = SelectorParser { let selector_parser = SelectorParser {
stylesheet_origin: self.stylesheet_origin, stylesheet_origin: self.context.stylesheet_origin,
namespaces: self.namespaces, namespaces: self.namespaces,
url_data: Some(self.context.url_data), url_data: Some(self.context.url_data),
}; };

View file

@ -374,7 +374,6 @@ impl Stylesheet {
); );
let rule_parser = TopLevelRuleParser { let rule_parser = TopLevelRuleParser {
stylesheet_origin: origin,
shared_lock, shared_lock,
loader: stylesheet_loader, loader: stylesheet_loader,
context, context,

View file

@ -12,6 +12,8 @@ use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
use parser::ParserContext; use parser::ParserContext;
use properties::{PropertyDeclaration, PropertyId, SourcePropertyDeclaration}; use properties::{PropertyDeclaration, PropertyId, SourcePropertyDeclaration};
use selectors::parser::SelectorParseErrorKind; use selectors::parser::SelectorParseErrorKind;
use selector_parser::{SelectorImpl, SelectorParser};
use selectors::parser::Selector;
use servo_arc::Arc; use servo_arc::Arc;
use shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; use shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked};
use shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; use shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
@ -19,8 +21,8 @@ use std::ffi::{CStr, CString};
use std::fmt::{self, Write}; use std::fmt::{self, Write};
use std::str; use std::str;
use str::CssStringWriter; use str::CssStringWriter;
use style_traits::{CssWriter, ParseError, ToCss}; use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
use stylesheets::{CssRuleType, CssRules}; use stylesheets::{CssRuleType, CssRules, Namespaces};
/// An [`@supports`][supports] rule. /// An [`@supports`][supports] rule.
/// ///
@ -87,6 +89,8 @@ pub enum SupportsCondition {
Or(Vec<SupportsCondition>), Or(Vec<SupportsCondition>),
/// `property-ident: value` (value can be any tokens) /// `property-ident: value` (value can be any tokens)
Declaration(Declaration), Declaration(Declaration),
/// A `selector()` function.
Selector(RawSelector),
/// `-moz-bool-pref("pref-name")` /// `-moz-bool-pref("pref-name")`
/// Since we need to pass it through FFI to get the pref value, /// Since we need to pass it through FFI to get the pref value,
/// we store it as CString directly. /// we store it as CString directly.
@ -99,8 +103,8 @@ impl SupportsCondition {
/// Parse a condition /// Parse a condition
/// ///
/// <https://drafts.csswg.org/css-conditional/#supports_condition> /// <https://drafts.csswg.org/css-conditional/#supports_condition>
pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<SupportsCondition, ParseError<'i>> { pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
if let Ok(_) = input.try(|i| i.expect_ident_matching("not")) { if input.try(|i| i.expect_ident_matching("not")).is_ok() {
let inner = SupportsCondition::parse_in_parens(input)?; let inner = SupportsCondition::parse_in_parens(input)?;
return Ok(SupportsCondition::Not(Box::new(inner))); return Ok(SupportsCondition::Not(Box::new(inner)));
} }
@ -109,10 +113,8 @@ impl SupportsCondition {
let location = input.current_source_location(); let location = input.current_source_location();
let (keyword, wrapper) = match input.next() { let (keyword, wrapper) = match input.next() {
Err(_) => { // End of input
// End of input Err(..) => return Ok(in_parens),
return Ok(in_parens);
},
Ok(&Token::Ident(ref ident)) => { Ok(&Token::Ident(ref ident)) => {
match_ignore_ascii_case! { &ident, match_ignore_ascii_case! { &ident,
"and" => ("and", SupportsCondition::And as fn(_) -> _), "and" => ("and", SupportsCondition::And as fn(_) -> _),
@ -132,17 +134,48 @@ impl SupportsCondition {
.is_err() .is_err()
{ {
// Did not find the expected keyword. // Did not find the expected keyword.
// If we found some other token, // If we found some other token, it will be rejected by
// it will be rejected by `Parser::parse_entirely` somewhere up the stack. // `Parser::parse_entirely` somewhere up the stack.
return Ok(wrapper(conditions)); return Ok(wrapper(conditions));
} }
} }
} }
/// Parses a functional supports condition.
fn parse_functional<'i, 't>(
function: &str,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
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))
}
}
}
/// <https://drafts.csswg.org/css-conditional-3/#supports_condition_in_parens> /// <https://drafts.csswg.org/css-conditional-3/#supports_condition_in_parens>
fn parse_in_parens<'i, 't>( fn parse_in_parens<'i, 't>(
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
) -> Result<SupportsCondition, ParseError<'i>> { ) -> Result<Self, ParseError<'i>> {
// Whitespace is normally taken care of in `Parser::next`, // Whitespace is normally taken care of in `Parser::next`,
// but we want to not include it in `pos` for the SupportsCondition::FutureSyntax cases. // but we want to not include it in `pos` for the SupportsCondition::FutureSyntax cases.
while input.try(Parser::expect_whitespace).is_ok() {} while input.try(Parser::expect_whitespace).is_ok() {}
@ -151,46 +184,45 @@ impl SupportsCondition {
// FIXME: remove clone() when lifetimes are non-lexical // FIXME: remove clone() when lifetimes are non-lexical
match input.next()?.clone() { match input.next()?.clone() {
Token::ParenthesisBlock => { Token::ParenthesisBlock => {
let nested = input let nested = input.try(|input| {
.try(|input| input.parse_nested_block(|i| parse_condition_or_declaration(i))); input.parse_nested_block(parse_condition_or_declaration)
});
if nested.is_ok() { if nested.is_ok() {
return nested; return nested;
} }
}, },
Token::Function(ident) => { Token::Function(ident) => {
// Although this is an internal syntax, it is not necessary to check let nested = input.try(|input| {
// parsing context as far as we accept any unexpected token as future input.parse_nested_block(|input| {
// syntax, and evaluate it to false when not in chrome / ua sheet. SupportsCondition::parse_functional(&ident, input)
// 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| { if nested.is_ok() {
i.parse_nested_block(|i| { return nested;
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));
}
} }
}, },
t => return Err(location.new_unexpected_token_error(t)), 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( Ok(SupportsCondition::FutureSyntax(
input.slice_from(pos).to_owned(), input.slice_from(pos).to_owned(),
)) ))
} }
/// Evaluate a supports condition /// Evaluate a supports condition
pub fn eval(&self, cx: &ParserContext) -> bool { pub fn eval(
&self,
cx: &ParserContext,
namespaces: &Namespaces,
) -> bool {
match *self { match *self {
SupportsCondition::Not(ref cond) => !cond.eval(cx), SupportsCondition::Not(ref cond) => !cond.eval(cx, namespaces),
SupportsCondition::Parenthesized(ref cond) => cond.eval(cx), SupportsCondition::Parenthesized(ref cond) => cond.eval(cx, namespaces),
SupportsCondition::And(ref vec) => vec.iter().all(|c| c.eval(cx)), SupportsCondition::And(ref vec) => vec.iter().all(|c| c.eval(cx, namespaces)),
SupportsCondition::Or(ref vec) => vec.iter().any(|c| c.eval(cx)), SupportsCondition::Or(ref vec) => vec.iter().any(|c| c.eval(cx, namespaces)),
SupportsCondition::Declaration(ref decl) => decl.eval(cx), SupportsCondition::Declaration(ref decl) => decl.eval(cx),
SupportsCondition::MozBoolPref(ref name) => eval_moz_bool_pref(name, cx), SupportsCondition::MozBoolPref(ref name) => eval_moz_bool_pref(name, cx),
SupportsCondition::Selector(ref selector) => selector.eval(cx, namespaces),
SupportsCondition::FutureSyntax(_) => false, SupportsCondition::FutureSyntax(_) => false,
} }
} }
@ -265,6 +297,11 @@ impl ToCss for SupportsCondition {
decl.to_css(dest)?; decl.to_css(dest)?;
dest.write_str(")") dest.write_str(")")
}, },
SupportsCondition::Selector(ref selector) => {
dest.write_str("selector(")?;
selector.to_css(dest)?;
dest.write_str(")")
}
SupportsCondition::MozBoolPref(ref name) => { SupportsCondition::MozBoolPref(ref name) => {
dest.write_str("-moz-bool-pref(")?; dest.write_str("-moz-bool-pref(")?;
let name = 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<W>(&self, dest: &mut CssWriter<W>) -> 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::<SelectorImpl>::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)] #[derive(Clone, Debug)]
/// A possibly-invalid property declaration /// A possibly-invalid property declaration
pub struct Declaration(pub String); pub struct Declaration(pub String);
@ -313,21 +412,20 @@ impl Declaration {
let mut input = ParserInput::new(&self.0); let mut input = ParserInput::new(&self.0);
let mut input = Parser::new(&mut input); let mut input = Parser::new(&mut input);
input input.parse_entirely(|input| -> Result<(), CssParseError<()>> {
.parse_entirely(|input| -> Result<(), CssParseError<()>> { let prop = input.expect_ident_cloned().unwrap();
let prop = input.expect_ident_cloned().unwrap(); input.expect_colon().unwrap();
input.expect_colon().unwrap();
let id = let id =
PropertyId::parse(&prop, context).map_err(|_| input.new_custom_error(()))?; PropertyId::parse(&prop, context).map_err(|_| input.new_custom_error(()))?;
let mut declarations = SourcePropertyDeclaration::new(); let mut declarations = SourcePropertyDeclaration::new();
input.parse_until_before(Delimiter::Bang, |input| { input.parse_until_before(Delimiter::Bang, |input| {
PropertyDeclaration::parse_into(&mut declarations, id, &context, input) PropertyDeclaration::parse_into(&mut declarations, id, &context, input)
.map_err(|_| input.new_custom_error(())) .map_err(|_| input.new_custom_error(()))
})?; })?;
let _ = input.try(parse_important); let _ = input.try(parse_important);
Ok(()) Ok(())
}).is_ok() }).is_ok()
} }
} }