Skip to main content

oxc_css_parser/parser/
value.rs

1use super::{Parser, state::QualifiedRuleContext};
2use crate::{
3    Parse, Syntax,
4    ast::*,
5    error::{Error, ErrorKind, PResult},
6    pos::Span,
7    tokenizer::{Token, TokenWithSpan},
8    util,
9};
10
11const PRECEDENCE_MULTIPLY: u8 = 2;
12const PRECEDENCE_PLUS: u8 = 1;
13
14/// Strip one leading `-vendor-` prefix (`-moz-calc` -> `calc`); returns the
15/// name unchanged when there is none.
16fn unvendored(name: &str) -> &str {
17    name.strip_prefix('-').and_then(|rest| rest.split_once('-')).map_or(name, |(_, base)| base)
18}
19
20/// dart-sass "special functions" whose contents may be raw text rather than
21/// values, but which are worth a typed parse first (so plain `element(#id)`
22/// or `-webkit-calc(1px + 2px)` keep their structured AST): `element(...)`
23/// and `type(...)`, plus `calc(...)`/`url(...)` under an unrecognized vendor
24/// prefix. (`expression(...)` and `progid:...(...)` are always raw, and an
25/// unvendored `calc`/`url` is parsed as a real calculation/URL.)
26fn is_special_typed_or_raw_function(name: &str) -> bool {
27    let base = unvendored(name);
28    let vendored = base.len() != name.len();
29    base.eq_ignore_ascii_case("element")
30        || (!vendored && (base.eq_ignore_ascii_case("type") || base.eq_ignore_ascii_case("if")))
31        || (vendored && (base.eq_ignore_ascii_case("calc") || base.eq_ignore_ascii_case("url")))
32}
33
34impl<'a> Parser<'a> {
35    pub(in crate::parser) fn parse_calc_expr(
36        &mut self,
37        allow_modulo: bool,
38    ) -> PResult<ComponentValue<'a>> {
39        self.parse_calc_expr_recursively(0, allow_modulo)
40    }
41
42    // https://drafts.csswg.org/css-values-4/#calc-syntax
43    //
44    // <calc-sum>     = <calc-product> [ [ '+' | '-' ] <calc-product> ]*
45    // <calc-product> = <calc-value>   [ [ '*' | '/' ] <calc-value> ]*
46    // <calc-value>   = <number> | <dimension> | <percentage> | ( <calc-sum> )
47    // Precedence-climbing over the two operator tiers (Sass `%` modulo shares the
48    // '*' tier when `allow_modulo`).
49    fn parse_calc_expr_recursively(
50        &mut self,
51        precedence: u8,
52        allow_modulo: bool,
53    ) -> PResult<ComponentValue<'a>> {
54        let mut left = if precedence >= PRECEDENCE_MULTIPLY {
55            if self.cursor.eat_l_paren()?.is_some() {
56                let expr = self.parse_calc_expr(allow_modulo)?;
57                self.cursor.expect_r_paren()?;
58                expr
59            } else if matches!(self.syntax, Syntax::Scss | Syntax::Sass)
60                && matches!(&self.cursor.peek()?.token, Token::Minus(..) | Token::Plus(..))
61                && {
62                    let span = &self.cursor.peek()?.span;
63                    self.source.as_bytes().get(span.end) == Some(&b'(')
64                }
65            {
66                // SassScript allows a unary sign glued to a parenthesized
67                // operand inside a calculation (`round(-(1) + 2)`); a spaced
68                // `calc(+ 1px)` stays invalid, as in dart-sass.
69                let op = match &self.cursor.peek()?.token {
70                    Token::Minus(..) => SassUnaryOperator {
71                        kind: SassUnaryOperatorKind::Minus,
72                        span: self.cursor.bump()?.span,
73                    },
74                    _ => SassUnaryOperator {
75                        kind: SassUnaryOperatorKind::Plus,
76                        span: self.cursor.bump()?.span,
77                    },
78                };
79                let expr = self.parse_calc_expr_recursively(PRECEDENCE_MULTIPLY, allow_modulo)?;
80                let span = Span { start: op.span.start, end: expr.span().end };
81                ComponentValue::SassUnaryExpression(SassUnaryExpression {
82                    expr: self.alloc(expr),
83                    op,
84                    span,
85                })
86            } else if self.syntax == Syntax::Less {
87                if matches!(self.cursor.peek()?.token, Token::Minus(..)) {
88                    ComponentValue::LessNegativeValue(self.parse()?)
89                } else {
90                    self.parse_component_value_atom()?
91                }
92            } else {
93                self.parse_component_value_atom()?
94            }
95        } else {
96            self.parse_calc_expr_recursively(precedence + 1, allow_modulo)?
97        };
98
99        // SassScript: a sign the lexer folded into a number/dimension token,
100        // glued to a function-call operand, is a binary operator (`max(map-get($m, a)-1, 0)` is `... - 1`).
101        // The same as the `parse_sass_bin_expr` value path.
102        // dart-sass rejects the glued form inside calculations outright,
103        // so structuring the operation keeps the intended subtraction instead of splitting off a signed value.
104        // Function-call lefts ONLY, deliberately:
105        // - word-glued runs (`100%-20px`, `10px+1px`) stay separate values and print verbatim as one postcss word
106        // - a parenthesized left can never glue-match anyway (its span ends before the `)`, see the `eat_l_paren` arm above)
107        // This can only fire right after `left` is parsed (a formed `Calc` is never a `Function`),
108        // so it lives before the operator loop.
109        if precedence == PRECEDENCE_PLUS
110            && matches!(self.syntax, Syntax::Scss | Syntax::Sass)
111            && matches!(left, ComponentValue::Function(..))
112        {
113            let left_end = left.span().end;
114            let split_op = |raw: &str, start: usize| CalcOperator {
115                kind: if raw.starts_with('+') {
116                    CalcOperatorKind::Plus
117                } else {
118                    CalcOperatorKind::Minus
119                },
120                span: Span { start, end: start + 1 },
121            };
122            match self.cursor.peek()? {
123                token @ TokenWithSpan { token: Token::Number(..), span }
124                    if token
125                        .number_raw(self.source)
126                        .is_some_and(|raw| raw.starts_with('+') || raw.starts_with('-'))
127                        && span.start == left_end =>
128                {
129                    let (number, number_span) = self.cursor.expect_number()?;
130                    let op = split_op(number.raw, number_span.start);
131                    let right = {
132                        let span = Span { start: number_span.start + 1, end: number_span.end };
133                        let raw = unsafe { number.raw.get_unchecked(1..number.raw.len()) };
134                        Number::try_from((crate::token::Number { raw }, span))
135                            .map(ComponentValue::Number)?
136                    };
137                    let right = self.parse_calc_mul_tail(right, allow_modulo)?;
138                    let span = Span { start: left.span().start, end: right.span().end };
139                    left = ComponentValue::Calc(Calc {
140                        left: self.alloc(left),
141                        op,
142                        right: self.alloc(right),
143                        span,
144                    });
145                }
146                token @ TokenWithSpan { token: Token::Dimension(..), span }
147                    if token
148                        .dimension_value_raw(self.source)
149                        .is_some_and(|raw| raw.starts_with('+') || raw.starts_with('-'))
150                        && span.start == left_end =>
151                {
152                    let (dimension, dimension_span) = self.cursor.expect_dimension()?;
153                    let op = split_op(dimension.value.raw, dimension_span.start);
154                    let right = self
155                        .dimension(
156                            crate::token::Dimension {
157                                value: crate::token::Number {
158                                    raw: unsafe {
159                                        dimension
160                                            .value
161                                            .raw
162                                            .get_unchecked(1..dimension.value.raw.len())
163                                    },
164                                },
165                                unit: dimension.unit,
166                            },
167                            Span { start: dimension_span.start + 1, end: dimension_span.end },
168                        )
169                        .map(ComponentValue::Dimension)?;
170                    let right = self.parse_calc_mul_tail(right, allow_modulo)?;
171                    let span = Span { start: left.span().start, end: right.span().end };
172                    left = ComponentValue::Calc(Calc {
173                        left: self.alloc(left),
174                        op,
175                        right: self.alloc(right),
176                        span,
177                    });
178                }
179                _ => {}
180            }
181        }
182
183        loop {
184            let operator = match &self.cursor.peek()?.token {
185                Token::Asterisk(..) if precedence == PRECEDENCE_MULTIPLY => CalcOperator {
186                    kind: CalcOperatorKind::Multiply,
187                    span: self.cursor.bump()?.span,
188                },
189                Token::Solidus(..) if precedence == PRECEDENCE_MULTIPLY => CalcOperator {
190                    kind: CalcOperatorKind::Division,
191                    span: self.cursor.bump()?.span,
192                },
193                // Sass modulo (`%`) shares multiplicative precedence, but only the
194                // legacy SassScript `min`/`max` accept it (`allow_modulo`); true
195                // calculations (`calc`, `clamp`, `sin`, ...) reject it, as does CSS.
196                Token::Percent(..) if precedence == PRECEDENCE_MULTIPLY && allow_modulo => {
197                    CalcOperator { kind: CalcOperatorKind::Modulo, span: self.cursor.bump()?.span }
198                }
199                Token::Plus(..) if precedence == PRECEDENCE_PLUS => {
200                    CalcOperator { kind: CalcOperatorKind::Plus, span: self.cursor.bump()?.span }
201                }
202                Token::Minus(..) if precedence == PRECEDENCE_PLUS => {
203                    CalcOperator { kind: CalcOperatorKind::Minus, span: self.cursor.bump()?.span }
204                }
205                _ => break,
206            };
207
208            let right = self.parse_calc_expr_recursively(precedence + 1, allow_modulo)?;
209            let span = Span { start: left.span().start, end: right.span().end };
210            left = ComponentValue::Calc(Calc {
211                left: self.alloc(left),
212                op: operator,
213                right: self.alloc(right),
214                span,
215            });
216        }
217
218        Ok(left)
219    }
220
221    // Multiplicative continuation for an already-parsed left operand:
222    // `*`, `/` (and `%` for the legacy `min`/`max`) bind tighter than a split-off sign,
223    // so `max(x()-1px*2)` is `x() - (1px * 2)`.
224    fn parse_calc_mul_tail(
225        &mut self,
226        mut left: ComponentValue<'a>,
227        allow_modulo: bool,
228    ) -> PResult<ComponentValue<'a>> {
229        loop {
230            let kind = match &self.cursor.peek()?.token {
231                Token::Asterisk(..) => CalcOperatorKind::Multiply,
232                Token::Solidus(..) => CalcOperatorKind::Division,
233                Token::Percent(..) if allow_modulo => CalcOperatorKind::Modulo,
234                _ => return Ok(left),
235            };
236            let op = CalcOperator { kind, span: self.cursor.bump()?.span };
237            let right = self.parse_calc_expr_recursively(PRECEDENCE_MULTIPLY + 1, allow_modulo)?;
238            let span = Span { start: left.span().start, end: right.span().end };
239            left = ComponentValue::Calc(Calc {
240                left: self.alloc(left),
241                op,
242                right: self.alloc(right),
243                span,
244            });
245        }
246    }
247
248    // A single CSS `<component-value>`: a function, a `[]`/`()` block, or a
249    // preserved token (ident, number, dimension, percentage, string, hash, url, …).
250    // https://drafts.csswg.org/css-syntax-3/#component-value
251    pub(super) fn parse_component_value_atom(&mut self) -> PResult<ComponentValue<'a>> {
252        let token_with_span = self.cursor.peek()?;
253        match &token_with_span.token {
254            Token::Ident(..) => {
255                let ident = token_with_span.ident(self.source).unwrap();
256                if unvendored(&ident.name()).eq_ignore_ascii_case("url") {
257                    match self.try_parse(Url::parse) {
258                        Ok(url) => return Ok(ComponentValue::Url(self.alloc(url))),
259                        Err(Error { kind: ErrorKind::TryParseError, .. }) => {}
260                        Err(error) => {
261                            // Not a `<url-token>` (quotes, parens, or raw
262                            // whitespace inside). Reference compilers accept
263                            // these as a function call with raw-ish contents
264                            // (`url(fn("s"))`, multi-line data: URIs), so fall
265                            // back to a function parse, keeping the original
266                            // error if even that shape doesn't fit.
267                            let (function_name, function_name_span) = self.cursor.expect_ident()?;
268                            let function_name = self.ident(function_name, function_name_span);
269                            return self
270                                .parse_function_typed_or_raw(function_name)
271                                .map(ComponentValue::Function)
272                                .map_err(|_| error);
273                        }
274                    }
275                }
276                let ident = self.parse::<InterpolableIdent>()?;
277                let ident_end = ident.span().end;
278                match self.cursor.peek()? {
279                    TokenWithSpan { token: Token::LParen(..), span } if span.start == ident_end => {
280                        return match ident {
281                            InterpolableIdent::Literal(ident)
282                                if ident.name.eq_ignore_ascii_case("src") =>
283                            {
284                                self.parse_src_url(ident)
285                                    .map(|url| ComponentValue::Url(self.alloc(url)))
286                            }
287                            InterpolableIdent::Literal(ident)
288                                if unvendored(ident.name).eq_ignore_ascii_case("expression") =>
289                            {
290                                // IE `expression(...)` (any vendor prefix):
291                                // contents are script, not CSS values.
292                                self.parse_raw_function(InterpolableIdent::Literal(ident))
293                                    .map(ComponentValue::Function)
294                            }
295                            InterpolableIdent::Literal(ident)
296                                if is_special_typed_or_raw_function(ident.name) =>
297                            {
298                                self.parse_function_typed_or_raw(ident)
299                                    .map(ComponentValue::Function)
300                            }
301                            ident => self.parse_function(ident).map(ComponentValue::Function),
302                        };
303                    }
304                    // IE filter syntax `-c-progid:d.e(...)` — everything to
305                    // the matching `)` is raw. (An unprefixed `progid:` at the
306                    // start of a value takes the whole-value raw path in
307                    // `Declaration::parse` instead.)
308                    TokenWithSpan { token: Token::Colon(..), span }
309                        if span.start == ident_end
310                            && matches!(
311                                &ident,
312                                InterpolableIdent::Literal(id)
313                                    if unvendored(id.name).eq_ignore_ascii_case("progid")
314                            ) =>
315                    {
316                        if let InterpolableIdent::Literal(ident) = ident {
317                            return self.parse_progid_function(ident).map(ComponentValue::Function);
318                        }
319                        unreachable!("guard matched a literal ident");
320                    }
321                    TokenWithSpan { token: Token::Dot(..), span }
322                        if matches!(self.syntax, Syntax::Scss | Syntax::Sass)
323                            && span.start == ident_end =>
324                    {
325                        if let InterpolableIdent::Literal(module) = &ident {
326                            let module =
327                                Ident { name: module.name, raw: module.raw, span: module.span };
328                            // A namespaced member is `foo.$var` or a glued
329                            // call `foo.bar(...)`.
330                            let qualified = self.try_parse(|parser| {
331                                let name = parser.parse_sass_qualified_name(module)?;
332                                if let SassQualifiedName {
333                                    member: SassModuleMemberName::Ident(..),
334                                    ..
335                                } = name
336                                {
337                                    let (_, lparen_span) = parser.cursor.expect_l_paren()?;
338                                    util::assert_no_ws_or_comment(&name.span, &lparen_span)?;
339                                    let args = parser.parse_function_args()?;
340                                    let (_, Span { end, .. }) = parser.cursor.expect_r_paren()?;
341                                    let span = Span { start: name.span.start, end };
342                                    Ok(ComponentValue::Function(Function {
343                                        name: FunctionName::SassQualifiedName(parser.alloc(name)),
344                                        args,
345                                        span,
346                                    }))
347                                } else {
348                                    Ok(ComponentValue::SassQualifiedName(parser.alloc(name)))
349                                }
350                            });
351                            return match qualified {
352                                Ok(value) => Ok(value),
353                                // `foo.bar` with no call: dart-sass rejects a
354                                // plain ident member at compile time, but
355                                // postcss-scss lexes the dotted run as ONE
356                                // word (xstyled / tailwind-theme tokens).
357                                // Keep the plain ident; the `.ident` tail
358                                // parses as raw tokens (the Css-mode shape)
359                                // via the `Token::Dot` atom arm.
360                                Err(_) => Ok(ComponentValue::InterpolableIdent(ident)),
361                            };
362                        }
363                    }
364                    _ => {}
365                }
366                match ident {
367                    InterpolableIdent::Literal(ident) if ident.raw.eq_ignore_ascii_case("u") => {
368                        match self.cursor.peek()? {
369                            TokenWithSpan { token: Token::Plus(..), span }
370                                if span.start == ident_end =>
371                            {
372                                self.parse_unicode_range(ident).map(ComponentValue::UnicodeRange)
373                            }
374                            token @ TokenWithSpan { token: Token::Number(..), span }
375                                if token
376                                    .number_raw(self.source)
377                                    .is_some_and(|raw| raw.starts_with('+'))
378                                    && span.start == ident_end =>
379                            {
380                                self.parse_unicode_range(ident).map(ComponentValue::UnicodeRange)
381                            }
382                            token @ TokenWithSpan { token: Token::Dimension(..), span }
383                                if token
384                                    .dimension_value_raw(self.source)
385                                    .is_some_and(|raw| raw.starts_with('+'))
386                                    && span.start == ident_end =>
387                            {
388                                self.parse_unicode_range(ident).map(ComponentValue::UnicodeRange)
389                            }
390                            _ => Ok(ComponentValue::InterpolableIdent(InterpolableIdent::Literal(
391                                ident,
392                            ))),
393                        }
394                    }
395                    _ => Ok(ComponentValue::InterpolableIdent(ident)),
396                }
397            }
398            Token::Solidus(..) | Token::Comma(..) => self.parse().map(ComponentValue::Delimiter),
399            Token::Number(..) => self.parse().map(ComponentValue::Number),
400            Token::Dimension(..) => self.parse().map(ComponentValue::Dimension),
401            Token::Percentage(..) => self.parse().map(ComponentValue::Percentage),
402            Token::Hash(..) => {
403                if self.syntax == Syntax::Less {
404                    self.parse_maybe_hex_color_or_less_mixin_call()
405                } else {
406                    self.parse().map(ComponentValue::HexColor)
407                }
408            }
409            Token::Str(..) => {
410                self.parse().map(InterpolableStr::Literal).map(ComponentValue::InterpolableStr)
411            }
412            Token::LBracket(..) => self.parse().map(ComponentValue::BracketBlock),
413            Token::DollarVar(..) if matches!(self.syntax, Syntax::Scss | Syntax::Sass) => {
414                self.parse().map(ComponentValue::SassVariable)
415            }
416            Token::DollarVar(..) if self.syntax == Syntax::Css => {
417                self.parse().map(ComponentValue::PostcssSimpleVar)
418            }
419            Token::LParen(..) if matches!(self.syntax, Syntax::Scss | Syntax::Sass) => {
420                match self.try_parse(SassParenthesizedExpression::parse) {
421                    Ok(expr) => Ok(ComponentValue::SassParenthesizedExpression(expr)),
422                    Err(err) => self.parse().map(ComponentValue::SassMap).map_err(|_| err),
423                }
424            }
425            Token::HashLBrace(..) if matches!(self.syntax, Syntax::Scss | Syntax::Sass) => {
426                let ident = self.parse_sass_interpolated_ident()?;
427                match self.cursor.peek()? {
428                    TokenWithSpan { token: Token::LParen(..), span }
429                        if span.start == ident.span().end =>
430                    {
431                        self.parse_function(ident).map(ComponentValue::Function)
432                    }
433                    _ => Ok(ComponentValue::InterpolableIdent(ident)),
434                }
435            }
436            Token::StrTemplate(..) if matches!(self.syntax, Syntax::Scss | Syntax::Sass) => self
437                .parse()
438                .map(InterpolableStr::SassInterpolated)
439                .map(ComponentValue::InterpolableStr),
440            Token::Ampersand(..) if matches!(self.syntax, Syntax::Scss | Syntax::Sass) => {
441                self.parse().map(ComponentValue::SassParentSelector)
442            }
443            Token::LBrace(..)
444                if self.syntax == Syntax::Scss
445                    && matches!(
446                        self.state.qualified_rule_ctx,
447                        Some(QualifiedRuleContext::DeclarationValue)
448                    ) =>
449            {
450                self.parse().map(ComponentValue::SassNestingDeclaration)
451            }
452            Token::Indent(..)
453                if self.syntax == Syntax::Sass
454                    && matches!(
455                        self.state.qualified_rule_ctx,
456                        Some(QualifiedRuleContext::DeclarationValue)
457                    ) =>
458            {
459                self.parse().map(ComponentValue::SassNestingDeclaration)
460            }
461            Token::AtKeyword(..) if self.syntax == Syntax::Less => {
462                self.parse_less_maybe_variable_or_with_lookups()
463            }
464            Token::Dot(..) if self.syntax == Syntax::Less => {
465                self.parse_less_maybe_mixin_call_or_with_lookups()
466            }
467            // Not Sass on its own — dart-sass has no bare `.` in values — but
468            // the ident atom declines the namespaced parse for postcss-word
469            // runs like `foo.bar.baz` (xstyled / tailwind-theme tokens),
470            // whose `.` then lands here. Accept it when glued to a following
471            // ident, keeping the Css-mode raw-token shape.
472            Token::Dot(..) if matches!(self.syntax, Syntax::Scss | Syntax::Sass) => {
473                let dot = self.cursor.bump()?;
474                match self.cursor.peek()? {
475                    TokenWithSpan { token: Token::Ident(..), span }
476                        if span.start == dot.span.end =>
477                    {
478                        Ok(ComponentValue::TokenWithSpan(dot))
479                    }
480                    _ => Err(Error { kind: ErrorKind::ExpectComponentValue, span: dot.span }),
481                }
482            }
483            Token::StrTemplate(..) if self.syntax == Syntax::Less => self
484                .parse()
485                .map(InterpolableStr::LessInterpolated)
486                .map(ComponentValue::InterpolableStr),
487            Token::At(..) if self.syntax == Syntax::Less => {
488                self.parse().map(ComponentValue::LessVariableVariable)
489            }
490            Token::DollarVar(..) if self.syntax == Syntax::Less => {
491                self.parse().map(ComponentValue::LessPropertyVariable)
492            }
493            Token::Tilde(..) if self.syntax == Syntax::Less => {
494                if let Ok(list_function_call) = self.try_parse(Function::parse) {
495                    Ok(ComponentValue::Function(list_function_call))
496                } else if let Ok(less_escaped_str) = self.try_parse(LessEscapedStr::parse) {
497                    Ok(ComponentValue::LessEscapedStr(less_escaped_str))
498                } else {
499                    self.parse().map(ComponentValue::LessJavaScriptSnippet)
500                }
501            }
502            Token::Percent(..) if self.syntax == Syntax::Less => self
503                .try_parse(Function::parse)
504                .map(ComponentValue::Function)
505                .or_else(|_| self.parse().map(ComponentValue::LessPercentKeyword)),
506            Token::BacktickCode(..) if self.syntax == Syntax::Less => {
507                self.parse().map(ComponentValue::LessJavaScriptSnippet)
508            }
509            Token::Placeholder(..) => {
510                let (placeholder, span) = self.cursor.expect_placeholder()?;
511                Ok(ComponentValue::Placeholder((placeholder, span).into()))
512            }
513            _ => Err(Error { kind: ErrorKind::ExpectComponentValue, span: token_with_span.span }),
514        }
515    }
516
517    // <dashed-ident> = <ident-token> whose name starts with '--'
518    pub(super) fn parse_dashed_ident(&mut self) -> PResult<InterpolableIdent<'a>> {
519        let ident = self.parse()?;
520        if let InterpolableIdent::Literal(ident) = &ident
521            && !ident.name.starts_with("--")
522        {
523            self.recoverable_errors
524                .push(Error { kind: ErrorKind::ExpectDashedIdent, span: ident.span });
525        }
526        Ok(ident)
527    }
528
529    // Build a `<function>` from an already-parsed name: '(' <function-args> ')'.
530    pub(super) fn parse_function(&mut self, name: InterpolableIdent<'a>) -> PResult<Function<'a>> {
531        self.cursor.expect_l_paren()?;
532        let args = if let Token::RParen(..) = &self.cursor.peek()?.token {
533            self.vec()
534        } else {
535            match &name {
536                InterpolableIdent::Literal(ident)
537                    if ident.name.eq_ignore_ascii_case("calc")
538                        || ident.name.eq_ignore_ascii_case("-webkit-calc")
539                        || ident.name.eq_ignore_ascii_case("-moz-calc")
540                        || ident.name.eq_ignore_ascii_case("min")
541                        || ident.name.eq_ignore_ascii_case("max")
542                        || ident.name.eq_ignore_ascii_case("clamp")
543                        || ident.name.eq_ignore_ascii_case("sin")
544                        || ident.name.eq_ignore_ascii_case("cos")
545                        || ident.name.eq_ignore_ascii_case("tan")
546                        || ident.name.eq_ignore_ascii_case("asin")
547                        || ident.name.eq_ignore_ascii_case("acos")
548                        || ident.name.eq_ignore_ascii_case("atan")
549                        || ident.name.eq_ignore_ascii_case("sqrt")
550                        || ident.name.eq_ignore_ascii_case("exp")
551                        || ident.name.eq_ignore_ascii_case("abs")
552                        || ident.name.eq_ignore_ascii_case("sign")
553                        || ident.name.eq_ignore_ascii_case("hypot")
554                        || ident.name.eq_ignore_ascii_case("round")
555                        || ident.name.eq_ignore_ascii_case("mod")
556                        || ident.name.eq_ignore_ascii_case("rem")
557                        || ident.name.eq_ignore_ascii_case("atan2")
558                        || ident.name.eq_ignore_ascii_case("pow")
559                        || ident.name.eq_ignore_ascii_case("log") =>
560                {
561                    // Only the legacy SassScript `min`/`max` accept the Sass `%` modulo
562                    // operator; true calculations (`calc`, `clamp`, `sin`, ...) reject it.
563                    let allow_modulo = matches!(self.syntax, Syntax::Scss | Syntax::Sass)
564                        && (ident.name.eq_ignore_ascii_case("min")
565                            || ident.name.eq_ignore_ascii_case("max"));
566                    // The calc grammar doesn't cover SassScript uses of these
567                    // names (`abs(\$number: -3)`, `max(1 2 3...)`,
568                    // `round(-(1) + 2)`); Scss/Sass fall back to a strict
569                    // SassScript call — but only there, so invalid calc stays
570                    // invalid. Other syntaxes have no fallback, so they skip
571                    // the rollback snapshot entirely.
572                    if !matches!(self.syntax, Syntax::Scss | Syntax::Sass) {
573                        self.parse_calc_args(allow_modulo)?
574                    } else {
575                        let typed = self.try_parse(|p| {
576                            let values = p.parse_calc_args(allow_modulo)?;
577                            let TokenWithSpan { token, span } = p.cursor.peek()?;
578                            if matches!(token, Token::RParen(..)) {
579                                Ok(values)
580                            } else {
581                                // A concrete kind (not the internal
582                                // `TryParseError` marker): the no-keyword-arg
583                                // path below surfaces this error.
584                                Err(Error {
585                                    kind: ErrorKind::Unexpected(")", token.symbol()),
586                                    span: *span,
587                                })
588                            }
589                        });
590                        match typed {
591                            Ok(values) => values,
592                            Err(error) => {
593                                let (args, comma_spans) = self.parse_sass_invocation_args()?;
594                                // Only a keyword argument justifies the fallback
595                                // (`abs(\$number: -3)` is a SassScript call); plain
596                                // expressions the calc grammar rejected
597                                // (`calc(1px % 2px)`, double spreads) stay invalid.
598                                if !args.iter().any(|arg| {
599                                    matches!(arg, ComponentValue::SassKeywordArgument(..))
600                                }) {
601                                    return Err(error);
602                                }
603                                let mut values = self.vec_with_capacity(args.len() * 2);
604                                let mut comma_spans = comma_spans.into_iter();
605                                for (i, arg) in args.into_iter().enumerate() {
606                                    if i > 0
607                                        && let Some(span) = comma_spans.next()
608                                    {
609                                        values.push(ComponentValue::Delimiter(Delimiter {
610                                            kind: DelimiterKind::Comma,
611                                            span,
612                                        }));
613                                    }
614                                    values.push(arg);
615                                }
616                                values
617                            }
618                        }
619                    }
620                }
621                InterpolableIdent::Literal(ident) if ident.name.eq_ignore_ascii_case("element") => {
622                    let id_selector = self.parse().map(ComponentValue::IdSelector)?;
623                    self.vec1(id_selector)
624                }
625                InterpolableIdent::Literal(Ident { raw: "boolean" | "if", .. })
626                    if self.syntax == Syntax::Less =>
627                {
628                    let less_condition = self.parse_less_condition(false)?;
629                    let condition = ComponentValue::LessCondition(self.alloc(less_condition));
630                    let mut args = self.parse_function_args()?;
631                    args.insert(0, condition);
632                    args
633                }
634                _ => self.parse_function_args()?,
635            }
636        };
637        let end = self.cursor.expect_r_paren()?.1.end;
638        let span = Span { start: name.span().start, end };
639        Ok(Function { name: FunctionName::Ident(name), args, span })
640    }
641
642    /// The `calc()`-family argument list: comma-delimited calc expressions,
643    /// with the SassScript spread (`max(1 2 3...)`) wrapping the preceding
644    /// value. Stops before the closing `)`.
645    fn parse_calc_args(
646        &mut self,
647        allow_modulo: bool,
648    ) -> PResult<oxc_allocator::Vec<'a, ComponentValue<'a>>> {
649        let mut values = self.vec_with_capacity(1);
650        loop {
651            match self.cursor.peek()? {
652                TokenWithSpan { token: Token::RParen(..), .. } => break,
653                TokenWithSpan { token: Token::Comma(..), .. } => {
654                    values.push(ComponentValue::Delimiter(self.parse()?));
655                }
656                // a spread is SassScript, so only the legacy `min`/`max`
657                // accept it (`clamp(1px 2px 3px...)` errors), and only once
658                TokenWithSpan { token: Token::DotDotDot(..), .. }
659                    if allow_modulo
660                        && matches!(self.syntax, Syntax::Scss | Syntax::Sass)
661                        && !values.is_empty()
662                        && !values
663                            .iter()
664                            .any(|v| matches!(v, ComponentValue::SassArbitraryArgument(..))) =>
665                {
666                    let TokenWithSpan { span: Span { end, .. }, .. } = self.cursor.bump()?;
667                    let value = values.pop().unwrap();
668                    let span = Span { start: value.span().start, end };
669                    values.push(ComponentValue::SassArbitraryArgument(SassArbitraryArgument {
670                        value: self.alloc(value),
671                        span,
672                    }));
673                }
674                _ => values.push(self.parse_calc_expr(allow_modulo)?),
675            }
676        }
677        Ok(values)
678    }
679
680    /// Parse a function with the typed grammar; when its arguments don't fit
681    /// (dart-sass special functions carry raw text: `element(/**/ c)`,
682    /// `-c-calc(@#$)`, `url(fn("s"))`), re-parse the contents as raw tokens.
683    pub(super) fn parse_function_typed_or_raw(&mut self, name: Ident<'a>) -> PResult<Function<'a>> {
684        let name_copy = Ident { name: name.name, raw: name.raw, span: name.span };
685        match self.try_parse(|p| p.parse_function(InterpolableIdent::Literal(name))) {
686            Ok(function) => Ok(function),
687            Err(_) => self.parse_raw_function(InterpolableIdent::Literal(name_copy)),
688        }
689    }
690
691    /// Parse `name(<raw contents>)` where the contents are preserved tokens
692    /// balanced to the matching `)` (IE `expression(...)`, unknown
693    /// `@supports` functions, and friends).
694    pub(in crate::parser) fn parse_raw_function(
695        &mut self,
696        name: InterpolableIdent<'a>,
697    ) -> PResult<Function<'a>> {
698        self.cursor.expect_l_paren()?;
699        let mut args = self.vec_with_capacity(4);
700        self.parse_raw_function_args_into(&mut args)?;
701        let end = self.cursor.expect_r_paren()?.1.end;
702        let span = Span { start: name.span().start, end };
703        Ok(Function { name: FunctionName::Ident(name), args, span })
704    }
705
706    /// IE filter syntax `progid:DXImageTransform.Microsoft.f(...)`, optionally
707    /// vendor prefixed: the `:dotted.path` prefix and the parenthesized
708    /// contents are all preserved tokens.
709    fn parse_progid_function(&mut self, name: Ident<'a>) -> PResult<Function<'a>> {
710        let mut args = self.vec_with_capacity(4);
711        // The path may hold Scss interpolation (`Microsoft.#{$f}(...)`): keep
712        // its `#{...}` balanced so the function's own `(` is the first
713        // top-level one and the interpolation's `}` never closes the block.
714        let mut pairs: Vec<util::PairedToken> = Vec::new();
715        loop {
716            match &self.cursor.peek()?.token {
717                Token::LParen(..) if pairs.is_empty() => break,
718                Token::Semicolon(..)
719                | Token::RBrace(..)
720                | Token::RParen(..)
721                | Token::Indent(..)
722                | Token::Dedent(..)
723                | Token::Linebreak(..)
724                    if pairs.is_empty() =>
725                {
726                    break;
727                }
728                Token::Eof(..) => break,
729                token => {
730                    if !util::track_paired_token(token, &mut pairs) {
731                        break;
732                    }
733                    args.push(ComponentValue::TokenWithSpan(self.cursor.bump()?));
734                }
735            }
736        }
737        self.cursor.expect_l_paren()?;
738        self.parse_raw_function_args_into(&mut args)?;
739        let end = self.cursor.expect_r_paren()?.1.end;
740        let span = Span { start: name.span.start, end };
741        Ok(Function { name: FunctionName::Ident(InterpolableIdent::Literal(name)), args, span })
742    }
743
744    /// Consume function contents as preserved tokens, balancing pairs, until
745    /// the function's own `)`. Semicolons and stray delimiters are legal here
746    /// (`expression(a;b)`, `url(data:...;base64,...)`).
747    fn parse_raw_function_args_into(
748        &mut self,
749        values: &mut oxc_allocator::Vec<'a, ComponentValue<'a>>,
750    ) -> PResult<()> {
751        let mut pairs: Vec<util::PairedToken> = Vec::with_capacity(1);
752        loop {
753            match &self.cursor.peek()?.token {
754                Token::Eof(..) => break,
755                // Interpolated strings must be parsed structurally so the
756                // tokenizer resumes the string after each `#{...}`.
757                Token::StrTemplate(..) => {
758                    values.push(ComponentValue::InterpolableStr(self.parse()?));
759                    continue;
760                }
761                token => {
762                    if !util::track_paired_token(token, &mut pairs) {
763                        break;
764                    }
765                }
766            }
767            values.push(ComponentValue::TokenWithSpan(self.cursor.bump()?));
768        }
769        Ok(())
770    }
771
772    // A function's argument list: a run of `<component-value>` up to the closing
773    // `)` (commas/`/` are preserved Delimiters).
774    pub(super) fn parse_function_args(
775        &mut self,
776    ) -> PResult<oxc_allocator::Vec<'a, ComponentValue<'a>>> {
777        let mut values = self.vec_with_capacity(4);
778        loop {
779            match &self.cursor.peek()?.token {
780                Token::RParen(..) | Token::Eof(..) => break,
781                Token::Semicolon(..) => {
782                    values.push(self.parse().map(ComponentValue::Delimiter)?);
783                }
784                Token::Exclamation(..) if matches!(self.syntax, Syntax::Scss | Syntax::Sass) => {
785                    // while this syntax is weird, Bootstrap is actually using it
786                    values.push(self.parse().map(ComponentValue::ImportantAnnotation)?);
787                }
788                Token::LBrace(..) if self.syntax == Syntax::Less => {
789                    values.push(self.parse().map(ComponentValue::LessDetachedRuleset)?);
790                }
791                Token::Dot(..) | Token::NumberSign(..) if self.syntax == Syntax::Less => {
792                    if let Ok(mixin) = self.try_parse(Parser::parse_less_anonymous_mixin) {
793                        values.push(ComponentValue::LessAnonymousMixin(mixin));
794                    } else if let Ok(value) = self.try_parse(ComponentValue::parse) {
795                        values.push(value);
796                    } else {
797                        values.push(ComponentValue::TokenWithSpan(self.cursor.bump()?));
798                    }
799                }
800                Token::Indent(..) | Token::Dedent(..) | Token::Linebreak(..) => {
801                    self.cursor.bump()?;
802                }
803                // A stray delimiter is a plain token in CSS, but the
804                // preprocessor dialects give it real syntax and their
805                // reference compilers reject it in function arguments.
806                Token::Unknown(..) if self.syntax != Syntax::Css => {
807                    let span = self.cursor.peek()?.span;
808                    return Err(Error { kind: ErrorKind::UnknownToken, span });
809                }
810                _ => {
811                    let value = if let Ok(value) = self.try_parse(ComponentValue::parse) {
812                        value
813                    } else {
814                        values.push(ComponentValue::TokenWithSpan(self.cursor.bump()?));
815                        continue;
816                    };
817                    if matches!(self.syntax, Syntax::Scss | Syntax::Sass) {
818                        if let Some((_, mut span)) = self.cursor.eat_dot_dot_dot()? {
819                            span.start = value.span().start;
820                            values.push(ComponentValue::SassArbitraryArgument(
821                                SassArbitraryArgument { value: self.alloc(value), span },
822                            ));
823                        } else if let ComponentValue::SassVariable(sass_var) = value {
824                            if let Some((_, colon_span)) = self.cursor.eat_colon()? {
825                                let value = self.parse::<ComponentValue>()?;
826                                let span =
827                                    Span { start: sass_var.span.start, end: value.span().end };
828                                values.push(ComponentValue::SassKeywordArgument(
829                                    SassKeywordArgument {
830                                        name: sass_var,
831                                        colon_span,
832                                        value: self.alloc(value),
833                                        span,
834                                    },
835                                ));
836                            } else {
837                                values.push(ComponentValue::SassVariable(sass_var));
838                            }
839                        } else {
840                            values.push(value);
841                        }
842                    } else {
843                        values.push(value);
844                    }
845                }
846            }
847        }
848        Ok(values)
849    }
850
851    // <ratio> = <number [0,∞]> [ '/' <number [0,∞]> ]?
852    // https://drafts.csswg.org/css-values-4/#ratios
853    pub(super) fn parse_ratio(&mut self, numerator: Number<'a>) -> PResult<Ratio<'a>> {
854        let (_, solidus_span) = self.cursor.expect_solidus()?;
855        let denominator = self.parse::<Number>()?;
856        if denominator.value <= 0.0 {
857            self.recoverable_errors
858                .push(Error { kind: ErrorKind::InvalidRatioDenominator, span: denominator.span });
859        }
860
861        let span = Span { start: numerator.span.start, end: denominator.span.end };
862        Ok(Ratio { numerator, solidus_span, denominator, span })
863    }
864
865    // The `src()` value function: src( [ <string> ]? <url-modifier>* )
866    // https://drafts.csswg.org/css-values-4/#funcdef-src
867    /// Parse the trailing modifier list inside `url(...)`, shared by
868    /// [`parse_src_url`](Self::parse_src_url) and the `Url` `Parse` impl.
869    fn parse_url_modifiers(&mut self) -> PResult<oxc_allocator::Vec<'a, UrlModifier<'a>>> {
870        Ok(match &self.cursor.peek()?.token {
871            Token::Ident(..) | Token::HashLBrace(..) | Token::AtLBraceVar(..) => {
872                let mut modifiers = self.vec_with_capacity(1);
873                loop {
874                    modifiers.push(self.parse()?);
875                    if let Token::RParen(..) = &self.cursor.peek()?.token {
876                        break;
877                    }
878                }
879                modifiers
880            }
881            _ => self.vec(),
882        })
883    }
884
885    fn parse_src_url(&mut self, name: Ident<'a>) -> PResult<Url<'a>> {
886        // caller of `parse_src_url` should make sure there're no whitespaces before paren
887        self.cursor.expect_l_paren()?;
888        let value = match &self.cursor.peek()?.token {
889            Token::Str(..) | Token::StrTemplate(..) => {
890                Some(UrlValue::Str(self.parse::<InterpolableStr>()?))
891            }
892            _ => None,
893        };
894        let modifiers = self.parse_url_modifiers()?;
895        let end = self.cursor.expect_r_paren()?.1.end;
896        let span = Span { start: name.span.start, end };
897        Ok(Url { name, value, modifiers, span })
898    }
899
900    // <urange> = u '+' <ident-token> '?'* | u <dimension-token> '?'* | u <number-token> …
901    // Written `U+0-10FFFF`, `U+4??`, etc. https://drafts.csswg.org/css-syntax-3/#urange-syntax
902    fn parse_unicode_range(&mut self, prefix_ident: Ident<'a>) -> PResult<UnicodeRange<'a>> {
903        let prefix = prefix_ident.raw.chars().next().unwrap();
904        let (span_start, span_end) = match self.cursor.bump()? {
905            TokenWithSpan { token: Token::Plus(..), span: plus_token_span } => {
906                let start = plus_token_span.start;
907                let mut end = match self.cursor.tokenizer.bump_without_ws_or_comments()? {
908                    TokenWithSpan { token: Token::Ident(..) | Token::Question(..), span } => {
909                        span.end
910                    }
911                    TokenWithSpan { token, span } => {
912                        return Err(Error {
913                            kind: ErrorKind::Unexpected("?", token.symbol()),
914                            span,
915                        });
916                    }
917                };
918                loop {
919                    match self.cursor.peek()? {
920                        TokenWithSpan { token: Token::Question(..), span } if span.start == end => {
921                            end = self.cursor.bump()?.span.end;
922                        }
923                        _ => break,
924                    }
925                }
926                (start, end)
927            }
928            TokenWithSpan { token: Token::Dimension(..), span: dimension_token_span } => {
929                let start = dimension_token_span.start;
930                let mut end = dimension_token_span.end;
931                loop {
932                    match self.cursor.peek()? {
933                        TokenWithSpan { token: Token::Question(..), span } if span.start == end => {
934                            end = self.cursor.bump()?.span.end;
935                        }
936                        _ => break,
937                    }
938                }
939                (start, end)
940            }
941            TokenWithSpan { token: Token::Number(..), span: number_token_span } => {
942                let start = number_token_span.start;
943                let mut end = number_token_span.end;
944                match &self.cursor.peek()?.token {
945                    Token::Question(..) => {
946                        end = self.cursor.bump()?.span.end;
947                        loop {
948                            match self.cursor.peek()? {
949                                TokenWithSpan { token: Token::Question(..), span }
950                                    if span.start == end =>
951                                {
952                                    end = self.cursor.bump()?.span.end;
953                                }
954                                _ => break,
955                            }
956                        }
957                    }
958                    Token::Dimension(..) | Token::Number(..) => {
959                        end = self.cursor.bump()?.span.end;
960                    }
961                    _ => {}
962                }
963                (start, end)
964            }
965            TokenWithSpan { span, .. } => {
966                return Err(Error { kind: ErrorKind::InvalidUnicodeRange, span });
967            }
968        };
969
970        let source = self.source.get(span_start + 1..span_end).ok_or(Error {
971            kind: ErrorKind::InvalidUnicodeRange,
972            span: Span { start: span_start + 1, end: span_end },
973        })?;
974        let span = Span { start: prefix_ident.span.start, end: span_end };
975        let unicode_range = if let Some((left, right)) = source.split_once('-') {
976            if left.len() > 6 || !left.chars().all(|c| c.is_ascii_hexdigit()) {
977                return Err(Error { kind: ErrorKind::InvalidUnicodeRange, span });
978            }
979            if right.len() > 6
980                || !right.trim_end_matches('?').chars().all(|c| c.is_ascii_hexdigit())
981            {
982                return Err(Error { kind: ErrorKind::InvalidUnicodeRange, span });
983            }
984            let start = u32::from_str_radix(left, 16)
985                .map_err(|_| Error { kind: ErrorKind::InvalidUnicodeRange, span })?;
986            let end = u32::from_str_radix(&replace_unicode_range_wildcards(right, 'F'), 16)
987                .map_err(|_| Error { kind: ErrorKind::InvalidUnicodeRange, span })?;
988            UnicodeRange { prefix, start, start_raw: left, end, end_raw: Some(right), span }
989        } else {
990            if source.len() > 6
991                || !source.trim_end_matches('?').chars().all(|c| c.is_ascii_hexdigit())
992            {
993                return Err(Error { kind: ErrorKind::InvalidUnicodeRange, span });
994            }
995            let start = u32::from_str_radix(&replace_unicode_range_wildcards(source, '0'), 16)
996                .map_err(|_| Error { kind: ErrorKind::InvalidUnicodeRange, span })?;
997            let end = u32::from_str_radix(&replace_unicode_range_wildcards(source, 'F'), 16)
998                .map_err(|_| Error { kind: ErrorKind::InvalidUnicodeRange, span })?;
999            UnicodeRange { prefix, start, start_raw: source, end, end_raw: None, span }
1000        };
1001        // Value-level checks (end > U+10FFFF, start > end) are deliberately
1002        // NOT errors: reference compilers pass such ranges through and
1003        // browsers clamp/ignore them at used-value time (`U+??????`,
1004        // `U+123456`, `U+1A2B3C-10FFFF` all appear in real-world corpora).
1005        Ok(unicode_range)
1006    }
1007}
1008
1009fn replace_unicode_range_wildcards(source: &str, replacement: char) -> String {
1010    source.chars().map(|c| if c == '?' { replacement } else { c }).collect()
1011}
1012
1013// A `[]`-block of component values (a `<simple-block>` opened by `[`).
1014// https://drafts.csswg.org/css-syntax-3/#simple-block
1015impl<'a> Parse<'a> for BracketBlock<'a> {
1016    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1017        let start = input.cursor.expect_l_bracket()?.1.start;
1018        let mut value = input.vec_with_capacity(3);
1019        loop {
1020            match &input.cursor.peek()?.token {
1021                Token::RBracket(..) => break,
1022                _ => value.push(input.parse()?),
1023            }
1024        }
1025        let end = input.cursor.expect_r_bracket()?.1.end;
1026        Ok(BracketBlock { value, span: Span { start, end } })
1027    }
1028}
1029
1030// https://drafts.csswg.org/css-syntax-3/#component-value
1031//
1032// <component-value> = <preserved-token> | <function> | <simple-block>
1033// (Scss/Sass and Less parse a full operator expression at this position instead.)
1034impl<'a> Parse<'a> for ComponentValue<'a> {
1035    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1036        match input.syntax {
1037            Syntax::Css => input.parse_component_value_atom(),
1038            Syntax::Scss | Syntax::Sass => {
1039                input.parse_sass_bin_expr(/* allow_comparison */ true)
1040            }
1041            Syntax::Less => input.parse_less_operation(/* allow_mixin_call */ true),
1042        }
1043    }
1044}
1045
1046// A list of `<component-value>` (public entry point; a `;` is kept as a Delimiter).
1047impl<'a> Parse<'a> for ComponentValues<'a> {
1048    /// This is for public-use only. For internal code of oxc-css-parser, **DO NOT** use.
1049    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1050        let first = input.parse::<ComponentValue>()?;
1051        let mut span = *first.span();
1052
1053        let mut values = input.vec_with_capacity(4);
1054        values.push(first);
1055        loop {
1056            match &input.cursor.peek()?.token {
1057                Token::Eof(..) => break,
1058                Token::Semicolon(..) => {
1059                    values.push(input.parse().map(ComponentValue::Delimiter)?);
1060                }
1061                _ => values.push(input.parse()?),
1062            }
1063        }
1064
1065        if let Some(value) = values.last() {
1066            span.end = value.span().end;
1067        }
1068        Ok(ComponentValues { values, span })
1069    }
1070}
1071
1072// A preserved delimiter token: '/' | ',' | ';'
1073impl<'a> Parse<'a> for Delimiter {
1074    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1075        use crate::tokenizer::token::*;
1076        match input.cursor.bump()? {
1077            TokenWithSpan { token: Token::Solidus(..), span } => {
1078                Ok(Delimiter { kind: DelimiterKind::Solidus, span })
1079            }
1080            TokenWithSpan { token: Token::Comma(..), span } => {
1081                Ok(Delimiter { kind: DelimiterKind::Comma, span })
1082            }
1083            TokenWithSpan { token: Token::Semicolon(..), span } => {
1084                Ok(Delimiter { kind: DelimiterKind::Semicolon, span })
1085            }
1086            _ => unreachable!(),
1087        }
1088    }
1089}
1090
1091// <dimension> = <number> <unit>   (a <dimension-token>: length, angle, time, …)
1092impl<'a> Parse<'a> for Dimension<'a> {
1093    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1094        let (dimension, span) = input.cursor.expect_dimension()?;
1095        input.dimension(dimension, span)
1096    }
1097}
1098
1099// https://drafts.csswg.org/css-syntax-3/#function
1100//
1101// <function> = <function-token> <component-value>* ')'
1102impl<'a> Parse<'a> for Function<'a> {
1103    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1104        let name = input.parse::<FunctionName>()?;
1105        match input.cursor.peek()? {
1106            TokenWithSpan { token: Token::LParen(..), span } => {
1107                util::assert_no_ws_or_comment(name.span(), span)?;
1108                match name {
1109                    FunctionName::Ident(name) => input.parse_function(name),
1110                    name => {
1111                        input.cursor.bump()?;
1112                        let args = input.parse_function_args()?;
1113                        let (_, Span { end, .. }) = input.cursor.expect_r_paren()?;
1114                        let span = Span { start: name.span().start, end };
1115                        Ok(Function { name, args, span })
1116                    }
1117                }
1118            }
1119            TokenWithSpan { token, span } => {
1120                Err(Error { kind: ErrorKind::Unexpected("(", token.symbol()), span: *span })
1121            }
1122        }
1123    }
1124}
1125
1126// The name before a function's `(`: an <ident-token>. Sass also allows a
1127// module-qualified `module.member`; Less adds `%`/`~` function forms.
1128impl<'a> Parse<'a> for FunctionName<'a> {
1129    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1130        match input.cursor.peek()?.token {
1131            Token::Ident(..) => {
1132                let ident = input.parse::<Ident>()?;
1133                match (&input.cursor.peek()?.token, input.syntax) {
1134                    (Token::Dot(..), Syntax::Scss | Syntax::Sass) => {
1135                        input.cursor.bump()?;
1136                        let member = input.parse::<Ident>()?;
1137                        let span = Span { start: ident.span.start, end: member.span.end };
1138                        Ok(FunctionName::SassQualifiedName(input.alloc(SassQualifiedName {
1139                            module: ident,
1140                            member: SassModuleMemberName::Ident(member),
1141                            span,
1142                        })))
1143                    }
1144                    _ => Ok(FunctionName::Ident(InterpolableIdent::Literal(ident))),
1145                }
1146            }
1147            Token::Percent(..) if input.syntax == Syntax::Less => {
1148                input.parse().map(FunctionName::LessFormatFunction)
1149            }
1150            Token::Tilde(..) if input.syntax == Syntax::Less => {
1151                input.parse().map(FunctionName::LessListFunction)
1152            }
1153            _ => {
1154                let TokenWithSpan { token, span } = input.cursor.bump()?;
1155                Err(Error { kind: ErrorKind::Unexpected("<ident>", token.symbol()), span })
1156            }
1157        }
1158    }
1159}
1160
1161// <hex-color> = '#' [ 3 | 4 | 6 | 8 hex digits ]   (a <hash-token>)
1162impl<'a> Parse<'a> for HexColor<'a> {
1163    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1164        let (token, span) = input.cursor.expect_hash()?;
1165        let raw = token.raw;
1166        let value = if token.escaped { util::handle_escape_in(raw, input.allocator) } else { raw };
1167        Ok(HexColor { value, raw, span })
1168    }
1169}
1170
1171// <ident-token>
1172impl<'a> Parse<'a> for Ident<'a> {
1173    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1174        let (ident, span) = input.cursor.expect_ident()?;
1175        Ok(input.ident(ident, span))
1176    }
1177}
1178
1179// An <ident-token>, or a preprocessor-interpolated ident (Sass `#{}`, Less `@{}`)
1180// / css-in-js placeholder standing in for one.
1181impl<'a> Parse<'a> for InterpolableIdent<'a> {
1182    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1183        // A css-in-js placeholder stands in for an interpolated ident anywhere one
1184        // is expected (id selector `#${x}`, attribute value `[a=${x}]`, ...).
1185        if let Token::Placeholder(..) = input.cursor.peek()?.token {
1186            let (placeholder, span) = input.cursor.expect_placeholder()?;
1187            return Ok(InterpolableIdent::Placeholder((placeholder, span).into()));
1188        }
1189        match input.syntax {
1190            Syntax::Css => input.parse().map(InterpolableIdent::Literal),
1191            Syntax::Scss | Syntax::Sass => input.parse_sass_interpolated_ident(),
1192            Syntax::Less => {
1193                // Less variable interpolation is disallowed in declaration value
1194                if matches!(
1195                    input.state.qualified_rule_ctx,
1196                    Some(QualifiedRuleContext::DeclarationValue)
1197                ) {
1198                    input.parse().map(InterpolableIdent::Literal)
1199                } else {
1200                    input.parse_less_interpolated_ident()
1201                }
1202            }
1203        }
1204    }
1205}
1206
1207// A <string-token>, or a Sass/Less interpolated string template.
1208impl<'a> Parse<'a> for InterpolableStr<'a> {
1209    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1210        match input.cursor.peek()? {
1211            TokenWithSpan { token: Token::Str(..), .. } => {
1212                input.parse().map(InterpolableStr::Literal)
1213            }
1214            TokenWithSpan { token: Token::StrTemplate(..), span } => match input.syntax {
1215                Syntax::Scss | Syntax::Sass => input.parse().map(InterpolableStr::SassInterpolated),
1216                Syntax::Less => input.parse().map(InterpolableStr::LessInterpolated),
1217                Syntax::Css => Err(Error { kind: ErrorKind::UnexpectedTemplateInCss, span: *span }),
1218            },
1219            TokenWithSpan { span, .. } => Err(Error { kind: ErrorKind::ExpectString, span: *span }),
1220        }
1221    }
1222}
1223
1224// <number-token>
1225impl<'a> Parse<'a> for Number<'a> {
1226    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1227        let (number, span) = input.cursor.expect_number()?;
1228        number
1229            .raw
1230            .parse()
1231            .map_err(|_| Error { kind: ErrorKind::InvalidNumber, span })
1232            .map(|value| Self { value, raw: number.raw, span })
1233    }
1234}
1235
1236// <percentage> = <percentage-token>   (a <number> immediately followed by '%')
1237impl<'a> Parse<'a> for Percentage<'a> {
1238    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1239        let (token, span) = input.cursor.expect_percentage()?;
1240        Ok(Percentage {
1241            value: (token.value, Span { start: span.start, end: span.end - 1 }).try_into()?,
1242            span,
1243        })
1244    }
1245}
1246
1247// <string-token>
1248impl<'a> Parse<'a> for Str<'a> {
1249    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1250        let (str, span) = input.cursor.expect_str()?;
1251        Ok(input.str(str, span))
1252    }
1253}
1254
1255// https://drafts.csswg.org/css-values-4/#urls
1256//
1257// <url> = url( <string> <url-modifier>* ) | <url-token>
1258// (also accepts the Gecko `url-prefix(…)` / `domain(…)` @document matchers)
1259impl<'a> Parse<'a> for Url<'a> {
1260    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1261        let (prefix, prefix_span) = input.cursor.expect_ident()?;
1262        // `url-prefix(...)` and `domain(...)` (Gecko `@document` matchers)
1263        // take the same unquoted-URL contents as `url(...)` — token-level
1264        // scanning would mis-lex `//` in `https://` as a comment.
1265        let prefix_name = prefix.name();
1266        let base_name = unvendored(&prefix_name);
1267        if !base_name.eq_ignore_ascii_case("url")
1268            && !base_name.eq_ignore_ascii_case("url-prefix")
1269            && !base_name.eq_ignore_ascii_case("domain")
1270        {
1271            return Err(Error { kind: ErrorKind::ExpectUrl, span: prefix_span });
1272        }
1273        let prefix_start = prefix_span.start;
1274        let name = input.ident(prefix, prefix_span);
1275
1276        match input.cursor.peek()? {
1277            TokenWithSpan { token: Token::LParen(..), span } if prefix_span.end == span.start => {
1278                input.cursor.bump()?;
1279            }
1280            TokenWithSpan { span, .. } => {
1281                // The internal marker means "not a url shape at all": callers
1282                // discriminate on it (`parse_component_value_atom`) or
1283                // concretize it before surfacing (`ImportPrelude`).
1284                return Err(Error { kind: ErrorKind::TryParseError, span: *span });
1285            }
1286        }
1287
1288        if input.cursor.tokenizer.is_start_of_url_string() {
1289            let value = input.parse()?;
1290            let modifiers = input.parse_url_modifiers()?;
1291            let end = input.cursor.expect_r_paren()?.1.end;
1292            let span = Span { start: prefix_start, end };
1293            Ok(Url { name, value: Some(UrlValue::Str(value)), modifiers, span })
1294        } else if let Ok(value) = input.try_parse(UrlRaw::parse) {
1295            let span = Span {
1296                start: prefix_start,
1297                end: value.span.end + 1, // `)` is consumed, but span excludes it
1298            };
1299            Ok(Url { name, value: Some(UrlValue::Raw(value)), modifiers: input.vec(), span })
1300        } else {
1301            match input.syntax {
1302                Syntax::Css => {
1303                    Err(Error { kind: ErrorKind::InvalidUrl, span: input.cursor.bump()?.span })
1304                }
1305                Syntax::Scss | Syntax::Sass => {
1306                    let value = input.parse::<SassInterpolatedUrl>()?;
1307                    let span = Span {
1308                        start: prefix_start,
1309                        end: value.span.end + 1, // `)` is consumed, but span excludes it
1310                    };
1311                    Ok(Url {
1312                        name,
1313                        value: Some(UrlValue::SassInterpolated(value)),
1314                        modifiers: input.vec(),
1315                        span,
1316                    })
1317                }
1318                Syntax::Less => {
1319                    let value = UrlValue::LessEscapedStr(input.parse()?);
1320                    let (_, Span { end, .. }) = input.cursor.expect_r_paren()?;
1321                    let span = Span { start: prefix_start, end };
1322                    Ok(Url { name, value: Some(value), modifiers: input.vec(), span })
1323                }
1324            }
1325        }
1326    }
1327}
1328
1329// <url-modifier> = <ident> | <function>
1330impl<'a> Parse<'a> for UrlModifier<'a> {
1331    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1332        let ident = input.parse::<InterpolableIdent>()?;
1333        match input.cursor.peek()? {
1334            TokenWithSpan { token: Token::LParen(..), span } if ident.span().end == span.start => {
1335                input.parse_function(ident).map(UrlModifier::Function)
1336            }
1337            _ => Ok(UrlModifier::Ident(ident)),
1338        }
1339    }
1340}
1341
1342// The unquoted URL body of a <url-token> (raw text up to the closing `)`).
1343impl<'a> Parse<'a> for UrlRaw<'a> {
1344    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1345        let token = input.cursor.tokenizer.scan_url_raw_or_template()?;
1346        match token.url_raw(input.source) {
1347            Some(url) => {
1348                let span = token.span;
1349                let value = if url.escaped {
1350                    util::handle_escape_in(url.raw, input.allocator)
1351                } else {
1352                    url.raw
1353                };
1354                Ok(UrlRaw { value, raw: url.raw, span })
1355            }
1356            None => Err(Error {
1357                kind: ErrorKind::Unexpected("<url>", token.token.symbol()),
1358                span: token.span,
1359            }),
1360        }
1361    }
1362}