Skip to main content

css_module_lexer/
lexer.rs

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