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