From 3157eb9e12e1c346b6217004cd0f85fdaa022e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Tue, 15 Sep 2026 12:23:00 +0200 Subject: [PATCH 1/2] parser: Collapse parser and tokenizer. Now they are 1:1 so the distinction is mostly internal. This allows state tracking to be a bit simpler. --- src/parser.rs | 215 +++------- src/rules_and_declarations.rs | 4 +- src/size_of_tests.rs | 1 - src/tokenizer.rs | 725 +++++++++++++++++----------------- 4 files changed, 417 insertions(+), 528 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 1ee17817..5d0454f9 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -3,11 +3,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use crate::cow_rc_str::CowRcStr; -use crate::tokenizer::{SourceLocation, SourcePosition, Token, Tokenizer}; +use crate::tokenizer::{SeenStatus, SourceLocation, SourcePosition, Token}; use smallvec::SmallVec; use std::fmt; use std::ops::BitOr; -use std::ops::Range; /// A capture of the internal state of a `Parser` (including the position within the input), /// obtained from the `Parser::position` method. @@ -223,14 +222,16 @@ impl std::error::Error for ParseError {} /// A CSS parser that borrows its `&str` input, yields `Token`s, and keeps track of nested blocks /// and functions. pub struct Parser<'i> { - tokenizer: Tokenizer<'i>, + pub(crate) input: &'i str, + pub(crate) state: ParserState, cached_token: CachedToken<'i>, current_block_depth: u8, nested_block_limit: u8, - /// If `Some(_)`, .parse_nested_block() can be called. - at_start_of: Option, /// For parsers from `parse_until` or `parse_nested_block` stop_before: Delimiters, + pub(crate) arbitrary_substitution_functions: SeenStatus<'i>, + pub(crate) source_map_url: Option<&'i str>, + pub(crate) source_url: Option<&'i str>, } struct CachedToken<'i> { @@ -365,8 +366,8 @@ impl<'i> Parser<'i> { #[inline] pub fn new(input: &'i str) -> Self { Self { - tokenizer: Tokenizer::new(input), - at_start_of: None, + input, + state: ParserState::default(), stop_before: Delimiter::None, nested_block_limit: Self::REASONABLE_NESTED_BLOCK_LIMIT, current_block_depth: 0, @@ -375,6 +376,9 @@ impl<'i> Parser<'i> { start_position: SourcePosition(usize::MAX), // No token would match this cache. end_state: ParserState::default(), }, + arbitrary_substitution_functions: SeenStatus::DontCare, + source_map_url: None, + source_url: None, } } @@ -385,11 +389,6 @@ impl<'i> Parser<'i> { self.nested_block_limit = limit; } - /// Return the current line that is being parsed. - pub fn current_line(&self) -> &'i str { - self.tokenizer.current_source_line() - } - /// Check whether the input is exhausted. That is, if `.next()` would return a token. /// /// This ignores whitespace and comments. @@ -417,38 +416,6 @@ impl<'i> Parser<'i> { result } - /// Return the current position within the input. - /// - /// This can be used with the `Parser::slice` and `slice_from` methods. - #[inline] - pub fn position(&self) -> SourcePosition { - self.tokenizer.position() - } - - /// The current line number and column number. - #[inline] - pub fn current_source_location(&self) -> SourceLocation { - self.tokenizer.current_source_location() - } - - /// The source map URL, if known. - /// - /// The source map URL is extracted from a specially formatted - /// comment. The last such comment is used, so this value may - /// change as parsing proceeds. - pub fn current_source_map_url(&self) -> Option<&str> { - self.tokenizer.current_source_map_url() - } - - /// The source URL, if known. - /// - /// The source URL is extracted from a specially formatted - /// comment. The last such comment is used, so this value may - /// change as parsing proceeds. - pub fn current_source_url(&self) -> Option<&str> { - self.tokenizer.current_source_url() - } - /// Create a new unexpected token or EOF ParseError at the current location #[inline] pub fn new_error_for_next_token(&mut self) -> ParseError { @@ -463,34 +430,14 @@ impl<'i> Parser<'i> { /// This state can later be restored with the `Parser::reset` method. #[inline] pub fn state(&self) -> ParserState { - ParserState { - at_start_of: self.at_start_of, - ..self.tokenizer.state() - } - } - - /// Advance the input until the next token that’s not whitespace or a comment. - #[inline] - pub fn skip_whitespace(&mut self) { - if let Some(block_type) = self.at_start_of.take() { - consume_until_end_of_block(block_type, &mut self.tokenizer); - } - - self.tokenizer.skip_whitespace() + self.state.clone() } + /// Like `next_byte`, but returns `None` if the next byte is one of the delimiters this + /// parser was told to stop before. #[inline] - pub(crate) fn skip_cdc_and_cdo(&mut self) { - if let Some(block_type) = self.at_start_of.take() { - consume_until_end_of_block(block_type, &mut self.tokenizer); - } - - self.tokenizer.skip_cdc_and_cdo() - } - - #[inline] - pub(crate) fn next_byte(&self) -> Option { - let byte = self.tokenizer.next_byte()?; + pub(crate) fn next_byte_before_delimiter(&self) -> Option { + let byte = self.next_byte()?; if self.stop_before.contains(Delimiters::from_byte(byte)) { return None; } @@ -503,26 +450,7 @@ impl<'i> Parser<'i> { /// Should only be used with `SourcePosition` values from the same `Parser` instance. #[inline] pub fn reset(&mut self, state: &ParserState) { - self.tokenizer.reset(state); - self.at_start_of = state.at_start_of; - } - - /// Start looking for arbitrary substitution functions like `var()` / `env()` functions. - /// (See the `.seen_arbitrary_substitution_functions()` method.) - #[inline] - pub fn look_for_arbitrary_substitution_functions( - &mut self, - fns: ArbitrarySubstitutionFunctions<'i>, - ) { - self.tokenizer - .look_for_arbitrary_substitution_functions(fns) - } - - /// Return whether a relevant function has been seen by the tokenizer since - /// `look_for_arbitrary_substitution_functions` was called, and stop looking. - #[inline] - pub fn seen_arbitrary_substitution_functions(&mut self) -> bool { - self.tokenizer.seen_arbitrary_substitution_functions() + self.state = state.clone(); } /// The old name of `try_parse`, which requires raw identifiers in the Rust 2018 edition. @@ -551,18 +479,6 @@ impl<'i> Parser<'i> { result } - /// Return a slice of the CSS input - #[inline] - pub fn slice(&self, range: Range) -> &'i str { - self.tokenizer.slice(range) - } - - /// Return a slice of the CSS input, from the given position to the current one. - #[inline] - pub fn slice_from(&self, start_position: SourcePosition) -> &'i str { - self.tokenizer.slice_from(start_position) - } - /// Return the next token in the input that is neither whitespace or a comment, /// and advance the position accordingly. /// @@ -597,41 +513,31 @@ impl<'i> Parser<'i> { pub fn next_including_whitespace_and_comments( &mut self, ) -> Result<&Token<'i>, BasicParseError> { - if let Some(block_type) = self.at_start_of.take() { - consume_until_end_of_block(block_type, &mut self.tokenizer); + if let Some(block_type) = self.state.at_start_of.take() { + self.consume_until_end_of_block(block_type); } - let Some(byte) = self.tokenizer.next_byte() else { - return Err(BasicParseError::new(BasicParseErrorKind::EndOfInput)); - }; - - if self.stop_before.contains(Delimiters::from_byte(byte)) { + if self.next_byte_before_delimiter().is_none() { return Err(BasicParseError::new(BasicParseErrorKind::EndOfInput)); } - let token_start_position = self.tokenizer.position(); + let token_start_position = self.position(); let using_cached_token = self.cached_token.start_position == token_start_position; - let token = if using_cached_token { - let cached_token = &self.cached_token; - self.tokenizer.reset(&cached_token.end_state); - if let Token::Function(ref name) = cached_token.token { - self.tokenizer.see_function(name) - } - &cached_token.token + if using_cached_token { + self.state = self.cached_token.end_state.clone(); } else { - let new_token = self.tokenizer.next_unchecked(); + let new_token = self.next_unchecked(); + if let Some(block_type) = BlockType::opening(&new_token) { + self.state.at_start_of = Some(block_type); + } self.cached_token = CachedToken { token: new_token, start_position: token_start_position, - end_state: self.tokenizer.state(), + end_state: self.state.clone(), }; - &self.cached_token.token - }; - - if let Some(block_type) = BlockType::opening(token) { - self.at_start_of = Some(block_type); } - Ok(token) + + Ok(&self.cached_token.token) } /// Have the given closure parse something, then check the the input is exhausted. @@ -1001,17 +907,17 @@ where if error_behavior == ParseUntilErrorBehavior::Stop && result.is_err() { return result; } - if let Some(block_type) = parser.at_start_of.take() { - consume_until_end_of_block(block_type, &mut parser.tokenizer); + if let Some(block_type) = parser.state.at_start_of.take() { + parser.consume_until_end_of_block(block_type); } // FIXME: have a special-purpose tokenizer method for this that does less work. - while let Some(next_byte) = parser.tokenizer.next_byte() { + while let Some(next_byte) = parser.next_byte() { if delimiters.contains(Delimiters::from_byte(next_byte)) { break; } - let token = parser.tokenizer.next_unchecked(); + let token = parser.next_unchecked(); if let Some(block_type) = BlockType::opening(&token) { - consume_until_end_of_block(block_type, &mut parser.tokenizer); + parser.consume_until_end_of_block(block_type); } } result @@ -1030,14 +936,14 @@ where if error_behavior == ParseUntilErrorBehavior::Stop && result.is_err() { return result; } - if let Some(next_byte) = parser.tokenizer.next_byte() { + if let Some(next_byte) = parser.next_byte() { let delimiter = Delimiters::from_byte(next_byte); if !parser.stop_before.contains(delimiter) { debug_assert!(delimiters.contains(delimiter)); // We know this byte is ASCII. - parser.tokenizer.advance(1); + parser.advance(1); if next_byte == b'{' { - consume_until_end_of_block(BlockType::CurlyBracket, &mut parser.tokenizer); + parser.consume_until_end_of_block(BlockType::CurlyBracket); } } } @@ -1051,7 +957,7 @@ pub fn parse_nested_block<'i, F, T, E>( where F: FnOnce(&mut Parser<'i>) -> Result>, { - let block_type = parser.at_start_of.take().expect( + let block_type = parser.state.at_start_of.take().expect( "\ A nested parser can only be created when a Function, \ ParenthesisBlock, SquareBracketBlock, or CurlyBracketBlock \ @@ -1073,34 +979,39 @@ where BlockType::Parenthesis => ClosingDelimiter::CloseParenthesis, }; let result = parser.parse_entirely(parse); - if let Some(nested_block_type) = parser.at_start_of.take() { - consume_until_end_of_block(nested_block_type, &mut parser.tokenizer); + if let Some(nested_block_type) = parser.state.at_start_of.take() { + parser.consume_until_end_of_block(nested_block_type); } - consume_until_end_of_block(block_type, &mut parser.tokenizer); + parser.consume_until_end_of_block(block_type); parser.stop_before = old_stop_before; parser.current_block_depth = parser.current_block_depth.wrapping_sub(1); result } -#[inline(never)] -#[cold] -fn consume_until_end_of_block(block_type: BlockType, tokenizer: &mut Tokenizer) { - let mut stack = SmallVec::<[BlockType; 16]>::new(); - stack.push(block_type); - - // FIXME: have a special-purpose tokenizer method for this that does less work. - while let Ok(ref token) = tokenizer.next() { - if let Some(b) = BlockType::closing(token) { - if *stack.last().unwrap() == b { - stack.pop(); - if stack.is_empty() { - return; +impl Parser<'_> { + /// Consume tokens until the end of a block of the given type that we're at the start of, + /// ignoring any `stop_before` delimiters. + #[inline(never)] + #[cold] + pub(crate) fn consume_until_end_of_block(&mut self, block_type: BlockType) { + let mut stack = SmallVec::<[BlockType; 16]>::new(); + stack.push(block_type); + + // FIXME: have a special-purpose tokenizer method for this that does less work. + while !self.is_eof() { + let token = self.next_unchecked(); + if let Some(b) = BlockType::closing(&token) { + if *stack.last().unwrap() == b { + stack.pop(); + if stack.is_empty() { + return; + } } } - } - if let Some(block_type) = BlockType::opening(token) { - stack.push(block_type); + if let Some(block_type) = BlockType::opening(&token) { + stack.push(block_type); + } } } } diff --git a/src/rules_and_declarations.rs b/src/rules_and_declarations.rs index f33eadb8..0a72b850 100644 --- a/src/rules_and_declarations.rs +++ b/src/rules_and_declarations.rs @@ -381,7 +381,7 @@ where loop { self.input.skip_cdc_and_cdo(); let start = self.input.state(); - let at_keyword = match self.input.next_byte()? { + let at_keyword = match self.input.next_byte_before_delimiter()? { b'@' => match self.input.next_including_whitespace_and_comments() { Ok(Token::AtKeyword(name)) => Some(name.clone()), _ => { @@ -458,7 +458,7 @@ where input.parse_entirely(|input| { input.skip_whitespace(); let start = input.state(); - let at_keyword = if input.next_byte() == Some(b'@') { + let at_keyword = if input.next_byte_before_delimiter() == Some(b'@') { match *input.next_including_whitespace_and_comments()? { Token::AtKeyword(ref name) => Some(name.clone()), _ => { diff --git a/src/size_of_tests.rs b/src/size_of_tests.rs index e407578b..9d58dee1 100644 --- a/src/size_of_tests.rs +++ b/src/size_of_tests.rs @@ -42,7 +42,6 @@ size_of_test!(token, Token, 32); size_of_test!(std_cow_str, std::borrow::Cow<'static, str>, 24, 32); size_of_test!(cow_rc_str, CowRcStr, 16); -size_of_test!(tokenizer, crate::tokenizer::Tokenizer, 96); size_of_test!(parser, crate::parser::Parser, 168); size_of_test!(source_position, crate::SourcePosition, 8); size_of_test!(parser_state, crate::ParserState, 24); diff --git a/src/tokenizer.rs b/src/tokenizer.rs index a1bca2d3..87fa4e12 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -6,7 +6,7 @@ use self::Token::*; use crate::cow_rc_str::CowRcStr; -use crate::parser::{ArbitrarySubstitutionFunctions, ParserState}; +use crate::parser::{ArbitrarySubstitutionFunctions, Parser}; use std::char; use std::ops::Range; @@ -182,7 +182,7 @@ pub enum Token<'a> { impl Token<'_> { /// Return whether this token represents a parse error. /// - /// `BadUrl` and `BadString` are tokenizer-level parse errors. + /// `BadUrl` and `BadString` are parser-level parse errors. /// /// `CloseParenthesis`, `CloseSquareBracket`, and `CloseCurlyBracket` are *unmatched* /// and therefore parse errors when returned by one of the `Parser::next*` methods. @@ -194,42 +194,29 @@ impl Token<'_> { } } -#[derive(Clone)] -pub struct Tokenizer<'a> { - input: &'a str, - /// Counted in bytes, not code points. From 0. - position: usize, - /// The position at the start of the current line; but adjusted to - /// ensure that computing the column will give the result in units - /// of UTF-16 characters. - current_line_start_position: usize, - current_line_number: u32, - arbitrary_substitution_functions: SeenStatus<'a>, - source_map_url: Option<&'a str>, - source_url: Option<&'a str>, -} - +/// Tracks whether the parser has seen any of the arbitrary substitution functions the caller +/// asked about via `Parser::look_for_arbitrary_substitution_functions`. #[derive(Copy, Clone, PartialEq, Eq)] -enum SeenStatus<'a> { +pub(crate) enum SeenStatus<'a> { DontCare, LookingForThem(ArbitrarySubstitutionFunctions<'a>), SeenAtLeastOne, } -impl<'a> Tokenizer<'a> { +impl SeenStatus<'_> { #[inline] - pub fn new(input: &'a str) -> Self { - Tokenizer { - input, - position: 0, - current_line_start_position: 0, - current_line_number: 0, - arbitrary_substitution_functions: SeenStatus::DontCare, - source_map_url: None, - source_url: None, + pub(crate) fn see_function(&mut self, name: &str) { + if let SeenStatus::LookingForThem(fns) = *self { + if fns.iter().any(|a| name.eq_ignore_ascii_case(a)) { + *self = SeenStatus::SeenAtLeastOne; + } } } +} +impl<'a> Parser<'a> { + /// Start looking for arbitrary substitution functions like `var()` / `env()` functions. + /// (See the `.seen_arbitrary_substitution_functions()` method.) #[inline] pub fn look_for_arbitrary_substitution_functions( &mut self, @@ -238,6 +225,8 @@ impl<'a> Tokenizer<'a> { self.arbitrary_substitution_functions = SeenStatus::LookingForThem(fns); } + /// Return whether a relevant function has been seen by the parser since + /// `look_for_arbitrary_substitution_functions` was called, and stop looking. #[inline] pub fn seen_arbitrary_substitution_functions(&mut self) -> bool { let seen = self.arbitrary_substitution_functions == SeenStatus::SeenAtLeastOne; @@ -245,92 +234,74 @@ impl<'a> Tokenizer<'a> { seen } + /// Tokenize the next token, without any of the block / delimiter handling that + /// `Parser::next` and friends do. Assumes non-EOF. #[inline] - pub fn see_function(&mut self, name: &str) { - if let SeenStatus::LookingForThem(fns) = self.arbitrary_substitution_functions { - if fns.iter().any(|a| name.eq_ignore_ascii_case(a)) { - self.arbitrary_substitution_functions = SeenStatus::SeenAtLeastOne; - } - } - } - - #[inline] - pub fn next(&mut self) -> Result, ()> { - if self.is_eof() { - return Err(()); - } - Ok(self.next_unchecked()) - } - - #[inline] - pub fn next_unchecked(&mut self) -> Token<'a> { + pub(crate) fn next_unchecked(&mut self) -> Token<'a> { next_token_unchecked(self) } + /// Return the current position within the input. + /// + /// This can be used with the `Parser::slice` and `slice_from` methods. #[inline] pub fn position(&self) -> SourcePosition { - debug_assert!(self.input.is_char_boundary(self.position)); - SourcePosition(self.position) + debug_assert!(self.input.is_char_boundary(self.state.position)); + SourcePosition(self.state.position) } + /// The current line number and column number. #[inline] pub fn current_source_location(&self) -> SourceLocation { - SourceLocation { - line: self.current_line_number, - column: (self.position - self.current_line_start_position + 1) as u32, - } + self.state.source_location() } + /// The source map URL, if known. + /// + /// The source map URL is extracted from a specially formatted + /// comment. The last such comment is used, so this value may + /// change as parsing proceeds. #[inline] pub fn current_source_map_url(&self) -> Option<&'a str> { self.source_map_url } + /// The source URL, if known. + /// + /// The source URL is extracted from a specially formatted + /// comment. The last such comment is used, so this value may + /// change as parsing proceeds. #[inline] pub fn current_source_url(&self) -> Option<&'a str> { self.source_url } + /// Return a slice of the CSS input, from the given position to the current one. #[inline] - pub fn state(&self) -> ParserState { - ParserState { - position: self.position, - current_line_start_position: self.current_line_start_position, - current_line_number: self.current_line_number, - at_start_of: None, - } - } - - #[inline] - pub fn reset(&mut self, state: &ParserState) { - self.position = state.position; - self.current_line_start_position = state.current_line_start_position; - self.current_line_number = state.current_line_number; - } - - #[inline] - pub(crate) fn slice_from(&self, start_pos: SourcePosition) -> &'a str { + pub fn slice_from(&self, start_pos: SourcePosition) -> &'a str { self.slice(start_pos..self.position()) } + /// Return a slice of the CSS input #[inline] - pub(crate) fn slice(&self, range: Range) -> &'a str { + pub fn slice(&self, range: Range) -> &'a str { debug_assert!(self.input.is_char_boundary(range.start.0)); debug_assert!(self.input.is_char_boundary(range.end.0)); unsafe { self.input.get_unchecked(range.start.0..range.end.0) } } #[inline] - pub(crate) fn byte_slice(&self, range: Range) -> &'a [u8] { + fn byte_slice(&self, range: Range) -> &'a [u8] { &self.input.as_bytes()[range] } #[inline] - pub(crate) fn byte_slice_from(&self, start: usize) -> &'a [u8] { - self.byte_slice(start..self.position) + fn byte_slice_from(&self, start: usize) -> &'a [u8] { + self.byte_slice(start..self.state.position) } - pub fn current_source_line(&self) -> &'a str { + /// Return the current line that is being parsed. + pub fn current_line(&self) -> &'a str { let current = self.position(); let start = self .slice(SourcePosition(0)..current) @@ -344,32 +315,32 @@ impl<'a> Tokenizer<'a> { } #[inline] - pub fn next_byte(&self) -> Option { + pub(crate) fn next_byte(&self) -> Option { if self.is_eof() { None } else { - Some(self.input.as_bytes()[self.position]) + Some(self.input.as_bytes()[self.state.position]) } } - // If false, `tokenizer.next_char()` will not panic. + // If false, `parser.next_char()` will not panic. #[inline] - fn is_eof(&self) -> bool { + pub(crate) fn is_eof(&self) -> bool { !self.has_at_least(0) } // If true, the input has at least `n` bytes left *after* the current one. - // That is, `tokenizer.char_at(n)` will not panic. + // That is, `parser.char_at(n)` will not panic. #[inline] fn has_at_least(&self, n: usize) -> bool { - self.position + n < self.input.len() + self.state.position + n < self.input.len() } // Advance over N bytes in the input. This function can advance // over ASCII bytes (excluding newlines), or UTF-8 sequence // leaders (excluding leaders for 4-byte sequences). #[inline] - pub fn advance(&mut self, n: usize) { + pub(crate) fn advance(&mut self, n: usize) { if cfg!(debug_assertions) { // Each byte must either be an ASCII byte or a sequence // leader, but not a 4-byte leader; also newlines are @@ -380,13 +351,13 @@ impl<'a> Tokenizer<'a> { debug_assert!(b != b'\r' && b != b'\n' && b != b'\x0C'); } } - self.position += n + self.state.position += n } /// Equivalent to calling advance() for runs of bytes for which `matches` returns true. /// Returns the byte slice advanced over. fn advance_while(&mut self, mut matches: impl FnMut(u8) -> bool) -> &[u8] { - let start = self.position; + let start = self.state.position; let mut position = start; let bytes = &self.input.as_bytes()[start..]; @@ -397,8 +368,8 @@ impl<'a> Tokenizer<'a> { position += 1; } - // Equivalent to self.position = position, but with advance()'s debug_assert!s - self.advance(position - self.position); + // Equivalent to self.state.position = position, but with advance()'s debug_assert!s + self.advance(position - self.state.position); self.byte_slice_from(start) } @@ -411,7 +382,7 @@ impl<'a> Tokenizer<'a> { #[inline] fn byte_at(&self, offset: usize) -> u8 { - self.input.as_bytes()[self.position + offset] + self.input.as_bytes()[self.state.position + offset] } // Advance over a single byte; the byte must be a UTF-8 sequence @@ -421,8 +392,9 @@ impl<'a> Tokenizer<'a> { debug_assert!(self.next_byte_unchecked() & 0xF0 == 0xF0); // This takes two UTF-16 characters to represent, so we // actually have an undercount. - self.current_line_start_position = self.current_line_start_position.wrapping_sub(1); - self.position += 1; + self.state.current_line_start_position = + self.state.current_line_start_position.wrapping_sub(1); + self.state.position += 1; } // Advance over a single byte; the byte must be a UTF-8 @@ -433,30 +405,33 @@ impl<'a> Tokenizer<'a> { // Continuation bytes contribute to column overcount. Note // that due to the special case for the 4-byte sequence intro, // we must use wrapping add here. - self.current_line_start_position = self.current_line_start_position.wrapping_add(1); - self.position += 1; + self.state.current_line_start_position = + self.state.current_line_start_position.wrapping_add(1); + self.state.position += 1; } // Advance over any kind of byte, excluding newlines. #[inline(never)] fn consume_known_byte(&mut self, byte: u8) { debug_assert!(byte != b'\r' && byte != b'\n' && byte != b'\x0C'); - self.position += 1; + self.state.position += 1; // Continuation bytes contribute to column overcount. if byte & 0xF0 == 0xF0 { // This takes two UTF-16 characters to represent, so we // actually have an undercount. - self.current_line_start_position = self.current_line_start_position.wrapping_sub(1); + self.state.current_line_start_position = + self.state.current_line_start_position.wrapping_sub(1); } else if byte & 0xC0 == 0x80 { // Note that due to the special case for the 4-byte // sequence intro, we must use wrapping add here. - self.current_line_start_position = self.current_line_start_position.wrapping_add(1); + self.state.current_line_start_position = + self.state.current_line_start_position.wrapping_add(1); } } #[inline] fn next_char(&self) -> char { - unsafe { self.input.get_unchecked(self.position().0..) } + unsafe { self.input.get_unchecked(self.state.position().0..) } .chars() .next() .unwrap() @@ -468,17 +443,17 @@ impl<'a> Tokenizer<'a> { fn consume_newline(&mut self) { let byte = self.next_byte_unchecked(); debug_assert!(byte == b'\r' || byte == b'\n' || byte == b'\x0C'); - self.position += 1; + self.state.position += 1; if byte == b'\r' && self.next_byte() == Some(b'\n') { - self.position += 1; + self.state.position += 1; } - self.current_line_start_position = self.position; - self.current_line_number += 1; + self.state.current_line_start_position = self.state.position; + self.state.current_line_number += 1; } #[inline] fn has_newline_at(&self, offset: usize) -> bool { - self.position + offset < self.input.len() + self.state.position + offset < self.input.len() && matches!(self.byte_at(offset), b'\n' | b'\r' | b'\x0C') } @@ -486,10 +461,11 @@ impl<'a> Tokenizer<'a> { fn consume_char(&mut self) -> char { let c = self.next_char(); let len_utf8 = c.len_utf8(); - self.position += len_utf8; + self.state.position += len_utf8; // Note that due to the special case for the 4-byte sequence // intro, we must use wrapping add here. - self.current_line_start_position = self + self.state.current_line_start_position = self + .state .current_line_start_position .wrapping_add(len_utf8 - c.len_utf16()); c @@ -497,10 +473,14 @@ impl<'a> Tokenizer<'a> { #[inline] fn starts_with(&self, needle: &[u8]) -> bool { - self.input.as_bytes()[self.position..].starts_with(needle) + self.input.as_bytes()[self.state.position..].starts_with(needle) } + /// Advance the input until the next token that’s not whitespace or a comment. pub fn skip_whitespace(&mut self) { + if let Some(block_type) = self.state.at_start_of.take() { + self.consume_until_end_of_block(block_type); + } while !self.is_eof() { match_byte! { self.next_byte_unchecked(), b' ' | b'\t' => { @@ -521,7 +501,10 @@ impl<'a> Tokenizer<'a> { } } - pub fn skip_cdc_and_cdo(&mut self) { + pub(crate) fn skip_cdc_and_cdo(&mut self) { + if let Some(block_type) = self.state.at_start_of.take() { + self.consume_until_end_of_block(block_type); + } while !self.is_eof() { match_byte! { self.next_byte_unchecked(), b' ' | b'\t' => { @@ -588,132 +571,132 @@ pub struct SourceLocation { #[cfg(feature = "malloc_size_of")] malloc_size_of::malloc_size_of_is_0!(SourceLocation); -fn next_token_unchecked<'a>(tokenizer: &mut Tokenizer<'a>) -> Token<'a> { - debug_assert!(!tokenizer.is_eof()); - let b = tokenizer.next_byte_unchecked(); +fn next_token_unchecked<'a>(parser: &mut Parser<'a>) -> Token<'a> { + debug_assert!(!parser.is_eof()); + let b = parser.next_byte_unchecked(); let token = match_byte! { b, b' ' | b'\t' => { - consume_whitespace(tokenizer, false) + consume_whitespace(parser, false) }, - b'\n' | b'\x0C' | b'\r' => consume_whitespace(tokenizer, true), - b'"' => consume_string(tokenizer, false), + b'\n' | b'\x0C' | b'\r' => consume_whitespace(parser, true), + b'"' => consume_string(parser, false), b'#' => { - tokenizer.advance(1); - if is_ident_start(tokenizer) { IDHash(consume_name(tokenizer)) } - else if !tokenizer.is_eof() && - matches!(tokenizer.next_byte_unchecked(), b'0'..=b'9' | b'-') { + parser.advance(1); + if is_ident_start(parser) { IDHash(consume_name(parser)) } + else if !parser.is_eof() && + matches!(parser.next_byte_unchecked(), b'0'..=b'9' | b'-') { // Any other valid case here already resulted in IDHash. - Hash(consume_name(tokenizer)) + Hash(consume_name(parser)) } else { Delim('#') } }, b'$' => { - if tokenizer.starts_with(b"$=") { tokenizer.advance(2); SuffixMatch } - else { tokenizer.advance(1); Delim('$') } + if parser.starts_with(b"$=") { parser.advance(2); SuffixMatch } + else { parser.advance(1); Delim('$') } }, - b'\'' => consume_string(tokenizer, true), - b'(' => { tokenizer.advance(1); ParenthesisBlock }, - b')' => { tokenizer.advance(1); CloseParenthesis }, + b'\'' => consume_string(parser, true), + b'(' => { parser.advance(1); ParenthesisBlock }, + b')' => { parser.advance(1); CloseParenthesis }, b'*' => { - if tokenizer.starts_with(b"*=") { tokenizer.advance(2); SubstringMatch } - else { tokenizer.advance(1); Delim('*') } + if parser.starts_with(b"*=") { parser.advance(2); SubstringMatch } + else { parser.advance(1); Delim('*') } }, b'+' => { if ( - tokenizer.has_at_least(1) - && tokenizer.byte_at(1).is_ascii_digit() + parser.has_at_least(1) + && parser.byte_at(1).is_ascii_digit() ) || ( - tokenizer.has_at_least(2) - && tokenizer.byte_at(1) == b'.' - && tokenizer.byte_at(2).is_ascii_digit() + parser.has_at_least(2) + && parser.byte_at(1) == b'.' + && parser.byte_at(2).is_ascii_digit() ) { - consume_numeric(tokenizer) + consume_numeric(parser) } else { - tokenizer.advance(1); + parser.advance(1); Delim('+') } }, - b',' => { tokenizer.advance(1); Comma }, + b',' => { parser.advance(1); Comma }, b'-' => { if ( - tokenizer.has_at_least(1) - && tokenizer.byte_at(1).is_ascii_digit() + parser.has_at_least(1) + && parser.byte_at(1).is_ascii_digit() ) || ( - tokenizer.has_at_least(2) - && tokenizer.byte_at(1) == b'.' - && tokenizer.byte_at(2).is_ascii_digit() + parser.has_at_least(2) + && parser.byte_at(1) == b'.' + && parser.byte_at(2).is_ascii_digit() ) { - consume_numeric(tokenizer) - } else if tokenizer.starts_with(b"-->") { - tokenizer.advance(3); + consume_numeric(parser) + } else if parser.starts_with(b"-->") { + parser.advance(3); CDC - } else if is_ident_start(tokenizer) { - consume_ident_like(tokenizer) + } else if is_ident_start(parser) { + consume_ident_like(parser) } else { - tokenizer.advance(1); + parser.advance(1); Delim('-') } }, b'.' => { - if tokenizer.has_at_least(1) - && tokenizer.byte_at(1).is_ascii_digit() { - consume_numeric(tokenizer) + if parser.has_at_least(1) + && parser.byte_at(1).is_ascii_digit() { + consume_numeric(parser) } else { - tokenizer.advance(1); + parser.advance(1); Delim('.') } } b'/' => { - if tokenizer.starts_with(b"/*") { - Comment(consume_comment(tokenizer)) + if parser.starts_with(b"/*") { + Comment(consume_comment(parser)) } else { - tokenizer.advance(1); + parser.advance(1); Delim('/') } } - b'0'..=b'9' => consume_numeric(tokenizer), - b':' => { tokenizer.advance(1); Colon }, - b';' => { tokenizer.advance(1); Semicolon }, + b'0'..=b'9' => consume_numeric(parser), + b':' => { parser.advance(1); Colon }, + b';' => { parser.advance(1); Semicolon }, b'<' => { - if tokenizer.starts_with(b"