` element.
svg_depth: usize,
}
impl TreeBuilder {
/// Create a new tree builder with an empty document.
pub fn new() -> Self {
TreeBuilder {
document: Document::new(),
open_elements: Vec::new(),
head_element: None,
body_element: None,
form_element: None,
insertion_mode: InsertionMode::Initial,
original_insertion_mode: None,
pending_text: String::new(),
svg_depth: 0,
}
}
/// Process a single token, updating the DOM tree.
pub fn process_token(&mut self, token: Token) {
// Handle SVG foreign content.
if self.svg_depth > 0 {
self.handle_svg_content(token);
return;
}
match self.insertion_mode {
InsertionMode::Initial => self.handle_initial(token),
InsertionMode::BeforeHtml => self.handle_before_html(token),
InsertionMode::BeforeHead => self.handle_before_head(token),
InsertionMode::InHead => self.handle_in_head(token),
InsertionMode::Text => self.handle_text(token),
InsertionMode::AfterHead => self.handle_after_head(token),
InsertionMode::InBody => self.handle_in_body(token),
InsertionMode::InSelect => self.handle_in_select(token),
InsertionMode::AfterBody => self.handle_after_body(token),
InsertionMode::AfterAfterBody => self.handle_after_after_body(token),
}
}
/// Finish building and return the constructed DOM document.
pub fn finish(self) -> Document {
self.document
}
// --- Insertion mode handlers ---
fn handle_initial(&mut self, token: Token) {
match token {
Token::Doctype { .. } => {
// For Phase 3, we just acknowledge the DOCTYPE and move on.
self.insertion_mode = InsertionMode::BeforeHtml;
}
Token::Comment(data) => {
let comment = self.document.create_comment(&data);
let root = self.document.root();
self.document.append_child(root, comment);
}
Token::Character(ref s) if s.chars().all(|c| c.is_ascii_whitespace()) => {
// Ignore whitespace in Initial mode.
}
_ => {
// Anything else: switch to BeforeHtml and reprocess.
self.insertion_mode = InsertionMode::BeforeHtml;
self.handle_before_html(token);
}
}
}
fn handle_before_html(&mut self, token: Token) {
match token {
Token::Doctype { .. } => { /* ignore */ }
Token::Comment(data) => {
let comment = self.document.create_comment(&data);
let root = self.document.root();
self.document.append_child(root, comment);
}
Token::Character(ref s) if s.chars().all(|c| c.is_ascii_whitespace()) => {
// Ignore whitespace.
}
Token::StartTag { ref name, .. } if name == "html" => {
let html = self.create_element_from_token(&token);
let root = self.document.root();
self.document.append_child(root, html);
self.open_elements.push(html);
self.insertion_mode = InsertionMode::BeforeHead;
}
Token::EndTag { ref name }
if name != "head" && name != "body" && name != "html" && name != "br" =>
{
// Parse error, ignore.
}
_ => {
// Create an implicit element.
let html = self.document.create_element("html");
let root = self.document.root();
self.document.append_child(root, html);
self.open_elements.push(html);
self.insertion_mode = InsertionMode::BeforeHead;
self.handle_before_head(token);
}
}
}
fn handle_before_head(&mut self, token: Token) {
match token {
Token::Character(ref s) if s.chars().all(|c| c.is_ascii_whitespace()) => {
// Ignore whitespace.
}
Token::Comment(data) => {
self.insert_comment(&data);
}
Token::Doctype { .. } => { /* ignore */ }
Token::StartTag { ref name, .. } if name == "html" => {
// Process as if InBody.
self.handle_in_body(token);
}
Token::StartTag { ref name, .. } if name == "head" => {
let head = self.create_element_from_token(&token);
self.insert_node(head);
self.open_elements.push(head);
self.head_element = Some(head);
self.insertion_mode = InsertionMode::InHead;
}
Token::EndTag { ref name }
if name != "head" && name != "body" && name != "html" && name != "br" =>
{
// Parse error, ignore.
}
_ => {
// Implied .
let head = self.document.create_element("head");
self.insert_node(head);
self.open_elements.push(head);
self.head_element = Some(head);
self.insertion_mode = InsertionMode::InHead;
self.handle_in_head(token);
}
}
}
fn handle_in_head(&mut self, token: Token) {
match token {
Token::Character(ref s) if s.chars().all(|c| c.is_ascii_whitespace()) => {
self.insert_text(s);
}
Token::Comment(data) => {
self.insert_comment(&data);
}
Token::Doctype { .. } => { /* ignore */ }
Token::StartTag { ref name, .. } if name == "title" => {
let elem = self.create_element_from_token(&token);
self.insert_node(elem);
self.open_elements.push(elem);
self.original_insertion_mode = Some(self.insertion_mode);
self.insertion_mode = InsertionMode::Text;
}
Token::StartTag { ref name, .. }
if name == "style" || name == "script" || name == "noscript" =>
{
let elem = self.create_element_from_token(&token);
self.insert_node(elem);
self.open_elements.push(elem);
self.original_insertion_mode = Some(self.insertion_mode);
self.insertion_mode = InsertionMode::Text;
}
Token::StartTag { ref name, .. } if name == "meta" || name == "link" => {
let elem = self.create_element_from_token(&token);
self.insert_node(elem);
// Void elements: don't push onto stack.
}
Token::StartTag { ref name, .. } if name == "head" => {
// Ignore duplicate .
}
Token::EndTag { ref name } if name == "head" => {
self.pop_until("head");
self.insertion_mode = InsertionMode::AfterHead;
}
Token::EndTag { ref name } if name != "body" && name != "html" && name != "br" => {
// Parse error, ignore.
}
_ => {
// Pop and switch to AfterHead, then reprocess.
self.pop_until("head");
self.insertion_mode = InsertionMode::AfterHead;
self.handle_after_head(token);
}
}
}
fn handle_text(&mut self, token: Token) {
match token {
Token::Character(s) => {
self.pending_text.push_str(&s);
}
Token::EndTag { .. } => {
// Flush pending text.
if !self.pending_text.is_empty() {
let text = self.pending_text.clone();
self.pending_text.clear();
self.insert_text(&text);
}
// Pop the element (e.g., ).
self.open_elements.pop();
self.insertion_mode = self
.original_insertion_mode
.unwrap_or(InsertionMode::InBody);
self.original_insertion_mode = None;
}
Token::Eof => {
// Flush pending text.
if !self.pending_text.is_empty() {
let text = self.pending_text.clone();
self.pending_text.clear();
self.insert_text(&text);
}
self.open_elements.pop();
self.insertion_mode = self
.original_insertion_mode
.unwrap_or(InsertionMode::InBody);
self.original_insertion_mode = None;
self.process_token(Token::Eof);
}
_ => {}
}
}
fn handle_after_head(&mut self, token: Token) {
match token {
Token::Character(ref s) if s.chars().all(|c| c.is_ascii_whitespace()) => {
self.insert_text(s);
}
Token::Comment(data) => {
self.insert_comment(&data);
}
Token::Doctype { .. } => { /* ignore */ }
Token::StartTag { ref name, .. } if name == "html" => {
self.handle_in_body(token);
}
Token::StartTag { ref name, .. } if name == "body" => {
let body = self.create_element_from_token(&token);
self.insert_node(body);
self.open_elements.push(body);
self.body_element = Some(body);
self.insertion_mode = InsertionMode::InBody;
}
Token::StartTag { ref name, .. } if name == "head" => {
// Ignore.
}
Token::EndTag { ref name } if name != "body" && name != "html" && name != "br" => {
// Ignore.
}
_ => {
// Implied .
let body = self.document.create_element("body");
self.insert_node(body);
self.open_elements.push(body);
self.body_element = Some(body);
self.insertion_mode = InsertionMode::InBody;
self.handle_in_body(token);
}
}
}
fn handle_in_body(&mut self, token: Token) {
match token {
Token::Character(s) => {
self.insert_text(&s);
}
Token::Comment(data) => {
self.insert_comment(&data);
}
Token::Doctype { .. } => { /* ignore */ }
Token::StartTag { ref name, .. } if name == "html" => {
// Merge attributes onto existing element.
if let Token::StartTag { attributes, .. } = &token {
if let Some(&html_id) = self.open_elements.first() {
for (attr_name, attr_value) in attributes {
if self.document.get_attribute(html_id, attr_name).is_none() {
self.document.set_attribute(html_id, attr_name, attr_value);
}
}
}
}
}
Token::StartTag { ref name, .. }
if name == "body"
|| name == "head"
|| name == "title"
|| name == "style"
|| name == "script" =>
{
match name.as_str() {
"body" => {
// Ignore duplicate .
}
"head" => {
// Ignore in body.
}
_ => {
// title/style/script: process using InHead rules
self.handle_in_head(token);
}
}
}
Token::StartTag { ref name, .. }
if name == "p"
|| name == "div"
|| name == "h1"
|| name == "h2"
|| name == "h3"
|| name == "h4"
|| name == "h5"
|| name == "h6"
|| name == "pre"
|| name == "blockquote"
|| name == "ul"
|| name == "ol"
|| name == "li" =>
{
// If there's a in button scope, close it first.
if self.has_element_in_button_scope("p") {
self.close_p_element();
}
let elem = self.create_element_from_token(&token);
self.insert_node(elem);
self.open_elements.push(elem);
}
// --- Form elements (Phase 16) ---
Token::StartTag { ref name, .. } if name == "form" => {
// Per spec: if the form element pointer is not null, ignore.
if self.form_element.is_some() {
// Parse error, ignore the token.
} else {
if self.has_element_in_button_scope("p") {
self.close_p_element();
}
let elem = self.create_element_from_token(&token);
self.insert_node(elem);
self.open_elements.push(elem);
self.form_element = Some(elem);
}
}
Token::StartTag { ref name, .. } if name == "fieldset" => {
if self.has_element_in_button_scope("p") {
self.close_p_element();
}
let elem = self.create_element_from_token(&token);
self.insert_node(elem);
self.open_elements.push(elem);
}
Token::StartTag { ref name, .. } if name == "button" => {
// If there's a button in scope, close it first.
if self.has_element_in_scope("button") {
self.generate_implied_end_tags(None);
self.pop_until("button");
}
let elem = self.create_element_from_token(&token);
self.insert_node(elem);
self.open_elements.push(elem);
}
Token::StartTag { ref name, .. } if name == "textarea" => {
let elem = self.create_element_from_token(&token);
self.insert_node(elem);
self.open_elements.push(elem);
// Switch to Text mode to collect raw text content.
self.original_insertion_mode = Some(self.insertion_mode);
self.insertion_mode = InsertionMode::Text;
}
Token::StartTag { ref name, .. } if name == "select" => {
let elem = self.create_element_from_token(&token);
self.insert_node(elem);
self.open_elements.push(elem);
self.insertion_mode = InsertionMode::InSelect;
}
Token::StartTag { ref name, .. } if name == "optgroup" || name == "option" => {
// Close any currently open .
if let Some(&top) = self.open_elements.last() {
if self.document.tag_name(top) == Some("option") {
self.open_elements.pop();
}
}
// Also close open when a new starts.
if name == "optgroup" {
if let Some(&top) = self.open_elements.last() {
if self.document.tag_name(top) == Some("optgroup") {
self.open_elements.pop();
}
}
}
let elem = self.create_element_from_token(&token);
self.insert_node(elem);
self.open_elements.push(elem);
}
// --- SVG / iframe / void / generic start tags ---
Token::StartTag { ref name, .. } if name == "svg" => {
let elem = self.create_svg_element_from_token(&token);
self.insert_node(elem);
self.open_elements.push(elem);
self.svg_depth = 1;
}
Token::StartTag { ref name, .. } if name == "iframe" => {
// Per HTML spec,