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