Skip to main content

oxc_css_parser/parser/at_rule/
media.rs

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