diff --git a/components/style/lib.rs b/components/style/lib.rs index 7cb15e22852..45954458a20 100644 --- a/components/style/lib.rs +++ b/components/style/lib.rs @@ -122,6 +122,7 @@ pub mod sequential; pub mod sink; pub mod str; pub mod stylesheets; +pub mod supports; pub mod thread_state; pub mod timer; pub mod traversal; diff --git a/components/style/stylesheets.rs b/components/style/stylesheets.rs index f6ed4ec618e..78867e81862 100644 --- a/components/style/stylesheets.rs +++ b/components/style/stylesheets.rs @@ -28,6 +28,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use style_traits::ToCss; use stylist::FnvHashMap; +use supports::SupportsCondition; use values::specified::url::SpecifiedUrl; use viewport::ViewportRule; @@ -215,6 +216,7 @@ pub enum CssRule { FontFace(Arc>), Viewport(Arc>), Keyframes(Arc>), + Supports(Arc>), } #[allow(missing_docs)] @@ -274,6 +276,7 @@ impl CssRule { CssRule::Keyframes(_) => CssRuleType::Keyframes, CssRule::Namespace(_) => CssRuleType::Namespace, CssRule::Viewport(_) => CssRuleType::Viewport, + CssRule::Supports(_) => CssRuleType::Supports, } } @@ -290,9 +293,10 @@ impl CssRule { /// /// Note that only some types of rules can contain rules. An empty slice is /// used for others. + /// + /// This will not recurse down unsupported @supports rules pub fn with_nested_rules_and_mq(&self, mut f: F) -> R - where F: FnMut(&[CssRule], Option<&MediaList>) -> R - { + where F: FnMut(&[CssRule], Option<&MediaList>) -> R { match *self { CssRule::Import(ref lock) => { let rule = lock.read(); @@ -315,6 +319,16 @@ impl CssRule { let rules = &media_rule.rules.read().0; f(rules, Some(&mq)) } + CssRule::Supports(ref lock) => { + let supports_rule = lock.read(); + let enabled = supports_rule.enabled; + if enabled { + let rules = &supports_rule.rules.read().0; + f(rules, None) + } else { + f(&[], None) + } + } } } @@ -367,6 +381,7 @@ impl ToCss for CssRule { CssRule::Viewport(ref lock) => lock.read().to_css(dest), CssRule::Keyframes(ref lock) => lock.read().to_css(dest), CssRule::Media(ref lock) => lock.read().to_css(dest), + CssRule::Supports(ref lock) => lock.read().to_css(dest), } } } @@ -441,7 +456,12 @@ impl ToCss for KeyframesRule { try!(dest.write_str(&*self.name.to_string())); try!(dest.write_str(" { ")); let iter = self.keyframes.iter(); + let mut first = true; for lock in iter { + if !first { + try!(dest.write_str(" ")); + } + first = false; let keyframe = lock.read(); try!(keyframe.to_css(dest)); } @@ -460,9 +480,34 @@ impl ToCss for MediaRule { // Serialization of MediaRule is not specced. // https://drafts.csswg.org/cssom/#serialize-a-css-rule CSSMediaRule fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { - try!(dest.write_str("@media (")); + try!(dest.write_str("@media ")); try!(self.media_queries.read().to_css(dest)); - try!(dest.write_str(") {")); + try!(dest.write_str(" {")); + for rule in self.rules.read().0.iter() { + try!(dest.write_str(" ")); + try!(rule.to_css(dest)); + } + dest.write_str(" }") + } +} + + +#[derive(Debug)] +/// An @supports rule +pub struct SupportsRule { + /// The parsed condition + pub condition: SupportsCondition, + /// Child rules + pub rules: Arc>, + /// The result of evaluating the condition + pub enabled: bool, +} + +impl ToCss for SupportsRule { + fn to_css(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + try!(dest.write_str("@supports ")); + try!(self.condition.to_css(dest)); + try!(dest.write_str(" {")); for rule in self.rules.read().0.iter() { try!(dest.write_str(" ")); try!(rule.to_css(dest)); @@ -712,6 +757,7 @@ rule_filter! { effective_font_face_rules(FontFace => FontFaceRule), effective_viewport_rules(Viewport => ViewportRule), effective_keyframes_rules(Keyframes => KeyframesRule), + effective_supports_rules(Supports => SupportsRule), } /// The stylesheet loader is the abstraction used to trigger network requests @@ -758,6 +804,8 @@ enum AtRulePrelude { FontFace, /// A @media rule prelude, with its media queries. Media(Arc>), + /// An @supports rule, with its conditional + Supports(SupportsCondition), /// A @viewport rule prelude. Viewport, /// A @keyframes rule, with its animation name. @@ -913,6 +961,10 @@ impl<'a, 'b> AtRuleParser for NestedRuleParser<'a, 'b> { let media_queries = parse_media_query_list(input); Ok(AtRuleType::WithBlock(AtRulePrelude::Media(Arc::new(RwLock::new(media_queries))))) }, + "supports" => { + let cond = SupportsCondition::parse(input)?; + Ok(AtRuleType::WithBlock(AtRulePrelude::Supports(cond))) + }, "font-face" => { Ok(AtRuleType::WithBlock(AtRulePrelude::FontFace)) }, @@ -949,6 +1001,14 @@ impl<'a, 'b> AtRuleParser for NestedRuleParser<'a, 'b> { rules: self.parse_nested_rules(input), })))) } + AtRulePrelude::Supports(cond) => { + let enabled = cond.eval(self.context); + Ok(CssRule::Supports(Arc::new(RwLock::new(SupportsRule { + condition: cond, + rules: self.parse_nested_rules(input), + enabled: enabled, + })))) + } AtRulePrelude::Viewport => { Ok(CssRule::Viewport(Arc::new(RwLock::new( try!(ViewportRule::parse(input, self.context)))))) diff --git a/components/style/stylist.rs b/components/style/stylist.rs index 9c3608ef0c0..e1b6df7f3a1 100644 --- a/components/style/stylist.rs +++ b/components/style/stylist.rs @@ -408,14 +408,15 @@ impl Stylist { fn mq_eval_changed(rules: &[CssRule], before: &Device, after: &Device) -> bool { for rule in rules { - if rule.with_nested_rules_and_mq(|rules, mq| { + let changed = rule.with_nested_rules_and_mq(|rules, mq| { if let Some(mq) = mq { if mq.evaluate(before) != mq.evaluate(after) { return true } } mq_eval_changed(rules, before, after) - }) { + }); + if changed { return true } } diff --git a/components/style/supports.rs b/components/style/supports.rs new file mode 100644 index 00000000000..a373ad58b0b --- /dev/null +++ b/components/style/supports.rs @@ -0,0 +1,227 @@ +/* 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/. */ + +//! [@supports rules](https://drafts.csswg.org/css-conditional-3/#at-supports) + +use cssparser::{parse_important, Parser, Token}; +use parser::ParserContext; +use properties::{PropertyDeclaration, PropertyId}; +use std::fmt; +use style_traits::ToCss; + +#[derive(Debug)] +/// An @supports condition +/// +/// https://drafts.csswg.org/css-conditional-3/#at-supports +pub enum SupportsCondition { + /// `not (condition)` + Not(Box), + /// `(condition)` + Parenthesized(Box), + /// `(condition) and (condition) and (condition) ..` + And(Vec), + /// `(condition) or (condition) or (condition) ..` + Or(Vec), + /// `property-ident: value` (value can be any tokens) + Declaration(Declaration), + /// `(any tokens)` or `func(any tokens)` + FutureSyntax(String), +} + +impl SupportsCondition { + /// Parse a condition + /// + /// https://drafts.csswg.org/css-conditional/#supports_condition + pub fn parse(input: &mut Parser) -> Result { + if let Ok(_) = input.try(|i| i.expect_ident_matching("not")) { + let inner = SupportsCondition::parse_in_parens(input)?; + return Ok(SupportsCondition::Not(Box::new(inner))); + } + + let in_parens = SupportsCondition::parse_in_parens(input)?; + + let (keyword, wrapper) = match input.next() { + Err(()) => { + // End of input + return Ok(SupportsCondition::Parenthesized(Box::new(in_parens))) + } + Ok(Token::Ident(ident)) => { + match_ignore_ascii_case! { ident, + "and" => ("and", SupportsCondition::And as fn(_) -> _), + "or" => ("or", SupportsCondition::Or as fn(_) -> _), + _ => return Err(()) + } + } + _ => return Err(()) + }; + + let mut conditions = Vec::with_capacity(2); + conditions.push(in_parens); + loop { + conditions.push(SupportsCondition::parse_in_parens(input)?); + if input.try(|input| input.expect_ident_matching(keyword)).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. + return Ok(wrapper(conditions)) + } + } + } + + /// https://drafts.csswg.org/css-conditional-3/#supports_condition_in_parens + fn parse_in_parens(input: &mut Parser) -> Result { + let pos = input.position(); + match input.next()? { + Token::ParenthesisBlock => { + input.parse_nested_block(|input| { + // `input.try()` not needed here since the alternative uses `consume_all()`. + parse_condition_or_declaration(input).or_else(|()| { + consume_all(input); + Ok(SupportsCondition::FutureSyntax(input.slice_from(pos).to_owned())) + }) + }) + } + Token::Function(_) => { + input.parse_nested_block(|i| Ok(consume_all(i))).unwrap(); + Ok(SupportsCondition::FutureSyntax(input.slice_from(pos).to_owned())) + } + _ => Err(()) + } + } + + /// Evaluate a supports condition + pub fn eval(&self, cx: &ParserContext) -> 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::Declaration(ref decl) => decl.eval(cx), + SupportsCondition::FutureSyntax(_) => false + } + } +} + +/// supports_condition | declaration +/// https://drafts.csswg.org/css-conditional/#dom-css-supports-conditiontext-conditiontext +pub fn parse_condition_or_declaration(input: &mut Parser) -> Result { + input.try(SupportsCondition::parse).or_else(|()| { + Declaration::parse(input).map(SupportsCondition::Declaration) + }) +} + +impl ToCss for SupportsCondition { + fn to_css(&self, dest: &mut W) -> fmt::Result + where W: fmt::Write { + match *self { + SupportsCondition::Not(ref cond) => { + dest.write_str("not (")?; + cond.to_css(dest)?; + dest.write_str(")") + } + SupportsCondition::Parenthesized(ref cond) => { + dest.write_str("(")?; + cond.to_css(dest)?; + dest.write_str(")") + } + SupportsCondition::And(ref vec) => { + let mut first = true; + for cond in vec { + if !first { + dest.write_str(" and ")?; + } + first = false; + dest.write_str("(")?; + cond.to_css(dest)?; + dest.write_str(")")?; + } + Ok(()) + } + SupportsCondition::Or(ref vec) => { + let mut first = true; + for cond in vec { + if !first { + dest.write_str(" or ")?; + } + first = false; + dest.write_str("(")?; + cond.to_css(dest)?; + dest.write_str(")")?; + } + Ok(()) + } + SupportsCondition::Declaration(ref decl) => decl.to_css(dest), + SupportsCondition::FutureSyntax(ref s) => dest.write_str(&s), + } + } +} + +#[derive(Debug)] +/// A possibly-invalid property declaration +pub struct Declaration { + /// The property name + prop: String, + /// The property value + val: String, +} + +impl ToCss for Declaration { + fn to_css(&self, dest: &mut W) -> fmt::Result + where W: fmt::Write { + dest.write_str(&self.prop)?; + dest.write_str(":")?; + // no space, the `val` already contains any possible spaces + dest.write_str(&self.val) + } +} + +/// Slurps up input till exhausted, return string from source position +fn parse_anything(input: &mut Parser) -> String { + let pos = input.position(); + consume_all(input); + input.slice_from(pos).to_owned() +} + +/// consume input till done +fn consume_all(input: &mut Parser) { + while let Ok(_) = input.next() {} +} + +impl Declaration { + /// Parse a declaration + pub fn parse(input: &mut Parser) -> Result { + let prop = input.expect_ident()?.into_owned(); + input.expect_colon()?; + let val = parse_anything(input); + Ok(Declaration { prop: prop, val: val }) + } + + /// Determine if a declaration parses + /// + /// https://drafts.csswg.org/css-conditional-3/#support-definition + pub fn eval(&self, cx: &ParserContext) -> bool { + use properties::PropertyDeclarationParseResult::*; + let id = if let Ok(id) = PropertyId::parse((&*self.prop).into()) { + id + } else { + return false + }; + let mut input = Parser::new(&self.val); + let mut list = Vec::new(); + let res = PropertyDeclaration::parse(id, cx, &mut input, + &mut list, /* in_keyframe */ false); + let _ = input.try(parse_important); + if !input.is_exhausted() { + return false; + } + match res { + UnknownProperty => false, + ExperimentalProperty => false, // only happens for experimental props + // that haven't been enabled + InvalidValue => false, + AnimationPropertyInKeyframeBlock => unreachable!(), + ValidOrIgnoredDeclaration => true, + } + } +} diff --git a/rust-stable-version b/rust-stable-version index 0eed1a29efd..feaae22bac7 100644 --- a/rust-stable-version +++ b/rust-stable-version @@ -1 +1 @@ -1.12.0 +1.13.0