style/stylesheets/
supports_rule.rs

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
7use crate::font_face::{FontFaceSourceFormatKeyword, FontFaceSourceTechFlags};
8use crate::parser::ParserContext;
9use crate::properties::{PropertyDeclaration, PropertyId, SourcePropertyDeclaration};
10use crate::selector_parser::{SelectorImpl, SelectorParser};
11use crate::shared_lock::{DeepCloneWithLock, Locked};
12use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
13use crate::str::CssStringWriter;
14use crate::stylesheets::{CssRuleType, CssRules};
15use cssparser::parse_important;
16use cssparser::{Delimiter, Parser, SourceLocation, Token};
17use cssparser::{ParseError as CssParseError, ParserInput};
18#[cfg(feature = "gecko")]
19use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
20use selectors::parser::{Selector, SelectorParseErrorKind};
21use servo_arc::Arc;
22use std::fmt::{self, Write};
23use std::str;
24use 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)]
30pub 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
41impl SupportsRule {
42    /// Measure heap usage.
43    #[cfg(feature = "gecko")]
44    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
51impl ToCssWithGuard for SupportsRule {
52    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
59impl DeepCloneWithLock for SupportsRule {
60    fn deep_clone_with_lock(&self, lock: &SharedRwLock, guard: &SharedRwLockReadGuard) -> Self {
61        let rules = self.rules.read_with(guard);
62        SupportsRule {
63            condition: self.condition.clone(),
64            rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard))),
65            enabled: self.enabled,
66            source_location: self.source_location.clone(),
67        }
68    }
69}
70
71/// An @supports condition
72///
73/// <https://drafts.csswg.org/css-conditional-3/#at-supports>
74#[derive(Clone, Debug, ToShmem)]
75pub enum SupportsCondition {
76    /// `not (condition)`
77    Not(Box<SupportsCondition>),
78    /// `(condition)`
79    Parenthesized(Box<SupportsCondition>),
80    /// `(condition) and (condition) and (condition) ..`
81    And(Vec<SupportsCondition>),
82    /// `(condition) or (condition) or (condition) ..`
83    Or(Vec<SupportsCondition>),
84    /// `property-ident: value` (value can be any tokens)
85    Declaration(Declaration),
86    /// A `selector()` function.
87    Selector(RawSelector),
88    /// `font-format(<font-format>)`
89    FontFormat(FontFaceSourceFormatKeyword),
90    /// `font-tech(<font-tech>)`
91    FontTech(FontFaceSourceTechFlags),
92    /// `(any tokens)` or `func(any tokens)`
93    FutureSyntax(String),
94}
95
96impl SupportsCondition {
97    /// Parse a condition
98    ///
99    /// <https://drafts.csswg.org/css-conditional/#supports_condition>
100    pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
101        if input.try_parse(|i| i.expect_ident_matching("not")).is_ok() {
102            let inner = SupportsCondition::parse_in_parens(input)?;
103            return Ok(SupportsCondition::Not(Box::new(inner)));
104        }
105
106        let in_parens = SupportsCondition::parse_in_parens(input)?;
107
108        let location = input.current_source_location();
109        let (keyword, wrapper) = match input.next() {
110            // End of input
111            Err(..) => return Ok(in_parens),
112            Ok(&Token::Ident(ref ident)) => {
113                match_ignore_ascii_case! { &ident,
114                    "and" => ("and", SupportsCondition::And as fn(_) -> _),
115                    "or" => ("or", SupportsCondition::Or as fn(_) -> _),
116                    _ => return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())))
117                }
118            },
119            Ok(t) => return Err(location.new_unexpected_token_error(t.clone())),
120        };
121
122        let mut conditions = Vec::with_capacity(2);
123        conditions.push(in_parens);
124        loop {
125            conditions.push(SupportsCondition::parse_in_parens(input)?);
126            if input
127                .try_parse(|input| input.expect_ident_matching(keyword))
128                .is_err()
129            {
130                // Did not find the expected keyword.
131                // If we found some other token, it will be rejected by
132                // `Parser::parse_entirely` somewhere up the stack.
133                return Ok(wrapper(conditions));
134            }
135        }
136    }
137
138    /// Parses a functional supports condition.
139    fn parse_functional<'i, 't>(
140        function: &str,
141        input: &mut Parser<'i, 't>,
142    ) -> Result<Self, ParseError<'i>> {
143        match_ignore_ascii_case! { function,
144            "selector" => {
145                let pos = input.position();
146                consume_any_value(input)?;
147                Ok(SupportsCondition::Selector(RawSelector(
148                    input.slice_from(pos).to_owned()
149                )))
150            },
151            "font-format" if static_prefs::pref!("layout.css.font-tech.enabled") => {
152                let kw = FontFaceSourceFormatKeyword::parse(input)?;
153                Ok(SupportsCondition::FontFormat(kw))
154            },
155            "font-tech" if static_prefs::pref!("layout.css.font-tech.enabled") => {
156                let flag = FontFaceSourceTechFlags::parse_one(input)?;
157                Ok(SupportsCondition::FontTech(flag))
158            },
159            _ => {
160                Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
161            },
162        }
163    }
164
165    /// Parses an `@import` condition as per
166    /// https://drafts.csswg.org/css-cascade-5/#typedef-import-conditions
167    pub fn parse_for_import<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
168        input.expect_function_matching("supports")?;
169        input.parse_nested_block(parse_condition_or_declaration)
170    }
171
172    /// <https://drafts.csswg.org/css-conditional-3/#supports_condition_in_parens>
173    fn parse_in_parens<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
174        // Whitespace is normally taken care of in `Parser::next`, but we want to not include it in
175        // `pos` for the SupportsCondition::FutureSyntax cases.
176        input.skip_whitespace();
177        let pos = input.position();
178        let location = input.current_source_location();
179        match *input.next()? {
180            Token::ParenthesisBlock => {
181                let nested = input
182                    .try_parse(|input| input.parse_nested_block(parse_condition_or_declaration));
183                if let Ok(nested) = nested {
184                    return Ok(Self::Parenthesized(Box::new(nested)));
185                }
186            },
187            Token::Function(ref ident) => {
188                let ident = ident.clone();
189                let nested = input.try_parse(|input| {
190                    input.parse_nested_block(|input| {
191                        SupportsCondition::parse_functional(&ident, input)
192                    })
193                });
194                if nested.is_ok() {
195                    return nested;
196                }
197            },
198            ref t => return Err(location.new_unexpected_token_error(t.clone())),
199        }
200        input.parse_nested_block(consume_any_value)?;
201        Ok(SupportsCondition::FutureSyntax(
202            input.slice_from(pos).to_owned(),
203        ))
204    }
205
206    /// Evaluate a supports condition
207    pub fn eval(&self, cx: &ParserContext) -> bool {
208        match *self {
209            SupportsCondition::Not(ref cond) => !cond.eval(cx),
210            SupportsCondition::Parenthesized(ref cond) => cond.eval(cx),
211            SupportsCondition::And(ref vec) => vec.iter().all(|c| c.eval(cx)),
212            SupportsCondition::Or(ref vec) => vec.iter().any(|c| c.eval(cx)),
213            SupportsCondition::Declaration(ref decl) => decl.eval(cx),
214            SupportsCondition::Selector(ref selector) => selector.eval(cx),
215            SupportsCondition::FontFormat(ref format) => eval_font_format(format),
216            SupportsCondition::FontTech(ref tech) => eval_font_tech(tech),
217            SupportsCondition::FutureSyntax(_) => false,
218        }
219    }
220}
221
222#[cfg(feature = "gecko")]
223fn eval_font_format(kw: &FontFaceSourceFormatKeyword) -> bool {
224    use crate::gecko_bindings::bindings;
225    unsafe { bindings::Gecko_IsFontFormatSupported(*kw) }
226}
227
228#[cfg(feature = "gecko")]
229fn eval_font_tech(flag: &FontFaceSourceTechFlags) -> bool {
230    use crate::gecko_bindings::bindings;
231    unsafe { bindings::Gecko_IsFontTechSupported(*flag) }
232}
233
234#[cfg(feature = "servo")]
235fn eval_font_format(_: &FontFaceSourceFormatKeyword) -> bool {
236    false
237}
238
239#[cfg(feature = "servo")]
240fn eval_font_tech(_: &FontFaceSourceTechFlags) -> bool {
241    false
242}
243
244/// supports_condition | declaration
245/// <https://drafts.csswg.org/css-conditional/#dom-css-supports-conditiontext-conditiontext>
246pub fn parse_condition_or_declaration<'i, 't>(
247    input: &mut Parser<'i, 't>,
248) -> Result<SupportsCondition, ParseError<'i>> {
249    if let Ok(condition) = input.try_parse(SupportsCondition::parse) {
250        Ok(condition)
251    } else {
252        Declaration::parse(input).map(SupportsCondition::Declaration)
253    }
254}
255
256impl ToCss for SupportsCondition {
257    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
258    where
259        W: Write,
260    {
261        match *self {
262            SupportsCondition::Not(ref cond) => {
263                dest.write_str("not ")?;
264                cond.to_css(dest)
265            },
266            SupportsCondition::Parenthesized(ref cond) => {
267                dest.write_char('(')?;
268                cond.to_css(dest)?;
269                dest.write_char(')')
270            },
271            SupportsCondition::And(ref vec) => {
272                let mut first = true;
273                for cond in vec {
274                    if !first {
275                        dest.write_str(" and ")?;
276                    }
277                    first = false;
278                    cond.to_css(dest)?;
279                }
280                Ok(())
281            },
282            SupportsCondition::Or(ref vec) => {
283                let mut first = true;
284                for cond in vec {
285                    if !first {
286                        dest.write_str(" or ")?;
287                    }
288                    first = false;
289                    cond.to_css(dest)?;
290                }
291                Ok(())
292            },
293            SupportsCondition::Declaration(ref decl) => decl.to_css(dest),
294            SupportsCondition::Selector(ref selector) => {
295                dest.write_str("selector(")?;
296                selector.to_css(dest)?;
297                dest.write_char(')')
298            },
299            SupportsCondition::FontFormat(ref kw) => {
300                dest.write_str("font-format(")?;
301                kw.to_css(dest)?;
302                dest.write_char(')')
303            },
304            SupportsCondition::FontTech(ref flag) => {
305                dest.write_str("font-tech(")?;
306                flag.to_css(dest)?;
307                dest.write_char(')')
308            },
309            SupportsCondition::FutureSyntax(ref s) => dest.write_str(&s),
310        }
311    }
312}
313
314#[derive(Clone, Debug, ToShmem)]
315/// A possibly-invalid CSS selector.
316pub struct RawSelector(pub String);
317
318impl ToCss for RawSelector {
319    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
320    where
321        W: Write,
322    {
323        dest.write_str(&self.0)
324    }
325}
326
327impl RawSelector {
328    /// Tries to evaluate a `selector()` function.
329    pub fn eval(&self, context: &ParserContext) -> bool {
330        let mut input = ParserInput::new(&self.0);
331        let mut input = Parser::new(&mut input);
332        input
333            .parse_entirely(|input| -> Result<(), CssParseError<()>> {
334                let parser = SelectorParser {
335                    namespaces: &context.namespaces,
336                    stylesheet_origin: context.stylesheet_origin,
337                    url_data: context.url_data,
338                    for_supports_rule: true,
339                };
340
341                Selector::<SelectorImpl>::parse(&parser, input)
342                    .map_err(|_| input.new_custom_error(()))?;
343
344                Ok(())
345            })
346            .is_ok()
347    }
348}
349
350#[derive(Clone, Debug, ToShmem)]
351/// A possibly-invalid property declaration
352pub struct Declaration(pub String);
353
354impl ToCss for Declaration {
355    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
356    where
357        W: Write,
358    {
359        dest.write_str(&self.0)
360    }
361}
362
363/// <https://drafts.csswg.org/css-syntax-3/#typedef-any-value>
364fn consume_any_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> {
365    input.expect_no_error_token().map_err(|err| err.into())
366}
367
368impl Declaration {
369    /// Parse a declaration
370    pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Declaration, ParseError<'i>> {
371        let pos = input.position();
372        input.expect_ident()?;
373        input.expect_colon()?;
374        consume_any_value(input)?;
375        Ok(Declaration(input.slice_from(pos).to_owned()))
376    }
377
378    /// Determine if a declaration parses
379    ///
380    /// <https://drafts.csswg.org/css-conditional-3/#support-definition>
381    pub fn eval(&self, context: &ParserContext) -> bool {
382        debug_assert!(context.rule_types().contains(CssRuleType::Style));
383
384        let mut input = ParserInput::new(&self.0);
385        let mut input = Parser::new(&mut input);
386        input
387            .parse_entirely(|input| -> Result<(), CssParseError<()>> {
388                let prop = input.expect_ident_cloned().unwrap();
389                input.expect_colon().unwrap();
390
391                let id =
392                    PropertyId::parse(&prop, context).map_err(|_| input.new_custom_error(()))?;
393
394                let mut declarations = SourcePropertyDeclaration::default();
395                input.parse_until_before(Delimiter::Bang, |input| {
396                    PropertyDeclaration::parse_into(&mut declarations, id, &context, input)
397                        .map_err(|_| input.new_custom_error(()))
398                })?;
399                let _ = input.try_parse(parse_important);
400                Ok(())
401            })
402            .is_ok()
403    }
404}