Skip to main content

oxc_css_parser/parser/
selector.rs

1use super::Parser;
2use crate::{
3    Parse, Syntax,
4    ast::*,
5    error::{Error, ErrorKind, PResult},
6    pos::Span,
7    tokenizer::{Token, TokenWithSpan, token},
8    util,
9};
10
11/// The `<ndashdigit-dimension>` / `<ndashdigit-ident>` tail:
12/// `<prefix><digits>` with the prefix matched ASCII case-insensitively
13/// (`2N-1` == `2n-1`; An+B is case-insensitive like all CSS keywords,
14/// matching the sibling `eq_ignore_ascii_case` branches).
15/// `None` = not this form;
16/// `Some` carries the negated `b`, with the digit errors pointed at `digits_span`.
17fn parse_ndashdigit_b(name: &str, prefix: &str, digits_span: Span) -> Option<PResult<i32>> {
18    let (head, digits) = name.split_at_checked(prefix.len())?;
19    if !head.eq_ignore_ascii_case(prefix) {
20        return None;
21    }
22    Some(if digits.bytes().any(|b| !b.is_ascii_digit()) {
23        Err(Error { kind: ErrorKind::ExpectUnsignedInteger, span: digits_span })
24    } else {
25        digits
26            .parse::<i32>()
27            .map(|b| -b)
28            .map_err(|_| Error { kind: ErrorKind::ExpectInteger, span: digits_span })
29    })
30}
31
32// https://www.w3.org/TR/css-syntax-3/#the-anb-type
33//
34// <an+b> = odd | even | <integer>
35//        | <n-dimension>        [ <signed-integer> | [ '+' | '-' ] <signless-integer> ]?
36//        | '+'? n               [ <signed-integer> | [ '+' | '-' ] <signless-integer> ]?
37//        | -n                   [ <signed-integer> | [ '+' | '-' ] <signless-integer> ]?
38//        | <ndashdigit-dimension> | '+'? <ndashdigit-ident> | <dashndashdigit-ident>
39impl<'a> Parse<'a> for AnPlusB {
40    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
41        match input.cursor.peek()? {
42            TokenWithSpan { token: Token::Dimension(..), .. } => {
43                let (token::Dimension { value, unit }, span) = input.cursor.expect_dimension()?;
44                let value_span = Span { start: span.start, end: span.start + value.raw.len() };
45                let unit_name = unit.name();
46                if unit_name.eq_ignore_ascii_case("n") {
47                    match &input.cursor.peek()?.token {
48                        // syntax: <n-dimension> ['+' | '-'] <signless-integer>
49                        // examples: '1n + 1', '1n - 1', '1n+ 1'
50                        sign @ Token::Plus(..) | sign @ Token::Minus(..) => {
51                            let sign = if let Token::Plus(..) = sign { 1 } else { -1 };
52                            input.cursor.bump()?;
53                            let (number, number_span) = expect_unsigned_int(input)?;
54                            let span = Span { start: span.start, end: number_span.end };
55                            Ok(AnPlusB {
56                                a: value
57                                    .try_into()
58                                    .map_err(|kind| Error { kind, span: value_span })?,
59                                b: sign
60                                    * i32::try_from(number)
61                                        .map_err(|kind| Error { kind, span: number_span })?,
62                                span,
63                            })
64                        }
65
66                        // syntax: <n-dimension> <signed-integer>
67                        // examples: '1n +1', '1n -1'
68                        Token::Number(..) => {
69                            let (number, number_span) = input.cursor.expect_number()?;
70                            let span = Span { start: span.start, end: number_span.end };
71                            Ok(AnPlusB {
72                                a: value
73                                    .try_into()
74                                    .map_err(|kind| Error { kind, span: value_span })?,
75                                b: number
76                                    .try_into()
77                                    .map_err(|kind| Error { kind, span: number_span })?,
78                                span,
79                            })
80                        }
81
82                        // syntax: <n-dimension>
83                        // examples: '1n'
84                        _ => Ok(AnPlusB {
85                            a: value.try_into().map_err(|kind| Error { kind, span: value_span })?,
86                            b: 0,
87                            span,
88                        }),
89                    }
90                } else if unit_name.eq_ignore_ascii_case("n-") {
91                    // syntax: <ndash-dimension> <signless-integer>
92                    // examples: '1n- 1'
93                    let (number, number_span) = expect_unsigned_int(input)?;
94                    let span = Span { start: span.start, end: number_span.end };
95                    Ok(AnPlusB {
96                        a: value.try_into().map_err(|kind| Error { kind, span: value_span })?,
97                        b: -i32::try_from(number)
98                            .map_err(|kind| Error { kind, span: number_span })?,
99                        span,
100                    })
101                } else if let Some(b) = parse_ndashdigit_b(
102                    &unit_name,
103                    "n-",
104                    Span { start: span.start + value.raw.len() + 2, end: span.end },
105                ) {
106                    // syntax: <ndashdigit-dimension>
107                    // examples: '1n-1'
108                    Ok(AnPlusB {
109                        a: value.try_into().map_err(|kind| Error { kind, span: value_span })?,
110                        b: b?,
111                        span,
112                    })
113                } else {
114                    Err(Error { kind: ErrorKind::InvalidAnPlusB, span })
115                }
116            }
117
118            TokenWithSpan { token: Token::Plus(..), .. } => {
119                let plus_span = input.cursor.bump()?.span;
120                let (ident, ident_span) =
121                    input.cursor.expect_ident_without_ws_or_comments(false)?;
122                let ident_name = ident.name();
123                if ident_name.eq_ignore_ascii_case("n") {
124                    match &input.cursor.peek()?.token {
125                        // syntax: +n ['+' | '-'] <signless-integer>
126                        // examples: '+n + 1', '+n - 1', '+n+ 1'
127                        sign @ Token::Plus(..) | sign @ Token::Minus(..) => {
128                            let sign = if let Token::Plus(..) = sign { 1 } else { -1 };
129                            input.cursor.bump()?;
130                            let (number, number_span) = expect_unsigned_int(input)?;
131                            let span = Span { start: plus_span.start, end: number_span.end };
132                            Ok(AnPlusB {
133                                a: 1,
134                                b: sign
135                                    * i32::try_from(number)
136                                        .map_err(|kind| Error { kind, span: number_span })?,
137                                span,
138                            })
139                        }
140
141                        // syntax: +n <signed-integer>
142                        // examples: '+n +1', '+n -1'
143                        Token::Number(..) => {
144                            let (number, number_span) = input.cursor.expect_number()?;
145                            let span = Span { start: plus_span.start, end: number_span.end };
146                            Ok(AnPlusB {
147                                a: 1,
148                                b: number
149                                    .try_into()
150                                    .map_err(|kind| Error { kind, span: number_span })?,
151                                span,
152                            })
153                        }
154
155                        // syntax: +n
156                        _ => Ok(AnPlusB {
157                            a: 1,
158                            b: 0,
159                            span: Span { start: plus_span.start, end: ident_span.end },
160                        }),
161                    }
162                } else if ident_name.eq_ignore_ascii_case("n-") {
163                    // syntax: +n- <signless-integer>
164                    // examples: '+n- 1'
165                    let (number, number_span) = expect_unsigned_int(input)?;
166                    let span = Span { start: plus_span.start, end: number_span.end };
167                    Ok(AnPlusB {
168                        a: 1,
169                        b: -i32::try_from(number)
170                            .map_err(|kind| Error { kind, span: number_span })?,
171                        span,
172                    })
173                } else if let Some(b) = parse_ndashdigit_b(
174                    &ident_name,
175                    "n-",
176                    Span { start: ident_span.start + 2, end: ident_span.end },
177                ) {
178                    // syntax: +<ndashdigit-ident>
179                    // examples: '+n-1'
180                    Ok(AnPlusB {
181                        a: 1,
182                        b: b?,
183                        span: Span { start: plus_span.start, end: ident_span.end },
184                    })
185                } else {
186                    Err(Error {
187                        kind: ErrorKind::InvalidAnPlusB,
188                        span: Span { start: plus_span.start, end: ident_span.end },
189                    })
190                }
191            }
192
193            TokenWithSpan { token: Token::Ident(..), .. } => {
194                let (ident, ident_span) = input.cursor.expect_ident()?;
195                let ident_name = ident.name();
196                if ident_name.eq_ignore_ascii_case("n") {
197                    match &input.cursor.peek()?.token {
198                        // syntax: n ['+' | '-'] <signless-integer>
199                        // examples: 'n + 1', 'n - 1', 'n+ 1'
200                        sign @ Token::Plus(..) | sign @ Token::Minus(..) => {
201                            let sign = if let Token::Plus(..) = sign { 1 } else { -1 };
202                            input.cursor.bump()?;
203                            let (number, number_span) = expect_unsigned_int(input)?;
204                            let span = Span { start: ident_span.start, end: number_span.end };
205                            Ok(AnPlusB {
206                                a: 1,
207                                b: sign
208                                    * i32::try_from(number)
209                                        .map_err(|kind| Error { kind, span: number_span })?,
210                                span,
211                            })
212                        }
213
214                        // syntax: n <signed-integer>
215                        // examples: 'n +1', 'n -1'
216                        Token::Number(..) => {
217                            let (number, number_span) = input.cursor.expect_number()?;
218                            let span = Span { start: ident_span.start, end: number_span.end };
219                            Ok(AnPlusB {
220                                a: 1,
221                                b: number
222                                    .try_into()
223                                    .map_err(|kind| Error { kind, span: number_span })?,
224                                span,
225                            })
226                        }
227
228                        // syntax: n
229                        _ => Ok(AnPlusB { a: 1, b: 0, span: ident_span }),
230                    }
231                } else if ident_name.eq_ignore_ascii_case("n-") {
232                    // syntax: n- <signless-integer>
233                    // examples: 'n- 1'
234                    let (number, number_span) = expect_unsigned_int(input)?;
235                    let span = Span { start: ident_span.start, end: number_span.end };
236                    Ok(AnPlusB {
237                        a: 1,
238                        b: -i32::try_from(number)
239                            .map_err(|kind| Error { kind, span: number_span })?,
240                        span,
241                    })
242                } else if let Some(b) = parse_ndashdigit_b(
243                    &ident_name,
244                    "n-",
245                    Span { start: ident_span.start + 2, end: ident_span.end },
246                ) {
247                    // syntax: <ndashdigit-ident>
248                    // examples: 'n-1'
249                    Ok(AnPlusB { a: 1, b: b?, span: ident_span })
250                } else if ident_name.eq_ignore_ascii_case("-n") {
251                    match &input.cursor.peek()?.token {
252                        // syntax: -n ['+' | '-'] <signless-integer>
253                        // examples: '-n + 1', '-n - 1', '-n+ 1'
254                        sign @ Token::Plus(..) | sign @ Token::Minus(..) => {
255                            let sign = if let Token::Plus(..) = sign { 1 } else { -1 };
256                            input.cursor.bump()?;
257                            let (number, number_span) = expect_unsigned_int(input)?;
258                            let span = Span { start: ident_span.start, end: number_span.end };
259                            Ok(AnPlusB {
260                                a: -1,
261                                b: sign
262                                    * i32::try_from(number)
263                                        .map_err(|kind| Error { kind, span: number_span })?,
264                                span,
265                            })
266                        }
267
268                        // syntax: -n <signed-integer>
269                        // examples: '-n +1', '-n -1'
270                        Token::Number(..) => {
271                            let (number, number_span) = input.cursor.expect_number()?;
272                            let span = Span { start: ident_span.start, end: number_span.end };
273                            Ok(AnPlusB {
274                                a: -1,
275                                b: number
276                                    .try_into()
277                                    .map_err(|kind| Error { kind, span: number_span })?,
278                                span,
279                            })
280                        }
281
282                        // syntax: -n
283                        _ => Ok(AnPlusB { a: -1, b: 0, span: ident_span }),
284                    }
285                } else if ident_name.eq_ignore_ascii_case("-n-") {
286                    // syntax: -n- <signless-integer>
287                    // examples: '-n- 1'
288                    let (number, number_span) = expect_unsigned_int(input)?;
289                    let span = Span { start: ident_span.start, end: number_span.end };
290                    Ok(AnPlusB {
291                        a: -1,
292                        b: -i32::try_from(number)
293                            .map_err(|kind| Error { kind, span: number_span })?,
294                        span,
295                    })
296                } else if let Some(b) = parse_ndashdigit_b(
297                    &ident_name,
298                    "-n-",
299                    Span { start: ident_span.start + 3, end: ident_span.end },
300                ) {
301                    // syntax: -n-<ndashdigit-ident>
302                    // examples: '-n-1'
303                    Ok(AnPlusB { a: -1, b: b?, span: ident_span })
304                } else {
305                    Err(Error { kind: ErrorKind::InvalidAnPlusB, span: ident_span })
306                }
307            }
308
309            TokenWithSpan { span, .. } => {
310                Err(Error { kind: ErrorKind::InvalidAnPlusB, span: *span })
311            }
312        }
313    }
314}
315
316// https://www.w3.org/TR/selectors-4/#attribute-selectors
317//
318// <attribute-selector> = '[' <wq-name> ']'
319//                      | '[' <wq-name> <attr-matcher> [ <string-token> | <ident-token> ] <attr-modifier>? ']'
320// <attr-matcher>  = [ '~' | '|' | '^' | '$' | '*' ]? '='
321// <attr-modifier> = i | s
322impl<'a> Parse<'a> for AttributeSelector<'a> {
323    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
324        let start = input.cursor.expect_l_bracket()?.1.start;
325
326        let name = match input.cursor.peek()? {
327            TokenWithSpan {
328                token: Token::Ident(..) | Token::HashLBrace(..) | Token::AtLBraceVar(..),
329                ..
330            } => {
331                let ident = input.parse::<InterpolableIdent>()?;
332                let ident_span = ident.span();
333                if let Some((_, bar_token_span)) = input.cursor.eat_bar()? {
334                    let name = input.parse::<InterpolableIdent>()?;
335                    let name_span = name.span();
336
337                    let start = ident_span.start;
338                    let end = name_span.end;
339                    WqName {
340                        name,
341                        prefix: Some(NsPrefix {
342                            kind: Some(NsPrefixKind::Ident(ident)),
343                            span: Span { start, end: bar_token_span.end },
344                        }),
345                        span: Span { start, end },
346                    }
347                } else {
348                    let span = *ident_span;
349                    WqName { name: ident, prefix: None, span }
350                }
351            }
352            TokenWithSpan { token: Token::Asterisk(..), .. } => {
353                let asterisk_span = input.cursor.bump()?.span;
354                let bar_token_span = input.cursor.expect_bar()?.1;
355                let name = input.parse::<InterpolableIdent>()?;
356
357                let start = asterisk_span.start;
358                let end = name.span().end;
359                WqName {
360                    name,
361                    prefix: Some(NsPrefix {
362                        kind: Some(NsPrefixKind::Universal(NsPrefixUniversal {
363                            span: asterisk_span,
364                        })),
365                        span: Span { start, end: bar_token_span.end },
366                    }),
367                    span: Span { start, end },
368                }
369            }
370            TokenWithSpan { token: Token::Bar(..), .. } => {
371                let bar_token_span = input.cursor.bump()?.span;
372                let name = input.parse::<InterpolableIdent>()?;
373
374                let start = bar_token_span.start;
375                let end = name.span().end;
376                WqName {
377                    name,
378                    prefix: Some(NsPrefix {
379                        kind: None,
380                        span: Span { start, end: bar_token_span.end },
381                    }),
382                    span: Span { start, end },
383                }
384            }
385            TokenWithSpan { span, .. } => {
386                return Err(Error { kind: ErrorKind::ExpectWqName, span: *span });
387            }
388        };
389
390        let matcher = match input.cursor.peek()? {
391            TokenWithSpan { token: Token::RBracket(..), .. } => None,
392            TokenWithSpan { token: Token::Equal(..), .. } => Some(AttributeSelectorMatcher {
393                kind: AttributeSelectorMatcherKind::Exact,
394                span: input.cursor.bump()?.span,
395            }),
396            TokenWithSpan { token: Token::TildeEqual(..), .. } => Some(AttributeSelectorMatcher {
397                kind: AttributeSelectorMatcherKind::MatchWord,
398                span: input.cursor.bump()?.span,
399            }),
400            TokenWithSpan { token: Token::BarEqual(..), .. } => Some(AttributeSelectorMatcher {
401                kind: AttributeSelectorMatcherKind::ExactOrPrefixThenHyphen,
402                span: input.cursor.bump()?.span,
403            }),
404            TokenWithSpan { token: Token::CaretEqual(..), .. } => Some(AttributeSelectorMatcher {
405                kind: AttributeSelectorMatcherKind::Prefix,
406                span: input.cursor.bump()?.span,
407            }),
408            TokenWithSpan { token: Token::DollarEqual(..), .. } => Some(AttributeSelectorMatcher {
409                kind: AttributeSelectorMatcherKind::Suffix,
410                span: input.cursor.bump()?.span,
411            }),
412            TokenWithSpan { token: Token::AsteriskEqual(..), .. } => {
413                Some(AttributeSelectorMatcher {
414                    kind: AttributeSelectorMatcherKind::Substring,
415                    span: input.cursor.bump()?.span,
416                })
417            }
418            TokenWithSpan { span, .. } => {
419                return Err(Error { kind: ErrorKind::ExpectAttributeSelectorMatcher, span: *span });
420            }
421        };
422
423        let value = if matcher.is_some() {
424            match input.cursor.peek()? {
425                TokenWithSpan {
426                    token:
427                        Token::Ident(..)
428                        | Token::HashLBrace(..)
429                        | Token::AtLBraceVar(..)
430                        | Token::Placeholder(..),
431                    ..
432                } => Some(AttributeSelectorValue::Ident(input.parse()?)),
433                TokenWithSpan { token: Token::Str(..) | Token::StrTemplate(..), .. } => {
434                    Some(AttributeSelectorValue::Str(input.parse()?))
435                }
436                // Unquoted numeric values such as `[size=1]` or `[size=1px]` are
437                // technically non-conforming (Selectors wants an ident or string), but
438                // browsers accept them and they appear in real CSS (incl. UA stylesheets).
439                TokenWithSpan { token: Token::Number(..), .. } => {
440                    Some(AttributeSelectorValue::Number(input.parse()?))
441                }
442                TokenWithSpan { token: Token::Dimension(..), .. } => {
443                    Some(AttributeSelectorValue::Dimension(input.parse()?))
444                }
445                TokenWithSpan { token: Token::Percentage(..), .. }
446                    if input.syntax == Syntax::Less =>
447                {
448                    Some(AttributeSelectorValue::Percentage(input.parse()?))
449                }
450                TokenWithSpan { token: Token::Tilde(..), .. } if input.syntax == Syntax::Less => {
451                    Some(AttributeSelectorValue::LessEscapedStr(input.parse()?))
452                }
453                TokenWithSpan { token: Token::RBracket(..), span } => {
454                    input
455                        .recoverable_errors
456                        .push(Error { kind: ErrorKind::ExpectAttributeSelectorValue, span: *span });
457                    None
458                }
459                // An unusual value like `[attr=;]` is invalid per the
460                // Selectors grammar, but postcss accepts it; preserve the raw
461                // tokens up to the closing `]`.
462                TokenWithSpan { span, .. } if input.syntax == Syntax::Css => {
463                    let start = span.start;
464                    let mut tokens = input.vec();
465                    while !matches!(
466                        input.cursor.peek()?.token,
467                        Token::RBracket(..) | Token::Eof(..)
468                    ) {
469                        tokens.push(input.cursor.bump()?);
470                    }
471                    let end = tokens.last().map_or(start, |t| t.span.end);
472                    Some(AttributeSelectorValue::TokenSeq(TokenSeq {
473                        tokens,
474                        span: Span { start, end },
475                    }))
476                }
477                token_with_span => {
478                    return Err(Error {
479                        kind: ErrorKind::ExpectAttributeSelectorValue,
480                        span: token_with_span.span,
481                    });
482                }
483            }
484        } else {
485            None
486        };
487
488        let modifier = if value.is_some() {
489            match &input.cursor.peek()?.token {
490                Token::Ident(..) | Token::HashLBrace(..) => {
491                    let ident = input.parse::<InterpolableIdent>()?;
492                    let span = *ident.span();
493                    Some(AttributeSelectorModifier { ident, span })
494                }
495                _ => None,
496            }
497        } else {
498            None
499        };
500
501        let end = input.cursor.expect_r_bracket()?.1.end;
502        Ok(AttributeSelector { name, matcher, value, modifier, span: Span { start, end } })
503    }
504}
505
506// <class-selector> = '.' <ident-token>
507impl<'a> Parse<'a> for ClassSelector<'a> {
508    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
509        let (_, dot_span) = input.cursor.expect_dot()?;
510        let start = dot_span.start;
511        let end;
512        // Detect an adjacent placeholder without `peek()`: `peek()` skips
513        // whitespace and caches a token, which would both break the no-ws rule
514        // (the name must immediately follow the dot) and trip the empty-cache
515        // assertion in the `expect_ident_without_ws_or_comments` fallback. `scan_placeholder`
516        // returns `None` (leaving the tokenizer untouched) unless a placeholder
517        // begins exactly here, so the fallback paths run with an empty cache.
518        let placeholder = if input.options.template_placeholder.is_some() {
519            input.cursor.tokenizer.scan_placeholder()
520        } else {
521            None
522        };
523        let name = if let Some(token) = placeholder {
524            let placeholder = token.placeholder(input.source).unwrap();
525            let span = token.span;
526            end = span.end;
527            InterpolableIdent::Placeholder((placeholder, span).into())
528        } else if input.syntax == Syntax::Css {
529            let (ident, ident_span) = input.cursor.expect_ident_without_ws_or_comments(false)?;
530            end = ident_span.end;
531            InterpolableIdent::Literal(input.ident(ident, ident_span))
532        } else {
533            let ident = input.parse::<InterpolableIdent>()?;
534            let ident_span = ident.span();
535            util::assert_no_ws_or_comment(&dot_span, ident_span)?;
536            end = ident_span.end;
537            ident
538        };
539
540        Ok(ClassSelector { name, span: Span { start, end } })
541    }
542}
543
544// <complex-selector> = <compound-selector> [ <combinator>? <compound-selector> ]*
545impl<'a> Parse<'a> for ComplexSelector<'a> {
546    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
547        let mut children = input.vec_with_capacity(3);
548
549        let (span, first, mut is_previous_combinator) = if let Token::GreaterThan(..)
550        | Token::Plus(..)
551        | Token::Tilde(..)
552        | Token::BarBar(..) =
553            input.cursor.peek()?.token
554        {
555            let end = input.cursor.tokenizer.current_offset();
556            if let Some(combinator) = input.parse_combinator(end)? {
557                (combinator.span, ComplexSelectorChild::Combinator(combinator), true)
558            } else {
559                return Err(Error {
560                    kind: ErrorKind::ExpectSimpleSelector,
561                    span: input.cursor.bump()?.span,
562                });
563            }
564        } else {
565            let compound_selector = input.parse::<CompoundSelector>()?;
566            (
567                compound_selector.span,
568                ComplexSelectorChild::CompoundSelector(compound_selector),
569                false,
570            )
571        };
572        let Span { start, mut end } = span;
573
574        children.push(first);
575        let is_less = input.syntax == Syntax::Less;
576        while !matches!(
577            input.cursor.peek()?.token,
578            Token::LBrace(..) | Token::Indent(..) | Token::Linebreak(..)
579        ) {
580            if is_previous_combinator {
581                // dart-sass allows consecutive combinators (`> >`, `+ ~`) and a
582                // trailing combinator (`:is(a +)`); after a combinator, take another
583                // combinator or stop at a selector boundary rather than requiring a
584                // compound selector. CSS keeps the strict alternation.
585                if matches!(input.syntax, Syntax::Scss | Syntax::Sass) {
586                    if matches!(
587                        input.cursor.peek()?.token,
588                        Token::GreaterThan(..)
589                            | Token::Plus(..)
590                            | Token::Tilde(..)
591                            | Token::BarBar(..)
592                    ) && let Some(combinator) = input.parse_combinator(end)?
593                    {
594                        end = combinator.span.end;
595                        children.push(ComplexSelectorChild::Combinator(combinator));
596                        continue;
597                    } else if matches!(
598                        input.cursor.peek()?.token,
599                        Token::RParen(..) | Token::Comma(..) | Token::RBrace(..) | Token::Eof(..)
600                    ) {
601                        break;
602                    }
603                }
604                let compound_selector = input.parse::<CompoundSelector>()?;
605                end = compound_selector.span.end;
606                children.push(ComplexSelectorChild::CompoundSelector(compound_selector));
607            } else if let Some(combinator) = input.parse_combinator(end)? {
608                if is_less
609                    && combinator.kind == CombinatorKind::Descendant
610                    && input.cursor.peek()?.is_ident_raw(input.source, "when")
611                {
612                    break;
613                }
614                children.push(ComplexSelectorChild::Combinator(combinator));
615            } else {
616                break;
617            }
618            is_previous_combinator = !is_previous_combinator;
619        }
620
621        Ok(ComplexSelector { children, span: Span { start, end } })
622    }
623}
624
625// <compound-selector> = [ <type-selector>? <subclass-selector>*
626//                         [ <pseudo-element-selector> <pseudo-class-selector>* ]* ]!
627impl<'a> Parse<'a> for CompoundSelector<'a> {
628    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
629        let first = input.parse::<SimpleSelector>()?;
630        let first_span = first.span();
631        let start = first_span.start;
632        let mut end = first_span.end;
633
634        let mut children = input.vec_with_capacity(2);
635        children.push(first);
636        loop {
637            use token::*;
638            match input.cursor.peek()? {
639                TokenWithSpan {
640                    token:
641                        Token::Dot(..)
642                        | Token::Hash(..)
643                        | Token::NumberSign(..)
644                        | Token::LBracket(..)
645                        | Token::Colon(..)
646                        | Token::ColonColon(..)
647                        | Token::Ident(..)
648                        | Token::Asterisk(..)
649                        | Token::HashLBrace(..)
650                        | Token::Bar(..)
651                        | Token::Ampersand(..)
652                        | Token::AtLBraceVar(..),
653                    span,
654                } if !util::has_ws(input.source, end, span.start) => {
655                    let child = input.parse::<SimpleSelector>()?;
656                    end = child.span().end;
657                    children.push(child);
658                }
659                TokenWithSpan { token: Token::Percent(..), span }
660                    if matches!(input.syntax, Syntax::Scss | Syntax::Sass)
661                        && !util::has_ws(input.source, end, span.start) =>
662                {
663                    let child = input.parse::<SimpleSelector>()?;
664                    end = child.span().end;
665                    children.push(child);
666                }
667                TokenWithSpan { token: Token::Placeholder(..), span }
668                    if !util::has_ws(input.source, end, span.start) =>
669                {
670                    let child = input.parse::<SimpleSelector>()?;
671                    end = child.span().end;
672                    children.push(child);
673                }
674                _ => break,
675            }
676        }
677
678        Ok(CompoundSelector { children, span: Span { start, end } })
679    }
680}
681
682// <compound-selector-list> = <compound-selector>#
683impl<'a> Parse<'a> for CompoundSelectorList<'a> {
684    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
685        let first = input.parse::<CompoundSelector>()?;
686        let mut span = first.span;
687
688        let mut selectors = input.vec1(first);
689        let mut comma_spans = input.vec();
690        while let Some((_, comma_span)) = input.cursor.eat_comma()? {
691            comma_spans.push(comma_span);
692            input.eat_sass_line_continuation()?;
693            selectors.push(input.parse()?);
694        }
695
696        // SAFETY: it has at least one element.
697        span.end = unsafe {
698            let index = selectors.len() - 1;
699            selectors.get_unchecked(index).span().end
700        };
701        Ok(CompoundSelectorList { selectors, comma_spans, span })
702    }
703}
704
705// <id-selector> = <hash-token>
706impl<'a> Parse<'a> for IdSelector<'a> {
707    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
708        match input.cursor.bump()? {
709            token @ TokenWithSpan { token: Token::Hash(..), span } => {
710                let token = token.hash(input.source).unwrap();
711                let first_span = Span { start: span.start + 1, end: span.end };
712                let raw = token.raw;
713                if raw.starts_with(|c: char| c.is_ascii_digit())
714                    || matches!(raw.as_bytes(), [b'-'] | [b'-', b'0'..=b'9', ..])
715                {
716                    input
717                        .recoverable_errors
718                        .push(Error { kind: ErrorKind::InvalidIdSelectorName, span });
719                }
720                let value =
721                    if token.escaped { util::handle_escape_in(raw, input.allocator) } else { raw };
722                let first = Ident { name: value, raw: token.raw, span: first_span };
723                let name = match input.cursor.peek()? {
724                    TokenWithSpan { token: Token::HashLBrace(..), span }
725                        if matches!(input.syntax, Syntax::Scss | Syntax::Sass)
726                            && first.span.end == span.start =>
727                    {
728                        match input.parse()? {
729                            InterpolableIdent::SassInterpolated(mut interpolation) => {
730                                interpolation.elements.insert(
731                                    0,
732                                    SassInterpolatedIdentElement::Static(
733                                        InterpolableIdentStaticPart {
734                                            value: first.name,
735                                            raw: first.raw,
736                                            span: first.span,
737                                        },
738                                    ),
739                                );
740                                InterpolableIdent::SassInterpolated(interpolation)
741                            }
742                            _ => unreachable!(),
743                        }
744                    }
745                    _ => InterpolableIdent::Literal(first),
746                };
747                let span = Span { start: span.start, end: name.span().end };
748                Ok(IdSelector { name, span })
749            }
750            TokenWithSpan { token: Token::NumberSign(..), span } => {
751                let name = input.parse::<InterpolableIdent>()?;
752                let span = Span { start: span.start, end: name.span().end };
753                Ok(IdSelector { name, span })
754            }
755            TokenWithSpan { span, .. } => Err(Error { kind: ErrorKind::ExpectIdSelector, span }),
756        }
757    }
758}
759
760// A `:lang()` argument: <lang-range> = <ident-token> | <string-token>  (BCP 47 range)
761impl<'a> Parse<'a> for LanguageRange<'a> {
762    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
763        match &input.cursor.peek()?.token {
764            Token::Str(..) | Token::StrTemplate(..) => input.parse().map(LanguageRange::Str),
765            _ => input.parse().map(LanguageRange::Ident),
766        }
767    }
768}
769
770// :lang( <lang-range># )
771impl<'a> Parse<'a> for LanguageRangeList<'a> {
772    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
773        let first = input.parse::<LanguageRange>()?;
774        let mut span = *first.span();
775
776        let mut ranges = input.vec1(first);
777        let mut comma_spans = input.vec();
778        while let Some((_, comma_span)) = input.cursor.eat_comma()? {
779            comma_spans.push(comma_span);
780            ranges.push(input.parse()?);
781        }
782        debug_assert_eq!(comma_spans.len() + 1, ranges.len());
783
784        if let Some(end) = ranges.last() {
785            span.end = end.span().end;
786        }
787        Ok(LanguageRangeList { ranges, comma_spans, span })
788    }
789}
790
791// https://drafts.csswg.org/css-nesting-1/#nest-selector
792//
793// The nesting selector `&`. This parser also accepts a glued ident/interpolation
794// suffix (`&__x`, `&#{$m}`, `&-@{v}`) as used by Sass/Less.
795impl<'a> Parse<'a> for NestingSelector<'a> {
796    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
797        let (_, mut span) = input.cursor.expect_ampersand()?;
798        let suffix = match input.syntax {
799            Syntax::Css => {
800                if let Some((ident, ident_span)) = input.cursor.tokenizer.scan_ident_template()? {
801                    span.end = ident_span.end;
802                    Some(InterpolableIdent::Literal(input.ident(ident, ident_span)))
803                } else {
804                    None
805                }
806            }
807            Syntax::Scss | Syntax::Sass => {
808                let start = span.end;
809                let elements = input.parse_sass_interpolated_ident_rest(&mut span.end)?;
810                if elements.is_empty() {
811                    None
812                } else {
813                    Some(InterpolableIdent::SassInterpolated(SassInterpolatedIdent {
814                        elements,
815                        span: Span { start, end: span.end },
816                    }))
817                }
818            }
819            Syntax::Less => {
820                let start = span.end;
821                let elements = input.parse_less_interpolated_ident_rest(&mut span.end)?;
822                if elements.is_empty() {
823                    None
824                } else {
825                    Some(InterpolableIdent::LessInterpolated(LessInterpolatedIdent {
826                        elements,
827                        span: Span { start, end: span.end },
828                    }))
829                }
830            }
831        };
832        Ok(NestingSelector { suffix, span })
833    }
834}
835
836// https://drafts.csswg.org/selectors-4/#the-nth-child-pseudo
837//
838// The `:nth-child()` argument: <nth> [ of <complex-selector-list> ]?
839impl<'a> Parse<'a> for Nth<'a> {
840    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
841        let index = input.parse::<NthIndex>()?;
842        let mut span = *index.span();
843        let matcher = if input.cursor.peek()?.is_ident_name_eq_ignore_ascii_case(input.source, "of")
844        {
845            let matcher = input.parse::<NthMatcher>()?;
846            span.end = matcher.span.end;
847            Some(matcher)
848        } else {
849            None
850        };
851
852        Ok(Nth { index, matcher, span })
853    }
854}
855
856// <nth> = <an+b> | even | odd   (plus a plain <integer>)
857impl<'a> Parse<'a> for NthIndex<'a> {
858    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
859        let peek = input.cursor.peek()?;
860        if peek.ident(input.source).is_some() {
861            if peek.is_ident_name_eq_ignore_ascii_case(input.source, "odd") {
862                input.parse().map(NthIndex::Odd)
863            } else if peek.is_ident_name_eq_ignore_ascii_case(input.source, "even") {
864                input.parse().map(NthIndex::Even)
865            } else {
866                input.parse().map(NthIndex::AnPlusB)
867            }
868        } else if matches!(peek.token, Token::Number(..)) {
869            let number = input.parse::<Number>()?;
870            if number.value.fract() == 0.0 {
871                Ok(NthIndex::Integer(number))
872            } else {
873                Err(Error { kind: ErrorKind::ExpectInteger, span: number.span })
874            }
875        } else {
876            input.parse().map(NthIndex::AnPlusB)
877        }
878    }
879}
880
881// of <complex-selector-list>
882impl<'a> Parse<'a> for NthMatcher<'a> {
883    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
884        let (ident, mut span) = input.cursor.expect_ident()?;
885        if !ident.name().eq_ignore_ascii_case("of") {
886            return Err(Error { kind: ErrorKind::ExpectNthOf, span });
887        }
888
889        let selector = if matches!(&input.cursor.peek()?.token, Token::RParen(..)) {
890            None
891        } else {
892            let selector = input.parse::<SelectorList>()?;
893            span.end = selector.span.end;
894            Some(selector)
895        };
896
897        Ok(NthMatcher { selector, span })
898    }
899}
900
901// https://www.w3.org/TR/selectors-4/#pseudo-classes
902//
903// <pseudo-class-selector> = ':' <ident-token>
904//                         | ':' <function-token> <any-value> ')'
905impl<'a> Parse<'a> for PseudoClassSelector<'a> {
906    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
907        let (_, colon_span) = input.cursor.expect_colon()?;
908        let name = input.parse::<InterpolableIdent>()?;
909        let name_span = name.span();
910        util::assert_no_ws(input.source, &colon_span, name_span)?;
911
912        let mut end = name_span.end;
913
914        let arg = match input.cursor.peek()? {
915            TokenWithSpan { token: Token::LParen(..), span: l_paren } if l_paren.start == end => {
916                let l_paren = *l_paren;
917                input.cursor.bump()?;
918                let kind = match &name {
919                    InterpolableIdent::Literal(Ident { name, .. })
920                        if name.eq_ignore_ascii_case("nth-child")
921                            || name.eq_ignore_ascii_case("nth-last-child") =>
922                    {
923                        if input.syntax == Syntax::Css {
924                            input.parse().map(PseudoClassSelectorArgKind::Nth)?
925                        } else if let Ok(nth) = input.try_parse(Nth::parse) {
926                            PseudoClassSelectorArgKind::Nth(nth)
927                        } else {
928                            input
929                                .parse_tokens_in_parens()
930                                .map(PseudoClassSelectorArgKind::TokenSeq)?
931                        }
932                    }
933                    InterpolableIdent::Literal(Ident { name, .. })
934                        if name.eq_ignore_ascii_case("nth-of-type")
935                            || name.eq_ignore_ascii_case("nth-last-of-type")
936                            || name.eq_ignore_ascii_case("nth-col")
937                            || name.eq_ignore_ascii_case("nth-last-col") =>
938                    'pseudo_arg: {
939                        let nth = if input.syntax == Syntax::Css {
940                            input.parse()?
941                        } else if let Ok(nth) = input.try_parse(Nth::parse) {
942                            nth
943                        } else {
944                            break 'pseudo_arg input
945                                .parse_tokens_in_parens()
946                                .map(PseudoClassSelectorArgKind::TokenSeq)?;
947                        };
948                        if let Some(NthMatcher { span, .. }) = &nth.matcher {
949                            input
950                                .recoverable_errors
951                                .push(Error { kind: ErrorKind::UnexpectedNthMatcher, span: *span });
952                        }
953                        PseudoClassSelectorArgKind::Nth(nth)
954                    }
955                    InterpolableIdent::Literal(Ident { name, .. })
956                        if name.eq_ignore_ascii_case("not")
957                            || name.eq_ignore_ascii_case("is")
958                            || name.eq_ignore_ascii_case("where")
959                            || name.eq_ignore_ascii_case("matches")
960                            || name.eq_ignore_ascii_case("global") =>
961                    {
962                        input.parse().map(PseudoClassSelectorArgKind::SelectorList)?
963                    }
964                    InterpolableIdent::Literal(Ident { name, .. })
965                        if name.eq_ignore_ascii_case("has") =>
966                    {
967                        input.parse().map(PseudoClassSelectorArgKind::RelativeSelectorList)?
968                    }
969                    InterpolableIdent::Literal(Ident { name, .. })
970                        if name.eq_ignore_ascii_case("dir") =>
971                    {
972                        input.parse().map(PseudoClassSelectorArgKind::Ident)?
973                    }
974                    InterpolableIdent::Literal(Ident { name, .. })
975                        if name.eq_ignore_ascii_case("lang") =>
976                    {
977                        input.parse().map(PseudoClassSelectorArgKind::LanguageRangeList)?
978                    }
979                    InterpolableIdent::Literal(Ident { name, .. })
980                        if name.eq_ignore_ascii_case("-moz-any")
981                            || name.eq_ignore_ascii_case("-webkit-any")
982                            || name.eq_ignore_ascii_case("any") =>
983                    {
984                        // formally compound selectors, but real-world usage
985                        // includes complex ones (`:-moz-any(ol p.blah, ul)`)
986                        input.parse().map(PseudoClassSelectorArgKind::SelectorList)?
987                    }
988                    InterpolableIdent::Literal(Ident { name, .. })
989                        if name.eq_ignore_ascii_case("current")
990                            || name.eq_ignore_ascii_case("past")
991                            || name.eq_ignore_ascii_case("future") =>
992                    {
993                        input.parse().map(PseudoClassSelectorArgKind::CompoundSelectorList)?
994                    }
995                    InterpolableIdent::Literal(Ident { name, .. })
996                        if name.eq_ignore_ascii_case("host")
997                            || name.eq_ignore_ascii_case("host-context") =>
998                    {
999                        // formally a single compound selector, but Angular's ShadowCss
1000                        // supports combinators and lists (`:host-context(.parent .child)`)
1001                        input.parse().map(PseudoClassSelectorArgKind::SelectorList)?
1002                    }
1003                    InterpolableIdent::Literal(Ident { name, .. })
1004                        if input.syntax == Syntax::Less && *name == "extend" =>
1005                    {
1006                        input.parse().map(PseudoClassSelectorArgKind::LessExtendList)?
1007                    }
1008                    _ => {
1009                        input.parse_tokens_in_parens().map(PseudoClassSelectorArgKind::TokenSeq)?
1010                    }
1011                };
1012
1013                let r_paren = input.cursor.expect_r_paren()?.1;
1014                end = r_paren.end;
1015                let span = Span { start: l_paren.start, end: r_paren.end };
1016                Some(PseudoClassSelectorArg { kind, l_paren, r_paren, span })
1017            }
1018            _ => None,
1019        };
1020
1021        let span = Span { start: colon_span.start, end };
1022        Ok(PseudoClassSelector { name, arg, span })
1023    }
1024}
1025
1026// https://www.w3.org/TR/selectors-4/#pseudo-elements
1027//
1028// <pseudo-element-selector> = '::' <ident-token>
1029//                           | '::' <function-token> <any-value> ')'
1030impl<'a> Parse<'a> for PseudoElementSelector<'a> {
1031    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1032        let (_, colon_colon_span) = input.cursor.expect_colon_colon()?;
1033        let mut end;
1034        let name = if input.syntax == Syntax::Css {
1035            let (ident, ident_span) = input.cursor.expect_ident()?;
1036            end = ident_span.end;
1037            util::assert_no_ws(input.source, &colon_colon_span, &ident_span)?;
1038            InterpolableIdent::Literal(input.ident(ident, ident_span))
1039        } else {
1040            let name = input.parse::<InterpolableIdent>()?;
1041            let name_span = name.span();
1042            end = name_span.end;
1043            util::assert_no_ws(input.source, &colon_colon_span, name_span)?;
1044            name
1045        };
1046
1047        let arg = match input.cursor.peek()? {
1048            TokenWithSpan { token: Token::LParen(..), span: l_paren } if l_paren.start == end => {
1049                let l_paren = *l_paren;
1050                input.cursor.bump()?;
1051                let kind = match &name {
1052                    InterpolableIdent::Literal(Ident { name, .. })
1053                        if name.eq_ignore_ascii_case("part") =>
1054                    {
1055                        // ::part( <ident>+ ) — CSS Shadow Parts allows
1056                        // selecting multiple part names at once.
1057                        let first = input.parse::<InterpolableIdent>()?;
1058                        if matches!(
1059                            input.cursor.peek()?.token,
1060                            Token::Ident(..) | Token::HashLBrace(..) | Token::AtLBraceVar(..)
1061                        ) {
1062                            let mut span = *first.span();
1063                            let mut idents = input.vec_with_capacity(2);
1064                            idents.push(first);
1065                            while matches!(
1066                                input.cursor.peek()?.token,
1067                                Token::Ident(..) | Token::HashLBrace(..) | Token::AtLBraceVar(..)
1068                            ) {
1069                                let ident = input.parse::<InterpolableIdent>()?;
1070                                span.end = ident.span().end;
1071                                idents.push(ident);
1072                            }
1073                            PseudoElementSelectorArgKind::IdentList(IdentList { idents, span })
1074                        } else {
1075                            PseudoElementSelectorArgKind::Ident(first)
1076                        }
1077                    }
1078                    InterpolableIdent::Literal(Ident { name, .. })
1079                        if name.eq_ignore_ascii_case("cue")
1080                            || name.eq_ignore_ascii_case("cue-region") =>
1081                    {
1082                        input.parse().map(PseudoElementSelectorArgKind::CompoundSelector)?
1083                    }
1084                    InterpolableIdent::Literal(Ident { name, .. })
1085                        if name.eq_ignore_ascii_case("slotted") =>
1086                    {
1087                        // formally a single compound selector, but sass extend
1088                        // output produces lists (`::slotted(.c.d, .d.e)`)
1089                        input.parse().map(PseudoElementSelectorArgKind::CompoundSelectorList)?
1090                    }
1091                    _ => input
1092                        .parse_tokens_in_parens()
1093                        .map(PseudoElementSelectorArgKind::TokenSeq)?,
1094                };
1095
1096                let r_paren = input.cursor.expect_r_paren()?.1;
1097                end = r_paren.end;
1098                let span = Span { start: l_paren.start, end: r_paren.end };
1099                Some(PseudoElementSelectorArg { kind, l_paren, r_paren, span })
1100            }
1101            _ => None,
1102        };
1103
1104        let span = Span { start: colon_colon_span.start, end };
1105        Ok(PseudoElementSelector { name, arg, span })
1106    }
1107}
1108
1109// <relative-selector> = <combinator>? <complex-selector>
1110impl<'a> Parse<'a> for RelativeSelector<'a> {
1111    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1112        let pos = input.cursor.tokenizer.current_offset();
1113        let combinator = match input.parse_combinator(pos)? {
1114            Some(Combinator { kind: CombinatorKind::Descendant, .. }) => None,
1115            combinator => combinator,
1116        };
1117        let complex_selector = input.parse::<ComplexSelector>()?;
1118        let mut span = complex_selector.span;
1119        if let Some(combinator) = &combinator {
1120            span.start = combinator.span.start;
1121        }
1122        Ok(RelativeSelector { combinator, complex_selector, span })
1123    }
1124}
1125
1126// <relative-selector-list> = <relative-selector>#
1127impl<'a> Parse<'a> for RelativeSelectorList<'a> {
1128    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1129        let first = input.parse::<RelativeSelector>()?;
1130        let mut span = first.span;
1131
1132        let mut selectors = input.vec1(first);
1133        let mut comma_spans = input.vec();
1134        while let Some((_, comma_span)) = input.cursor.eat_comma()? {
1135            comma_spans.push(comma_span);
1136            selectors.push(input.parse()?);
1137        }
1138
1139        // SAFETY: it has at least one element.
1140        span.end = unsafe {
1141            let index = selectors.len() - 1;
1142            selectors.get_unchecked(index).span().end
1143        };
1144        Ok(RelativeSelectorList { selectors, comma_spans, span })
1145    }
1146}
1147
1148// https://www.w3.org/TR/selectors-4/#typedef-selector-list
1149//
1150// <selector-list> = <complex-selector-list> = <complex-selector>#
1151impl<'a> Parse<'a> for SelectorList<'a> {
1152    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1153        let first = input.parse::<ComplexSelector>()?;
1154        let mut span = first.span;
1155
1156        let mut selectors = input.vec_with_capacity(2);
1157        selectors.push(first);
1158        let mut comma_spans = input.vec();
1159
1160        let is_css = input.syntax == Syntax::Css;
1161        while let Some((_, comma_span)) = input.cursor.eat_comma()? {
1162            span.end = comma_span.end;
1163            comma_spans.push(comma_span);
1164            // legacy corpora carry doubled/trailing commas (`div,, span,, {`);
1165            // absorb the extras in SCSS like libsass did
1166            if input.syntax == Syntax::Scss {
1167                while let Some((_, comma_span)) = input.cursor.eat_comma()? {
1168                    span.end = comma_span.end;
1169                    comma_spans.push(comma_span);
1170                }
1171            }
1172            // In the indented syntax a deeper line after the comma continues
1173            // the selector list (`a,\n    b\n  c: d`); a same-level line or
1174            // `{` means the comma was trailing.
1175            if input.syntax == Syntax::Sass
1176                && matches!(input.cursor.peek()?.token, Token::Indent(..))
1177            {
1178                input.eat_sass_line_continuation()?;
1179            } else if !is_css
1180                && matches!(
1181                    input.cursor.peek()?.token,
1182                    Token::LBrace(..) | Token::Indent(..) | Token::Linebreak(..)
1183                )
1184            {
1185                break;
1186            }
1187
1188            let selector = input.parse::<ComplexSelector>()?;
1189            span.end = selector.span.end;
1190            selectors.push(selector);
1191        }
1192
1193        // absorbed doubled/trailing commas can outnumber the selectors, so
1194        // phrase the invariants without subtraction (usize underflow)
1195        debug_assert!(if is_css {
1196            selectors.len() == comma_spans.len() + 1
1197        } else {
1198            selectors.len() <= comma_spans.len() + 1
1199        });
1200
1201        Ok(SelectorList { selectors, comma_spans, span })
1202    }
1203}
1204
1205// https://www.w3.org/TR/selectors-4/#ref-for-typedef-simple-selector
1206//
1207// <simple-selector>   = <type-selector> | <subclass-selector>
1208// <subclass-selector> = <id-selector> | <class-selector>
1209//                     | <attribute-selector> | <pseudo-class-selector>
1210impl<'a> Parse<'a> for SimpleSelector<'a> {
1211    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1212        match input.cursor.peek()? {
1213            TokenWithSpan { token: Token::Dot(..), .. } => input.parse().map(SimpleSelector::Class),
1214            TokenWithSpan { token: Token::Hash(..) | Token::NumberSign(..), .. } => {
1215                input.parse().map(SimpleSelector::Id)
1216            }
1217            TokenWithSpan { token: Token::LBracket(..), .. } => {
1218                let selector = input.parse()?;
1219                Ok(SimpleSelector::Attribute(input.alloc(selector)))
1220            }
1221            TokenWithSpan { token: Token::Colon(..), .. } => {
1222                let selector = input.parse()?;
1223                Ok(SimpleSelector::PseudoClass(input.alloc(selector)))
1224            }
1225            TokenWithSpan { token: Token::ColonColon(..), .. } => {
1226                let selector = input.parse()?;
1227                Ok(SimpleSelector::PseudoElement(input.alloc(selector)))
1228            }
1229            TokenWithSpan {
1230                token:
1231                    Token::Ident(..)
1232                    | Token::Asterisk(..)
1233                    | Token::HashLBrace(..)
1234                    | Token::Bar(..)
1235                    | Token::AtLBraceVar(..),
1236                ..
1237            } => input.parse().map(SimpleSelector::Type),
1238            TokenWithSpan { token: Token::Ampersand(..), .. } => {
1239                input.parse().map(SimpleSelector::Nesting)
1240            }
1241            // Css too: postcss-extend-rule uses Sass-style placeholders in
1242            // plain CSS (`%thick-border {}` + `@extend %thick-border;`), and
1243            // postcss parses `%x` as an ordinary selector. A selector-position
1244            // `%` is invalid per spec, so accepting it is purely additive.
1245            TokenWithSpan { token: Token::Percent(..), .. }
1246                if matches!(input.syntax, Syntax::Scss | Syntax::Sass | Syntax::Css) =>
1247            {
1248                input.parse().map(SimpleSelector::SassPlaceholder)
1249            }
1250            TokenWithSpan { token: Token::Placeholder(..), .. } => {
1251                let name = input.parse::<InterpolableIdent>()?;
1252                let span = *name.span();
1253                Ok(SimpleSelector::Type(TypeSelector::TagName(TagNameSelector {
1254                    name: WqName { name, prefix: None, span },
1255                    span,
1256                })))
1257            }
1258            token_with_span => {
1259                Err(Error { kind: ErrorKind::ExpectSimpleSelector, span: token_with_span.span })
1260            }
1261        }
1262    }
1263}
1264
1265// <type-selector> = <wq-name> | <ns-prefix>? '*'
1266// <wq-name>       = <ns-prefix>? <ident-token>
1267// <ns-prefix>     = [ <ident-token> | '*' ]? '|'
1268impl<'a> Parse<'a> for TypeSelector<'a> {
1269    fn parse(input: &mut Parser<'a>) -> PResult<Self> {
1270        enum IdentOrAsterisk<'a> {
1271            Ident(InterpolableIdent<'a>),
1272            Asterisk(Span),
1273        }
1274
1275        let ident_or_asterisk = match &input.cursor.peek()?.token {
1276            Token::Ident(..) | Token::HashLBrace(..) | Token::AtLBraceVar(..) => {
1277                input.parse().map(IdentOrAsterisk::Ident).map(Some)?
1278            }
1279            Token::Asterisk(..) => Some(IdentOrAsterisk::Asterisk(input.cursor.bump()?.span)),
1280            Token::Bar(..) => None,
1281            _ => unreachable!(),
1282        };
1283
1284        match input.cursor.peek()? {
1285            TokenWithSpan { token: Token::Bar(..), span }
1286                if ident_or_asterisk
1287                    .as_ref()
1288                    .map(|t| match t {
1289                        IdentOrAsterisk::Ident(ident) => {
1290                            !util::has_ws(input.source, ident.span().end, span.start)
1291                        }
1292                        IdentOrAsterisk::Asterisk(asterisk_span) => {
1293                            !util::has_ws(input.source, asterisk_span.end, span.start)
1294                        }
1295                    })
1296                    .unwrap_or(true) =>
1297            {
1298                let bar_token_span = input.cursor.bump()?.span;
1299
1300                let prefix = match ident_or_asterisk {
1301                    Some(IdentOrAsterisk::Ident(ident)) => {
1302                        let mut span = *ident.span();
1303                        span.end = bar_token_span.end;
1304                        NsPrefix { kind: Some(NsPrefixKind::Ident(ident)), span }
1305                    }
1306                    Some(IdentOrAsterisk::Asterisk(asterisk_span)) => {
1307                        let mut span = asterisk_span;
1308                        span.end = bar_token_span.end;
1309                        NsPrefix {
1310                            kind: Some(NsPrefixKind::Universal(NsPrefixUniversal {
1311                                span: asterisk_span,
1312                            })),
1313                            span,
1314                        }
1315                    }
1316                    None => NsPrefix { kind: None, span: bar_token_span },
1317                };
1318
1319                match input.cursor.peek()? {
1320                    TokenWithSpan { token: Token::Ident(..) | Token::HashLBrace(..), .. } => {
1321                        let name = input.parse::<InterpolableIdent>()?;
1322                        let name_span = name.span();
1323                        util::assert_no_ws(input.source, &prefix.span, name_span)?;
1324                        let span = Span { start: prefix.span.start, end: name_span.end };
1325                        Ok(TypeSelector::TagName(TagNameSelector {
1326                            name: WqName { name, prefix: Some(prefix), span },
1327                            span,
1328                        }))
1329                    }
1330                    TokenWithSpan { token: Token::Asterisk(..), .. } => {
1331                        let asterisk_span = input.cursor.bump()?.span;
1332                        util::assert_no_ws(input.source, &prefix.span, &asterisk_span)?;
1333                        let span = Span { start: prefix.span.start, end: asterisk_span.end };
1334                        Ok(TypeSelector::Universal(UniversalSelector {
1335                            prefix: Some(prefix),
1336                            span,
1337                        }))
1338                    }
1339                    TokenWithSpan { span, .. } => {
1340                        Err(Error { kind: ErrorKind::ExpectTypeSelector, span: *span })
1341                    }
1342                }
1343            }
1344
1345            _ => match ident_or_asterisk {
1346                Some(IdentOrAsterisk::Ident(ident)) => {
1347                    let span = *ident.span();
1348                    Ok(TypeSelector::TagName(TagNameSelector {
1349                        name: WqName { name: ident, prefix: None, span },
1350                        span,
1351                    }))
1352                }
1353                Some(IdentOrAsterisk::Asterisk(span)) => {
1354                    Ok(TypeSelector::Universal(UniversalSelector { prefix: None, span }))
1355                }
1356                None => unreachable!(),
1357            },
1358        }
1359    }
1360}
1361
1362impl<'a> Parser<'a> {
1363    // <combinator> = '>' | '+' | '~' | [ '|' '|' ]
1364    // An absent combinator between two compounds is the descendant combinator
1365    // (whitespace), which this returns as `CombinatorKind::Descendant`.
1366    fn parse_combinator(&mut self, pos: usize) -> PResult<Option<Combinator>> {
1367        match self.cursor.peek()? {
1368            TokenWithSpan {
1369                token:
1370                    Token::Ident(..)
1371                    | Token::Dot(..)
1372                    | Token::Hash(..)
1373                    | Token::Colon(..)
1374                    | Token::ColonColon(..)
1375                    | Token::LBracket(..)
1376                    | Token::Asterisk(..)
1377                    | Token::Ampersand(..)
1378                    | Token::Bar(..) // selector like `|type` (with <ns-prefix>)
1379                    | Token::AtLBraceVar(..)
1380                    | Token::NumberSign(..)
1381                    | Token::HashLBrace(..)
1382                    | Token::Percent(..) // Sass `%placeholder` descendant
1383                    | Token::Placeholder(..), // `${a} ${b}` descendant
1384                span,
1385            } if pos < span.start => Ok(Some(Combinator {
1386                kind: CombinatorKind::Descendant,
1387                span: Span {
1388                    start: pos,
1389                    end: span.start,
1390                },
1391            })),
1392            TokenWithSpan {
1393                token: Token::GreaterThan(..),
1394                ..
1395            } => Ok(Some(Combinator {
1396                kind: CombinatorKind::Child,
1397                span: self.cursor.bump()?.span,
1398            })),
1399            TokenWithSpan {
1400                token: Token::Plus(..),
1401                ..
1402            } => Ok(Some(Combinator {
1403                kind: CombinatorKind::NextSibling,
1404                span: self.cursor.bump()?.span,
1405            })),
1406            TokenWithSpan {
1407                token: Token::Tilde(..),
1408                ..
1409            } => Ok(Some(Combinator {
1410                kind: CombinatorKind::LaterSibling,
1411                span: self.cursor.bump()?.span,
1412            })),
1413            TokenWithSpan {
1414                token: Token::BarBar(..),
1415                ..
1416            } => Ok(Some(Combinator {
1417                kind: CombinatorKind::Column,
1418                span: self.cursor.bump()?.span,
1419            })),
1420            // deprecated shadow-piercing `/deep/` and less.js's arbitrary
1421            // slashed combinators (`.container /shadow/ .content`) — but not
1422            // in Scss/Sass, where dart-sass rejects reference combinators
1423            TokenWithSpan { token: Token::Solidus(..), .. }
1424                if !matches!(self.syntax, Syntax::Scss | Syntax::Sass) =>
1425            {
1426                let deep = self.try_parse(|p| {
1427                    let start = p.cursor.bump()?.span; // `/`
1428                    let ident_end = match p.cursor.peek()? {
1429                        TokenWithSpan { token: Token::Ident(..), span }
1430                            if span.start == start.end =>
1431                        {
1432                            p.cursor.bump()?.span.end
1433                        }
1434                        TokenWithSpan { span, .. } => {
1435                            return Err(Error {
1436                                kind: ErrorKind::TryParseError,
1437                                span: *span,
1438                            });
1439                        }
1440                    };
1441                    match p.cursor.peek()? {
1442                        TokenWithSpan { token: Token::Solidus(..), span }
1443                            if span.start == ident_end =>
1444                        {
1445                            let end = p.cursor.bump()?.span.end;
1446                            Ok(Span { start: start.start, end })
1447                        }
1448                        TokenWithSpan { span, .. } => Err(Error {
1449                            kind: ErrorKind::TryParseError,
1450                            span: *span,
1451                        }),
1452                    }
1453                });
1454                match deep {
1455                    Ok(span) => Ok(Some(Combinator { kind: CombinatorKind::Deep, span })),
1456                    Err(_) => Ok(None),
1457                }
1458            }
1459            // deprecated shadow combinators `^` and `^^` (Less corpora and
1460            // the CSS files Less emits)
1461            TokenWithSpan {
1462                token: Token::Unknown(..),
1463                span,
1464            } if !matches!(self.syntax, Syntax::Scss | Syntax::Sass)
1465                && self.source.as_bytes().get(span.start) == Some(&b'^') =>
1466            {
1467                let start = self.cursor.bump()?.span.start;
1468                if matches!(&self.cursor.peek()?.token, Token::Unknown(..))
1469                    && self.cursor.peek()?.span.start == start + 1
1470                    && self.source.as_bytes().get(start + 1) == Some(&b'^')
1471                {
1472                    let end = self.cursor.bump()?.span.end;
1473                    Ok(Some(Combinator {
1474                        kind: CombinatorKind::ShadowDescendant,
1475                        span: Span { start, end },
1476                    }))
1477                } else {
1478                    Ok(Some(Combinator {
1479                        kind: CombinatorKind::ShadowChild,
1480                        span: Span { start, end: start + 1 },
1481                    }))
1482                }
1483            }
1484            _ => Ok(None),
1485        }
1486    }
1487}
1488
1489fn expect_unsigned_int<'a>(input: &mut Parser<'a>) -> PResult<(token::Number<'a>, Span)> {
1490    let (number, span) = input.cursor.expect_number()?;
1491    if number.raw.chars().any(|c| !c.is_ascii_digit()) {
1492        Err(Error { kind: ErrorKind::ExpectUnsignedInteger, span })
1493    } else {
1494        Ok((number, span))
1495    }
1496}
1497
1498#[cfg(test)]
1499mod tests {
1500    use crate::{Allocator, Parser, Syntax, ast::AnPlusB};
1501
1502    /// The dash-digit An+B forms compute the same `a`/`b` regardless of case;
1503    /// the ast fixtures only pin acceptance, so pin the values here.
1504    #[test]
1505    fn an_plus_b_dash_digit_case_insensitive() {
1506        for (source, a, b) in [
1507            ("2n-1", 2, -1),
1508            ("2N-1", 2, -1),
1509            ("n-1", 1, -1),
1510            ("N-1", 1, -1),
1511            ("+n-1", 1, -1),
1512            ("+N-1", 1, -1),
1513            ("-n-1", -1, -1),
1514            ("-N-1", -1, -1),
1515        ] {
1516            let allocator = Allocator::default();
1517            let nth = Parser::new(&allocator, source, Syntax::Css).parse::<AnPlusB>().unwrap();
1518            assert_eq!((nth.a, nth.b), (a, b), "{source}");
1519        }
1520    }
1521}