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