Skip to main content

oxc_css_parser/parser/at_rule/
import.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// https://www.w3.org/TR/css-cascade-5/#at-import
11impl<'a> Parse<'a> for ImportPrelude<'a> {
12    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
13        // The indented syntax accepts an unquoted path (`@import other.css`),
14        // but an ident glued to `(` is a function href
15        // (`@import url("theme.css")`), which the `Url::parse` arm handles.
16        let sass_unquoted_path = input.syntax == Syntax::Sass
17            && matches!(input.cursor.peek()?, TokenWithSpan { token: Token::Ident(..), span }
18                if input.source.as_bytes().get(span.end) != Some(&b'('));
19        let href = match &input.cursor.peek()?.token {
20            Token::Str(..) | Token::StrTemplate(..) => input.parse().map(ImportPreludeHref::Str)?,
21            Token::Ident(..) if sass_unquoted_path => {
22                let start = input.cursor.peek()?.span.start;
23                let mut end = start;
24                while matches!(
25                    &input.cursor.peek()?.token,
26                    Token::Ident(..) | Token::Dot(..) | Token::Minus(..) | Token::Solidus(..)
27                ) && input.cursor.peek()?.span.start == end
28                {
29                    end = input.cursor.bump()?.span.end;
30                }
31                let raw = unsafe { input.source.get_unchecked(start..end) };
32                let span = Span { start, end };
33                ImportPreludeHref::Str(InterpolableStr::Literal(Str { value: raw, raw, span }))
34            }
35            _ => match input.try_parse(Url::parse) {
36                Ok(url) => ImportPreludeHref::Url(url),
37                // Sass only: the content of `url(...)` may be SassScript that
38                // is not a parsable URL, e.g. `@import url($dir+"/path");`.
39                // Mirrors the fallback in `parse_component_value_atom`.
40                Err(error) if matches!(input.syntax, Syntax::Scss | Syntax::Sass) => {
41                    let (function_name, function_name_span) = input.cursor.expect_ident()?;
42                    let function_name = input.ident(function_name, function_name_span);
43                    if !function_name.name.eq_ignore_ascii_case("url") {
44                        return Err(error);
45                    }
46                    input
47                        .parse_function(InterpolableIdent::Literal(function_name))
48                        .map(ImportPreludeHref::Function)
49                        .map_err(|_| error)?
50                }
51                Err(error) => return Err(error),
52            },
53        };
54        let mut span = href.span().clone();
55
56        let structured = input.try_parse(Self::parse_structured_tail);
57        let (layer, supports, media, modifiers) = match structured {
58            Ok((layer, supports, media)) => (layer, supports, media, None),
59            // Reference compilers accept arbitrary import modifiers (idents,
60            // unknown functions, media-ish parens, further comma-chained
61            // imports); keep the whole tail as raw component values.
62            Err(_) => {
63                let start = input.cursor.peek()?.span.start;
64                let values = input.parse_declaration_value_tokens(true)?;
65                // A trailing comma has no import after it — reference
66                // compilers reject that, so don't paper over it.
67                if let Some(ComponentValue::TokenWithSpan(TokenWithSpan {
68                    token: Token::Comma(..),
69                    span,
70                })) = values.last()
71                {
72                    return Err(Error { kind: ErrorKind::ExpectRule, span: span.clone() });
73                }
74                let end = values.last().map_or(start, |value| value.span().end);
75                (None, None, None, Some(ComponentValues { values, span: Span { start, end } }))
76            }
77        };
78        if let Some(layer) = &layer {
79            span.end = layer.span().end;
80        }
81        if let Some(supports) = &supports {
82            span.end = supports.span().end;
83        }
84        if let Some(media) = &media {
85            span.end = media.span.end;
86        }
87        if let Some(modifiers) = &modifiers
88            && modifiers.span.end > modifiers.span.start
89        {
90            span.end = modifiers.span.end;
91        }
92
93        Ok(ImportPrelude { href, layer, supports, media, modifiers, span })
94    }
95}
96
97impl<'a> ImportPrelude<'a> {
98    /// The standard post-URL grammar: optional `layer`/`layer(...)`, optional
99    /// `supports(...)`, optional media query list — valid only if it accounts
100    /// for everything up to the end of the statement.
101    #[allow(clippy::type_complexity)]
102    fn parse_structured_tail(
103        input: &mut Parser<'a>,
104    ) -> PResult<(
105        Option<ImportPreludeLayer<'a>>,
106        Option<ImportPreludeSupports<'a>>,
107        Option<MediaQueryList<'a>>,
108    )> {
109        let layer = match &input.cursor.peek()?.token {
110            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("layer") => {
111                let ident = input.parse::<Ident>()?;
112                let layer = match input.cursor.peek()? {
113                    TokenWithSpan { token: Token::LParen(..), span }
114                        if span.start == ident.span.end =>
115                    {
116                        input.cursor.bump()?;
117                        let layer_name = input.parse().map(ComponentValue::LayerName)?;
118                        let args = input.vec1(layer_name);
119                        let end = input.cursor.expect_r_paren()?.1.end;
120                        let span = Span { start: ident.span.start, end };
121                        ImportPreludeLayer::WithName(Function {
122                            name: FunctionName::Ident(InterpolableIdent::Literal(ident)),
123                            args,
124                            span,
125                        })
126                    }
127                    _ => ImportPreludeLayer::Empty(ident),
128                };
129                Some(layer)
130            }
131            _ => None,
132        };
133
134        let supports = input.try_parse(|parser| {
135            // (kept as its own try so a non-`supports` ident rolls back)
136            let (ident, span) = parser.cursor.expect_ident()?;
137            if !ident.name().eq_ignore_ascii_case("supports") {
138                return Err(Error { kind: ErrorKind::TryParseError, span });
139            }
140
141            parser.cursor.expect_l_paren_without_ws_or_comments()?;
142
143            let kind = if let Ok(supports_condition) = parser.try_parse(SupportsCondition::parse) {
144                ImportPreludeSupportsKind::SupportsCondition(supports_condition)
145            } else {
146                parser.parse().map(ImportPreludeSupportsKind::Declaration)?
147            };
148            let (_, Span { end, .. }) = parser.cursor.expect_r_paren()?;
149            Ok(ImportPreludeSupports { kind, span: Span { start: span.start, end } })
150        });
151        // `}` ends the at-rule too, so an `@import` nested in a style rule needs no
152        // trailing `;` (`a { @import "b.css" }`); it can't start a media query.
153        let media = if at_import_prelude_end(&input.cursor.peek()?.token) {
154            None
155        } else {
156            Some(input.parse::<MediaQueryList>()?)
157        };
158
159        // Anything left over means this tail isn't the standard grammar.
160        if at_import_prelude_end(&input.cursor.peek()?.token) {
161            Ok((layer, supports.ok(), media))
162        } else {
163            let span = input.cursor.peek()?.span.clone();
164            Err(Error { kind: ErrorKind::TryParseError, span })
165        }
166    }
167}
168
169/// End of an `@import` prelude: the statement boundary tokens.
170fn at_import_prelude_end(token: &Token) -> bool {
171    matches!(
172        token,
173        Token::Semicolon(..)
174            | Token::Eof(..)
175            | Token::RBrace(..)
176            | Token::Dedent(..)
177            | Token::Linebreak(..)
178    )
179}