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