Skip to main content

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::derives::*;
8use crate::font_face::{FontFaceSourceFormatKeyword, FontFaceSourceTechFlags};
9use crate::parser::ParserContext;
10use crate::properties::{PropertyDeclaration, PropertyId, SourcePropertyDeclaration};
11use crate::selector_parser::{SelectorImpl, SelectorParser};
12use crate::shared_lock::{DeepCloneWithLock, Locked};
13use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard};
14use crate::stylesheets::rule_parser::AtRuleType;
15use crate::stylesheets::{CssRuleType, CssRules};
16use cssparser::{Delimiter, Parser, SourceLocation, Token};
17use cssparser::{ParseError as CssParseError, match_ignore_ascii_case};
18use cssparser::{parse_important, serialize_identifier};
19#[cfg(feature = "gecko")]
20use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf};
21use selectors::parser::{Selector, SelectorParseErrorKind};
22use servo_arc::Arc;
23use std::fmt::{self, Write};
24use std::str;
25use style_traits::{CssStringWriter, CssWriter, ParseError, StyleParseErrorKind, ToCss};
26
27/// An [`@supports`][supports] rule.
28///
29/// [supports]: https://drafts.csswg.org/css-conditional-3/#at-supports
30#[derive(Debug, ToShmem)]
31pub struct SupportsRule {
32    /// The parsed condition
33    pub condition: SupportsCondition,
34    /// Child rules
35    pub rules: Arc<Locked<CssRules>>,
36    /// The result of evaluating the condition
37    pub enabled: bool,
38    /// The line and column of the rule's source code.
39    pub source_location: SourceLocation,
40}
41
42impl SupportsRule {
43    /// Measure heap usage.
44    #[cfg(feature = "gecko")]
45    pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize {
46        // Measurement of other fields may be added later.
47        self.rules.unconditional_shallow_size_of(ops)
48            + self.rules.read_with(guard).size_of(guard, ops)
49    }
50}
51
52impl ToCssWithGuard for SupportsRule {
53    fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result {
54        dest.write_str("@supports ")?;
55        self.condition.to_css(&mut CssWriter::new(dest))?;
56        self.rules.read_with(guard).to_css_block(guard, dest)
57    }
58}
59
60impl DeepCloneWithLock for SupportsRule {
61    fn deep_clone_with_lock(&self, lock: &SharedRwLock, guard: &SharedRwLockReadGuard) -> Self {
62        let rules = self.rules.read_with(guard);
63        SupportsRule {
64            condition: self.condition.clone(),
65            rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard))),
66            enabled: self.enabled,
67            source_location: self.source_location,
68        }
69    }
70}
71
72/// An @supports condition
73///
74/// <https://drafts.csswg.org/css-conditional-3/#at-supports>
75#[derive(Clone, Debug, ToShmem)]
76pub enum SupportsCondition {
77    /// `not (condition)`
78    Not(Box<SupportsCondition>),
79    /// `(condition)`
80    Parenthesized(Box<SupportsCondition>),
81    /// `(condition) and (condition) and (condition) ..`
82    And(Vec<SupportsCondition>),
83    /// `(condition) or (condition) or (condition) ..`
84    Or(Vec<SupportsCondition>),
85    /// `property-ident: value` (value can be any tokens)
86    Declaration(Declaration),
87    /// `at-rule(<at-keyword-token>)`
88    AtRule(AtRuleKeyword),
89    /// A `selector()` function.
90    Selector(RawSelector),
91    /// `font-format(<font-format>)`
92    FontFormat(FontFaceSourceFormatKeyword),
93    /// `font-tech(<font-tech>)`
94    FontTech(FontFaceSourceTechFlags),
95    /// `named-feature(<ident>)`
96    NamedFeature(NamedFeature),
97    /// `(any tokens)` or `func(any tokens)`
98    FutureSyntax(String),
99}
100
101impl SupportsCondition {
102    /// Parse a condition
103    ///
104    /// <https://drafts.csswg.org/css-conditional/#supports_condition>
105    pub fn parse(input: &mut Parser) -> Result<Self, ParseError> {
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 (keyword, wrapper) = match input.next() {
114            // End of input
115            Err(..) => return Ok(in_parens),
116            Ok(Token::Ident(ident)) => {
117                match_ignore_ascii_case! { &ident,
118                    "and" => ("and", SupportsCondition::And as fn(_) -> _),
119                    "or" => ("or", SupportsCondition::Or as fn(_) -> _),
120                    _ => return Err(ParseError::custom(SelectorParseErrorKind::UnexpectedIdent))
121                }
122            },
123            Ok(_) => return Err(ParseError::unexpected_token()),
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(function: &str, input: &mut Parser) -> Result<Self, ParseError> {
144        match_ignore_ascii_case! { function,
145            "at-rule" if crate::pref!("layout.css.supports.at-rule.enabled") => {
146                let kw = AtRuleKeyword::parse(input)?;
147                Ok(SupportsCondition::AtRule(kw))
148            },
149            "selector" => {
150                let pos = input.position();
151                consume_any_value(input)?;
152                Ok(SupportsCondition::Selector(RawSelector(
153                    input.slice_from(pos).to_owned()
154                )))
155            },
156            "font-format" if crate::pref!("layout.css.font-tech.enabled", gecko = true) => {
157                let kw = FontFaceSourceFormatKeyword::parse(input)?;
158                Ok(SupportsCondition::FontFormat(kw))
159            },
160            "font-tech" if crate::pref!("layout.css.font-tech.enabled", gecko = true) => {
161                let flag = FontFaceSourceTechFlags::parse_one(input)?;
162                Ok(SupportsCondition::FontTech(flag))
163            },
164            "named-feature" => {
165                let feature = NamedFeature::parse(input)?;
166                Ok(SupportsCondition::NamedFeature(feature))
167            },
168            _ => {
169                Err(ParseError::custom(StyleParseErrorKind::UnspecifiedError))
170            },
171        }
172    }
173
174    /// Parses an `@import` condition as per
175    /// https://drafts.csswg.org/css-cascade-5/#typedef-import-conditions
176    pub fn parse_for_import(input: &mut Parser) -> Result<Self, ParseError> {
177        input.expect_function_matching("supports")?;
178        input.parse_nested_block(parse_condition_or_declaration)
179    }
180
181    /// <https://drafts.csswg.org/css-conditional-3/#supports_condition_in_parens>
182    fn parse_in_parens(input: &mut Parser) -> Result<Self, ParseError> {
183        // Whitespace is normally taken care of in `Parser::next`, but we want to not include it in
184        // `pos` for the SupportsCondition::FutureSyntax cases.
185        input.skip_whitespace();
186        let pos = input.position();
187        match *input.next()? {
188            Token::ParenthesisBlock => {
189                let nested = input
190                    .try_parse(|input| input.parse_nested_block(parse_condition_or_declaration));
191                if let Ok(nested) = nested {
192                    return Ok(Self::Parenthesized(Box::new(nested)));
193                }
194            },
195            Token::Function(ref ident) => {
196                let ident = ident.clone();
197                let nested = input.try_parse(|input| {
198                    input.parse_nested_block(|input| {
199                        SupportsCondition::parse_functional(&ident, input)
200                    })
201                });
202                if nested.is_ok() {
203                    return nested;
204                }
205            },
206            _ => return Err(ParseError::unexpected_token()),
207        }
208        input.parse_nested_block(consume_any_value)?;
209        Ok(SupportsCondition::FutureSyntax(
210            input.slice_from(pos).to_owned(),
211        ))
212    }
213
214    /// Evaluate a supports condition
215    pub fn eval(&self, cx: &ParserContext) -> bool {
216        match *self {
217            SupportsCondition::Not(ref cond) => !cond.eval(cx),
218            SupportsCondition::Parenthesized(ref cond) => cond.eval(cx),
219            SupportsCondition::And(ref vec) => vec.iter().all(|c| c.eval(cx)),
220            SupportsCondition::Or(ref vec) => vec.iter().any(|c| c.eval(cx)),
221            SupportsCondition::AtRule(ref kw) => kw.eval(cx),
222            SupportsCondition::Declaration(ref decl) => decl.eval(cx),
223            SupportsCondition::Selector(ref selector) => selector.eval(cx),
224            SupportsCondition::FontFormat(ref format) => eval_font_format(format),
225            SupportsCondition::FontTech(ref tech) => eval_font_tech(tech),
226            SupportsCondition::NamedFeature(ref feature) => feature.eval(),
227            SupportsCondition::FutureSyntax(_) => false,
228        }
229    }
230}
231
232#[cfg(feature = "gecko")]
233fn eval_font_format(kw: &FontFaceSourceFormatKeyword) -> bool {
234    use crate::gecko_bindings::bindings;
235    unsafe { bindings::Gecko_IsFontFormatSupported(*kw) }
236}
237
238#[cfg(feature = "gecko")]
239fn eval_font_tech(flag: &FontFaceSourceTechFlags) -> bool {
240    use crate::gecko_bindings::bindings;
241    unsafe { bindings::Gecko_IsFontTechSupported(*flag) }
242}
243
244#[cfg(feature = "servo")]
245fn eval_font_format(_: &FontFaceSourceFormatKeyword) -> bool {
246    false
247}
248
249#[cfg(feature = "servo")]
250fn eval_font_tech(_: &FontFaceSourceTechFlags) -> bool {
251    false
252}
253
254/// supports_condition | declaration
255/// <https://drafts.csswg.org/css-conditional/#dom-css-supports-conditiontext-conditiontext>
256pub fn parse_condition_or_declaration(input: &mut Parser) -> Result<SupportsCondition, ParseError> {
257    if let Ok(condition) = input.try_parse(SupportsCondition::parse) {
258        Ok(condition)
259    } else {
260        Declaration::parse(input).map(SupportsCondition::Declaration)
261    }
262}
263
264impl ToCss for SupportsCondition {
265    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
266    where
267        W: Write,
268    {
269        match *self {
270            SupportsCondition::Not(ref cond) => {
271                dest.write_str("not ")?;
272                cond.to_css(dest)
273            },
274            SupportsCondition::Parenthesized(ref cond) => {
275                dest.write_char('(')?;
276                cond.to_css(dest)?;
277                dest.write_char(')')
278            },
279            SupportsCondition::And(ref vec) => {
280                let mut first = true;
281                for cond in vec {
282                    if !first {
283                        dest.write_str(" and ")?;
284                    }
285                    first = false;
286                    cond.to_css(dest)?;
287                }
288                Ok(())
289            },
290            SupportsCondition::Or(ref vec) => {
291                let mut first = true;
292                for cond in vec {
293                    if !first {
294                        dest.write_str(" or ")?;
295                    }
296                    first = false;
297                    cond.to_css(dest)?;
298                }
299                Ok(())
300            },
301            SupportsCondition::AtRule(ref kw) => {
302                dest.write_str("at-rule(")?;
303                kw.to_css(dest)?;
304                dest.write_char(')')
305            },
306            SupportsCondition::Declaration(ref decl) => decl.to_css(dest),
307            SupportsCondition::Selector(ref selector) => {
308                dest.write_str("selector(")?;
309                selector.to_css(dest)?;
310                dest.write_char(')')
311            },
312            SupportsCondition::FontFormat(ref kw) => {
313                dest.write_str("font-format(")?;
314                kw.to_css(dest)?;
315                dest.write_char(')')
316            },
317            SupportsCondition::FontTech(ref flag) => {
318                dest.write_str("font-tech(")?;
319                flag.to_css(dest)?;
320                dest.write_char(')')
321            },
322            SupportsCondition::NamedFeature(ref feature) => {
323                dest.write_str("named-feature(")?;
324                feature.to_css(dest)?;
325                dest.write_char(')')
326            },
327            SupportsCondition::FutureSyntax(ref s) => dest.write_str(s),
328        }
329    }
330}
331
332#[derive(Clone, Debug, ToShmem)]
333/// A possibly-invalid CSS selector.
334pub struct RawSelector(pub String);
335
336impl ToCss for RawSelector {
337    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
338    where
339        W: Write,
340    {
341        dest.write_str(&self.0)
342    }
343}
344
345impl RawSelector {
346    /// Tries to evaluate a `selector()` function.
347    pub fn eval(&self, context: &ParserContext) -> bool {
348        let mut input = Parser::new(&self.0);
349        input
350            .parse_entirely(|input| -> Result<(), CssParseError<()>> {
351                let parser = SelectorParser {
352                    namespaces: &context.namespaces,
353                    stylesheet_origin: context.stylesheet_origin,
354                    url_data: context.url_data,
355                    for_supports_rule: true,
356                };
357
358                Selector::<SelectorImpl>::parse(&parser, input)
359                    .map_err(|_| CssParseError::custom(()))?;
360
361                Ok(())
362            })
363            .is_ok()
364    }
365}
366
367#[derive(Clone, Debug, ToShmem)]
368/// A possibly-invalid property declaration
369pub struct Declaration(pub String);
370
371impl ToCss for Declaration {
372    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
373    where
374        W: Write,
375    {
376        dest.write_str(&self.0)
377    }
378}
379
380/// <https://drafts.csswg.org/css-syntax-3/#typedef-any-value>
381fn consume_any_value(input: &mut Parser) -> Result<(), ParseError> {
382    input.expect_no_error_token().map_err(|err| err.into())
383}
384
385impl Declaration {
386    /// Parse a declaration
387    pub fn parse(input: &mut Parser) -> Result<Declaration, ParseError> {
388        let pos = input.position();
389        input.expect_ident()?;
390        input.expect_colon()?;
391        consume_any_value(input)?;
392        Ok(Declaration(input.slice_from(pos).to_owned()))
393    }
394
395    /// Determine if a declaration parses
396    ///
397    /// <https://drafts.csswg.org/css-conditional-3/#support-definition>
398    pub fn eval(&self, context: &ParserContext) -> bool {
399        debug_assert!(context.rule_types().contains(CssRuleType::Style));
400
401        let mut input = Parser::new(&self.0);
402        input
403            .parse_entirely(|input| -> Result<(), CssParseError<()>> {
404                let prop = input.expect_ident_cloned().unwrap();
405                input.expect_colon().unwrap();
406
407                let id =
408                    PropertyId::parse(&prop, context).map_err(|_| CssParseError::custom(()))?;
409
410                let mut declarations = SourcePropertyDeclaration::default();
411                input.parse_until_before(Delimiter::Bang, |input| {
412                    PropertyDeclaration::parse_into(&mut declarations, id, context, input)
413                        .map_err(|_| CssParseError::custom(()))
414                })?;
415                let _ = input.try_parse(parse_important);
416                Ok(())
417            })
418            .is_ok()
419    }
420}
421
422/// A possibly-invalid at-rule keyword
423#[derive(Clone, Debug, ToShmem)]
424pub struct AtRuleKeyword(pub Box<str>);
425
426impl AtRuleKeyword {
427    /// Parse an <at-keyword-token>.
428    pub fn parse(input: &mut Parser) -> Result<Self, ParseError> {
429        match input.next()? {
430            Token::AtKeyword(kw) => Ok(Self(kw.as_ref().into())),
431            _ => Err(ParseError::unexpected_token()),
432        }
433    }
434
435    /// Determine if an at-rule is supported.
436    /// https://drafts.csswg.org/css-conditional-5/#dfn-support-at-rule
437    pub fn eval(&self, context: &ParserContext) -> bool {
438        AtRuleType::is_supported(&self.0, context)
439    }
440}
441
442impl ToCss for AtRuleKeyword {
443    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
444    where
445        W: Write,
446    {
447        dest.write_char('@')?;
448        serialize_identifier(&self.0, dest)
449    }
450}
451
452/// List of named features.
453///
454/// <https://drafts.csswg.org/css-conditional-5/#support-definition-named-features>
455#[derive(Clone, Copy, Debug, Parse, ToCss, ToShmem)]
456#[repr(u8)]
457pub enum NamedFeature {
458    /// Anchoring to a transformed element takes the anchor's transforms into account.
459    AnchorPositionFollowsTransforms,
460    /// A scroll container that scrolls in one axis and clips in the other.
461    SingleAxisScrollContainer,
462}
463
464impl NamedFeature {
465    /// Determine if a named feature is supported.
466    ///
467    /// <https://drafts.csswg.org/css-conditional-5/#typedef-supports-named-feature-fn>
468    #[cfg(feature = "gecko")]
469    pub fn eval(self) -> bool {
470        match self {
471            Self::AnchorPositionFollowsTransforms => {
472                crate::pref!("layout.css.anchor-positioning.follows-transforms.enabled")
473            },
474            // Not implemented. See Bug 2044147.
475            Self::SingleAxisScrollContainer => false,
476        }
477    }
478
479    /// Determine if a named feature is supported.
480    ///
481    /// <https://drafts.csswg.org/css-conditional-5/#typedef-supports-named-feature-fn>
482    #[cfg(feature = "servo")]
483    pub fn eval(self) -> bool {
484        false
485    }
486}