diff options
Diffstat (limited to 'servo/components/style/stylesheets/supports_rule.rs')
-rw-r--r-- | servo/components/style/stylesheets/supports_rule.rs | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/servo/components/style/stylesheets/supports_rule.rs b/servo/components/style/stylesheets/supports_rule.rs new file mode 100644 index 0000000000..14b9afa18e --- /dev/null +++ b/servo/components/style/stylesheets/supports_rule.rs @@ -0,0 +1,433 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! [@supports rules](https://drafts.csswg.org/css-conditional-3/#at-supports) + +use crate::font_face::{FontFaceSourceFormatKeyword, FontFaceSourceTechFlags}; +use crate::parser::ParserContext; +use crate::properties::{PropertyDeclaration, PropertyId, SourcePropertyDeclaration}; +use crate::selector_parser::{SelectorImpl, SelectorParser}; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::{CssRuleType, CssRules, Namespaces}; +use cssparser::parse_important; +use cssparser::{Delimiter, Parser, SourceLocation, Token}; +use cssparser::{ParseError as CssParseError, ParserInput}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use selectors::parser::{Selector, SelectorParseErrorKind}; +use servo_arc::Arc; +use std::ffi::{CStr, CString}; +use std::fmt::{self, Write}; +use std::str; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// An [`@supports`][supports] rule. +/// +/// [supports]: https://drafts.csswg.org/css-conditional-3/#at-supports +#[derive(Debug, ToShmem)] +pub struct SupportsRule { + /// The parsed condition + pub condition: SupportsCondition, + /// Child rules + pub rules: Arc<Locked<CssRules>>, + /// The result of evaluating the condition + pub enabled: bool, + /// The line and column of the rule's source code. + pub source_location: SourceLocation, +} + +impl SupportsRule { + /// Measure heap usage. + #[cfg(feature = "gecko")] + pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + // Measurement of other fields may be added later. + self.rules.unconditional_shallow_size_of(ops) + + self.rules.read_with(guard).size_of(guard, ops) + } +} + +impl ToCssWithGuard for SupportsRule { + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@supports ")?; + self.condition.to_css(&mut CssWriter::new(dest))?; + self.rules.read_with(guard).to_css_block(guard, dest) + } +} + +impl DeepCloneWithLock for SupportsRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + let rules = self.rules.read_with(guard); + SupportsRule { + condition: self.condition.clone(), + rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))), + enabled: self.enabled, + source_location: self.source_location.clone(), + } + } +} + +/// An @supports condition +/// +/// <https://drafts.csswg.org/css-conditional-3/#at-supports> +#[derive(Clone, Debug, ToShmem)] +pub enum SupportsCondition { + /// `not (condition)` + Not(Box<SupportsCondition>), + /// `(condition)` + Parenthesized(Box<SupportsCondition>), + /// `(condition) and (condition) and (condition) ..` + And(Vec<SupportsCondition>), + /// `(condition) or (condition) or (condition) ..` + Or(Vec<SupportsCondition>), + /// `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. + MozBoolPref(CString), + /// `font-format(<font-format>)` + FontFormat(FontFaceSourceFormatKeyword), + /// `font-tech(<font-tech>)` + FontTech(FontFaceSourceTechFlags), + /// `(any tokens)` or `func(any tokens)` + FutureSyntax(String), +} + +impl SupportsCondition { + /// Parse a condition + /// + /// <https://drafts.csswg.org/css-conditional/#supports_condition> + pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("not")).is_ok() { + let inner = SupportsCondition::parse_in_parens(input)?; + return Ok(SupportsCondition::Not(Box::new(inner))); + } + + let in_parens = SupportsCondition::parse_in_parens(input)?; + + let location = input.current_source_location(); + let (keyword, wrapper) = match input.next() { + // End of input + Err(..) => return Ok(in_parens), + Ok(&Token::Ident(ref ident)) => { + match_ignore_ascii_case! { &ident, + "and" => ("and", SupportsCondition::And as fn(_) -> _), + "or" => ("or", SupportsCondition::Or as fn(_) -> _), + _ => return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))) + } + }, + Ok(t) => return Err(location.new_unexpected_token_error(t.clone())), + }; + + let mut conditions = Vec::with_capacity(2); + conditions.push(in_parens); + loop { + conditions.push(SupportsCondition::parse_in_parens(input)?); + if input + .try_parse(|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)); + } + } + } + + /// 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() + ))) + }, + "font-format" if static_prefs::pref!("layout.css.font-tech.enabled") => { + let kw = FontFaceSourceFormatKeyword::parse(input)?; + Ok(SupportsCondition::FontFormat(kw)) + }, + "font-tech" if static_prefs::pref!("layout.css.font-tech.enabled") => { + let flag = FontFaceSourceTechFlags::parse_one(input)?; + Ok(SupportsCondition::FontTech(flag)) + }, + _ => { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + } + } + + /// <https://drafts.csswg.org/css-conditional-3/#supports_condition_in_parens> + fn parse_in_parens<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + // 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_parse(Parser::expect_whitespace).is_ok() {} + let pos = input.position(); + let location = input.current_source_location(); + match *input.next()? { + Token::ParenthesisBlock => { + let nested = input + .try_parse(|input| input.parse_nested_block(parse_condition_or_declaration)); + if nested.is_ok() { + return nested; + } + }, + Token::Function(ref ident) => { + let ident = ident.clone(); + let nested = input.try_parse(|input| { + input.parse_nested_block(|input| { + SupportsCondition::parse_functional(&ident, input) + }) + }); + if nested.is_ok() { + return nested; + } + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + 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, namespaces: &Namespaces) -> bool { + match *self { + 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::FontFormat(ref format) => eval_font_format(format), + SupportsCondition::FontTech(ref tech) => eval_font_tech(tech), + SupportsCondition::FutureSyntax(_) => false, + } + } +} + +#[cfg(feature = "gecko")] +fn eval_moz_bool_pref(name: &CStr, cx: &ParserContext) -> bool { + use crate::gecko_bindings::bindings; + if !cx.in_ua_or_chrome_sheet() { + return false; + } + unsafe { bindings::Gecko_GetBoolPrefValue(name.as_ptr()) } +} + +fn eval_font_format(kw: &FontFaceSourceFormatKeyword) -> bool { + use crate::gecko_bindings::bindings; + unsafe { bindings::Gecko_IsFontFormatSupported(*kw) } +} + +fn eval_font_tech(flag: &FontFaceSourceTechFlags) -> bool { + use crate::gecko_bindings::bindings; + unsafe { bindings::Gecko_IsFontTechSupported(*flag) } +} + +#[cfg(feature = "servo")] +fn eval_moz_bool_pref(_: &CStr, _: &ParserContext) -> bool { + false +} + +/// supports_condition | declaration +/// <https://drafts.csswg.org/css-conditional/#dom-css-supports-conditiontext-conditiontext> +pub fn parse_condition_or_declaration<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<SupportsCondition, ParseError<'i>> { + if let Ok(condition) = input.try_parse(SupportsCondition::parse) { + Ok(SupportsCondition::Parenthesized(Box::new(condition))) + } else { + Declaration::parse(input).map(SupportsCondition::Declaration) + } +} + +impl ToCss for SupportsCondition { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + SupportsCondition::Not(ref cond) => { + dest.write_str("not ")?; + cond.to_css(dest) + }, + 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; + cond.to_css(dest)?; + } + Ok(()) + }, + SupportsCondition::Or(ref vec) => { + let mut first = true; + for cond in vec { + if !first { + dest.write_str(" or ")?; + } + first = false; + cond.to_css(dest)?; + } + Ok(()) + }, + SupportsCondition::Declaration(ref decl) => { + dest.write_str("(")?; + 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 = + str::from_utf8(name.as_bytes()).expect("Should be parsed from valid UTF-8"); + name.to_css(dest)?; + dest.write_str(")") + }, + SupportsCondition::FontFormat(ref kw) => { + dest.write_str("font-format(")?; + kw.to_css(dest)?; + dest.write_str(")") + }, + SupportsCondition::FontTech(ref flag) => { + dest.write_str("font-tech(")?; + flag.to_css(dest)?; + dest.write_str(")") + }, + SupportsCondition::FutureSyntax(ref s) => dest.write_str(&s), + } + } +} + +#[derive(Clone, Debug, ToShmem)] +/// 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 { + 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: context.url_data, + for_supports_rule: true, + }; + + Selector::<SelectorImpl>::parse(&parser, input) + .map_err(|_| input.new_custom_error(()))?; + + Ok(()) + }) + .is_ok() + } +} + +#[derive(Clone, Debug, ToShmem)] +/// A possibly-invalid property declaration +pub struct Declaration(pub String); + +impl ToCss for Declaration { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(&self.0) + } +} + +/// <https://drafts.csswg.org/css-syntax-3/#typedef-any-value> +fn consume_any_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> { + input.expect_no_error_token().map_err(|err| err.into()) +} + +impl Declaration { + /// Parse a declaration + pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Declaration, ParseError<'i>> { + let pos = input.position(); + input.expect_ident()?; + input.expect_colon()?; + consume_any_value(input)?; + Ok(Declaration(input.slice_from(pos).to_owned())) + } + + /// Determine if a declaration parses + /// + /// <https://drafts.csswg.org/css-conditional-3/#support-definition> + pub fn eval(&self, context: &ParserContext) -> bool { + debug_assert_eq!(context.rule_type(), CssRuleType::Style); + + 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(); + + 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(parse_important); + Ok(()) + }) + .is_ok() + } +} |