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