ladybird/Libraries/LibWeb/HTML/Parser/Rust/src/tokenizer.rs
Andreas Kling 29784ea397 LibWeb: Remove the C++ HTML tree builder
Delete the old C++ tree-construction implementation and helper classes
that became unused once the Rust parser is unconditional. Remove the C++
stack of open elements, active formatting elements, speculative mock
element, and tree-builder-only token storage.

Keep the C++ parser entry points that still own LibWeb DOM integration,
encoding detection, tokenizer bridging, incremental parsing, and the
speculative parser support used by resource discovery.
2026-05-17 15:35:56 +02:00

3243 lines
136 KiB
Rust

/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
use std::collections::VecDeque;
use crate::entities::NamedCharacterReferenceMatcher;
use crate::token::{Attribute, DoctypeData, Position, Token, TokenPayload, TokenType};
/// Tokenizer states per the WHATWG HTML spec section 13.2.5.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum State {
Data = 0,
RCDATA,
RAWTEXT,
ScriptData,
PLAINTEXT,
TagOpen,
EndTagOpen,
TagName,
RCDATALessThanSign,
RCDATAEndTagOpen,
RCDATAEndTagName,
RAWTEXTLessThanSign,
RAWTEXTEndTagOpen,
RAWTEXTEndTagName,
ScriptDataLessThanSign,
ScriptDataEndTagOpen,
ScriptDataEndTagName,
ScriptDataEscapeStart,
ScriptDataEscapeStartDash,
ScriptDataEscaped,
ScriptDataEscapedDash,
ScriptDataEscapedDashDash,
ScriptDataEscapedLessThanSign,
ScriptDataEscapedEndTagOpen,
ScriptDataEscapedEndTagName,
ScriptDataDoubleEscapeStart,
ScriptDataDoubleEscaped,
ScriptDataDoubleEscapedDash,
ScriptDataDoubleEscapedDashDash,
ScriptDataDoubleEscapedLessThanSign,
ScriptDataDoubleEscapeEnd,
BeforeAttributeName,
AttributeName,
AfterAttributeName,
BeforeAttributeValue,
AttributeValueDoubleQuoted,
AttributeValueSingleQuoted,
AttributeValueUnquoted,
AfterAttributeValueQuoted,
SelfClosingStartTag,
BogusComment,
MarkupDeclarationOpen,
CommentStart,
CommentStartDash,
Comment,
CommentLessThanSign,
CommentLessThanSignBang,
CommentLessThanSignBangDash,
CommentLessThanSignBangDashDash,
CommentEndDash,
CommentEnd,
CommentEndBang,
DOCTYPE,
BeforeDOCTYPEName,
DOCTYPEName,
AfterDOCTYPEName,
AfterDOCTYPEPublicKeyword,
BeforeDOCTYPEPublicIdentifier,
DOCTYPEPublicIdentifierDoubleQuoted,
DOCTYPEPublicIdentifierSingleQuoted,
AfterDOCTYPEPublicIdentifier,
BetweenDOCTYPEPublicAndSystemIdentifiers,
AfterDOCTYPESystemKeyword,
BeforeDOCTYPESystemIdentifier,
DOCTYPESystemIdentifierDoubleQuoted,
DOCTYPESystemIdentifierSingleQuoted,
AfterDOCTYPESystemIdentifier,
BogusDOCTYPE,
CDATASection,
CDATASectionBracket,
CDATASectionEnd,
CharacterReference,
NamedCharacterReference,
AmbiguousAmpersand,
NumericCharacterReference,
HexadecimalCharacterReferenceStart,
DecimalCharacterReferenceStart,
HexadecimalCharacterReference,
DecimalCharacterReference,
NumericCharacterReferenceEnd,
}
impl State {
pub(crate) fn from_ffi(value: u8) -> Option<Self> {
Some(match value {
value if value == Self::Data as u8 => Self::Data,
value if value == Self::RCDATA as u8 => Self::RCDATA,
value if value == Self::RAWTEXT as u8 => Self::RAWTEXT,
value if value == Self::ScriptData as u8 => Self::ScriptData,
value if value == Self::PLAINTEXT as u8 => Self::PLAINTEXT,
value if value == Self::TagOpen as u8 => Self::TagOpen,
value if value == Self::EndTagOpen as u8 => Self::EndTagOpen,
value if value == Self::TagName as u8 => Self::TagName,
value if value == Self::RCDATALessThanSign as u8 => Self::RCDATALessThanSign,
value if value == Self::RCDATAEndTagOpen as u8 => Self::RCDATAEndTagOpen,
value if value == Self::RCDATAEndTagName as u8 => Self::RCDATAEndTagName,
value if value == Self::RAWTEXTLessThanSign as u8 => Self::RAWTEXTLessThanSign,
value if value == Self::RAWTEXTEndTagOpen as u8 => Self::RAWTEXTEndTagOpen,
value if value == Self::RAWTEXTEndTagName as u8 => Self::RAWTEXTEndTagName,
value if value == Self::ScriptDataLessThanSign as u8 => Self::ScriptDataLessThanSign,
value if value == Self::ScriptDataEndTagOpen as u8 => Self::ScriptDataEndTagOpen,
value if value == Self::ScriptDataEndTagName as u8 => Self::ScriptDataEndTagName,
value if value == Self::ScriptDataEscapeStart as u8 => Self::ScriptDataEscapeStart,
value if value == Self::ScriptDataEscapeStartDash as u8 => Self::ScriptDataEscapeStartDash,
value if value == Self::ScriptDataEscaped as u8 => Self::ScriptDataEscaped,
value if value == Self::ScriptDataEscapedDash as u8 => Self::ScriptDataEscapedDash,
value if value == Self::ScriptDataEscapedDashDash as u8 => Self::ScriptDataEscapedDashDash,
value if value == Self::ScriptDataEscapedLessThanSign as u8 => Self::ScriptDataEscapedLessThanSign,
value if value == Self::ScriptDataEscapedEndTagOpen as u8 => Self::ScriptDataEscapedEndTagOpen,
value if value == Self::ScriptDataEscapedEndTagName as u8 => Self::ScriptDataEscapedEndTagName,
value if value == Self::ScriptDataDoubleEscapeStart as u8 => Self::ScriptDataDoubleEscapeStart,
value if value == Self::ScriptDataDoubleEscaped as u8 => Self::ScriptDataDoubleEscaped,
value if value == Self::ScriptDataDoubleEscapedDash as u8 => Self::ScriptDataDoubleEscapedDash,
value if value == Self::ScriptDataDoubleEscapedDashDash as u8 => Self::ScriptDataDoubleEscapedDashDash,
value if value == Self::ScriptDataDoubleEscapedLessThanSign as u8 => {
Self::ScriptDataDoubleEscapedLessThanSign
}
value if value == Self::ScriptDataDoubleEscapeEnd as u8 => Self::ScriptDataDoubleEscapeEnd,
value if value == Self::BeforeAttributeName as u8 => Self::BeforeAttributeName,
value if value == Self::AttributeName as u8 => Self::AttributeName,
value if value == Self::AfterAttributeName as u8 => Self::AfterAttributeName,
value if value == Self::BeforeAttributeValue as u8 => Self::BeforeAttributeValue,
value if value == Self::AttributeValueDoubleQuoted as u8 => Self::AttributeValueDoubleQuoted,
value if value == Self::AttributeValueSingleQuoted as u8 => Self::AttributeValueSingleQuoted,
value if value == Self::AttributeValueUnquoted as u8 => Self::AttributeValueUnquoted,
value if value == Self::AfterAttributeValueQuoted as u8 => Self::AfterAttributeValueQuoted,
value if value == Self::SelfClosingStartTag as u8 => Self::SelfClosingStartTag,
value if value == Self::BogusComment as u8 => Self::BogusComment,
value if value == Self::MarkupDeclarationOpen as u8 => Self::MarkupDeclarationOpen,
value if value == Self::CommentStart as u8 => Self::CommentStart,
value if value == Self::CommentStartDash as u8 => Self::CommentStartDash,
value if value == Self::Comment as u8 => Self::Comment,
value if value == Self::CommentLessThanSign as u8 => Self::CommentLessThanSign,
value if value == Self::CommentLessThanSignBang as u8 => Self::CommentLessThanSignBang,
value if value == Self::CommentLessThanSignBangDash as u8 => Self::CommentLessThanSignBangDash,
value if value == Self::CommentLessThanSignBangDashDash as u8 => Self::CommentLessThanSignBangDashDash,
value if value == Self::CommentEndDash as u8 => Self::CommentEndDash,
value if value == Self::CommentEnd as u8 => Self::CommentEnd,
value if value == Self::CommentEndBang as u8 => Self::CommentEndBang,
value if value == Self::DOCTYPE as u8 => Self::DOCTYPE,
value if value == Self::BeforeDOCTYPEName as u8 => Self::BeforeDOCTYPEName,
value if value == Self::DOCTYPEName as u8 => Self::DOCTYPEName,
value if value == Self::AfterDOCTYPEName as u8 => Self::AfterDOCTYPEName,
value if value == Self::AfterDOCTYPEPublicKeyword as u8 => Self::AfterDOCTYPEPublicKeyword,
value if value == Self::BeforeDOCTYPEPublicIdentifier as u8 => Self::BeforeDOCTYPEPublicIdentifier,
value if value == Self::DOCTYPEPublicIdentifierDoubleQuoted as u8 => {
Self::DOCTYPEPublicIdentifierDoubleQuoted
}
value if value == Self::DOCTYPEPublicIdentifierSingleQuoted as u8 => {
Self::DOCTYPEPublicIdentifierSingleQuoted
}
value if value == Self::AfterDOCTYPEPublicIdentifier as u8 => Self::AfterDOCTYPEPublicIdentifier,
value if value == Self::BetweenDOCTYPEPublicAndSystemIdentifiers as u8 => {
Self::BetweenDOCTYPEPublicAndSystemIdentifiers
}
value if value == Self::AfterDOCTYPESystemKeyword as u8 => Self::AfterDOCTYPESystemKeyword,
value if value == Self::BeforeDOCTYPESystemIdentifier as u8 => Self::BeforeDOCTYPESystemIdentifier,
value if value == Self::DOCTYPESystemIdentifierDoubleQuoted as u8 => {
Self::DOCTYPESystemIdentifierDoubleQuoted
}
value if value == Self::DOCTYPESystemIdentifierSingleQuoted as u8 => {
Self::DOCTYPESystemIdentifierSingleQuoted
}
value if value == Self::AfterDOCTYPESystemIdentifier as u8 => Self::AfterDOCTYPESystemIdentifier,
value if value == Self::BogusDOCTYPE as u8 => Self::BogusDOCTYPE,
value if value == Self::CDATASection as u8 => Self::CDATASection,
value if value == Self::CDATASectionBracket as u8 => Self::CDATASectionBracket,
value if value == Self::CDATASectionEnd as u8 => Self::CDATASectionEnd,
value if value == Self::CharacterReference as u8 => Self::CharacterReference,
value if value == Self::NamedCharacterReference as u8 => Self::NamedCharacterReference,
value if value == Self::AmbiguousAmpersand as u8 => Self::AmbiguousAmpersand,
value if value == Self::NumericCharacterReference as u8 => Self::NumericCharacterReference,
value if value == Self::HexadecimalCharacterReferenceStart as u8 => {
Self::HexadecimalCharacterReferenceStart
}
value if value == Self::DecimalCharacterReferenceStart as u8 => Self::DecimalCharacterReferenceStart,
value if value == Self::HexadecimalCharacterReference as u8 => Self::HexadecimalCharacterReference,
value if value == Self::DecimalCharacterReference as u8 => Self::DecimalCharacterReference,
value if value == Self::NumericCharacterReferenceEnd as u8 => Self::NumericCharacterReferenceEnd,
_ => return None,
})
}
}
/// The HTML tokenizer state machine.
///
/// Implements the WHATWG HTML tokenizer specification (section 13.2.5).
pub struct HtmlTokenizer {
pub state: State,
return_state: State,
pub input: Vec<u32>,
pub current_offset: usize,
prev_offset: usize,
current_token: Token,
current_builder: String,
pub queued_tokens: VecDeque<Token>,
temporary_buffer: Vec<u32>,
character_reference_code: u32,
last_emitted_start_tag_name: Option<String>,
source_positions: Vec<Position>,
// Mirror of the most recent entry in source_positions, kept in sync
// with it on the slow path and updated directly on the fast
// character-emission path so we don't have to push into the Vec per
// character in Data-state text runs.
current_line: u64,
current_column: u64,
has_emitted_eof: bool,
aborted: bool,
insertion_point: Option<usize>,
old_insertion_points: Vec<Option<usize>>,
input_stream_closed: bool,
stop_at_insertion_point: bool,
cdata_allowed: bool,
entity_matcher: NamedCharacterReferenceMatcher,
}
#[inline]
fn is_ascii_alpha(c: u32) -> bool {
matches!(c, 0x41..=0x5A | 0x61..=0x7A)
}
#[inline]
fn is_ascii_upper_alpha(c: u32) -> bool {
matches!(c, 0x41..=0x5A)
}
#[inline]
fn is_ascii_digit(c: u32) -> bool {
matches!(c, 0x30..=0x39)
}
#[inline]
fn is_ascii_hex_digit(c: u32) -> bool {
matches!(c, 0x30..=0x39 | 0x41..=0x46 | 0x61..=0x66)
}
#[inline]
fn is_ascii_alphanumeric(c: u32) -> bool {
is_ascii_alpha(c) || is_ascii_digit(c)
}
#[inline]
fn to_ascii_lowercase(c: u32) -> u32 {
if is_ascii_upper_alpha(c) { c + 0x20 } else { c }
}
#[inline]
fn is_whitespace(c: u32) -> bool {
matches!(c, 0x09 | 0x0A | 0x0C | 0x20)
}
/// Numeric character reference replacement table (spec section 13.2.5.5).
fn numeric_char_ref_replacement(code: u32) -> Option<u32> {
match code {
0x80 => Some(0x20AC),
0x82 => Some(0x201A),
0x83 => Some(0x0192),
0x84 => Some(0x201E),
0x85 => Some(0x2026),
0x86 => Some(0x2020),
0x87 => Some(0x2021),
0x88 => Some(0x02C6),
0x89 => Some(0x2030),
0x8A => Some(0x0160),
0x8B => Some(0x2039),
0x8C => Some(0x0152),
0x8E => Some(0x017D),
0x91 => Some(0x2018),
0x92 => Some(0x2019),
0x93 => Some(0x201C),
0x94 => Some(0x201D),
0x95 => Some(0x2022),
0x96 => Some(0x2013),
0x97 => Some(0x2014),
0x98 => Some(0x02DC),
0x99 => Some(0x2122),
0x9A => Some(0x0161),
0x9B => Some(0x203A),
0x9C => Some(0x0153),
0x9E => Some(0x017E),
0x9F => Some(0x0178),
_ => None,
}
}
/// Check if a code point is a Unicode surrogate.
fn is_surrogate(c: u32) -> bool {
(0xD800..=0xDFFF).contains(&c)
}
/// Check if a code point is a Unicode noncharacter.
fn is_noncharacter(c: u32) -> bool {
(0xFDD0..=0xFDEF).contains(&c) || matches!(c & 0xFFFF, 0xFFFE | 0xFFFF)
}
#[inline]
fn is_ascii_lower_alpha(c: u32) -> bool {
matches!(c, 0x61..=0x7A)
}
/// Push a code point onto a `String` as UTF-8 without paying
/// `char::from_u32`'s surrogate check or `String::push`'s len_utf8 branch
/// for the common ASCII case. For `cp < 0x80` this is a single byte push
/// to the underlying Vec; for wider code points we fall back to the
/// normal slow path.
#[inline(always)]
fn push_code_point(buf: &mut String, cp: u32) {
if cp < 0x80 {
// SAFETY: ASCII is always valid UTF-8.
unsafe { buf.as_mut_vec().push(cp as u8) };
} else {
buf.push(char::from_u32(cp).unwrap_or('\u{FFFD}'));
}
}
impl HtmlTokenizer {
/// Create a new tokenizer for the given input (as UTF-32 code points).
pub fn new(input: Vec<u32>) -> Self {
HtmlTokenizer {
state: State::Data,
return_state: State::Data,
input,
current_offset: 0,
prev_offset: 0,
current_token: Token::default(),
current_builder: String::new(),
queued_tokens: VecDeque::new(),
temporary_buffer: Vec::new(),
character_reference_code: 0,
last_emitted_start_tag_name: None,
source_positions: vec![Position { line: 0, column: 0 }],
current_line: 0,
current_column: 0,
has_emitted_eof: false,
aborted: false,
insertion_point: None,
old_insertion_points: Vec::new(),
input_stream_closed: true,
stop_at_insertion_point: false,
cdata_allowed: false,
entity_matcher: NamedCharacterReferenceMatcher::new(),
}
}
/// Set the tokenizer state.
pub fn switch_to(&mut self, state: State) {
self.state = state;
}
// -- Insertion point management --
pub fn store_insertion_point(&mut self) {
self.old_insertion_points.push(self.insertion_point);
}
pub fn restore_insertion_point(&mut self) {
self.insertion_point = self.old_insertion_points.pop().unwrap_or(None);
}
pub fn update_insertion_point(&mut self) {
self.insertion_point = Some(self.current_offset);
}
pub fn undefine_insertion_point(&mut self) {
self.insertion_point = None;
}
pub fn is_insertion_point_defined(&self) -> bool {
self.insertion_point.is_some()
}
pub fn is_insertion_point_reached(&self) -> bool {
self.insertion_point
.is_some_and(|insertion_point| self.current_offset >= insertion_point)
}
pub fn append_input(&mut self, code_points: &[u32]) {
self.input.extend_from_slice(code_points);
}
pub fn insert_input_at_insertion_point(&mut self, code_points: &[u32]) {
if let Some(ip) = self.insertion_point {
let ip = ip.min(self.input.len());
self.input.splice(ip..ip, code_points.iter().copied());
self.insertion_point = Some(ip + code_points.len());
for old_insertion_point in &mut self.old_insertion_points {
if let Some(old_ip) = old_insertion_point
&& ip <= *old_ip
{
*old_ip += code_points.len();
}
}
}
}
pub fn unparsed_input(&self) -> String {
let mut output = String::new();
for code_point in self.input[self.current_offset..].iter().copied() {
push_code_point(&mut output, code_point);
}
output
}
pub fn parser_did_run(&mut self) {
if self.current_offset == 0
|| self.current_offset != self.input.len()
|| self.insertion_point.is_some_and(|insertion_point| insertion_point != 0)
|| !self.old_insertion_points.is_empty()
{
return;
}
self.input = Vec::new();
self.current_offset = 0;
self.prev_offset = 0;
let last_position = *self.source_positions.last().unwrap_or(&Position::default());
self.source_positions.clear();
self.source_positions.push(last_position);
self.current_line = last_position.line;
self.current_column = last_position.column;
}
pub fn insert_eof(&mut self) {
self.input_stream_closed = true;
}
pub fn set_input_stream_closed(&mut self, closed: bool) {
self.input_stream_closed = closed;
}
pub fn abort(&mut self) {
self.aborted = true;
}
// -- Input helpers --
#[inline]
fn peek_code_point(&self, offset: isize) -> Option<u32> {
let idx = self.current_offset as isize + offset;
if idx < 0 || idx as usize >= self.input.len() {
return None;
}
if self.stop_at_insertion_point
&& let Some(ip) = self.insertion_point
&& idx as usize >= ip
{
return None;
}
Some(self.input[idx as usize])
}
#[inline(always)]
fn skip(&mut self, count: usize) {
if !self.source_positions.is_empty() {
let last = *self.source_positions.last().unwrap();
self.source_positions.push(last);
}
// Keep position updates in local registers across the loop to
// avoid a store-to-load dependency on the source_positions stack
// top. The stack top is written back once at the end.
let (mut line, mut column) = if let Some(pos) = self.source_positions.last() {
(pos.line, pos.column)
} else {
(self.current_line, self.current_column)
};
for _ in 0..count {
self.prev_offset = self.current_offset;
let code_point = self.input[self.current_offset];
if code_point == 0x0A {
line += 1;
column = 0;
} else {
column += 1;
}
self.current_offset += 1;
}
self.current_line = line;
self.current_column = column;
if let Some(pos) = self.source_positions.last_mut() {
pos.line = line;
pos.column = column;
}
}
#[inline(always)]
fn next_code_point(&mut self) -> Option<u32> {
if self.current_offset >= self.input.len() {
self.prev_offset = self.current_offset;
return None;
}
let cp = self.input[self.current_offset];
if cp != 0x0D {
self.skip(1);
return Some(cp);
}
// Slow path: CR normalization
let next = self.peek_code_point(1).unwrap_or(0);
if next == 0x0A {
self.skip(2);
} else {
self.skip(1);
}
Some(0x0A)
}
#[inline(always)]
fn nth_last_position(&self, n: usize) -> Position {
if n + 1 > self.source_positions.len() {
return Position { line: 0, column: 0 };
}
self.source_positions[self.source_positions.len() - 1 - n]
}
/// Fast-path character emission from the Data state: if the tokenizer
/// is positioned at a plain-text code point (not `<`, `&`, NUL, CR,
/// or any non-ASCII character) and the queue is empty, advance one
/// code point and return the character's position, skipping the
/// full `next_token` state-machine dispatch and Token construction.
/// Returns None if the slow path is required.
#[inline(always)]
pub fn try_fast_data_char(&mut self) -> Option<(u32, Position)> {
if self.aborted {
return None;
}
if self.state as u8 != State::Data as u8 {
return None;
}
if !self.queued_tokens.is_empty() {
return None;
}
if self.current_offset >= self.input.len() {
self.sync_source_positions();
return None;
}
let cp = self.input[self.current_offset];
if cp == 0x3C || cp == 0x26 || cp == 0x00 || cp == 0x0D || cp >= 0x80 {
self.sync_source_positions();
return None;
}
if cp == 0x0A {
self.current_line += 1;
self.current_column = 0;
} else {
self.current_column += 1;
}
let pos = Position {
line: self.current_line,
column: self.current_column,
};
self.prev_offset = self.current_offset;
self.current_offset += 1;
Some((cp, pos))
}
/// Resynchronise `source_positions` with the scalar current_line /
/// current_column after a run of fast-path character emissions.
#[inline(always)]
fn sync_source_positions(&mut self) {
self.source_positions.clear();
self.source_positions.push(Position {
line: self.current_line,
column: self.current_column,
});
}
fn restore_to(&mut self, offset: usize) {
while self.current_offset > offset && self.source_positions.len() > 1 {
self.source_positions.pop();
self.current_offset -= 1;
}
self.current_offset = offset;
if let Some(pos) = self.source_positions.last() {
self.current_line = pos.line;
self.current_column = pos.column;
}
}
fn consume_current_builder(&mut self) -> String {
std::mem::take(&mut self.current_builder)
}
/// Commit `current_builder` into `current_token.tag_name` (or its
/// interned-id slot). If the accumulated bytes hit the intern table
/// we clear the builder in place, preserving its capacity for the
/// next token and avoiding the String allocation entirely. Most real
/// HTML tag names are interned.
#[inline]
fn commit_current_builder_as_tag_name(&mut self) {
let id = crate::interned_names::lookup_tag_name(self.current_builder.as_bytes());
if id != 0 {
self.current_builder.clear();
self.current_token.set_tag_name_id(id);
} else {
let name = std::mem::take(&mut self.current_builder);
*self.current_token.tag_name_mut() = name;
}
}
fn create_new_token(&mut self, token_type: TokenType) {
let payload = match token_type {
TokenType::StartTag | TokenType::EndTag => TokenPayload::Tag {
tag_name: String::new(),
tag_name_id: 0,
self_closing: false,
had_duplicate_attribute: false,
attributes: Vec::new(),
},
TokenType::Comment => TokenPayload::Comment(String::new()),
TokenType::Doctype => TokenPayload::Doctype(Box::default()),
_ => TokenPayload::None,
};
let pos = match token_type {
TokenType::StartTag | TokenType::EndTag => self.nth_last_position(1),
_ => self.nth_last_position(0),
};
self.current_token = Token {
token_type,
code_point: 0,
payload,
start_position: pos,
end_position: Position::default(),
};
}
fn current_end_tag_token_is_appropriate(&self) -> bool {
if self.current_token.token_type != TokenType::EndTag {
return false;
}
match &self.last_emitted_start_tag_name {
Some(name) => self.current_token.tag_name() == name,
None => false,
}
}
fn consumed_as_part_of_an_attribute(&self) -> bool {
matches!(
self.return_state,
State::AttributeValueDoubleQuoted | State::AttributeValueSingleQuoted | State::AttributeValueUnquoted
)
}
fn will_emit(&mut self, token_idx: usize) {
// token_idx: 0 = current_token, 1+ = queued_tokens index
// For simplicity, we handle position setting here.
if token_idx == 0 {
if self.current_token.token_type == TokenType::StartTag
|| self.current_token.token_type == TokenType::EndTag
{
self.current_token.normalize_attributes();
}
if self.current_token.token_type == TokenType::StartTag {
self.last_emitted_start_tag_name = Some(self.current_token.tag_name().to_string());
}
let is_start_or_end_tag = self.current_token.token_type == TokenType::StartTag
|| self.current_token.token_type == TokenType::EndTag;
self.current_token.end_position = self.nth_last_position(if is_start_or_end_tag { 1 } else { 0 });
}
}
fn emit_current_token(&mut self) {
self.will_emit(0);
let token = std::mem::take(&mut self.current_token);
self.queued_tokens.push_back(token);
}
/// Fast-path version of `emit_current_token(); self.queued_tokens.pop_front()`.
/// When the queue is empty we skip the push/pop round trip entirely,
/// which removes two VecDeque ops and a Token move per emitted token
/// for the overwhelmingly common single-emit case.
#[inline]
fn emit_current_token_direct(&mut self) -> Option<Token> {
self.will_emit(0);
let token = std::mem::take(&mut self.current_token);
if self.queued_tokens.is_empty() {
Some(token)
} else {
self.queued_tokens.push_back(token);
self.queued_tokens.pop_front()
}
}
#[inline(always)]
fn return_character_token(&mut self, code_point: u32) -> Option<Token> {
let mut token = Token::new_character(code_point);
token.start_position = self.nth_last_position(0);
if self.queued_tokens.is_empty() {
Some(token)
} else {
self.queued_tokens.push_back(token);
self.queued_tokens.pop_front()
}
}
fn emit_eof(&mut self) {
if self.has_emitted_eof {
return;
}
self.has_emitted_eof = true;
self.create_new_token(TokenType::EndOfFile);
self.emit_current_token();
}
fn emit_current_token_followed_by_eof(&mut self) {
self.emit_current_token();
self.has_emitted_eof = true;
self.create_new_token(TokenType::EndOfFile);
self.emit_current_token();
}
fn flush_codepoints_consumed_as_character_reference(&mut self) {
for i in 0..self.temporary_buffer.len() {
let code_point = self.temporary_buffer[i];
if self.consumed_as_part_of_an_attribute() {
push_code_point(&mut self.current_builder, code_point);
} else {
let mut token = Token::new_character(code_point);
token.start_position = self.nth_last_position(0);
self.queued_tokens.push_back(token);
}
}
}
fn can_run_out_at(&self, idx: usize) -> bool {
if self.stop_at_insertion_point
&& let Some(ip) = self.insertion_point
&& idx >= ip
{
return true;
}
!self.input_stream_closed && idx >= self.input.len()
}
#[inline]
fn fast_scan_limit(&self) -> usize {
if self.stop_at_insertion_point
&& let Some(ip) = self.insertion_point
{
return ip.min(self.input.len());
}
self.input.len()
}
fn should_pause_before_next_input_character(&self) -> bool {
if self.stop_at_insertion_point
&& let Some(ip) = self.insertion_point
{
if self.current_offset >= ip {
return true;
}
if self.current_offset < self.input.len()
&& self.input[self.current_offset] == 0x0D
&& self.current_offset + 1 == ip
{
return true;
}
}
if self.input_stream_closed {
return false;
}
if self.current_offset >= self.input.len() {
return true;
}
self.input[self.current_offset] == 0x0D && self.current_offset + 1 >= self.input.len()
}
/// Case-insensitive match of upcoming input against a string.
/// Returns Some(true) if matched and consumed, Some(false) if no match,
/// None if ran out of characters at the insertion point.
fn consume_next_if_match(&mut self, s: &str) -> Option<bool> {
for (i, b) in s.bytes().enumerate() {
match self.peek_code_point(i as isize) {
None => {
if self.can_run_out_at(self.current_offset + i) {
return None;
}
return Some(false);
}
Some(cp) => {
if to_ascii_lowercase(cp) != to_ascii_lowercase(b as u32) {
return Some(false);
}
}
}
}
// All matched, consume them
self.skip(s.len());
Some(true)
}
/// Case-sensitive match of upcoming input against a string.
fn consume_next_if_match_exact(&mut self, s: &str) -> Option<bool> {
for (i, b) in s.bytes().enumerate() {
match self.peek_code_point(i as isize) {
None => {
if self.can_run_out_at(self.current_offset + i) {
return None;
}
return Some(false);
}
Some(cp) => {
if cp != b as u32 {
return Some(false);
}
}
}
}
self.skip(s.len());
Some(true)
}
/// Get the next token from the tokenizer.
pub fn next_token(&mut self, stop_at_insertion_point: bool, cdata_allowed: bool) -> Option<Token> {
self.stop_at_insertion_point = stop_at_insertion_point;
self.cdata_allowed = cdata_allowed;
// Return queued tokens first.
{
let last = *self.source_positions.last().unwrap_or(&Position::default());
self.source_positions.clear();
self.source_positions.push(last);
}
if let Some(token) = self.queued_tokens.pop_front() {
return Some(token);
}
if self.aborted {
return None;
}
loop {
// Check insertion point before consuming.
if self.should_pause_before_next_input_character() {
return None;
}
let current_input_character = self.next_code_point();
match self.state {
// 13.2.5.1 Data state
State::Data => {
match current_input_character {
Some(0x26) => {
// '&'
self.return_state = State::Data;
self.state = State::CharacterReference;
continue;
}
Some(0x3C) => {
// '<'
self.state = State::TagOpen;
continue;
}
Some(0x00) => {
// NULL - parse error
return self.return_character_token(0x00);
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
return self.return_character_token(cp);
}
}
}
// 13.2.5.2 RCDATA state
State::RCDATA => match current_input_character {
Some(0x26) => {
self.return_state = State::RCDATA;
self.state = State::CharacterReference;
continue;
}
Some(0x3C) => {
self.state = State::RCDATALessThanSign;
continue;
}
Some(0x00) => {
return self.return_character_token(0xFFFD);
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
return self.return_character_token(cp);
}
},
// 13.2.5.3 RAWTEXT state
State::RAWTEXT => match current_input_character {
Some(0x3C) => {
self.state = State::RAWTEXTLessThanSign;
continue;
}
Some(0x00) => {
return self.return_character_token(0xFFFD);
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
return self.return_character_token(cp);
}
},
// 13.2.5.4 Script data state
State::ScriptData => match current_input_character {
Some(0x3C) => {
self.state = State::ScriptDataLessThanSign;
continue;
}
Some(0x00) => {
return self.return_character_token(0xFFFD);
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
return self.return_character_token(cp);
}
},
// 13.2.5.5 PLAINTEXT state
State::PLAINTEXT => match current_input_character {
Some(0x00) => {
return self.return_character_token(0xFFFD);
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
return self.return_character_token(cp);
}
},
// 13.2.5.6 Tag open state
State::TagOpen => match current_input_character {
Some(0x21) => {
self.state = State::MarkupDeclarationOpen;
continue;
}
Some(0x2F) => {
self.state = State::EndTagOpen;
continue;
}
Some(cp) if is_ascii_alpha(cp) => {
self.create_new_token(TokenType::StartTag);
self.reconsume(State::TagName);
continue;
}
Some(0x3F) => {
// '?' - parse error
self.create_new_token(TokenType::Comment);
self.current_token.start_position = self.nth_last_position(2);
self.reconsume(State::BogusComment);
continue;
}
None => {
// parse error
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
// parse error
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.reconsume(State::Data);
continue;
}
},
// 13.2.5.7 End tag open state
State::EndTagOpen => match current_input_character {
Some(cp) if is_ascii_alpha(cp) => {
self.create_new_token(TokenType::EndTag);
self.reconsume(State::TagName);
continue;
}
Some(0x3E) => {
// '>' - parse error
self.state = State::Data;
continue;
}
None => {
// parse error
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.queued_tokens.push_back(self.make_character_token(0x2F));
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
// parse error
self.create_new_token(TokenType::Comment);
self.reconsume(State::BogusComment);
continue;
}
},
// 13.2.5.8 Tag name state
State::TagName => {
// Fast-path run: bulk-scan consecutive lowercase
// ASCII characters and append them to current_builder.
// This handles the common case where a tag name is
// a plain `div`, `span`, `a` etc. and avoids paying
// the per-character state-machine dispatch cost.
// We keep the first character the outer state loop
// already consumed, then extend the builder by
// scanning from current_offset until we see a byte
// that needs special handling.
if let Some(cp) = current_input_character
&& cp >= b'a' as u32
&& cp <= b'z' as u32
{
// SAFETY: pushing ASCII bytes into a String is
// always valid UTF-8. We drop into a tight
// inner loop that directly advances
// current_offset and current_column without
// going through skip(1) per character.
unsafe { self.current_builder.as_mut_vec().push(cp as u8) };
let start_offset = self.current_offset;
let mut off = start_offset;
let input_len = self.fast_scan_limit();
while off < input_len {
let next = self.input[off];
if next < b'a' as u32 || next > b'z' as u32 {
break;
}
unsafe { self.current_builder.as_mut_vec().push(next as u8) };
off += 1;
}
let consumed = off - start_offset;
if consumed > 0 {
self.current_column += consumed as u64;
self.prev_offset = off - 1;
self.current_offset = off;
if let Some(pos) = self.source_positions.last_mut() {
pos.line = self.current_line;
pos.column = self.current_column;
}
}
self.current_token.end_position = self.nth_last_position(0);
continue;
}
match current_input_character {
Some(cp) if is_whitespace(cp) => {
self.commit_current_builder_as_tag_name();
self.current_token.end_position = self.nth_last_position(1);
self.state = State::BeforeAttributeName;
continue;
}
Some(0x2F) => {
self.commit_current_builder_as_tag_name();
self.current_token.end_position = self.nth_last_position(0);
self.state = State::SelfClosingStartTag;
continue;
}
Some(0x3E) => {
self.commit_current_builder_as_tag_name();
self.state = State::Data;
return self.emit_current_token_direct();
}
Some(cp) if is_ascii_upper_alpha(cp) => {
push_code_point(&mut self.current_builder, to_ascii_lowercase(cp));
self.current_token.end_position = self.nth_last_position(0);
continue;
}
Some(0x00) => {
self.current_builder.push('\u{FFFD}');
self.current_token.end_position = self.nth_last_position(0);
continue;
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
push_code_point(&mut self.current_builder, cp);
self.current_token.end_position = self.nth_last_position(0);
continue;
}
}
}
// 13.2.5.32 Before attribute name state
State::BeforeAttributeName => match current_input_character {
Some(cp) if is_whitespace(cp) => {
continue;
}
Some(0x2F) | Some(0x3E) | None => {
self.reconsume(State::AfterAttributeName);
continue;
}
Some(0x3D) => {
// '=' - parse error
self.current_token.attributes_mut().push(Attribute::default());
self.current_builder.push('=');
self.state = State::AttributeName;
continue;
}
Some(_) => {
self.current_token.attributes_mut().push(Attribute::default());
self.reconsume(State::AttributeName);
continue;
}
},
// 13.2.5.33 Attribute name state
State::AttributeName => {
// Fast-path run: bulk-scan consecutive lowercase ASCII
// and a few other name-safe bytes (digits, '-', '_').
if let Some(cp) = current_input_character {
let is_name_char = (cp >= b'a' as u32 && cp <= b'z' as u32)
|| (cp >= b'0' as u32 && cp <= b'9' as u32)
|| cp == b'-' as u32
|| cp == b'_' as u32;
if is_name_char {
unsafe { self.current_builder.as_mut_vec().push(cp as u8) };
let start_offset = self.current_offset;
let mut off = start_offset;
let input_len = self.fast_scan_limit();
while off < input_len {
let next = self.input[off];
let ok = (next >= b'a' as u32 && next <= b'z' as u32)
|| (next >= b'0' as u32 && next <= b'9' as u32)
|| next == b'-' as u32
|| next == b'_' as u32;
if !ok {
break;
}
unsafe { self.current_builder.as_mut_vec().push(next as u8) };
off += 1;
}
let consumed = off - start_offset;
if consumed > 0 {
self.current_column += consumed as u64;
self.prev_offset = off - 1;
self.current_offset = off;
if let Some(pos) = self.source_positions.last_mut() {
pos.line = self.current_line;
pos.column = self.current_column;
}
}
continue;
}
}
match current_input_character {
Some(cp) if is_whitespace(cp) => {
self.set_attribute_name();
self.reconsume(State::AfterAttributeName);
continue;
}
Some(0x2F) | Some(0x3E) | None => {
self.set_attribute_name();
self.reconsume(State::AfterAttributeName);
continue;
}
Some(0x3D) => {
self.set_attribute_name();
self.state = State::BeforeAttributeValue;
continue;
}
Some(cp) if is_ascii_upper_alpha(cp) => {
push_code_point(&mut self.current_builder, to_ascii_lowercase(cp));
continue;
}
Some(0x00) => {
self.current_builder.push('\u{FFFD}');
continue;
}
Some(cp @ (0x22 | 0x27 | 0x3C)) => {
// '"', '\'', '<' - parse error
push_code_point(&mut self.current_builder, cp);
continue;
}
Some(cp) => {
push_code_point(&mut self.current_builder, cp);
continue;
}
}
}
// 13.2.5.34 After attribute name state
State::AfterAttributeName => match current_input_character {
Some(cp) if is_whitespace(cp) => {
continue;
}
Some(0x2F) => {
self.state = State::SelfClosingStartTag;
continue;
}
Some(0x3D) => {
self.state = State::BeforeAttributeValue;
continue;
}
Some(0x3E) => {
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
self.current_token.attributes_mut().push(Attribute::default());
self.reconsume(State::AttributeName);
continue;
}
},
// 13.2.5.35 Before attribute value state
State::BeforeAttributeValue => {
let pos = self.nth_last_position(1);
if let Some(attr) = self.current_token.attributes_mut().last_mut() {
attr.value_start_position = pos;
}
match current_input_character {
Some(cp) if is_whitespace(cp) => {
continue;
}
Some(0x22) => {
self.state = State::AttributeValueDoubleQuoted;
continue;
}
Some(0x27) => {
self.state = State::AttributeValueSingleQuoted;
continue;
}
Some(0x3E) => {
// parse error
self.state = State::Data;
return self.emit_current_token_direct();
}
_ => {
self.reconsume(State::AttributeValueUnquoted);
continue;
}
}
}
// 13.2.5.36 Attribute value (double-quoted) state
State::AttributeValueDoubleQuoted => {
// Fast-path run: bulk-append printable ASCII until we
// hit `"`, `&`, NUL, CR or non-ASCII. The inner loop
// advances a local offset directly and only updates
// tokenizer/position state once at the end.
if let Some(cp) = current_input_character
&& (0x20..0x80).contains(&cp)
&& cp != 0x22
&& cp != 0x26
{
unsafe { self.current_builder.as_mut_vec().push(cp as u8) };
let start_offset = self.current_offset;
let mut off = start_offset;
let input_len = self.fast_scan_limit();
while off < input_len {
let next = self.input[off];
if !(0x20..0x80).contains(&next) || next == 0x22 || next == 0x26 {
break;
}
unsafe { self.current_builder.as_mut_vec().push(next as u8) };
off += 1;
}
let consumed = off - start_offset;
if consumed > 0 {
self.current_column += consumed as u64;
self.prev_offset = off - 1;
self.current_offset = off;
if let Some(pos) = self.source_positions.last_mut() {
pos.line = self.current_line;
pos.column = self.current_column;
}
}
continue;
}
match current_input_character {
Some(0x22) => {
self.set_attribute_value();
self.state = State::AfterAttributeValueQuoted;
continue;
}
Some(0x26) => {
self.return_state = State::AttributeValueDoubleQuoted;
self.state = State::CharacterReference;
continue;
}
Some(0x00) => {
self.current_builder.push('\u{FFFD}');
continue;
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
push_code_point(&mut self.current_builder, cp);
continue;
}
}
}
// 13.2.5.37 Attribute value (single-quoted) state
State::AttributeValueSingleQuoted => match current_input_character {
Some(0x27) => {
self.set_attribute_value();
self.state = State::AfterAttributeValueQuoted;
continue;
}
Some(0x26) => {
self.return_state = State::AttributeValueSingleQuoted;
self.state = State::CharacterReference;
continue;
}
Some(0x00) => {
self.current_builder.push('\u{FFFD}');
continue;
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
push_code_point(&mut self.current_builder, cp);
continue;
}
},
// 13.2.5.38 Attribute value (unquoted) state
State::AttributeValueUnquoted => match current_input_character {
Some(cp) if is_whitespace(cp) => {
self.set_attribute_value();
self.state = State::BeforeAttributeName;
continue;
}
Some(0x26) => {
self.return_state = State::AttributeValueUnquoted;
self.state = State::CharacterReference;
continue;
}
Some(0x3E) => {
self.set_attribute_value();
self.state = State::Data;
return self.emit_current_token_direct();
}
Some(0x00) => {
self.current_builder.push('\u{FFFD}');
continue;
}
Some(cp @ (0x22 | 0x27 | 0x3C | 0x3D | 0x60)) => {
// parse error, treat as anything else
push_code_point(&mut self.current_builder, cp);
continue;
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
push_code_point(&mut self.current_builder, cp);
continue;
}
},
// 13.2.5.39 After attribute value (quoted) state
State::AfterAttributeValueQuoted => {
let pos = self.nth_last_position(1);
if let Some(attr) = self.current_token.attributes_mut().last_mut() {
attr.value_end_position = pos;
}
match current_input_character {
Some(cp) if is_whitespace(cp) => {
self.state = State::BeforeAttributeName;
continue;
}
Some(0x2F) => {
self.state = State::SelfClosingStartTag;
continue;
}
Some(0x3E) => {
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
// parse error
self.reconsume(State::BeforeAttributeName);
continue;
}
}
}
// 13.2.5.40 Self-closing start tag state
State::SelfClosingStartTag => match current_input_character {
Some(0x3E) => {
self.current_token.set_self_closing(true);
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
// parse error
self.reconsume(State::BeforeAttributeName);
continue;
}
},
// 13.2.5.41 Bogus comment state
State::BogusComment => match current_input_character {
Some(0x3E) => {
{
let data = self.consume_current_builder();
self.current_token.set_comment_data(data);
}
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
{
let data = self.consume_current_builder();
self.current_token.set_comment_data(data);
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(0x00) => {
self.current_builder.push('\u{FFFD}');
continue;
}
Some(cp) => {
push_code_point(&mut self.current_builder, cp);
continue;
}
},
// 13.2.5.42 Markup declaration open state
State::MarkupDeclarationOpen => {
// Don't consume next input character (reconsume).
if current_input_character.is_some() {
self.restore_to(self.prev_offset);
}
match self.consume_next_if_match_exact("--") {
Some(true) => {
self.create_new_token(TokenType::Comment);
self.current_token.start_position = self.nth_last_position(3);
self.state = State::CommentStart;
continue;
}
None => return None,
_ => {}
}
match self.consume_next_if_match("DOCTYPE") {
Some(true) => {
self.state = State::DOCTYPE;
continue;
}
None => return None,
_ => {}
}
match self.consume_next_if_match_exact("[CDATA[") {
Some(true) => {
if self.cdata_allowed {
self.state = State::CDATASection;
} else {
self.create_new_token(TokenType::Comment);
self.current_builder.push_str("[CDATA[");
self.state = State::BogusComment;
}
continue;
}
None => return None,
_ => {}
}
// parse error
self.create_new_token(TokenType::Comment);
self.state = State::BogusComment;
continue;
}
// 13.2.5.43 Comment start state
State::CommentStart => match current_input_character {
Some(0x2D) => {
self.state = State::CommentStartDash;
continue;
}
Some(0x3E) => {
// parse error
{
let data = self.consume_current_builder();
self.current_token.set_comment_data(data);
}
self.state = State::Data;
return self.emit_current_token_direct();
}
_ => {
self.reconsume(State::Comment);
continue;
}
},
// 13.2.5.44 Comment start dash state
State::CommentStartDash => match current_input_character {
Some(0x2D) => {
self.state = State::CommentEnd;
continue;
}
Some(0x3E) => {
// parse error
{
let data = self.consume_current_builder();
self.current_token.set_comment_data(data);
}
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
{
let data = self.consume_current_builder();
self.current_token.set_comment_data(data);
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
self.current_builder.push('-');
self.reconsume(State::Comment);
continue;
}
},
// 13.2.5.45 Comment state
State::Comment => match current_input_character {
Some(0x3C) => {
self.current_builder.push('<');
self.state = State::CommentLessThanSign;
continue;
}
Some(0x2D) => {
self.state = State::CommentEndDash;
continue;
}
Some(0x00) => {
self.current_builder.push('\u{FFFD}');
continue;
}
None => {
{
let data = self.consume_current_builder();
self.current_token.set_comment_data(data);
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
push_code_point(&mut self.current_builder, cp);
continue;
}
},
// 13.2.5.46 Comment less-than sign state
State::CommentLessThanSign => match current_input_character {
Some(0x21) => {
self.current_builder.push('!');
self.state = State::CommentLessThanSignBang;
continue;
}
Some(0x3C) => {
self.current_builder.push('<');
continue;
}
_ => {
self.reconsume(State::Comment);
continue;
}
},
// 13.2.5.47 Comment less-than sign bang state
State::CommentLessThanSignBang => match current_input_character {
Some(0x2D) => {
self.state = State::CommentLessThanSignBangDash;
continue;
}
_ => {
self.reconsume(State::Comment);
continue;
}
},
// 13.2.5.48 Comment less-than sign bang dash state
State::CommentLessThanSignBangDash => match current_input_character {
Some(0x2D) => {
self.state = State::CommentLessThanSignBangDashDash;
continue;
}
_ => {
self.reconsume(State::CommentEndDash);
continue;
}
},
// 13.2.5.49 Comment less-than sign bang dash dash state
State::CommentLessThanSignBangDashDash => match current_input_character {
Some(0x3E) | None => {
self.reconsume(State::CommentEnd);
continue;
}
_ => {
// parse error
self.reconsume(State::CommentEnd);
continue;
}
},
// 13.2.5.50 Comment end dash state
State::CommentEndDash => match current_input_character {
Some(0x2D) => {
self.state = State::CommentEnd;
continue;
}
None => {
{
let data = self.consume_current_builder();
self.current_token.set_comment_data(data);
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
self.current_builder.push('-');
self.reconsume(State::Comment);
continue;
}
},
// 13.2.5.51 Comment end state
State::CommentEnd => match current_input_character {
Some(0x3E) => {
{
let data = self.consume_current_builder();
self.current_token.set_comment_data(data);
}
self.state = State::Data;
return self.emit_current_token_direct();
}
Some(0x21) => {
self.state = State::CommentEndBang;
continue;
}
Some(0x2D) => {
self.current_builder.push('-');
continue;
}
None => {
{
let data = self.consume_current_builder();
self.current_token.set_comment_data(data);
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
self.current_builder.push_str("--");
self.reconsume(State::Comment);
continue;
}
},
// 13.2.5.52 Comment end bang state
State::CommentEndBang => match current_input_character {
Some(0x2D) => {
self.current_builder.push_str("--!");
self.state = State::CommentEndDash;
continue;
}
Some(0x3E) => {
// parse error
{
let data = self.consume_current_builder();
self.current_token.set_comment_data(data);
}
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
{
let data = self.consume_current_builder();
self.current_token.set_comment_data(data);
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
self.current_builder.push_str("--!");
self.reconsume(State::Comment);
continue;
}
},
// 13.2.5.53 DOCTYPE state
State::DOCTYPE => match current_input_character {
Some(cp) if is_whitespace(cp) => {
self.state = State::BeforeDOCTYPEName;
continue;
}
Some(0x3E) => {
self.reconsume(State::BeforeDOCTYPEName);
continue;
}
None => {
self.create_new_token(TokenType::Doctype);
*self.current_token.doctype_data_mut() = DoctypeData {
force_quirks: true,
missing_name: true,
missing_public_identifier: true,
missing_system_identifier: true,
..Default::default()
};
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
// parse error
self.reconsume(State::BeforeDOCTYPEName);
continue;
}
},
// 13.2.5.54 Before DOCTYPE name state
State::BeforeDOCTYPEName => match current_input_character {
Some(cp) if is_whitespace(cp) => {
continue;
}
Some(cp) if is_ascii_upper_alpha(cp) => {
self.create_new_token(TokenType::Doctype);
*self.current_token.doctype_data_mut() = DoctypeData {
missing_name: false,
missing_public_identifier: true,
missing_system_identifier: true,
..Default::default()
};
push_code_point(&mut self.current_builder, to_ascii_lowercase(cp));
self.state = State::DOCTYPEName;
continue;
}
Some(0x00) => {
self.create_new_token(TokenType::Doctype);
*self.current_token.doctype_data_mut() = DoctypeData {
missing_name: false,
missing_public_identifier: true,
missing_system_identifier: true,
..Default::default()
};
self.current_builder.push('\u{FFFD}');
self.state = State::DOCTYPEName;
continue;
}
Some(0x3E) => {
// parse error
self.create_new_token(TokenType::Doctype);
*self.current_token.doctype_data_mut() = DoctypeData {
force_quirks: true,
missing_name: true,
missing_public_identifier: true,
missing_system_identifier: true,
..Default::default()
};
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
self.create_new_token(TokenType::Doctype);
*self.current_token.doctype_data_mut() = DoctypeData {
force_quirks: true,
missing_name: true,
missing_public_identifier: true,
missing_system_identifier: true,
..Default::default()
};
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
self.create_new_token(TokenType::Doctype);
*self.current_token.doctype_data_mut() = DoctypeData {
missing_name: false,
missing_public_identifier: true,
missing_system_identifier: true,
..Default::default()
};
push_code_point(&mut self.current_builder, cp);
self.state = State::DOCTYPEName;
continue;
}
},
// 13.2.5.55 DOCTYPE name state
State::DOCTYPEName => match current_input_character {
Some(cp) if is_whitespace(cp) => {
{
let name = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.name = name;
}
}
self.state = State::AfterDOCTYPEName;
continue;
}
Some(0x3E) => {
{
let name = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.name = name;
}
}
self.state = State::Data;
return self.emit_current_token_direct();
}
Some(cp) if is_ascii_upper_alpha(cp) => {
push_code_point(&mut self.current_builder, to_ascii_lowercase(cp));
continue;
}
Some(0x00) => {
self.current_builder.push('\u{FFFD}');
continue;
}
None => {
let name = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.name = name;
dd.force_quirks = true;
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
push_code_point(&mut self.current_builder, cp);
continue;
}
},
// 13.2.5.56 After DOCTYPE name state
State::AfterDOCTYPEName => match current_input_character {
Some(cp) if is_whitespace(cp) => {
continue;
}
Some(0x3E) => {
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
// Reconsume and try PUBLIC/SYSTEM
if current_input_character.is_some() {
self.restore_to(self.prev_offset);
}
match self.consume_next_if_match("PUBLIC") {
Some(true) => {
self.state = State::AfterDOCTYPEPublicKeyword;
continue;
}
None => return None,
_ => {}
}
match self.consume_next_if_match("SYSTEM") {
Some(true) => {
self.state = State::AfterDOCTYPESystemKeyword;
continue;
}
None => return None,
_ => {}
}
// Re-consume the character we put back
let _ = self.next_code_point();
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.reconsume(State::BogusDOCTYPE);
continue;
}
},
// 13.2.5.57 After DOCTYPE public keyword state
State::AfterDOCTYPEPublicKeyword => match current_input_character {
Some(cp) if is_whitespace(cp) => {
self.state = State::BeforeDOCTYPEPublicIdentifier;
continue;
}
Some(0x22) => {
{
let dd = self.current_token.doctype_data_mut();
dd.missing_public_identifier = false;
}
self.state = State::DOCTYPEPublicIdentifierDoubleQuoted;
continue;
}
Some(0x27) => {
{
let dd = self.current_token.doctype_data_mut();
dd.missing_public_identifier = false;
}
self.state = State::DOCTYPEPublicIdentifierSingleQuoted;
continue;
}
Some(0x3E) => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.reconsume(State::BogusDOCTYPE);
continue;
}
},
// 13.2.5.58 Before DOCTYPE public identifier state
State::BeforeDOCTYPEPublicIdentifier => match current_input_character {
Some(cp) if is_whitespace(cp) => continue,
Some(0x22) => {
{
let dd = self.current_token.doctype_data_mut();
dd.missing_public_identifier = false;
}
self.state = State::DOCTYPEPublicIdentifierDoubleQuoted;
continue;
}
Some(0x27) => {
{
let dd = self.current_token.doctype_data_mut();
dd.missing_public_identifier = false;
}
self.state = State::DOCTYPEPublicIdentifierSingleQuoted;
continue;
}
Some(0x3E) => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.reconsume(State::BogusDOCTYPE);
continue;
}
},
// 13.2.5.59 DOCTYPE public identifier (double-quoted) state
State::DOCTYPEPublicIdentifierDoubleQuoted => match current_input_character {
Some(0x22) => {
{
let val = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.public_identifier = val;
}
}
self.state = State::AfterDOCTYPEPublicIdentifier;
continue;
}
Some(0x00) => {
self.current_builder.push('\u{FFFD}');
continue;
}
Some(0x3E) => {
{
let val = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.public_identifier = val;
dd.force_quirks = true;
}
}
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
{
let val = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.public_identifier = val;
dd.force_quirks = true;
}
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
push_code_point(&mut self.current_builder, cp);
continue;
}
},
// 13.2.5.60 DOCTYPE public identifier (single-quoted) state
State::DOCTYPEPublicIdentifierSingleQuoted => match current_input_character {
Some(0x27) => {
{
let val = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.public_identifier = val;
}
}
self.state = State::AfterDOCTYPEPublicIdentifier;
continue;
}
Some(0x00) => {
self.current_builder.push('\u{FFFD}');
continue;
}
Some(0x3E) => {
{
let val = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.public_identifier = val;
dd.force_quirks = true;
}
}
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
{
let val = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.public_identifier = val;
dd.force_quirks = true;
}
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
push_code_point(&mut self.current_builder, cp);
continue;
}
},
// 13.2.5.61 After DOCTYPE public identifier state
State::AfterDOCTYPEPublicIdentifier => match current_input_character {
Some(cp) if is_whitespace(cp) => {
self.state = State::BetweenDOCTYPEPublicAndSystemIdentifiers;
continue;
}
Some(0x3E) => {
self.state = State::Data;
return self.emit_current_token_direct();
}
Some(0x22) => {
{
let dd = self.current_token.doctype_data_mut();
dd.system_identifier = String::new();
dd.missing_system_identifier = false;
}
self.state = State::DOCTYPESystemIdentifierDoubleQuoted;
continue;
}
Some(0x27) => {
{
let dd = self.current_token.doctype_data_mut();
dd.system_identifier = String::new();
dd.missing_system_identifier = false;
}
self.state = State::DOCTYPESystemIdentifierSingleQuoted;
continue;
}
None => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.reconsume(State::BogusDOCTYPE);
continue;
}
},
// 13.2.5.62 Between DOCTYPE public and system identifiers state
State::BetweenDOCTYPEPublicAndSystemIdentifiers => match current_input_character {
Some(cp) if is_whitespace(cp) => continue,
Some(0x3E) => {
self.state = State::Data;
return self.emit_current_token_direct();
}
Some(0x22) => {
{
let dd = self.current_token.doctype_data_mut();
dd.system_identifier = String::new();
dd.missing_system_identifier = false;
}
self.state = State::DOCTYPESystemIdentifierDoubleQuoted;
continue;
}
Some(0x27) => {
{
let dd = self.current_token.doctype_data_mut();
dd.system_identifier = String::new();
dd.missing_system_identifier = false;
}
self.state = State::DOCTYPESystemIdentifierSingleQuoted;
continue;
}
None => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.reconsume(State::BogusDOCTYPE);
continue;
}
},
// 13.2.5.63 After DOCTYPE system keyword state
State::AfterDOCTYPESystemKeyword => match current_input_character {
Some(cp) if is_whitespace(cp) => {
self.state = State::BeforeDOCTYPESystemIdentifier;
continue;
}
Some(0x22) => {
{
let dd = self.current_token.doctype_data_mut();
dd.system_identifier = String::new();
dd.missing_system_identifier = false;
}
self.state = State::DOCTYPESystemIdentifierDoubleQuoted;
continue;
}
Some(0x27) => {
{
let dd = self.current_token.doctype_data_mut();
dd.system_identifier = String::new();
dd.missing_system_identifier = false;
}
self.state = State::DOCTYPESystemIdentifierSingleQuoted;
continue;
}
Some(0x3E) => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.reconsume(State::BogusDOCTYPE);
continue;
}
},
// 13.2.5.64 Before DOCTYPE system identifier state
State::BeforeDOCTYPESystemIdentifier => match current_input_character {
Some(cp) if is_whitespace(cp) => continue,
Some(0x22) => {
{
let dd = self.current_token.doctype_data_mut();
dd.system_identifier = String::new();
dd.missing_system_identifier = false;
}
self.state = State::DOCTYPESystemIdentifierDoubleQuoted;
continue;
}
Some(0x27) => {
{
let dd = self.current_token.doctype_data_mut();
dd.system_identifier = String::new();
dd.missing_system_identifier = false;
}
self.state = State::DOCTYPESystemIdentifierSingleQuoted;
continue;
}
Some(0x3E) => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.reconsume(State::BogusDOCTYPE);
continue;
}
},
// 13.2.5.65 DOCTYPE system identifier (double-quoted) state
State::DOCTYPESystemIdentifierDoubleQuoted => match current_input_character {
Some(0x22) => {
let sys_id = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.system_identifier = sys_id;
}
self.state = State::AfterDOCTYPESystemIdentifier;
continue;
}
Some(0x00) => {
self.current_builder.push('\u{FFFD}');
continue;
}
Some(0x3E) => {
let sys_id = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.system_identifier = sys_id;
dd.force_quirks = true;
}
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
let sys_id = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.system_identifier = sys_id;
dd.force_quirks = true;
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
push_code_point(&mut self.current_builder, cp);
continue;
}
},
// 13.2.5.66 DOCTYPE system identifier (single-quoted) state
State::DOCTYPESystemIdentifierSingleQuoted => match current_input_character {
Some(0x27) => {
let sys_id = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.system_identifier = sys_id;
}
self.state = State::AfterDOCTYPESystemIdentifier;
continue;
}
Some(0x00) => {
self.current_builder.push('\u{FFFD}');
continue;
}
Some(0x3E) => {
let sys_id = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.system_identifier = sys_id;
dd.force_quirks = true;
}
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
let sys_id = self.consume_current_builder();
{
let dd = self.current_token.doctype_data_mut();
dd.system_identifier = sys_id;
dd.force_quirks = true;
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
push_code_point(&mut self.current_builder, cp);
continue;
}
},
// 13.2.5.67 After DOCTYPE system identifier state
State::AfterDOCTYPESystemIdentifier => match current_input_character {
Some(cp) if is_whitespace(cp) => continue,
Some(0x3E) => {
self.state = State::Data;
return self.emit_current_token_direct();
}
None => {
{
let dd = self.current_token.doctype_data_mut();
dd.force_quirks = true;
}
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(_) => {
// parse error - do NOT set force_quirks
self.reconsume(State::BogusDOCTYPE);
continue;
}
},
// 13.2.5.68 Bogus DOCTYPE state
State::BogusDOCTYPE => match current_input_character {
Some(0x3E) => {
self.state = State::Data;
return self.emit_current_token_direct();
}
Some(0x00) => continue,
None => {
self.emit_current_token_followed_by_eof();
return self.queued_tokens.pop_front();
}
Some(_) => continue,
},
// 13.2.5.69 CDATA section state
State::CDATASection => match current_input_character {
Some(0x5D) => {
self.state = State::CDATASectionBracket;
continue;
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
return self.return_character_token(cp);
}
},
// 13.2.5.70 CDATA section bracket state
State::CDATASectionBracket => match current_input_character {
Some(0x5D) => {
self.state = State::CDATASectionEnd;
continue;
}
_ => {
self.queued_tokens.push_back(self.make_character_token(0x5D));
self.reconsume(State::CDATASection);
continue;
}
},
// 13.2.5.71 CDATA section end state
State::CDATASectionEnd => match current_input_character {
Some(0x5D) => {
return self.return_character_token(0x5D);
}
Some(0x3E) => {
self.state = State::Data;
continue;
}
_ => {
self.queued_tokens.push_back(self.make_character_token(0x5D));
self.queued_tokens.push_back(self.make_character_token(0x5D));
self.reconsume(State::CDATASection);
continue;
}
},
// 13.2.5.72 Character reference state
State::CharacterReference => {
self.temporary_buffer.clear();
self.temporary_buffer.push(0x26); // '&'
self.entity_matcher = NamedCharacterReferenceMatcher::new();
match current_input_character {
Some(cp) if is_ascii_alphanumeric(cp) => {
self.reconsume(State::NamedCharacterReference);
continue;
}
Some(0x23) => {
self.temporary_buffer.push(0x23);
self.state = State::NumericCharacterReference;
continue;
}
_ => {
self.flush_codepoints_consumed_as_character_reference();
self.reconsume_in_return_state(current_input_character);
if !self.queued_tokens.is_empty() {
return self.queued_tokens.pop_front();
}
continue;
}
}
}
// 13.2.5.73 Named character reference state
State::NamedCharacterReference => {
// Insertion-point path: feed one character at a time.
if self.stop_at_insertion_point && self.insertion_point.is_some() {
if let Some(cp) = current_input_character {
if self.entity_matcher.try_consume_code_point(cp) {
self.temporary_buffer.push(cp);
continue;
}
// Character not accepted by matcher. Reconsume it.
self.restore_to(self.prev_offset);
} else if self.can_run_out_at(self.current_offset) {
// At insertion point with no more chars -- pause.
return None;
}
// Fall through to resolution.
} else {
// Normal path: feed all remaining chars in a tight loop.
if current_input_character.is_some() {
self.restore_to(self.prev_offset);
}
let limit = self.input.len();
while self.current_offset < limit {
let cp = self.input[self.current_offset];
if !self.entity_matcher.try_consume_code_point(cp) {
break;
}
self.temporary_buffer.push(cp);
self.skip(1);
}
if !self.input_stream_closed && self.current_offset >= limit {
return None;
}
}
// Resolution: backtrack overconsumed characters.
let overconsumed = self.entity_matcher.overconsumed_code_points() as usize;
if overconsumed > 0 {
self.restore_to(self.current_offset - overconsumed);
self.temporary_buffer
.truncate(self.temporary_buffer.len() - overconsumed);
}
if let Some((first_cp, second_cp)) = self.entity_matcher.code_points() {
let ends_with_semi = self.entity_matcher.last_match_ends_with_semicolon();
// Check special attribute handling.
let next_cp = self.peek_code_point(0);
if self.consumed_as_part_of_an_attribute()
&& !ends_with_semi
&& next_cp.is_some_and(|c| c == 0x3D || is_ascii_alphanumeric(c))
{
self.flush_codepoints_consumed_as_character_reference();
self.state = self.return_state;
if !self.queued_tokens.is_empty() {
return self.queued_tokens.pop_front();
}
continue;
}
if !ends_with_semi {
// parse error
}
self.temporary_buffer.clear();
self.temporary_buffer.push(first_cp);
if second_cp != 0 {
self.temporary_buffer.push(second_cp);
}
self.flush_codepoints_consumed_as_character_reference();
self.state = self.return_state;
if !self.queued_tokens.is_empty() {
return self.queued_tokens.pop_front();
}
continue;
} else {
// No match found.
self.flush_codepoints_consumed_as_character_reference();
self.state = State::AmbiguousAmpersand;
continue;
}
}
// 13.2.5.74 Ambiguous ampersand state
State::AmbiguousAmpersand => match current_input_character {
Some(cp) if is_ascii_alphanumeric(cp) => {
if self.consumed_as_part_of_an_attribute() {
push_code_point(&mut self.current_builder, cp);
continue;
} else {
return self.return_character_token(cp);
}
}
Some(0x3B) => {
// parse error
self.reconsume_in_return_state(current_input_character);
if !self.queued_tokens.is_empty() {
return self.queued_tokens.pop_front();
}
continue;
}
_ => {
self.reconsume_in_return_state(current_input_character);
if !self.queued_tokens.is_empty() {
return self.queued_tokens.pop_front();
}
continue;
}
},
// 13.2.5.75 Numeric character reference state
State::NumericCharacterReference => {
self.character_reference_code = 0;
match current_input_character {
Some(0x78) | Some(0x58) => {
// 'x' or 'X'
self.temporary_buffer.push(current_input_character.unwrap());
self.state = State::HexadecimalCharacterReferenceStart;
continue;
}
_ => {
self.reconsume(State::DecimalCharacterReferenceStart);
continue;
}
}
}
// 13.2.5.76 Hexadecimal character reference start state
State::HexadecimalCharacterReferenceStart => match current_input_character {
Some(cp) if is_ascii_hex_digit(cp) => {
self.reconsume(State::HexadecimalCharacterReference);
continue;
}
_ => {
// parse error
self.flush_codepoints_consumed_as_character_reference();
self.reconsume_in_return_state(current_input_character);
if !self.queued_tokens.is_empty() {
return self.queued_tokens.pop_front();
}
continue;
}
},
// 13.2.5.77 Decimal character reference start state
State::DecimalCharacterReferenceStart => match current_input_character {
Some(cp) if is_ascii_digit(cp) => {
self.reconsume(State::DecimalCharacterReference);
continue;
}
_ => {
// parse error
self.flush_codepoints_consumed_as_character_reference();
self.reconsume_in_return_state(current_input_character);
if !self.queued_tokens.is_empty() {
return self.queued_tokens.pop_front();
}
continue;
}
},
// 13.2.5.78 Hexadecimal character reference state
State::HexadecimalCharacterReference => match current_input_character {
Some(cp) if is_ascii_digit(cp) => {
self.character_reference_code =
self.character_reference_code.wrapping_mul(16).wrapping_add(cp - 0x30);
continue;
}
Some(cp @ 0x41..=0x46) => {
self.character_reference_code =
self.character_reference_code.wrapping_mul(16).wrapping_add(cp - 0x37);
continue;
}
Some(cp @ 0x61..=0x66) => {
self.character_reference_code =
self.character_reference_code.wrapping_mul(16).wrapping_add(cp - 0x57);
continue;
}
Some(0x3B) => {
self.state = State::NumericCharacterReferenceEnd;
continue;
}
_ => {
// parse error
self.reconsume(State::NumericCharacterReferenceEnd);
continue;
}
},
// 13.2.5.79 Decimal character reference state
State::DecimalCharacterReference => match current_input_character {
Some(cp) if is_ascii_digit(cp) => {
self.character_reference_code =
self.character_reference_code.wrapping_mul(10).wrapping_add(cp - 0x30);
continue;
}
Some(0x3B) => {
self.state = State::NumericCharacterReferenceEnd;
continue;
}
_ => {
// parse error
self.reconsume(State::NumericCharacterReferenceEnd);
continue;
}
},
// 13.2.5.80 Numeric character reference end state
State::NumericCharacterReferenceEnd => {
// Don't consume
if current_input_character.is_some() {
self.restore_to(self.prev_offset);
}
let code = self.character_reference_code;
let code = if code == 0 || code > 0x10FFFF || is_surrogate(code) {
0xFFFD
} else if is_noncharacter(code) {
code // parse error but keep value
} else if let Some(replacement) = numeric_char_ref_replacement(code) {
replacement
} else if code == 0x0D {
0x000D // parse error but keep
} else if code != 0 && code <= 0x1F && code != 0x09 && code != 0x0A && code != 0x0C {
code // control char, parse error but keep
} else if (0x7F..=0x9F).contains(&code) {
if let Some(replacement) = numeric_char_ref_replacement(code) {
replacement
} else {
code
}
} else {
code
};
self.temporary_buffer.clear();
self.temporary_buffer.push(code);
self.flush_codepoints_consumed_as_character_reference();
self.state = self.return_state;
if !self.queued_tokens.is_empty() {
return self.queued_tokens.pop_front();
}
continue;
}
// RCDATA less-than sign state
State::RCDATALessThanSign => match current_input_character {
Some(0x2F) => {
self.temporary_buffer.clear();
self.state = State::RCDATAEndTagOpen;
continue;
}
_ => {
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.reconsume(State::RCDATA);
continue;
}
},
// RCDATA end tag open state
State::RCDATAEndTagOpen => match current_input_character {
Some(cp) if is_ascii_alpha(cp) => {
self.create_new_token(TokenType::EndTag);
self.reconsume(State::RCDATAEndTagName);
continue;
}
_ => {
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.queued_tokens.push_back(self.make_character_token(0x2F));
self.reconsume(State::RCDATA);
continue;
}
},
// RCDATA end tag name state
State::RCDATAEndTagName => {
self.handle_rawtext_end_tag_name(current_input_character, State::RCDATA);
if !self.queued_tokens.is_empty() {
return self.queued_tokens.pop_front();
}
continue;
}
// RAWTEXT less-than sign state
State::RAWTEXTLessThanSign => match current_input_character {
Some(0x2F) => {
self.temporary_buffer.clear();
self.state = State::RAWTEXTEndTagOpen;
continue;
}
_ => {
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.reconsume(State::RAWTEXT);
continue;
}
},
// RAWTEXT end tag open state
State::RAWTEXTEndTagOpen => match current_input_character {
Some(cp) if is_ascii_alpha(cp) => {
self.create_new_token(TokenType::EndTag);
self.reconsume(State::RAWTEXTEndTagName);
continue;
}
_ => {
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.queued_tokens.push_back(self.make_character_token(0x2F));
self.reconsume(State::RAWTEXT);
continue;
}
},
// RAWTEXT end tag name state
State::RAWTEXTEndTagName => {
self.handle_rawtext_end_tag_name(current_input_character, State::RAWTEXT);
if !self.queued_tokens.is_empty() {
return self.queued_tokens.pop_front();
}
continue;
}
// Script data less-than sign state
State::ScriptDataLessThanSign => match current_input_character {
Some(0x2F) => {
self.temporary_buffer.clear();
self.state = State::ScriptDataEndTagOpen;
continue;
}
Some(0x21) => {
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.queued_tokens.push_back(self.make_character_token(0x21));
self.state = State::ScriptDataEscapeStart;
return self.queued_tokens.pop_front();
}
_ => {
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.reconsume(State::ScriptData);
continue;
}
},
// Script data end tag open state
State::ScriptDataEndTagOpen => match current_input_character {
Some(cp) if is_ascii_alpha(cp) => {
self.create_new_token(TokenType::EndTag);
self.reconsume(State::ScriptDataEndTagName);
continue;
}
_ => {
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.queued_tokens.push_back(self.make_character_token(0x2F));
self.reconsume(State::ScriptData);
continue;
}
},
// Script data end tag name state
State::ScriptDataEndTagName => {
self.handle_rawtext_end_tag_name(current_input_character, State::ScriptData);
if !self.queued_tokens.is_empty() {
return self.queued_tokens.pop_front();
}
continue;
}
// Script data escape start state
State::ScriptDataEscapeStart => match current_input_character {
Some(0x2D) => {
self.state = State::ScriptDataEscapeStartDash;
return self.return_character_token(0x2D);
}
_ => {
self.reconsume(State::ScriptData);
continue;
}
},
// Script data escape start dash state
State::ScriptDataEscapeStartDash => match current_input_character {
Some(0x2D) => {
self.state = State::ScriptDataEscapedDashDash;
return self.return_character_token(0x2D);
}
_ => {
self.reconsume(State::ScriptData);
continue;
}
},
// Script data escaped state
State::ScriptDataEscaped => match current_input_character {
Some(0x2D) => {
self.state = State::ScriptDataEscapedDash;
return self.return_character_token(0x2D);
}
Some(0x3C) => {
self.state = State::ScriptDataEscapedLessThanSign;
continue;
}
Some(0x00) => {
return self.return_character_token(0xFFFD);
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
return self.return_character_token(cp);
}
},
// Script data escaped dash state
State::ScriptDataEscapedDash => match current_input_character {
Some(0x2D) => {
self.state = State::ScriptDataEscapedDashDash;
return self.return_character_token(0x2D);
}
Some(0x3C) => {
self.state = State::ScriptDataEscapedLessThanSign;
continue;
}
Some(0x00) => {
self.state = State::ScriptDataEscaped;
return self.return_character_token(0xFFFD);
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
self.state = State::ScriptDataEscaped;
return self.return_character_token(cp);
}
},
// Script data escaped dash dash state
State::ScriptDataEscapedDashDash => match current_input_character {
Some(0x2D) => {
return self.return_character_token(0x2D);
}
Some(0x3C) => {
self.state = State::ScriptDataEscapedLessThanSign;
continue;
}
Some(0x3E) => {
self.state = State::ScriptData;
return self.return_character_token(0x3E);
}
Some(0x00) => {
self.state = State::ScriptDataEscaped;
return self.return_character_token(0xFFFD);
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
self.state = State::ScriptDataEscaped;
return self.return_character_token(cp);
}
},
// Script data escaped less-than sign state
State::ScriptDataEscapedLessThanSign => match current_input_character {
Some(0x2F) => {
self.temporary_buffer.clear();
self.state = State::ScriptDataEscapedEndTagOpen;
continue;
}
Some(cp) if is_ascii_alpha(cp) => {
self.temporary_buffer.clear();
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.reconsume(State::ScriptDataDoubleEscapeStart);
continue;
}
_ => {
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.reconsume(State::ScriptDataEscaped);
continue;
}
},
// Script data escaped end tag open state
State::ScriptDataEscapedEndTagOpen => match current_input_character {
Some(cp) if is_ascii_alpha(cp) => {
self.create_new_token(TokenType::EndTag);
self.reconsume(State::ScriptDataEscapedEndTagName);
continue;
}
_ => {
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.queued_tokens.push_back(self.make_character_token(0x2F));
self.reconsume(State::ScriptDataEscaped);
continue;
}
},
// Script data escaped end tag name state
State::ScriptDataEscapedEndTagName => {
self.handle_rawtext_end_tag_name(current_input_character, State::ScriptDataEscaped);
if !self.queued_tokens.is_empty() {
return self.queued_tokens.pop_front();
}
continue;
}
// Script data double escape start state
State::ScriptDataDoubleEscapeStart => match current_input_character {
Some(cp) if is_whitespace(cp) || cp == 0x2F || cp == 0x3E => {
if self.temporary_buffer_equals_script() {
self.state = State::ScriptDataDoubleEscaped;
} else {
self.state = State::ScriptDataEscaped;
}
return self.return_character_token(cp);
}
Some(cp) if is_ascii_upper_alpha(cp) => {
self.temporary_buffer.push(to_ascii_lowercase(cp));
return self.return_character_token(cp);
}
Some(cp) if is_ascii_lower_alpha(cp) => {
self.temporary_buffer.push(cp);
return self.return_character_token(cp);
}
_ => {
self.reconsume(State::ScriptDataEscaped);
continue;
}
},
// Script data double escaped state
State::ScriptDataDoubleEscaped => match current_input_character {
Some(0x2D) => {
self.state = State::ScriptDataDoubleEscapedDash;
return self.return_character_token(0x2D);
}
Some(0x3C) => {
self.state = State::ScriptDataDoubleEscapedLessThanSign;
return self.return_character_token(0x3C);
}
Some(0x00) => {
return self.return_character_token(0xFFFD);
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
return self.return_character_token(cp);
}
},
// Script data double escaped dash state
State::ScriptDataDoubleEscapedDash => match current_input_character {
Some(0x2D) => {
self.state = State::ScriptDataDoubleEscapedDashDash;
return self.return_character_token(0x2D);
}
Some(0x3C) => {
self.state = State::ScriptDataDoubleEscapedLessThanSign;
return self.return_character_token(0x3C);
}
Some(0x00) => {
self.state = State::ScriptDataDoubleEscaped;
return self.return_character_token(0xFFFD);
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
self.state = State::ScriptDataDoubleEscaped;
return self.return_character_token(cp);
}
},
// Script data double escaped dash dash state
State::ScriptDataDoubleEscapedDashDash => match current_input_character {
Some(0x2D) => {
return self.return_character_token(0x2D);
}
Some(0x3C) => {
self.state = State::ScriptDataDoubleEscapedLessThanSign;
return self.return_character_token(0x3C);
}
Some(0x3E) => {
self.state = State::ScriptData;
return self.return_character_token(0x3E);
}
Some(0x00) => {
self.state = State::ScriptDataDoubleEscaped;
return self.return_character_token(0xFFFD);
}
None => {
self.emit_eof();
return self.queued_tokens.pop_front();
}
Some(cp) => {
self.state = State::ScriptDataDoubleEscaped;
return self.return_character_token(cp);
}
},
// Script data double escaped less-than sign state
State::ScriptDataDoubleEscapedLessThanSign => match current_input_character {
Some(0x2F) => {
self.temporary_buffer.clear();
self.state = State::ScriptDataDoubleEscapeEnd;
return self.return_character_token(0x2F);
}
_ => {
self.reconsume(State::ScriptDataDoubleEscaped);
continue;
}
},
// Script data double escape end state
State::ScriptDataDoubleEscapeEnd => match current_input_character {
Some(cp) if is_whitespace(cp) || cp == 0x2F || cp == 0x3E => {
if self.temporary_buffer_equals_script() {
self.state = State::ScriptDataEscaped;
} else {
self.state = State::ScriptDataDoubleEscaped;
}
return self.return_character_token(cp);
}
Some(cp) if is_ascii_upper_alpha(cp) => {
self.temporary_buffer.push(to_ascii_lowercase(cp));
return self.return_character_token(cp);
}
Some(cp) if is_ascii_lower_alpha(cp) => {
self.temporary_buffer.push(cp);
return self.return_character_token(cp);
}
_ => {
self.reconsume(State::ScriptDataDoubleEscaped);
continue;
}
},
}
}
}
// -- Helper methods --
fn reconsume(&mut self, new_state: State) {
self.state = new_state;
if self.current_offset > 0 {
self.restore_to(self.prev_offset);
}
}
fn reconsume_in_return_state(&mut self, current_input_character: Option<u32>) {
self.state = self.return_state;
if current_input_character.is_some() {
self.restore_to(self.prev_offset);
}
}
#[inline(always)]
fn make_character_token(&self, code_point: u32) -> Token {
Token::new_character(code_point)
}
fn set_attribute_name(&mut self) {
let id = crate::interned_names::lookup_attr_name(self.current_builder.as_bytes());
let name_length = self.current_builder.chars().count() as u64;
let name_end = self.nth_last_position(1);
let name_start = Position {
line: name_end.line,
column: name_end.column.saturating_sub(name_length),
};
if id != 0 {
self.current_builder.clear();
if let Some(attr) = self.current_token.attributes_mut().last_mut() {
attr.local_name_id = id;
attr.local_name.clear();
attr.name_start_position = name_start;
attr.name_end_position = name_end;
}
} else {
let name = self.consume_current_builder();
if let Some(attr) = self.current_token.attributes_mut().last_mut() {
attr.local_name_id = 0;
attr.local_name = name;
attr.name_start_position = name_start;
attr.name_end_position = name_end;
}
}
}
fn set_attribute_value(&mut self) {
let value = self.consume_current_builder();
let value_end = self.nth_last_position(1);
if let Some(attr) = self.current_token.attributes_mut().last_mut() {
attr.value = value;
attr.value_end_position = value_end;
}
}
fn temporary_buffer_equals_script(&self) -> bool {
self.temporary_buffer == [0x73, 0x63, 0x72, 0x69, 0x70, 0x74]
}
/// Handle the common end-tag-name pattern shared by RCDATA, RAWTEXT,
/// ScriptData, and ScriptDataEscaped end tag name states.
fn handle_rawtext_end_tag_name(&mut self, current_input_character: Option<u32>, fallback_state: State) {
match current_input_character {
Some(cp) if is_whitespace(cp) || cp == 0x2F || cp == 0x3E => {
self.commit_current_builder_as_tag_name();
if self.current_end_tag_token_is_appropriate() {
match cp {
_ if is_whitespace(cp) => {
self.state = State::BeforeAttributeName;
return;
}
0x2F => {
self.state = State::SelfClosingStartTag;
return;
}
0x3E => {
self.state = State::Data;
self.emit_current_token();
return;
}
_ => {}
}
}
// Not appropriate - emit buffered chars
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.queued_tokens.push_back(self.make_character_token(0x2F));
for i in 0..self.temporary_buffer.len() {
let cp = self.temporary_buffer[i];
self.queued_tokens.push_back(self.make_character_token(cp));
}
self.current_builder.clear();
self.reconsume(fallback_state);
}
Some(cp) if is_ascii_upper_alpha(cp) => {
push_code_point(&mut self.current_builder, to_ascii_lowercase(cp));
self.temporary_buffer.push(cp);
}
Some(cp) if is_ascii_lower_alpha(cp) => {
self.current_builder.push(char::from_u32(cp).unwrap());
self.temporary_buffer.push(cp);
}
_ => {
self.queued_tokens.push_back(self.make_character_token(0x3C));
self.queued_tokens.push_back(self.make_character_token(0x2F));
for i in 0..self.temporary_buffer.len() {
let cp = self.temporary_buffer[i];
self.queued_tokens.push_back(self.make_character_token(cp));
}
self.current_builder.clear();
self.reconsume(fallback_state);
}
}
}
}