Skip to main content

cssparser/
tokenizer.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5// https://drafts.csswg.org/css-syntax/#tokenization
6
7use self::Token::*;
8use crate::cow_rc_str::CowRcStr;
9use crate::parser::{ArbitrarySubstitutionFunctions, ParserState};
10use std::char;
11use std::ops::Range;
12
13#[cfg(feature = "fast_match_byte")]
14pub use crate::match_byte;
15
16/// One of the pieces the CSS input is broken into.
17///
18/// Some components use `Cow` in order to borrow from the original input string
19/// and avoid allocating/copying when possible.
20#[derive(PartialEq, Debug, Clone)]
21pub enum Token<'a> {
22    /// A [`<ident-token>`](https://drafts.csswg.org/css-syntax/#ident-token-diagram)
23    Ident(CowRcStr<'a>),
24
25    /// A [`<at-keyword-token>`](https://drafts.csswg.org/css-syntax/#at-keyword-token-diagram)
26    ///
27    /// The value does not include the `@` marker.
28    AtKeyword(CowRcStr<'a>),
29
30    /// A [`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "unrestricted"
31    ///
32    /// The value does not include the `#` marker.
33    Hash(CowRcStr<'a>),
34
35    /// A [`<hash-token>`](https://drafts.csswg.org/css-syntax/#hash-token-diagram) with the type flag set to "id"
36    ///
37    /// The value does not include the `#` marker.
38    IDHash(CowRcStr<'a>), // Hash that is a valid ID selector.
39
40    /// A [`<string-token>`](https://drafts.csswg.org/css-syntax/#string-token-diagram)
41    ///
42    /// The value does not include the quotes.
43    QuotedString(CowRcStr<'a>),
44
45    /// A [`<url-token>`](https://drafts.csswg.org/css-syntax/#url-token-diagram)
46    ///
47    /// The value does not include the `url(` `)` markers.  Note that `url( <string-token> )` is represented by a
48    /// `Function` token.
49    UnquotedUrl(CowRcStr<'a>),
50
51    /// A `<delim-token>`
52    Delim(char),
53
54    /// A [`<number-token>`](https://drafts.csswg.org/css-syntax/#number-token-diagram)
55    Number {
56        /// Whether the number had a `+` or `-` sign.
57        ///
58        /// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)
59        has_sign: bool,
60
61        /// The value as a float
62        value: f32,
63
64        /// If the origin source did not include a fractional part, the value as an integer.
65        int_value: Option<i32>,
66    },
67
68    /// A [`<percentage-token>`](https://drafts.csswg.org/css-syntax/#percentage-token-diagram)
69    Percentage {
70        /// Whether the number had a `+` or `-` sign.
71        has_sign: bool,
72
73        /// The value as a float, divided by 100 so that the nominal range is 0.0 to 1.0.
74        unit_value: f32,
75
76        /// If the origin source did not include a fractional part, the value as an integer.
77        /// It is **not** divided by 100.
78        int_value: Option<i32>,
79    },
80
81    /// A [`<dimension-token>`](https://drafts.csswg.org/css-syntax/#dimension-token-diagram)
82    Dimension {
83        /// Whether the number had a `+` or `-` sign.
84        ///
85        /// This is used is some cases like the <An+B> micro syntax. (See the `parse_nth` function.)
86        has_sign: bool,
87
88        /// The value as a float
89        value: f32,
90
91        /// If the origin source did not include a fractional part, the value as an integer.
92        int_value: Option<i32>,
93
94        /// The unit, e.g. "px" in `12px`
95        unit: CowRcStr<'a>,
96    },
97
98    /// A [`<whitespace-token>`](https://drafts.csswg.org/css-syntax/#whitespace-token-diagram)
99    WhiteSpace(&'a str),
100
101    /// A comment.
102    ///
103    /// The CSS Syntax spec does not generate tokens for comments,
104    /// But we do, because we can (borrowed &str makes it cheap).
105    ///
106    /// The value does not include the `/*` `*/` markers.
107    Comment(&'a str),
108
109    /// A `:` `<colon-token>`
110    Colon, // :
111
112    /// A `;` `<semicolon-token>`
113    Semicolon, // ;
114
115    /// A `,` `<comma-token>`
116    Comma, // ,
117
118    /// A `~=` [`<include-match-token>`](https://drafts.csswg.org/css-syntax/#include-match-token-diagram)
119    IncludeMatch,
120
121    /// A `|=` [`<dash-match-token>`](https://drafts.csswg.org/css-syntax/#dash-match-token-diagram)
122    DashMatch,
123
124    /// A `^=` [`<prefix-match-token>`](https://drafts.csswg.org/css-syntax/#prefix-match-token-diagram)
125    PrefixMatch,
126
127    /// A `$=` [`<suffix-match-token>`](https://drafts.csswg.org/css-syntax/#suffix-match-token-diagram)
128    SuffixMatch,
129
130    /// A `*=` [`<substring-match-token>`](https://drafts.csswg.org/css-syntax/#substring-match-token-diagram)
131    SubstringMatch,
132
133    /// A `<!--` [`<CDO-token>`](https://drafts.csswg.org/css-syntax/#CDO-token-diagram)
134    CDO,
135
136    /// A `-->` [`<CDC-token>`](https://drafts.csswg.org/css-syntax/#CDC-token-diagram)
137    CDC,
138
139    /// A [`<function-token>`](https://drafts.csswg.org/css-syntax/#function-token-diagram)
140    ///
141    /// The value (name) does not include the `(` marker.
142    Function(CowRcStr<'a>),
143
144    /// A `<(-token>`
145    ParenthesisBlock,
146
147    /// A `<[-token>`
148    SquareBracketBlock,
149
150    /// A `<{-token>`
151    CurlyBracketBlock,
152
153    /// A `<bad-url-token>`
154    ///
155    /// This token always indicates a parse error.
156    BadUrl(CowRcStr<'a>),
157
158    /// A `<bad-string-token>`
159    ///
160    /// This token always indicates a parse error.
161    BadString(CowRcStr<'a>),
162
163    /// A `<)-token>`
164    ///
165    /// When obtained from one of the `Parser::next*` methods,
166    /// this token is always unmatched and indicates a parse error.
167    CloseParenthesis,
168
169    /// A `<]-token>`
170    ///
171    /// When obtained from one of the `Parser::next*` methods,
172    /// this token is always unmatched and indicates a parse error.
173    CloseSquareBracket,
174
175    /// A `<}-token>`
176    ///
177    /// When obtained from one of the `Parser::next*` methods,
178    /// this token is always unmatched and indicates a parse error.
179    CloseCurlyBracket,
180}
181
182impl Token<'_> {
183    /// Return whether this token represents a parse error.
184    ///
185    /// `BadUrl` and `BadString` are tokenizer-level parse errors.
186    ///
187    /// `CloseParenthesis`, `CloseSquareBracket`, and `CloseCurlyBracket` are *unmatched*
188    /// and therefore parse errors when returned by one of the `Parser::next*` methods.
189    pub fn is_parse_error(&self) -> bool {
190        matches!(
191            *self,
192            BadUrl(_) | BadString(_) | CloseParenthesis | CloseSquareBracket | CloseCurlyBracket
193        )
194    }
195}
196
197#[derive(Clone)]
198pub struct Tokenizer<'a> {
199    input: &'a str,
200    /// Counted in bytes, not code points. From 0.
201    position: usize,
202    /// The position at the start of the current line; but adjusted to
203    /// ensure that computing the column will give the result in units
204    /// of UTF-16 characters.
205    current_line_start_position: usize,
206    current_line_number: u32,
207    arbitrary_substitution_functions: SeenStatus<'a>,
208    source_map_url: Option<&'a str>,
209    source_url: Option<&'a str>,
210}
211
212#[derive(Copy, Clone, PartialEq, Eq)]
213enum SeenStatus<'a> {
214    DontCare,
215    LookingForThem(ArbitrarySubstitutionFunctions<'a>),
216    SeenAtLeastOne,
217}
218
219impl<'a> Tokenizer<'a> {
220    #[inline]
221    pub fn new(input: &'a str) -> Self {
222        Tokenizer {
223            input,
224            position: 0,
225            current_line_start_position: 0,
226            current_line_number: 0,
227            arbitrary_substitution_functions: SeenStatus::DontCare,
228            source_map_url: None,
229            source_url: None,
230        }
231    }
232
233    #[inline]
234    pub fn look_for_arbitrary_substitution_functions(
235        &mut self,
236        fns: ArbitrarySubstitutionFunctions<'a>,
237    ) {
238        self.arbitrary_substitution_functions = SeenStatus::LookingForThem(fns);
239    }
240
241    #[inline]
242    pub fn seen_arbitrary_substitution_functions(&mut self) -> bool {
243        let seen = self.arbitrary_substitution_functions == SeenStatus::SeenAtLeastOne;
244        self.arbitrary_substitution_functions = SeenStatus::DontCare;
245        seen
246    }
247
248    #[inline]
249    pub fn see_function(&mut self, name: &str) {
250        if let SeenStatus::LookingForThem(fns) = self.arbitrary_substitution_functions {
251            if fns.iter().any(|a| name.eq_ignore_ascii_case(a)) {
252                self.arbitrary_substitution_functions = SeenStatus::SeenAtLeastOne;
253            }
254        }
255    }
256
257    #[inline]
258    pub fn next(&mut self) -> Result<Token<'a>, ()> {
259        if self.is_eof() {
260            return Err(());
261        }
262        Ok(self.next_unchecked())
263    }
264
265    #[inline]
266    pub fn next_unchecked(&mut self) -> Token<'a> {
267        next_token_unchecked(self)
268    }
269
270    #[inline]
271    pub fn position(&self) -> SourcePosition {
272        debug_assert!(self.input.is_char_boundary(self.position));
273        SourcePosition(self.position)
274    }
275
276    #[inline]
277    pub fn current_source_location(&self) -> SourceLocation {
278        SourceLocation {
279            line: self.current_line_number,
280            column: (self.position - self.current_line_start_position + 1) as u32,
281        }
282    }
283
284    #[inline]
285    pub fn current_source_map_url(&self) -> Option<&'a str> {
286        self.source_map_url
287    }
288
289    #[inline]
290    pub fn current_source_url(&self) -> Option<&'a str> {
291        self.source_url
292    }
293
294    #[inline]
295    pub fn state(&self) -> ParserState {
296        ParserState {
297            position: self.position,
298            current_line_start_position: self.current_line_start_position,
299            current_line_number: self.current_line_number,
300            at_start_of: None,
301        }
302    }
303
304    #[inline]
305    pub fn reset(&mut self, state: &ParserState) {
306        self.position = state.position;
307        self.current_line_start_position = state.current_line_start_position;
308        self.current_line_number = state.current_line_number;
309    }
310
311    #[inline]
312    pub(crate) fn slice_from(&self, start_pos: SourcePosition) -> &'a str {
313        self.slice(start_pos..self.position())
314    }
315
316    #[inline]
317    pub(crate) fn slice(&self, range: Range<SourcePosition>) -> &'a str {
318        debug_assert!(self.input.is_char_boundary(range.start.0));
319        debug_assert!(self.input.is_char_boundary(range.end.0));
320        unsafe { self.input.get_unchecked(range.start.0..range.end.0) }
321    }
322
323    #[inline]
324    pub(crate) fn byte_slice(&self, range: Range<usize>) -> &'a [u8] {
325        &self.input.as_bytes()[range]
326    }
327
328    #[inline]
329    pub(crate) fn byte_slice_from(&self, start: usize) -> &'a [u8] {
330        self.byte_slice(start..self.position)
331    }
332
333    pub fn current_source_line(&self) -> &'a str {
334        let current = self.position();
335        let start = self
336            .slice(SourcePosition(0)..current)
337            .rfind(['\r', '\n', '\x0C'])
338            .map_or(0, |start| start + 1);
339        let end = self
340            .slice(current..SourcePosition(self.input.len()))
341            .find(['\r', '\n', '\x0C'])
342            .map_or(self.input.len(), |end| current.0 + end);
343        self.slice(SourcePosition(start)..SourcePosition(end))
344    }
345
346    #[inline]
347    pub fn next_byte(&self) -> Option<u8> {
348        if self.is_eof() {
349            None
350        } else {
351            Some(self.input.as_bytes()[self.position])
352        }
353    }
354
355    // If false, `tokenizer.next_char()` will not panic.
356    #[inline]
357    fn is_eof(&self) -> bool {
358        !self.has_at_least(0)
359    }
360
361    // If true, the input has at least `n` bytes left *after* the current one.
362    // That is, `tokenizer.char_at(n)` will not panic.
363    #[inline]
364    fn has_at_least(&self, n: usize) -> bool {
365        self.position + n < self.input.len()
366    }
367
368    // Advance over N bytes in the input.  This function can advance
369    // over ASCII bytes (excluding newlines), or UTF-8 sequence
370    // leaders (excluding leaders for 4-byte sequences).
371    #[inline]
372    pub fn advance(&mut self, n: usize) {
373        if cfg!(debug_assertions) {
374            // Each byte must either be an ASCII byte or a sequence
375            // leader, but not a 4-byte leader; also newlines are
376            // rejected.
377            for i in 0..n {
378                let b = self.byte_at(i);
379                debug_assert!(b.is_ascii() || (b & 0xF0 != 0xF0 && b & 0xC0 != 0x80));
380                debug_assert!(b != b'\r' && b != b'\n' && b != b'\x0C');
381            }
382        }
383        self.position += n
384    }
385
386    /// Equivalent to calling advance() for runs of bytes for which `matches` returns true.
387    /// Returns the byte slice advanced over.
388    fn advance_while(&mut self, mut matches: impl FnMut(u8) -> bool) -> &[u8] {
389        let start = self.position;
390        let mut position = start;
391
392        let bytes = &self.input.as_bytes()[start..];
393        for b in bytes {
394            if !matches(*b) {
395                break;
396            }
397            position += 1;
398        }
399
400        // Equivalent to self.position = position, but with advance()'s debug_assert!s
401        self.advance(position - self.position);
402
403        self.byte_slice_from(start)
404    }
405
406    // Assumes non-EOF
407    #[inline]
408    fn next_byte_unchecked(&self) -> u8 {
409        self.byte_at(0)
410    }
411
412    #[inline]
413    fn byte_at(&self, offset: usize) -> u8 {
414        self.input.as_bytes()[self.position + offset]
415    }
416
417    // Advance over a single byte; the byte must be a UTF-8 sequence
418    // leader for a 4-byte sequence.
419    #[inline]
420    fn consume_4byte_intro(&mut self) {
421        debug_assert!(self.next_byte_unchecked() & 0xF0 == 0xF0);
422        // This takes two UTF-16 characters to represent, so we
423        // actually have an undercount.
424        self.current_line_start_position = self.current_line_start_position.wrapping_sub(1);
425        self.position += 1;
426    }
427
428    // Advance over a single byte; the byte must be a UTF-8
429    // continuation byte.
430    #[inline]
431    fn consume_continuation_byte(&mut self) {
432        debug_assert!(self.next_byte_unchecked() & 0xC0 == 0x80);
433        // Continuation bytes contribute to column overcount.  Note
434        // that due to the special case for the 4-byte sequence intro,
435        // we must use wrapping add here.
436        self.current_line_start_position = self.current_line_start_position.wrapping_add(1);
437        self.position += 1;
438    }
439
440    // Advance over any kind of byte, excluding newlines.
441    #[inline(never)]
442    fn consume_known_byte(&mut self, byte: u8) {
443        debug_assert!(byte != b'\r' && byte != b'\n' && byte != b'\x0C');
444        self.position += 1;
445        // Continuation bytes contribute to column overcount.
446        if byte & 0xF0 == 0xF0 {
447            // This takes two UTF-16 characters to represent, so we
448            // actually have an undercount.
449            self.current_line_start_position = self.current_line_start_position.wrapping_sub(1);
450        } else if byte & 0xC0 == 0x80 {
451            // Note that due to the special case for the 4-byte
452            // sequence intro, we must use wrapping add here.
453            self.current_line_start_position = self.current_line_start_position.wrapping_add(1);
454        }
455    }
456
457    #[inline]
458    fn next_char(&self) -> char {
459        unsafe { self.input.get_unchecked(self.position().0..) }
460            .chars()
461            .next()
462            .unwrap()
463    }
464
465    // Given that a newline has been seen, advance over the newline
466    // and update the state.
467    #[inline]
468    fn consume_newline(&mut self) {
469        let byte = self.next_byte_unchecked();
470        debug_assert!(byte == b'\r' || byte == b'\n' || byte == b'\x0C');
471        self.position += 1;
472        if byte == b'\r' && self.next_byte() == Some(b'\n') {
473            self.position += 1;
474        }
475        self.current_line_start_position = self.position;
476        self.current_line_number += 1;
477    }
478
479    #[inline]
480    fn has_newline_at(&self, offset: usize) -> bool {
481        self.position + offset < self.input.len()
482            && matches!(self.byte_at(offset), b'\n' | b'\r' | b'\x0C')
483    }
484
485    #[inline]
486    fn consume_char(&mut self) -> char {
487        let c = self.next_char();
488        let len_utf8 = c.len_utf8();
489        self.position += len_utf8;
490        // Note that due to the special case for the 4-byte sequence
491        // intro, we must use wrapping add here.
492        self.current_line_start_position = self
493            .current_line_start_position
494            .wrapping_add(len_utf8 - c.len_utf16());
495        c
496    }
497
498    #[inline]
499    fn starts_with(&self, needle: &[u8]) -> bool {
500        self.input.as_bytes()[self.position..].starts_with(needle)
501    }
502
503    pub fn skip_whitespace(&mut self) {
504        while !self.is_eof() {
505            match_byte! { self.next_byte_unchecked(),
506                b' ' | b'\t' => {
507                    self.advance(1)
508                },
509                b'\n' | b'\x0C' | b'\r' => {
510                    self.consume_newline();
511                },
512                b'/' => {
513                    if self.starts_with(b"/*") {
514                        consume_comment(self);
515                    } else {
516                        return
517                    }
518                }
519                _ => return,
520            }
521        }
522    }
523
524    pub fn skip_cdc_and_cdo(&mut self) {
525        while !self.is_eof() {
526            match_byte! { self.next_byte_unchecked(),
527                b' ' | b'\t' => {
528                    self.advance(1)
529                },
530                b'\n' | b'\x0C' | b'\r' => {
531                    self.consume_newline();
532                },
533                b'/' => {
534                    if self.starts_with(b"/*") {
535                        consume_comment(self);
536                    } else {
537                        return
538                    }
539                }
540                b'<' => {
541                    if self.starts_with(b"<!--") {
542                        self.advance(4)
543                    } else {
544                        return
545                    }
546                }
547                b'-' => {
548                    if self.starts_with(b"-->") {
549                        self.advance(3)
550                    } else {
551                        return
552                    }
553                }
554                _ => {
555                    return
556                }
557            }
558        }
559    }
560}
561
562/// A position from the start of the input, counted in UTF-8 bytes.
563#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
564pub struct SourcePosition(pub(crate) usize);
565
566#[cfg(feature = "malloc_size_of")]
567malloc_size_of::malloc_size_of_is_0!(SourcePosition);
568
569impl SourcePosition {
570    /// Returns the current byte index in the original input.
571    #[inline]
572    pub fn byte_index(&self) -> usize {
573        self.0
574    }
575}
576
577/// The line and column number for a given position within the input.
578#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)]
579pub struct SourceLocation {
580    /// The line number, starting at 0 for the first line.
581    pub line: u32,
582
583    /// The column number within a line, starting at 1 for first the character of the line.
584    /// Column numbers are counted in UTF-16 code units.
585    pub column: u32,
586}
587
588#[cfg(feature = "malloc_size_of")]
589malloc_size_of::malloc_size_of_is_0!(SourceLocation);
590
591fn next_token_unchecked<'a>(tokenizer: &mut Tokenizer<'a>) -> Token<'a> {
592    debug_assert!(!tokenizer.is_eof());
593    let b = tokenizer.next_byte_unchecked();
594    let token = match_byte! { b,
595        b' ' | b'\t' => {
596            consume_whitespace(tokenizer, false)
597        },
598        b'\n' | b'\x0C' | b'\r' => consume_whitespace(tokenizer, true),
599        b'"' => consume_string(tokenizer, false),
600        b'#' => {
601            tokenizer.advance(1);
602            if is_ident_start(tokenizer) { IDHash(consume_name(tokenizer)) }
603            else if !tokenizer.is_eof() &&
604                matches!(tokenizer.next_byte_unchecked(), b'0'..=b'9' | b'-') {
605                // Any other valid case here already resulted in IDHash.
606                Hash(consume_name(tokenizer))
607            }
608            else { Delim('#') }
609        },
610        b'$' => {
611            if tokenizer.starts_with(b"$=") { tokenizer.advance(2); SuffixMatch }
612            else { tokenizer.advance(1); Delim('$') }
613        },
614        b'\'' => consume_string(tokenizer, true),
615        b'(' => { tokenizer.advance(1); ParenthesisBlock },
616        b')' => { tokenizer.advance(1); CloseParenthesis },
617        b'*' => {
618            if tokenizer.starts_with(b"*=") { tokenizer.advance(2); SubstringMatch }
619            else { tokenizer.advance(1); Delim('*') }
620        },
621        b'+' => {
622            if (
623                tokenizer.has_at_least(1)
624                && tokenizer.byte_at(1).is_ascii_digit()
625            ) || (
626                tokenizer.has_at_least(2)
627                && tokenizer.byte_at(1) == b'.'
628                && tokenizer.byte_at(2).is_ascii_digit()
629            ) {
630                consume_numeric(tokenizer)
631            } else {
632                tokenizer.advance(1);
633                Delim('+')
634            }
635        },
636        b',' => { tokenizer.advance(1); Comma },
637        b'-' => {
638            if (
639                tokenizer.has_at_least(1)
640                && tokenizer.byte_at(1).is_ascii_digit()
641            ) || (
642                tokenizer.has_at_least(2)
643                && tokenizer.byte_at(1) == b'.'
644                && tokenizer.byte_at(2).is_ascii_digit()
645            ) {
646                consume_numeric(tokenizer)
647            } else if tokenizer.starts_with(b"-->") {
648                tokenizer.advance(3);
649                CDC
650            } else if is_ident_start(tokenizer) {
651                consume_ident_like(tokenizer)
652            } else {
653                tokenizer.advance(1);
654                Delim('-')
655            }
656        },
657        b'.' => {
658            if tokenizer.has_at_least(1)
659                && tokenizer.byte_at(1).is_ascii_digit() {
660                consume_numeric(tokenizer)
661            } else {
662                tokenizer.advance(1);
663                Delim('.')
664            }
665        }
666        b'/' => {
667            if tokenizer.starts_with(b"/*") {
668                Comment(consume_comment(tokenizer))
669            } else {
670                tokenizer.advance(1);
671                Delim('/')
672            }
673        }
674        b'0'..=b'9' => consume_numeric(tokenizer),
675        b':' => { tokenizer.advance(1); Colon },
676        b';' => { tokenizer.advance(1); Semicolon },
677        b'<' => {
678            if tokenizer.starts_with(b"<!--") {
679                tokenizer.advance(4);
680                CDO
681            } else {
682                tokenizer.advance(1);
683                Delim('<')
684            }
685        },
686        b'@' => {
687            tokenizer.advance(1);
688            if is_ident_start(tokenizer) { AtKeyword(consume_name(tokenizer)) }
689            else { Delim('@') }
690        },
691        b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'\0' => consume_ident_like(tokenizer),
692        b'[' => { tokenizer.advance(1); SquareBracketBlock },
693        b'\\' => {
694            if !tokenizer.has_newline_at(1) { consume_ident_like(tokenizer) }
695            else { tokenizer.advance(1); Delim('\\') }
696        },
697        b']' => { tokenizer.advance(1); CloseSquareBracket },
698        b'^' => {
699            if tokenizer.starts_with(b"^=") { tokenizer.advance(2); PrefixMatch }
700            else { tokenizer.advance(1); Delim('^') }
701        },
702        b'{' => { tokenizer.advance(1); CurlyBracketBlock },
703        b'|' => {
704            if tokenizer.starts_with(b"|=") { tokenizer.advance(2); DashMatch }
705            else { tokenizer.advance(1); Delim('|') }
706        },
707        b'}' => { tokenizer.advance(1); CloseCurlyBracket },
708        b'~' => {
709            if tokenizer.starts_with(b"~=") { tokenizer.advance(2); IncludeMatch }
710            else { tokenizer.advance(1); Delim('~') }
711        },
712        _ => {
713            if !b.is_ascii() {
714                consume_ident_like(tokenizer)
715            } else {
716                tokenizer.advance(1);
717                Delim(b as char)
718            }
719        },
720    };
721    token
722}
723
724fn consume_whitespace<'a>(tokenizer: &mut Tokenizer<'a>, newline: bool) -> Token<'a> {
725    let start_position = tokenizer.position();
726    if newline {
727        tokenizer.consume_newline();
728    } else {
729        tokenizer.advance(1);
730    }
731    while !tokenizer.is_eof() {
732        let b = tokenizer.next_byte_unchecked();
733        match_byte! { b,
734            b' ' | b'\t' => {
735                tokenizer.advance(1);
736            }
737            b'\n' | b'\x0C' | b'\r' => {
738                tokenizer.consume_newline();
739            }
740            _ => {
741                break
742            }
743        }
744    }
745    WhiteSpace(tokenizer.slice_from(start_position))
746}
747
748// Check for sourceMappingURL or sourceURL comments and update the
749// tokenizer appropriately.
750fn check_for_source_map<'a>(tokenizer: &mut Tokenizer<'a>, contents: &'a str) {
751    let directive = "# sourceMappingURL=";
752    let directive_old = "@ sourceMappingURL=";
753
754    // If there is a source map directive, extract the URL.
755    if contents.starts_with(directive) || contents.starts_with(directive_old) {
756        let contents = &contents[directive.len()..];
757        tokenizer.source_map_url = contents.split([' ', '\t', '\x0C', '\r', '\n']).next();
758    }
759
760    let directive = "# sourceURL=";
761    let directive_old = "@ sourceURL=";
762
763    // If there is a source map directive, extract the URL.
764    if contents.starts_with(directive) || contents.starts_with(directive_old) {
765        let contents = &contents[directive.len()..];
766        tokenizer.source_url = contents.split([' ', '\t', '\x0C', '\r', '\n']).next()
767    }
768}
769
770fn consume_comment<'a>(tokenizer: &mut Tokenizer<'a>) -> &'a str {
771    tokenizer.advance(2); // consume "/*"
772    let start_position = tokenizer.position();
773    while !tokenizer.is_eof() {
774        match_byte! { tokenizer.next_byte_unchecked(),
775            b'*' => {
776                let end_position = tokenizer.position();
777                tokenizer.advance(1);
778                if tokenizer.next_byte() == Some(b'/') {
779                    tokenizer.advance(1);
780                    let contents = tokenizer.slice(start_position..end_position);
781                    check_for_source_map(tokenizer, contents);
782                    return contents
783                }
784            }
785            b'\n' | b'\x0C' | b'\r' => {
786                tokenizer.consume_newline();
787            }
788            b'\x80'..=b'\xBF' => { tokenizer.consume_continuation_byte(); }
789            b'\xF0'..=b'\xFF' => { tokenizer.consume_4byte_intro(); }
790            _ => {
791                // ASCII or other leading byte.
792                tokenizer.advance(1);
793            }
794        }
795    }
796    let contents = tokenizer.slice_from(start_position);
797    check_for_source_map(tokenizer, contents);
798    contents
799}
800
801fn consume_string<'a>(tokenizer: &mut Tokenizer<'a>, single_quote: bool) -> Token<'a> {
802    match consume_quoted_string(tokenizer, single_quote) {
803        Ok(value) => QuotedString(value),
804        Err(value) => BadString(value),
805    }
806}
807
808/// Return `Err(())` on syntax error (ie. unescaped newline)
809fn consume_quoted_string<'a>(
810    tokenizer: &mut Tokenizer<'a>,
811    single_quote: bool,
812) -> Result<CowRcStr<'a>, CowRcStr<'a>> {
813    tokenizer.advance(1); // Skip the initial quote
814    // start_pos is at code point boundary, after " or '
815    let start_pos = tokenizer.position();
816    let mut string_bytes;
817    loop {
818        if tokenizer.is_eof() {
819            return Ok(tokenizer.slice_from(start_pos).into());
820        }
821        match_byte! { tokenizer.next_byte_unchecked(),
822            b'"' => {
823                if !single_quote {
824                    let value = tokenizer.slice_from(start_pos);
825                    tokenizer.advance(1);
826                    return Ok(value.into())
827                }
828                tokenizer.advance(1);
829            }
830            b'\'' => {
831                if single_quote {
832                    let value = tokenizer.slice_from(start_pos);
833                    tokenizer.advance(1);
834                    return Ok(value.into())
835                }
836                tokenizer.advance(1);
837            }
838            b'\\' | b'\0' => {
839                // * The tokenizer’s input is UTF-8 since it’s `&str`.
840                // * start_pos is at a code point boundary
841                // * so is the current position (which is before '\\' or '\0'
842                //
843                // So `string_bytes` is well-formed UTF-8.
844                string_bytes = tokenizer.slice_from(start_pos).as_bytes().to_owned();
845                break
846            }
847            b'\n' | b'\r' | b'\x0C' => {
848                return Err(tokenizer.slice_from(start_pos).into())
849            },
850            b'\x80'..=b'\xBF' => { tokenizer.consume_continuation_byte(); }
851            b'\xF0'..=b'\xFF' => { tokenizer.consume_4byte_intro(); }
852            _ => {
853                // ASCII or other leading byte.
854                tokenizer.advance(1);
855            }
856        }
857    }
858
859    while !tokenizer.is_eof() {
860        let b = tokenizer.next_byte_unchecked();
861        match_byte! { b,
862            b'\n' | b'\r' | b'\x0C' => {
863                return Err(
864                    // string_bytes is well-formed UTF-8, see other comments.
865                    unsafe {
866                        from_utf8_release_unchecked(string_bytes)
867                    }.into()
868                );
869            }
870            b'"' => {
871                tokenizer.advance(1);
872                if !single_quote {
873                    break;
874                }
875            }
876            b'\'' => {
877                tokenizer.advance(1);
878                if single_quote {
879                    break;
880                }
881            }
882            b'\\' => {
883                tokenizer.advance(1);
884                if !tokenizer.is_eof() {
885                    match tokenizer.next_byte_unchecked() {
886                        // Escaped newline
887                        b'\n' | b'\x0C' | b'\r' => {
888                            tokenizer.consume_newline();
889                        }
890                        // This pushes one well-formed code point
891                        _ => consume_escape_and_write(tokenizer, &mut string_bytes)
892                    }
893                }
894                // else: escaped EOF, do nothing.
895                continue;
896            }
897            b'\0' => {
898                tokenizer.advance(1);
899                string_bytes.extend("\u{FFFD}".as_bytes());
900                continue;
901            }
902            b'\x80'..=b'\xBF' => { tokenizer.consume_continuation_byte(); }
903            b'\xF0'..=b'\xFF' => { tokenizer.consume_4byte_intro(); }
904            _ => {
905                // ASCII or other leading byte.
906                tokenizer.advance(1);
907            },
908        }
909
910        // If this byte is part of a multi-byte code point,
911        // we’ll end up copying the whole code point before this loop does something else.
912        string_bytes.push(b);
913    }
914
915    Ok(
916        // string_bytes is well-formed UTF-8, see other comments.
917        unsafe { from_utf8_release_unchecked(string_bytes) }.into(),
918    )
919}
920
921#[inline]
922fn is_ident_start(tokenizer: &Tokenizer) -> bool {
923    !tokenizer.is_eof()
924        && match_byte! { tokenizer.next_byte_unchecked(),
925            b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'\0' => true,
926            b'-' => {
927                tokenizer.has_at_least(1) && match_byte! { tokenizer.byte_at(1),
928                    b'a'..=b'z' | b'A'..=b'Z' | b'-' | b'_' | b'\0' => {
929                        true
930                    }
931                    b'\\' => !tokenizer.has_newline_at(1),
932                    b => !b.is_ascii(),
933                }
934            },
935            b'\\' => !tokenizer.has_newline_at(1),
936            b => !b.is_ascii(),
937        }
938}
939
940fn consume_ident_like<'a>(tokenizer: &mut Tokenizer<'a>) -> Token<'a> {
941    let value = consume_name(tokenizer);
942    if !tokenizer.is_eof() && tokenizer.next_byte_unchecked() == b'(' {
943        tokenizer.advance(1);
944        if value.eq_ignore_ascii_case("url") {
945            consume_unquoted_url(tokenizer).unwrap_or(Function(value))
946        } else {
947            tokenizer.see_function(&value);
948            Function(value)
949        }
950    } else {
951        Ident(value)
952    }
953}
954
955fn consume_name<'a>(tokenizer: &mut Tokenizer<'a>) -> CowRcStr<'a> {
956    // These are the overwhelmingly common bytes, that we can just skip over in a tight loop.
957    static IS_SIMPLE_NAME_BYTE: [bool; 256] = {
958        let mut table = [false; 256];
959        let mut i = 0;
960        while i < 256 {
961            table[i as usize] = matches!(i as u8, b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' | b'-' | b'\xC0'..=b'\xEF');
962            i += 1;
963        }
964        table
965    };
966
967    // start_pos is the end of the previous token, therefore at a code point boundary
968    let start_pos = tokenizer.position();
969    let mut value_bytes;
970    loop {
971        tokenizer.advance_while(|b| IS_SIMPLE_NAME_BYTE[b as usize]);
972        if tokenizer.is_eof() {
973            return tokenizer.slice_from(start_pos).into();
974        }
975        match_byte! { tokenizer.next_byte_unchecked(),
976            b'\\' | b'\0' => {
977                // * The tokenizer’s input is UTF-8 since it’s `&str`.
978                // * start_pos is at a code point boundary
979                // * so is the current position (which is before '\\' or '\0'
980                //
981                // So `value_bytes` is well-formed UTF-8.
982                value_bytes = tokenizer.slice_from(start_pos).as_bytes().to_owned();
983                break
984            }
985            b'\x80'..=b'\xBF' => { tokenizer.consume_continuation_byte(); }
986            b'\xF0'..=b'\xFF' => { tokenizer.consume_4byte_intro(); }
987            _b => {
988                return tokenizer.slice_from(start_pos).into();
989            }
990        }
991    }
992
993    loop {
994        value_bytes.extend(tokenizer.advance_while(|b| IS_SIMPLE_NAME_BYTE[b as usize]));
995        if tokenizer.is_eof() {
996            break;
997        }
998        let b = tokenizer.next_byte_unchecked();
999        match_byte! { b,
1000            b'\\' => {
1001                if tokenizer.has_newline_at(1) { break }
1002                tokenizer.advance(1);
1003                // This pushes one well-formed code point
1004                consume_escape_and_write(tokenizer, &mut value_bytes)
1005            }
1006            b'\0' => {
1007                tokenizer.advance(1);
1008                value_bytes.extend("\u{FFFD}".as_bytes());
1009            },
1010            b'\x80'..=b'\xBF' => {
1011                tokenizer.consume_continuation_byte();
1012                value_bytes.push(b)
1013            }
1014            b'\xF0'..=b'\xFF' => {
1015                tokenizer.consume_4byte_intro();
1016                value_bytes.push(b)
1017            }
1018            _ => {
1019                // ASCII
1020                break;
1021            }
1022        }
1023    }
1024    // string_bytes is well-formed UTF-8, see other comments.
1025    unsafe { from_utf8_release_unchecked(value_bytes) }.into()
1026}
1027
1028fn byte_to_hex_digit(b: u8) -> Option<u32> {
1029    Some(match_byte! { b,
1030        b'0' ..= b'9' => b - b'0',
1031        b'a' ..= b'f' => b - b'a' + 10,
1032        b'A' ..= b'F' => b - b'A' + 10,
1033        _ => {
1034            return None
1035        }
1036    } as u32)
1037}
1038
1039fn byte_to_decimal_digit(b: u8) -> Option<u32> {
1040    if b.is_ascii_digit() {
1041        Some((b - b'0') as u32)
1042    } else {
1043        None
1044    }
1045}
1046
1047fn consume_numeric<'a>(tokenizer: &mut Tokenizer<'a>) -> Token<'a> {
1048    // Parse [+-]?\d*(\.\d+)?([eE][+-]?\d+)?
1049    // But this is always called so that there is at least one digit in \d*(\.\d+)?
1050
1051    // Do all the math in f64 so that large numbers overflow to +/-inf
1052    // and i32::{MIN, MAX} are within range.
1053
1054    let (has_sign, sign) = match tokenizer.next_byte_unchecked() {
1055        b'-' => (true, -1.),
1056        b'+' => (true, 1.),
1057        _ => (false, 1.),
1058    };
1059    if has_sign {
1060        tokenizer.advance(1);
1061    }
1062
1063    let mut integral_part: f64 = 0.;
1064    while let Some(digit) = byte_to_decimal_digit(tokenizer.next_byte_unchecked()) {
1065        integral_part = integral_part * 10. + digit as f64;
1066        tokenizer.advance(1);
1067        if tokenizer.is_eof() {
1068            break;
1069        }
1070    }
1071
1072    let mut is_integer = true;
1073
1074    let mut fractional_part: f64 = 0.;
1075    if tokenizer.has_at_least(1)
1076        && tokenizer.next_byte_unchecked() == b'.'
1077        && tokenizer.byte_at(1).is_ascii_digit()
1078    {
1079        is_integer = false;
1080        tokenizer.advance(1); // Consume '.'
1081        let mut factor = 0.1;
1082        while let Some(digit) = byte_to_decimal_digit(tokenizer.next_byte_unchecked()) {
1083            fractional_part += digit as f64 * factor;
1084            factor *= 0.1;
1085            tokenizer.advance(1);
1086            if tokenizer.is_eof() {
1087                break;
1088            }
1089        }
1090    }
1091
1092    let mut value = sign * (integral_part + fractional_part);
1093
1094    if tokenizer.has_at_least(1)
1095        && matches!(tokenizer.next_byte_unchecked(), b'e' | b'E')
1096        && (tokenizer.byte_at(1).is_ascii_digit()
1097            || (tokenizer.has_at_least(2)
1098                && matches!(tokenizer.byte_at(1), b'+' | b'-')
1099                && tokenizer.byte_at(2).is_ascii_digit()))
1100    {
1101        is_integer = false;
1102        tokenizer.advance(1);
1103        let (has_sign, sign) = match tokenizer.next_byte_unchecked() {
1104            b'-' => (true, -1.),
1105            b'+' => (true, 1.),
1106            _ => (false, 1.),
1107        };
1108        if has_sign {
1109            tokenizer.advance(1);
1110        }
1111        let mut exponent: f64 = 0.;
1112        while let Some(digit) = byte_to_decimal_digit(tokenizer.next_byte_unchecked()) {
1113            exponent = exponent * 10. + digit as f64;
1114            tokenizer.advance(1);
1115            if tokenizer.is_eof() {
1116                break;
1117            }
1118        }
1119        value *= f64::powf(10., sign * exponent);
1120    }
1121
1122    let int_value = if is_integer {
1123        Some(if value >= i32::MAX as f64 {
1124            i32::MAX
1125        } else if value <= i32::MIN as f64 {
1126            i32::MIN
1127        } else {
1128            value as i32
1129        })
1130    } else {
1131        None
1132    };
1133
1134    if !tokenizer.is_eof() && tokenizer.next_byte_unchecked() == b'%' {
1135        tokenizer.advance(1);
1136        return Percentage {
1137            unit_value: (value / 100.) as f32,
1138            int_value,
1139            has_sign,
1140        };
1141    }
1142    let value = value as f32;
1143    if is_ident_start(tokenizer) {
1144        let unit = consume_name(tokenizer);
1145        Dimension {
1146            value,
1147            int_value,
1148            has_sign,
1149            unit,
1150        }
1151    } else {
1152        Number {
1153            value,
1154            int_value,
1155            has_sign,
1156        }
1157    }
1158}
1159
1160#[inline]
1161unsafe fn from_utf8_release_unchecked(string_bytes: Vec<u8>) -> String {
1162    unsafe {
1163        if cfg!(debug_assertions) {
1164            String::from_utf8(string_bytes).unwrap()
1165        } else {
1166            String::from_utf8_unchecked(string_bytes)
1167        }
1168    }
1169}
1170
1171fn consume_unquoted_url<'a>(tokenizer: &mut Tokenizer<'a>) -> Result<Token<'a>, ()> {
1172    // This is only called after "url(", so the current position is a code point boundary.
1173    let start_position = tokenizer.position;
1174    let from_start = &tokenizer.input[tokenizer.position..];
1175    let mut newlines = 0;
1176    let mut last_newline = 0;
1177    let mut found_printable_char = false;
1178    let mut iter = from_start.bytes().enumerate();
1179    loop {
1180        let (offset, b) = match iter.next() {
1181            Some(item) => item,
1182            None => {
1183                tokenizer.position = tokenizer.input.len();
1184                break;
1185            }
1186        };
1187        match_byte! { b,
1188            b' ' | b'\t' => {},
1189            b'\n' | b'\x0C' => {
1190                newlines += 1;
1191                last_newline = offset;
1192            }
1193            b'\r' => {
1194                if from_start.as_bytes().get(offset + 1) != Some(&b'\n') {
1195                    newlines += 1;
1196                    last_newline = offset;
1197                }
1198            }
1199            b'"' | b'\'' => return Err(()),  // Do not advance
1200            b')' => {
1201                // Don't use advance, because we may be skipping
1202                // newlines here, and we want to avoid the assert.
1203                tokenizer.position += offset + 1;
1204                break
1205            }
1206            _ => {
1207                // Don't use advance, because we may be skipping
1208                // newlines here, and we want to avoid the assert.
1209                tokenizer.position += offset;
1210                found_printable_char = true;
1211                break
1212            }
1213        }
1214    }
1215
1216    if newlines > 0 {
1217        tokenizer.current_line_number += newlines;
1218        // No need for wrapping_add here, because there's no possible
1219        // way to wrap.
1220        tokenizer.current_line_start_position = start_position + last_newline + 1;
1221    }
1222
1223    if found_printable_char {
1224        // This function only consumed ASCII (whitespace) bytes,
1225        // so the current position is a code point boundary.
1226        return Ok(consume_unquoted_url_internal(tokenizer));
1227    } else {
1228        return Ok(UnquotedUrl("".into()));
1229    }
1230
1231    fn consume_unquoted_url_internal<'a>(tokenizer: &mut Tokenizer<'a>) -> Token<'a> {
1232        // This function is only called with start_pos at a code point boundary.
1233        let start_pos = tokenizer.position();
1234        let mut string_bytes: Vec<u8>;
1235        loop {
1236            if tokenizer.is_eof() {
1237                return UnquotedUrl(tokenizer.slice_from(start_pos).into());
1238            }
1239            match_byte! { tokenizer.next_byte_unchecked(),
1240                b' ' | b'\t' | b'\n' | b'\r' | b'\x0C' => {
1241                    let value = tokenizer.slice_from(start_pos);
1242                    return consume_url_end(tokenizer, start_pos, value.into())
1243                }
1244                b')' => {
1245                    let value = tokenizer.slice_from(start_pos);
1246                    tokenizer.advance(1);
1247                    return UnquotedUrl(value.into())
1248                }
1249                b'\x01'..=b'\x08' | b'\x0B' | b'\x0E'..=b'\x1F' | b'\x7F'  // non-printable
1250                    | b'"' | b'\'' | b'(' => {
1251                    tokenizer.advance(1);
1252                    return consume_bad_url(tokenizer, start_pos)
1253                },
1254                b'\\' | b'\0' => {
1255                    // * The tokenizer’s input is UTF-8 since it’s `&str`.
1256                    // * start_pos is at a code point boundary
1257                    // * so is the current position (which is before '\\' or '\0'
1258                    //
1259                    // So `string_bytes` is well-formed UTF-8.
1260                    string_bytes = tokenizer.slice_from(start_pos).as_bytes().to_owned();
1261                    break
1262                }
1263                b'\x80'..=b'\xBF' => { tokenizer.consume_continuation_byte(); }
1264                b'\xF0'..=b'\xFF' => { tokenizer.consume_4byte_intro(); }
1265                _ => {
1266                    // ASCII or other leading byte.
1267                    tokenizer.advance(1);
1268                }
1269            }
1270        }
1271        while !tokenizer.is_eof() {
1272            let b = tokenizer.next_byte_unchecked();
1273            match_byte! { b,
1274                b' ' | b'\t' | b'\n' | b'\r' | b'\x0C' => {
1275                    // string_bytes is well-formed UTF-8, see other comments.
1276                    let string = unsafe { from_utf8_release_unchecked(string_bytes) }.into();
1277                    return consume_url_end(tokenizer, start_pos, string)
1278                }
1279                b')' => {
1280                    tokenizer.advance(1);
1281                    break;
1282                }
1283                b'\x01'..=b'\x08' | b'\x0B' | b'\x0E'..=b'\x1F' | b'\x7F'  // non-printable
1284                    | b'"' | b'\'' | b'(' => {
1285                    tokenizer.advance(1);
1286                    return consume_bad_url(tokenizer, start_pos);
1287                }
1288                b'\\' => {
1289                    tokenizer.advance(1);
1290                    if tokenizer.has_newline_at(0) {
1291                        return consume_bad_url(tokenizer, start_pos)
1292                    }
1293
1294                    // This pushes one well-formed code point to string_bytes
1295                    consume_escape_and_write(tokenizer, &mut string_bytes)
1296                },
1297                b'\0' => {
1298                    tokenizer.advance(1);
1299                    string_bytes.extend("\u{FFFD}".as_bytes());
1300                }
1301                b'\x80'..=b'\xBF' => {
1302                    // We’ll end up copying the whole code point
1303                    // before this loop does something else.
1304                    tokenizer.consume_continuation_byte();
1305                    string_bytes.push(b);
1306                }
1307                b'\xF0'..=b'\xFF' => {
1308                    // We’ll end up copying the whole code point
1309                    // before this loop does something else.
1310                    tokenizer.consume_4byte_intro();
1311                    string_bytes.push(b);
1312                }
1313                // If this byte is part of a multi-byte code point,
1314                // we’ll end up copying the whole code point before this loop does something else.
1315                b => {
1316                    // ASCII or other leading byte.
1317                    tokenizer.advance(1);
1318                    string_bytes.push(b)
1319                }
1320            }
1321        }
1322        UnquotedUrl(
1323            // string_bytes is well-formed UTF-8, see other comments.
1324            unsafe { from_utf8_release_unchecked(string_bytes) }.into(),
1325        )
1326    }
1327
1328    fn consume_url_end<'a>(
1329        tokenizer: &mut Tokenizer<'a>,
1330        start_pos: SourcePosition,
1331        string: CowRcStr<'a>,
1332    ) -> Token<'a> {
1333        while !tokenizer.is_eof() {
1334            match_byte! { tokenizer.next_byte_unchecked(),
1335                b')' => {
1336                    tokenizer.advance(1);
1337                    break
1338                }
1339                b' ' | b'\t' => { tokenizer.advance(1); }
1340                b'\n' | b'\x0C' | b'\r' => {
1341                    tokenizer.consume_newline();
1342                }
1343                b => {
1344                    tokenizer.consume_known_byte(b);
1345                    return consume_bad_url(tokenizer, start_pos);
1346                }
1347            }
1348        }
1349        UnquotedUrl(string)
1350    }
1351
1352    fn consume_bad_url<'a>(tokenizer: &mut Tokenizer<'a>, start_pos: SourcePosition) -> Token<'a> {
1353        // Consume up to the closing )
1354        while !tokenizer.is_eof() {
1355            match_byte! { tokenizer.next_byte_unchecked(),
1356                b')' => {
1357                    let contents = tokenizer.slice_from(start_pos).into();
1358                    tokenizer.advance(1);
1359                    return BadUrl(contents)
1360                }
1361                b'\\' => {
1362                    tokenizer.advance(1);
1363                    if matches!(tokenizer.next_byte(), Some(b')') | Some(b'\\')) {
1364                        tokenizer.advance(1); // Skip an escaped ')' or '\'
1365                    }
1366                }
1367                b'\n' | b'\x0C' | b'\r' => {
1368                    tokenizer.consume_newline();
1369                }
1370                b => {
1371                    tokenizer.consume_known_byte(b);
1372                }
1373            }
1374        }
1375        BadUrl(tokenizer.slice_from(start_pos).into())
1376    }
1377}
1378
1379// (value, number of digits up to 6)
1380fn consume_hex_digits(tokenizer: &mut Tokenizer<'_>) -> (u32, u32) {
1381    let mut value = 0;
1382    let mut digits = 0;
1383    while digits < 6 && !tokenizer.is_eof() {
1384        match byte_to_hex_digit(tokenizer.next_byte_unchecked()) {
1385            Some(digit) => {
1386                value = value * 16 + digit;
1387                digits += 1;
1388                tokenizer.advance(1);
1389            }
1390            None => break,
1391        }
1392    }
1393    (value, digits)
1394}
1395
1396// Same constraints as consume_escape except it writes into `bytes` the result
1397// instead of returning it.
1398fn consume_escape_and_write(tokenizer: &mut Tokenizer, bytes: &mut Vec<u8>) {
1399    bytes.extend(
1400        consume_escape(tokenizer)
1401            .encode_utf8(&mut [0; 4])
1402            .as_bytes(),
1403    )
1404}
1405
1406// Assumes that the U+005C REVERSE SOLIDUS (\) has already been consumed
1407// and that the next input character has already been verified
1408// to not be a newline.
1409fn consume_escape(tokenizer: &mut Tokenizer) -> char {
1410    if tokenizer.is_eof() {
1411        return '\u{FFFD}';
1412    } // Escaped EOF
1413    match_byte! { tokenizer.next_byte_unchecked(),
1414        b'0'..=b'9' | b'A'..=b'F' | b'a'..=b'f' => {
1415            let (c, _) = consume_hex_digits(tokenizer);
1416            if !tokenizer.is_eof() {
1417                match_byte! { tokenizer.next_byte_unchecked(),
1418                    b' ' | b'\t' => {
1419                        tokenizer.advance(1)
1420                    }
1421                    b'\n' | b'\x0C' | b'\r' => {
1422                        tokenizer.consume_newline();
1423                    }
1424                    _ => {}
1425                }
1426            }
1427            static REPLACEMENT_CHAR: char = '\u{FFFD}';
1428            if c != 0 {
1429                let c = char::from_u32(c);
1430                c.unwrap_or(REPLACEMENT_CHAR)
1431            } else {
1432                REPLACEMENT_CHAR
1433            }
1434        },
1435        b'\0' => {
1436            tokenizer.advance(1);
1437            '\u{FFFD}'
1438        }
1439        _ => tokenizer.consume_char(),
1440    }
1441}