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