Skip to main content

css_module_lexer/
lexer.rs

1use crate::Range;
2use crate::dependencies::PropertyKind;
3use crate::dependencies::special_value_is_candidate;
4use rustc_hash::FxHashSet;
5use smallvec::SmallVec;
6use std::collections::VecDeque;
7use std::str;
8
9const fn build_plain_ascii_name_byte_table() -> [bool; 256] {
10    let mut table = [false; 256];
11    let mut byte = 0usize;
12    while byte < table.len() {
13        table[byte] = (byte >= b'0' as usize && byte <= b'9' as usize)
14            || (byte >= b'A' as usize && byte <= b'Z' as usize)
15            || (byte >= b'a' as usize && byte <= b'z' as usize)
16            || byte == b'_' as usize
17            || byte == b'-' as usize;
18        byte += 1;
19    }
20    table
21}
22
23const PLAIN_ASCII_NAME_BYTE: [bool; 256] = build_plain_ascii_name_byte_table();
24
25pub const C_LINE_FEED: u8 = b'\n';
26pub const C_CARRIAGE_RETURN: u8 = b'\r';
27pub const C_FORM_FEED: u8 = b'\x0c';
28
29pub const C_TAB: u8 = b'\t';
30pub const C_SPACE: u8 = b' ';
31
32pub const C_SOLIDUS: u8 = b'/';
33pub const C_REVERSE_SOLIDUS: u8 = b'\\';
34pub const C_ASTERISK: u8 = b'*';
35
36pub const C_LEFT_PARENTHESIS: u8 = b'(';
37pub const C_RIGHT_PARENTHESIS: u8 = b')';
38pub const C_LEFT_CURLY: u8 = b'{';
39pub const C_RIGHT_CURLY: u8 = b'}';
40pub const C_LEFT_SQUARE: u8 = b'[';
41pub const C_RIGHT_SQUARE: u8 = b']';
42
43pub const C_QUOTATION_MARK: u8 = b'"';
44pub const C_APOSTROPHE: u8 = b'\'';
45
46pub const C_FULL_STOP: u8 = b'.';
47pub const C_COLON: u8 = b':';
48pub const C_SEMICOLON: u8 = b';';
49pub const C_COMMA: u8 = b',';
50pub const C_PERCENTAGE: u8 = b'%';
51pub const C_AT_SIGN: u8 = b'@';
52
53pub const C_LOW_LINE: u8 = b'_';
54pub const C_LOWER_E: u8 = b'e';
55pub const C_UPPER_E: u8 = b'E';
56
57pub const C_NUMBER_SIGN: u8 = b'#';
58pub const C_PLUS_SIGN: u8 = b'+';
59pub const C_HYPHEN_MINUS: u8 = b'-';
60
61pub type Pos = u32;
62
63/// The lexical kind of a CSS token.
64///
65/// Tokens retain only byte ranges into the original source.  Comments are
66/// emitted deliberately so that CSS Modules magic comments can be interpreted
67/// without a second scan of the input.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum TokenKind {
70    Eof,
71    Ident,
72    AtKeyword,
73    Hash,
74    IdHash,
75    QuotedString,
76    Url,
77    Function,
78    Number,
79    Percentage,
80    Dimension,
81    WhiteSpace,
82    Comment,
83    BadComment,
84    BadString,
85    BadUrl,
86    Delim,
87    Colon,
88    Semicolon,
89    Comma,
90    LeftParenthesis,
91    RightParenthesis,
92    LeftSquareBracket,
93    RightSquareBracket,
94    LeftCurlyBracket,
95    RightCurlyBracket,
96    IncludeMatch,
97    DashMatch,
98    PrefixMatch,
99    SuffixMatch,
100    SubstringMatch,
101}
102
103impl TokenKind {
104    #[inline]
105    pub fn is_trivia(self) -> bool {
106        matches!(self, Self::WhiteSpace | Self::Comment)
107    }
108
109    #[inline]
110    pub fn is_scan_error(self) -> bool {
111        matches!(self, Self::BadComment | Self::BadString | Self::BadUrl)
112    }
113}
114
115/// A CSS token represented by ranges into the input string.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub struct Token {
118    pub kind: TokenKind,
119    pub range: Range,
120    pub value_range: Range,
121    pub flags: TokenFlags,
122}
123
124impl Token {
125    #[inline]
126    pub const fn new(kind: TokenKind, range: Range, value_range: Range) -> Self {
127        Self::with_flags(kind, range, value_range, TokenFlags::ascii())
128    }
129
130    #[inline]
131    pub const fn with_flags(
132        kind: TokenKind,
133        range: Range,
134        value_range: Range,
135        flags: TokenFlags,
136    ) -> Self {
137        Self {
138            kind,
139            range,
140            value_range,
141            flags,
142        }
143    }
144}
145
146/// Properties collected while scanning a token. They let dependency parsing
147/// skip escape/null checks for the overwhelmingly common plain-ASCII path.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub struct TokenFlags(u8);
150
151impl TokenFlags {
152    pub const HAS_ESCAPE: Self = Self(1 << 0);
153    pub const HAS_NULL: Self = Self(1 << 1);
154    pub const IS_ASCII: Self = Self(1 << 2);
155
156    #[inline]
157    pub const fn ascii() -> Self {
158        Self::IS_ASCII
159    }
160
161    #[inline]
162    pub const fn has_escape(self) -> bool {
163        self.0 & Self::HAS_ESCAPE.0 != 0
164    }
165
166    #[inline]
167    pub const fn has_null(self) -> bool {
168        self.0 & Self::HAS_NULL.0 != 0
169    }
170
171    #[inline]
172    pub const fn is_ascii(self) -> bool {
173        self.0 & Self::IS_ASCII.0 != 0
174    }
175
176    #[inline]
177    fn mark_escape(&mut self) {
178        self.0 |= Self::HAS_ESCAPE.0;
179    }
180
181    #[inline]
182    fn mark_null(&mut self) {
183        self.0 |= Self::HAS_NULL.0;
184    }
185
186    #[inline]
187    fn mark_non_ascii(&mut self) {
188        self.0 &= !Self::IS_ASCII.0;
189    }
190
191    #[inline]
192    fn merge(&mut self, other: Self) {
193        self.0 |= other.0;
194    }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198pub struct Trivia {
199    pub range: Range,
200    pub end: Pos,
201    pub first_comment_start: Option<Pos>,
202    pub has_white_space: bool,
203}
204
205impl Trivia {
206    #[inline]
207    pub fn has_whitespace(self) -> bool {
208        self.has_white_space
209    }
210}
211
212/// A significant token together with all immediately preceding whitespace and
213/// comments.  The leading trivia is consumed once and never rescanned.
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub struct TokenWithTrivia {
216    pub token: Token,
217    pub leading: Trivia,
218}
219
220#[derive(Debug)]
221pub struct Lexer<'s> {
222    value: &'s [u8],
223    scan_pos: Pos,
224}
225
226impl<'s> Lexer<'s> {
227    pub fn new(value: &'s str) -> Self {
228        assert!(value.len() <= Pos::MAX as usize, "CSS input is too large");
229        Self {
230            value: value.as_bytes(),
231            scan_pos: 0,
232        }
233    }
234
235    #[inline]
236    pub(crate) fn source_end(&self) -> Pos {
237        self.value.len() as Pos
238    }
239
240    #[inline(always)]
241    pub(crate) fn byte_at(&self, position: Pos) -> Option<u8> {
242        self.value.get(position as usize).copied()
243    }
244
245    #[inline(always)]
246    pub(crate) fn could_start_ident_at(&self, position: Pos) -> bool {
247        self.byte_at(position).is_some_and(|byte| {
248            matches!(byte, C_HYPHEN_MINUS | C_REVERSE_SOLIDUS) || is_name_start_byte(byte)
249        })
250    }
251
252    /// Skip value text that cannot produce a dependency or change the block
253    /// structure. Delimiters of ordinary functions are tracked without
254    /// manufacturing tokens, and the scanner stops before dependency-bearing
255    /// functions, ICSS symbols, selector-independent magic comments, or a
256    /// declaration boundary.
257    fn fast_forward_generic_value<const STOP_AT_TOP_LEVEL_LEFT_CURLY: bool, F, C>(
258        &mut self,
259        state: &mut GenericValueScanState,
260        options: GenericValueScanOptions,
261        mut is_candidate: F,
262        mut is_comment_candidate: C,
263        icss_symbols: Option<&FxHashSet<&str>>,
264    ) -> Option<PrescannedIdent>
265    where
266        F: FnMut(&str) -> bool,
267        C: FnMut(&str) -> bool,
268    {
269        let mut position = self.scan_pos as usize;
270        let scan_start = position;
271        let mut candidate_token = None;
272        while position < self.value.len() {
273            let byte = self.value[position];
274            if is_white_space(byte) {
275                position += 1;
276                continue;
277            }
278
279            if byte == C_SOLIDUS && self.value.get(position + 1) == Some(&C_ASTERISK) {
280                let (kind, end, _, _, _) = self.scan_comment(position);
281                if options.keep_comments && kind == TokenKind::Comment {
282                    let content = &self.value[position + 2..end.saturating_sub(2)];
283                    let mut first = 0usize;
284                    while content.get(first).is_some_and(|byte| is_white_space(*byte)) {
285                        first += 1;
286                    }
287                    if content.get(first) == Some(&b'c') {
288                        let content = unsafe { str::from_utf8_unchecked(content) };
289                        if is_comment_candidate(content) {
290                            break;
291                        }
292                    }
293                }
294                position = end;
295                continue;
296            }
297
298            if matches!(byte, C_QUOTATION_MARK | C_APOSTROPHE) {
299                if options.preserve_strings {
300                    break;
301                }
302                position = self.scan_string(position, byte).1;
303                continue;
304            }
305
306            let starts_ident = match byte {
307                C_HYPHEN_MINUS | C_REVERSE_SOLIDUS => self.starts_ident_at(position),
308                _ => is_name_start_byte(byte),
309            };
310            if starts_ident {
311                let end = self.scan_plain_ascii_name(position);
312                if end == position || self.raw_name_needs_tokenizer(end) {
313                    break;
314                }
315                let flags = TokenFlags::ascii();
316                let name = unsafe { str::from_utf8_unchecked(&self.value[position..end]) };
317                let is_function = self.value.get(end) == Some(&C_LEFT_PARENTHESIS);
318                let is_candidate = !flags.has_escape()
319                    && (is_candidate(name)
320                        || options.property.is_some_and(|property| {
321                            special_value_is_candidate(property, name, icss_symbols)
322                        }));
323                if flags.has_escape()
324                    || is_candidate
325                    || (is_function && is_dependency_value_function(name))
326                    || (is_function && options.preserve_delimiters)
327                {
328                    if is_candidate && !is_function && flags.is_ascii() && !flags.has_null() {
329                        candidate_token = Some(PrescannedIdent {
330                            start: position as Pos,
331                            end: end as Pos,
332                            flags,
333                        });
334                        position = end;
335                    }
336                    break;
337                }
338                if is_function {
339                    state.parentheses += 1;
340                    position = end + 1;
341                } else {
342                    position = end;
343                }
344                continue;
345            }
346
347            let starts_number = match byte {
348                b'0'..=b'9' => true,
349                C_PLUS_SIGN | C_HYPHEN_MINUS | C_FULL_STOP => self.starts_number_at(position),
350                _ => false,
351            };
352            if starts_number {
353                let Some(end) = self.scan_raw_numeric_end(position) else {
354                    break;
355                };
356                position = end;
357                continue;
358            }
359
360            match byte {
361                C_NUMBER_SIGN => {
362                    let name_start = position + 1;
363                    if self.starts_ident_at(name_start)
364                        || self
365                            .value
366                            .get(name_start)
367                            .is_some_and(|byte| is_digit(*byte) || *byte == C_HYPHEN_MINUS)
368                    {
369                        let end = self.scan_plain_ascii_name(name_start);
370                        if end == name_start || self.raw_name_needs_tokenizer(end) {
371                            break;
372                        }
373                        position = end;
374                    } else {
375                        position = name_start;
376                    }
377                }
378                C_LEFT_PARENTHESIS => {
379                    if options.preserve_delimiters {
380                        break;
381                    }
382                    state.parentheses += 1;
383                    position += 1;
384                }
385                C_RIGHT_PARENTHESIS => {
386                    if options.preserve_delimiters {
387                        break;
388                    }
389                    state.parentheses = state.parentheses.saturating_sub(1);
390                    position += 1;
391                }
392                C_LEFT_SQUARE => {
393                    if options.preserve_delimiters {
394                        break;
395                    }
396                    state.squares += 1;
397                    position += 1;
398                }
399                C_RIGHT_SQUARE => {
400                    if options.preserve_delimiters {
401                        break;
402                    }
403                    state.squares = state.squares.saturating_sub(1);
404                    position += 1;
405                }
406                C_LEFT_CURLY if STOP_AT_TOP_LEVEL_LEFT_CURLY && !state.is_nested() => break,
407                C_LEFT_CURLY => {
408                    if options.preserve_delimiters {
409                        break;
410                    }
411                    state.curlies += 1;
412                    position += 1;
413                }
414                C_RIGHT_CURLY if state.curlies > 0 && !options.preserve_delimiters => {
415                    state.curlies -= 1;
416                    position += 1;
417                }
418                C_RIGHT_CURLY => break,
419                C_SEMICOLON if !state.is_nested() => break,
420                _ => position += 1,
421            }
422        }
423        self.scan_pos = position as Pos;
424        debug_assert!(
425            self.scan_pos >= scan_start as Pos,
426            "fast_forward moved scan_pos backward from {scan_start} to {position}"
427        );
428        candidate_token
429    }
430
431    /// Skip selector text that cannot produce a CSS Modules dependency.
432    /// Attribute selectors remain opaque across calls through `square_depth`;
433    /// the scanner stops before dependency candidates and structural tokens so
434    /// the normal token stream remains the sole owner of parser state changes.
435    pub(crate) fn fast_forward_selector<C>(
436        &mut self,
437        square_depth: &mut u32,
438        keep_comments: bool,
439        has_mode: bool,
440        mut is_comment_candidate: C,
441    ) -> bool
442    where
443        C: FnMut(&str) -> bool,
444    {
445        let mut position = self.scan_pos as usize;
446        let scan_start = position;
447        let mut invalidates_composes = false;
448        let mut trivia_start = None;
449
450        while position < self.value.len() {
451            let byte = self.value[position];
452
453            if is_white_space(byte) {
454                trivia_start.get_or_insert(position);
455                position += 1;
456                continue;
457            }
458
459            if byte == C_SOLIDUS && self.value.get(position + 1) == Some(&C_ASTERISK) {
460                let (kind, end, _, _, _) = self.scan_comment(position);
461                if keep_comments && kind == TokenKind::Comment {
462                    let content = &self.value[position + 2..end.saturating_sub(2)];
463                    // SAFETY: comments are slices of the original UTF-8 input.
464                    let content = unsafe { str::from_utf8_unchecked(content) };
465                    if is_comment_candidate(content) {
466                        break;
467                    }
468                }
469                trivia_start.get_or_insert(position);
470                position = end;
471                continue;
472            }
473
474            if *square_depth > 0 {
475                match byte {
476                    C_QUOTATION_MARK | C_APOSTROPHE => {
477                        position = self.scan_string(position, byte).1;
478                    }
479                    C_LEFT_SQUARE => {
480                        *square_depth += 1;
481                        position += 1;
482                    }
483                    C_RIGHT_SQUARE => {
484                        *square_depth -= 1;
485                        position += 1;
486                    }
487                    _ => position += 1,
488                }
489                trivia_start = None;
490                continue;
491            }
492
493            let starts_ident = match byte {
494                C_HYPHEN_MINUS | C_REVERSE_SOLIDUS => self.starts_ident_at(position),
495                _ => is_name_start_byte(byte),
496            };
497            if starts_ident {
498                let end = self.scan_name(position);
499                if end == position {
500                    break;
501                }
502                // Functions own balanced-stack state in the dependency
503                // scanner. In particular, `url()` and `image-set()` can
504                // produce dependencies even when invalid CSS made them
505                // appear while recovering from a selector. Leave the name
506                // to the regular tokenizer so it can emit `Function`.
507                if self.value.get(end) == Some(&C_LEFT_PARENTHESIS) {
508                    break;
509                }
510                invalidates_composes = true;
511                trivia_start = None;
512                position = end;
513                continue;
514            }
515
516            let starts_number = match byte {
517                b'0'..=b'9' => true,
518                C_PLUS_SIGN | C_HYPHEN_MINUS | C_FULL_STOP => self.starts_number_at(position),
519                _ => false,
520            };
521            if starts_number {
522                invalidates_composes = true;
523                trivia_start = None;
524                position = self.scan_numeric(position).1;
525                continue;
526            }
527
528            match byte {
529                C_LEFT_SQUARE => {
530                    invalidates_composes = true;
531                    *square_depth = 1;
532                    trivia_start = None;
533                    position += 1;
534                }
535                // Outside an attribute selector a string may belong to
536                // `url()` or `image-set()`, whose balanced-stack kind decides
537                // whether it creates a dependency.
538                C_QUOTATION_MARK | C_APOSTROPHE => break,
539                C_FULL_STOP | C_NUMBER_SIGN | C_COLON if has_mode => break,
540                C_NUMBER_SIGN => {
541                    let name_start = position + 1;
542                    position = if self.starts_ident_at(name_start)
543                        || self
544                            .value
545                            .get(name_start)
546                            .is_some_and(|byte| is_digit(*byte) || *byte == C_HYPHEN_MINUS)
547                    {
548                        self.scan_name(name_start)
549                    } else {
550                        name_start
551                    };
552                    invalidates_composes = true;
553                    trivia_start = None;
554                }
555                C_RIGHT_PARENTHESIS => {
556                    position = trivia_start.unwrap_or(position);
557                    break;
558                }
559                C_LEFT_PARENTHESIS | C_LEFT_CURLY | C_RIGHT_CURLY | C_COMMA | C_SEMICOLON
560                | C_AT_SIGN => break,
561                _ => {
562                    invalidates_composes = true;
563                    trivia_start = None;
564                    position += 1;
565                }
566            }
567        }
568
569        self.scan_pos = position as Pos;
570        debug_assert!(
571            self.scan_pos >= scan_start as Pos,
572            "selector fast-forward moved scan_pos backward from {scan_start} to {position}"
573        );
574        invalidates_composes
575    }
576
577    /// Skip a balanced delimiter run whose opening token has already been
578    /// consumed. Only `RightParenthesis`, `RightSquareBracket`, and
579    /// `RightCurlyBracket` are accepted as `end`.
580    ///
581    /// Returns the range from the opening token through the matching closing
582    /// token on success. The caller is responsible for passing the position
583    /// just past the opening token.
584    ///
585    /// Nested delimiters are tracked on a local stack. Strings, comments,
586    /// escapes, and non-ASCII code points are skipped without tokenizing so
587    /// their content can never close the run. The scan is transactional: on
588    /// failure (EOF or an unbalanced closing token) `scan_pos` is left
589    /// unchanged and `None` is returned so the caller can fall back to the
590    /// regular tokenizer, which remains the sole owner of parser state
591    /// changes.
592    pub(crate) fn fast_forward(&mut self, end: TokenKind) -> Option<Range> {
593        match end {
594            TokenKind::RightParenthesis
595            | TokenKind::RightSquareBracket
596            | TokenKind::RightCurlyBracket => {}
597            _ => unreachable!("fast_forward accepts only closing bracket kinds"),
598        }
599        let open_start = self.scan_pos;
600        let mut stack: SmallVec<[TokenKind; 8]> = SmallVec::new();
601        stack.push(end);
602        let mut position = open_start as usize;
603        let bytes = self.value;
604        while position < bytes.len() {
605            let byte = bytes[position];
606            match byte {
607                C_QUOTATION_MARK | C_APOSTROPHE => {
608                    let (kind, string_end, _, _, _) = self.scan_string(position, byte);
609                    if kind == TokenKind::BadString {
610                        return None;
611                    }
612                    position = string_end;
613                }
614                C_SOLIDUS if bytes.get(position + 1) == Some(&C_ASTERISK) => {
615                    let (kind, comment_end, _, _, _) = self.scan_comment(position);
616                    if kind == TokenKind::BadComment {
617                        return None;
618                    }
619                    position = comment_end;
620                }
621                C_REVERSE_SOLIDUS if self.is_valid_escape_at(position) => {
622                    position = self.scan_escape(position);
623                }
624                C_LEFT_PARENTHESIS => {
625                    stack.push(TokenKind::RightParenthesis);
626                    position += 1;
627                }
628                C_LEFT_SQUARE => {
629                    stack.push(TokenKind::RightSquareBracket);
630                    position += 1;
631                }
632                C_LEFT_CURLY => {
633                    stack.push(TokenKind::RightCurlyBracket);
634                    position += 1;
635                }
636                C_RIGHT_PARENTHESIS | C_RIGHT_SQUARE | C_RIGHT_CURLY => {
637                    if *stack.last().expect("delimiter stack never empties") != kind_to_right(byte)
638                    {
639                        return None;
640                    }
641                    stack.pop();
642                    position += 1;
643                    if stack.is_empty() {
644                        self.scan_pos = position as Pos;
645                        debug_assert!(
646                            self.scan_pos >= open_start as Pos,
647                            "fast_forward moved scan_pos backward from {open_start} to {position}"
648                        );
649                        return Some(Range::new(open_start, position as Pos));
650                    }
651                }
652                // A semicolon ends an at-rule prelude regardless of nesting
653                // depth. Bail so the regular tokenizer resumes and the
654                // dependency scanner completes the at-rule at the semicolon.
655                C_SEMICOLON => return None,
656                _ if byte >= 0x80 => {
657                    position += self.utf8_width_at(position);
658                }
659                _ => position += 1,
660            }
661        }
662        None
663    }
664
665    pub(crate) fn slice(&self, start: Pos, end: Pos) -> Option<&'s str> {
666        let range = Range::new(start, end);
667        let start = start as usize;
668        let end = end as usize;
669        if start > end || end > self.value.len() {
670            return None;
671        }
672        // SAFETY: Lexer-generated positions always delimit complete tokens or
673        // source fragments and therefore stay on UTF-8 code point boundaries.
674        Some(unsafe { Self::slice_unchecked(self.value, &range) })
675    }
676
677    #[inline(always)]
678    pub(crate) fn slice_trusted(&self, start: Pos, end: Pos) -> &'s str {
679        debug_assert!(start <= end && end <= self.value.len() as Pos);
680        // SAFETY: Token ranges and parser positions originate from this
681        // lexer, so they delimit valid UTF-8 source boundaries.
682        unsafe { Self::slice_unchecked(self.value, &Range::new(start, end)) }
683    }
684
685    pub fn slice_range<'a>(input: &'a str, range: &Range) -> Option<&'a str> {
686        let start = range.start as usize;
687        let end = range.end as usize;
688        if start > end
689            || end > input.len()
690            || !input.is_char_boundary(start)
691            || !input.is_char_boundary(end)
692        {
693            return None;
694        }
695        // SAFETY: The range was checked against the original `str` above.
696        Some(unsafe { Self::slice_unchecked(input.as_bytes(), range) })
697    }
698
699    unsafe fn slice_unchecked<'a>(input: &'a [u8], range: &Range) -> &'a str {
700        unsafe {
701            let value = input.get_unchecked(range.start as usize..range.end as usize);
702            str::from_utf8_unchecked(value)
703        }
704    }
705}
706
707impl Lexer<'_> {
708    #[inline]
709    pub(crate) fn scan_pos(&self) -> Pos {
710        self.scan_pos
711    }
712}
713
714impl<'s> Lexer<'s> {
715    /// Return the next CSS token without allocating or decoding its value.
716    ///
717    /// `TokenKind::Eof` is returned after the input is exhausted. Unterminated
718    /// comments, strings, and URLs are represented by the corresponding
719    /// `Bad*` token kinds, so EOF and tokenizer errors are never conflated.
720    #[inline]
721    pub fn next_token(&mut self) -> Token {
722        let start = self.scan_pos as usize;
723        let len = self.value.len();
724        if start >= len {
725            self.scan_pos = len as Pos;
726            return Token::new(
727                TokenKind::Eof,
728                Range::new(len as Pos, len as Pos),
729                Range::new(len as Pos, len as Pos),
730            );
731        }
732
733        let byte = self.value[start];
734        let (kind, end, value_start, value_end, flags) = match byte {
735            C_SPACE | C_TAB | C_LINE_FEED | C_CARRIAGE_RETURN | C_FORM_FEED => {
736                self.scan_whitespace(start)
737            }
738            C_QUOTATION_MARK => self.scan_string(start, C_QUOTATION_MARK),
739            C_APOSTROPHE => self.scan_string(start, C_APOSTROPHE),
740            C_NUMBER_SIGN => {
741                let value_start = start + 1;
742                if self.starts_ident_at(value_start) {
743                    let (end, flags) = self.scan_name_with_flags(value_start);
744                    (TokenKind::IdHash, end, value_start, end, flags)
745                } else if self
746                    .value
747                    .get(value_start)
748                    .is_some_and(|byte| is_digit(*byte) || *byte == C_HYPHEN_MINUS)
749                {
750                    let (end, flags) = self.scan_name_with_flags(value_start);
751                    (TokenKind::Hash, end, value_start, end, flags)
752                } else {
753                    (
754                        TokenKind::Delim,
755                        start + 1,
756                        start,
757                        start + 1,
758                        TokenFlags::ascii(),
759                    )
760                }
761            }
762            C_LEFT_PARENTHESIS => (
763                TokenKind::LeftParenthesis,
764                start + 1,
765                start,
766                start + 1,
767                TokenFlags::ascii(),
768            ),
769            C_RIGHT_PARENTHESIS => (
770                TokenKind::RightParenthesis,
771                start + 1,
772                start,
773                start + 1,
774                TokenFlags::ascii(),
775            ),
776            C_LEFT_SQUARE => (
777                TokenKind::LeftSquareBracket,
778                start + 1,
779                start,
780                start + 1,
781                TokenFlags::ascii(),
782            ),
783            C_RIGHT_SQUARE => (
784                TokenKind::RightSquareBracket,
785                start + 1,
786                start,
787                start + 1,
788                TokenFlags::ascii(),
789            ),
790            C_LEFT_CURLY => (
791                TokenKind::LeftCurlyBracket,
792                start + 1,
793                start,
794                start + 1,
795                TokenFlags::ascii(),
796            ),
797            C_RIGHT_CURLY => (
798                TokenKind::RightCurlyBracket,
799                start + 1,
800                start,
801                start + 1,
802                TokenFlags::ascii(),
803            ),
804            C_COLON => (
805                TokenKind::Colon,
806                start + 1,
807                start,
808                start + 1,
809                TokenFlags::ascii(),
810            ),
811            C_SEMICOLON => (
812                TokenKind::Semicolon,
813                start + 1,
814                start,
815                start + 1,
816                TokenFlags::ascii(),
817            ),
818            C_COMMA => (
819                TokenKind::Comma,
820                start + 1,
821                start,
822                start + 1,
823                TokenFlags::ascii(),
824            ),
825            C_PLUS_SIGN => {
826                if self.starts_number_at(start) {
827                    self.scan_numeric(start)
828                } else {
829                    (
830                        TokenKind::Delim,
831                        start + 1,
832                        start,
833                        start + 1,
834                        TokenFlags::ascii(),
835                    )
836                }
837            }
838            C_HYPHEN_MINUS => {
839                if self.starts_number_at(start) {
840                    self.scan_numeric(start)
841                } else if self.starts_ident_at(start) {
842                    self.scan_ident_like(start)
843                } else {
844                    (
845                        TokenKind::Delim,
846                        start + 1,
847                        start,
848                        start + 1,
849                        TokenFlags::ascii(),
850                    )
851                }
852            }
853            C_FULL_STOP => {
854                if self.starts_number_at(start) {
855                    self.scan_numeric(start)
856                } else {
857                    (
858                        TokenKind::Delim,
859                        start + 1,
860                        start,
861                        start + 1,
862                        TokenFlags::ascii(),
863                    )
864                }
865            }
866            C_SOLIDUS => {
867                if self.value.get(start + 1) == Some(&C_ASTERISK) {
868                    self.scan_comment(start)
869                } else {
870                    (
871                        TokenKind::Delim,
872                        start + 1,
873                        start,
874                        start + 1,
875                        TokenFlags::ascii(),
876                    )
877                }
878            }
879            C_AT_SIGN => {
880                let value_start = start + 1;
881                if self.starts_ident_at(value_start) {
882                    let (end, flags) = self.scan_name_with_flags(value_start);
883                    (TokenKind::AtKeyword, end, value_start, end, flags)
884                } else {
885                    (
886                        TokenKind::Delim,
887                        start + 1,
888                        start,
889                        start + 1,
890                        TokenFlags::ascii(),
891                    )
892                }
893            }
894            C_REVERSE_SOLIDUS => {
895                if self.is_valid_escape_at(start) {
896                    self.scan_ident_like(start)
897                } else {
898                    (
899                        TokenKind::Delim,
900                        start + 1,
901                        start,
902                        start + 1,
903                        TokenFlags::ascii(),
904                    )
905                }
906            }
907            b'0'..=b'9' => self.scan_numeric(start),
908            b'$' if self.value[start..].starts_with(b"$=") => (
909                TokenKind::SuffixMatch,
910                start + 2,
911                start,
912                start + 2,
913                TokenFlags::ascii(),
914            ),
915            b'^' if self.value[start..].starts_with(b"^=") => (
916                TokenKind::PrefixMatch,
917                start + 2,
918                start,
919                start + 2,
920                TokenFlags::ascii(),
921            ),
922            b'|' if self.value[start..].starts_with(b"|=") => (
923                TokenKind::DashMatch,
924                start + 2,
925                start,
926                start + 2,
927                TokenFlags::ascii(),
928            ),
929            b'~' if self.value[start..].starts_with(b"~=") => (
930                TokenKind::IncludeMatch,
931                start + 2,
932                start,
933                start + 2,
934                TokenFlags::ascii(),
935            ),
936            b'*' if self.value[start..].starts_with(b"*=") => (
937                TokenKind::SubstringMatch,
938                start + 2,
939                start,
940                start + 2,
941                TokenFlags::ascii(),
942            ),
943            _ if self.starts_ident_at(start) => self.scan_ident_like(start),
944            _ => (
945                TokenKind::Delim,
946                start + 1,
947                start,
948                start + 1,
949                TokenFlags::ascii(),
950            ),
951        };
952
953        self.scan_pos = end as Pos;
954        debug_assert!(
955            self.scan_pos >= start as Pos,
956            "scan_pos moved backward from {start} to {}",
957            self.scan_pos
958        );
959        Token::with_flags(
960            kind,
961            Range::new(start as Pos, end as Pos),
962            Range::new(value_start as Pos, value_end as Pos),
963            flags,
964        )
965    }
966
967    #[inline]
968    fn scan_whitespace(&self, start: usize) -> (TokenKind, usize, usize, usize, TokenFlags) {
969        let bytes = self.value;
970        let mut end = start;
971        while end < bytes.len() && is_white_space(bytes[end]) {
972            end += 1;
973        }
974        (TokenKind::WhiteSpace, end, start, end, TokenFlags::ascii())
975    }
976
977    fn scan_comment(&self, start: usize) -> (TokenKind, usize, usize, usize, TokenFlags) {
978        let bytes = self.value;
979        let mut end = start + 2;
980        let mut flags = TokenFlags::ascii();
981        while end + 1 < bytes.len() {
982            if bytes[end] == C_ASTERISK && bytes[end + 1] == C_SOLIDUS {
983                return (TokenKind::Comment, end + 2, start + 2, end, flags);
984            }
985            if bytes[end] == 0 {
986                flags.mark_null();
987            } else if !bytes[end].is_ascii() {
988                flags.mark_non_ascii();
989            }
990            end += if bytes[end] < 0x80 {
991                1
992            } else {
993                self.utf8_width_at(end)
994            };
995        }
996        (
997            TokenKind::BadComment,
998            bytes.len(),
999            start + 2,
1000            bytes.len(),
1001            flags,
1002        )
1003    }
1004
1005    fn scan_string(&self, start: usize, quote: u8) -> (TokenKind, usize, usize, usize, TokenFlags) {
1006        let mut end = start + 1;
1007        let mut flags = TokenFlags::ascii();
1008        while end < self.value.len() {
1009            let byte = self.value[end];
1010            if byte == quote {
1011                return (TokenKind::QuotedString, end + 1, start + 1, end, flags);
1012            }
1013            if is_new_line(byte) {
1014                return (TokenKind::BadString, end, start + 1, end, flags);
1015            }
1016            if byte == 0 {
1017                flags.mark_null();
1018            } else if !byte.is_ascii() {
1019                flags.mark_non_ascii();
1020            }
1021            if byte == C_REVERSE_SOLIDUS {
1022                flags.mark_escape();
1023                if self.value.get(end + 1).is_some_and(|next| !next.is_ascii()) {
1024                    flags.mark_non_ascii();
1025                }
1026                if self
1027                    .value
1028                    .get(end + 1)
1029                    .is_some_and(|next| is_new_line(*next))
1030                {
1031                    end += 1;
1032                    if self.value.get(end) == Some(&C_CARRIAGE_RETURN)
1033                        && self.value.get(end + 1) == Some(&C_LINE_FEED)
1034                    {
1035                        end += 1;
1036                    }
1037                    end += 1;
1038                } else if self.is_valid_escape_at(end) {
1039                    end = self.scan_escape(end);
1040                } else {
1041                    end += 1;
1042                }
1043            } else {
1044                end += self.utf8_width_at(end);
1045            }
1046        }
1047        (TokenKind::BadString, end, start + 1, end, flags)
1048    }
1049
1050    fn scan_ident_like(&self, start: usize) -> (TokenKind, usize, usize, usize, TokenFlags) {
1051        let (name_end, name_flags) = self.scan_name_with_flags(start);
1052        if self.value.get(name_end) != Some(&C_LEFT_PARENTHESIS) {
1053            return (TokenKind::Ident, name_end, start, name_end, name_flags);
1054        }
1055
1056        let open_end = name_end + 1;
1057        let name = &self.value[start..name_end];
1058        let is_url = name.eq_ignore_ascii_case(b"url")
1059            || (name.contains(&C_REVERSE_SOLIDUS) && eq_css_ascii_case(name, b"url"));
1060        if is_url {
1061            let content_start = self.skip_white_space(open_end);
1062            if !matches!(
1063                self.value.get(content_start),
1064                Some(&C_QUOTATION_MARK) | Some(&C_APOSTROPHE)
1065            ) {
1066                let (kind, end, value_start, value_end, url_flags) =
1067                    self.scan_url(start, content_start);
1068                let mut flags = name_flags;
1069                flags.merge(url_flags);
1070                return (kind, end, value_start, value_end, flags);
1071            }
1072        }
1073        (TokenKind::Function, open_end, start, name_end, name_flags)
1074    }
1075
1076    fn scan_url(
1077        &self,
1078        start: usize,
1079        content_start: usize,
1080    ) -> (TokenKind, usize, usize, usize, TokenFlags) {
1081        let mut end = content_start;
1082        let mut flags = TokenFlags::ascii();
1083        while end < self.value.len() {
1084            let byte = self.value[end];
1085            if byte == C_RIGHT_PARENTHESIS {
1086                return (TokenKind::Url, end + 1, content_start, end, flags);
1087            }
1088            if is_white_space(byte) {
1089                let content_end = end;
1090                let close = self.skip_white_space(end);
1091                if self.value.get(close) == Some(&C_RIGHT_PARENTHESIS) {
1092                    return (TokenKind::Url, close + 1, content_start, content_end, flags);
1093                }
1094                return self.scan_bad_url(start, close, content_start, flags);
1095            }
1096            if byte == 0 {
1097                flags.mark_null();
1098            } else if !byte.is_ascii() {
1099                flags.mark_non_ascii();
1100            }
1101            if byte == C_QUOTATION_MARK
1102                || byte == C_APOSTROPHE
1103                || byte == C_LEFT_PARENTHESIS
1104                || is_non_printable(byte)
1105            {
1106                return self.scan_bad_url(start, end, content_start, flags);
1107            }
1108            if byte == C_REVERSE_SOLIDUS {
1109                flags.mark_escape();
1110                if self.value.get(end + 1).is_some_and(|next| !next.is_ascii()) {
1111                    flags.mark_non_ascii();
1112                }
1113                if self.is_valid_escape_at(end) {
1114                    end = self.scan_escape(end);
1115                } else if self
1116                    .value
1117                    .get(end + 1)
1118                    .is_some_and(|next| is_new_line(*next))
1119                {
1120                    end += 2;
1121                    if self.value.get(end - 1) == Some(&C_CARRIAGE_RETURN)
1122                        && self.value.get(end) == Some(&C_LINE_FEED)
1123                    {
1124                        end += 1;
1125                    }
1126                } else {
1127                    return self.scan_bad_url(start, end, content_start, flags);
1128                }
1129            } else {
1130                end += self.utf8_width_at(end);
1131            }
1132        }
1133        (
1134            TokenKind::BadUrl,
1135            self.value.len(),
1136            content_start,
1137            self.value.len(),
1138            flags,
1139        )
1140    }
1141
1142    fn scan_bad_url(
1143        &self,
1144        _start: usize,
1145        mut end: usize,
1146        content_start: usize,
1147        mut flags: TokenFlags,
1148    ) -> (TokenKind, usize, usize, usize, TokenFlags) {
1149        while end < self.value.len() {
1150            let byte = self.value[end];
1151            if byte == 0 {
1152                flags.mark_null();
1153            } else if !byte.is_ascii() {
1154                flags.mark_non_ascii();
1155            }
1156            if byte == C_RIGHT_PARENTHESIS {
1157                end += 1;
1158                break;
1159            }
1160            if byte == C_REVERSE_SOLIDUS {
1161                flags.mark_escape();
1162                if self.value.get(end + 1).is_some_and(|next| !next.is_ascii()) {
1163                    flags.mark_non_ascii();
1164                }
1165                if self.is_valid_escape_at(end) {
1166                    end = self.scan_escape(end);
1167                } else if self
1168                    .value
1169                    .get(end + 1)
1170                    .is_some_and(|next| is_new_line(*next))
1171                {
1172                    end += 2;
1173                    if self.value.get(end - 1) == Some(&C_CARRIAGE_RETURN)
1174                        && self.value.get(end) == Some(&C_LINE_FEED)
1175                    {
1176                        end += 1;
1177                    }
1178                } else {
1179                    end += 1;
1180                }
1181            } else {
1182                end += self.utf8_width_at(end);
1183            }
1184        }
1185        (TokenKind::BadUrl, end, content_start, end, flags)
1186    }
1187
1188    fn scan_numeric(&self, start: usize) -> (TokenKind, usize, usize, usize, TokenFlags) {
1189        let mut end = start;
1190        if matches!(
1191            self.value.get(end),
1192            Some(&C_PLUS_SIGN) | Some(&C_HYPHEN_MINUS)
1193        ) {
1194            end += 1;
1195        }
1196        while self.value.get(end).is_some_and(|byte| is_digit(*byte)) {
1197            end += 1;
1198        }
1199        if self.value.get(end) == Some(&C_FULL_STOP)
1200            && self.value.get(end + 1).is_some_and(|byte| is_digit(*byte))
1201        {
1202            end += 1;
1203            while self.value.get(end).is_some_and(|byte| is_digit(*byte)) {
1204                end += 1;
1205            }
1206        }
1207        if matches!(self.value.get(end), Some(&C_LOWER_E) | Some(&C_UPPER_E)) {
1208            let exponent_start = end;
1209            let mut exponent_end = end + 1;
1210            if matches!(
1211                self.value.get(exponent_end),
1212                Some(&C_PLUS_SIGN) | Some(&C_HYPHEN_MINUS)
1213            ) {
1214                exponent_end += 1;
1215            }
1216            if self
1217                .value
1218                .get(exponent_end)
1219                .is_some_and(|byte| is_digit(*byte))
1220            {
1221                end = exponent_end + 1;
1222                while self.value.get(end).is_some_and(|byte| is_digit(*byte)) {
1223                    end += 1;
1224                }
1225            } else {
1226                end = exponent_start;
1227            }
1228        }
1229        if self.value.get(end) == Some(&C_PERCENTAGE) {
1230            return (
1231                TokenKind::Percentage,
1232                end + 1,
1233                start,
1234                end + 1,
1235                TokenFlags::ascii(),
1236            );
1237        }
1238        if self.starts_ident_at(end) {
1239            let (end, flags) = self.scan_name_with_flags(end);
1240            return (TokenKind::Dimension, end, start, end, flags);
1241        }
1242        (TokenKind::Number, end, start, end, TokenFlags::ascii())
1243    }
1244
1245    #[inline]
1246    fn skip_white_space(&self, mut position: usize) -> usize {
1247        while self
1248            .value
1249            .get(position)
1250            .is_some_and(|byte| is_white_space(*byte))
1251        {
1252            position += 1;
1253        }
1254        position
1255    }
1256
1257    #[inline]
1258    fn scan_plain_ascii_name(&self, mut position: usize) -> usize {
1259        while self
1260            .value
1261            .get(position)
1262            .is_some_and(|byte| PLAIN_ASCII_NAME_BYTE[*byte as usize])
1263        {
1264            position += 1;
1265        }
1266        position
1267    }
1268
1269    #[inline]
1270    fn raw_name_needs_tokenizer(&self, position: usize) -> bool {
1271        self.value
1272            .get(position)
1273            .is_some_and(|byte| *byte == 0 || *byte == C_REVERSE_SOLIDUS || !byte.is_ascii())
1274    }
1275
1276    #[inline]
1277    fn scan_raw_numeric_end(&self, start: usize) -> Option<usize> {
1278        let mut end = start;
1279        if matches!(
1280            self.value.get(end),
1281            Some(&C_PLUS_SIGN) | Some(&C_HYPHEN_MINUS)
1282        ) {
1283            end += 1;
1284        }
1285        while self.value.get(end).is_some_and(|byte| is_digit(*byte)) {
1286            end += 1;
1287        }
1288        if self.value.get(end) == Some(&C_FULL_STOP)
1289            && self.value.get(end + 1).is_some_and(|byte| is_digit(*byte))
1290        {
1291            end += 1;
1292            while self.value.get(end).is_some_and(|byte| is_digit(*byte)) {
1293                end += 1;
1294            }
1295        }
1296        if matches!(self.value.get(end), Some(&C_LOWER_E) | Some(&C_UPPER_E)) {
1297            let exponent_start = end;
1298            let mut exponent_end = end + 1;
1299            if matches!(
1300                self.value.get(exponent_end),
1301                Some(&C_PLUS_SIGN) | Some(&C_HYPHEN_MINUS)
1302            ) {
1303                exponent_end += 1;
1304            }
1305            if self
1306                .value
1307                .get(exponent_end)
1308                .is_some_and(|byte| is_digit(*byte))
1309            {
1310                end = exponent_end + 1;
1311                while self.value.get(end).is_some_and(|byte| is_digit(*byte)) {
1312                    end += 1;
1313                }
1314            } else {
1315                end = exponent_start;
1316            }
1317        }
1318        if self.value.get(end) == Some(&C_PERCENTAGE) {
1319            return Some(end + 1);
1320        }
1321        if self.starts_ident_at(end) {
1322            let name_end = self.scan_plain_ascii_name(end);
1323            if name_end == end || self.raw_name_needs_tokenizer(name_end) {
1324                return None;
1325            }
1326            return Some(name_end);
1327        }
1328        if self.raw_name_needs_tokenizer(end) {
1329            return None;
1330        }
1331        Some(end)
1332    }
1333
1334    #[inline]
1335    fn scan_name(&self, position: usize) -> usize {
1336        self.scan_name_impl::<false>(position).0
1337    }
1338
1339    #[inline]
1340    fn scan_name_with_flags(&self, position: usize) -> (usize, TokenFlags) {
1341        self.scan_name_impl::<true>(position)
1342    }
1343
1344    #[inline]
1345    fn scan_name_impl<const TRACK_FLAGS: bool>(&self, mut position: usize) -> (usize, TokenFlags) {
1346        let bytes = self.value;
1347        let mut flags = TokenFlags::ascii();
1348        while position < bytes.len() {
1349            while position < bytes.len() {
1350                let byte = bytes[position];
1351                if PLAIN_ASCII_NAME_BYTE[byte as usize] {
1352                    position += 1;
1353                } else if byte == 0 {
1354                    if TRACK_FLAGS {
1355                        flags.mark_null();
1356                    }
1357                    position += 1;
1358                } else {
1359                    break;
1360                }
1361            }
1362            if position == bytes.len() {
1363                break;
1364            }
1365
1366            let byte = bytes[position];
1367            if byte == C_REVERSE_SOLIDUS {
1368                if self.is_valid_escape_at(position) {
1369                    if TRACK_FLAGS {
1370                        flags.mark_escape();
1371                        if bytes.get(position + 1).is_some_and(|next| !next.is_ascii()) {
1372                            flags.mark_non_ascii();
1373                        }
1374                    }
1375                    position = self.scan_escape(position);
1376                } else {
1377                    break;
1378                }
1379            } else if byte.is_ascii() {
1380                break;
1381            } else {
1382                if TRACK_FLAGS {
1383                    flags.mark_non_ascii();
1384                }
1385                position += self.utf8_width_at(position);
1386            }
1387        }
1388        (position, flags)
1389    }
1390
1391    #[inline]
1392    fn starts_ident_at(&self, position: usize) -> bool {
1393        let Some(byte) = self.value.get(position).copied() else {
1394            return false;
1395        };
1396        match byte {
1397            C_HYPHEN_MINUS => match self.value.get(position + 1).copied() {
1398                Some(next) => {
1399                    is_name_start_byte(next)
1400                        || next == C_HYPHEN_MINUS
1401                        || self.is_valid_escape_at(position + 1)
1402                }
1403                None => false,
1404            },
1405            C_REVERSE_SOLIDUS => self.is_valid_escape_at(position),
1406            _ => is_name_start_byte(byte),
1407        }
1408    }
1409
1410    #[inline]
1411    fn starts_number_at(&self, position: usize) -> bool {
1412        let Some(first) = self.value.get(position).copied() else {
1413            return false;
1414        };
1415        let second = self.value.get(position + 1).copied();
1416        let third = self.value.get(position + 2).copied();
1417        match first {
1418            C_PLUS_SIGN | C_HYPHEN_MINUS => {
1419                second.is_some_and(is_digit)
1420                    || (second == Some(C_FULL_STOP) && third.is_some_and(is_digit))
1421            }
1422            C_FULL_STOP => second.is_some_and(is_digit),
1423            _ => is_digit(first),
1424        }
1425    }
1426
1427    #[inline]
1428    fn is_valid_escape_at(&self, position: usize) -> bool {
1429        self.value.get(position) == Some(&C_REVERSE_SOLIDUS)
1430            && self
1431                .value
1432                .get(position + 1)
1433                .is_some_and(|byte| !is_new_line(*byte))
1434    }
1435
1436    #[inline]
1437    fn scan_escape(&self, position: usize) -> usize {
1438        debug_assert!(self.is_valid_escape_at(position));
1439        let mut end = position + 1;
1440        if self.value[end].is_ascii_hexdigit() {
1441            let mut digits = 0;
1442            while end < self.value.len() && digits < 6 && self.value[end].is_ascii_hexdigit() {
1443                end += 1;
1444                digits += 1;
1445            }
1446            if self
1447                .value
1448                .get(end)
1449                .is_some_and(|byte| is_white_space(*byte))
1450            {
1451                end += 1;
1452                if self.value.get(end - 1) == Some(&C_CARRIAGE_RETURN)
1453                    && self.value.get(end) == Some(&C_LINE_FEED)
1454                {
1455                    end += 1;
1456                }
1457            }
1458            return end;
1459        }
1460        end + self.utf8_width_at(end)
1461    }
1462
1463    #[inline]
1464    fn utf8_width_at(&self, position: usize) -> usize {
1465        let byte = self.value[position];
1466        if byte < 0x80 {
1467            1
1468        } else if byte < 0xE0 {
1469            2
1470        } else if byte < 0xF0 {
1471            3
1472        } else {
1473            4
1474        }
1475    }
1476}
1477
1478/// The single forward stream used by dependency extraction.
1479///
1480/// The stream owns the main scanner state and gives the dependency scanner one
1481/// token of lookahead without cloning the lexer for every decision. Special
1482/// parsers use the same stream through `next_parser_token`, so consuming a
1483/// subgrammar never creates a second scanner over the source.
1484///
1485/// The cursor is strictly one-directional:
1486///
1487/// ```text
1488/// Lexer::scan_pos         farthest tokenized position, only advances
1489/// TokenStream::consumed   farthest semantically consumed position, only advances
1490/// buffered lookahead      tokens scanned but not yet consumed
1491/// ```
1492///
1493/// `next` consumes from the buffer first and only calls `lexer.next_token`
1494/// when the buffer is empty; it never moves the scanner backwards, so every
1495/// source range is tokenized at most once and every token is consumed at most
1496/// once.
1497pub(crate) struct TokenStream<'a, 's> {
1498    lexer: &'a mut Lexer<'s>,
1499    consumed: Pos,
1500    buffered: VecDeque<TokenWithTrivia>,
1501    generic_value_state: GenericValueScanState,
1502    at_rule_state: GenericValueScanState,
1503    special_value_state: GenericValueScanState,
1504}
1505
1506#[derive(Debug, Default)]
1507pub(crate) struct GenericValueScanState {
1508    parentheses: u32,
1509    squares: u32,
1510    curlies: u32,
1511}
1512
1513#[derive(Debug, Clone, Copy)]
1514struct GenericValueScanOptions {
1515    keep_comments: bool,
1516    preserve_strings: bool,
1517    preserve_delimiters: bool,
1518    property: Option<PropertyKind>,
1519}
1520
1521#[derive(Debug, Clone, Copy)]
1522struct PrescannedIdent {
1523    start: Pos,
1524    end: Pos,
1525    flags: TokenFlags,
1526}
1527
1528impl GenericValueScanState {
1529    #[inline]
1530    fn is_nested(&self) -> bool {
1531        self.parentheses != 0 || self.squares != 0 || self.curlies != 0
1532    }
1533}
1534
1535#[inline]
1536fn is_dependency_value_function(name: &str) -> bool {
1537    name.eq_ignore_ascii_case("url")
1538        || name.eq_ignore_ascii_case("var")
1539        || name.eq_ignore_ascii_case("image-set")
1540        || ["-webkit-", "-moz-", "-ms-", "-o-"]
1541            .into_iter()
1542            .any(|prefix| {
1543                name.strip_prefix(prefix)
1544                    .is_some_and(|name| name.eq_ignore_ascii_case("image-set"))
1545            })
1546        || name.starts_with("--")
1547}
1548
1549fn eq_css_ascii_case(input: &[u8], expected: &[u8]) -> bool {
1550    let mut input_position = 0;
1551    let mut expected_position = 0;
1552    while input_position < input.len() {
1553        let byte = if input[input_position] == C_REVERSE_SOLIDUS {
1554            input_position += 1;
1555            let Some(&first) = input.get(input_position) else {
1556                return false;
1557            };
1558            if first.is_ascii_hexdigit() {
1559                let mut value = 0u32;
1560                let mut digits = 0;
1561                while let Some(&digit) = input.get(input_position) {
1562                    if digits == 6 || !digit.is_ascii_hexdigit() {
1563                        break;
1564                    }
1565                    value = value * 16 + hex_digit_value(digit) as u32;
1566                    input_position += 1;
1567                    digits += 1;
1568                }
1569                if input
1570                    .get(input_position)
1571                    .is_some_and(|byte| is_white_space(*byte))
1572                {
1573                    input_position += 1;
1574                    if input.get(input_position - 1) == Some(&C_CARRIAGE_RETURN)
1575                        && input.get(input_position) == Some(&C_LINE_FEED)
1576                    {
1577                        input_position += 1;
1578                    }
1579                }
1580                let Ok(byte) = u8::try_from(value) else {
1581                    return false;
1582                };
1583                if byte == 0 {
1584                    return false;
1585                }
1586                byte
1587            } else {
1588                if !first.is_ascii() || is_new_line(first) {
1589                    return false;
1590                }
1591                input_position += 1;
1592                first
1593            }
1594        } else {
1595            let byte = input[input_position];
1596            if !byte.is_ascii() || byte == 0 {
1597                return false;
1598            }
1599            input_position += 1;
1600            byte
1601        };
1602        let Some(expected_byte) = expected.get(expected_position) else {
1603            return false;
1604        };
1605        if !byte.eq_ignore_ascii_case(expected_byte) {
1606            return false;
1607        }
1608        expected_position += 1;
1609    }
1610    expected_position == expected.len()
1611}
1612
1613fn hex_digit_value(byte: u8) -> u8 {
1614    match byte {
1615        b'0'..=b'9' => byte - b'0',
1616        b'a'..=b'f' => byte - b'a' + 10,
1617        b'A'..=b'F' => byte - b'A' + 10,
1618        _ => unreachable!("hex digit checked by caller"),
1619    }
1620}
1621
1622#[inline(always)]
1623fn never_fast_forward_candidate(_: &str) -> bool {
1624    false
1625}
1626
1627fn is_css_modules_pure_magic_comment(input: &str) -> bool {
1628    let input = trim_slice(input);
1629    let Some(keyword) = input.strip_prefix("cssmodules-pure-") else {
1630        return false;
1631    };
1632    ["ignore", "no-check"].into_iter().any(|expected| {
1633        keyword.strip_prefix(expected).is_some_and(|rest| {
1634            rest.is_empty()
1635                || rest
1636                    .as_bytes()
1637                    .first()
1638                    .is_some_and(|byte| is_white_space(*byte))
1639        })
1640    })
1641}
1642
1643fn trim_slice(input: &str) -> &str {
1644    let bytes = input.as_bytes();
1645    let mut start = 0usize;
1646    let mut end = bytes.len();
1647    while start < end && is_white_space(bytes[start]) {
1648        start += 1;
1649    }
1650    while end > start && is_white_space(bytes[end - 1]) {
1651        end -= 1;
1652    }
1653    &input[start..end]
1654}
1655
1656impl<'a, 's> TokenStream<'a, 's> {
1657    pub(crate) fn from_lexer(lexer: &'a mut Lexer<'s>) -> Self {
1658        Self {
1659            lexer,
1660            consumed: 0,
1661            buffered: VecDeque::new(),
1662            generic_value_state: GenericValueScanState::default(),
1663            at_rule_state: GenericValueScanState::default(),
1664            special_value_state: GenericValueScanState::default(),
1665        }
1666    }
1667
1668    #[inline(always)]
1669    pub(crate) fn next<const KEEP_COMMENTS: bool>(&mut self) -> TokenWithTrivia {
1670        let token = if let Some(token) = self.buffered.pop_front() {
1671            token
1672        } else {
1673            self.read_significant::<KEEP_COMMENTS>()
1674        };
1675        debug_assert!(
1676            token.token.range.start >= self.consumed,
1677            "token range starts before consumed_pos: {:?} < {}",
1678            token.token.range,
1679            self.consumed
1680        );
1681        self.consumed = token.token.range.end;
1682        if matches!(
1683            token.token.kind,
1684            TokenKind::Semicolon | TokenKind::RightCurlyBracket
1685        ) {
1686            self.generic_value_state = GenericValueScanState::default();
1687        }
1688        debug_assert!(
1689            self.consumed <= self.lexer.scan_pos(),
1690            "consumed_pos ({}) exceeded scan_pos ({})",
1691            self.consumed,
1692            self.lexer.scan_pos()
1693        );
1694        token
1695    }
1696
1697    /// Consume the next parser token while folding comments into its leading
1698    /// trivia. This is the equivalent of the old isolated cursor's behavior,
1699    /// but it advances the main stream and therefore preserves one scanner
1700    /// ownership for all subgrammars.
1701    #[inline]
1702    pub(crate) fn next_parser_token(&mut self) -> TokenWithTrivia {
1703        let mut item = self.next::<true>();
1704        if item.token.kind != TokenKind::Comment {
1705            return item;
1706        }
1707
1708        let start = item.leading.range.start;
1709        let first_comment_start = item
1710            .leading
1711            .first_comment_start
1712            .or(Some(item.token.range.start));
1713        let mut has_white_space = item.leading.has_white_space;
1714        loop {
1715            item = self.next::<true>();
1716            has_white_space |= item.leading.has_white_space;
1717            if item.token.kind == TokenKind::Comment {
1718                continue;
1719            }
1720            let end = item.leading.end;
1721            item.leading = Trivia {
1722                range: Range::new(start, end),
1723                end,
1724                first_comment_start,
1725                has_white_space,
1726            };
1727            return item;
1728        }
1729    }
1730
1731    #[inline]
1732    pub(crate) fn peek<const KEEP_COMMENTS: bool>(&mut self) -> TokenWithTrivia {
1733        if self.buffered.is_empty() {
1734            let item = self.read_significant::<KEEP_COMMENTS>();
1735            self.buffered.push_back(item);
1736        }
1737        *self.buffered.front().unwrap()
1738    }
1739
1740    /// Peek the next parser token (folding comments into its leading trivia)
1741    /// without consuming it or any preceding comments. The stream state is
1742    /// unchanged: `next` later returns the same tokens in the same order.
1743    #[inline]
1744    pub(crate) fn peek_parser_token(&mut self) -> TokenWithTrivia {
1745        self.peek::<true>();
1746        let mut leading_start = None;
1747        let mut first_comment_start = None;
1748        let mut has_white_space = false;
1749        let mut index = 0usize;
1750        loop {
1751            if index == self.buffered.len() {
1752                let item = self.read_significant::<true>();
1753                self.buffered.push_back(item);
1754            }
1755            let item = self.buffered[index];
1756            leading_start.get_or_insert(item.leading.range.start);
1757            has_white_space |= item.leading.has_whitespace();
1758            if matches!(item.token.kind, TokenKind::Comment | TokenKind::BadComment) {
1759                first_comment_start.get_or_insert(item.token.range.start);
1760                index += 1;
1761                continue;
1762            }
1763
1764            let end = item.leading.end;
1765            let mut result = item;
1766            result.leading = Trivia {
1767                range: Range::new(leading_start.unwrap_or(end), end),
1768                end,
1769                first_comment_start: first_comment_start.or(item.leading.first_comment_start),
1770                has_white_space,
1771            };
1772            return result;
1773        }
1774    }
1775
1776    /// Peek at the next significant token, leaving any skipped comments in
1777    /// the buffer for `next` to consume in source order.
1778    #[inline]
1779    pub(crate) fn peek_significant_skipping_comments<const KEEP_COMMENTS: bool>(
1780        &mut self,
1781    ) -> TokenWithTrivia {
1782        self.peek::<KEEP_COMMENTS>();
1783        for item in &self.buffered {
1784            if !matches!(item.token.kind, TokenKind::Comment | TokenKind::BadComment) {
1785                return *item;
1786            }
1787        }
1788        loop {
1789            let item = self.read_significant::<KEEP_COMMENTS>();
1790            self.buffered.push_back(item);
1791            if !matches!(item.token.kind, TokenKind::Comment | TokenKind::BadComment) {
1792                return item;
1793            }
1794        }
1795    }
1796
1797    /// The position of the next yet-untokenized source byte. The buffer may
1798    /// still hold tokens; callers must drain `next` before fast-forwarding.
1799    #[inline]
1800    pub(crate) fn fast_forward_generic_value_if_buffer_empty<F, C>(
1801        &mut self,
1802        keep_comments: bool,
1803        preserve_strings: bool,
1804        preserve_delimiters: bool,
1805        mut is_candidate: F,
1806        mut is_comment_candidate: C,
1807    ) where
1808        F: FnMut(&str) -> bool,
1809        C: FnMut(&str) -> bool,
1810    {
1811        if !self.buffered.is_empty() {
1812            return;
1813        }
1814        let candidate = self.lexer.fast_forward_generic_value::<false, _, _>(
1815            &mut self.generic_value_state,
1816            GenericValueScanOptions {
1817                keep_comments,
1818                preserve_strings,
1819                preserve_delimiters,
1820                property: None,
1821            },
1822            &mut is_candidate,
1823            &mut is_comment_candidate,
1824            None,
1825        );
1826        self.finish_value_fast_forward(candidate);
1827    }
1828
1829    #[inline]
1830    pub(crate) fn fast_forward_generic_value_without_candidates_if_buffer_empty(
1831        &mut self,
1832        preserve_strings: bool,
1833        preserve_delimiters: bool,
1834    ) {
1835        if !self.buffered.is_empty() {
1836            return;
1837        }
1838        let candidate = self.lexer.fast_forward_generic_value::<false, _, _>(
1839            &mut self.generic_value_state,
1840            GenericValueScanOptions {
1841                keep_comments: false,
1842                preserve_strings,
1843                preserve_delimiters,
1844                property: None,
1845            },
1846            never_fast_forward_candidate,
1847            never_fast_forward_candidate,
1848            None,
1849        );
1850        self.finish_value_fast_forward(candidate);
1851    }
1852
1853    #[inline]
1854    pub(crate) fn fast_forward_at_rule_if_buffer_empty<F, C>(
1855        &mut self,
1856        keep_comments: bool,
1857        preserve_strings: bool,
1858        preserve_delimiters: bool,
1859        mut is_candidate: F,
1860        mut is_comment_candidate: C,
1861    ) where
1862        F: FnMut(&str) -> bool,
1863        C: FnMut(&str) -> bool,
1864    {
1865        if !self.buffered.is_empty() {
1866            return;
1867        }
1868        let candidate = self.lexer.fast_forward_generic_value::<true, _, _>(
1869            &mut self.at_rule_state,
1870            GenericValueScanOptions {
1871                keep_comments,
1872                preserve_strings,
1873                preserve_delimiters,
1874                property: None,
1875            },
1876            &mut is_candidate,
1877            &mut is_comment_candidate,
1878            None,
1879        );
1880        self.finish_value_fast_forward(candidate);
1881    }
1882
1883    #[inline]
1884    pub(crate) fn reset_at_rule_scan_state(&mut self) {
1885        self.at_rule_state = GenericValueScanState::default();
1886    }
1887
1888    #[inline]
1889    pub(crate) fn fast_forward_special_value_if_buffer_empty(
1890        &mut self,
1891        keep_comments: bool,
1892        preserve_strings: bool,
1893        preserve_delimiters: bool,
1894        property: PropertyKind,
1895        icss_symbols: Option<&FxHashSet<&str>>,
1896    ) {
1897        if !self.buffered.is_empty() {
1898            return;
1899        }
1900        let candidate = self.lexer.fast_forward_generic_value::<false, _, _>(
1901            &mut self.special_value_state,
1902            GenericValueScanOptions {
1903                keep_comments,
1904                preserve_strings,
1905                preserve_delimiters,
1906                property: Some(property),
1907            },
1908            never_fast_forward_candidate,
1909            is_css_modules_pure_magic_comment,
1910            icss_symbols,
1911        );
1912        self.finish_value_fast_forward(candidate);
1913    }
1914
1915    #[inline]
1916    fn finish_value_fast_forward(&mut self, candidate: Option<PrescannedIdent>) {
1917        let Some(candidate) = candidate else {
1918            self.consumed = self.lexer.scan_pos();
1919            return;
1920        };
1921        debug_assert_eq!(candidate.end, self.lexer.scan_pos());
1922        self.consumed = candidate.start;
1923        let range = Range::new(candidate.start, candidate.end);
1924        self.buffered.push_back(TokenWithTrivia {
1925            token: Token::with_flags(TokenKind::Ident, range, range, candidate.flags),
1926            leading: Trivia {
1927                range: Range::new(candidate.start, candidate.start),
1928                end: candidate.start,
1929                first_comment_start: None,
1930                has_white_space: false,
1931            },
1932        });
1933    }
1934
1935    #[inline]
1936    pub(crate) fn reset_special_value_scan_state(&mut self) {
1937        self.special_value_state = GenericValueScanState::default();
1938    }
1939
1940    #[inline]
1941    pub(crate) fn fast_forward_selector_if_buffer_empty<C>(
1942        &mut self,
1943        square_depth: &mut u32,
1944        keep_comments: bool,
1945        has_mode: bool,
1946        is_comment_candidate: C,
1947    ) -> bool
1948    where
1949        C: FnMut(&str) -> bool,
1950    {
1951        if !self.buffered.is_empty() {
1952            return false;
1953        }
1954        if *square_depth == 0 {
1955            let scan_pos = self.lexer.scan_pos();
1956            let Some(next) = self.lexer.byte_at(scan_pos) else {
1957                return false;
1958            };
1959            if matches!(
1960                next,
1961                b'"' | b'\'' | b'(' | b')' | b'{' | b'}' | b',' | b';' | b'@'
1962            ) || (has_mode && matches!(next, b'.' | b'#' | b':'))
1963            {
1964                return false;
1965            }
1966
1967            // A raw scan has a fixed call/setup cost. Keep short, dense
1968            // selector fragments on the regular tokenizer path and only
1969            // enter the scanner when it can skip a useful run. Comments and
1970            // attributes are exceptions because their contents are opaque.
1971            let mut probe = scan_pos;
1972            while probe - scan_pos < 4 {
1973                let Some(byte) = self.lexer.byte_at(probe) else {
1974                    return false;
1975                };
1976                if matches!(byte, b'/' | b'[') {
1977                    break;
1978                }
1979                if probe != scan_pos
1980                    && (matches!(
1981                        byte,
1982                        b'"' | b'\'' | b'(' | b')' | b'{' | b'}' | b',' | b';' | b'@'
1983                    ) || (has_mode && matches!(byte, b'.' | b'#' | b':')))
1984                {
1985                    return false;
1986                }
1987                probe += 1;
1988            }
1989        }
1990        let invalidates_composes = self.lexer.fast_forward_selector(
1991            square_depth,
1992            keep_comments,
1993            has_mode,
1994            is_comment_candidate,
1995        );
1996        self.consumed = self.lexer.scan_pos();
1997        invalidates_composes
1998    }
1999
2000    /// Skip a balanced delimiter run without manufacturing tokens. The opening
2001    /// token must already be consumed, and no further tokens may be buffered.
2002    ///
2003    /// On success `consumed` is advanced to the end of the closing token and
2004    /// the full range is returned. On failure (`None`) the lexer position is
2005    /// unchanged so the caller can re-tokenize the region normally.
2006    pub(crate) fn fast_forward(&mut self, end: TokenKind) -> Option<Range> {
2007        debug_assert!(
2008            self.buffered.is_empty(),
2009            "fast_forward requires an empty lookahead buffer"
2010        );
2011        let old_scan_pos = self.lexer.scan_pos();
2012        let old_consumed = self.consumed;
2013        let range = self.lexer.fast_forward(end)?;
2014        self.consumed = range.end;
2015        debug_assert!(old_scan_pos <= self.lexer.scan_pos());
2016        debug_assert!(old_consumed <= self.consumed);
2017        debug_assert!(self.consumed <= self.lexer.scan_pos());
2018        Some(range)
2019    }
2020
2021    /// The farthest position consumed by `next`. This is the current semantic
2022    /// position of the dependency scanner.
2023    #[inline]
2024    pub(crate) fn consumed_pos(&self) -> Pos {
2025        self.consumed
2026    }
2027
2028    #[inline]
2029    pub(crate) fn lexer(&self) -> &Lexer<'s> {
2030        self.lexer
2031    }
2032
2033    #[inline]
2034    pub(crate) fn lexer_mut(&mut self) -> &mut Lexer<'s> {
2035        self.lexer
2036    }
2037
2038    #[inline]
2039    pub(crate) fn source_end(&self) -> Pos {
2040        self.lexer.source_end()
2041    }
2042
2043    #[inline(always)]
2044    pub(crate) fn byte_at(&self, position: Pos) -> Option<u8> {
2045        self.lexer.byte_at(position)
2046    }
2047
2048    #[inline]
2049    pub(crate) fn slice(&self, start: Pos, end: Pos) -> Option<&'s str> {
2050        self.lexer.slice(start, end)
2051    }
2052
2053    #[inline(always)]
2054    pub(crate) fn slice_trusted(&self, start: Pos, end: Pos) -> &'s str {
2055        self.lexer.slice_trusted(start, end)
2056    }
2057
2058    #[inline(always)]
2059    fn read_significant<const KEEP_COMMENTS: bool>(&mut self) -> TokenWithTrivia {
2060        let start = self.lexer.scan_pos();
2061        let mut end = start;
2062        let mut first_comment_start = None;
2063        let mut has_white_space = false;
2064        loop {
2065            let token = self.lexer.next_token();
2066            debug_assert!(token.range.start >= end);
2067            if token.kind == TokenKind::WhiteSpace {
2068                has_white_space = true;
2069                end = token.range.end;
2070                continue;
2071            }
2072            if !KEEP_COMMENTS && matches!(token.kind, TokenKind::Comment | TokenKind::BadComment) {
2073                first_comment_start.get_or_insert(token.range.start);
2074                end = token.range.end;
2075                continue;
2076            }
2077            let leading = Trivia {
2078                range: Range::new(start, end),
2079                end,
2080                first_comment_start,
2081                has_white_space,
2082            };
2083            return TokenWithTrivia { token, leading };
2084        }
2085    }
2086}
2087
2088pub fn is_new_line(c: u8) -> bool {
2089    c == C_LINE_FEED || c == C_CARRIAGE_RETURN || c == C_FORM_FEED
2090}
2091
2092#[inline]
2093fn kind_to_right(byte: u8) -> TokenKind {
2094    match byte {
2095        C_RIGHT_PARENTHESIS => TokenKind::RightParenthesis,
2096        C_RIGHT_SQUARE => TokenKind::RightSquareBracket,
2097        C_RIGHT_CURLY => TokenKind::RightCurlyBracket,
2098        _ => unreachable!("fast_forward only sees closing bracket bytes"),
2099    }
2100}
2101
2102pub fn is_space(c: u8) -> bool {
2103    c == C_TAB || c == C_SPACE
2104}
2105
2106pub fn is_white_space(c: u8) -> bool {
2107    is_new_line(c) || is_space(c)
2108}
2109
2110pub fn is_digit(c: u8) -> bool {
2111    c.is_ascii_digit()
2112}
2113
2114#[inline]
2115fn is_name_start_byte(c: u8) -> bool {
2116    c == 0 || c == C_LOW_LINE || c.is_ascii_alphabetic() || !c.is_ascii()
2117}
2118
2119#[inline]
2120fn is_non_printable(c: u8) -> bool {
2121    matches!(c, 0x00..=0x08 | 0x0B | 0x0E..=0x1F | 0x7F)
2122}
2123
2124#[cfg(test)]
2125#[path = "../tests/unit/lexer_tests.rs"]
2126mod tests;