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          // CSS Syntax §4.3.6: EOF ends a url-token rather than making it bad.
1201          if close == self.value.len() {
1202            return (TokenKind::Url, close, content_start, content_end, flags);
1203          }
1204          return self.scan_bad_url(start, close, content_start, flags);
1205        }
1206        if byte == 0 {
1207          flags.mark_null();
1208        } else if !byte.is_ascii() {
1209          flags.mark_non_ascii();
1210        }
1211        if byte == C_QUOTATION_MARK
1212          || byte == C_APOSTROPHE
1213          || byte == C_LEFT_PARENTHESIS
1214          || is_non_printable(byte)
1215        {
1216          return self.scan_bad_url(start, end, content_start, flags);
1217        }
1218        if byte == C_REVERSE_SOLIDUS {
1219          flags.mark_escape();
1220          if self.value.get(end + 1).is_some_and(|next| !next.is_ascii()) {
1221            flags.mark_non_ascii();
1222          }
1223          if self.is_valid_escape_at(end) {
1224            end = self.scan_escape(end);
1225          } else if self
1226            .value
1227            .get(end + 1)
1228            .is_some_and(|next| is_new_line(*next))
1229          {
1230            end += 2;
1231            if self.value.get(end - 1) == Some(&C_CARRIAGE_RETURN)
1232              && self.value.get(end) == Some(&C_LINE_FEED)
1233            {
1234              end += 1;
1235            }
1236          } else {
1237            return self.scan_bad_url(start, end, content_start, flags);
1238          }
1239        } else {
1240          end += self.utf8_width_at(end);
1241        }
1242      }
1243      if end == self.value.len() {
1244        return (
1245          TokenKind::Url,
1246          self.value.len(),
1247          content_start,
1248          self.value.len(),
1249          flags,
1250        );
1251      }
1252      // Bulk search: the closing parenthesis is almost always the next byte
1253      // that needs attention, so find it with a vectorized search and then
1254      // verify the skipped run. Any byte before it that requires per-byte
1255      // handling (whitespace, quote, parenthesis, escape, or non-printable)
1256      // hands control back to the scalar loop at that exact position.
1257      let rest = &self.value[end..];
1258      let close_rel = memchr::memchr(C_RIGHT_PARENTHESIS, rest).unwrap_or(rest.len());
1259      let segment = &rest[..close_rel];
1260      if !segment.is_ascii() {
1261        flags.mark_non_ascii();
1262      }
1263      let bad_rel = segment.iter().position(|byte| {
1264        *byte <= C_SPACE
1265          || *byte == C_QUOTATION_MARK
1266          || *byte == C_APOSTROPHE
1267          || *byte == C_LEFT_PARENTHESIS
1268          || *byte == C_REVERSE_SOLIDUS
1269          || *byte == 0x7F
1270      });
1271      if let Some(offset) = bad_rel {
1272        end += offset;
1273        continue 'scan;
1274      }
1275      end += close_rel;
1276      if close_rel == rest.len() {
1277        return (
1278          TokenKind::Url,
1279          self.value.len(),
1280          content_start,
1281          self.value.len(),
1282          flags,
1283        );
1284      }
1285      return (TokenKind::Url, end + 1, content_start, end, flags);
1286    }
1287  }
1288
1289  fn scan_bad_url(
1290    &self,
1291    _start: usize,
1292    mut end: usize,
1293    content_start: usize,
1294    mut flags: TokenFlags,
1295  ) -> (TokenKind, usize, usize, usize, TokenFlags) {
1296    loop {
1297      let short_end = end.saturating_add(32).min(self.value.len());
1298      while end < short_end {
1299        let byte = self.value[end];
1300        if byte == 0 {
1301          flags.mark_null();
1302        } else if !byte.is_ascii() {
1303          flags.mark_non_ascii();
1304        }
1305        if byte == C_RIGHT_PARENTHESIS {
1306          end += 1;
1307          return (TokenKind::BadUrl, end, content_start, end, flags);
1308        }
1309        if byte == C_REVERSE_SOLIDUS {
1310          flags.mark_escape();
1311          if self.value.get(end + 1).is_some_and(|next| !next.is_ascii()) {
1312            flags.mark_non_ascii();
1313          }
1314          if self.is_valid_escape_at(end) {
1315            end = self.scan_escape(end);
1316          } else if self
1317            .value
1318            .get(end + 1)
1319            .is_some_and(|next| is_new_line(*next))
1320          {
1321            end += 2;
1322            if self.value.get(end - 1) == Some(&C_CARRIAGE_RETURN)
1323              && self.value.get(end) == Some(&C_LINE_FEED)
1324            {
1325              end += 1;
1326            }
1327          } else {
1328            end += 1;
1329          }
1330        } else {
1331          end += self.utf8_width_at(end);
1332        }
1333      }
1334      if end == self.value.len() {
1335        break;
1336      }
1337      // Bulk search: skip ordinary content with a vectorized search for the
1338      // next closing parenthesis or escape.
1339      let rest = &self.value[end..];
1340      let rel = memchr2(C_RIGHT_PARENTHESIS, C_REVERSE_SOLIDUS, rest).unwrap_or(rest.len());
1341      let segment = &rest[..rel];
1342      if memchr::memchr(b'\0', segment).is_some() {
1343        flags.mark_null();
1344      }
1345      if !segment.is_ascii() {
1346        flags.mark_non_ascii();
1347      }
1348      end += rel;
1349      // end now points at a closing parenthesis, escape, or EOF. The scalar
1350      // loop handles it with the original per-byte logic.
1351    }
1352    (TokenKind::BadUrl, end, content_start, end, flags)
1353  }
1354
1355  fn scan_numeric(&self, start: usize) -> (TokenKind, usize, usize, usize, TokenFlags) {
1356    let end = self.scan_number_end(start);
1357    if self.value.get(end) == Some(&C_PERCENTAGE) {
1358      return (
1359        TokenKind::Percentage,
1360        end + 1,
1361        start,
1362        end + 1,
1363        TokenFlags::ascii(),
1364      );
1365    }
1366    if self.starts_ident_at(end) {
1367      let (end, flags) = self.scan_name_with_flags(end);
1368      return (TokenKind::Dimension, end, start, end, flags);
1369    }
1370    (TokenKind::Number, end, start, end, TokenFlags::ascii())
1371  }
1372
1373  #[inline]
1374  fn scan_number_end(&self, start: usize) -> usize {
1375    let mut end = start;
1376    if matches!(self.value.get(end), Some(&C_PLUS_SIGN | &C_HYPHEN_MINUS)) {
1377      end += 1;
1378    }
1379    while self.value.get(end).is_some_and(|byte| is_digit(*byte)) {
1380      end += 1;
1381    }
1382    if self.value.get(end) == Some(&C_FULL_STOP)
1383      && self.value.get(end + 1).is_some_and(|byte| is_digit(*byte))
1384    {
1385      end += 1;
1386      while self.value.get(end).is_some_and(|byte| is_digit(*byte)) {
1387        end += 1;
1388      }
1389    }
1390    if matches!(self.value.get(end), Some(&C_LOWER_E | &C_UPPER_E)) {
1391      let exponent_start = end;
1392      let mut exponent_end = end + 1;
1393      if matches!(
1394        self.value.get(exponent_end),
1395        Some(&C_PLUS_SIGN | &C_HYPHEN_MINUS)
1396      ) {
1397        exponent_end += 1;
1398      }
1399      if self
1400        .value
1401        .get(exponent_end)
1402        .is_some_and(|byte| is_digit(*byte))
1403      {
1404        end = exponent_end + 1;
1405        while self.value.get(end).is_some_and(|byte| is_digit(*byte)) {
1406          end += 1;
1407        }
1408      } else {
1409        end = exponent_start;
1410      }
1411    }
1412    end
1413  }
1414
1415  #[inline]
1416  fn skip_white_space(&self, mut position: usize) -> usize {
1417    while self
1418      .value
1419      .get(position)
1420      .is_some_and(|byte| is_white_space(*byte))
1421    {
1422      position += 1;
1423    }
1424    position
1425  }
1426
1427  #[inline]
1428  fn scan_plain_ascii_name(&self, mut position: usize) -> usize {
1429    while self
1430      .value
1431      .get(position)
1432      .is_some_and(|byte| PLAIN_ASCII_NAME_BYTE[*byte as usize])
1433    {
1434      position += 1;
1435    }
1436    position
1437  }
1438
1439  #[inline]
1440  fn raw_name_needs_tokenizer(&self, position: usize) -> bool {
1441    self
1442      .value
1443      .get(position)
1444      .is_some_and(|byte| *byte == 0 || *byte == C_REVERSE_SOLIDUS || !byte.is_ascii())
1445  }
1446
1447  #[inline]
1448  fn scan_raw_numeric_end(&self, start: usize) -> Option<usize> {
1449    let end = self.scan_number_end(start);
1450    if self.value.get(end) == Some(&C_PERCENTAGE) {
1451      return Some(end + 1);
1452    }
1453    if self.starts_ident_at(end) {
1454      let name_end = self.scan_plain_ascii_name(end);
1455      if name_end == end || self.raw_name_needs_tokenizer(name_end) {
1456        return None;
1457      }
1458      return Some(name_end);
1459    }
1460    if self.raw_name_needs_tokenizer(end) {
1461      return None;
1462    }
1463    Some(end)
1464  }
1465
1466  #[inline]
1467  fn scan_name(&self, position: usize) -> usize {
1468    self.scan_name_impl(position, false).0
1469  }
1470
1471  #[inline]
1472  fn scan_name_with_flags(&self, position: usize) -> (usize, TokenFlags) {
1473    self.scan_name_impl(position, true)
1474  }
1475
1476  #[inline]
1477  fn scan_name_impl(&self, mut position: usize, track_flags: bool) -> (usize, TokenFlags) {
1478    let bytes = self.value;
1479    let mut flags = TokenFlags::ascii();
1480    while position < bytes.len() {
1481      while position < bytes.len() {
1482        let byte = bytes[position];
1483        if PLAIN_ASCII_NAME_BYTE[byte as usize] {
1484          position += 1;
1485        } else if byte == 0 {
1486          if track_flags {
1487            flags.mark_null();
1488          }
1489          position += 1;
1490        } else {
1491          break;
1492        }
1493      }
1494      if position == bytes.len() {
1495        break;
1496      }
1497
1498      let byte = bytes[position];
1499      if byte == C_REVERSE_SOLIDUS {
1500        if self.is_valid_escape_at(position) {
1501          if track_flags {
1502            flags.mark_escape();
1503            if bytes.get(position + 1).is_some_and(|next| !next.is_ascii()) {
1504              flags.mark_non_ascii();
1505            }
1506          }
1507          position = self.scan_escape(position);
1508        } else {
1509          break;
1510        }
1511      } else if byte.is_ascii() {
1512        break;
1513      } else {
1514        if track_flags {
1515          flags.mark_non_ascii();
1516        }
1517        position += self.utf8_width_at(position);
1518      }
1519    }
1520    (position, flags)
1521  }
1522
1523  #[inline]
1524  fn starts_ident_at(&self, position: usize) -> bool {
1525    let Some(byte) = self.value.get(position).copied() else {
1526      return false;
1527    };
1528    match byte {
1529      C_HYPHEN_MINUS => match self.value.get(position + 1).copied() {
1530        Some(next) => {
1531          is_name_start_byte(next)
1532            || next == C_HYPHEN_MINUS
1533            || self.is_valid_escape_at(position + 1)
1534        }
1535        None => false,
1536      },
1537      C_REVERSE_SOLIDUS => self.is_valid_escape_at(position),
1538      _ => is_name_start_byte(byte),
1539    }
1540  }
1541
1542  #[inline]
1543  fn starts_number_at(&self, position: usize) -> bool {
1544    let Some(first) = self.value.get(position).copied() else {
1545      return false;
1546    };
1547    let second = self.value.get(position + 1).copied();
1548    let third = self.value.get(position + 2).copied();
1549    match first {
1550      C_PLUS_SIGN | C_HYPHEN_MINUS => {
1551        second.is_some_and(is_digit) || (second == Some(C_FULL_STOP) && third.is_some_and(is_digit))
1552      }
1553      C_FULL_STOP => second.is_some_and(is_digit),
1554      _ => is_digit(first),
1555    }
1556  }
1557
1558  #[inline]
1559  fn is_valid_escape_at(&self, position: usize) -> bool {
1560    self.value.get(position) == Some(&C_REVERSE_SOLIDUS)
1561      && self
1562        .value
1563        .get(position + 1)
1564        .is_some_and(|byte| !is_new_line(*byte))
1565  }
1566
1567  #[inline]
1568  fn scan_escape(&self, position: usize) -> usize {
1569    debug_assert!(self.is_valid_escape_at(position));
1570    let mut end = position + 1;
1571    if self.value[end].is_ascii_hexdigit() {
1572      let mut digits = 0;
1573      while end < self.value.len() && digits < 6 && self.value[end].is_ascii_hexdigit() {
1574        end += 1;
1575        digits += 1;
1576      }
1577      if self
1578        .value
1579        .get(end)
1580        .is_some_and(|byte| is_white_space(*byte))
1581      {
1582        end += 1;
1583        if self.value.get(end - 1) == Some(&C_CARRIAGE_RETURN)
1584          && self.value.get(end) == Some(&C_LINE_FEED)
1585        {
1586          end += 1;
1587        }
1588      }
1589      return end;
1590    }
1591    end + self.utf8_width_at(end)
1592  }
1593
1594  #[inline]
1595  fn utf8_width_at(&self, position: usize) -> usize {
1596    let byte = self.value[position];
1597    if byte < 0x80 {
1598      1
1599    } else if byte < 0xE0 {
1600      2
1601    } else if byte < 0xF0 {
1602      3
1603    } else {
1604      4
1605    }
1606  }
1607}
1608
1609/// The single forward stream used by dependency extraction.
1610///
1611/// The stream owns the main scanner state and gives the dependency scanner one
1612/// token of lookahead without cloning the lexer for every decision. Special
1613/// parsers use the same stream through `next_parser_token`, so consuming a
1614/// subgrammar never creates a second scanner over the source.
1615///
1616/// The cursor is strictly one-directional:
1617///
1618/// ```text
1619/// Lexer::scan_pos         farthest tokenized position, only advances
1620/// TokenStream::consumed   farthest semantically consumed position, only advances
1621/// buffered lookahead      tokens scanned but not yet consumed
1622/// ```
1623///
1624/// `next` consumes from the buffer first and only calls `lexer.next_token`
1625/// when the buffer is empty; it never moves the scanner backwards, so every
1626/// source range is tokenized at most once and every token is consumed at most
1627/// once.
1628pub(crate) struct TokenStream<'a, 's, V: LexerVisitor = ()> {
1629  lexer: &'a mut Lexer<'s, V>,
1630  consumed: Pos,
1631  buffered: VecDeque<TokenWithTrivia>,
1632  generic_value_state: GenericValueScanState,
1633  at_rule_state: GenericValueScanState,
1634  special_value_state: GenericValueScanState,
1635}
1636
1637#[derive(Debug, Default)]
1638pub(crate) struct GenericValueScanState {
1639  parentheses: u32,
1640  squares: u32,
1641  curlies: u32,
1642}
1643
1644#[derive(Debug, Clone, Copy)]
1645struct GenericValueScanOptions {
1646  keep_comments: bool,
1647  preserve_strings: bool,
1648  preserve_delimiters: bool,
1649  property: Option<PropertyKind>,
1650}
1651
1652#[derive(Debug, Clone, Copy)]
1653struct PrescannedIdent {
1654  start: Pos,
1655  end: Pos,
1656  flags: TokenFlags,
1657}
1658
1659impl GenericValueScanState {
1660  #[inline]
1661  fn is_nested(&self) -> bool {
1662    self.parentheses != 0 || self.squares != 0 || self.curlies != 0
1663  }
1664}
1665
1666#[inline]
1667fn is_dependency_value_function(name: &str) -> bool {
1668  let mut lowercase = [0; MAX_CSS_KEYWORD_LEN];
1669  let Some(name) = lowercase_ascii_keyword(name, &mut lowercase) else {
1670    return name.starts_with("--");
1671  };
1672  matches!(name, "url" | "var" | "image-set")
1673    || strip_vendor_prefix(name) == Some("image-set")
1674    || name.starts_with("--")
1675}
1676
1677#[inline(always)]
1678fn never_fast_forward_candidate(_: &str) -> bool {
1679  false
1680}
1681
1682impl<'a, 's, V: LexerVisitor> TokenStream<'a, 's, V> {
1683  pub(crate) fn from_lexer(lexer: &'a mut Lexer<'s, V>) -> Self {
1684    Self {
1685      lexer,
1686      consumed: 0,
1687      buffered: VecDeque::new(),
1688      generic_value_state: GenericValueScanState::default(),
1689      at_rule_state: GenericValueScanState::default(),
1690      special_value_state: GenericValueScanState::default(),
1691    }
1692  }
1693
1694  #[inline(always)]
1695  pub(crate) fn next(&mut self, keep_comments: bool) -> TokenWithTrivia {
1696    let token = if let Some(token) = self.buffered.pop_front() {
1697      token
1698    } else {
1699      self.read_significant(keep_comments)
1700    };
1701    debug_assert!(
1702      token.token.range.start >= self.consumed,
1703      "token range starts before consumed_pos: {:?} < {}",
1704      token.token.range,
1705      self.consumed
1706    );
1707    self.consumed = token.token.range.end;
1708    if matches!(
1709      token.token.kind,
1710      TokenKind::Semicolon | TokenKind::RightCurlyBracket
1711    ) {
1712      self.generic_value_state = GenericValueScanState::default();
1713    }
1714    debug_assert!(
1715      self.consumed <= self.lexer.scan_pos(),
1716      "consumed_pos ({}) exceeded scan_pos ({})",
1717      self.consumed,
1718      self.lexer.scan_pos()
1719    );
1720    token
1721  }
1722
1723  /// Consume the next parser token while folding comments into its leading
1724  /// trivia. This is the equivalent of the old isolated cursor's behavior,
1725  /// but it advances the main stream and therefore preserves one scanner
1726  /// ownership for all subgrammars.
1727  #[inline]
1728  pub(crate) fn next_parser_token(&mut self) -> TokenWithTrivia {
1729    let mut item = self.next(true);
1730    if item.token.kind != TokenKind::Comment {
1731      return item;
1732    }
1733
1734    let start = item.leading.range.start;
1735    let first_comment_start = item
1736      .leading
1737      .first_comment_start
1738      .or(Some(item.token.range.start));
1739    let mut has_white_space = item.leading.has_white_space;
1740    loop {
1741      item = self.next(true);
1742      has_white_space |= item.leading.has_white_space;
1743      if item.token.kind == TokenKind::Comment {
1744        continue;
1745      }
1746      let end = item.leading.end;
1747      item.leading = Trivia {
1748        range: Range::new(start, end),
1749        end,
1750        first_comment_start,
1751        has_white_space,
1752      };
1753      return item;
1754    }
1755  }
1756
1757  #[inline]
1758  pub(crate) fn peek(&mut self, keep_comments: bool) -> TokenWithTrivia {
1759    if self.buffered.is_empty() {
1760      let item = self.read_significant(keep_comments);
1761      self.buffered.push_back(item);
1762    }
1763    *self
1764      .buffered
1765      .front()
1766      .expect("peek must buffer an item before reading the front")
1767  }
1768
1769  /// Peek the next parser token (folding comments into its leading trivia)
1770  /// without consuming it or any preceding comments. The stream state is
1771  /// unchanged: `next` later returns the same tokens in the same order.
1772  #[inline]
1773  pub(crate) fn peek_parser_token(&mut self) -> TokenWithTrivia {
1774    self.peek(true);
1775    let mut leading_start = None;
1776    let mut first_comment_start = None;
1777    let mut has_white_space = false;
1778    let mut index = 0usize;
1779    loop {
1780      if index == self.buffered.len() {
1781        let item = self.read_significant(true);
1782        self.buffered.push_back(item);
1783      }
1784      let item = self.buffered[index];
1785      leading_start.get_or_insert(item.leading.range.start);
1786      has_white_space |= item.leading.has_whitespace();
1787      if matches!(item.token.kind, TokenKind::Comment | TokenKind::BadComment) {
1788        first_comment_start.get_or_insert(item.token.range.start);
1789        index += 1;
1790        continue;
1791      }
1792
1793      let end = item.leading.end;
1794      let mut result = item;
1795      result.leading = Trivia {
1796        range: Range::new(leading_start.unwrap_or(end), end),
1797        end,
1798        first_comment_start: first_comment_start.or(item.leading.first_comment_start),
1799        has_white_space,
1800      };
1801      return result;
1802    }
1803  }
1804
1805  /// Peek at the next significant token, leaving any skipped comments in
1806  /// the buffer for `next` to consume in source order.
1807  #[inline]
1808  pub(crate) fn peek_significant_skipping_comments(
1809    &mut self,
1810    keep_comments: bool,
1811  ) -> TokenWithTrivia {
1812    self.peek(keep_comments);
1813    for item in &self.buffered {
1814      if !matches!(item.token.kind, TokenKind::Comment | TokenKind::BadComment) {
1815        return *item;
1816      }
1817    }
1818    loop {
1819      let item = self.read_significant(keep_comments);
1820      self.buffered.push_back(item);
1821      if !matches!(item.token.kind, TokenKind::Comment | TokenKind::BadComment) {
1822        return item;
1823      }
1824    }
1825  }
1826
1827  /// The position of the next yet-untokenized source byte. The buffer may
1828  /// still hold tokens; callers must drain `next` before fast-forwarding.
1829  #[inline]
1830  pub(crate) fn fast_forward_generic_value_if_buffer_empty<F, C>(
1831    &mut self,
1832    keep_comments: bool,
1833    preserve_strings: bool,
1834    preserve_delimiters: bool,
1835    mut is_candidate: F,
1836    mut is_comment_candidate: C,
1837  ) where
1838    F: FnMut(&str) -> bool,
1839    C: FnMut(&str) -> bool,
1840  {
1841    if !self.buffered.is_empty() {
1842      return;
1843    }
1844    let candidate = self.lexer.fast_forward_generic_value(
1845      &mut self.generic_value_state,
1846      GenericValueScanOptions {
1847        keep_comments,
1848        preserve_strings,
1849        preserve_delimiters,
1850        property: None,
1851      },
1852      false,
1853      &mut is_candidate,
1854      &mut is_comment_candidate,
1855      None,
1856    );
1857    self.finish_value_fast_forward(candidate);
1858  }
1859
1860  #[inline]
1861  pub(crate) fn fast_forward_generic_value_without_candidates_if_buffer_empty(
1862    &mut self,
1863    preserve_strings: bool,
1864    preserve_delimiters: bool,
1865  ) {
1866    if !self.buffered.is_empty() {
1867      return;
1868    }
1869    let candidate = self.lexer.fast_forward_generic_value(
1870      &mut self.generic_value_state,
1871      GenericValueScanOptions {
1872        keep_comments: false,
1873        preserve_strings,
1874        preserve_delimiters,
1875        property: None,
1876      },
1877      false,
1878      never_fast_forward_candidate,
1879      never_fast_forward_candidate,
1880      None,
1881    );
1882    self.finish_value_fast_forward(candidate);
1883  }
1884
1885  #[inline]
1886  pub(crate) fn fast_forward_at_rule_if_buffer_empty<F, C>(
1887    &mut self,
1888    keep_comments: bool,
1889    preserve_strings: bool,
1890    preserve_delimiters: bool,
1891    mut is_candidate: F,
1892    mut is_comment_candidate: C,
1893  ) where
1894    F: FnMut(&str) -> bool,
1895    C: FnMut(&str) -> bool,
1896  {
1897    if !self.buffered.is_empty() {
1898      return;
1899    }
1900    let candidate = self.lexer.fast_forward_generic_value(
1901      &mut self.at_rule_state,
1902      GenericValueScanOptions {
1903        keep_comments,
1904        preserve_strings,
1905        preserve_delimiters,
1906        property: None,
1907      },
1908      true,
1909      &mut is_candidate,
1910      &mut is_comment_candidate,
1911      None,
1912    );
1913    self.finish_value_fast_forward(candidate);
1914  }
1915
1916  #[inline]
1917  pub(crate) fn reset_at_rule_scan_state(&mut self) {
1918    self.at_rule_state = GenericValueScanState::default();
1919  }
1920
1921  #[inline]
1922  pub(crate) fn fast_forward_special_value_if_buffer_empty(
1923    &mut self,
1924    keep_comments: bool,
1925    preserve_strings: bool,
1926    preserve_delimiters: bool,
1927    property: PropertyKind,
1928    icss_symbols: Option<&FxHashSet<&str>>,
1929  ) {
1930    if !self.buffered.is_empty() {
1931      return;
1932    }
1933    let candidate = self.lexer.fast_forward_generic_value(
1934      &mut self.special_value_state,
1935      GenericValueScanOptions {
1936        keep_comments,
1937        preserve_strings,
1938        preserve_delimiters,
1939        property: Some(property),
1940      },
1941      false,
1942      never_fast_forward_candidate,
1943      is_css_modules_pure_magic_comment,
1944      icss_symbols,
1945    );
1946    self.finish_value_fast_forward(candidate);
1947  }
1948
1949  #[inline]
1950  fn finish_value_fast_forward(&mut self, candidate: Option<PrescannedIdent>) {
1951    let Some(candidate) = candidate else {
1952      self.consumed = self.lexer.scan_pos();
1953      return;
1954    };
1955    debug_assert_eq!(candidate.end, self.lexer.scan_pos());
1956    self.consumed = candidate.start;
1957    let range = Range::new(candidate.start, candidate.end);
1958    self.buffered.push_back(TokenWithTrivia {
1959      token: Token::with_flags(TokenKind::Ident, range, range, candidate.flags),
1960      leading: Trivia {
1961        range: Range::new(candidate.start, candidate.start),
1962        end: candidate.start,
1963        first_comment_start: None,
1964        has_white_space: false,
1965      },
1966    });
1967  }
1968
1969  #[inline]
1970  pub(crate) fn reset_special_value_scan_state(&mut self) {
1971    self.special_value_state = GenericValueScanState::default();
1972  }
1973
1974  #[inline]
1975  pub(crate) fn fast_forward_selector_if_buffer_empty<F, C>(
1976    &mut self,
1977    square_depth: &mut u32,
1978    keep_comments: bool,
1979    has_mode: bool,
1980    is_ident_candidate: F,
1981    is_comment_candidate: C,
1982  ) -> bool
1983  where
1984    F: FnMut(&str) -> bool,
1985    C: FnMut(&str) -> bool,
1986  {
1987    if !self.buffered.is_empty() {
1988      return false;
1989    }
1990    if *square_depth == 0 {
1991      let scan_pos = self.lexer.scan_pos();
1992      let Some(next) = self.lexer.byte_at(scan_pos) else {
1993        return false;
1994      };
1995      if matches!(
1996        next,
1997        b'"' | b'\'' | b'(' | b')' | b'{' | b'}' | b',' | b';' | b'@'
1998      ) || (has_mode && matches!(next, b'.' | b'#' | b':'))
1999      {
2000        return false;
2001      }
2002
2003      // A raw scan has a fixed call/setup cost. Keep short, dense
2004      // selector fragments on the regular tokenizer path and only
2005      // enter the scanner when it can skip a useful run. Comments and
2006      // attributes are exceptions because their contents are opaque.
2007      let mut probe = scan_pos;
2008      while probe - scan_pos < 4 {
2009        let Some(byte) = self.lexer.byte_at(probe) else {
2010          return false;
2011        };
2012        if matches!(byte, b'/' | b'[') {
2013          break;
2014        }
2015        if probe != scan_pos
2016          && (matches!(
2017            byte,
2018            b'"' | b'\'' | b'(' | b')' | b'{' | b'}' | b',' | b';' | b'@'
2019          ) || (has_mode && matches!(byte, b'.' | b'#' | b':')))
2020        {
2021          return false;
2022        }
2023        probe += 1;
2024      }
2025    }
2026    let invalidates_composes = self.lexer.fast_forward_selector(
2027      square_depth,
2028      keep_comments,
2029      has_mode,
2030      is_ident_candidate,
2031      is_comment_candidate,
2032    );
2033    self.consumed = self.lexer.scan_pos();
2034    invalidates_composes
2035  }
2036
2037  /// Skip a balanced delimiter run without manufacturing tokens. The opening
2038  /// token must already be consumed, and no further tokens may be buffered.
2039  ///
2040  /// On success `consumed` is advanced to the end of the closing token and
2041  /// the full range is returned. On failure (`None`) the lexer position is
2042  /// unchanged so the caller can re-tokenize the region normally.
2043  pub(crate) fn fast_forward(&mut self, end: TokenKind) -> Option<Range> {
2044    debug_assert!(
2045      self.buffered.is_empty(),
2046      "fast_forward requires an empty lookahead buffer"
2047    );
2048    let old_scan_pos = self.lexer.scan_pos();
2049    let old_consumed = self.consumed;
2050    let range = self.lexer.fast_forward(end)?;
2051    self.consumed = range.end;
2052    debug_assert!(old_scan_pos <= self.lexer.scan_pos());
2053    debug_assert!(old_consumed <= self.consumed);
2054    debug_assert!(self.consumed <= self.lexer.scan_pos());
2055    Some(range)
2056  }
2057
2058  /// The farthest position consumed by `next`. This is the current semantic
2059  /// position of the dependency scanner.
2060  #[inline]
2061  pub(crate) fn consumed_pos(&self) -> Pos {
2062    self.consumed
2063  }
2064
2065  #[inline]
2066  pub(crate) fn lexer(&self) -> &Lexer<'s, V> {
2067    self.lexer
2068  }
2069
2070  #[inline]
2071  pub(crate) fn lexer_mut(&mut self) -> &mut Lexer<'s, V> {
2072    self.lexer
2073  }
2074
2075  #[inline]
2076  pub(crate) fn source_end(&self) -> Pos {
2077    self.lexer.source_end()
2078  }
2079
2080  #[inline(always)]
2081  pub(crate) fn byte_at(&self, position: Pos) -> Option<u8> {
2082    self.lexer.byte_at(position)
2083  }
2084
2085  #[inline]
2086  pub(crate) fn slice(&self, start: Pos, end: Pos) -> Option<&'s str> {
2087    self.lexer.slice(start, end)
2088  }
2089
2090  #[inline(always)]
2091  pub(crate) fn slice_trusted(&self, start: Pos, end: Pos) -> &'s str {
2092    self.lexer.slice_trusted(start, end)
2093  }
2094
2095  #[inline(always)]
2096  fn read_significant(&mut self, keep_comments: bool) -> TokenWithTrivia {
2097    let start = self.lexer.scan_pos();
2098    let mut end = start;
2099    let mut first_comment_start = None;
2100    let mut has_white_space = false;
2101    loop {
2102      let token = self.lexer.next_token();
2103      debug_assert!(token.range.start >= end);
2104      if token.kind == TokenKind::WhiteSpace {
2105        has_white_space = true;
2106        end = token.range.end;
2107        continue;
2108      }
2109      if !keep_comments && matches!(token.kind, TokenKind::Comment | TokenKind::BadComment) {
2110        first_comment_start.get_or_insert(token.range.start);
2111        end = token.range.end;
2112        continue;
2113      }
2114      let leading = Trivia {
2115        range: Range::new(start, end),
2116        end,
2117        first_comment_start,
2118        has_white_space,
2119      };
2120      return TokenWithTrivia { token, leading };
2121    }
2122  }
2123}
2124
2125pub fn is_new_line(c: u8) -> bool {
2126  c == C_LINE_FEED || c == C_CARRIAGE_RETURN || c == C_FORM_FEED
2127}
2128
2129#[inline]
2130fn kind_to_right(byte: u8) -> TokenKind {
2131  match byte {
2132    C_RIGHT_PARENTHESIS => TokenKind::RightParenthesis,
2133    C_RIGHT_SQUARE => TokenKind::RightSquareBracket,
2134    C_RIGHT_CURLY => TokenKind::RightCurlyBracket,
2135    _ => unreachable!("fast_forward only sees closing bracket bytes"),
2136  }
2137}
2138
2139pub fn is_space(c: u8) -> bool {
2140  c == C_TAB || c == C_SPACE
2141}
2142
2143pub fn is_white_space(c: u8) -> bool {
2144  is_new_line(c) || is_space(c)
2145}
2146
2147pub fn is_digit(c: u8) -> bool {
2148  c.is_ascii_digit()
2149}
2150
2151#[inline]
2152fn is_name_start_byte(c: u8) -> bool {
2153  c == 0 || c == C_LOW_LINE || c.is_ascii_alphabetic() || !c.is_ascii()
2154}
2155
2156#[inline]
2157fn is_non_printable(c: u8) -> bool {
2158  matches!(c, 0x00..=0x08 | 0x0B | 0x0E..=0x1F | 0x7F)
2159}
2160
2161#[cfg(test)]
2162#[path = "../tests/lexer_tests.rs"]
2163mod tests;