Skip to main content

oxc_css_parser/parser/at_rule/
media.rs

1use super::Parser;
2use crate::{
3    Parse, Syntax, arena_box, arena_vec,
4    ast::*,
5    bump, eat,
6    error::{Error, ErrorKind, PResult},
7    expect, peek,
8    pos::{Span, Spanned},
9    tokenizer::{Token, TokenWithSpan},
10};
11
12impl<'a> Parse<'a> for MediaAnd<'a> {
13    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
14        let keyword = input.parse::<Ident>()?;
15        if keyword.name.eq_ignore_ascii_case("and") {
16            let media_in_parens = input.parse::<MediaInParens>()?;
17            let span = Span { start: keyword.span.start, end: media_in_parens.span.end };
18            Ok(MediaAnd { keyword, media_in_parens, span })
19        } else {
20            Err(Error { kind: ErrorKind::ExpectMediaAnd, span: keyword.span })
21        }
22    }
23}
24
25impl<'a> Parse<'a> for MediaConditionAfterMediaType<'a> {
26    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
27        let and: Ident = match bump!(input) {
28            TokenWithSpan { token: Token::Ident(ident), span }
29                if ident.name().eq_ignore_ascii_case("and") =>
30            {
31                input.ident(ident, span)
32            }
33            TokenWithSpan { span, .. } => {
34                return Err(Error { kind: ErrorKind::ExpectMediaAnd, span });
35            }
36        };
37
38        let condition = input.parse_media_condition(/* allow_or */ false)?;
39
40        let span = Span { start: and.span.start, end: condition.span.end };
41        Ok(MediaConditionAfterMediaType { and, condition, span })
42    }
43}
44
45impl<'a> Parse<'a> for MediaFeature<'a> {
46    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
47        match input.parse_media_feature_value()? {
48            ComponentValue::InterpolableIdent(ident) => match &peek!(input).token {
49                Token::Colon(..) => input.parse_media_feature_plain(ident).map(MediaFeature::Plain),
50                Token::LessThan(..)
51                | Token::LessThanEqual(..)
52                | Token::GreaterThan(..)
53                | Token::GreaterThanEqual(..)
54                | Token::Equal(..) => input.parse_media_feature_range_or_range_interval(
55                    ComponentValue::InterpolableIdent(ident),
56                ),
57                _ => {
58                    let span = ident.span().clone();
59                    Ok(MediaFeature::Boolean(MediaFeatureBoolean {
60                        name: MediaFeatureName::Ident(ident),
61                        span,
62                    }))
63                }
64            },
65            ComponentValue::SassVariable(variable) => {
66                let span = variable.span.clone();
67                Ok(MediaFeature::Boolean(MediaFeatureBoolean {
68                    name: MediaFeatureName::SassVariable(variable),
69                    span,
70                }))
71            }
72            ComponentValue::PostcssSimpleVar(variable) => {
73                let span = variable.span.clone();
74                Ok(MediaFeature::Boolean(MediaFeatureBoolean {
75                    name: MediaFeatureName::PostcssSimpleVar(variable),
76                    span,
77                }))
78            }
79            value => input.parse_media_feature_range_or_range_interval(value),
80        }
81    }
82}
83
84impl<'a> Parse<'a> for MediaFeatureComparison {
85    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
86        match bump!(input) {
87            TokenWithSpan { token: Token::LessThan(..), span } => {
88                Ok(MediaFeatureComparison { kind: MediaFeatureComparisonKind::LessThan, span })
89            }
90            TokenWithSpan { token: Token::LessThanEqual(..), span } => Ok(MediaFeatureComparison {
91                kind: MediaFeatureComparisonKind::LessThanOrEqual,
92                span,
93            }),
94            TokenWithSpan { token: Token::GreaterThan(..), span } => {
95                Ok(MediaFeatureComparison { kind: MediaFeatureComparisonKind::GreaterThan, span })
96            }
97            TokenWithSpan { token: Token::GreaterThanEqual(..), span } => {
98                Ok(MediaFeatureComparison {
99                    kind: MediaFeatureComparisonKind::GreaterThanOrEqual,
100                    span,
101                })
102            }
103            TokenWithSpan { token: Token::Equal(..), span } => {
104                Ok(MediaFeatureComparison { kind: MediaFeatureComparisonKind::Equal, span })
105            }
106            TokenWithSpan { span, .. } => {
107                Err(Error { kind: ErrorKind::ExpectMediaFeatureComparison, span })
108            }
109        }
110    }
111}
112
113impl<'a> Parse<'a> for MediaInParens<'a> {
114    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
115        // Sass allows an interpolation wherever `<media-in-parens>` is expected,
116        // e.g. `@media screen and #{$query} {}`.
117        if matches!(input.syntax, Syntax::Scss | Syntax::Sass)
118            && matches!(&peek!(input).token, Token::HashLBrace(..))
119            && let InterpolableIdent::SassInterpolated(interpolation) =
120                input.parse_sass_interpolated_ident()?
121        {
122            let span = interpolation.span.clone();
123            return Ok(MediaInParens {
124                kind: MediaInParensKind::SassInterpolation(interpolation),
125                span,
126            });
127        }
128        let (_, Span { start, .. }) = expect!(input, LParen);
129        let kind = input.parse()?;
130        let (_, Span { end, .. }) = expect!(input, RParen);
131        Ok(MediaInParens { kind, span: Span { start, end } })
132    }
133}
134
135impl<'a> Parse<'a> for MediaInParensKind<'a> {
136    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
137        if let Ok(media_condition) = input.try_parse(|parser| {
138            let media_condition = parser.parse_media_condition(/* allow_or */ true)?;
139            // `(#{$x}-width: 1px)`: the interpolation parses as a
140            // `SassInterpolation` media condition, but a trailing `:` means it is
141            // really a media feature name. Require the closing `)` here so such
142            // cases fall through to the media-feature branch below.
143            if matches!(&peek!(parser).token, Token::RParen(..)) {
144                Ok(media_condition)
145            } else {
146                let span = peek!(parser).span.clone();
147                Err(Error { kind: ErrorKind::ExpectMediaFeatureName, span })
148            }
149        }) {
150            Ok(MediaInParensKind::MediaCondition(media_condition))
151        } else if let Ok(media_feature) = input.try_parse(|parser| {
152            let media_feature = parser.parse::<MediaFeature>()?;
153            if matches!(&peek!(parser).token, Token::RParen(..)) {
154                Ok(media_feature)
155            } else {
156                let span = peek!(parser).span.clone();
157                Err(Error { kind: ErrorKind::ExpectMediaFeatureName, span })
158            }
159        }) {
160            Ok(MediaInParensKind::MediaFeature(arena_box!(input, media_feature)))
161        } else {
162            // <general-enclosed>: MQ L4 forward-compat catch-all, evaluates false at runtime.
163            let tokens = input.parse_tokens_in_parens()?;
164            Ok(MediaInParensKind::GeneralEnclosed(tokens))
165        }
166    }
167}
168
169impl<'a> Parse<'a> for MediaNot<'a> {
170    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
171        let keyword = input.parse::<Ident>()?;
172        if keyword.name.eq_ignore_ascii_case("not") {
173            let media_in_parens = input.parse::<MediaInParens>()?;
174            let span = Span { start: keyword.span.start, end: media_in_parens.span.end };
175            Ok(MediaNot { keyword, media_in_parens, span })
176        } else {
177            Err(Error { kind: ErrorKind::ExpectMediaNot, span: keyword.span })
178        }
179    }
180}
181
182impl<'a> Parse<'a> for MediaOr<'a> {
183    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
184        let keyword = input.parse::<Ident>()?;
185        if keyword.name.eq_ignore_ascii_case("or") {
186            let media_in_parens = input.parse::<MediaInParens>()?;
187            let span = Span { start: keyword.span.start, end: media_in_parens.span.end };
188            Ok(MediaOr { keyword, media_in_parens, span })
189        } else {
190            Err(Error { kind: ErrorKind::ExpectMediaOr, span: keyword.span })
191        }
192    }
193}
194
195impl<'a> Parse<'a> for MediaQuery<'a> {
196    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
197        if let Ok(condition_only) =
198            input.try_parse(|parser| parser.parse_media_condition(/* allow_or */ true))
199        {
200            Ok(MediaQuery::ConditionOnly(condition_only))
201        } else if input.syntax == Syntax::Less {
202            match peek!(input).token {
203                Token::AtKeyword(..) => {
204                    input.parse_less_maybe_variable_or_with_lookups().map(|value| match value {
205                        ComponentValue::LessVariable(variable) => {
206                            MediaQuery::LessVariable(variable)
207                        }
208                        ComponentValue::LessNamespaceValue(namespace_value) => {
209                            MediaQuery::LessNamespaceValue(namespace_value)
210                        }
211                        _ => unreachable!(),
212                    })
213                }
214                Token::Dot(..) | Token::Hash(..) => {
215                    let less_namespace_value = input.parse()?;
216                    Ok(MediaQuery::LessNamespaceValue(arena_box!(input, less_namespace_value)))
217                }
218                _ => input.parse_media_query_with_type_or_function(),
219            }
220        } else {
221            input.parse_media_query_with_type_or_function()
222        }
223    }
224}
225
226// https://www.w3.org/TR/mediaqueries-4/#mq-syntax
227impl<'a> Parse<'a> for MediaQueryList<'a> {
228    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
229        let first = input.parse::<MediaQuery>()?;
230        let mut span = first.span().clone();
231
232        let mut queries = arena_vec!(input; first);
233        let mut comma_spans = arena_vec!(input);
234        while let Some((_, comma_span)) = eat!(input, Comma) {
235            comma_spans.push(comma_span);
236            queries.push(input.parse()?);
237        }
238        debug_assert_eq!(comma_spans.len() + 1, queries.len());
239
240        // SAFETY: it has at least one element.
241        span.end = unsafe {
242            let index = queries.len() - 1;
243            queries.get_unchecked(index).span().end
244        };
245        Ok(MediaQueryList { queries, comma_spans, span })
246    }
247}
248
249impl<'a> Parse<'a> for MediaQueryWithType<'a> {
250    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
251        let modifier = if let Token::Ident(ident) = &peek!(input).token {
252            let name = ident.name();
253            if name.eq_ignore_ascii_case("not") || name.eq_ignore_ascii_case("only") {
254                Some(input.parse::<Ident>()?)
255            } else {
256                None
257            }
258        } else {
259            None
260        };
261        let media_type = input.parse::<InterpolableIdent>()?;
262        if let InterpolableIdent::Literal(Ident { name, span, .. }) = &media_type
263            && (name.eq_ignore_ascii_case("only")
264                || name.eq_ignore_ascii_case("not")
265                || name.eq_ignore_ascii_case("and")
266                || name.eq_ignore_ascii_case("or")
267                || name.eq_ignore_ascii_case("layer"))
268        {
269            input.recoverable_errors.push(Error {
270                kind: ErrorKind::MediaTypeKeywordDisallowed(name.to_string()),
271                span: span.clone(),
272            });
273        }
274        let condition = match &peek!(input).token {
275            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("and") => {
276                input.parse::<MediaConditionAfterMediaType>().map(Some)?
277            }
278            _ => None,
279        };
280
281        let mut span = media_type.span().clone();
282        if let Some(modifier) = &modifier {
283            span.start = modifier.span.start;
284        }
285        if let Some(condition) = &condition {
286            span.end = condition.span.end;
287        }
288        Ok(MediaQueryWithType { modifier, media_type, condition, span })
289    }
290}
291
292impl<'a> Parser<'a> {
293    fn parse_media_condition(&mut self, allow_or: bool) -> PResult<MediaCondition<'a>> {
294        match &peek!(self).token {
295            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("not") => {
296                let media_not = self.parse::<MediaNot>()?;
297                let span = media_not.span.clone();
298                Ok(MediaCondition {
299                    conditions: arena_vec!(self; MediaConditionKind::Not(media_not)),
300                    span,
301                })
302            }
303            _ => {
304                let first = self.parse::<MediaInParens>()?;
305                let mut span = first.span.clone();
306                let mut conditions = arena_vec!(self; MediaConditionKind::MediaInParens(first));
307                if let Token::Ident(ident) = &peek!(self).token {
308                    let name = ident.name();
309                    if name.eq_ignore_ascii_case("and") {
310                        loop {
311                            conditions.push(MediaConditionKind::And(self.parse()?));
312                            match &peek!(self).token {
313                                Token::Ident(ident) if ident.name().eq_ignore_ascii_case("and") => {
314                                }
315                                _ => break,
316                            }
317                        }
318                    } else if allow_or && name.eq_ignore_ascii_case("or") {
319                        loop {
320                            conditions.push(MediaConditionKind::Or(self.parse()?));
321                            match &peek!(self).token {
322                                Token::Ident(ident) if ident.name().eq_ignore_ascii_case("or") => {}
323                                _ => break,
324                            }
325                        }
326                    }
327                }
328
329                if let Some(last) = conditions.last() {
330                    span.end = last.span().end;
331                }
332                Ok(MediaCondition { conditions, span })
333            }
334        }
335    }
336
337    fn parse_media_feature_plain(
338        &mut self,
339        ident: InterpolableIdent<'a>,
340    ) -> PResult<MediaFeaturePlain<'a>> {
341        let (_, colon_span) = expect!(self, Colon);
342        let value = self.parse_media_feature_value()?;
343        let span = Span { start: ident.span().start, end: value.span().end };
344        Ok(MediaFeaturePlain { name: MediaFeatureName::Ident(ident), colon_span, value, span })
345    }
346
347    fn parse_media_feature_range_or_range_interval(
348        &mut self,
349        left: ComponentValue<'a>,
350    ) -> PResult<MediaFeature<'a>> {
351        let comparison = self.parse()?;
352        let name_or_right = self.parse_media_feature_value()?;
353        if let ComponentValue::InterpolableIdent(ident) = name_or_right {
354            match &peek!(self).token {
355                Token::LessThan(..)
356                | Token::LessThanEqual(..)
357                | Token::GreaterThan(..)
358                | Token::GreaterThanEqual(..)
359                | Token::Equal(..) => {
360                    let right_comparison = self.parse()?;
361                    let right = self.parse_media_feature_value()?;
362                    let span = Span { start: left.span().start, end: right.span().end };
363                    Ok(MediaFeature::RangeInterval(MediaFeatureRangeInterval {
364                        left,
365                        left_comparison: comparison,
366                        name: MediaFeatureName::Ident(ident),
367                        right_comparison,
368                        right,
369                        span,
370                    }))
371                }
372                _ => {
373                    let span = Span { start: left.span().start, end: ident.span().end };
374                    Ok(MediaFeature::Range(MediaFeatureRange {
375                        left,
376                        comparison,
377                        right: ComponentValue::InterpolableIdent(ident),
378                        span,
379                    }))
380                }
381            }
382        } else {
383            if !matches!(left, ComponentValue::InterpolableIdent(..))
384                && !matches!(name_or_right, ComponentValue::InterpolableIdent(..))
385            {
386                self.recoverable_errors.push(Error {
387                    kind: ErrorKind::ExpectMediaFeatureName,
388                    span: name_or_right.span().clone(),
389                });
390            }
391            let span = Span { start: left.span().start, end: name_or_right.span().end };
392            Ok(MediaFeature::Range(MediaFeatureRange {
393                left,
394                comparison,
395                right: name_or_right,
396                span,
397            }))
398        }
399    }
400
401    fn parse_media_feature_value(&mut self) -> PResult<ComponentValue<'a>> {
402        let value = match self.syntax {
403            Syntax::Css => self.parse_component_value_atom()?,
404            Syntax::Scss | Syntax::Sass => {
405                self.parse_sass_bin_expr(/* allow_comparison */ false)?
406            }
407            Syntax::Less => self.parse_less_operation(/* allow_mixin_call */ true)?,
408        };
409        match value {
410            ComponentValue::Number(number)
411                if number.value >= 0.0 && matches!(peek!(self).token, Token::Solidus(..)) =>
412            {
413                self.parse_ratio(number).map(ComponentValue::Ratio)
414            }
415            value => Ok(value),
416        }
417    }
418
419    fn parse_media_query_with_type_or_function(&mut self) -> PResult<MediaQuery<'a>> {
420        let media_query_with_type = self.parse::<MediaQueryWithType>()?;
421        match (media_query_with_type, peek!(self)) {
422            (
423                MediaQueryWithType {
424                    modifier: None,
425                    media_type: name,
426                    condition: None,
427                    span: mq_span,
428                },
429                TokenWithSpan { token: crate::token::Token::LParen(..), span: lparen_span },
430            ) if mq_span.end == lparen_span.start => {
431                bump!(self);
432                let args = self.parse_function_args()?;
433                let (_, Span { end, .. }) = expect!(self, RParen);
434                Ok(MediaQuery::Function(Function {
435                    name: FunctionName::Ident(name),
436                    args,
437                    span: Span { start: mq_span.start, end },
438                }))
439            }
440            (media_query_with_type, _) => Ok(MediaQuery::WithType(media_query_with_type)),
441        }
442    }
443}