Skip to main content

css_module_lexer/
lexer.rs

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