1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4 
5 //! [@supports rules](https://drafts.csswg.org/css-conditional-3/#at-supports)
6 
7 use crate::parser::ParserContext;
8 use crate::properties::{PropertyDeclaration, PropertyId, SourcePropertyDeclaration};
9 use crate::selector_parser::{SelectorImpl, SelectorParser};
10 use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked};
11 use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
12 use crate::str::CssStringWriter;
13 use crate::stylesheets::{CssRuleType, CssRules, Namespaces};
14 use cssparser::parse_important;
15 use cssparser::{Delimiter, Parser, SourceLocation, Token};
16 use cssparser::{ParseError as CssParseError, ParserInput};
17 #[cfg(feature = "gecko")]
18 use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
19 use selectors::parser::{Selector, SelectorParseErrorKind};
20 use servo_arc::Arc;
21 use std::ffi::{CStr, CString};
22 use std::fmt::{self, Write};
23 use std::str;
24 use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
25 
26 /// An [`@supports`][supports] rule.
27 ///
28 /// [supports]: https://drafts.csswg.org/css-conditional-3/#at-supports
29 #[derive(Debug, ToShmem)]
30 pub struct SupportsRule {
31     /// The parsed condition
32     pub condition: SupportsCondition,
33     /// Child rules
34     pub rules: Arc<Locked<CssRules>>,
35     /// The result of evaluating the condition
36     pub enabled: bool,
37     /// The line and column of the rule's source code.
38     pub source_location: SourceLocation,
39 }
40 
41 impl SupportsRule {
42     /// Measure heap usage.
43     #[cfg(feature = "gecko")]
size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize44     pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
45         // Measurement of other fields may be added later.
46         self.rules.unconditional_shallow_size_of(ops) +
47             self.rules.read_with(guard).size_of(guard, ops)
48     }
49 }
50 
51 impl ToCssWithGuard for SupportsRule {
to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result52     fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
53         dest.write_str("@supports ")?;
54         self.condition.to_css(&mut CssWriter::new(dest))?;
55         self.rules.read_with(guard).to_css_block(guard, dest)
56     }
57 }
58 
59 impl DeepCloneWithLock for SupportsRule {
deep_clone_with_lock( &self, lock: &SharedRwLock, guard: &SharedRwLockReadGuard, params: &DeepCloneParams, ) -> Self60     fn deep_clone_with_lock(
61         &self,
62         lock: &SharedRwLock,
63         guard: &SharedRwLockReadGuard,
64         params: &DeepCloneParams,
65     ) -> Self {
66         let rules = self.rules.read_with(guard);
67         SupportsRule {
68             condition: self.condition.clone(),
69             rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))),
70             enabled: self.enabled,
71             source_location: self.source_location.clone(),
72         }
73     }
74 }
75 
76 /// An @supports condition
77 ///
78 /// <https://drafts.csswg.org/css-conditional-3/#at-supports>
79 #[derive(Clone, Debug, ToShmem)]
80 pub enum SupportsCondition {
81     /// `not (condition)`
82     Not(Box<SupportsCondition>),
83     /// `(condition)`
84     Parenthesized(Box<SupportsCondition>),
85     /// `(condition) and (condition) and (condition) ..`
86     And(Vec<SupportsCondition>),
87     /// `(condition) or (condition) or (condition) ..`
88     Or(Vec<SupportsCondition>),
89     /// `property-ident: value` (value can be any tokens)
90     Declaration(Declaration),
91     /// A `selector()` function.
92     Selector(RawSelector),
93     /// `-moz-bool-pref("pref-name")`
94     /// Since we need to pass it through FFI to get the pref value,
95     /// we store it as CString directly.
96     MozBoolPref(CString),
97     /// `(any tokens)` or `func(any tokens)`
98     FutureSyntax(String),
99 }
100 
101 impl SupportsCondition {
102     /// Parse a condition
103     ///
104     /// <https://drafts.csswg.org/css-conditional/#supports_condition>
parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>>105     pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
106         if input.try_parse(|i| i.expect_ident_matching("not")).is_ok() {
107             let inner = SupportsCondition::parse_in_parens(input)?;
108             return Ok(SupportsCondition::Not(Box::new(inner)));
109         }
110 
111         let in_parens = SupportsCondition::parse_in_parens(input)?;
112 
113         let location = input.current_source_location();
114         let (keyword, wrapper) = match input.next() {
115             // End of input
116             Err(..) => return Ok(in_parens),
117             Ok(&Token::Ident(ref ident)) => {
118                 match_ignore_ascii_case! { &ident,
119                     "and" => ("and", SupportsCondition::And as fn(_) -> _),
120                     "or" => ("or", SupportsCondition::Or as fn(_) -> _),
121                     _ => return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())))
122                 }
123             },
124             Ok(t) => return Err(location.new_unexpected_token_error(t.clone())),
125         };
126 
127         let mut conditions = Vec::with_capacity(2);
128         conditions.push(in_parens);
129         loop {
130             conditions.push(SupportsCondition::parse_in_parens(input)?);
131             if input
132                 .try_parse(|input| input.expect_ident_matching(keyword))
133                 .is_err()
134             {
135                 // Did not find the expected keyword.
136                 // If we found some other token, it will be rejected by
137                 // `Parser::parse_entirely` somewhere up the stack.
138                 return Ok(wrapper(conditions));
139             }
140         }
141     }
142 
143     /// Parses a functional supports condition.
parse_functional<'i, 't>( function: &str, input: &mut Parser<'i, 't>, ) -> Result<Self, ParseError<'i>>144     fn parse_functional<'i, 't>(
145         function: &str,
146         input: &mut Parser<'i, 't>,
147     ) -> Result<Self, ParseError<'i>> {
148         match_ignore_ascii_case! { function,
149             // Although this is an internal syntax, it is not necessary
150             // to check parsing context as far as we accept any
151             // unexpected token as future syntax, and evaluate it to
152             // false when not in chrome / ua sheet.
153             // See https://drafts.csswg.org/css-conditional-3/#general_enclosed
154             "-moz-bool-pref" => {
155                 let name = {
156                     let name = input.expect_string()?;
157                     CString::new(name.as_bytes())
158                 }.map_err(|_| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))?;
159                 Ok(SupportsCondition::MozBoolPref(name))
160             },
161             "selector" => {
162                 let pos = input.position();
163                 consume_any_value(input)?;
164                 Ok(SupportsCondition::Selector(RawSelector(
165                     input.slice_from(pos).to_owned()
166                 )))
167             },
168             _ => {
169                 Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
170             },
171         }
172     }
173 
174     /// <https://drafts.csswg.org/css-conditional-3/#supports_condition_in_parens>
parse_in_parens<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>>175     fn parse_in_parens<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
176         // Whitespace is normally taken care of in `Parser::next`,
177         // but we want to not include it in `pos` for the SupportsCondition::FutureSyntax cases.
178         while input.try_parse(Parser::expect_whitespace).is_ok() {}
179         let pos = input.position();
180         let location = input.current_source_location();
181         match *input.next()? {
182             Token::ParenthesisBlock => {
183                 let nested = input
184                     .try_parse(|input| input.parse_nested_block(parse_condition_or_declaration));
185                 if nested.is_ok() {
186                     return nested;
187                 }
188             },
189             Token::Function(ref ident) => {
190                 let ident = ident.clone();
191                 let nested = input.try_parse(|input| {
192                     input.parse_nested_block(|input| {
193                         SupportsCondition::parse_functional(&ident, input)
194                     })
195                 });
196                 if nested.is_ok() {
197                     return nested;
198                 }
199             },
200             ref t => return Err(location.new_unexpected_token_error(t.clone())),
201         }
202         input.parse_nested_block(consume_any_value)?;
203         Ok(SupportsCondition::FutureSyntax(
204             input.slice_from(pos).to_owned(),
205         ))
206     }
207 
208     /// Evaluate a supports condition
eval(&self, cx: &ParserContext, namespaces: &Namespaces) -> bool209     pub fn eval(&self, cx: &ParserContext, namespaces: &Namespaces) -> bool {
210         match *self {
211             SupportsCondition::Not(ref cond) => !cond.eval(cx, namespaces),
212             SupportsCondition::Parenthesized(ref cond) => cond.eval(cx, namespaces),
213             SupportsCondition::And(ref vec) => vec.iter().all(|c| c.eval(cx, namespaces)),
214             SupportsCondition::Or(ref vec) => vec.iter().any(|c| c.eval(cx, namespaces)),
215             SupportsCondition::Declaration(ref decl) => decl.eval(cx),
216             SupportsCondition::MozBoolPref(ref name) => eval_moz_bool_pref(name, cx),
217             SupportsCondition::Selector(ref selector) => selector.eval(cx, namespaces),
218             SupportsCondition::FutureSyntax(_) => false,
219         }
220     }
221 }
222 
223 #[cfg(feature = "gecko")]
eval_moz_bool_pref(name: &CStr, cx: &ParserContext) -> bool224 fn eval_moz_bool_pref(name: &CStr, cx: &ParserContext) -> bool {
225     use crate::gecko_bindings::bindings;
226     if !cx.in_ua_or_chrome_sheet() {
227         return false;
228     }
229     unsafe { bindings::Gecko_GetBoolPrefValue(name.as_ptr()) }
230 }
231 
232 #[cfg(feature = "servo")]
eval_moz_bool_pref(_: &CStr, _: &ParserContext) -> bool233 fn eval_moz_bool_pref(_: &CStr, _: &ParserContext) -> bool {
234     false
235 }
236 
237 /// supports_condition | declaration
238 /// <https://drafts.csswg.org/css-conditional/#dom-css-supports-conditiontext-conditiontext>
parse_condition_or_declaration<'i, 't>( input: &mut Parser<'i, 't>, ) -> Result<SupportsCondition, ParseError<'i>>239 pub fn parse_condition_or_declaration<'i, 't>(
240     input: &mut Parser<'i, 't>,
241 ) -> Result<SupportsCondition, ParseError<'i>> {
242     if let Ok(condition) = input.try_parse(SupportsCondition::parse) {
243         Ok(SupportsCondition::Parenthesized(Box::new(condition)))
244     } else {
245         Declaration::parse(input).map(SupportsCondition::Declaration)
246     }
247 }
248 
249 impl ToCss for SupportsCondition {
to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: Write,250     fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
251     where
252         W: Write,
253     {
254         match *self {
255             SupportsCondition::Not(ref cond) => {
256                 dest.write_str("not ")?;
257                 cond.to_css(dest)
258             },
259             SupportsCondition::Parenthesized(ref cond) => {
260                 dest.write_str("(")?;
261                 cond.to_css(dest)?;
262                 dest.write_str(")")
263             },
264             SupportsCondition::And(ref vec) => {
265                 let mut first = true;
266                 for cond in vec {
267                     if !first {
268                         dest.write_str(" and ")?;
269                     }
270                     first = false;
271                     cond.to_css(dest)?;
272                 }
273                 Ok(())
274             },
275             SupportsCondition::Or(ref vec) => {
276                 let mut first = true;
277                 for cond in vec {
278                     if !first {
279                         dest.write_str(" or ")?;
280                     }
281                     first = false;
282                     cond.to_css(dest)?;
283                 }
284                 Ok(())
285             },
286             SupportsCondition::Declaration(ref decl) => {
287                 dest.write_str("(")?;
288                 decl.to_css(dest)?;
289                 dest.write_str(")")
290             },
291             SupportsCondition::Selector(ref selector) => {
292                 dest.write_str("selector(")?;
293                 selector.to_css(dest)?;
294                 dest.write_str(")")
295             },
296             SupportsCondition::MozBoolPref(ref name) => {
297                 dest.write_str("-moz-bool-pref(")?;
298                 let name =
299                     str::from_utf8(name.as_bytes()).expect("Should be parsed from valid UTF-8");
300                 name.to_css(dest)?;
301                 dest.write_str(")")
302             },
303             SupportsCondition::FutureSyntax(ref s) => dest.write_str(&s),
304         }
305     }
306 }
307 
308 #[derive(Clone, Debug, ToShmem)]
309 /// A possibly-invalid CSS selector.
310 pub struct RawSelector(pub String);
311 
312 impl ToCss for RawSelector {
to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: Write,313     fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
314     where
315         W: Write,
316     {
317         dest.write_str(&self.0)
318     }
319 }
320 
321 impl RawSelector {
322     /// Tries to evaluate a `selector()` function.
eval(&self, context: &ParserContext, namespaces: &Namespaces) -> bool323     pub fn eval(&self, context: &ParserContext, namespaces: &Namespaces) -> bool {
324         #[cfg(feature = "gecko")]
325         {
326             if !static_prefs::pref!("layout.css.supports-selector.enabled") {
327                 return false;
328             }
329         }
330 
331         let mut input = ParserInput::new(&self.0);
332         let mut input = Parser::new(&mut input);
333         input
334             .parse_entirely(|input| -> Result<(), CssParseError<()>> {
335                 let parser = SelectorParser {
336                     namespaces,
337                     stylesheet_origin: context.stylesheet_origin,
338                     url_data: context.url_data,
339                 };
340 
341                 #[allow(unused_variables)]
342                 let selector = Selector::<SelectorImpl>::parse(&parser, input)
343                     .map_err(|_| input.new_custom_error(()))?;
344 
345                 #[cfg(feature = "gecko")]
346                 {
347                     use crate::selector_parser::PseudoElement;
348                     use selectors::parser::Component;
349 
350                     let has_any_unknown_webkit_pseudo = selector.has_pseudo_element() &&
351                         selector.iter_raw_match_order().any(|component| {
352                             matches!(
353                                 *component,
354                                 Component::PseudoElement(PseudoElement::UnknownWebkit(..))
355                             )
356                         });
357                     if has_any_unknown_webkit_pseudo {
358                         return Err(input.new_custom_error(()));
359                     }
360                 }
361 
362                 Ok(())
363             })
364             .is_ok()
365     }
366 }
367 
368 #[derive(Clone, Debug, ToShmem)]
369 /// A possibly-invalid property declaration
370 pub struct Declaration(pub String);
371 
372 impl ToCss for Declaration {
to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: Write,373     fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
374     where
375         W: Write,
376     {
377         dest.write_str(&self.0)
378     }
379 }
380 
381 /// <https://drafts.csswg.org/css-syntax-3/#typedef-any-value>
consume_any_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>>382 fn consume_any_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> {
383     input.expect_no_error_token().map_err(|err| err.into())
384 }
385 
386 impl Declaration {
387     /// Parse a declaration
parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Declaration, ParseError<'i>>388     pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Declaration, ParseError<'i>> {
389         let pos = input.position();
390         input.expect_ident()?;
391         input.expect_colon()?;
392         consume_any_value(input)?;
393         Ok(Declaration(input.slice_from(pos).to_owned()))
394     }
395 
396     /// Determine if a declaration parses
397     ///
398     /// <https://drafts.csswg.org/css-conditional-3/#support-definition>
eval(&self, context: &ParserContext) -> bool399     pub fn eval(&self, context: &ParserContext) -> bool {
400         debug_assert_eq!(context.rule_type(), CssRuleType::Style);
401 
402         let mut input = ParserInput::new(&self.0);
403         let mut input = Parser::new(&mut input);
404         input
405             .parse_entirely(|input| -> Result<(), CssParseError<()>> {
406                 let prop = input.expect_ident_cloned().unwrap();
407                 input.expect_colon().unwrap();
408 
409                 let id =
410                     PropertyId::parse(&prop, context).map_err(|_| input.new_custom_error(()))?;
411 
412                 let mut declarations = SourcePropertyDeclaration::new();
413                 input.parse_until_before(Delimiter::Bang, |input| {
414                     PropertyDeclaration::parse_into(&mut declarations, id, &context, input)
415                         .map_err(|_| input.new_custom_error(()))
416                 })?;
417                 let _ = input.try_parse(parse_important);
418                 Ok(())
419             })
420             .is_ok()
421     }
422 }
423