Replace the C++ HTML tokenizer with a Rust implementation behind the existing HTMLTokenizer API. Keep the parser-facing integration points for streaming input, insertion points, document.write(), EOF insertion, parser aborts, speculative parser input, and last start tag tracking. The generated FFI handle stays an implementation detail of HTMLTokenizer, so callers keep a single tokenizer class. Preserve duplicate attributes through FFI so C++ token normalization can record the duplicate-attribute signal used by CSP nonce checks. Keep bulk tag-name and attribute scans capped at the active insertion point so streamed parser input is spliced at the right offset. Use generated DAFSA tables for named character references and intern common tag and attribute names to reduce FFI marshalling overhead. This also fixes attribute name source positions, nested old insertion points, and aborted fast-path handling. TestHTMLTokenizer covers duplicate attributes and insertion points in fast tag-name, attribute-name, and quoted-value scans. A CSP text test covers duplicate nonce attributes on parser-created script elements. The tokenizer dump fixtures still match, TestHTMLTokenizer passes, and the full release test-web run passes with 6981 tests and 226 skipped.
202 lines
5.6 KiB
Rust
202 lines
5.6 KiB
Rust
/*
|
|
* Copyright (c) 2026-present, the Ladybird developers.
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
/// Source position in the input.
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
pub struct Position {
|
|
pub line: u64,
|
|
pub column: u64,
|
|
}
|
|
|
|
/// A single attribute on a start or end tag token.
|
|
///
|
|
/// If `local_name_id` is non-zero it is an index into
|
|
/// `interned_names::INTERNED_ATTR_NAMES` and `local_name` is unused.
|
|
/// Otherwise `local_name` holds the owned bytes.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct Attribute {
|
|
pub local_name: String,
|
|
pub local_name_id: u16,
|
|
pub value: String,
|
|
pub name_start_position: Position,
|
|
pub name_end_position: Position,
|
|
pub value_start_position: Position,
|
|
pub value_end_position: Position,
|
|
}
|
|
|
|
/// Data specific to DOCTYPE tokens.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct DoctypeData {
|
|
pub name: String,
|
|
pub public_identifier: String,
|
|
pub system_identifier: String,
|
|
pub missing_name: bool,
|
|
pub missing_public_identifier: bool,
|
|
pub missing_system_identifier: bool,
|
|
pub force_quirks: bool,
|
|
}
|
|
|
|
/// The type of an HTML token.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
#[repr(u8)]
|
|
pub enum TokenType {
|
|
#[default]
|
|
Invalid = 0,
|
|
Doctype = 1,
|
|
StartTag = 2,
|
|
EndTag = 3,
|
|
Comment = 4,
|
|
Character = 5,
|
|
EndOfFile = 6,
|
|
}
|
|
|
|
/// Type-specific data for an HTML token.
|
|
///
|
|
/// If `tag_name_id` is non-zero it is an index into
|
|
/// `interned_names::INTERNED_TAG_NAMES` and `tag_name` is unused.
|
|
/// Otherwise `tag_name` holds the owned bytes.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub enum TokenPayload {
|
|
#[default]
|
|
None,
|
|
Tag {
|
|
tag_name: String,
|
|
tag_name_id: u16,
|
|
self_closing: bool,
|
|
attributes: Vec<Attribute>,
|
|
},
|
|
Comment(String),
|
|
Doctype(Box<DoctypeData>),
|
|
}
|
|
|
|
/// An HTML token produced by the tokenizer.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct Token {
|
|
pub token_type: TokenType,
|
|
pub code_point: u32,
|
|
pub payload: TokenPayload,
|
|
pub start_position: Position,
|
|
pub end_position: Position,
|
|
}
|
|
|
|
impl Token {
|
|
pub fn new_character(code_point: u32) -> Self {
|
|
Token {
|
|
token_type: TokenType::Character,
|
|
code_point,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
pub fn new_eof() -> Self {
|
|
Token {
|
|
token_type: TokenType::EndOfFile,
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Return the tag name as a &str. For interned names this resolves
|
|
/// through the interned name table; for un-interned names it returns
|
|
/// the owned String's contents.
|
|
#[inline(always)]
|
|
pub fn tag_name(&self) -> &str {
|
|
match &self.payload {
|
|
TokenPayload::Tag {
|
|
tag_name, tag_name_id, ..
|
|
} => {
|
|
if *tag_name_id != 0 {
|
|
// SAFETY: interned names are compile-time ASCII byte
|
|
// literals in interned_names_generated.rs, so they are
|
|
// always valid UTF-8.
|
|
match crate::interned_names::tag_name_by_id(*tag_name_id) {
|
|
Some(bytes) => unsafe { std::str::from_utf8_unchecked(bytes) },
|
|
None => "",
|
|
}
|
|
} else {
|
|
tag_name
|
|
}
|
|
}
|
|
_ => "",
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn tag_name_mut(&mut self) -> &mut String {
|
|
match &mut self.payload {
|
|
TokenPayload::Tag {
|
|
tag_name, tag_name_id, ..
|
|
} => {
|
|
*tag_name_id = 0;
|
|
tag_name
|
|
}
|
|
_ => panic!("tag_name_mut called on non-tag token"),
|
|
}
|
|
}
|
|
|
|
/// Set the tag name to an interned id, clearing any previously stored
|
|
/// owned name.
|
|
#[inline(always)]
|
|
pub fn set_tag_name_id(&mut self, id: u16) {
|
|
match &mut self.payload {
|
|
TokenPayload::Tag {
|
|
tag_name, tag_name_id, ..
|
|
} => {
|
|
tag_name.clear();
|
|
*tag_name_id = id;
|
|
}
|
|
_ => panic!("set_tag_name_id called on non-tag token"),
|
|
}
|
|
}
|
|
|
|
/// Returns the interned tag-name id, or 0 if the name is un-interned.
|
|
#[inline(always)]
|
|
pub fn tag_name_id(&self) -> u16 {
|
|
match &self.payload {
|
|
TokenPayload::Tag { tag_name_id, .. } => *tag_name_id,
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn set_self_closing(&mut self, value: bool) {
|
|
match &mut self.payload {
|
|
TokenPayload::Tag { self_closing, .. } => *self_closing = value,
|
|
_ => panic!("set_self_closing called on non-tag token"),
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn is_self_closing(&self) -> bool {
|
|
match &self.payload {
|
|
TokenPayload::Tag { self_closing, .. } => *self_closing,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn attributes_mut(&mut self) -> &mut Vec<Attribute> {
|
|
match &mut self.payload {
|
|
TokenPayload::Tag { attributes, .. } => attributes,
|
|
_ => panic!("attributes_mut called on non-tag token"),
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn set_comment_data(&mut self, data: String) {
|
|
match &mut self.payload {
|
|
TokenPayload::Comment(s) => *s = data,
|
|
_ => panic!("set_comment_data called on non-comment token"),
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
pub fn doctype_data_mut(&mut self) -> &mut DoctypeData {
|
|
match &mut self.payload {
|
|
TokenPayload::Doctype(dd) => dd,
|
|
_ => panic!("doctype_data_mut called on non-doctype token"),
|
|
}
|
|
}
|
|
}
|