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
10impl<'a> Parse<'a> for MediaAnd<'a> {
11    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
12        let keyword = input.parse::<Ident>()?;
13        if keyword.name.eq_ignore_ascii_case("and") {
14            let media_in_parens = input.parse_media_in_parens_after_logic()?;
15            let span = Span { start: keyword.span.start, end: media_in_parens.span.end };
16            Ok(MediaAnd { keyword, media_in_parens, span })
17        } else {
18            Err(Error { kind: ErrorKind::ExpectMediaAnd, span: keyword.span })
19        }
20    }
21}
22
23impl<'a> Parse<'a> for MediaConditionAfterMediaType<'a> {
24    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
25        let and: Ident = match input.cursor.bump()? {
26            TokenWithSpan { token: Token::Ident(ident), span }
27                if ident.name().eq_ignore_ascii_case("and") =>
28            {
29                input.ident(ident, span)
30            }
31            TokenWithSpan { span, .. } => {
32                return Err(Error { kind: ErrorKind::ExpectMediaAnd, span });
33            }
34        };
35
36        let condition = input.parse_media_condition(
37            /* allow_or */ false, /* after_logic_keyword */ true,
38        )?;
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 &input.cursor.peek()?.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 input.cursor.bump()? {
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!(&input.cursor.peek()?.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, .. }) = input.cursor.expect_l_paren()?;
129        let kind = input.parse()?;
130        let (_, Span { end, .. }) = input.cursor.expect_r_paren()?;
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(
139                /* allow_or */ true, /* after_logic_keyword */ false,
140            )?;
141            // `(#{$x}-width: 1px)`: the interpolation parses as a
142            // `SassInterpolation` media condition, but a trailing `:` means it is
143            // really a media feature name. Require the closing `)` here so such
144            // cases fall through to the media-feature branch below.
145            if matches!(&parser.cursor.peek()?.token, Token::RParen(..)) {
146                Ok(media_condition)
147            } else {
148                let span = parser.cursor.peek()?.span.clone();
149                Err(Error { kind: ErrorKind::ExpectMediaFeatureName, span })
150            }
151        }) {
152            Ok(MediaInParensKind::MediaCondition(media_condition))
153        } else if let Ok(media_feature) = input.try_parse(|parser| {
154            let media_feature = parser.parse::<MediaFeature>()?;
155            if matches!(&parser.cursor.peek()?.token, Token::RParen(..)) {
156                Ok(media_feature)
157            } else {
158                let span = parser.cursor.peek()?.span.clone();
159                Err(Error { kind: ErrorKind::ExpectMediaFeatureName, span })
160            }
161        }) {
162            Ok(MediaInParensKind::MediaFeature(input.alloc(media_feature)))
163        } else {
164            // <general-enclosed>: MQ L4 forward-compat catch-all, evaluates false at runtime.
165            let tokens = input.parse_tokens_in_parens()?;
166            Ok(MediaInParensKind::GeneralEnclosed(tokens))
167        }
168    }
169}
170
171impl<'a> Parse<'a> for MediaNot<'a> {
172    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
173        let keyword = input.parse::<Ident>()?;
174        if keyword.name.eq_ignore_ascii_case("not") {
175            let media_in_parens = input.parse::<MediaInParens>()?;
176            let span = Span { start: keyword.span.start, end: media_in_parens.span.end };
177            Ok(MediaNot { keyword, media_in_parens, span })
178        } else {
179            Err(Error { kind: ErrorKind::ExpectMediaNot, span: keyword.span })
180        }
181    }
182}
183
184impl<'a> Parse<'a> for MediaOr<'a> {
185    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
186        let keyword = input.parse::<Ident>()?;
187        if keyword.name.eq_ignore_ascii_case("or") {
188            let media_in_parens = input.parse_media_in_parens_after_logic()?;
189            let span = Span { start: keyword.span.start, end: media_in_parens.span.end };
190            Ok(MediaOr { keyword, media_in_parens, span })
191        } else {
192            Err(Error { kind: ErrorKind::ExpectMediaOr, span: keyword.span })
193        }
194    }
195}
196
197impl<'a> Parse<'a> for MediaQuery<'a> {
198    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
199        if let Ok(condition_only) = input.try_parse(|parser| {
200            parser.parse_media_condition(
201                /* allow_or */ true, /* after_logic_keyword */ false,
202            )
203        }) {
204            Ok(MediaQuery::ConditionOnly(condition_only))
205        } else if input.syntax == Syntax::Less {
206            match input.cursor.peek()?.token {
207                Token::AtKeyword(..) => {
208                    input.parse_less_maybe_variable_or_with_lookups().map(|value| match value {
209                        ComponentValue::LessVariable(variable) => {
210                            MediaQuery::LessVariable(variable)
211                        }
212                        ComponentValue::LessNamespaceValue(namespace_value) => {
213                            MediaQuery::LessNamespaceValue(namespace_value)
214                        }
215                        _ => unreachable!(),
216                    })
217                }
218                Token::Dot(..) | Token::Hash(..) => {
219                    let less_namespace_value = input.parse()?;
220                    Ok(MediaQuery::LessNamespaceValue(input.alloc(less_namespace_value)))
221                }
222                _ => input.parse_media_query_with_type_or_function(),
223            }
224        } else {
225            input.parse_media_query_with_type_or_function()
226        }
227    }
228}
229
230// https://www.w3.org/TR/mediaqueries-4/#mq-syntax
231impl<'a> Parse<'a> for MediaQueryList<'a> {
232    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
233        let first = input.parse::<MediaQuery>()?;
234        let mut span = first.span().clone();
235
236        let mut queries = input.vec1(first);
237        let mut comma_spans = input.vec();
238        while let Some((_, comma_span)) = input.cursor.eat_comma()? {
239            comma_spans.push(comma_span);
240            queries.push(input.parse()?);
241        }
242        debug_assert_eq!(comma_spans.len() + 1, queries.len());
243
244        // SAFETY: it has at least one element.
245        span.end = unsafe {
246            let index = queries.len() - 1;
247            queries.get_unchecked(index).span().end
248        };
249        Ok(MediaQueryList { queries, comma_spans, span })
250    }
251}
252
253impl<'a> Parse<'a> for MediaQueryWithType<'a> {
254    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
255        let modifier = if let Token::Ident(ident) = &input.cursor.peek()?.token {
256            let name = ident.name();
257            if name.eq_ignore_ascii_case("not") || name.eq_ignore_ascii_case("only") {
258                Some(input.parse::<Ident>()?)
259            } else {
260                None
261            }
262        } else {
263            None
264        };
265        let media_type = input.parse::<InterpolableIdent>()?;
266        if let InterpolableIdent::Literal(Ident { name, span, .. }) = &media_type
267            && (name.eq_ignore_ascii_case("only")
268                || name.eq_ignore_ascii_case("not")
269                || name.eq_ignore_ascii_case("and")
270                || name.eq_ignore_ascii_case("or")
271                || name.eq_ignore_ascii_case("layer"))
272        {
273            input.recoverable_errors.push(Error {
274                kind: ErrorKind::MediaTypeKeywordDisallowed(name.to_string()),
275                span: span.clone(),
276            });
277        }
278        let condition = match &input.cursor.peek()?.token {
279            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("and") => {
280                input.parse::<MediaConditionAfterMediaType>().map(Some)?
281            }
282            _ => None,
283        };
284
285        let mut span = media_type.span().clone();
286        if let Some(modifier) = &modifier {
287            span.start = modifier.span.start;
288        }
289        if let Some(condition) = &condition {
290            span.end = condition.span.end;
291        }
292        Ok(MediaQueryWithType { modifier, media_type, condition, span })
293    }
294}
295
296impl<'a> Parser<'a> {
297    /// `<media-in-parens>` right after `and`/`or`. A bare ident there
298    /// (`@media screen and print`) is not valid MQ syntax, but browsers still
299    /// parse the rule — the query merely evaluates to "not all" — so preserve
300    /// it like `<general-enclosed>` raw tokens. Only this position is safe to
301    /// relax: elsewhere an ident is a media type (`@media print`).
302    fn parse_media_in_parens_after_logic(&mut self) -> PResult<MediaInParens<'a>> {
303        if self.syntax == Syntax::Css
304            && let TokenWithSpan { token: Token::Ident(ident), span } = self.cursor.peek()?
305            && !ident.name().eq_ignore_ascii_case("not")
306            && self.source.as_bytes().get(span.end) != Some(&b'(')
307        {
308            let token = self.cursor.bump()?;
309            let span = token.span.clone();
310            return Ok(MediaInParens {
311                kind: MediaInParensKind::GeneralEnclosed(TokenSeq {
312                    tokens: self.vec1(token),
313                    span: span.clone(),
314                }),
315                span,
316            });
317        }
318        self.parse()
319    }
320
321    fn parse_media_condition(
322        &mut self,
323        allow_or: bool,
324        after_logic_keyword: bool,
325    ) -> PResult<MediaCondition<'a>> {
326        match &self.cursor.peek()?.token {
327            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("not") => {
328                let media_not = self.parse::<MediaNot>()?;
329                let span = media_not.span.clone();
330                Ok(MediaCondition {
331                    conditions: self.vec1(MediaConditionKind::Not(media_not)),
332                    span,
333                })
334            }
335            _ => {
336                let first = if after_logic_keyword {
337                    self.parse_media_in_parens_after_logic()?
338                } else {
339                    self.parse::<MediaInParens>()?
340                };
341                let mut span = first.span.clone();
342                let mut conditions = self.vec1(MediaConditionKind::MediaInParens(first));
343                if let Token::Ident(ident) = &self.cursor.peek()?.token {
344                    let name = ident.name();
345                    if name.eq_ignore_ascii_case("and") {
346                        loop {
347                            conditions.push(MediaConditionKind::And(self.parse()?));
348                            match &self.cursor.peek()?.token {
349                                Token::Ident(ident) if ident.name().eq_ignore_ascii_case("and") => {
350                                }
351                                _ => break,
352                            }
353                        }
354                    } else if allow_or && name.eq_ignore_ascii_case("or") {
355                        loop {
356                            conditions.push(MediaConditionKind::Or(self.parse()?));
357                            match &self.cursor.peek()?.token {
358                                Token::Ident(ident) if ident.name().eq_ignore_ascii_case("or") => {}
359                                _ => break,
360                            }
361                        }
362                    }
363                }
364
365                if let Some(last) = conditions.last() {
366                    span.end = last.span().end;
367                }
368                Ok(MediaCondition { conditions, span })
369            }
370        }
371    }
372
373    fn parse_media_feature_plain(
374        &mut self,
375        ident: InterpolableIdent<'a>,
376    ) -> PResult<MediaFeaturePlain<'a>> {
377        let (_, colon_span) = self.cursor.expect_colon()?;
378        let value = self.parse_media_feature_value()?;
379        let span = Span { start: ident.span().start, end: value.span().end };
380        Ok(MediaFeaturePlain { name: MediaFeatureName::Ident(ident), colon_span, value, span })
381    }
382
383    fn parse_media_feature_range_or_range_interval(
384        &mut self,
385        left: ComponentValue<'a>,
386    ) -> PResult<MediaFeature<'a>> {
387        let comparison = self.parse()?;
388        let name_or_right = self.parse_media_feature_value()?;
389        if let ComponentValue::InterpolableIdent(ident) = name_or_right {
390            match &self.cursor.peek()?.token {
391                Token::LessThan(..)
392                | Token::LessThanEqual(..)
393                | Token::GreaterThan(..)
394                | Token::GreaterThanEqual(..)
395                | Token::Equal(..) => {
396                    let right_comparison = self.parse()?;
397                    let right = self.parse_media_feature_value()?;
398                    let span = Span { start: left.span().start, end: right.span().end };
399                    Ok(MediaFeature::RangeInterval(MediaFeatureRangeInterval {
400                        left,
401                        left_comparison: comparison,
402                        name: MediaFeatureName::Ident(ident),
403                        right_comparison,
404                        right,
405                        span,
406                    }))
407                }
408                _ => {
409                    let span = Span { start: left.span().start, end: ident.span().end };
410                    Ok(MediaFeature::Range(MediaFeatureRange {
411                        left,
412                        comparison,
413                        right: ComponentValue::InterpolableIdent(ident),
414                        span,
415                    }))
416                }
417            }
418        } else {
419            if !matches!(left, ComponentValue::InterpolableIdent(..))
420                && !matches!(name_or_right, ComponentValue::InterpolableIdent(..))
421            {
422                self.recoverable_errors.push(Error {
423                    kind: ErrorKind::ExpectMediaFeatureName,
424                    span: name_or_right.span().clone(),
425                });
426            }
427            let span = Span { start: left.span().start, end: name_or_right.span().end };
428            Ok(MediaFeature::Range(MediaFeatureRange {
429                left,
430                comparison,
431                right: name_or_right,
432                span,
433            }))
434        }
435    }
436
437    fn parse_media_feature_value(&mut self) -> PResult<ComponentValue<'a>> {
438        let value = match self.syntax {
439            Syntax::Css => self.parse_component_value_atom()?,
440            Syntax::Scss | Syntax::Sass => {
441                self.parse_sass_bin_expr(/* allow_comparison */ false)?
442            }
443            Syntax::Less => self.parse_less_operation(/* allow_mixin_call */ true)?,
444        };
445        match value {
446            ComponentValue::Number(number)
447                if number.value >= 0.0
448                    && matches!(self.cursor.peek()?.token, Token::Solidus(..)) =>
449            {
450                self.parse_ratio(number).map(ComponentValue::Ratio)
451            }
452            value => Ok(value),
453        }
454    }
455
456    fn parse_media_query_with_type_or_function(&mut self) -> PResult<MediaQuery<'a>> {
457        let media_query_with_type = self.parse::<MediaQueryWithType>()?;
458        match (media_query_with_type, self.cursor.peek()?) {
459            (
460                MediaQueryWithType {
461                    modifier: None,
462                    media_type: name,
463                    condition: None,
464                    span: mq_span,
465                },
466                TokenWithSpan { token: crate::token::Token::LParen(..), span: lparen_span },
467            ) if mq_span.end == lparen_span.start => {
468                self.cursor.bump()?;
469                let args = self.parse_function_args()?;
470                let (_, Span { end, .. }) = self.cursor.expect_r_paren()?;
471                Ok(MediaQuery::Function(Function {
472                    name: FunctionName::Ident(name),
473                    args,
474                    span: Span { start: mq_span.start, end },
475                }))
476            }
477            (media_query_with_type, _) => Ok(MediaQuery::WithType(media_query_with_type)),
478        }
479    }
480}