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