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