Skip to main content

oxc_css_parser/parser/at_rule/
supports.rs

1use super::Parser;
2use crate::{
3    Parse,
4    ast::*,
5    error::{Error, ErrorKind, PResult},
6    pos::Span,
7    tokenizer::{Token, TokenWithSpan},
8};
9
10// https://drafts.csswg.org/css-conditional-3/#at-supports
11//
12// <supports-condition> = not <supports-in-parens>
13//                      | <supports-in-parens> [ and <supports-in-parens> ]*
14//                      | <supports-in-parens> [ or <supports-in-parens> ]*
15impl<'a> Parse<'a> for SupportsCondition<'a> {
16    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
17        if input.cursor.peek()?.is_ident_name_eq_ignore_ascii_case(input.source, "not") {
18            let keyword = input.parse::<Ident>()?;
19            let condition = input.parse::<SupportsInParens>()?;
20            let span = Span { start: keyword.span.start, end: condition.span().end };
21            Ok(SupportsCondition {
22                conditions: input.vec1(SupportsConditionKind::Not(SupportsNot {
23                    keyword,
24                    condition,
25                    span,
26                })),
27                span,
28            })
29        } else {
30            let first = input.parse::<SupportsInParens>()?;
31            let mut span = *first.span();
32            let mut conditions = input.vec1(SupportsConditionKind::SupportsInParens(first));
33            while input.cursor.peek()?.ident(input.source).is_some() {
34                if input.cursor.peek()?.is_ident_name_eq_ignore_ascii_case(input.source, "and") {
35                    let ident = input.parse::<Ident>()?;
36                    let condition = input.parse::<SupportsInParens>()?;
37                    let span = Span { start: ident.span.start, end: condition.span().end };
38                    conditions.push(SupportsConditionKind::And(SupportsAnd {
39                        keyword: ident,
40                        condition,
41                        span,
42                    }));
43                } else if input
44                    .cursor
45                    .peek()?
46                    .is_ident_name_eq_ignore_ascii_case(input.source, "or")
47                {
48                    let ident = input.parse::<Ident>()?;
49                    let condition = input.parse::<SupportsInParens>()?;
50                    let span = Span { start: ident.span.start, end: condition.span().end };
51                    conditions.push(SupportsConditionKind::Or(SupportsOr {
52                        keyword: ident,
53                        condition,
54                        span,
55                    }));
56                } else {
57                    break;
58                }
59            }
60            if let Some(last) = conditions.last() {
61                span.end = last.span().end;
62            }
63            Ok(SupportsCondition { conditions, span })
64        }
65    }
66}
67
68// https://drafts.csswg.org/css-conditional-3/#typedef-supports-in-parens
69//
70// <supports-in-parens> = ( <supports-condition> )
71//                      | <supports-feature>
72//                      | <general-enclosed>
73impl<'a> Parse<'a> for SupportsInParens<'a> {
74    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
75        match input.cursor.peek()? {
76            TokenWithSpan { token: Token::LParen(..), .. } => input
77                .try_parse(|parser| {
78                    parser.parse::<SupportsDecl>().map(|supports_decl| {
79                        let span = supports_decl.span;
80                        SupportsInParens {
81                            kind: SupportsInParensKind::Feature(parser.alloc(supports_decl)),
82                            span,
83                        }
84                    })
85                })
86                .or_else(|_| {
87                    input.try_parse(|parser| {
88                        let (_, Span { start, .. }) = parser.cursor.expect_l_paren()?;
89                        let condition = parser.parse::<SupportsCondition>()?;
90                        let (_, Span { end, .. }) = parser.cursor.expect_r_paren()?;
91                        Ok(SupportsInParens {
92                            kind: SupportsInParensKind::SupportsCondition(condition),
93                            span: Span { start, end },
94                        })
95                    })
96                })
97                .or_else(|_| {
98                    // <general-enclosed>: MQ L4 catch-all (referenced from <supports-condition>),
99                    // evaluates false at runtime.
100                    let (_, Span { start, .. }) = input.cursor.expect_l_paren()?;
101                    let tokens = input.parse_tokens_in_parens()?;
102                    let (_, Span { end, .. }) = input.cursor.expect_r_paren()?;
103                    Ok(SupportsInParens {
104                        kind: SupportsInParensKind::GeneralEnclosed(tokens),
105                        span: Span { start, end },
106                    })
107                }),
108            // Sass: an interpolation may stand for a whole condition operand
109            // (`@supports #{"(a: b)"} and (c: d)`) or splice into a function
110            // name (`@supports a#{"b"}c(d)`).
111            TokenWithSpan { token: Token::Ident(..) | Token::HashLBrace(..), .. } => {
112                let name = input.parse::<InterpolableIdent>()?;
113                let name_end = name.span().end;
114                match name {
115                    InterpolableIdent::Literal(function_ident)
116                        if function_ident.name.eq_ignore_ascii_case("selector") =>
117                    {
118                        input.cursor.expect_l_paren()?;
119                        let selector_list = input.parse::<SelectorList>()?;
120                        input.cursor.expect_r_paren()?;
121                        let span = selector_list.span;
122                        Ok(SupportsInParens {
123                            kind: SupportsInParensKind::Selector(selector_list),
124                            span,
125                        })
126                    }
127                    name => {
128                        let glued_lparen = matches!(
129                            input.cursor.peek()?,
130                            TokenWithSpan { token: Token::LParen(..), span }
131                                if span.start == name_end
132                        );
133                        // Only a pure interpolation may stand alone
134                        // (`#{"(a: b)"}`); a mixed ident like `a#{b}` still
135                        // needs parens or a function call, as in dart-sass.
136                        let pure_interpolation = matches!(
137                            &name,
138                            InterpolableIdent::SassInterpolated(interpolation)
139                                if matches!(
140                                    interpolation.elements.as_slice(),
141                                    [SassInterpolatedIdentElement::Expression(..)]
142                                )
143                        );
144                        if glued_lparen {
145                            // An unknown function here is `<general-enclosed>`
146                            // (css-conditional): its contents are raw tokens.
147                            let function = input.parse_raw_function(name)?;
148                            let span = function.span;
149                            Ok(SupportsInParens {
150                                kind: SupportsInParensKind::Function(function),
151                                span,
152                            })
153                        } else if pure_interpolation {
154                            let span = *name.span();
155                            Ok(SupportsInParens {
156                                kind: SupportsInParensKind::Interpolation(name),
157                                span,
158                            })
159                        } else {
160                            let TokenWithSpan { token, span } = input.cursor.peek()?;
161                            Err(Error {
162                                kind: ErrorKind::Unexpected("'('", token.symbol()),
163                                span: *span,
164                            })
165                        }
166                    }
167                }
168            }
169            TokenWithSpan { token, span } => {
170                Err(Error { kind: ErrorKind::Unexpected("'('", token.symbol()), span: *span })
171            }
172        }
173    }
174}
175
176// https://drafts.csswg.org/css-conditional-3/#typedef-supports-feature
177//
178// <supports-feature> = <supports-decl>
179// <supports-decl>    = ( <declaration> )
180impl<'a> Parse<'a> for SupportsDecl<'a> {
181    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
182        let start = input.cursor.expect_l_paren()?.1.start;
183        let decl = input.parse()?;
184        let end = input.cursor.expect_r_paren()?.1.end;
185        Ok(SupportsDecl { decl, span: Span { start, end } })
186    }
187}