Skip to main content

oxc_css_parser/parser/
sass.rs

1use super::{
2    Parser,
3    state::{ParserState, SASS_CTX_ALLOW_DIV, SASS_CTX_IN_PARENS},
4};
5use crate::{
6    Parse,
7    ast::*,
8    config::Syntax,
9    error::{Error, ErrorKind, PResult},
10    pos::Span,
11    tokenizer::{Token, TokenWithSpan},
12    util,
13};
14
15const PRECEDENCE_MULTIPLY: u8 = 6;
16const PRECEDENCE_PLUS: u8 = 5;
17const PRECEDENCE_RELATIONAL: u8 = 4;
18const PRECEDENCE_EQUALITY: u8 = 3;
19const PRECEDENCE_AND: u8 = 2;
20const PRECEDENCE_OR: u8 = 1;
21
22type SassParams<'a> = (
23    oxc_allocator::Vec<'a, SassParameter<'a>>,
24    Option<SassArbitraryParameter<'a>>,
25    oxc_allocator::Vec<'a, Span>, // comma spans
26    usize,                        // end pos
27);
28
29impl<'a> Parser<'a> {
30    /// In the indented syntax a statement may continue onto following lines
31    /// at points where the grammar still requires more input (`@for $i`
32    /// newline `from ...`, `$a:` newline `b`, a dangling binary operator).
33    /// Skip the line-structure tokens there. A deeper line's `Indent` is
34    /// remembered in [`Parser::sass_pending_indents`]: the statement's own
35    /// block then starts "virtually" at that depth (see [`SimpleBlock`]), and
36    /// leftovers are drained against `Dedent`s after the statement.
37    pub(super) fn eat_sass_line_continuation(&mut self) -> PResult<()> {
38        if self.syntax != Syntax::Sass {
39            return Ok(());
40        }
41        loop {
42            match &self.cursor.peek()?.token {
43                Token::Indent(..) => {
44                    self.cursor.bump()?;
45                    self.sass_pending_indents += 1;
46                }
47                Token::Linebreak(..) => {
48                    self.cursor.bump()?;
49                }
50                _ => break,
51            }
52        }
53        Ok(())
54    }
55
56    /// Cancel pending continuation indents against incoming `Dedent`s (their
57    /// mirror tokens). Returns whether any were drained.
58    pub(super) fn drain_sass_pending_dedents(&mut self) -> PResult<bool> {
59        let mut drained = false;
60        while self.sass_pending_indents > 0
61            && matches!(self.cursor.peek()?.token, Token::Dedent(..))
62        {
63            self.cursor.bump()?;
64            self.sass_pending_indents -= 1;
65            drained = true;
66        }
67        Ok(drained)
68    }
69
70    pub(super) fn parse_maybe_sass_list(
71        &mut self,
72        allow_comma: bool,
73    ) -> PResult<ComponentValue<'a>> {
74        use util::ListSeparatorKind;
75
76        let single_value = if allow_comma {
77            self.parse_maybe_sass_list(false)?
78        } else if let Token::Exclamation(..) = self.cursor.peek()?.token {
79            self.parse().map(ComponentValue::ImportantAnnotation)?
80        } else {
81            self.parse_sass_bin_expr(/* allow_comparison */ true)?
82        };
83
84        let mut elements = self.vec();
85        let mut comma_spans: Option<oxc_allocator::Vec<'a, Span>> = None;
86        let mut separator = ListSeparatorKind::Unknown;
87        let mut end = single_value.span().end;
88        loop {
89            match self.cursor.peek()?.token {
90                Token::LBrace(..)
91                | Token::RBrace(..)
92                | Token::RParen(..)
93                | Token::Semicolon(..)
94                | Token::Colon(..)
95                | Token::DotDotDot(..)
96                | Token::Eof(..) => break,
97                Token::Indent(..) | Token::Dedent(..) | Token::Linebreak(..) => {
98                    if comma_spans.as_ref().is_none_or(|spans| spans.is_empty())
99                        || self.state.sass_ctx & SASS_CTX_IN_PARENS == 0
100                    {
101                        break;
102                    } else {
103                        self.cursor.bump()?;
104                    }
105                }
106                Token::Comma(..) => {
107                    if !allow_comma {
108                        break;
109                    }
110                    if separator == ListSeparatorKind::Space {
111                        break;
112                    } else {
113                        if separator == ListSeparatorKind::Unknown {
114                            separator = ListSeparatorKind::Comma;
115                        }
116                        let TokenWithSpan { span, .. } = self.cursor.bump()?;
117                        end = span.end;
118                        if let Some(spans) = &mut comma_spans {
119                            spans.push(span);
120                        } else {
121                            comma_spans = Some(self.vec1(span));
122                        }
123                    }
124                }
125                Token::Exclamation(..) => {
126                    if let Ok(important_annotation) = self.try_parse(ImportantAnnotation::parse) {
127                        if end < important_annotation.span.start
128                            && matches!(separator, ListSeparatorKind::Unknown)
129                        {
130                            separator = ListSeparatorKind::Space;
131                        }
132                        end = important_annotation.span.end;
133                        elements.push(ComponentValue::ImportantAnnotation(important_annotation));
134                    } else {
135                        break;
136                    }
137                }
138                _ => {
139                    if separator == ListSeparatorKind::Unknown {
140                        separator = ListSeparatorKind::Space;
141                    }
142                    let item = if separator == ListSeparatorKind::Comma {
143                        self.parse_maybe_sass_list(false)?
144                    } else {
145                        self.parse_sass_bin_expr(/* allow_comparison */ true)?
146                    };
147                    end = item.span().end;
148                    elements.push(item);
149                }
150            }
151        }
152
153        if elements.is_empty() && separator != ListSeparatorKind::Comma {
154            // If there is a trailing comma it can be a Sass list,
155            // though there is only one element.
156            Ok(single_value)
157        } else {
158            debug_assert_ne!(separator, ListSeparatorKind::Unknown);
159
160            let span = Span { start: single_value.span().start, end };
161            elements.insert(0, single_value);
162            Ok(ComponentValue::SassList(SassList { elements, comma_spans, span }))
163        }
164    }
165
166    pub(super) fn parse_sass_bin_expr(
167        &mut self,
168        allow_comparison: bool,
169    ) -> PResult<ComponentValue<'a>> {
170        debug_assert!(matches!(self.syntax, Syntax::Scss | Syntax::Sass));
171        self.parse_sass_bin_expr_with_min_precedence(PRECEDENCE_OR, allow_comparison)
172    }
173
174    fn parse_sass_bin_expr_with_min_precedence(
175        &mut self,
176        min_precedence: u8,
177        allow_comparison: bool,
178    ) -> PResult<ComponentValue<'a>> {
179        let mut left = self.parse_sass_unary_expression()?;
180
181        // delimiter can't be calculated
182        if matches!(left, ComponentValue::Delimiter(..)) {
183            return Ok(left);
184        }
185
186        loop {
187            if PRECEDENCE_PLUS >= min_precedence {
188                match self.cursor.peek()? {
189                    TokenWithSpan { token: Token::Number(token), span }
190                        if token.raw.starts_with('+')
191                            || token.raw.starts_with('-') && span.start == left.span().end =>
192                    {
193                        let (number, number_span) = self.cursor.expect_number()?;
194                        let op = SassBinaryOperator {
195                            kind: if number.raw.starts_with('+') {
196                                SassBinaryOperatorKind::Plus
197                            } else {
198                                SassBinaryOperatorKind::Minus
199                            },
200                            span: Span { start: number_span.start, end: number_span.start + 1 },
201                        };
202                        let span = Span { start: left.span().start, end: number_span.end };
203                        let right = {
204                            let span = Span { start: number_span.start + 1, end: number_span.end };
205                            let raw = unsafe { number.raw.get_unchecked(1..number.raw.len()) };
206                            raw.parse()
207                                .map_err(|_| Error {
208                                    kind: ErrorKind::InvalidNumber,
209                                    span: span.clone(),
210                                })
211                                .map(|value| ComponentValue::Number(Number { value, raw, span }))?
212                        };
213                        left = ComponentValue::SassBinaryExpression(SassBinaryExpression {
214                            left: self.alloc(left),
215                            op,
216                            right: self.alloc(right),
217                            span,
218                        });
219                        continue;
220                    }
221                    TokenWithSpan { token: Token::Dimension(token), span }
222                        if token.value.raw.starts_with('+')
223                            || token.value.raw.starts_with('-')
224                                && span.start == left.span().end =>
225                    {
226                        let (dimension, dimension_span) = self.cursor.expect_dimension()?;
227                        let op = SassBinaryOperator {
228                            kind: if dimension.value.raw.starts_with('+') {
229                                SassBinaryOperatorKind::Plus
230                            } else {
231                                SassBinaryOperatorKind::Minus
232                            },
233                            span: Span {
234                                start: dimension_span.start,
235                                end: dimension_span.start + 1,
236                            },
237                        };
238                        let span = Span { start: left.span().start, end: dimension_span.end };
239                        let right = {
240                            self.dimension(
241                                crate::token::Dimension {
242                                    value: crate::token::Number {
243                                        raw: unsafe {
244                                            dimension
245                                                .value
246                                                .raw
247                                                .get_unchecked(1..dimension.value.raw.len())
248                                        },
249                                    },
250                                    unit: dimension.unit,
251                                },
252                                Span { start: dimension_span.start + 1, end: dimension_span.end },
253                            )
254                            .map(ComponentValue::Dimension)?
255                        };
256                        left = ComponentValue::SassBinaryExpression(SassBinaryExpression {
257                            left: self.alloc(left),
258                            op,
259                            right: self.alloc(right),
260                            span,
261                        });
262                        continue;
263                    }
264                    _ => {}
265                }
266            }
267
268            let (operator, precedence) = match self.cursor.peek()? {
269                TokenWithSpan { token: Token::Asterisk(..), .. }
270                    if PRECEDENCE_MULTIPLY >= min_precedence =>
271                {
272                    (
273                        SassBinaryOperator {
274                            kind: SassBinaryOperatorKind::Multiply,
275                            span: self.cursor.bump()?.span,
276                        },
277                        PRECEDENCE_MULTIPLY,
278                    )
279                }
280                TokenWithSpan { token: Token::Solidus(..), .. }
281                    if PRECEDENCE_MULTIPLY >= min_precedence
282                        && (self.state.sass_ctx & SASS_CTX_ALLOW_DIV != 0
283                            || matches!(
284                                left,
285                                ComponentValue::SassParenthesizedExpression(..)
286                                    | ComponentValue::SassBinaryExpression(..)
287                                    | ComponentValue::SassUnaryExpression(..)
288                                    | ComponentValue::SassVariable(..)
289                                    | ComponentValue::Function(..)
290                                    | ComponentValue::SassQualifiedName(..)
291                            )) =>
292                {
293                    (
294                        SassBinaryOperator {
295                            kind: SassBinaryOperatorKind::Division,
296                            span: self.cursor.bump()?.span,
297                        },
298                        PRECEDENCE_MULTIPLY,
299                    )
300                }
301                TokenWithSpan { token: Token::Percent(..), .. }
302                    if PRECEDENCE_MULTIPLY >= min_precedence =>
303                {
304                    // `b: c %` — a bare `%` is a plain value token in Sass, so
305                    // only take it as the modulo operator when a right operand
306                    // actually follows.
307                    let modulo = self.try_parse(|p| {
308                        let op_span = p.cursor.bump()?.span;
309                        p.eat_sass_line_continuation()?;
310                        let right = p.parse_sass_bin_expr_with_min_precedence(
311                            PRECEDENCE_MULTIPLY + 1,
312                            allow_comparison,
313                        )?;
314                        Ok((op_span, right))
315                    });
316                    match modulo {
317                        Ok((op_span, right)) => {
318                            let span = Span { start: left.span().start, end: right.span().end };
319                            left = ComponentValue::SassBinaryExpression(SassBinaryExpression {
320                                left: self.alloc(left),
321                                op: SassBinaryOperator {
322                                    kind: SassBinaryOperatorKind::Modulo,
323                                    span: op_span,
324                                },
325                                right: self.alloc(right),
326                                span,
327                            });
328                            continue;
329                        }
330                        Err(_) => break,
331                    }
332                }
333                TokenWithSpan { token: Token::Plus(..), .. }
334                    if PRECEDENCE_PLUS >= min_precedence =>
335                {
336                    (
337                        SassBinaryOperator {
338                            kind: SassBinaryOperatorKind::Plus,
339                            span: self.cursor.bump()?.span,
340                        },
341                        PRECEDENCE_PLUS,
342                    )
343                }
344                TokenWithSpan { token: Token::Minus(..), .. }
345                    if PRECEDENCE_PLUS >= min_precedence =>
346                {
347                    (
348                        SassBinaryOperator {
349                            kind: SassBinaryOperatorKind::Minus,
350                            span: self.cursor.bump()?.span,
351                        },
352                        PRECEDENCE_PLUS,
353                    )
354                }
355                TokenWithSpan { token: Token::GreaterThan(..), .. }
356                    if allow_comparison && PRECEDENCE_RELATIONAL >= min_precedence =>
357                {
358                    (
359                        SassBinaryOperator {
360                            kind: SassBinaryOperatorKind::GreaterThan,
361                            span: self.cursor.bump()?.span,
362                        },
363                        PRECEDENCE_RELATIONAL,
364                    )
365                }
366                TokenWithSpan { token: Token::GreaterThanEqual(..), .. }
367                    if allow_comparison && PRECEDENCE_RELATIONAL >= min_precedence =>
368                {
369                    (
370                        SassBinaryOperator {
371                            kind: SassBinaryOperatorKind::GreaterThanOrEqual,
372                            span: self.cursor.bump()?.span,
373                        },
374                        PRECEDENCE_RELATIONAL,
375                    )
376                }
377                TokenWithSpan { token: Token::LessThan(..), .. }
378                    if allow_comparison && PRECEDENCE_RELATIONAL >= min_precedence =>
379                {
380                    (
381                        SassBinaryOperator {
382                            kind: SassBinaryOperatorKind::LessThan,
383                            span: self.cursor.bump()?.span,
384                        },
385                        PRECEDENCE_RELATIONAL,
386                    )
387                }
388                TokenWithSpan { token: Token::LessThanEqual(..), .. }
389                    if allow_comparison && PRECEDENCE_RELATIONAL >= min_precedence =>
390                {
391                    (
392                        SassBinaryOperator {
393                            kind: SassBinaryOperatorKind::LessThanOrEqual,
394                            span: self.cursor.bump()?.span,
395                        },
396                        PRECEDENCE_RELATIONAL,
397                    )
398                }
399                TokenWithSpan { token: Token::EqualEqual(..), .. }
400                    if PRECEDENCE_EQUALITY >= min_precedence =>
401                {
402                    (
403                        SassBinaryOperator {
404                            kind: SassBinaryOperatorKind::EqualsEquals,
405                            span: self.cursor.bump()?.span,
406                        },
407                        PRECEDENCE_EQUALITY,
408                    )
409                }
410                TokenWithSpan { token: Token::ExclamationEqual(..), .. }
411                    if PRECEDENCE_EQUALITY >= min_precedence =>
412                {
413                    (
414                        SassBinaryOperator {
415                            kind: SassBinaryOperatorKind::ExclamationEquals,
416                            span: self.cursor.bump()?.span,
417                        },
418                        PRECEDENCE_EQUALITY,
419                    )
420                }
421                TokenWithSpan { token: Token::Ident(token), .. }
422                    if token.raw == "and" && PRECEDENCE_AND >= min_precedence =>
423                {
424                    (
425                        SassBinaryOperator {
426                            kind: SassBinaryOperatorKind::And,
427                            span: self.cursor.bump()?.span,
428                        },
429                        PRECEDENCE_AND,
430                    )
431                }
432                TokenWithSpan { token: Token::Ident(token), .. }
433                    if token.raw == "or" && PRECEDENCE_OR >= min_precedence =>
434                {
435                    (
436                        SassBinaryOperator {
437                            kind: SassBinaryOperatorKind::Or,
438                            span: self.cursor.bump()?.span,
439                        },
440                        PRECEDENCE_OR,
441                    )
442                }
443                _ => break,
444            };
445
446            // `$a == b and` may continue on the next line
447            self.eat_sass_line_continuation()?;
448            let right =
449                self.parse_sass_bin_expr_with_min_precedence(precedence + 1, allow_comparison)?;
450            // delimiter can't be calculated
451            if let ComponentValue::Delimiter(Delimiter { span, .. }) = right {
452                return Err(Error { kind: ErrorKind::ExpectSassExpression, span });
453            }
454
455            let span = Span { start: left.span().start, end: right.span().end };
456            left = ComponentValue::SassBinaryExpression(SassBinaryExpression {
457                left: self.alloc(left),
458                op: operator,
459                right: self.alloc(right),
460                span,
461            });
462        }
463
464        Ok(left)
465    }
466
467    fn parse_sass_flags(
468        &mut self,
469    ) -> PResult<(oxc_allocator::Vec<'a, SassFlag<'a>>, Option<usize>)> {
470        let mut flags: oxc_allocator::Vec<'a, SassFlag<'a>> = self.vec_with_capacity(1);
471        let mut end = None;
472        while let Some((_, exclamation_span)) = self.cursor.eat_exclamation()? {
473            let keyword = self.parse::<Ident>()?;
474            let keyword_span = keyword.span.clone();
475            util::assert_no_ws_or_comment(&exclamation_span, &keyword_span)?;
476            end = Some(keyword_span.end);
477
478            // duplicated flags (`!default !default`) are accepted, as in dart-sass
479            if !matches!(keyword.name, "default" | "global") {
480                self.recoverable_errors.push(Error {
481                    kind: ErrorKind::InvalidSassFlagName(keyword.name.to_string()),
482                    span: keyword.span.clone(),
483                });
484            }
485
486            flags.push(SassFlag {
487                keyword,
488                span: Span { start: exclamation_span.start, end: keyword_span.end },
489            });
490        }
491
492        Ok((flags, end))
493    }
494
495    pub(super) fn parse_sass_interpolated_ident(&mut self) -> PResult<InterpolableIdent<'a>> {
496        debug_assert!(matches!(self.syntax, Syntax::Scss | Syntax::Sass));
497
498        let (first, Span { start, mut end }) = match self.cursor.peek()? {
499            TokenWithSpan { token: Token::Ident(..), .. } => {
500                let (ident, ident_span) = self.cursor.expect_ident()?;
501                (
502                    SassInterpolatedIdentElement::Static(
503                        self.interpolable_ident_static_part(ident, ident_span.clone()),
504                    ),
505                    ident_span,
506                )
507            }
508            TokenWithSpan { token: Token::HashLBrace(..), .. } => {
509                self.parse_sass_interpolated_ident_expr()?
510            }
511            TokenWithSpan { token, span } => {
512                return Err(Error {
513                    kind: ErrorKind::ExpectOneOf(vec!["<ident>", "#{"], token.symbol()),
514                    span: span.clone(),
515                });
516            }
517        };
518
519        let mut elements = self.parse_sass_interpolated_ident_rest(&mut end)?;
520        if elements.is_empty()
521            && let SassInterpolatedIdentElement::Static(ident) = first
522        {
523            return Ok(InterpolableIdent::Literal(Ident {
524                name: ident.value,
525                raw: ident.raw,
526                span: ident.span,
527            }));
528        }
529
530        elements.insert(0, first);
531        Ok(InterpolableIdent::SassInterpolated(SassInterpolatedIdent {
532            elements,
533            span: Span { start, end },
534        }))
535    }
536
537    pub(super) fn parse_sass_interpolated_ident_rest(
538        &mut self,
539        end: &mut usize,
540    ) -> PResult<oxc_allocator::Vec<'a, SassInterpolatedIdentElement<'a>>> {
541        let mut elements = self.vec();
542        loop {
543            if let Some((token, span)) = self.cursor.tokenizer.scan_ident_template()? {
544                *end = span.end;
545                elements.push(SassInterpolatedIdentElement::Static(
546                    self.interpolable_ident_static_part(token, span),
547                ));
548            } else if matches!(
549                self.cursor.peek()?,
550                TokenWithSpan { token: Token::HashLBrace(..), span } if *end == span.start
551            ) {
552                let (element, span) = self.parse_sass_interpolated_ident_expr()?;
553                *end = span.end;
554                elements.push(element);
555            } else {
556                return Ok(elements);
557            }
558        }
559    }
560
561    fn parse_sass_interpolated_ident_expr(
562        &mut self,
563    ) -> PResult<(SassInterpolatedIdentElement<'a>, Span)> {
564        debug_assert!(matches!(self.syntax, Syntax::Scss | Syntax::Sass));
565
566        let start = self.cursor.expect_hash_l_brace()?.1.start;
567        let expr = self.parse_maybe_sass_list(/* allow_comma */ true)?;
568        let end = self.cursor.expect_r_brace()?.1.end;
569        Ok((SassInterpolatedIdentElement::Expression(expr), Span { start, end }))
570    }
571
572    pub(super) fn parse_sass_invocation_args(
573        &mut self,
574    ) -> PResult<(oxc_allocator::Vec<'a, ComponentValue<'a>>, oxc_allocator::Vec<'a, Span>)> {
575        debug_assert!(matches!(self.syntax, Syntax::Scss | Syntax::Sass));
576
577        let mut values = self.vec_with_capacity(4);
578        let mut comma_spans = self.vec();
579        while !matches!(self.cursor.peek()?.token, Token::RParen(..) | Token::Eof(..)) {
580            match self.cursor.peek()?.token {
581                Token::Comma(..) => {
582                    let TokenWithSpan { span, .. } = self.cursor.bump()?;
583                    self.recoverable_errors
584                        .push(Error { kind: ErrorKind::ExpectComponentValue, span });
585                    continue;
586                }
587                _ => {
588                    let value = self.parse_maybe_sass_list(/* allow_comma */ false)?;
589                    if let Some((_, span)) = self.cursor.eat_dot_dot_dot()? {
590                        let span = Span { start: value.span().start, end: span.end };
591                        values.push(ComponentValue::SassArbitraryArgument(SassArbitraryArgument {
592                            value: self.alloc(value),
593                            span,
594                        }));
595                    } else if let ComponentValue::SassVariable(sass_var) = value {
596                        if let Some((_, colon_span)) = self.cursor.eat_colon()? {
597                            let value = self.parse_maybe_sass_list(/* allow_comma */ false)?;
598                            let span = Span { start: sass_var.span.start, end: value.span().end };
599                            values.push(ComponentValue::SassKeywordArgument(SassKeywordArgument {
600                                name: sass_var,
601                                colon_span,
602                                value: self.alloc(value),
603                                span,
604                            }));
605                        } else {
606                            values.push(ComponentValue::SassVariable(sass_var));
607                        }
608                    } else {
609                        values.push(value);
610                    }
611                }
612            }
613            if !matches!(self.cursor.peek()?.token, Token::RParen(..) | Token::Eof(..)) {
614                comma_spans.push(self.cursor.expect_comma()?.1);
615            }
616        }
617        debug_assert!(values.len() - comma_spans.len() <= 1);
618        Ok((values, comma_spans))
619    }
620
621    fn parse_sass_module_config(
622        &mut self,
623        allow_overridable: bool,
624    ) -> PResult<Option<SassModuleConfig<'a>>> {
625        match &self.cursor.peek()?.token {
626            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("with") => {
627                let TokenWithSpan { span: with_span, .. } = self.cursor.bump()?;
628                let start = with_span.start;
629                let end;
630                self.eat_sass_line_continuation()?;
631                let (_, lparen_span) = self.cursor.expect_l_paren()?;
632
633                let item = self.parse_sass_module_config_item(allow_overridable)?;
634                let mut items = self.vec1(item);
635                let mut comma_spans = self.vec();
636                if let Some((_, span)) = self.cursor.eat_r_paren()? {
637                    end = span.end;
638                } else {
639                    comma_spans.push(self.cursor.expect_comma()?.1);
640                    loop {
641                        if let Some((_, span)) = self.cursor.eat_r_paren()? {
642                            end = span.end;
643                            break;
644                        }
645
646                        items.push(self.parse_sass_module_config_item(allow_overridable)?);
647                        if let Some((_, span)) = self.cursor.eat_r_paren()? {
648                            end = span.end;
649                            break;
650                        } else {
651                            comma_spans.push(self.cursor.expect_comma()?.1);
652                        }
653                    }
654                }
655                debug_assert!(items.len() - comma_spans.len() <= 1);
656
657                Ok(Some(SassModuleConfig {
658                    with_span,
659                    lparen_span,
660                    items,
661                    comma_spans,
662                    span: Span { start, end },
663                }))
664            }
665            _ => Ok(None),
666        }
667    }
668
669    fn parse_sass_module_config_item(
670        &mut self,
671        allow_overridable: bool,
672    ) -> PResult<SassModuleConfigItem<'a>> {
673        let variable = self.parse::<SassVariable>()?;
674        let (_, colon_span) = self.cursor.expect_colon()?;
675        let value = self.parse_maybe_sass_list(/* allow_comma */ false)?;
676
677        let (flags, end) = if allow_overridable {
678            self.parse_sass_flags()
679                .map(|(flags, end)| (flags, end.unwrap_or_else(|| value.span().end)))?
680        } else {
681            (self.vec(), value.span().end)
682        };
683
684        let span = Span { start: variable.span.start, end };
685        Ok(SassModuleConfigItem { variable, colon_span, value, flags, span })
686    }
687
688    /// This method will consume `)` token.
689    fn parse_sass_params(&mut self) -> PResult<SassParams<'a>> {
690        let mut parameters = self.vec();
691        let mut arbitrary_parameter = None;
692        let mut comma_spans = self.vec();
693        let end;
694        loop {
695            if let Some((_, span)) = self.cursor.eat_r_paren()? {
696                end = span.end;
697                break;
698            }
699
700            let name = self.parse::<SassVariable>()?;
701            let token_with_span = self.cursor.bump()?;
702            match token_with_span.token {
703                Token::Comma(..) => {
704                    let span = name.span.clone();
705                    parameters.push(SassParameter { name, default_value: None, span });
706                    comma_spans.push(token_with_span.span);
707                    continue;
708                }
709                Token::Colon(..) => {
710                    let value = self.parse_maybe_sass_list(/* allow_comma */ false)?;
711                    let end = value.span().end;
712                    let default_value_span = Span { start: token_with_span.span.start, end };
713                    let span = Span { start: name.span.start, end };
714                    parameters.push(SassParameter {
715                        name,
716                        default_value: Some(SassParameterDefaultValue {
717                            colon_span: token_with_span.span,
718                            value,
719                            span: default_value_span,
720                        }),
721                        span,
722                    });
723                }
724                Token::DotDotDot(..) => {
725                    let span = Span { start: name.span().start, end: token_with_span.span.end };
726                    arbitrary_parameter = Some(SassArbitraryParameter { name, span });
727                    if let Some((_, comma_span)) = self.cursor.eat_comma()? {
728                        comma_spans.push(comma_span);
729                    }
730                    end = self.cursor.expect_r_paren()?.1.end;
731                    break;
732                }
733                Token::RParen(..) => {
734                    let span = name.span.clone();
735                    parameters.push(SassParameter { name, default_value: None, span });
736                    end = token_with_span.span.end;
737                    break;
738                }
739                token => {
740                    return Err(Error {
741                        kind: ErrorKind::Unexpected(")", token.symbol()),
742                        span: token_with_span.span,
743                    });
744                }
745            }
746            if let Some((_, span)) = self.cursor.eat_r_paren()? {
747                end = span.end;
748                break;
749            } else {
750                comma_spans.push(self.cursor.expect_comma()?.1);
751            }
752        }
753
754        debug_assert!(
755            parameters.len() + arbitrary_parameter.iter().count() - comma_spans.len() <= 1
756        );
757        Ok((parameters, arbitrary_parameter, comma_spans, end))
758    }
759
760    pub(super) fn parse_sass_qualified_name(
761        &mut self,
762        module: Ident<'a>,
763    ) -> PResult<SassQualifiedName<'a>> {
764        debug_assert!(matches!(self.syntax, Syntax::Scss | Syntax::Sass));
765
766        let (_, dot_span) = self.cursor.expect_dot()?;
767        let member = if let Token::DollarVar(..) = self.cursor.peek()?.token {
768            self.parse().map(SassModuleMemberName::Variable)?
769        } else {
770            self.parse().map(SassModuleMemberName::Ident)?
771        };
772
773        let expr_span = member.span();
774        util::assert_no_ws_or_comment(&dot_span, expr_span)?;
775
776        let span = Span { start: module.span.start, end: expr_span.end };
777        Ok(SassQualifiedName { module, member, span })
778    }
779
780    fn parse_sass_unary_expression(&mut self) -> PResult<ComponentValue<'a>> {
781        // dart-sass accepts a bare `%` in declaration values (`b: %`,
782        // `b: % c`, `b: c %`) as a plain token. Calculations bypass this
783        // (they parse atoms directly), so `calc(1px % 2px)` stays invalid.
784        if let Token::Percent(..) = &self.cursor.peek()?.token {
785            return Ok(ComponentValue::TokenWithSpan(self.cursor.bump()?));
786        }
787        let op = match &self.cursor.peek()?.token {
788            Token::Plus(..) => SassUnaryOperator {
789                kind: SassUnaryOperatorKind::Plus,
790                span: self.cursor.bump()?.span,
791            },
792            Token::Minus(..) => SassUnaryOperator {
793                kind: SassUnaryOperatorKind::Minus,
794                span: self.cursor.bump()?.span,
795            },
796            Token::Ident(token) if token.raw == "not" => SassUnaryOperator {
797                kind: SassUnaryOperatorKind::Not,
798                span: self.cursor.bump()?.span,
799            },
800            _ => return self.parse_component_value_atom(),
801        };
802
803        // `$a: not` may continue on the next line
804        self.eat_sass_line_continuation()?;
805        let expr = self.parse_sass_unary_expression()?;
806        let span = Span { start: op.span.start, end: expr.span().end };
807        Ok(ComponentValue::SassUnaryExpression(SassUnaryExpression {
808            expr: self.alloc(expr),
809            op,
810            span,
811        }))
812    }
813}
814
815impl<'a> Parse<'a> for SassAtRoot<'a> {
816    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
817        let kind = if matches!(input.cursor.peek()?.token, Token::LParen(..)) {
818            SassAtRootKind::Query(input.parse()?)
819        } else {
820            SassAtRootKind::Selector(input.parse()?)
821        };
822
823        let span = kind.span().clone();
824        Ok(SassAtRoot { kind, span })
825    }
826}
827
828impl<'a> Parse<'a> for SassAtRootQuery<'a> {
829    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
830        let start = input.cursor.expect_l_paren()?.1.start;
831
832        let modifier = {
833            let (token, span) = input.cursor.expect_ident()?;
834            let ident_name = token.name();
835            if ident_name.eq_ignore_ascii_case("with") {
836                SassAtRootQueryModifier { kind: SassAtRootQueryModifierKind::With, span }
837            } else if ident_name.eq_ignore_ascii_case("without") {
838                SassAtRootQueryModifier { kind: SassAtRootQueryModifierKind::Without, span }
839            } else {
840                return Err(Error { kind: ErrorKind::ExpectSassAtRootWithOrWithout, span });
841            }
842        };
843        let colon_span = input.cursor.expect_colon()?.1;
844
845        let mut rules = input.vec_with_capacity(1);
846        loop {
847            match &input.cursor.peek()?.token {
848                Token::Ident(..) | Token::HashLBrace(..) => {
849                    rules.push(SassAtRootQueryRule::Ident(input.parse()?));
850                }
851                Token::Str(..) | Token::StrTemplate(..) => {
852                    rules.push(SassAtRootQueryRule::Str(input.parse()?));
853                }
854                _ => break,
855            }
856        }
857        let end = input.cursor.expect_r_paren()?.1.end;
858
859        Ok(SassAtRootQuery { modifier, colon_span, rules, span: Span { start, end } })
860    }
861}
862
863impl<'a> Parse<'a> for SassConditionalClause<'a> {
864    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
865        input.eat_sass_line_continuation()?;
866        let condition = input.parse::<ComponentValue>()?;
867        let block = input.parse::<SimpleBlock>()?;
868        let span = Span { start: condition.span().start, end: block.span.end };
869        Ok(SassConditionalClause { condition, block, span })
870    }
871}
872
873impl<'a> Parse<'a> for SassContent<'a> {
874    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
875        let (_, Span { start, .. }) = input.cursor.expect_l_paren()?;
876        let (args, comma_spans) = input.parse_sass_invocation_args()?;
877        let (_, Span { end, .. }) = input.cursor.expect_r_paren()?;
878        Ok(SassContent { args, comma_spans, span: Span { start, end } })
879    }
880}
881
882impl<'a> Parse<'a> for SassEach<'a> {
883    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
884        debug_assert!(matches!(input.syntax, Syntax::Scss | Syntax::Sass));
885
886        input.eat_sass_line_continuation()?;
887        let first_binding = input.parse::<SassVariable>()?;
888        let start = first_binding.span().start;
889
890        let mut bindings = input.vec1(first_binding);
891        let mut comma_spans = input.vec();
892        loop {
893            // the comma may sit on a continuation line (`@each $a\n  , $b in ...`)
894            input.eat_sass_line_continuation()?;
895            let Some((_, comma_span)) = input.cursor.eat_comma()? else { break };
896            comma_spans.push(comma_span);
897            input.eat_sass_line_continuation()?;
898            bindings.push(input.parse()?);
899        }
900        debug_assert_eq!(comma_spans.len() + 1, bindings.len());
901
902        input.eat_sass_line_continuation()?;
903        let (keyword_in, keyword_in_span) = input.cursor.expect_ident()?;
904        if keyword_in.name() != "in" {
905            return Err(Error { kind: ErrorKind::ExpectSassKeyword("in"), span: keyword_in_span });
906        }
907
908        input.eat_sass_line_continuation()?;
909        let expr = input.parse_maybe_sass_list(/* allow_comma */ true)?;
910        let span = Span { start, end: expr.span().end };
911        Ok(SassEach { bindings, comma_spans, in_span: keyword_in_span, expr, span })
912    }
913}
914
915impl<'a> Parse<'a> for SassExtend<'a> {
916    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
917        input.eat_sass_line_continuation()?;
918        let selectors = input.parse::<CompoundSelectorList>()?;
919        let start = selectors.span.start;
920        let mut end = selectors.span.end;
921
922        let optional = if let Some((_, exclamation_span)) = input.cursor.eat_exclamation()? {
923            let (keyword, keyword_span) =
924                input.cursor.expect_ident_without_ws_or_comments(false)?;
925            if keyword.name().eq_ignore_ascii_case("optional") {
926                end = keyword_span.end;
927                let span = Span { start: exclamation_span.start, end: keyword_span.end };
928                Some(SassFlag { keyword: input.ident(keyword, keyword_span), span })
929            } else {
930                input.recoverable_errors.push(Error {
931                    kind: ErrorKind::ExpectSassKeyword("optional"),
932                    span: keyword_span,
933                });
934                None
935            }
936        } else {
937            None
938        };
939
940        Ok(SassExtend { selectors, optional, span: Span { start, end } })
941    }
942}
943
944impl<'a> Parse<'a> for SassFor<'a> {
945    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
946        debug_assert!(matches!(input.syntax, Syntax::Scss | Syntax::Sass));
947
948        input.eat_sass_line_continuation()?;
949        let binding = input.parse::<SassVariable>()?;
950
951        input.eat_sass_line_continuation()?;
952        let (keyword_from, keyword_from_span) = input.cursor.expect_ident()?;
953        if keyword_from.name() != "from" {
954            return Err(Error {
955                kind: ErrorKind::ExpectSassKeyword("from"),
956                span: keyword_from_span,
957            });
958        }
959
960        input.eat_sass_line_continuation()?;
961        let start = input.parse()?;
962        input.eat_sass_line_continuation()?;
963        let boundary = input.parse()?;
964        input.eat_sass_line_continuation()?;
965        let end = input.parse::<ComponentValue>()?;
966
967        let span = Span { start: binding.span.start, end: end.span().end };
968        Ok(SassFor { binding, from_span: keyword_from_span, start, end, boundary, span })
969    }
970}
971
972impl<'a> Parse<'a> for SassForBoundary {
973    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
974        let (keyword, span) = input.cursor.expect_ident()?;
975        match &*keyword.name() {
976            "to" => Ok(SassForBoundary { kind: SassForBoundaryKind::Exclusive, span }),
977            "through" => Ok(SassForBoundary { kind: SassForBoundaryKind::Inclusive, span }),
978            _ => Err(Error { kind: ErrorKind::ExpectSassKeyword("to"), span }),
979        }
980    }
981}
982
983impl<'a> Parse<'a> for SassForward<'a> {
984    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
985        debug_assert!(matches!(input.syntax, Syntax::Scss | Syntax::Sass));
986
987        input.eat_sass_line_continuation()?;
988        let path = input.parse::<InterpolableStr>()?;
989        let mut span = path.span().clone();
990
991        let prefix = match &input.cursor.peek()?.token {
992            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("as") => {
993                let TokenWithSpan { span: as_span, .. } = input.cursor.bump()?;
994                input.eat_sass_line_continuation()?;
995                let name = input.parse()?;
996                let (_, Span { end, .. }) =
997                    input.cursor.expect_asterisk_without_ws_or_comments()?;
998                let span = Span { start: as_span.start, end };
999                Some(SassForwardPrefix { as_span, name, span })
1000            }
1001            _ => None,
1002        };
1003
1004        let visibility = if let TokenWithSpan { token: Token::Ident(keyword), span: keyword_span } =
1005            input.cursor.peek()?
1006        {
1007            let start = keyword_span.start;
1008            let name = keyword.name();
1009            if name.eq_ignore_ascii_case("hide") {
1010                let keyword_span = input.cursor.bump()?.span;
1011                let mut members = input.vec();
1012                let mut comma_spans = input.vec();
1013                loop {
1014                    input.eat_sass_line_continuation()?;
1015                    match &input.cursor.peek()?.token {
1016                        Token::Ident(..) => {
1017                            members.push(input.parse().map(SassForwardMember::Ident)?)
1018                        }
1019                        _ => members.push(input.parse().map(SassForwardMember::Variable)?),
1020                    }
1021                    if let Some((_, span)) = input.cursor.eat_comma()? {
1022                        comma_spans.push(span);
1023                    } else {
1024                        break;
1025                    }
1026                }
1027                Some(SassForwardVisibility {
1028                    modifier: SassForwardVisibilityModifier {
1029                        kind: SassForwardVisibilityModifierKind::Hide,
1030                        span: keyword_span,
1031                    },
1032                    members,
1033                    comma_spans,
1034                    span: Span { start, end: input.cursor.tokenizer.current_offset() },
1035                })
1036            } else if name.eq_ignore_ascii_case("show") {
1037                let keyword_span = input.cursor.bump()?.span;
1038                let mut members = input.vec();
1039                let mut comma_spans = input.vec();
1040                loop {
1041                    input.eat_sass_line_continuation()?;
1042                    match &input.cursor.peek()?.token {
1043                        Token::Ident(..) => {
1044                            members.push(input.parse().map(SassForwardMember::Ident)?)
1045                        }
1046                        _ => members.push(input.parse().map(SassForwardMember::Variable)?),
1047                    }
1048                    if let Some((_, span)) = input.cursor.eat_comma()? {
1049                        comma_spans.push(span);
1050                    } else {
1051                        break;
1052                    }
1053                }
1054                Some(SassForwardVisibility {
1055                    modifier: SassForwardVisibilityModifier {
1056                        kind: SassForwardVisibilityModifierKind::Show,
1057                        span: keyword_span,
1058                    },
1059                    members,
1060                    comma_spans,
1061                    span: Span { start, end: input.cursor.tokenizer.current_offset() },
1062                })
1063            } else {
1064                None
1065            }
1066        } else {
1067            None
1068        };
1069
1070        let config = input.parse_sass_module_config(/* allow_overridable */ true)?;
1071        if let Some(config) = &config {
1072            span.end = config.span.end;
1073        }
1074
1075        Ok(SassForward { path, prefix, visibility, config, span })
1076    }
1077}
1078
1079impl<'a> Parse<'a> for SassFunction<'a> {
1080    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1081        debug_assert!(matches!(input.syntax, Syntax::Scss | Syntax::Sass));
1082
1083        input.eat_sass_line_continuation()?;
1084        let name = input.parse::<Ident>()?;
1085        let start = name.span.start;
1086
1087        input.eat_sass_line_continuation()?;
1088        let parameters = input.parse::<SassParameters>()?;
1089
1090        let span = Span { start, end: parameters.span.end };
1091        Ok(SassFunction { name, parameters, span })
1092    }
1093}
1094
1095impl<'a> Parse<'a> for SassIfAtRule<'a> {
1096    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1097        debug_assert!(matches!(input.syntax, Syntax::Scss | Syntax::Sass));
1098
1099        let start = input.cursor.expect_at_keyword()?.1.start;
1100
1101        let if_clause = input.parse::<SassConditionalClause>()?;
1102        let mut else_if_clauses: oxc_allocator::Vec<'a, SassConditionalClause<'a>> = input.vec();
1103        let mut else_clause: Option<SimpleBlock> = None;
1104        let mut else_spans = input.vec();
1105
1106        loop {
1107            // In the indented syntax `@else` sits on the line after the
1108            // previous clause's block, so a Linebreak may precede it; only
1109            // consume that separator when an else/elseif actually follows.
1110            // (Linebreak tokens exist only in the indented syntax, so the
1111            // rollback snapshot is needed only when one is present.)
1112            fn else_keyword<'a>(p: &mut Parser<'a>) -> PResult<(Span, bool)> {
1113                while p.cursor.eat_linebreak()?.is_some() {}
1114                let is_else = match &p.cursor.peek()?.token {
1115                    Token::AtKeyword(at_keyword) => match &*at_keyword.ident.name() {
1116                        "else" => true,
1117                        // `elseif` is deprecated by Sass
1118                        "elseif" => false,
1119                        _ => {
1120                            let span = p.cursor.peek()?.span.clone();
1121                            return Err(Error { kind: ErrorKind::TryParseError, span });
1122                        }
1123                    },
1124                    _ => {
1125                        let span = p.cursor.peek()?.span.clone();
1126                        return Err(Error { kind: ErrorKind::TryParseError, span });
1127                    }
1128                };
1129                Ok((p.cursor.bump()?.span, is_else))
1130            }
1131            let else_keyword = if matches!(input.cursor.peek()?.token, Token::Linebreak(..)) {
1132                input.try_parse(else_keyword)
1133            } else {
1134                else_keyword(input)
1135            };
1136            let Ok((else_span, is_else)) = else_keyword else { break };
1137            else_spans.push(else_span);
1138            if is_else {
1139                input.eat_sass_line_continuation()?;
1140                match &input.cursor.peek()?.token {
1141                    Token::Ident(ident) if ident.name() == "if" => {
1142                        input.cursor.bump()?;
1143                        else_if_clauses.push(input.parse()?);
1144                    }
1145                    _ => {
1146                        else_clause = Some(input.parse()?);
1147                        break;
1148                    }
1149                }
1150            } else {
1151                else_if_clauses.push(input.parse()?);
1152            }
1153        }
1154
1155        debug_assert_eq!(else_spans.len(), else_if_clauses.len() + else_clause.iter().count());
1156        let span = Span {
1157            start,
1158            end: else_clause
1159                .as_ref()
1160                .map(|else_clause| else_clause.span.end)
1161                .or_else(|| else_if_clauses.last().map(|else_if_clause| else_if_clause.span.end))
1162                .unwrap_or(if_clause.span.end),
1163        };
1164        Ok(SassIfAtRule { if_clause, else_if_clauses, else_clause, else_spans, span })
1165    }
1166}
1167
1168impl<'a> Parse<'a> for SassImportPrelude<'a> {
1169    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1170        let first = input.parse::<Str>()?;
1171        let mut span = first.span.clone();
1172
1173        let mut paths = input.vec1(first);
1174        let mut comma_spans = input.vec();
1175        while let Some((_, comma_span)) = input.cursor.eat_comma()? {
1176            comma_spans.push(comma_span);
1177            paths.push(input.parse()?);
1178        }
1179        debug_assert_eq!(comma_spans.len() + 1, paths.len());
1180
1181        if let Some(Str { span: Span { end, .. }, .. }) = paths.last() {
1182            span.end = *end;
1183        }
1184        Ok(SassImportPrelude { paths, comma_spans, span })
1185    }
1186}
1187
1188impl<'a> Parse<'a> for SassInclude<'a> {
1189    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1190        debug_assert!(matches!(input.syntax, Syntax::Scss | Syntax::Sass));
1191
1192        input.eat_sass_line_continuation()?;
1193        let name = input.parse::<FunctionName>()?;
1194        let mut span = name.span().clone();
1195
1196        let arguments = if matches!(input.cursor.peek()?.token, Token::LParen(..)) {
1197            let arguments = input.parse::<SassIncludeArgs>()?;
1198            span.end = arguments.span.end;
1199            Some(arguments)
1200        } else {
1201            None
1202        };
1203
1204        let content_block_params = match &input.cursor.peek()?.token {
1205            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("using") => {
1206                let content_block_params = input.parse::<SassIncludeContentBlockParams>()?;
1207                span.end = content_block_params.span.end;
1208                Some(content_block_params)
1209            }
1210            _ => None,
1211        };
1212
1213        Ok(SassInclude { name, arguments, content_block_params, span })
1214    }
1215}
1216
1217impl<'a> Parse<'a> for SassIncludeArgs<'a> {
1218    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1219        let (_, Span { start, .. }) = input.cursor.expect_l_paren()?;
1220        let (args, comma_spans) = input.parse_sass_invocation_args()?;
1221        let (_, Span { end, .. }) = input.cursor.expect_r_paren()?;
1222        Ok(SassIncludeArgs { args, comma_spans, span: Span { start, end } })
1223    }
1224}
1225
1226impl<'a> Parse<'a> for SassIncludeContentBlockParams<'a> {
1227    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1228        match input.cursor.bump()? {
1229            TokenWithSpan { token: Token::Ident(ident), span: using_span }
1230                if ident.name().eq_ignore_ascii_case("using") =>
1231            {
1232                input.eat_sass_line_continuation()?;
1233                let params = input.parse::<SassParameters>()?;
1234                let span = Span { start: using_span.start, end: params.span.end };
1235                Ok(SassIncludeContentBlockParams { using_span, params, span })
1236            }
1237            TokenWithSpan { span, .. } => {
1238                Err(Error { kind: ErrorKind::ExpectSassKeyword("using"), span })
1239            }
1240        }
1241    }
1242}
1243
1244impl<'a> Parse<'a> for SassInterpolatedStr<'a> {
1245    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1246        let (first, first_span) = input.cursor.expect_str_template()?;
1247        let quote = first.raw.chars().next().unwrap();
1248        debug_assert!(quote == '\'' || quote == '"');
1249        let mut span = first_span.clone();
1250        let first = input.interpolable_str_static_part(first, first_span);
1251        let mut elements = input.vec1(SassInterpolatedStrElement::Static(first));
1252
1253        let mut is_parsing_static_part = false;
1254        loop {
1255            if is_parsing_static_part {
1256                let (token, str_tpl_span) = input.cursor.tokenizer.scan_string_template(quote)?;
1257                let tail = token.tail;
1258                let end = str_tpl_span.end;
1259                elements.push(SassInterpolatedStrElement::Static(
1260                    input.interpolable_str_static_part(token, str_tpl_span),
1261                ));
1262                if tail {
1263                    span.end = end;
1264                    break;
1265                }
1266            } else {
1267                // '#' is consumed, so '{' left only
1268                input.cursor.expect_l_brace()?;
1269                elements.push(SassInterpolatedStrElement::Expression(
1270                    input.parse_maybe_sass_list(/* allow_comma */ true)?,
1271                ));
1272                input.cursor.expect_r_brace()?;
1273            }
1274            is_parsing_static_part = !is_parsing_static_part;
1275        }
1276
1277        Ok(SassInterpolatedStr { elements, span })
1278    }
1279}
1280
1281impl<'a> Parse<'a> for SassInterpolatedUrl<'a> {
1282    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1283        debug_assert!(matches!(input.syntax, Syntax::Scss | Syntax::Sass));
1284
1285        let (first, first_span) = match input.cursor.tokenizer.scan_url_raw_or_template()? {
1286            TokenWithSpan { token: Token::UrlTemplate(template), span } => (template, span),
1287            TokenWithSpan { token, span } => {
1288                return Err(Error {
1289                    kind: ErrorKind::Unexpected("<url template>", token.symbol()),
1290                    span,
1291                });
1292            }
1293        };
1294        let mut span = first_span.clone();
1295        let first = input.interpolable_url_static_part(first, first_span);
1296        let mut elements = input.vec1(SassInterpolatedUrlElement::Static(first));
1297
1298        let mut is_parsing_static_part = false;
1299        loop {
1300            if is_parsing_static_part {
1301                let (token, url_tpl_span @ Span { end, .. }) =
1302                    input.cursor.tokenizer.scan_url_template()?;
1303                let tail = token.tail;
1304                elements.push(SassInterpolatedUrlElement::Static(
1305                    input.interpolable_url_static_part(token, url_tpl_span),
1306                ));
1307                if tail {
1308                    span.end = end;
1309                    break;
1310                }
1311            } else {
1312                // '#' is consumed, so '{' left only
1313                input.cursor.expect_l_brace()?;
1314                elements.push(SassInterpolatedUrlElement::Expression(
1315                    input.parse_maybe_sass_list(/* allow_comma */ true)?,
1316                ));
1317                input.cursor.expect_r_brace()?;
1318            }
1319            is_parsing_static_part = !is_parsing_static_part;
1320        }
1321
1322        Ok(SassInterpolatedUrl { elements, span })
1323    }
1324}
1325
1326impl<'a> Parse<'a> for SassList<'a> {
1327    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1328        if let ComponentValue::SassList(list) =
1329            input.parse_maybe_sass_list(/* allow_comma */ true)?
1330        {
1331            Ok(list)
1332        } else {
1333            let TokenWithSpan { token, span } = input.cursor.bump()?;
1334            Err(Error { kind: ErrorKind::Unexpected(",", token.symbol()), span })
1335        }
1336    }
1337}
1338
1339impl<'a> Parse<'a> for SassMap<'a> {
1340    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1341        let start = input.cursor.expect_l_paren()?.1.start;
1342
1343        let mut items = input.vec();
1344        let mut comma_spans = input.vec();
1345        loop {
1346            match input.cursor.peek()?.token {
1347                Token::RParen(..) => break,
1348                Token::Indent(..) | Token::Dedent(..) | Token::Linebreak(..) => {
1349                    input.cursor.bump()?;
1350                }
1351                _ => {
1352                    items.push(input.parse()?);
1353                    if matches!(
1354                        input.cursor.peek()?.token,
1355                        Token::Indent(..) | Token::Dedent(..) | Token::Linebreak(..)
1356                    ) {
1357                        input.cursor.bump()?;
1358                    }
1359                    if !matches!(&input.cursor.peek()?.token, Token::RParen(..)) {
1360                        comma_spans.push(input.cursor.expect_comma()?.1);
1361                    }
1362                }
1363            }
1364        }
1365        debug_assert!(items.len() - comma_spans.len() <= 1);
1366
1367        let end = input.cursor.expect_r_paren()?.1.end;
1368        Ok(SassMap { items, comma_spans, span: Span { start, end } })
1369    }
1370}
1371
1372impl<'a> Parse<'a> for SassMapItem<'a> {
1373    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1374        let key = input.parse_maybe_sass_list(/* allow_comma */ false)?;
1375        let (_, colon_span) = input.cursor.expect_colon()?;
1376        let value = input.parse_maybe_sass_list(/* allow_comma */ false)?;
1377        let span = Span { start: key.span().start, end: value.span().end };
1378        Ok(SassMapItem { key, colon_span, value, span })
1379    }
1380}
1381
1382impl<'a> Parse<'a> for SassMixin<'a> {
1383    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1384        debug_assert!(matches!(input.syntax, Syntax::Scss | Syntax::Sass));
1385
1386        input.eat_sass_line_continuation()?;
1387        let name = input.parse::<Ident>()?;
1388        let start = name.span.start;
1389        let mut end = name.span.end;
1390
1391        let parameters = if matches!(input.cursor.peek()?.token, Token::LParen(..)) {
1392            let parameters = input.parse::<SassParameters>()?;
1393            end = parameters.span.end;
1394            Some(parameters)
1395        } else {
1396            None
1397        };
1398
1399        Ok(SassMixin { name, parameters, span: Span { start, end } })
1400    }
1401}
1402
1403impl<'a> Parse<'a> for SassParameters<'a> {
1404    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1405        let (_, Span { start, .. }) = input.cursor.expect_l_paren()?;
1406        let (params, arbitrary_param, comma_spans, end) = input.parse_sass_params()?;
1407        Ok(SassParameters { params, arbitrary_param, comma_spans, span: Span { start, end } })
1408    }
1409}
1410
1411impl<'a> Parse<'a> for SassNestingDeclaration<'a> {
1412    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1413        let block = input.parse::<SimpleBlock>()?;
1414        let span = block.span.clone();
1415
1416        Ok(SassNestingDeclaration { block, span })
1417    }
1418}
1419
1420impl<'a> Parse<'a> for SassParenthesizedExpression<'a> {
1421    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1422        let start = input.cursor.expect_l_paren()?.1.start;
1423        input.cursor.eat_indent()?;
1424        let expr = input
1425            .with_state(ParserState {
1426                sass_ctx: input.state.sass_ctx | SASS_CTX_ALLOW_DIV | SASS_CTX_IN_PARENS,
1427                ..input.state.clone()
1428            })
1429            .parse_maybe_sass_list(/* allow_comma */ true)?;
1430        let expr = input.alloc(expr);
1431        let end = input.cursor.expect_r_paren()?.1.end;
1432        Ok(SassParenthesizedExpression { expr, span: Span { start, end } })
1433    }
1434}
1435
1436impl<'a> Parse<'a> for SassPlaceholderSelector<'a> {
1437    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1438        let (_, percent_span) = input.cursor.expect_percent()?;
1439        let name = input.parse::<InterpolableIdent>()?;
1440        let name_span = name.span();
1441        util::assert_no_ws_or_comment(&percent_span, name_span)?;
1442        let span = Span { start: percent_span.start, end: name_span.end };
1443        Ok(SassPlaceholderSelector { name, span })
1444    }
1445}
1446
1447impl<'a> Parse<'a> for SassUse<'a> {
1448    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1449        input.eat_sass_line_continuation()?;
1450        let path = input.parse::<InterpolableStr>()?;
1451        let mut span = path.span().clone();
1452
1453        let namespace = match &input.cursor.peek()?.token {
1454            Token::Ident(ident) if ident.name().eq_ignore_ascii_case("as") => {
1455                let namespace = input.parse::<SassUseNamespace>()?;
1456                span.end = namespace.span.end;
1457                Some(namespace)
1458            }
1459            _ => None,
1460        };
1461
1462        let config = input.parse_sass_module_config(/* allow_overridable */ false)?;
1463        if let Some(config) = &config {
1464            span.end = config.span.end;
1465        }
1466
1467        Ok(SassUse { path, namespace, config, span })
1468    }
1469}
1470
1471impl<'a> Parse<'a> for SassUseNamespace<'a> {
1472    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1473        let as_span = match input.cursor.peek()? {
1474            TokenWithSpan { token: Token::Ident(ident), .. }
1475                if ident.name().eq_ignore_ascii_case("as") =>
1476            {
1477                input.cursor.bump()?.span
1478            }
1479            TokenWithSpan { span, .. } => {
1480                return Err(Error { kind: ErrorKind::ExpectSassKeyword("as"), span: span.clone() });
1481            }
1482        };
1483        input.eat_sass_line_continuation()?;
1484        match input.cursor.bump()? {
1485            TokenWithSpan { token: Token::Asterisk(..), span: asterisk_span } => {
1486                let span = Span { start: as_span.start, end: asterisk_span.end };
1487                Ok(SassUseNamespace {
1488                    as_span,
1489                    kind: SassUseNamespaceKind::Unnamed(SassUnnamedNamespace {
1490                        span: asterisk_span,
1491                    }),
1492                    span,
1493                })
1494            }
1495            TokenWithSpan { token: Token::Ident(ident), span: ident_span } => {
1496                let span = Span { start: as_span.start, end: ident_span.end };
1497                Ok(SassUseNamespace {
1498                    as_span,
1499                    kind: SassUseNamespaceKind::Named(input.ident(ident, ident_span)),
1500                    span,
1501                })
1502            }
1503            TokenWithSpan { span, .. } => {
1504                Err(Error { kind: ErrorKind::ExpectSassUseNamespace, span })
1505            }
1506        }
1507    }
1508}
1509
1510impl<'a> Parse<'a> for SassVariable<'a> {
1511    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1512        debug_assert!(matches!(input.syntax, Syntax::Scss | Syntax::Sass));
1513
1514        let (name, span) = input.parse_dollar_var_ident()?;
1515        Ok(SassVariable { name, span })
1516    }
1517}
1518
1519impl<'a> Parse<'a> for SassVariableDeclaration<'a> {
1520    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1521        debug_assert!(matches!(input.syntax, Syntax::Scss | Syntax::Sass));
1522
1523        let namespace = if let Some((ident_token, span)) = input.cursor.eat_ident()? {
1524            let (_, dot_span) = input.cursor.expect_dot()?;
1525            util::assert_no_ws_or_comment(&span, &dot_span)?;
1526            let TokenWithSpan { span: next_span, .. } = input.cursor.peek()?;
1527            util::assert_no_ws_or_comment(&dot_span, next_span)?;
1528            Some(input.ident(ident_token, span))
1529        } else {
1530            None
1531        };
1532
1533        let name = input.parse::<SassVariable>()?;
1534        input.eat_sass_line_continuation()?;
1535        let (_, colon_span) = input.cursor.expect_colon()?;
1536        input.eat_sass_line_continuation()?;
1537        let value = input
1538            .with_state(ParserState {
1539                sass_ctx: input.state.sass_ctx | SASS_CTX_ALLOW_DIV,
1540                ..input.state.clone()
1541            })
1542            .parse_maybe_sass_list(/* allow_comma */ true)?;
1543
1544        let (flags, end) = input.parse_sass_flags()?;
1545
1546        let span = Span {
1547            start: namespace
1548                .as_ref()
1549                .map(|namespace| namespace.span.start)
1550                .unwrap_or(name.span.start),
1551            end: end.unwrap_or_else(|| value.span().end),
1552        };
1553
1554        if namespace.is_some() && flags.iter().any(|flag| flag.keyword.name == "global") {
1555            input
1556                .recoverable_errors
1557                .push(Error { kind: ErrorKind::UnexpectedSassFlag("global"), span: span.clone() });
1558        }
1559
1560        Ok(SassVariableDeclaration { namespace, name, colon_span, value, flags, span })
1561    }
1562}
1563
1564impl<'a> Parse<'a> for UnknownSassAtRule<'a> {
1565    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1566        debug_assert!(matches!(input.syntax, Syntax::Scss | Syntax::Sass));
1567
1568        let (_, at_span) = input.cursor.expect_at()?;
1569        let name = input.parse_sass_interpolated_ident()?;
1570        let name_span = name.span();
1571        util::assert_no_ws_or_comment(&at_span, name_span)?;
1572
1573        let (prelude, block, end) = input.parse_unknown_at_rule()?;
1574        let span = Span { start: at_span.start, end: end.unwrap_or(name_span.end) };
1575        Ok(UnknownSassAtRule { name, prelude, block, span })
1576    }
1577}