Skip to main content

css_module_lexer/
lexer.rs

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