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