Skip to main content

oxc_css_parser/parser/at_rule/
container.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-5/#container-queries
11//
12// Spec `<container-query>` — the boolean logic over `<query-in-parens>`
13// (this AST names the node `ContainerCondition`):
14// <container-query> = not <query-in-parens>
15//                   | <query-in-parens> [ [ and <query-in-parens> ]* | [ or <query-in-parens> ]* ]
16impl<'a> Parse<'a> for ContainerCondition<'a> {
17    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
18        match &input.cursor.peek()?.token {
19            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("not") => {
20                let container_condition_not = input.parse::<ContainerConditionNot>()?;
21                let span = container_condition_not.span.clone();
22                Ok(ContainerCondition {
23                    conditions: input.vec1(ContainerConditionKind::Not(container_condition_not)),
24                    span,
25                })
26            }
27            _ => {
28                let first = input.parse::<QueryInParens>()?;
29                let mut span = first.span.clone();
30                let mut conditions = input.vec1(ContainerConditionKind::QueryInParens(first));
31                // formally `and`/`or` may not mix without parens and `not`
32                // is leading-only, but real-world code (less.js) chains them
33                // freely: `(a) or (b) and (c)`, `(a) not (b)`.
34                loop {
35                    let kind = match &input.cursor.peek()?.token {
36                        Token::Ident(ident) if ident.name().eq_ignore_ascii_case("and") => {
37                            ContainerConditionKind::And(input.parse()?)
38                        }
39                        Token::Ident(ident) if ident.name().eq_ignore_ascii_case("or") => {
40                            ContainerConditionKind::Or(input.parse()?)
41                        }
42                        Token::Ident(ident) if ident.name().eq_ignore_ascii_case("not") => {
43                            ContainerConditionKind::Not(input.parse()?)
44                        }
45                        _ => break,
46                    };
47                    conditions.push(kind);
48                }
49
50                if let Some(last) = conditions.last() {
51                    span.end = last.span().end;
52                }
53                Ok(ContainerCondition { conditions, span })
54            }
55        }
56    }
57}
58
59// and <query-in-parens>
60impl<'a> Parse<'a> for ContainerConditionAnd<'a> {
61    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
62        let keyword = input.parse::<Ident>()?;
63        if keyword.name.eq_ignore_ascii_case("and") {
64            let query_in_parens = input.parse::<QueryInParens>()?;
65            let span = Span { start: keyword.span.start, end: query_in_parens.span.end };
66            Ok(ContainerConditionAnd { keyword, query_in_parens, span })
67        } else {
68            Err(Error { kind: ErrorKind::ExpectContainerConditionAnd, span: keyword.span })
69        }
70    }
71}
72
73// not <query-in-parens>
74impl<'a> Parse<'a> for ContainerConditionNot<'a> {
75    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
76        let keyword = input.parse::<Ident>()?;
77        if keyword.name.eq_ignore_ascii_case("not") {
78            let query_in_parens = input.parse::<QueryInParens>()?;
79            let span = Span { start: keyword.span.start, end: query_in_parens.span.end };
80            Ok(ContainerConditionNot { keyword, query_in_parens, span })
81        } else {
82            Err(Error { kind: ErrorKind::ExpectContainerConditionNot, span: keyword.span })
83        }
84    }
85}
86
87// or <query-in-parens>
88impl<'a> Parse<'a> for ContainerConditionOr<'a> {
89    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
90        let keyword = input.parse::<Ident>()?;
91        if keyword.name.eq_ignore_ascii_case("or") {
92            let query_in_parens = input.parse::<QueryInParens>()?;
93            let span = Span { start: keyword.span.start, end: query_in_parens.span.end };
94            Ok(ContainerConditionOr { keyword, query_in_parens, span })
95        } else {
96            Err(Error { kind: ErrorKind::ExpectContainerConditionOr, span: keyword.span })
97        }
98    }
99}
100
101// <query-in-parens> = ( <container-query> )
102//                   | ( <size-feature> )
103//                   | style( <style-query> )
104//                   | scroll-state( <scroll-state-query> )
105//                   | <general-enclosed>
106impl<'a> Parse<'a> for QueryInParens<'a> {
107    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
108        if let Some((_, Span { start, .. })) = input.cursor.eat_l_paren()? {
109            let kind = if let Ok(container_condition) = input.try_parse(ContainerCondition::parse) {
110                QueryInParensKind::ContainerCondition(container_condition)
111            } else {
112                let size_feature = input.parse()?;
113                QueryInParensKind::SizeFeature(input.alloc(size_feature))
114            };
115            let (_, Span { end, .. }) = input.cursor.expect_r_paren()?;
116            Ok(QueryInParens { kind, span: Span { start, end } })
117        } else {
118            let (style_keyword, ident_span) = input.cursor.expect_ident()?;
119            let keyword = style_keyword.name();
120            if keyword.eq_ignore_ascii_case("style") {
121                input.cursor.expect_l_paren_without_ws_or_comments()?;
122                let kind = input.parse().map(QueryInParensKind::StyleQuery)?;
123                let (_, Span { end, .. }) = input.cursor.expect_r_paren()?;
124                Ok(QueryInParens { kind, span: Span { start: ident_span.start, end } })
125            } else if keyword.eq_ignore_ascii_case("scroll-state") {
126                // https://drafts.csswg.org/css-conditional-5/#scroll-state-container
127                input.cursor.expect_l_paren_without_ws_or_comments()?;
128                let media = input.parse()?;
129                let kind = QueryInParensKind::ScrollState(input.alloc(media));
130                let (_, Span { end, .. }) = input.cursor.expect_r_paren()?;
131                Ok(QueryInParens { kind, span: Span { start: ident_span.start, end } })
132            } else {
133                Err(Error { kind: ErrorKind::ExpectStyleQuery, span: ident_span })
134            }
135        }
136    }
137}
138
139// https://drafts.csswg.org/css-contain-3/#typedef-style-query
140//
141// <style-condition> = not <style-in-parens>
142//                   | <style-in-parens> [ [ and <style-in-parens> ]* | [ or <style-in-parens> ]* ]
143impl<'a> Parse<'a> for StyleCondition<'a> {
144    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
145        match &input.cursor.peek()?.token {
146            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("not") => {
147                let style_condition_not = input.parse::<StyleConditionNot>()?;
148                let span = style_condition_not.span.clone();
149                Ok(StyleCondition {
150                    conditions: input.vec1(StyleConditionKind::Not(style_condition_not)),
151                    span,
152                })
153            }
154            _ => {
155                let first = input.parse::<StyleInParens>()?;
156                let mut span = first.span.clone();
157                let mut conditions = input.vec1(StyleConditionKind::StyleInParens(first));
158                if let Token::Ident(ident) = &input.cursor.peek()?.token {
159                    let name = ident.name();
160                    if name.eq_ignore_ascii_case("and") {
161                        loop {
162                            conditions.push(StyleConditionKind::And(input.parse()?));
163                            match &input.cursor.peek()?.token {
164                                Token::Ident(ident) if ident.name().eq_ignore_ascii_case("and") => {
165                                }
166                                _ => break,
167                            }
168                        }
169                    } else if name.eq_ignore_ascii_case("or") {
170                        loop {
171                            conditions.push(StyleConditionKind::Or(input.parse()?));
172                            match &input.cursor.peek()?.token {
173                                Token::Ident(ident) if ident.name().eq_ignore_ascii_case("or") => {}
174                                _ => break,
175                            }
176                        }
177                    }
178                }
179
180                if let Some(last) = conditions.last() {
181                    span.end = last.span().end;
182                }
183                Ok(StyleCondition { conditions, span })
184            }
185        }
186    }
187}
188
189// and <style-in-parens>
190impl<'a> Parse<'a> for StyleConditionAnd<'a> {
191    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
192        let ident = input.parse::<Ident>()?;
193        if ident.name.eq_ignore_ascii_case("and") {
194            let style_in_parens = input.parse::<StyleInParens>()?;
195            let span = Span { start: ident.span.start, end: style_in_parens.span.end };
196            Ok(StyleConditionAnd { keyword: ident, style_in_parens, span })
197        } else {
198            Err(Error { kind: ErrorKind::ExpectStyleConditionAnd, span: ident.span })
199        }
200    }
201}
202
203// not <style-in-parens>
204impl<'a> Parse<'a> for StyleConditionNot<'a> {
205    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
206        let keyword = input.parse::<Ident>()?;
207        if keyword.name.eq_ignore_ascii_case("not") {
208            let style_in_parens = input.parse::<StyleInParens>()?;
209            let span = Span { start: keyword.span.start, end: style_in_parens.span.end };
210            Ok(StyleConditionNot { keyword, style_in_parens, span })
211        } else {
212            Err(Error { kind: ErrorKind::ExpectStyleConditionNot, span: keyword.span })
213        }
214    }
215}
216
217// or <style-in-parens>
218impl<'a> Parse<'a> for StyleConditionOr<'a> {
219    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
220        let keyword = input.parse::<Ident>()?;
221        if keyword.name.eq_ignore_ascii_case("or") {
222            let style_in_parens = input.parse::<StyleInParens>()?;
223            let span = Span { start: keyword.span.start, end: style_in_parens.span.end };
224            Ok(StyleConditionOr { keyword, style_in_parens, span })
225        } else {
226            Err(Error { kind: ErrorKind::ExpectStyleConditionOr, span: keyword.span })
227        }
228    }
229}
230
231// <style-in-parens> = ( <style-condition> ) | ( <style-feature> ) | <general-enclosed>
232impl<'a> Parse<'a> for StyleInParens<'a> {
233    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
234        let (_, Span { start, .. }) = input.cursor.expect_l_paren()?;
235        let kind = input.parse()?;
236        let (_, Span { end, .. }) = input.cursor.expect_r_paren()?;
237        Ok(StyleInParens { kind, span: Span { start, end } })
238    }
239}
240
241// <style-condition> | <style-feature>
242impl<'a> Parse<'a> for StyleInParensKind<'a> {
243    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
244        if let Ok(style_condition) = input.try_parse(StyleCondition::parse) {
245            Ok(StyleInParensKind::Condition(style_condition))
246        } else {
247            input.parse().map(StyleInParensKind::Feature)
248        }
249    }
250}
251
252// <style-query> = <style-condition> | <style-feature>
253// (a bare custom-property name, e.g. `style(--theme)`, is a boolean-context <style-feature>)
254impl<'a> Parse<'a> for StyleQuery<'a> {
255    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
256        if let Ok(condition) = input.try_parse(StyleCondition::parse) {
257            Ok(StyleQuery::Condition(condition))
258        } else if let Ok(name) = input.try_parse(|p| {
259            // a bare custom-property existence test: `style(--theme)`
260            let name = p.parse::<InterpolableIdent>()?;
261            match (&name, &p.cursor.peek()?.token) {
262                (InterpolableIdent::Literal(ident), Token::RParen(..))
263                    if ident.name.starts_with("--") =>
264                {
265                    Ok(name)
266                }
267                _ => {
268                    let span = p.cursor.peek()?.span.clone();
269                    Err(Error { kind: ErrorKind::TryParseError, span })
270                }
271            }
272        }) {
273            Ok(StyleQuery::FeatureName(name))
274        } else {
275            let feature = input.parse().map(StyleQuery::Feature);
276            input.cursor.eat_semicolon()?;
277            feature
278        }
279    }
280}
281
282// https://drafts.csswg.org/css-contain-3/#container-rule
283//
284// @container <container-name>? <container-query> { <block-contents> }
285// <container-name> = <custom-ident>
286impl<'a> Parse<'a> for ContainerPrelude<'a> {
287    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
288        let name = input.try_parse(|parser| match parser.parse()? {
289            InterpolableIdent::Literal(ident)
290                if ident.name.eq_ignore_ascii_case("not")
291                    || ident.name.eq_ignore_ascii_case("scroll-state") =>
292            {
293                Err(Error { kind: ErrorKind::TryParseError, span: ident.span })
294            }
295            InterpolableIdent::Literal(ident) if ident.name.eq_ignore_ascii_case("style") => {
296                match parser.cursor.peek()? {
297                    TokenWithSpan { token: Token::LParen(..), span }
298                        if span.start == ident.span.end =>
299                    {
300                        Err(Error { kind: ErrorKind::TryParseError, span: ident.span })
301                    }
302                    _ => Ok(InterpolableIdent::Literal(ident)),
303                }
304            }
305            ident => Ok(ident),
306        });
307        let condition = input.parse::<ContainerCondition>()?;
308        let mut span = condition.span().clone();
309        if let Ok(name) = &name {
310            span.start = name.span().start;
311        }
312        Ok(ContainerPrelude { name: name.ok(), condition, span })
313    }
314}