Skip to main content

oxc_css_parser/parser/
less.rs

1use super::{
2    Parser,
3    state::{LESS_CTX_ALLOW_DIV, LESS_CTX_ALLOW_KEYFRAME_BLOCK, ParserState, QualifiedRuleContext},
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};
14use std::mem;
15
16const PRECEDENCE_AND: u8 = 2;
17const PRECEDENCE_OR: u8 = 1;
18
19const PRECEDENCE_MULTIPLY: u8 = 2;
20const PRECEDENCE_PLUS: u8 = 1;
21
22impl<'a> Parser<'a> {
23    pub(super) fn parse_less_condition(
24        &mut self,
25        needs_parens: bool,
26    ) -> PResult<LessCondition<'a>> {
27        self.parse_less_condition_recursively(needs_parens, 0)
28    }
29
30    fn parse_less_condition_atom(&mut self) -> PResult<LessCondition<'a>> {
31        let left =
32            self.parse_less_operation(/* allow_mixin_call */ false).map(LessCondition::Value)?;
33
34        let op = match &self.cursor.peek()?.token {
35            Token::GreaterThan(..) => LessBinaryConditionOperator {
36                kind: LessBinaryConditionOperatorKind::GreaterThan,
37                span: self.cursor.bump()?.span,
38            },
39            Token::GreaterThanEqual(..) => LessBinaryConditionOperator {
40                kind: LessBinaryConditionOperatorKind::GreaterThanOrEqual,
41                span: self.cursor.bump()?.span,
42            },
43            Token::LessThan(..) => LessBinaryConditionOperator {
44                kind: LessBinaryConditionOperatorKind::LessThan,
45                span: self.cursor.bump()?.span,
46            },
47            Token::LessThanEqual(..) => LessBinaryConditionOperator {
48                kind: LessBinaryConditionOperatorKind::LessThanOrEqual,
49                span: self.cursor.bump()?.span,
50            },
51            Token::Equal(..) => {
52                let eq_span = self.cursor.bump()?.span;
53                match self.cursor.peek()? {
54                    TokenWithSpan { token: Token::GreaterThan(..), span: gt_span }
55                        if eq_span.end == gt_span.start =>
56                    {
57                        LessBinaryConditionOperator {
58                            kind: LessBinaryConditionOperatorKind::EqualOrGreaterThan,
59                            span: Span { start: eq_span.start, end: self.cursor.bump()?.span.end },
60                        }
61                    }
62                    TokenWithSpan { token: Token::LessThan(..), span: lt_span }
63                        if eq_span.end == lt_span.start =>
64                    {
65                        LessBinaryConditionOperator {
66                            kind: LessBinaryConditionOperatorKind::EqualOrLessThan,
67                            span: Span { start: eq_span.start, end: self.cursor.bump()?.span.end },
68                        }
69                    }
70                    _ => LessBinaryConditionOperator {
71                        kind: LessBinaryConditionOperatorKind::Equal,
72                        span: eq_span,
73                    },
74                }
75            }
76            _ => return Ok(left),
77        };
78
79        let right =
80            self.parse_less_operation(/* allow_mixin_call */ false).map(LessCondition::Value)?;
81
82        let span = Span { start: left.span().start, end: right.span().end };
83        Ok(LessCondition::Binary(LessBinaryCondition {
84            left: self.alloc(left),
85            op,
86            right: self.alloc(right),
87            span,
88        }))
89    }
90
91    /// The parenthesized part of a guard condition, after its `(` was
92    /// consumed: guards are math mode (`when (8+(5-1) < 13)`). Returns the
93    /// condition and the `)`'s end offset.
94    fn parse_less_guard_paren_condition(
95        &mut self,
96        needs_parens: bool,
97    ) -> PResult<(LessCondition<'a>, usize)> {
98        let condition = self
99            .with_state(ParserState {
100                less_ctx: self.state.less_ctx | LESS_CTX_ALLOW_DIV,
101                ..self.state.clone()
102            })
103            .parse_less_condition_inside_parens(needs_parens)?;
104        let (_, Span { end, .. }) = self.cursor.expect_r_paren()?;
105        Ok((condition, end))
106    }
107
108    fn parse_less_condition_inside_parens(
109        &mut self,
110        needs_parens: bool,
111    ) -> PResult<LessCondition<'a>> {
112        self.try_parse(|parser| {
113            let condition = parser.parse_less_condition(needs_parens);
114            match &condition {
115                Ok(LessCondition::Parenthesized(LessParenthesizedCondition {
116                    condition: inner_condition,
117                    span,
118                })) => match &**inner_condition {
119                    LessCondition::Value(ComponentValue::LessBinaryOperation(..))
120                        if matches!(
121                            parser.cursor.peek()?.token,
122                            Token::GreaterThan(..)
123                                | Token::GreaterThanEqual(..)
124                                | Token::LessThan(..)
125                                | Token::LessThanEqual(..)
126                                | Token::Equal(..)
127                                | Token::Plus(..)
128                                | Token::Minus(..)
129                                | Token::Asterisk(..)
130                                | Token::Solidus(..)
131                        ) =>
132                    {
133                        // special case:
134                        // `when ((8 + 6) > 13)`
135                        // the `(8 + 6)` above is operation, not condition
136                        Err(Error { kind: ErrorKind::TryParseError, span: span.clone() })
137                    }
138                    _ => condition,
139                },
140                _ => condition,
141            }
142        })
143        .or_else(|_| self.parse_less_condition_atom())
144    }
145
146    fn parse_less_condition_recursively(
147        &mut self,
148        needs_parens: bool,
149        precedence: u8,
150    ) -> PResult<LessCondition<'a>> {
151        let mut left = if precedence >= PRECEDENCE_AND {
152            match &self.cursor.peek()?.token {
153                Token::LParen(..) => {
154                    let Span { start, .. } = self.cursor.bump()?.span;
155                    let (condition, end) = self.parse_less_guard_paren_condition(needs_parens)?;
156                    LessCondition::Parenthesized(LessParenthesizedCondition {
157                        condition: self.alloc(condition),
158                        span: Span { start, end },
159                    })
160                }
161                Token::Ident(ident) if ident.raw == "not" => {
162                    let Span { start, .. } = self.cursor.bump()?.span;
163                    let (condition, end) = if self.cursor.eat_l_paren()?.is_some() {
164                        self.parse_less_guard_paren_condition(needs_parens)?
165                    } else {
166                        // less.js also accepts a bare operand: `when not @a`
167                        let condition = self.parse_less_condition_atom()?;
168                        let end = condition.span().end;
169                        (condition, end)
170                    };
171                    LessCondition::Negated(LessNegatedCondition {
172                        condition: self.alloc(condition),
173                        span: Span { start, end },
174                    })
175                }
176                _ => {
177                    if needs_parens {
178                        let TokenWithSpan { token, span } = self.cursor.bump()?;
179                        return Err(Error {
180                            kind: ErrorKind::Unexpected("(", token.symbol()),
181                            span,
182                        });
183                    } else {
184                        self.parse_less_condition_atom()?
185                    }
186                }
187            }
188        } else {
189            self.parse_less_condition_recursively(needs_parens, precedence + 1)?
190        };
191
192        loop {
193            let op = match &self.cursor.peek()?.token {
194                Token::Ident(token) if token.raw == "and" && precedence == PRECEDENCE_AND => {
195                    LessBinaryConditionOperator {
196                        kind: LessBinaryConditionOperatorKind::And,
197                        span: self.cursor.bump()?.span,
198                    }
199                }
200                Token::Ident(token) if token.raw == "or" && precedence == PRECEDENCE_OR => {
201                    LessBinaryConditionOperator {
202                        kind: LessBinaryConditionOperatorKind::Or,
203                        span: self.cursor.bump()?.span,
204                    }
205                }
206                _ => break,
207            };
208
209            // multiple conditions in Less are right-associated
210            let right = self.parse_less_condition_recursively(needs_parens, precedence)?;
211
212            let span = Span { start: left.span().start, end: right.span().end };
213            left = LessCondition::Binary(LessBinaryCondition {
214                left: self.alloc(left),
215                op,
216                right: self.alloc(right),
217                span,
218            });
219        }
220
221        Ok(left)
222    }
223
224    pub(super) fn parse_less_interpolated_ident(&mut self) -> PResult<InterpolableIdent<'a>> {
225        debug_assert_eq!(self.syntax, Syntax::Less);
226
227        let (first, Span { start, mut end }) = match self.cursor.peek()? {
228            TokenWithSpan { token: Token::Ident(..), .. } => {
229                let (ident, ident_span) = self.cursor.expect_ident()?;
230                (
231                    LessInterpolatedIdentElement::Static(
232                        self.interpolable_ident_static_part(ident, ident_span.clone()),
233                    ),
234                    ident_span,
235                )
236            }
237            TokenWithSpan { token: Token::AtLBraceVar(..), .. } => {
238                let interpolation = self.parse::<LessVariableInterpolation>()?;
239                let span = interpolation.span.clone();
240                (LessInterpolatedIdentElement::Variable(interpolation), span)
241            }
242            TokenWithSpan { token: Token::DollarLBraceVar(..), .. }
243                if matches!(
244                    self.state.qualified_rule_ctx,
245                    Some(QualifiedRuleContext::DeclarationName)
246                ) =>
247            {
248                let interpolation = self.parse::<LessPropertyInterpolation>()?;
249                let span = interpolation.span.clone();
250                (LessInterpolatedIdentElement::Property(interpolation), span)
251            }
252            TokenWithSpan { token, span } => {
253                return Err(Error {
254                    kind: ErrorKind::ExpectOneOf(vec!["<ident>", "@{"], token.symbol()),
255                    span: span.clone(),
256                });
257            }
258        };
259
260        let mut elements = self.parse_less_interpolated_ident_rest(&mut end)?;
261        if elements.is_empty()
262            && let LessInterpolatedIdentElement::Static(ident) = first
263        {
264            return Ok(InterpolableIdent::Literal(Ident {
265                name: ident.value,
266                raw: ident.raw,
267                span: ident.span,
268            }));
269        }
270
271        elements.insert(0, first);
272        Ok(InterpolableIdent::LessInterpolated(LessInterpolatedIdent {
273            elements,
274            span: Span { start, end },
275        }))
276    }
277
278    pub(super) fn parse_less_interpolated_ident_rest(
279        &mut self,
280        end: &mut usize,
281    ) -> PResult<oxc_allocator::Vec<'a, LessInterpolatedIdentElement<'a>>> {
282        let mut elements = self.vec();
283        loop {
284            if let Some((token, span)) = self.cursor.tokenizer.scan_ident_template()? {
285                *end = span.end;
286                elements.push(LessInterpolatedIdentElement::Static(
287                    self.interpolable_ident_static_part(token, span),
288                ));
289            } else {
290                match self.cursor.peek()? {
291                    TokenWithSpan { token: Token::AtLBraceVar(..), span: at_lbrace_var_span }
292                        if *end == at_lbrace_var_span.start =>
293                    {
294                        let variable = self.parse::<LessVariableInterpolation>()?;
295                        *end = variable.span.end;
296                        elements.push(LessInterpolatedIdentElement::Variable(variable));
297                    }
298                    TokenWithSpan {
299                        token: Token::DollarLBraceVar(..),
300                        span: dollar_lbrace_var_span,
301                    } if matches!(
302                        self.state.qualified_rule_ctx,
303                        Some(QualifiedRuleContext::DeclarationName)
304                    ) && *end == dollar_lbrace_var_span.start =>
305                    {
306                        let property = self.parse::<LessPropertyInterpolation>()?;
307                        *end = property.span.end;
308                        elements.push(LessInterpolatedIdentElement::Property(property));
309                    }
310                    _ => return Ok(elements),
311                }
312            }
313        }
314    }
315
316    pub(super) fn parse_less_maybe_mixin_call_or_with_lookups(
317        &mut self,
318    ) -> PResult<ComponentValue<'a>> {
319        let mixin_call = self.parse::<LessMixinCall>()?;
320        if matches!(self.cursor.peek()?.token, Token::LBracket(..)) {
321            let lookups = self.parse::<LessLookups>()?;
322            let span = Span { start: mixin_call.span.start, end: lookups.span.end };
323            Ok(ComponentValue::LessNamespaceValue(self.alloc(LessNamespaceValue {
324                callee: LessNamespaceValueCallee::LessMixinCall(mixin_call),
325                lookups,
326                span,
327            })))
328        } else {
329            Ok(ComponentValue::LessMixinCall(self.alloc(mixin_call)))
330        }
331    }
332
333    pub(super) fn parse_less_maybe_variable_or_with_lookups(
334        &mut self,
335    ) -> PResult<ComponentValue<'a>> {
336        let variable = self.parse::<LessVariable>()?;
337        match self.cursor.peek()? {
338            // a detached-ruleset call in value position: `a: @a();`
339            TokenWithSpan { token: Token::LParen(..), span } if variable.span.end == span.start => {
340                self.cursor.bump()?;
341                let (_, Span { end, .. }) = self.cursor.expect_r_paren()?;
342                let span = Span { start: variable.span.start, end };
343                Ok(ComponentValue::LessVariableCall(LessVariableCall { variable, span }))
344            }
345            TokenWithSpan { token: Token::LBracket(..), span }
346                if variable.span.end == span.start =>
347            {
348                let lookups = self.parse::<LessLookups>()?;
349                let span = Span { start: variable.span.start, end: lookups.span.end };
350                Ok(ComponentValue::LessNamespaceValue(self.alloc(LessNamespaceValue {
351                    callee: LessNamespaceValueCallee::LessVariable(variable),
352                    lookups,
353                    span,
354                })))
355            }
356            _ => Ok(ComponentValue::LessVariable(variable)),
357        }
358    }
359
360    pub(super) fn parse_less_operation(
361        &mut self,
362        allow_mixin_call: bool,
363    ) -> PResult<ComponentValue<'a>> {
364        self.parse_less_operation_recursively(allow_mixin_call, 0)
365    }
366
367    fn parse_less_operation_recursively(
368        &mut self,
369        allow_mixin_call: bool,
370        precedence: u8,
371    ) -> PResult<ComponentValue<'a>> {
372        let mut left = if precedence >= PRECEDENCE_MULTIPLY {
373            match self.cursor.peek()?.token {
374                Token::LParen(..) => self
375                    .parse_less_parenthesized_operation(allow_mixin_call)
376                    .map(ComponentValue::LessParenthesizedOperation)?,
377                Token::Minus(..) => {
378                    // less.js's keyword regex also matches a bare `-`
379                    // (`a:hover when (2 = true) {5:-}`); only treat it as one
380                    // when a terminator follows immediately
381                    let end = self.cursor.peek()?.span.end;
382                    if matches!(self.source.as_bytes().get(end), None | Some(b';' | b'}')) {
383                        let span = self.cursor.bump()?.span;
384                        ComponentValue::InterpolableIdent(InterpolableIdent::Literal(Ident {
385                            name: "-",
386                            raw: "-",
387                            span,
388                        }))
389                    } else {
390                        self.parse::<LessNegativeValue>().map(ComponentValue::LessNegativeValue)?
391                    }
392                }
393                _ => {
394                    let value = self.parse_component_value_atom()?;
395                    if let ComponentValue::LessMixinCall(mixin_call) = &value
396                        && !allow_mixin_call
397                    {
398                        self.recoverable_errors.push(Error {
399                            kind: ErrorKind::UnexpectedLessMixinCall,
400                            span: mixin_call.span.clone(),
401                        });
402                    }
403                    value
404                }
405            }
406        } else {
407            self.parse_less_operation_recursively(allow_mixin_call, precedence + 1)?
408        };
409
410        loop {
411            let op = match self.cursor.peek()? {
412                TokenWithSpan { token: Token::Asterisk(..), .. }
413                    if precedence == PRECEDENCE_MULTIPLY =>
414                {
415                    LessOperationOperator {
416                        kind: LessOperationOperatorKind::Multiply,
417                        span: self.cursor.bump()?.span,
418                    }
419                }
420                TokenWithSpan { token: Token::Solidus(..), .. }
421                    if precedence == PRECEDENCE_MULTIPLY
422                        && (self.state.less_ctx & LESS_CTX_ALLOW_DIV != 0
423                            || can_be_division_operand(&left)) =>
424                {
425                    LessOperationOperator {
426                        kind: LessOperationOperatorKind::Division,
427                        span: self.cursor.bump()?.span,
428                    }
429                }
430                TokenWithSpan { token: Token::Dot(..), .. }
431                    if precedence == PRECEDENCE_MULTIPLY =>
432                {
433                    // `./` is also division
434                    let Span { start, .. } = self.cursor.bump()?.span;
435                    let (_, Span { end, .. }) =
436                        self.cursor.expect_solidus_without_ws_or_comments()?;
437                    LessOperationOperator {
438                        kind: LessOperationOperatorKind::Division,
439                        span: Span { start, end },
440                    }
441                }
442                // A `+`/`-` is a binary operator only when followed by
443                // whitespace. lessc is whitespace-sensitive here: `@a - @b`
444                // is subtraction, but `@a -@b` is two values (`-@b` is a
445                // signed value, not an operator) — see the `margin` shorthand.
446                TokenWithSpan { token: Token::Plus(..), span }
447                    if precedence == PRECEDENCE_PLUS
448                        && (is_followed_by_whitespace(self.source, span.end)
449                            || self.state.less_ctx & LESS_CTX_ALLOW_DIV != 0) =>
450                {
451                    LessOperationOperator {
452                        kind: LessOperationOperatorKind::Plus,
453                        span: self.cursor.bump()?.span,
454                    }
455                }
456                TokenWithSpan { token: Token::Minus(..), span }
457                    if precedence == PRECEDENCE_PLUS
458                        && (is_followed_by_whitespace(self.source, span.end)
459                            || self.state.less_ctx & LESS_CTX_ALLOW_DIV != 0) =>
460                {
461                    LessOperationOperator {
462                        kind: LessOperationOperatorKind::Minus,
463                        span: self.cursor.bump()?.span,
464                    }
465                }
466                TokenWithSpan { token: Token::Number(token), span }
467                    if precedence == PRECEDENCE_PLUS
468                        && (token.raw.starts_with('+')
469                            || token.raw.starts_with('-') && span.start == left.span().end) =>
470                {
471                    let (number, number_span) = self.cursor.expect_number()?;
472                    let op = LessOperationOperator {
473                        kind: if number.raw.starts_with('+') {
474                            LessOperationOperatorKind::Plus
475                        } else {
476                            LessOperationOperatorKind::Minus
477                        },
478                        span: Span { start: number_span.start, end: number_span.start + 1 },
479                    };
480                    let span = Span { start: left.span().start, end: number_span.end };
481                    let right = {
482                        let span = Span { start: number_span.start + 1, end: number_span.end };
483                        let raw = unsafe { number.raw.get_unchecked(1..number.raw.len()) };
484                        raw.parse()
485                            .map_err(|_| Error {
486                                kind: ErrorKind::InvalidNumber,
487                                span: span.clone(),
488                            })
489                            .map(|value| ComponentValue::Number(Number { value, raw, span }))?
490                    };
491                    left = ComponentValue::LessBinaryOperation(LessBinaryOperation {
492                        left: self.alloc(left),
493                        op,
494                        right: self.alloc(right),
495                        span,
496                    });
497                    continue;
498                }
499                TokenWithSpan { token: Token::Dimension(token), span }
500                    if precedence == PRECEDENCE_PLUS
501                        && (token.value.raw.starts_with('+')
502                            || token.value.raw.starts_with('-')
503                                && span.start == left.span().end) =>
504                {
505                    let (dimension, dimension_span) = self.cursor.expect_dimension()?;
506                    let op = LessOperationOperator {
507                        kind: if dimension.value.raw.starts_with('+') {
508                            LessOperationOperatorKind::Plus
509                        } else {
510                            LessOperationOperatorKind::Minus
511                        },
512                        span: Span { start: dimension_span.start, end: dimension_span.start + 1 },
513                    };
514                    let mut right = {
515                        self.dimension(
516                            crate::token::Dimension {
517                                value: crate::token::Number {
518                                    raw: unsafe {
519                                        dimension
520                                            .value
521                                            .raw
522                                            .get_unchecked(1..dimension.value.raw.len())
523                                    },
524                                },
525                                unit: dimension.unit,
526                            },
527                            Span { start: dimension_span.start + 1, end: dimension_span.end },
528                        )
529                        .map(ComponentValue::Dimension)?
530                    };
531                    // multiplication binds tighter than the split-off sign:
532                    // `(6px-1px*2)` is `6px - (1px * 2)`
533                    while matches!(
534                        &self.cursor.peek()?.token,
535                        Token::Asterisk(..) | Token::Solidus(..)
536                    ) {
537                        let mul_op = LessOperationOperator {
538                            kind: if matches!(&self.cursor.peek()?.token, Token::Asterisk(..)) {
539                                LessOperationOperatorKind::Multiply
540                            } else {
541                                LessOperationOperatorKind::Division
542                            },
543                            span: self.cursor.bump()?.span,
544                        };
545                        let mul_rhs = self.parse_less_operation_recursively(
546                            allow_mixin_call,
547                            PRECEDENCE_MULTIPLY + 1,
548                        )?;
549                        let span = Span { start: right.span().start, end: mul_rhs.span().end };
550                        right = ComponentValue::LessBinaryOperation(LessBinaryOperation {
551                            left: self.alloc(right),
552                            op: mul_op,
553                            right: self.alloc(mul_rhs),
554                            span,
555                        });
556                    }
557                    let span = Span { start: left.span().start, end: right.span().end };
558                    left = ComponentValue::LessBinaryOperation(LessBinaryOperation {
559                        left: self.alloc(left),
560                        op,
561                        right: self.alloc(right),
562                        span,
563                    });
564                    continue;
565                }
566                _ => break,
567            };
568
569            let right = self.parse_less_operation_recursively(allow_mixin_call, precedence + 1)?;
570            let span = Span { start: left.span().start, end: right.span().end };
571            left = ComponentValue::LessBinaryOperation(LessBinaryOperation {
572                left: self.alloc(left),
573                op,
574                right: self.alloc(right),
575                span,
576            });
577        }
578
579        Ok(left)
580    }
581
582    fn parse_less_parenthesized_operation(
583        &mut self,
584        allow_mixin_call: bool,
585    ) -> PResult<LessParenthesizedOperation<'a>> {
586        let (_, Span { start, .. }) = self.cursor.expect_l_paren()?;
587        let operation = self
588            .with_state(ParserState {
589                less_ctx: self.state.less_ctx | LESS_CTX_ALLOW_DIV,
590                ..self.state.clone()
591            })
592            .parse_less_operation(allow_mixin_call)?;
593        let (_, Span { end, .. }) = self.cursor.expect_r_paren()?;
594        Ok(LessParenthesizedOperation {
595            operation: self.alloc(operation),
596            span: Span { start, end },
597        })
598    }
599
600    pub(super) fn parse_less_qualified_rule(&mut self) -> PResult<Statement<'a>> {
601        debug_assert_eq!(self.syntax, Syntax::Less);
602
603        let selector_list = self
604            .with_state(ParserState {
605                qualified_rule_ctx: Some(QualifiedRuleContext::Selector),
606                ..self.state
607            })
608            .parse::<SelectorList>()?;
609
610        match &self.cursor.peek()?.token {
611            Token::Ident(ident) if ident.raw == "when" => {
612                // less.js: "Guards are only currently allowed on a single
613                // selector." — a guard on a selector list is rejected whether
614                // the comma comes before or after the `when`.
615                if selector_list.selectors.len() > 1 {
616                    let span = self.cursor.peek()?.span.clone();
617                    return Err(Error {
618                        kind: ErrorKind::LessGuardOnMultipleComplexSelectors,
619                        span,
620                    });
621                }
622                let guard = self.parse::<LessConditions>()?;
623                let block = self.parse::<SimpleBlock>()?;
624                let span = Span { start: selector_list.span.start, end: block.span.end };
625                return Ok(Statement::LessConditionalQualifiedRule(LessConditionalQualifiedRule {
626                    selector: selector_list,
627                    guard,
628                    block,
629                    span,
630                }));
631            }
632            _ => {}
633        }
634
635        let block = self.parse::<SimpleBlock>()?;
636        let span = Span { start: selector_list.span.start, end: block.span.end };
637        Ok(Statement::QualifiedRule(QualifiedRule { selector: selector_list, block, span }))
638    }
639
640    pub(super) fn parse_maybe_hex_color_or_less_mixin_call(
641        &mut self,
642    ) -> PResult<ComponentValue<'a>> {
643        debug_assert_eq!(self.syntax, Syntax::Less);
644
645        let attempt = self.try_parse(|parser| {
646            let hex_color = parser.parse::<HexColor>()?;
647            match parser.cursor.peek()? {
648                TokenWithSpan { token: Token::LParen(..), span } => {
649                    Err(Error { kind: ErrorKind::TryParseError, span: span.clone() })
650                }
651                TokenWithSpan {
652                    token: Token::LBracket(..) | Token::Dot(..) | Token::Hash(..),
653                    span,
654                } if hex_color.span.end == span.start => {
655                    Err(Error { kind: ErrorKind::TryParseError, span: span.clone() })
656                }
657                _ => Ok(hex_color),
658            }
659        });
660        match attempt {
661            Err(Error { kind: ErrorKind::TryParseError, .. }) => {
662                self.parse_less_maybe_mixin_call_or_with_lookups()
663            }
664            hex_color => hex_color.map(ComponentValue::HexColor),
665        }
666    }
667
668    pub(super) fn parse_maybe_less_list(
669        &mut self,
670        allow_comma: bool,
671    ) -> PResult<ComponentValue<'a>> {
672        use util::ListSeparatorKind;
673
674        let single_value = if allow_comma {
675            self.parse_maybe_less_list(false)?
676        } else if let Token::Exclamation(..) = self.cursor.peek()?.token {
677            self.parse().map(ComponentValue::ImportantAnnotation)?
678        } else {
679            self.parse_less_operation(/* allow_mixin_call */ true)?
680        };
681
682        let mut elements = self.vec();
683        let mut comma_spans: Option<oxc_allocator::Vec<'a, Span>> = None;
684        let mut separator = ListSeparatorKind::Unknown;
685        let mut end = single_value.span().end;
686        loop {
687            match self.cursor.peek()?.token {
688                Token::LBrace(..)
689                | Token::RBrace(..)
690                | Token::RParen(..)
691                | Token::Semicolon(..)
692                | Token::Colon(..)
693                | Token::DotDotDot(..)
694                | Token::Eof(..) => break,
695                Token::Comma(..) => {
696                    if !allow_comma {
697                        break;
698                    }
699                    if separator == ListSeparatorKind::Space {
700                        break;
701                    } else {
702                        if separator == ListSeparatorKind::Unknown {
703                            separator = ListSeparatorKind::Comma;
704                        }
705                        let TokenWithSpan { span, .. } = self.cursor.bump()?;
706                        end = span.end;
707                        if let Some(spans) = &mut comma_spans {
708                            spans.push(span);
709                        } else {
710                            comma_spans = Some(self.vec1(span));
711                        }
712                    }
713                }
714                Token::Exclamation(..) => {
715                    if let Ok(important_annotation) = self.try_parse(ImportantAnnotation::parse) {
716                        if end < important_annotation.span.start
717                            && separator == ListSeparatorKind::Unknown
718                        {
719                            separator = ListSeparatorKind::Space;
720                        }
721                        end = important_annotation.span.end;
722                        elements.push(ComponentValue::ImportantAnnotation(important_annotation));
723                    } else {
724                        break;
725                    }
726                }
727                _ => {
728                    if separator == ListSeparatorKind::Unknown {
729                        separator = ListSeparatorKind::Space;
730                    }
731                    let item = if separator == ListSeparatorKind::Comma {
732                        self.parse_maybe_less_list(false)?
733                    } else {
734                        self.parse_less_operation(/* allow_mixin_call */ true)?
735                    };
736                    end = item.span().end;
737                    elements.push(item);
738                }
739            }
740        }
741
742        if elements.is_empty() && separator != ListSeparatorKind::Comma {
743            // If there is a trailing comma it can be a list,
744            // though there is only one element.
745            Ok(single_value)
746        } else {
747            debug_assert_ne!(separator, ListSeparatorKind::Unknown);
748
749            let span = Span { start: single_value.span().start, end };
750            elements.insert(0, single_value);
751            Ok(ComponentValue::LessList(LessList { elements, comma_spans, span }))
752        }
753    }
754}
755
756impl<'a> Parse<'a> for LessConditions<'a> {
757    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
758        let when_span = match input.cursor.bump()? {
759            TokenWithSpan { token: Token::Ident(ident), span } if ident.raw == "when" => span,
760            TokenWithSpan { span, .. } => {
761                return Err(Error { kind: ErrorKind::ExpectLessKeyword("when"), span });
762            }
763        };
764
765        let first = input.parse_less_condition(true)?;
766        let mut span = first.span().clone();
767
768        let mut conditions = input.vec1(first);
769        let mut comma_spans = input.vec();
770        // A comma may also separate the next guarded selector of the rule
771        // (`.a when (..), .b when (..) {`), so only consume it when a
772        // condition actually follows.
773        while matches!(input.cursor.peek()?.token, Token::Comma(..)) {
774            let Ok((comma_span, condition)) = input.try_parse(|p| {
775                let (_, comma_span) = p.cursor.expect_comma()?;
776                let condition = p.parse_less_condition(true)?;
777                Ok((comma_span, condition))
778            }) else {
779                break;
780            };
781            comma_spans.push(comma_span);
782            conditions.push(condition);
783        }
784        debug_assert_eq!(comma_spans.len() + 1, conditions.len());
785
786        if let Some(last) = conditions.last() {
787            span.end = last.span().end;
788        }
789        Ok(LessConditions { conditions, when_span, comma_spans, span })
790    }
791}
792
793impl<'a> Parse<'a> for LessDetachedRuleset<'a> {
794    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
795        let block = input.parse::<SimpleBlock>()?;
796        let span = block.span.clone();
797        Ok(LessDetachedRuleset { block, span })
798    }
799}
800
801impl<'a> Parse<'a> for LessEscapedStr<'a> {
802    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
803        let (_, Span { start, .. }) = input.cursor.expect_tilde()?;
804        let str = {
805            let (str, span) = input.cursor.tokenizer.scan_string_only()?;
806            input.str(str, span)
807        };
808        let span = Span { start, end: str.span().end };
809        Ok(LessEscapedStr { str, span })
810    }
811}
812
813impl<'a> Parse<'a> for LessExtend<'a> {
814    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
815        let mut selector = input.parse::<ComplexSelector>()?;
816
817        let span = selector.span.clone();
818        let mut all = None;
819
820        if let [
821            ..,
822            complex_child,
823            ComplexSelectorChild::Combinator(Combinator {
824                kind: CombinatorKind::Descendant, ..
825            }),
826            ComplexSelectorChild::CompoundSelector(CompoundSelector { children, .. }),
827        ] = &selector.children[..]
828            && let [
829                SimpleSelector::Type(TypeSelector::TagName(TagNameSelector {
830                    name:
831                        WqName {
832                            name: InterpolableIdent::Literal(token_all @ Ident { raw: "all", .. }),
833                            prefix: None,
834                            ..
835                        },
836                    ..
837                })),
838            ] = &children[..]
839        {
840            all = Some(Ident {
841                name: token_all.name,
842                raw: token_all.raw,
843                span: token_all.span.clone(),
844            });
845            selector.span.end = complex_child.span().end;
846            let len = selector.children.len();
847            selector.children.truncate(len - 2);
848        }
849
850        Ok(LessExtend { selector, all, span })
851    }
852}
853
854impl<'a> Parse<'a> for LessExtendList<'a> {
855    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
856        debug_assert_eq!(input.syntax, Syntax::Less);
857
858        let first = input.parse::<LessExtend>()?;
859        let mut span = first.span.clone();
860
861        let mut elements = input.vec1(first);
862        let mut comma_spans = input.vec();
863        while let Some((_, comma_span)) = input.cursor.eat_comma()? {
864            comma_spans.push(comma_span);
865            elements.push(input.parse()?);
866        }
867        debug_assert_eq!(comma_spans.len() + 1, elements.len());
868
869        if let Some(last) = elements.last() {
870            span.end = last.span.end;
871        }
872        Ok(LessExtendList { elements, comma_spans, span })
873    }
874}
875
876impl<'a> Parse<'a> for LessExtendRule<'a> {
877    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
878        let nesting_selector = input.parse::<NestingSelector>()?;
879        if nesting_selector.suffix.is_some() {
880            return Err(Error {
881                kind: ErrorKind::ExpectLessExtendRule,
882                span: nesting_selector.span,
883            });
884        }
885
886        let pseudo_class_selector = input.parse::<PseudoClassSelector>()?;
887        util::assert_no_ws_or_comment(&nesting_selector.span, &pseudo_class_selector.span)?;
888        let span = Span { start: nesting_selector.span.start, end: pseudo_class_selector.span.end };
889
890        let InterpolableIdent::Literal(name_of_extend @ Ident { raw: "extend", .. }) =
891            pseudo_class_selector.name
892        else {
893            return Err(Error { kind: ErrorKind::ExpectLessExtendRule, span });
894        };
895        let Some(PseudoClassSelectorArg {
896            kind: PseudoClassSelectorArgKind::LessExtendList(extend),
897            ..
898        }) = pseudo_class_selector.arg
899        else {
900            return Err(Error { kind: ErrorKind::ExpectLessExtendRule, span });
901        };
902
903        Ok(LessExtendRule { nesting_selector, name_of_extend, extend, span })
904    }
905}
906
907impl<'a> Parse<'a> for LessFormatFunction {
908    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
909        let (_, span) = input.cursor.expect_percent()?;
910        Ok(LessFormatFunction { span })
911    }
912}
913
914impl<'a> Parse<'a> for LessImportOptions<'a> {
915    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
916        let (_, Span { start, .. }) = input.cursor.expect_l_paren()?;
917
918        let mut names = input.vec_with_capacity(1);
919        let mut comma_spans = input.vec();
920        while let Token::Ident(crate::token::Ident {
921            raw: "less" | "css" | "multiple" | "once" | "inline" | "reference" | "optional",
922            ..
923        }) = input.cursor.peek()?.token
924        {
925            names.push(input.parse()?);
926            if !matches!(input.cursor.peek()?.token, Token::RParen(..)) {
927                comma_spans.push(input.cursor.expect_comma()?.1);
928            }
929        }
930        debug_assert!(names.len() - comma_spans.len() <= 1);
931
932        let (_, Span { end, .. }) = input.cursor.expect_r_paren()?;
933
934        Ok(LessImportOptions { names, comma_spans, span: Span { start, end } })
935    }
936}
937
938impl<'a> Parse<'a> for LessImportPrelude<'a> {
939    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
940        let options = input.parse::<LessImportOptions>()?;
941        let start = options.span.start;
942
943        let href = match &input.cursor.peek()?.token {
944            Token::Str(..) | Token::StrTemplate(..) => input.parse().map(ImportPreludeHref::Str)?,
945            _ => input.parse().map(ImportPreludeHref::Url)?,
946        };
947        let mut end = href.span().end;
948
949        let media = if matches!(input.cursor.peek()?.token, Token::Semicolon(..)) {
950            None
951        } else {
952            let media = input.parse::<MediaQueryList>()?;
953            end = media.span.end;
954            Some(media)
955        };
956
957        Ok(LessImportPrelude { href, options, media, span: Span { start, end } })
958    }
959}
960
961impl<'a> Parse<'a> for LessInterpolatedStr<'a> {
962    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
963        let (first, first_span) = input.cursor.expect_str_template()?;
964        let quote = first.raw.chars().next().unwrap();
965        debug_assert!(quote == '\'' || quote == '"');
966        let mut span = first_span.clone();
967        let mut elements = input.vec1(LessInterpolatedStrElement::Static(
968            input.interpolable_str_static_part(first, first_span),
969        ));
970
971        let mut is_parsing_static_part = false;
972        loop {
973            if is_parsing_static_part {
974                let (token, str_tpl_span) = input.cursor.tokenizer.scan_string_template(quote)?;
975                let tail = token.tail;
976                let end = str_tpl_span.end;
977                elements.push(LessInterpolatedStrElement::Static(
978                    input.interpolable_str_static_part(token, str_tpl_span),
979                ));
980                if tail {
981                    span.end = end;
982                    break;
983                }
984            } else {
985                // '@' or '$' is consumed, so '{' left only
986                let start = input.cursor.expect_l_brace()?.1.start - 1;
987                // Less interpolation names may start with a digit (`@{3}`).
988                let (name, name_span) = input.cursor.expect_ident_without_ws_or_comments(true)?;
989
990                let end = input.cursor.expect_r_brace()?.1.end;
991                elements.push(match input.source.as_bytes().get(start) {
992                    Some(b'@') => LessInterpolatedStrElement::Variable(LessVariableInterpolation {
993                        name: input.ident(name, name_span),
994                        span: Span { start, end },
995                    }),
996                    Some(b'$') => LessInterpolatedStrElement::Property(LessPropertyInterpolation {
997                        name: input.ident(name, name_span),
998                        span: Span { start, end },
999                    }),
1000                    _ => unreachable!(),
1001                });
1002            }
1003            is_parsing_static_part = !is_parsing_static_part;
1004        }
1005
1006        Ok(LessInterpolatedStr { elements, span })
1007    }
1008}
1009
1010impl<'a> Parse<'a> for LessJavaScriptSnippet<'a> {
1011    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1012        let tilde = input.cursor.eat_tilde()?;
1013        let (token, span) = input.cursor.expect_backtick_code()?;
1014
1015        Ok(LessJavaScriptSnippet {
1016            code: &token.raw[1..token.raw.len() - 1],
1017            raw: token.raw,
1018            escaped: tilde.is_some(),
1019            span: Span {
1020                start: tilde.map(|(_, span)| span.start).unwrap_or(span.start),
1021                end: span.end,
1022            },
1023        })
1024    }
1025}
1026
1027impl<'a> Parse<'a> for LessListFunction {
1028    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1029        let (_, span) = input.cursor.expect_tilde()?;
1030        Ok(LessListFunction { span })
1031    }
1032}
1033
1034impl<'a> Parse<'a> for LessLookup<'a> {
1035    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1036        debug_assert_eq!(input.syntax, Syntax::Less);
1037
1038        let (_, Span { start, .. }) = input.cursor.expect_l_bracket()?;
1039        let name = if let Token::RBracket(..) = input.cursor.peek()?.token {
1040            None
1041        } else {
1042            Some(input.parse()?)
1043        };
1044        let (_, Span { end, .. }) = input.cursor.expect_r_bracket()?;
1045        Ok(LessLookup { name, span: Span { start, end } })
1046    }
1047}
1048
1049impl<'a> Parse<'a> for LessLookupName<'a> {
1050    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1051        debug_assert_eq!(input.syntax, Syntax::Less);
1052
1053        let is_dollar = {
1054            let TokenWithSpan { token, span } = input.cursor.peek()?;
1055            matches!(token, Token::Unknown(..))
1056                && input.source.as_bytes().get(span.start) == Some(&b'$')
1057        };
1058        match input.cursor.peek()?.token {
1059            Token::AtKeyword(..) => input.parse().map(LessLookupName::LessVariable),
1060            Token::At(..) => input.parse().map(LessLookupName::LessVariableVariable),
1061            Token::DollarVar(..) => input.parse().map(LessLookupName::LessPropertyVariable),
1062            // `[$@var]` — a property lookup whose name is interpolated from a
1063            // variable; the lone `$` arrives as an <unknown-token>
1064            Token::Unknown(..) if is_dollar => {
1065                let dollar_span = input.cursor.bump()?.span;
1066                let variable = input.parse::<LessVariable>()?;
1067                util::assert_no_ws_or_comment(&dollar_span, &variable.span)?;
1068                let span = Span { start: dollar_span.start, end: variable.span.end };
1069                Ok(LessLookupName::LessPropertyInterpolation(LessPropertyInterpolation {
1070                    name: variable.name,
1071                    span,
1072                }))
1073            }
1074            _ => input.parse().map(LessLookupName::Ident),
1075        }
1076    }
1077}
1078
1079impl<'a> Parse<'a> for LessLookups<'a> {
1080    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1081        debug_assert_eq!(input.syntax, Syntax::Less);
1082
1083        let first = input.parse::<LessLookup>()?;
1084        let mut span = first.span.clone();
1085
1086        let mut lookups = input.vec1(first);
1087        while let Token::LBracket(..) = input.cursor.peek()?.token {
1088            lookups.push(input.parse()?);
1089        }
1090
1091        if let Some(last) = lookups.last() {
1092            span.end = last.span.end;
1093        }
1094        Ok(LessLookups { lookups, span })
1095    }
1096}
1097
1098impl<'a> Parse<'a> for LessMixinCall<'a> {
1099    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1100        debug_assert_eq!(input.syntax, Syntax::Less);
1101
1102        let callee = input.parse::<LessMixinCallee>()?;
1103
1104        let mut end = callee.span.end;
1105        let args = if let Some((_, lparen_span)) = input.cursor.eat_l_paren()? {
1106            let mut semicolon_comes_at = 0;
1107            let mut args = input.vec();
1108            let mut comma_spans = input.vec();
1109            let mut semicolon_spans = input.vec();
1110            loop {
1111                match input.cursor.peek()?.token {
1112                    Token::RParen(..) => {
1113                        let TokenWithSpan { span, .. } = input.cursor.bump()?;
1114                        if semicolon_comes_at > 0 {
1115                            let comma_spans = mem::replace(&mut comma_spans, input.vec());
1116                            wrap_less_mixin_args_into_less_list(
1117                                input.allocator,
1118                                &mut args,
1119                                comma_spans,
1120                                semicolon_comes_at,
1121                            )
1122                            .map_err(|kind| Error {
1123                                kind,
1124                                span: Span {
1125                                    // We've checked `semicolon_comes_at` must be greater than 0,
1126                                    // so `args` won't be empty.
1127                                    start: args.first().unwrap().span().start,
1128                                    end: args.last().unwrap().span().end,
1129                                },
1130                            })?;
1131                        }
1132                        end = span.end;
1133                        break;
1134                    }
1135                    Token::LBrace(..) => args.push(LessMixinArgument::Value(
1136                        ComponentValue::LessDetachedRuleset(input.parse()?),
1137                    )),
1138                    Token::Comma(..) => {
1139                        return Err(Error {
1140                            kind: ErrorKind::ExpectComponentValue,
1141                            span: input.cursor.bump()?.span,
1142                        });
1143                    }
1144                    _ => 'maybe: {
1145                        let value = input.parse_maybe_less_list(/* allow_comma */ false)?;
1146                        let name = {
1147                            match value {
1148                                ComponentValue::LessVariable(variable) => {
1149                                    LessMixinParameterName::Variable(variable)
1150                                }
1151                                ComponentValue::LessPropertyVariable(property) => {
1152                                    LessMixinParameterName::PropertyVariable(property)
1153                                }
1154                                value => {
1155                                    args.push(LessMixinArgument::Value(value));
1156                                    break 'maybe;
1157                                }
1158                            }
1159                        };
1160                        if let Some((_, colon_span)) = input.cursor.eat_colon()? {
1161                            let value = if matches!(input.cursor.peek()?.token, Token::LBrace(..)) {
1162                                input.parse().map(ComponentValue::LessDetachedRuleset)?
1163                            } else {
1164                                input.parse_maybe_less_list(
1165                                    /* allow_comma */ semicolon_comes_at > 0,
1166                                )?
1167                            };
1168                            let span = Span { start: name.span().start, end: value.span().end };
1169                            args.push(LessMixinArgument::Named(LessMixinNamedArgument {
1170                                name,
1171                                colon_span,
1172                                value,
1173                                span,
1174                            }));
1175                        } else if let Some((_, dotdotdot_span)) = input.cursor.eat_dot_dot_dot()? {
1176                            // unlike definitions, call-site spreads may appear
1177                            // anywhere and repeat: `.m(@x..., @a: 0)`,
1178                            // `.aa(@y, @x..., and again, @y...)`
1179                            let span = Span { start: name.span().start, end: dotdotdot_span.end };
1180                            args.push(LessMixinArgument::Variadic(LessMixinVariadicArgument {
1181                                name,
1182                                span,
1183                            }));
1184                        } else {
1185                            args.push(LessMixinArgument::Value(match name {
1186                                LessMixinParameterName::Variable(variable) => {
1187                                    ComponentValue::LessVariable(variable)
1188                                }
1189                                LessMixinParameterName::PropertyVariable(property_variable) => {
1190                                    ComponentValue::LessPropertyVariable(property_variable)
1191                                }
1192                            }));
1193                        }
1194                    }
1195                };
1196
1197                match input.cursor.peek()?.token {
1198                    Token::RParen(..) => {}
1199                    Token::Comma(..) => {
1200                        comma_spans.push(input.cursor.bump()?.span);
1201                    }
1202                    Token::Semicolon(..) => {
1203                        let TokenWithSpan { span, .. } = input.cursor.bump()?;
1204                        let comma_spans = mem::replace(&mut comma_spans, input.vec());
1205                        wrap_less_mixin_args_into_less_list(
1206                            input.allocator,
1207                            &mut args,
1208                            comma_spans,
1209                            semicolon_comes_at,
1210                        )
1211                        .map_err(|kind| Error { kind, span: span.clone() })?;
1212                        semicolon_comes_at = args.len();
1213                        semicolon_spans.push(span);
1214                    }
1215                    _ => {
1216                        let TokenWithSpan { token, span } = input.cursor.bump()?;
1217
1218                        return Err(Error {
1219                            kind: ErrorKind::Unexpected(")", token.symbol()),
1220                            span,
1221                        });
1222                    }
1223                }
1224            }
1225            let is_comma_separated = semicolon_spans.is_empty();
1226            let separator_spans =
1227                if semicolon_spans.is_empty() { comma_spans } else { semicolon_spans };
1228            debug_assert!(args.len() - separator_spans.len() <= 1);
1229            Some(LessMixinArguments {
1230                args,
1231                is_comma_separated,
1232                separator_spans,
1233                span: Span { start: lparen_span.start, end },
1234            })
1235        } else {
1236            None
1237        };
1238
1239        let important = if !matches!(
1240            input.state.qualified_rule_ctx,
1241            Some(QualifiedRuleContext::DeclarationValue)
1242        ) && matches!(input.cursor.peek()?.token, Token::Exclamation(..))
1243        {
1244            input.parse::<ImportantAnnotation>().map(Some)?
1245        } else {
1246            None
1247        };
1248
1249        let span = Span {
1250            start: callee.span.start,
1251            end: important.as_ref().map(|important| important.span.end).unwrap_or(end),
1252        };
1253        Ok(LessMixinCall { callee, args, important, span })
1254    }
1255}
1256
1257impl<'a> Parser<'a> {
1258    fn parse_less_mixin_parameters(&mut self) -> PResult<LessMixinParameters<'a>> {
1259        let (_, lparen_span) = self.cursor.expect_l_paren()?;
1260        let rparen_span;
1261        let mut semicolon_comes_at = 0;
1262        let mut params = self.vec();
1263        let mut comma_spans = self.vec();
1264        let mut semicolon_spans = self.vec();
1265        'params: loop {
1266            match self.cursor.peek()?.token {
1267                Token::RParen(..) => {
1268                    rparen_span = self.cursor.bump()?.span;
1269                    break;
1270                }
1271                Token::DotDotDot(..) => {
1272                    let TokenWithSpan { span, .. } = self.cursor.bump()?;
1273                    params.push(LessMixinParameter::Variadic(LessMixinVariadicParameter {
1274                        name: None,
1275                        span,
1276                    }));
1277                    self.cursor.eat_semicolon()?;
1278                    (_, rparen_span) = self.cursor.expect_r_paren()?;
1279                    break;
1280                }
1281                Token::Comma(..) => {
1282                    return Err(Error {
1283                        kind: ErrorKind::ExpectComponentValue,
1284                        span: self.cursor.bump()?.span,
1285                    });
1286                }
1287                _ => 'maybe: {
1288                    let value = self
1289                        .with_state(ParserState {
1290                            less_ctx: self.state.less_ctx | LESS_CTX_ALLOW_DIV,
1291                            ..self.state.clone()
1292                        })
1293                        .parse::<ComponentValue>()?;
1294                    let name = {
1295                        match value {
1296                            ComponentValue::LessVariable(variable) => {
1297                                LessMixinParameterName::Variable(variable)
1298                            }
1299                            ComponentValue::LessPropertyVariable(property) => {
1300                                LessMixinParameterName::PropertyVariable(property)
1301                            }
1302                            value => {
1303                                let span = value.span().clone();
1304                                params.push(LessMixinParameter::Unnamed(
1305                                    LessMixinUnnamedParameter { value, span },
1306                                ));
1307                                break 'maybe;
1308                            }
1309                        }
1310                    };
1311                    let name_span = name.span();
1312                    if let Some((_, colon_span)) = self.cursor.eat_colon()? {
1313                        let value = if matches!(self.cursor.peek()?.token, Token::LBrace(..)) {
1314                            self.parse().map(ComponentValue::LessDetachedRuleset)?
1315                        } else {
1316                            self.with_state(ParserState {
1317                                less_ctx: self.state.less_ctx | LESS_CTX_ALLOW_DIV,
1318                                ..self.state.clone()
1319                            })
1320                            .parse_maybe_less_list(/* allow_comma */ false)?
1321                        };
1322                        let end = value.span().end;
1323                        let default_value = {
1324                            let span = Span { start: colon_span.start, end };
1325                            LessMixinNamedParameterDefaultValue { colon_span, value, span }
1326                        };
1327                        let span = Span { start: name_span.start, end };
1328                        params.push(LessMixinParameter::Named(LessMixinNamedParameter {
1329                            name,
1330                            value: Some(default_value),
1331                            span,
1332                        }));
1333                    } else if let Some((_, Span { end, .. })) = self.cursor.eat_dot_dot_dot()? {
1334                        let span = Span { start: name_span.start, end };
1335                        params.push(LessMixinParameter::Variadic(LessMixinVariadicParameter {
1336                            name: Some(name),
1337                            span,
1338                        }));
1339                        if let Some((_, semicolon_span)) = self.cursor.eat_semicolon()? {
1340                            semicolon_spans.push(semicolon_span);
1341                        };
1342                        (_, rparen_span) = self.cursor.expect_r_paren()?;
1343                        break 'params;
1344                    } else {
1345                        let span = name_span.clone();
1346                        params.push(LessMixinParameter::Named(LessMixinNamedParameter {
1347                            name,
1348                            value: None,
1349                            span,
1350                        }));
1351                    }
1352                }
1353            }
1354
1355            match &self.cursor.peek()?.token {
1356                Token::RParen(..) => {
1357                    let span = self.cursor.bump()?.span;
1358                    if semicolon_comes_at > 0 {
1359                        let comma_spans = mem::replace(&mut comma_spans, self.vec());
1360                        wrap_less_mixin_params_into_less_list(
1361                            self.allocator,
1362                            &mut params,
1363                            comma_spans,
1364                            semicolon_comes_at,
1365                        )
1366                        .map_err(|kind| Error {
1367                            kind,
1368                            span: Span {
1369                                // We've checked `semicolon_comes_at` must be greater than 0,
1370                                // so `params` won't be empty.
1371                                start: params.first().unwrap().span().start,
1372                                end: params.last().unwrap().span().end,
1373                            },
1374                        })?;
1375                    }
1376                    rparen_span = span;
1377                    break;
1378                }
1379                Token::Comma(..) => {
1380                    comma_spans.push(self.cursor.bump()?.span);
1381                }
1382                Token::Semicolon(..) => {
1383                    let span = self.cursor.bump()?.span;
1384                    let comma_spans = mem::replace(&mut comma_spans, self.vec());
1385                    wrap_less_mixin_params_into_less_list(
1386                        self.allocator,
1387                        &mut params,
1388                        comma_spans,
1389                        semicolon_comes_at,
1390                    )
1391                    .map_err(|kind| Error { kind, span: span.clone() })?;
1392                    semicolon_comes_at = params.len();
1393                    semicolon_spans.push(span);
1394                }
1395                // less.js also accepts space-separated parameters
1396                // (`.m(@a @b)`); each element becomes its own parameter
1397                _ => {}
1398            }
1399        }
1400        let is_comma_separated = semicolon_spans.is_empty();
1401        let separator_spans =
1402            if semicolon_spans.is_empty() { comma_spans } else { semicolon_spans };
1403        debug_assert!(separator_spans.len() <= params.len());
1404        Ok(LessMixinParameters {
1405            params,
1406            is_comma_separated,
1407            separator_spans,
1408            span: Span { start: lparen_span.start, end: rparen_span.end },
1409        })
1410    }
1411
1412    /// `.(@v; @i) { ... }` / `#(@v, @k, @i) { ... }` — an anonymous mixin,
1413    /// used as a callback argument to `each()` and friends.
1414    pub(super) fn parse_less_anonymous_mixin(&mut self) -> PResult<LessAnonymousMixin<'a>> {
1415        debug_assert_eq!(self.syntax, Syntax::Less);
1416
1417        let TokenWithSpan { token, span: head_span } = self.cursor.bump()?;
1418        if !matches!(token, Token::Dot(..) | Token::NumberSign(..))
1419            || self.cursor.peek()?.span.start != head_span.end
1420        {
1421            return Err(Error { kind: ErrorKind::TryParseError, span: head_span });
1422        }
1423        let params = self.parse_less_mixin_parameters()?;
1424        let block = self.parse::<SimpleBlock>()?;
1425        let span = Span { start: head_span.start, end: block.span.end };
1426        Ok(LessAnonymousMixin { params, block, span })
1427    }
1428}
1429
1430impl<'a> Parse<'a> for LessMixinCallee<'a> {
1431    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1432        let first_name = input.parse::<LessMixinName>()?;
1433        let mut span = first_name.span().clone();
1434
1435        let mut children = input.vec1(LessMixinCalleeChild {
1436            name: first_name,
1437            combinator: None,
1438            span: span.clone(),
1439        });
1440        loop {
1441            let combinator = input
1442                .cursor
1443                .eat_greater_than()?
1444                .map(|(_, span)| Combinator { kind: CombinatorKind::Child, span });
1445            let at_name = {
1446                let TokenWithSpan { token, span } = input.cursor.peek()?;
1447                matches!(token, Token::Dot(..) | Token::Hash(..))
1448                    || (matches!(token, Token::Dimension(..))
1449                        && input.source.as_bytes().get(span.start) == Some(&b'.'))
1450            };
1451            if at_name {
1452                let name = input.parse::<LessMixinName>()?;
1453                let name_span = name.span();
1454                let span = Span {
1455                    start: combinator
1456                        .as_ref()
1457                        .map(|combinator| combinator.span.start)
1458                        .unwrap_or(name_span.start),
1459                    end: name_span.end,
1460                };
1461                children.push(LessMixinCalleeChild { name, combinator, span });
1462            } else {
1463                break;
1464            }
1465        }
1466
1467        if let Some(last) = children.last() {
1468            span.end = last.span.end;
1469        }
1470        Ok(LessMixinCallee { children, span })
1471    }
1472}
1473
1474impl<'a> Parse<'a> for LessMixinDefinition<'a> {
1475    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1476        debug_assert_eq!(input.syntax, Syntax::Less);
1477
1478        let name = input.parse::<LessMixinName>()?;
1479
1480        let params = input.parse_less_mixin_parameters()?;
1481
1482        let guard = match &input.cursor.peek()?.token {
1483            Token::Ident(ident) if ident.raw == "when" => Some(input.parse()?),
1484            _ => None,
1485        };
1486
1487        let block = input
1488            .with_state(ParserState {
1489                less_ctx: input.state.less_ctx | LESS_CTX_ALLOW_KEYFRAME_BLOCK,
1490                ..input.state.clone()
1491            })
1492            .parse::<SimpleBlock>()?;
1493
1494        let span = Span { start: name.span().start, end: block.span.end };
1495
1496        Ok(LessMixinDefinition { name, params, guard, block, span })
1497    }
1498}
1499
1500impl<'a> Parse<'a> for LessMixinName<'a> {
1501    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1502        match input.cursor.bump()? {
1503            // `.3D` — a digit-led name arrives as one <dimension-token>
1504            TokenWithSpan { token: Token::Dimension(..), span }
1505                if input.source.as_bytes().get(span.start) == Some(&b'.') =>
1506            {
1507                let name_span = Span { start: span.start + 1, end: span.end };
1508                let raw = &input.source[name_span.start..name_span.end];
1509                Ok(LessMixinName::ClassSelector(ClassSelector {
1510                    name: InterpolableIdent::Literal(Ident { name: raw, raw, span: name_span }),
1511                    span,
1512                }))
1513            }
1514            TokenWithSpan { token: Token::Dot(..), span: dot_span } => {
1515                let (ident, ident_span) =
1516                    input.cursor.expect_ident_without_ws_or_comments(false)?;
1517                let ident = input.ident(ident, ident_span);
1518                let span = Span { start: dot_span.start, end: ident.span.end };
1519                Ok(LessMixinName::ClassSelector(ClassSelector {
1520                    name: InterpolableIdent::Literal(ident),
1521                    span,
1522                }))
1523            }
1524            TokenWithSpan { token: Token::Hash(hash), span } => {
1525                let raw = hash.raw;
1526                if raw.starts_with(|c: char| c.is_ascii_digit())
1527                    || matches!(raw.as_bytes(), [b'-'] | [b'-', b'0'..=b'9', ..])
1528                {
1529                    input
1530                        .recoverable_errors
1531                        .push(Error { kind: ErrorKind::InvalidIdSelectorName, span: span.clone() });
1532                }
1533                let name =
1534                    if hash.escaped { util::handle_escape_in(raw, input.allocator) } else { raw };
1535                let name_span = Span { start: span.start + 1, end: span.end };
1536                Ok(LessMixinName::IdSelector(IdSelector {
1537                    name: InterpolableIdent::Literal(Ident { name, raw, span: name_span }),
1538                    span,
1539                }))
1540            }
1541            TokenWithSpan { token, span } => Err(Error {
1542                kind: ErrorKind::ExpectOneOf(vec![".", "<hash>"], token.symbol()),
1543                span,
1544            }),
1545        }
1546    }
1547}
1548
1549impl<'a> Parse<'a> for LessMixinParameterName<'a> {
1550    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1551        if matches!(input.cursor.peek()?.token, Token::AtKeyword(..)) {
1552            input.parse().map(LessMixinParameterName::Variable)
1553        } else {
1554            input.parse().map(LessMixinParameterName::PropertyVariable)
1555        }
1556    }
1557}
1558
1559impl<'a> Parse<'a> for LessNamespaceValue<'a> {
1560    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1561        let callee = input.parse::<LessNamespaceValueCallee>()?;
1562        let callee_span = callee.span();
1563
1564        let lookups = input.parse::<LessLookups>()?;
1565        util::assert_no_ws_or_comment(callee_span, &lookups.span)?;
1566
1567        let span = Span { start: callee_span.start, end: lookups.span.end };
1568        Ok(LessNamespaceValue { callee, lookups, span })
1569    }
1570}
1571
1572impl<'a> Parse<'a> for LessNamespaceValueCallee<'a> {
1573    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1574        if matches!(input.cursor.peek()?.token, Token::AtKeyword(..)) {
1575            input.parse().map(LessNamespaceValueCallee::LessVariable)
1576        } else {
1577            input.parse().map(LessNamespaceValueCallee::LessMixinCall)
1578        }
1579    }
1580}
1581
1582impl<'a> Parse<'a> for LessNegativeValue<'a> {
1583    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1584        let (_, minus_span) = input.cursor.expect_minus()?;
1585        let value = match input.cursor.peek()? {
1586            TokenWithSpan {
1587                token: Token::AtKeyword(..) | Token::At(..) | Token::DollarVar(..),
1588                span,
1589            } if minus_span.end == span.start => {
1590                let value = input.parse_component_value_atom()?;
1591                input.alloc(value)
1592            }
1593            TokenWithSpan { token: Token::LParen(..), span } if minus_span.end == span.start => {
1594                let value = ComponentValue::LessParenthesizedOperation(
1595                    input.parse_less_parenthesized_operation(/* allow_mixin_call */ true)?,
1596                );
1597                input.alloc(value)
1598            }
1599            TokenWithSpan { token, span } => {
1600                return Err(Error {
1601                    kind: ErrorKind::ExpectOneOf(vec!["<at-keyword>", "$var", "("], token.symbol()),
1602                    span: span.clone(),
1603                });
1604            }
1605        };
1606
1607        let span = Span { start: minus_span.start, end: value.span().end };
1608        Ok(LessNegativeValue { value, span })
1609    }
1610}
1611
1612impl<'a> Parse<'a> for LessPercentKeyword {
1613    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1614        let (_, span) = input.cursor.expect_percent()?;
1615        Ok(LessPercentKeyword { span })
1616    }
1617}
1618
1619impl<'a> Parse<'a> for LessPlugin<'a> {
1620    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1621        debug_assert_eq!(input.syntax, Syntax::Less);
1622
1623        let mut start = None;
1624
1625        let args = if let Some((_, span)) = input.cursor.eat_l_paren()? {
1626            start = Some(span.start);
1627            let args = input.parse_tokens_in_parens()?;
1628            input.cursor.expect_r_paren()?;
1629            Some(args)
1630        } else {
1631            None
1632        };
1633
1634        let path = input.parse::<LessPluginPath>()?;
1635        let path_span = path.span();
1636
1637        let span = Span { start: start.unwrap_or(path_span.start), end: path_span.end };
1638        Ok(LessPlugin { path, args, span })
1639    }
1640}
1641
1642impl<'a> Parse<'a> for LessPluginPath<'a> {
1643    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1644        if let Token::Str(..) = input.cursor.peek()?.token {
1645            input.parse().map(LessPluginPath::Str)
1646        } else {
1647            input.parse().map(LessPluginPath::Url)
1648        }
1649    }
1650}
1651
1652impl<'a> Parse<'a> for LessPropertyInterpolation<'a> {
1653    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1654        let (dollar_lbrace_var, span) = input.cursor.expect_dollar_l_brace_var()?;
1655        Ok(LessPropertyInterpolation {
1656            name: input
1657                .ident(dollar_lbrace_var.ident, Span { start: span.start + 2, end: span.end - 1 }),
1658            span,
1659        })
1660    }
1661}
1662
1663impl<'a> Parse<'a> for Option<LessPropertyMerge> {
1664    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1665        debug_assert!(matches!(input.syntax, Syntax::Less | Syntax::Css));
1666
1667        match &input.cursor.peek()?.token {
1668            Token::Plus(..) => Ok(Some(LessPropertyMerge {
1669                kind: LessPropertyMergeKind::Comma,
1670                span: input.cursor.bump()?.span,
1671            })),
1672            Token::PlusUnderscore(..) => Ok(Some(LessPropertyMerge {
1673                kind: LessPropertyMergeKind::Space,
1674                span: input.cursor.bump()?.span,
1675            })),
1676            _ => Ok(None),
1677        }
1678    }
1679}
1680
1681impl<'a> Parse<'a> for LessPropertyVariable<'a> {
1682    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1683        let (dollar_var, span) = input.cursor.expect_dollar_var()?;
1684        Ok(LessPropertyVariable {
1685            name: input.ident(dollar_var.ident, Span { start: span.start + 1, end: span.end }),
1686            span,
1687        })
1688    }
1689}
1690
1691impl<'a> Parse<'a> for LessVariable<'a> {
1692    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1693        let (at_keyword, span) = input.cursor.expect_at_keyword()?;
1694        Ok(LessVariable {
1695            name: input.ident(at_keyword.ident, Span { start: span.start + 1, end: span.end }),
1696            span,
1697        })
1698    }
1699}
1700
1701impl<'a> Parse<'a> for LessVariableCall<'a> {
1702    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1703        let variable = input.parse::<LessVariable>()?;
1704        input.cursor.expect_l_paren_without_ws_or_comments()?;
1705        let (_, Span { end, .. }) = input.cursor.expect_r_paren()?;
1706
1707        let span = Span { start: variable.span.start, end };
1708        Ok(LessVariableCall { variable, span })
1709    }
1710}
1711
1712impl<'a> Parse<'a> for LessVariableDeclaration<'a> {
1713    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1714        debug_assert_eq!(input.syntax, Syntax::Less);
1715
1716        let name = input.parse::<LessVariable>()?;
1717        let (_, colon_span) = input.cursor.expect_colon()?;
1718        let value = if matches!(input.cursor.peek()?.token, Token::LBrace(..)) {
1719            ComponentValue::LessDetachedRuleset(input.parse()?)
1720        } else {
1721            let typed = input.try_parse(|p| {
1722                let value = p
1723                    .with_state(ParserState {
1724                        less_ctx: p.state.less_ctx | LESS_CTX_ALLOW_DIV,
1725                        ..p.state.clone()
1726                    })
1727                    .parse_maybe_less_list(/* allow_comma */ true)?;
1728                // The declaration must account for everything up to a
1729                // statement boundary — `@page :first {` is an at-rule, not a
1730                // variable named `@page` with the value `first`.
1731                if !matches!(
1732                    &p.cursor.peek()?.token,
1733                    Token::Semicolon(..) | Token::RBrace(..) | Token::Eof(..)
1734                ) {
1735                    let span = p.cursor.peek()?.span.clone();
1736                    return Err(Error { kind: ErrorKind::TryParseError, span });
1737                }
1738                Ok(value)
1739            });
1740            match typed {
1741                Ok(value) => value,
1742                Err(error) => {
1743                    // less.js `permissiveValue`: a variable's value may be any
1744                    // balanced token run (`@this: () => { ... };`), but only
1745                    // when explicitly terminated by `;` — otherwise
1746                    // `@page :first { ... }` would be swallowed too.
1747                    let start = input.cursor.peek()?.span.start;
1748                    let values = input
1749                        .parse_declaration_value_tokens(/* stop_at_top_level_brace */ false)?;
1750                    let end = values.last().map(|v| v.span().end);
1751                    if !matches!(input.cursor.peek()?.token, Token::Semicolon(..)) {
1752                        return Err(error);
1753                    }
1754                    let Some(end) = end else {
1755                        return Err(error);
1756                    };
1757                    ComponentValue::LessList(LessList {
1758                        elements: values,
1759                        comma_spans: None,
1760                        span: Span { start, end },
1761                    })
1762                }
1763            }
1764        };
1765        let span = Span { start: name.span.start, end: value.span().end };
1766        Ok(LessVariableDeclaration { name, colon_span, value, span })
1767    }
1768}
1769
1770impl<'a> Parse<'a> for LessVariableInterpolation<'a> {
1771    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1772        let (at_lbrace_var, span) = input.cursor.expect_at_l_brace_var()?;
1773        Ok(LessVariableInterpolation {
1774            name: input
1775                .ident(at_lbrace_var.ident, Span { start: span.start + 2, end: span.end - 1 }),
1776            span,
1777        })
1778    }
1779}
1780
1781impl<'a> Parse<'a> for LessVariableVariable<'a> {
1782    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1783        let (_, at_span) = input.cursor.expect_at()?;
1784        let variable = input.parse::<LessVariable>()?;
1785        util::assert_no_ws_or_comment(&at_span, &variable.span)?;
1786
1787        let span = Span { start: at_span.start, end: variable.span.end };
1788        Ok(LessVariableVariable { variable, span })
1789    }
1790}
1791
1792fn wrap_less_mixin_params_into_less_list<'a>(
1793    allocator: &'a oxc_allocator::Allocator,
1794    params: &mut oxc_allocator::Vec<'a, LessMixinParameter<'a>>,
1795    comma_spans: oxc_allocator::Vec<'a, Span>,
1796    index: usize,
1797) -> Result<(), ErrorKind> {
1798    if let [first, .., last] = &params[index..] {
1799        let span = Span { start: first.span().start, end: last.span().end };
1800        // `@margin: 2, 2, 2, 2; ...` — a named parameter followed by plain
1801        // values is one parameter whose default is the comma list. Two named
1802        // parameters in one comma group stay rejected
1803        // (`.mixin(@a: 5, @b: 6; @c: 7)`), as in less.js.
1804        let named_head = matches!(
1805            &params[index],
1806            LessMixinParameter::Named(LessMixinNamedParameter { value: Some(..), .. })
1807        ) && params.len() - index > 1;
1808        let mut drained = params.drain(index..);
1809        let head = if named_head { drained.next() } else { None };
1810        let mut elements = oxc_allocator::Vec::with_capacity_in(drained.len() + 1, &allocator);
1811        let head = match head {
1812            Some(LessMixinParameter::Named(LessMixinNamedParameter {
1813                name,
1814                value: Some(default),
1815                ..
1816            })) => {
1817                elements.push(default.value);
1818                Some((name, default.colon_span))
1819            }
1820            _ => None,
1821        };
1822        for param in drained {
1823            if let LessMixinParameter::Unnamed(LessMixinUnnamedParameter { value, .. }) = param {
1824                elements.push(value);
1825            } else {
1826                // reject code like this:
1827                // .mixin(@a: 5, @b: 6; @c: 7) {}
1828                // .mixin(@a: 5; @b: 6, @c: 7) {}
1829                return Err(ErrorKind::MixedDelimiterKindInLessMixin);
1830            }
1831        }
1832        debug_assert!(comma_spans.len() < elements.len());
1833        let list_span =
1834            Span { start: elements.first().map_or(span.start, |v| v.span().start), end: span.end };
1835        let list = ComponentValue::LessList(LessList {
1836            elements,
1837            comma_spans: Some(comma_spans),
1838            span: list_span.clone(),
1839        });
1840        params.push(match head {
1841            Some((name, colon_span)) => LessMixinParameter::Named(LessMixinNamedParameter {
1842                span: Span { start: name.span().start, end: span.end },
1843                name,
1844                value: Some(LessMixinNamedParameterDefaultValue {
1845                    colon_span,
1846                    value: list,
1847                    span: list_span,
1848                }),
1849            }),
1850            None => LessMixinParameter::Unnamed(LessMixinUnnamedParameter { value: list, span }),
1851        });
1852    }
1853    Ok(())
1854}
1855
1856fn wrap_less_mixin_args_into_less_list<'a>(
1857    allocator: &'a oxc_allocator::Allocator,
1858    args: &mut oxc_allocator::Vec<'a, LessMixinArgument<'a>>,
1859    comma_spans: oxc_allocator::Vec<'a, Span>,
1860    index: usize,
1861) -> Result<(), ErrorKind> {
1862    if let [first, .., last] = &args[index..] {
1863        let span = Span { start: first.span().start, end: last.span().end };
1864        // `@a : d, e; @b : f` — a named argument followed by plain values is
1865        // one named argument whose value is the comma list; its trailing
1866        // values fold into it. Two named arguments in one comma group stay
1867        // rejected (`.mixin(@a: 5, @b: 6; @c: 7)`), as in less.js.
1868        let named_head =
1869            matches!(&args[index], LessMixinArgument::Named(..)) && args.len() - index > 1;
1870        let mut drained = args.drain(index..);
1871        let head = if named_head { drained.next() } else { None };
1872        let mut elements = oxc_allocator::Vec::with_capacity_in(drained.len() + 1, &allocator);
1873        let mut head = match head {
1874            Some(LessMixinArgument::Named(named)) => {
1875                elements.push(named.value);
1876                Some((named.name, named.colon_span))
1877            }
1878            _ => None,
1879        };
1880        for arg in drained {
1881            if let LessMixinArgument::Value(value) = arg {
1882                elements.push(value);
1883            } else {
1884                // reject code like this:
1885                // .mixin(@a: 5, @b: 6; @c: 7) {}
1886                // .mixin(@a: 5; @b: 6, @c: 7) {}
1887                return Err(ErrorKind::MixedDelimiterKindInLessMixin);
1888            }
1889        }
1890        debug_assert!(comma_spans.len() < elements.len());
1891        let list_span =
1892            Span { start: elements.first().map_or(span.start, |v| v.span().start), end: span.end };
1893        let list = ComponentValue::LessList(LessList {
1894            elements,
1895            comma_spans: Some(comma_spans),
1896            span: list_span,
1897        });
1898        args.push(match head.take() {
1899            Some((name, colon_span)) => LessMixinArgument::Named(LessMixinNamedArgument {
1900                span: Span { start: name.span().start, end: span.end },
1901                name,
1902                colon_span,
1903                value: list,
1904            }),
1905            None => LessMixinArgument::Value(list),
1906        });
1907    }
1908    Ok(())
1909}
1910
1911fn can_be_division_operand(left: &ComponentValue) -> bool {
1912    matches!(
1913        left,
1914        ComponentValue::LessVariable(..)
1915            | ComponentValue::LessPropertyVariable(..)
1916            | ComponentValue::LessBinaryOperation(..)
1917            | ComponentValue::LessParenthesizedOperation(..)
1918    )
1919}
1920
1921/// Whether the byte at `pos` in `source` is ASCII whitespace. Used to tell a
1922/// `+`/`-` binary operator (followed by whitespace) from a value sign.
1923fn is_followed_by_whitespace(source: &str, pos: usize) -> bool {
1924    source.as_bytes().get(pos).is_some_and(u8::is_ascii_whitespace)
1925}