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
56                    .parse_media_feature_plain(ident)
57                    .map(|f| MediaFeature::Plain(input.alloc(f))),
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();
67                    Ok(MediaFeature::Boolean(MediaFeatureBoolean {
68                        name: MediaFeatureName::Ident(ident),
69                        span,
70                    }))
71                }
72            },
73            ComponentValue::SassVariable(variable) => {
74                let span = variable.span;
75                Ok(MediaFeature::Boolean(MediaFeatureBoolean {
76                    name: MediaFeatureName::SassVariable(variable),
77                    span,
78                }))
79            }
80            ComponentValue::PostcssSimpleVar(variable) => {
81                let span = variable.span;
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;
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;
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;
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();
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 = {
277            let peek = input.cursor.peek()?;
278            if peek.is_ident_name_eq_ignore_ascii_case(input.source, "not")
279                || peek.is_ident_name_eq_ignore_ascii_case(input.source, "only")
280            {
281                Some(input.parse::<Ident>()?)
282            } else {
283                None
284            }
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,
297            });
298        }
299        let condition =
300            if input.cursor.peek()?.is_ident_name_eq_ignore_ascii_case(input.source, "and") {
301                input.parse::<MediaConditionAfterMediaType>().map(Some)?
302            } else {
303                None
304            };
305
306        let mut span = *media_type.span();
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        let relax_bare_ident = {
325            let peek = self.cursor.peek()?;
326            self.syntax == Syntax::Css
327                && peek.ident(self.source).is_some()
328                && !peek.is_ident_name_eq_ignore_ascii_case(self.source, "not")
329                && self.source.as_bytes().get(peek.span.end) != Some(&b'(')
330        };
331        if relax_bare_ident {
332            let token = self.cursor.bump()?;
333            let span = token.span;
334            return Ok(MediaInParens {
335                kind: MediaInParensKind::GeneralEnclosed(TokenSeq {
336                    tokens: self.vec1(token),
337                    span,
338                }),
339                span,
340            });
341        }
342        self.parse()
343    }
344
345    // <media-condition>            = <media-not> | <media-in-parens> [ <media-and>* | <media-or>* ]
346    // <media-condition-without-or> = <media-not> | <media-in-parens> <media-and>*   (allow_or = false)
347    fn parse_media_condition(
348        &mut self,
349        allow_or: bool,
350        after_logic_keyword: bool,
351    ) -> PResult<MediaCondition<'a>> {
352        if self.cursor.peek()?.is_ident_name_eq_ignore_ascii_case(self.source, "not") {
353            let media_not = self.parse::<MediaNot>()?;
354            let span = media_not.span;
355            Ok(MediaCondition { conditions: self.vec1(MediaConditionKind::Not(media_not)), span })
356        } else {
357            let first = if after_logic_keyword {
358                self.parse_media_in_parens_after_logic()?
359            } else {
360                self.parse::<MediaInParens>()?
361            };
362            let mut span = first.span;
363            let mut conditions = self.vec1(MediaConditionKind::MediaInParens(first));
364            let peek = self.cursor.peek()?;
365            if peek.ident(self.source).is_some() {
366                if peek.is_ident_name_eq_ignore_ascii_case(self.source, "and") {
367                    loop {
368                        conditions.push(MediaConditionKind::And(self.parse()?));
369                        if !self
370                            .cursor
371                            .peek()?
372                            .is_ident_name_eq_ignore_ascii_case(self.source, "and")
373                        {
374                            break;
375                        }
376                    }
377                } else if allow_or && peek.is_ident_name_eq_ignore_ascii_case(self.source, "or") {
378                    loop {
379                        conditions.push(MediaConditionKind::Or(self.parse()?));
380                        if !self
381                            .cursor
382                            .peek()?
383                            .is_ident_name_eq_ignore_ascii_case(self.source, "or")
384                        {
385                            break;
386                        }
387                    }
388                }
389            }
390
391            if let Some(last) = conditions.last() {
392                span.end = last.span().end;
393            }
394            Ok(MediaCondition { conditions, span })
395        }
396    }
397
398    // <mf-plain> = <mf-name> : <mf-value>
399    fn parse_media_feature_plain(
400        &mut self,
401        ident: InterpolableIdent<'a>,
402    ) -> PResult<MediaFeaturePlain<'a>> {
403        let (_, colon_span) = self.cursor.expect_colon()?;
404        let value = self.parse_media_feature_value()?;
405        let span = Span { start: ident.span().start, end: value.span().end };
406        Ok(MediaFeaturePlain { name: MediaFeatureName::Ident(ident), colon_span, value, span })
407    }
408
409    // <mf-range> = <mf-name>  <mf-comparison> <mf-value>                             (range)
410    //            | <mf-value> <mf-comparison> <mf-name>                             (range)
411    //            | <mf-value> <mf-comparison> <mf-name> <mf-comparison> <mf-value>  (interval)
412    fn parse_media_feature_range_or_range_interval(
413        &mut self,
414        left: ComponentValue<'a>,
415    ) -> PResult<MediaFeature<'a>> {
416        let comparison = self.parse()?;
417        let name_or_right = self.parse_media_feature_value()?;
418        if let ComponentValue::InterpolableIdent(ident) = name_or_right {
419            match &self.cursor.peek()?.token {
420                Token::LessThan(..)
421                | Token::LessThanEqual(..)
422                | Token::GreaterThan(..)
423                | Token::GreaterThanEqual(..)
424                | Token::Equal(..) => {
425                    let right_comparison = self.parse()?;
426                    let right = self.parse_media_feature_value()?;
427                    let span = Span { start: left.span().start, end: right.span().end };
428                    Ok(MediaFeature::RangeInterval(self.alloc(MediaFeatureRangeInterval {
429                        left,
430                        left_comparison: comparison,
431                        name: MediaFeatureName::Ident(ident),
432                        right_comparison,
433                        right,
434                        span,
435                    })))
436                }
437                _ => {
438                    let span = Span { start: left.span().start, end: ident.span().end };
439                    Ok(MediaFeature::Range(self.alloc(MediaFeatureRange {
440                        left,
441                        comparison,
442                        right: ComponentValue::InterpolableIdent(ident),
443                        span,
444                    })))
445                }
446            }
447        } else {
448            if !matches!(left, ComponentValue::InterpolableIdent(..))
449                && !matches!(name_or_right, ComponentValue::InterpolableIdent(..))
450            {
451                self.recoverable_errors.push(Error {
452                    kind: ErrorKind::ExpectMediaFeatureName,
453                    span: *name_or_right.span(),
454                });
455            }
456            let span = Span { start: left.span().start, end: name_or_right.span().end };
457            Ok(MediaFeature::Range(self.alloc(MediaFeatureRange {
458                left,
459                comparison,
460                right: name_or_right,
461                span,
462            })))
463        }
464    }
465
466    // <mf-value> = <number> | <dimension> | <ident> | <ratio>
467    fn parse_media_feature_value(&mut self) -> PResult<ComponentValue<'a>> {
468        let value = match self.syntax {
469            Syntax::Css => self.parse_component_value_atom()?,
470            Syntax::Scss | Syntax::Sass => {
471                self.parse_sass_bin_expr(/* allow_comparison */ false)?
472            }
473            Syntax::Less => self.parse_less_operation(/* allow_mixin_call */ true)?,
474        };
475        match value {
476            ComponentValue::Number(number)
477                if number.value >= 0.0
478                    && matches!(self.cursor.peek()?.token, Token::Solidus(..)) =>
479            {
480                self.parse_ratio(number).map(ComponentValue::Ratio)
481            }
482            value => Ok(value),
483        }
484    }
485
486    // The `[ not | only ]? <media-type> …` branch of <media-query>; a media type
487    // glued to `(` is also accepted as a function form (`screen(...)`, used by Less).
488    fn parse_media_query_with_type_or_function(&mut self) -> PResult<MediaQuery<'a>> {
489        let media_query_with_type = self.parse::<MediaQueryWithType>()?;
490        match (media_query_with_type, self.cursor.peek()?) {
491            (
492                MediaQueryWithType {
493                    modifier: None,
494                    media_type: name,
495                    condition: None,
496                    span: mq_span,
497                },
498                TokenWithSpan { token: crate::token::Token::LParen(..), span: lparen_span },
499            ) if mq_span.end == lparen_span.start => {
500                self.cursor.bump()?;
501                let args = self.parse_function_args()?;
502                let (_, Span { end, .. }) = self.cursor.expect_r_paren()?;
503                Ok(MediaQuery::Function(Function {
504                    name: FunctionName::Ident(name),
505                    args,
506                    span: Span { start: mq_span.start, end },
507                }))
508            }
509            (media_query_with_type, _) => {
510                Ok(MediaQuery::WithType(self.alloc(media_query_with_type)))
511            }
512        }
513    }
514}