Skip to main content

oxc_css_parser/parser/at_rule/
media.rs

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