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 and: Ident = match input.cursor.bump()? {
29            TokenWithSpan { token: Token::Ident(ident), span }
30                if ident.name().eq_ignore_ascii_case("and") =>
31            {
32                input.ident(ident, span)
33            }
34            TokenWithSpan { span, .. } => {
35                return Err(Error { kind: ErrorKind::ExpectMediaAnd, span });
36            }
37        };
38
39        let condition = input.parse_media_condition(
40            /* allow_or */ false, /* after_logic_keyword */ true,
41        )?;
42
43        let span = Span { start: and.span.start, end: condition.span.end };
44        Ok(MediaConditionAfterMediaType { and, condition, span })
45    }
46}
47
48// https://www.w3.org/TR/mediaqueries-4/#mq-features
49//
50// <media-feature> = ( [ <mf-plain> | <mf-boolean> | <mf-range> ] )
51// <mf-plain>   = <mf-name> : <mf-value>
52// <mf-boolean> = <mf-name>
53impl<'a> Parse<'a> for MediaFeature<'a> {
54    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
55        match input.parse_media_feature_value()? {
56            ComponentValue::InterpolableIdent(ident) => match &input.cursor.peek()?.token {
57                Token::Colon(..) => input.parse_media_feature_plain(ident).map(MediaFeature::Plain),
58                Token::LessThan(..)
59                | Token::LessThanEqual(..)
60                | Token::GreaterThan(..)
61                | Token::GreaterThanEqual(..)
62                | Token::Equal(..) => input.parse_media_feature_range_or_range_interval(
63                    ComponentValue::InterpolableIdent(ident),
64                ),
65                _ => {
66                    let span = ident.span().clone();
67                    Ok(MediaFeature::Boolean(MediaFeatureBoolean {
68                        name: MediaFeatureName::Ident(ident),
69                        span,
70                    }))
71                }
72            },
73            ComponentValue::SassVariable(variable) => {
74                let span = variable.span.clone();
75                Ok(MediaFeature::Boolean(MediaFeatureBoolean {
76                    name: MediaFeatureName::SassVariable(variable),
77                    span,
78                }))
79            }
80            ComponentValue::PostcssSimpleVar(variable) => {
81                let span = variable.span.clone();
82                Ok(MediaFeature::Boolean(MediaFeatureBoolean {
83                    name: MediaFeatureName::PostcssSimpleVar(variable),
84                    span,
85                }))
86            }
87            value => input.parse_media_feature_range_or_range_interval(value),
88        }
89    }
90}
91
92// <mf-comparison> = '<' | '>' | '<=' | '>=' | '='
93impl<'a> Parse<'a> for MediaFeatureComparison {
94    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
95        match input.cursor.bump()? {
96            TokenWithSpan { token: Token::LessThan(..), span } => {
97                Ok(MediaFeatureComparison { kind: MediaFeatureComparisonKind::LessThan, span })
98            }
99            TokenWithSpan { token: Token::LessThanEqual(..), span } => Ok(MediaFeatureComparison {
100                kind: MediaFeatureComparisonKind::LessThanOrEqual,
101                span,
102            }),
103            TokenWithSpan { token: Token::GreaterThan(..), span } => {
104                Ok(MediaFeatureComparison { kind: MediaFeatureComparisonKind::GreaterThan, span })
105            }
106            TokenWithSpan { token: Token::GreaterThanEqual(..), span } => {
107                Ok(MediaFeatureComparison {
108                    kind: MediaFeatureComparisonKind::GreaterThanOrEqual,
109                    span,
110                })
111            }
112            TokenWithSpan { token: Token::Equal(..), span } => {
113                Ok(MediaFeatureComparison { kind: MediaFeatureComparisonKind::Equal, span })
114            }
115            TokenWithSpan { span, .. } => {
116                Err(Error { kind: ErrorKind::ExpectMediaFeatureComparison, span })
117            }
118        }
119    }
120}
121
122// <media-in-parens> = ( <media-condition> ) | <media-feature> | <general-enclosed>
123impl<'a> Parse<'a> for MediaInParens<'a> {
124    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
125        // Sass allows an interpolation wherever `<media-in-parens>` is expected,
126        // e.g. `@media screen and #{$query} {}`.
127        if matches!(input.syntax, Syntax::Scss | Syntax::Sass)
128            && matches!(&input.cursor.peek()?.token, Token::HashLBrace(..))
129            && let InterpolableIdent::SassInterpolated(interpolation) =
130                input.parse_sass_interpolated_ident()?
131        {
132            let span = interpolation.span.clone();
133            return Ok(MediaInParens {
134                kind: MediaInParensKind::SassInterpolation(interpolation),
135                span,
136            });
137        }
138        let (_, Span { start, .. }) = input.cursor.expect_l_paren()?;
139        let kind = input.parse()?;
140        let (_, Span { end, .. }) = input.cursor.expect_r_paren()?;
141        Ok(MediaInParens { kind, span: Span { start, end } })
142    }
143}
144
145// The contents inside the parens: ( <media-condition> ) | <media-feature> | <general-enclosed>
146impl<'a> Parse<'a> for MediaInParensKind<'a> {
147    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
148        if let Ok(media_condition) = input.try_parse(|parser| {
149            let media_condition = parser.parse_media_condition(
150                /* allow_or */ true, /* after_logic_keyword */ false,
151            )?;
152            // `(#{$x}-width: 1px)`: the interpolation parses as a
153            // `SassInterpolation` media condition, but a trailing `:` means it is
154            // really a media feature name. Require the closing `)` here so such
155            // cases fall through to the media-feature branch below.
156            if matches!(&parser.cursor.peek()?.token, Token::RParen(..)) {
157                Ok(media_condition)
158            } else {
159                let span = parser.cursor.peek()?.span.clone();
160                Err(Error { kind: ErrorKind::ExpectMediaFeatureName, span })
161            }
162        }) {
163            Ok(MediaInParensKind::MediaCondition(media_condition))
164        } else if let Ok(media_feature) = input.try_parse(|parser| {
165            let media_feature = parser.parse::<MediaFeature>()?;
166            if matches!(&parser.cursor.peek()?.token, Token::RParen(..)) {
167                Ok(media_feature)
168            } else {
169                let span = parser.cursor.peek()?.span.clone();
170                Err(Error { kind: ErrorKind::ExpectMediaFeatureName, span })
171            }
172        }) {
173            Ok(MediaInParensKind::MediaFeature(input.alloc(media_feature)))
174        } else {
175            // <general-enclosed>: MQ L4 forward-compat catch-all, evaluates false at runtime.
176            let tokens = input.parse_tokens_in_parens()?;
177            Ok(MediaInParensKind::GeneralEnclosed(tokens))
178        }
179    }
180}
181
182// <media-not> = not <media-in-parens>
183impl<'a> Parse<'a> for MediaNot<'a> {
184    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
185        let keyword = input.parse::<Ident>()?;
186        if keyword.name.eq_ignore_ascii_case("not") {
187            let media_in_parens = input.parse::<MediaInParens>()?;
188            let span = Span { start: keyword.span.start, end: media_in_parens.span.end };
189            Ok(MediaNot { keyword, media_in_parens, span })
190        } else {
191            Err(Error { kind: ErrorKind::ExpectMediaNot, span: keyword.span })
192        }
193    }
194}
195
196// <media-or> = or <media-in-parens>
197impl<'a> Parse<'a> for MediaOr<'a> {
198    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
199        let keyword = input.parse::<Ident>()?;
200        if keyword.name.eq_ignore_ascii_case("or") {
201            let media_in_parens = input.parse_media_in_parens_after_logic()?;
202            let span = Span { start: keyword.span.start, end: media_in_parens.span.end };
203            Ok(MediaOr { keyword, media_in_parens, span })
204        } else {
205            Err(Error { kind: ErrorKind::ExpectMediaOr, span: keyword.span })
206        }
207    }
208}
209
210// https://www.w3.org/TR/mediaqueries-4/#mq-syntax
211//
212// <media-query> = <media-condition>
213//               | [ not | only ]? <media-type> [ and <media-condition-without-or> ]?
214impl<'a> Parse<'a> for MediaQuery<'a> {
215    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
216        if let Ok(condition_only) = input.try_parse(|parser| {
217            parser.parse_media_condition(
218                /* allow_or */ true, /* after_logic_keyword */ false,
219            )
220        }) {
221            Ok(MediaQuery::ConditionOnly(condition_only))
222        } else if input.syntax == Syntax::Less {
223            match input.cursor.peek()?.token {
224                Token::AtKeyword(..) => {
225                    input.parse_less_maybe_variable_or_with_lookups().map(|value| match value {
226                        ComponentValue::LessVariable(variable) => {
227                            MediaQuery::LessVariable(variable)
228                        }
229                        ComponentValue::LessNamespaceValue(namespace_value) => {
230                            MediaQuery::LessNamespaceValue(namespace_value)
231                        }
232                        _ => unreachable!(),
233                    })
234                }
235                Token::Dot(..) | Token::Hash(..) => {
236                    let less_namespace_value = input.parse()?;
237                    Ok(MediaQuery::LessNamespaceValue(input.alloc(less_namespace_value)))
238                }
239                _ => input.parse_media_query_with_type_or_function(),
240            }
241        } else {
242            input.parse_media_query_with_type_or_function()
243        }
244    }
245}
246
247// https://www.w3.org/TR/mediaqueries-4/#mq-syntax
248//
249// <media-query-list> = <media-query>#
250impl<'a> Parse<'a> for MediaQueryList<'a> {
251    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
252        let first = input.parse::<MediaQuery>()?;
253        let mut span = first.span().clone();
254
255        let mut queries = input.vec1(first);
256        let mut comma_spans = input.vec();
257        while let Some((_, comma_span)) = input.cursor.eat_comma()? {
258            comma_spans.push(comma_span);
259            queries.push(input.parse()?);
260        }
261        debug_assert_eq!(comma_spans.len() + 1, queries.len());
262
263        // SAFETY: it has at least one element.
264        span.end = unsafe {
265            let index = queries.len() - 1;
266            queries.get_unchecked(index).span().end
267        };
268        Ok(MediaQueryList { queries, comma_spans, span })
269    }
270}
271
272// [ not | only ]? <media-type> [ and <media-condition-without-or> ]?
273// <media-type> = <ident>   (not `only` / `not` / `and` / `or` / `layer`)
274impl<'a> Parse<'a> for MediaQueryWithType<'a> {
275    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
276        let modifier = if let Token::Ident(ident) = &input.cursor.peek()?.token {
277            let name = ident.name();
278            if name.eq_ignore_ascii_case("not") || name.eq_ignore_ascii_case("only") {
279                Some(input.parse::<Ident>()?)
280            } else {
281                None
282            }
283        } else {
284            None
285        };
286        let media_type = input.parse::<InterpolableIdent>()?;
287        if let InterpolableIdent::Literal(Ident { name, span, .. }) = &media_type
288            && (name.eq_ignore_ascii_case("only")
289                || name.eq_ignore_ascii_case("not")
290                || name.eq_ignore_ascii_case("and")
291                || name.eq_ignore_ascii_case("or")
292                || name.eq_ignore_ascii_case("layer"))
293        {
294            input.recoverable_errors.push(Error {
295                kind: ErrorKind::MediaTypeKeywordDisallowed(name.to_string()),
296                span: span.clone(),
297            });
298        }
299        let condition = match &input.cursor.peek()?.token {
300            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("and") => {
301                input.parse::<MediaConditionAfterMediaType>().map(Some)?
302            }
303            _ => None,
304        };
305
306        let mut span = media_type.span().clone();
307        if let Some(modifier) = &modifier {
308            span.start = modifier.span.start;
309        }
310        if let Some(condition) = &condition {
311            span.end = condition.span.end;
312        }
313        Ok(MediaQueryWithType { modifier, media_type, condition, span })
314    }
315}
316
317impl<'a> Parser<'a> {
318    /// `<media-in-parens>` right after `and`/`or`. A bare ident there
319    /// (`@media screen and print`) is not valid MQ syntax, but browsers still
320    /// parse the rule — the query merely evaluates to "not all" — so preserve
321    /// it like `<general-enclosed>` raw tokens. Only this position is safe to
322    /// relax: elsewhere an ident is a media type (`@media print`).
323    fn parse_media_in_parens_after_logic(&mut self) -> PResult<MediaInParens<'a>> {
324        if self.syntax == Syntax::Css
325            && let TokenWithSpan { token: Token::Ident(ident), span } = self.cursor.peek()?
326            && !ident.name().eq_ignore_ascii_case("not")
327            && self.source.as_bytes().get(span.end) != Some(&b'(')
328        {
329            let token = self.cursor.bump()?;
330            let span = token.span.clone();
331            return Ok(MediaInParens {
332                kind: MediaInParensKind::GeneralEnclosed(TokenSeq {
333                    tokens: self.vec1(token),
334                    span: span.clone(),
335                }),
336                span,
337            });
338        }
339        self.parse()
340    }
341
342    // <media-condition>            = <media-not> | <media-in-parens> [ <media-and>* | <media-or>* ]
343    // <media-condition-without-or> = <media-not> | <media-in-parens> <media-and>*   (allow_or = false)
344    fn parse_media_condition(
345        &mut self,
346        allow_or: bool,
347        after_logic_keyword: bool,
348    ) -> PResult<MediaCondition<'a>> {
349        match &self.cursor.peek()?.token {
350            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("not") => {
351                let media_not = self.parse::<MediaNot>()?;
352                let span = media_not.span.clone();
353                Ok(MediaCondition {
354                    conditions: self.vec1(MediaConditionKind::Not(media_not)),
355                    span,
356                })
357            }
358            _ => {
359                let first = if after_logic_keyword {
360                    self.parse_media_in_parens_after_logic()?
361                } else {
362                    self.parse::<MediaInParens>()?
363                };
364                let mut span = first.span.clone();
365                let mut conditions = self.vec1(MediaConditionKind::MediaInParens(first));
366                if let Token::Ident(ident) = &self.cursor.peek()?.token {
367                    let name = ident.name();
368                    if name.eq_ignore_ascii_case("and") {
369                        loop {
370                            conditions.push(MediaConditionKind::And(self.parse()?));
371                            match &self.cursor.peek()?.token {
372                                Token::Ident(ident) if ident.name().eq_ignore_ascii_case("and") => {
373                                }
374                                _ => break,
375                            }
376                        }
377                    } else if allow_or && name.eq_ignore_ascii_case("or") {
378                        loop {
379                            conditions.push(MediaConditionKind::Or(self.parse()?));
380                            match &self.cursor.peek()?.token {
381                                Token::Ident(ident) if ident.name().eq_ignore_ascii_case("or") => {}
382                                _ => break,
383                            }
384                        }
385                    }
386                }
387
388                if let Some(last) = conditions.last() {
389                    span.end = last.span().end;
390                }
391                Ok(MediaCondition { conditions, span })
392            }
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().clone(),
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}