// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// ============================================================================
// WHATWG 13.2.6.4 Tree construction dispatcher - Insertion modes
// ============================================================================

///|
/// 13.2.6.4.1 Initial insertion mode
fn TreeBuilder::process_initial_mode(self : TreeBuilder, token : Token) -> Bool {
  match token {
    Character(c) if is_whitespace_char(c) =>
      // Ignore whitespace
      false
    Comment(text) => {
      self.insert_comment_at_document(text)
      false
    }
    DOCTYPE(name~, public_id~, system_id~, force_quirks~) => {
      // Check for parse errors
      if name != Some("html") ||
        public_id is Some(_) ||
        (system_id is Some(_) && system_id != Some("about:legacy-compat")) {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      }
      self.insert_doctype(name, public_id, system_id)
      // Set document mode (quirks detection per WHATWG 13.2.6.4.1)
      self.document.quirks_mode = self.determine_quirks_mode(
        name, public_id, system_id, force_quirks,
      )
      self.mode = BeforeHtml
      false
    }
    _ => {
      // Parse error: no DOCTYPE - set quirks mode per WHATWG 13.2.6.4.1
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      self.document.quirks_mode = Quirks
      self.mode = BeforeHtml
      true // Reprocess
    }
  }
}

///|
/// 13.2.6.4.2 Before html insertion mode
fn TreeBuilder::process_before_html_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    DOCTYPE(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    Comment(text) => {
      self.insert_comment_at_document(text)
      false
    }
    Character(c) if is_whitespace_char(c) =>
      // Ignore whitespace
      false
    StartTag(name~, attrs~, ..) if name == "html" => {
      let _ = self.insert_html_element(name, attrs)
      self.mode = BeforeHead
      false
    }
    EndTag(name~) if name == "head" ||
      name == "body" ||
      name == "html" ||
      name == "br" => {
      // Act as if a start tag "html" was seen
      let _ = self.insert_html_element("html", [])
      self.mode = BeforeHead
      true // Reprocess
    }
    EndTag(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    _ => {
      // Create html element and reprocess
      let _ = self.insert_html_element("html", [])
      self.mode = BeforeHead
      true // Reprocess
    }
  }
}

///|
/// 13.2.6.4.3 Before head insertion mode
fn TreeBuilder::process_before_head_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    Character(c) if is_whitespace_char(c) =>
      // Ignore whitespace
      false
    Comment(text) => {
      self.insert_comment(text)
      false
    }
    ProcessingInstruction(target~, data~) => {
      self.insert_processing_instruction(target, data)
      false
    }
    DOCTYPE(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, attrs~, ..) if name == "html" =>
      // Process using InBody rules
      self.process_in_body_mode(token)
    StartTag(name~, attrs~, ..) if name == "head" => {
      let _ = self.insert_html_element(name, attrs)
      self.mode = InHead
      false
    }
    EndTag(name~) if name == "head" ||
      name == "body" ||
      name == "html" ||
      name == "br" => {
      // Insert head element and reprocess
      let _ = self.insert_html_element("head", [])
      self.mode = InHead
      true // Reprocess
    }
    EndTag(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    _ => {
      // Insert head element and reprocess
      let _ = self.insert_html_element("head", [])
      self.mode = InHead
      true // Reprocess
    }
  }
}

///|
/// 13.2.6.4.4 In head insertion mode
fn TreeBuilder::process_in_head_mode(self : TreeBuilder, token : Token) -> Bool {
  match token {
    Character(c) if is_whitespace_char(c) => {
      self.insert_character(c)
      false
    }
    Comment(text) => {
      self.insert_comment(text)
      false
    }
    ProcessingInstruction(target~, data~) => {
      self.insert_processing_instruction(target, data)
      false
    }
    DOCTYPE(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, attrs~, ..) if name == "html" =>
      self.process_in_body_mode(token)
    StartTag(name~, attrs~, ..) if name == "base" ||
      name == "basefont" ||
      name == "bgsound" ||
      name == "link" => {
      let _ = self.insert_html_element(name, attrs)
      let _ = self.pop_element()
      false
    }
    StartTag(name~, attrs~, ..) if name == "meta" => {
      let _ = self.insert_html_element(name, attrs)
      let _ = self.pop_element()
      // TODO: Check charset attribute
      false
    }
    StartTag(name~, attrs~, ..) if name == "title" => {
      self.parse_rcdata(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "noscript" && self.scripting => {
      self.parse_rawtext(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "noframes" || name == "style" => {
      self.parse_rawtext(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "noscript" && !self.scripting => {
      let _ = self.insert_html_element(name, attrs)
      self.mode = InHeadNoscript
      false
    }
    StartTag(name~, attrs~, ..) if name == "script" => {
      self.parse_script(name, attrs)
      false
    }
    EndTag(name~) if name == "head" => {
      let _ = self.pop_element()
      self.mode = AfterHead
      false
    }
    EndTag(name~) if name == "body" || name == "html" || name == "br" => {
      let _ = self.pop_element()
      self.mode = AfterHead
      true // Reprocess
    }
    StartTag(name~, attrs~, ..) if name == "template" => {
      let _ = self.insert_html_element(name, attrs)
      self.push_formatting_marker()
      self.frameset_ok = false
      self.mode = InTemplate
      self.template_modes.push(InTemplate)
      false
    }
    EndTag(name~) if name == "template" => {
      if !self.has_element_in_stack("template") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.generate_implied_end_tags_thoroughly()
      if self.current_node().tag_name != "template" {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      }
      self.pop_until("template")
      self.clear_active_formatting_to_marker()
      let _ = self.template_modes.pop()
      self.reset_insertion_mode()
      false
    }
    StartTag(name~, ..) if name == "head" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    EndTag(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    _ => {
      let _ = self.pop_element()
      self.mode = AfterHead
      true // Reprocess
    }
  }
}

///|
/// 13.2.6.4.5 In head noscript insertion mode
fn TreeBuilder::process_in_head_noscript_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    DOCTYPE(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, ..) if name == "html" => self.process_in_body_mode(token)
    EndTag(name~) if name == "noscript" => {
      let _ = self.pop_element()
      self.mode = InHead
      false
    }
    Character(c) if is_whitespace_char(c) => self.process_in_head_mode(token)
    Comment(_) => self.process_in_head_mode(token)
    StartTag(name~, ..) if name == "basefont" ||
      name == "bgsound" ||
      name == "link" ||
      name == "meta" ||
      name == "noframes" ||
      name == "style" => self.process_in_head_mode(token)
    EndTag(name~) if name == "br" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      let _ = self.pop_element()
      self.mode = InHead
      true // Reprocess
    }
    StartTag(name~, ..) if name == "head" || name == "noscript" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    EndTag(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    _ => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      let _ = self.pop_element()
      self.mode = InHead
      true // Reprocess
    }
  }
}

///|
/// 13.2.6.4.6 After head insertion mode
fn TreeBuilder::process_after_head_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    Character(c) if is_whitespace_char(c) => {
      self.insert_character(c)
      false
    }
    Comment(text) => {
      self.insert_comment(text)
      false
    }
    DOCTYPE(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, ..) if name == "html" => self.process_in_body_mode(token)
    StartTag(name~, attrs~, ..) if name == "body" => {
      let _ = self.insert_html_element(name, attrs)
      self.frameset_ok = false
      self.mode = InBody
      false
    }
    StartTag(name~, attrs~, ..) if name == "frameset" => {
      let _ = self.insert_html_element(name, attrs)
      self.mode = InFrameset
      false
    }
    StartTag(name~, ..) if name == "base" ||
      name == "basefont" ||
      name == "bgsound" ||
      name == "link" ||
      name == "meta" ||
      name == "noframes" ||
      name == "script" ||
      name == "style" ||
      name == "template" ||
      name == "title" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      self.open_elements.push({
        node_id: self.head_element,
        tag_name: "head",
        ns: HTML,
      })
      let result = self.process_in_head_mode(token)
      // Remove head from stack
      self.remove_from_stack(self.head_element)
      result
    }
    EndTag(name~) if name == "template" => self.process_in_head_mode(token)
    EndTag(name~) if name == "body" || name == "html" || name == "br" => {
      let _ = self.insert_html_element("body", [])
      self.mode = InBody
      true // Reprocess
    }
    StartTag(name~, ..) if name == "head" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    EndTag(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    _ => {
      let _ = self.insert_html_element("body", [])
      self.mode = InBody
      true // Reprocess
    }
  }
}

///|
/// 13.2.6.4.7 In body insertion mode
fn TreeBuilder::process_in_body_mode(self : TreeBuilder, token : Token) -> Bool {
  match token {
    Character('\u{0000}') => {
      self.emit_error(UnexpectedNullCharacter)
      false
    }
    Character(c) if is_whitespace_char(c) => {
      self.reconstruct_active_formatting()
      self.insert_character(c)
      false
    }
    Character(c) => {
      self.reconstruct_active_formatting()
      self.insert_character(c)
      self.frameset_ok = false
      false
    }
    Comment(text) => {
      self.insert_comment(text)
      false
    }
    ProcessingInstruction(target~, data~) => {
      self.insert_processing_instruction(target, data)
      false
    }
    DOCTYPE(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, attrs~, ..) if name == "html" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      if self.has_element_in_stack("template") {
        return false
      }
      // Add attributes to html element
      if self.open_elements.length() > 0 {
        let html_id = self.open_elements[0].node_id
        for attr in attrs {
          if !self.document.has_attribute(html_id, attr.name) {
            self.document.set_attribute(html_id, attr.name, attr.value)
          }
        }
      }
      false
    }
    StartTag(name~, ..) if name == "base" ||
      name == "basefont" ||
      name == "bgsound" ||
      name == "link" ||
      name == "meta" ||
      name == "noframes" ||
      name == "script" ||
      name == "style" ||
      name == "template" ||
      name == "title" => self.process_in_head_mode(token)
    EndTag(name~) if name == "template" => self.process_in_head_mode(token)
    StartTag(name~, attrs~, ..) if name == "body" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      if self.open_elements.length() < 2 {
        return false
      }
      if self.open_elements[1].tag_name != "body" {
        return false
      }
      if self.has_element_in_stack("template") {
        return false
      }
      self.frameset_ok = false
      // Add attributes to body element
      let body_id = self.open_elements[1].node_id
      for attr in attrs {
        if !self.document.has_attribute(body_id, attr.name) {
          self.document.set_attribute(body_id, attr.name, attr.value)
        }
      }
      false
    }
    StartTag(name~, attrs~, ..) if name == "frameset" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      if self.open_elements.length() < 2 ||
        self.open_elements[1].tag_name != "body" {
        return false
      }
      if !self.frameset_ok {
        return false
      }
      // Remove body from parent
      let body_id = self.open_elements[1].node_id
      let parent = self.document.get_parent(body_id)
      if parent != NO_NODE {
        self.document.remove_child(parent, body_id)
      }
      // Pop all until html
      while self.open_elements.length() > 1 {
        let _ = self.pop_element()
      }
      let _ = self.insert_html_element(name, attrs)
      self.mode = InFrameset
      false
    }
    EOF => {
      if self.template_modes.length() > 0 {
        return self.process_in_template_mode(token)
      }
      // Check for open elements that shouldn't be there
      for entry in self.open_elements {
        match entry.tag_name {
          "dd"
          | "dt"
          | "li"
          | "optgroup"
          | "option"
          | "p"
          | "rb"
          | "rp"
          | "rt"
          | "rtc"
          | "tbody"
          | "td"
          | "tfoot"
          | "th"
          | "thead"
          | "tr"
          | "body"
          | "html" => ()
          _ => {
            self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
            break
          }
        }
      }
      false
    }
    EndTag(name~) if name == "body" => {
      if !self.in_default_scope("body") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      // Check for unexpected open elements
      for entry in self.open_elements {
        match entry.tag_name {
          "dd"
          | "dt"
          | "li"
          | "optgroup"
          | "option"
          | "p"
          | "rb"
          | "rp"
          | "rt"
          | "rtc"
          | "tbody"
          | "td"
          | "tfoot"
          | "th"
          | "thead"
          | "tr"
          | "body"
          | "html" => ()
          _ => {
            self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
            break
          }
        }
      }
      self.mode = AfterBody
      false
    }
    EndTag(name~) if name == "html" => {
      if !self.in_default_scope("body") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.mode = AfterBody
      true // Reprocess
    }
    StartTag(name~, attrs~, ..) if name == "address" ||
      name == "article" ||
      name == "aside" ||
      name == "blockquote" ||
      name == "center" ||
      name == "details" ||
      name == "dialog" ||
      name == "dir" ||
      name == "div" ||
      name == "dl" ||
      name == "fieldset" ||
      name == "figcaption" ||
      name == "figure" ||
      name == "footer" ||
      name == "header" ||
      name == "hgroup" ||
      name == "main" ||
      name == "menu" ||
      name == "nav" ||
      name == "ol" ||
      name == "p" ||
      name == "search" ||
      name == "section" ||
      name == "summary" ||
      name == "ul" => {
      if self.in_button_scope("p") {
        self.close_p_element()
      }
      let _ = self.insert_html_element(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "h1" ||
      name == "h2" ||
      name == "h3" ||
      name == "h4" ||
      name == "h5" ||
      name == "h6" => {
      if self.in_button_scope("p") {
        self.close_p_element()
      }
      let current = self.current_node()
      if current.tag_name == "h1" ||
        current.tag_name == "h2" ||
        current.tag_name == "h3" ||
        current.tag_name == "h4" ||
        current.tag_name == "h5" ||
        current.tag_name == "h6" {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        let _ = self.pop_element()
      }
      let _ = self.insert_html_element(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "pre" || name == "listing" => {
      if self.in_button_scope("p") {
        self.close_p_element()
      }
      let _ = self.insert_html_element(name, attrs)
      self.skip_next_newline = true
      self.frameset_ok = false
      false
    }
    StartTag(name~, attrs~, ..) if name == "form" => {
      if self.form_element != NO_NODE && !self.has_element_in_stack("template") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      if self.in_button_scope("p") {
        self.close_p_element()
      }
      let id = self.insert_html_element(name, attrs)
      if !self.has_element_in_stack("template") {
        self.form_element = id
      }
      false
    }
    StartTag(name~, attrs~, ..) if name == "li" => {
      self.frameset_ok = false
      // Walk the stack
      for i = self.open_elements.length() - 1; i >= 0; i = i - 1 {
        let entry = self.open_elements[i]
        if entry.tag_name == "li" {
          self.generate_implied_end_tags("li")
          if self.current_node().tag_name != "li" {
            self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
          }
          self.pop_until("li")
          break
        }
        if is_special_element(entry.tag_name) &&
          entry.tag_name != "address" &&
          entry.tag_name != "div" &&
          entry.tag_name != "p" {
          break
        }
      }
      if self.in_button_scope("p") {
        self.close_p_element()
      }
      let _ = self.insert_html_element(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "dd" || name == "dt" => {
      self.frameset_ok = false
      for i = self.open_elements.length() - 1; i >= 0; i = i - 1 {
        let entry = self.open_elements[i]
        if entry.tag_name == "dd" {
          self.generate_implied_end_tags("dd")
          if self.current_node().tag_name != "dd" {
            self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
          }
          self.pop_until("dd")
          break
        }
        if entry.tag_name == "dt" {
          self.generate_implied_end_tags("dt")
          if self.current_node().tag_name != "dt" {
            self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
          }
          self.pop_until("dt")
          break
        }
        if is_special_element(entry.tag_name) &&
          entry.tag_name != "address" &&
          entry.tag_name != "div" &&
          entry.tag_name != "p" {
          break
        }
      }
      if self.in_button_scope("p") {
        self.close_p_element()
      }
      let _ = self.insert_html_element(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "plaintext" => {
      if self.in_button_scope("p") {
        self.close_p_element()
      }
      let _ = self.insert_html_element(name, attrs)
      self.tokenizer.state = PLAINTEXT
      false
    }
    StartTag(name~, attrs~, ..) if name == "button" => {
      if self.in_default_scope("button") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        self.generate_implied_end_tags("")
        self.pop_until("button")
      }
      self.reconstruct_active_formatting()
      let _ = self.insert_html_element(name, attrs)
      self.frameset_ok = false
      false
    }
    EndTag(name~) if name == "address" ||
      name == "article" ||
      name == "aside" ||
      name == "blockquote" ||
      name == "button" ||
      name == "center" ||
      name == "details" ||
      name == "dialog" ||
      name == "dir" ||
      name == "div" ||
      name == "dl" ||
      name == "fieldset" ||
      name == "figcaption" ||
      name == "figure" ||
      name == "footer" ||
      name == "header" ||
      name == "hgroup" ||
      name == "listing" ||
      name == "main" ||
      name == "menu" ||
      name == "nav" ||
      name == "ol" ||
      name == "pre" ||
      name == "search" ||
      name == "section" ||
      name == "summary" ||
      name == "ul" => {
      if !self.in_default_scope(name) {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.generate_implied_end_tags("")
      if self.current_node().tag_name != name {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      }
      self.pop_until(name)
      false
    }
    EndTag(name~) if name == "form" => {
      if !self.has_element_in_stack("template") {
        let form_id = self.form_element
        self.form_element = NO_NODE
        if form_id == NO_NODE || !self.in_default_scope("form") {
          self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
          return false
        }
        self.generate_implied_end_tags("")
        if self.current_node().node_id != form_id {
          self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        }
        self.remove_from_stack(form_id)
      } else {
        if !self.in_default_scope("form") {
          self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
          return false
        }
        self.generate_implied_end_tags("")
        if self.current_node().tag_name != "form" {
          self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        }
        self.pop_until("form")
      }
      false
    }
    EndTag(name~) if name == "p" => {
      if !self.in_button_scope("p") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        let _ = self.insert_html_element("p", [])
      }
      self.close_p_element()
      false
    }
    EndTag(name~) if name == "li" => {
      if !self.in_list_item_scope("li") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.generate_implied_end_tags("li")
      if self.current_node().tag_name != "li" {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      }
      self.pop_until("li")
      false
    }
    EndTag(name~) if name == "dd" || name == "dt" => {
      if !self.in_default_scope(name) {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.generate_implied_end_tags(name)
      if self.current_node().tag_name != name {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      }
      self.pop_until(name)
      false
    }
    EndTag(name~) if name == "h1" ||
      name == "h2" ||
      name == "h3" ||
      name == "h4" ||
      name == "h5" ||
      name == "h6" => {
      if !self.in_default_scope("h1") &&
        !self.in_default_scope("h2") &&
        !self.in_default_scope("h3") &&
        !self.in_default_scope("h4") &&
        !self.in_default_scope("h5") &&
        !self.in_default_scope("h6") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.generate_implied_end_tags("")
      if self.current_node().tag_name != name {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      }
      // Pop until any heading
      while self.open_elements.length() > 0 {
        let entry = self.pop_element()
        if entry.tag_name == "h1" ||
          entry.tag_name == "h2" ||
          entry.tag_name == "h3" ||
          entry.tag_name == "h4" ||
          entry.tag_name == "h5" ||
          entry.tag_name == "h6" {
          break
        }
      }
      false
    }
    StartTag(name~, attrs~, ..) if name == "a" => {
      // Check for existing a in formatting
      match self.find_formatting_element("a") {
        Some(Element(node_id~, ..)) => {
          self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
          self.run_adoption_agency("a")
          // Remove from formatting list and stack
          self.remove_from_active_formatting(node_id)
          self.remove_from_stack(node_id)
        }
        _ => ()
      }
      self.reconstruct_active_formatting()
      let id = self.insert_html_element(name, attrs)
      self.push_active_formatting(id, name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "b" ||
      name == "big" ||
      name == "code" ||
      name == "em" ||
      name == "font" ||
      name == "i" ||
      name == "s" ||
      name == "small" ||
      name == "strike" ||
      name == "strong" ||
      name == "tt" ||
      name == "u" => {
      self.reconstruct_active_formatting()
      let id = self.insert_html_element(name, attrs)
      self.push_active_formatting(id, name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "nobr" => {
      self.reconstruct_active_formatting()
      if self.in_default_scope("nobr") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        self.run_adoption_agency("nobr")
        self.reconstruct_active_formatting()
      }
      let id = self.insert_html_element(name, attrs)
      self.push_active_formatting(id, name, attrs)
      false
    }
    EndTag(name~) if name == "a" ||
      name == "b" ||
      name == "big" ||
      name == "code" ||
      name == "em" ||
      name == "font" ||
      name == "i" ||
      name == "nobr" ||
      name == "s" ||
      name == "small" ||
      name == "strike" ||
      name == "strong" ||
      name == "tt" ||
      name == "u" => {
      self.run_adoption_agency(name)
      false
    }
    StartTag(name~, attrs~, ..) if name == "applet" ||
      name == "marquee" ||
      name == "object" => {
      self.reconstruct_active_formatting()
      let _ = self.insert_html_element(name, attrs)
      self.push_formatting_marker()
      self.frameset_ok = false
      false
    }
    EndTag(name~) if name == "applet" || name == "marquee" || name == "object" => {
      if !self.in_default_scope(name) {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.generate_implied_end_tags("")
      if self.current_node().tag_name != name {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      }
      self.pop_until(name)
      self.clear_active_formatting_to_marker()
      false
    }
    StartTag(name~, attrs~, ..) if name == "table" => {
      // In quirks mode, don't close p element (table goes inside p)
      if self.document.quirks_mode != Quirks && self.in_button_scope("p") {
        self.close_p_element()
      }
      let _ = self.insert_html_element(name, attrs)
      self.frameset_ok = false
      self.mode = InTable
      false
    }
    EndTag(name~) if name == "br" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      self.reconstruct_active_formatting()
      let _ = self.insert_html_element("br", [])
      let _ = self.pop_element()
      self.frameset_ok = false
      false
    }
    StartTag(name~, attrs~, ..) if name == "area" ||
      name == "br" ||
      name == "embed" ||
      name == "img" ||
      name == "keygen" ||
      name == "wbr" => {
      self.reconstruct_active_formatting()
      let _ = self.insert_html_element(name, attrs)
      let _ = self.pop_element()
      self.frameset_ok = false
      false
    }
    StartTag(name~, attrs~, ..) if name == "input" => {
      self.reconstruct_active_formatting()
      let _ = self.insert_html_element(name, attrs)
      let _ = self.pop_element()
      // Check for type=hidden
      let is_hidden = attrs
        .iter()
        .any(fn(a) { a.name == "type" && a.value.to_lower() == "hidden" })
      if !is_hidden {
        self.frameset_ok = false
      }
      false
    }
    StartTag(name~, attrs~, ..) if name == "param" ||
      name == "source" ||
      name == "track" => {
      let _ = self.insert_html_element(name, attrs)
      let _ = self.pop_element()
      false
    }
    StartTag(name~, attrs~, ..) if name == "hr" => {
      if self.in_button_scope("p") {
        self.close_p_element()
      }
      let _ = self.insert_html_element(name, attrs)
      let _ = self.pop_element()
      self.frameset_ok = false
      false
    }
    StartTag(name~, attrs~, ..) if name == "image" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      // Treat as 
      self.reconstruct_active_formatting()
      let _ = self.insert_html_element("img", attrs)
      let _ = self.pop_element()
      self.frameset_ok = false
      false
    }
    StartTag(name~, attrs~, ..) if name == "textarea" => {
      let _ = self.insert_html_element(name, attrs)
      self.skip_next_newline = true
      self.tokenizer.state = RCDATA
      self.original_mode = self.mode
      self.frameset_ok = false
      self.mode = Text
      false
    }
    StartTag(name~, attrs~, ..) if name == "xmp" => {
      if self.in_button_scope("p") {
        self.close_p_element()
      }
      self.reconstruct_active_formatting()
      self.frameset_ok = false
      self.parse_rawtext(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "iframe" => {
      self.frameset_ok = false
      self.parse_rawtext(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "noembed" => {
      self.parse_rawtext(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "noscript" && self.scripting => {
      self.parse_rawtext(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "select" => {
      self.reconstruct_active_formatting()
      let _ = self.insert_html_element(name, attrs)
      self.frameset_ok = false
      match self.mode {
        InTable | InCaption | InTableBody | InRow | InCell =>
          self.mode = InSelectInTable
        _ => self.mode = InSelect
      }
      false
    }
    StartTag(name~, attrs~, ..) if name == "optgroup" || name == "option" => {
      if self.current_node().tag_name == "option" {
        let _ = self.pop_element()
      }
      self.reconstruct_active_formatting()
      let _ = self.insert_html_element(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "rb" || name == "rtc" => {
      if self.in_default_scope("ruby") {
        self.generate_implied_end_tags("")
        if self.current_node().tag_name != "ruby" {
          self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        }
      }
      let _ = self.insert_html_element(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "rp" || name == "rt" => {
      if self.in_default_scope("ruby") {
        self.generate_implied_end_tags("rtc")
        if self.current_node().tag_name != "rtc" &&
          self.current_node().tag_name != "ruby" {
          self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        }
      }
      let _ = self.insert_html_element(name, attrs)
      false
    }
    StartTag(name~, attrs~, self_closing~) if name == "math" => {
      self.reconstruct_active_formatting()
      // Adjust MathML attributes
      let _ = self.insert_foreign_element(name, attrs, MathML)
      // If self-closing, pop the element immediately
      if self_closing {
        let _ = self.pop_element()
      }
      false
    }
    StartTag(name~, attrs~, self_closing~) if name == "svg" => {
      self.reconstruct_active_formatting()
      // Adjust SVG attributes
      let _ = self.insert_foreign_element(name, attrs, SVG)
      // If self-closing, pop the element immediately
      if self_closing {
        let _ = self.pop_element()
      }
      false
    }
    StartTag(name~, ..) if name == "caption" ||
      name == "col" ||
      name == "colgroup" ||
      name == "frame" ||
      name == "head" ||
      name == "tbody" ||
      name == "td" ||
      name == "tfoot" ||
      name == "th" ||
      name == "thead" ||
      name == "tr" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, attrs~, ..) => {
      self.reconstruct_active_formatting()
      let _ = self.insert_html_element(name, attrs)
      false
    }
    EndTag(name~) => {
      self.process_any_other_end_tag(name)
      false
    }
  }
}

///|
/// Process any other end tag in body mode
fn TreeBuilder::process_any_other_end_tag(
  self : TreeBuilder,
  name : String,
) -> Unit {
  for i = self.open_elements.length() - 1; i >= 0; i = i - 1 {
    let entry = self.open_elements[i]
    if entry.tag_name == name && entry.ns == HTML {
      self.generate_implied_end_tags(name)
      if self.current_node().tag_name != name {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      }
      // Pop until and including this element
      while self.open_elements.length() > i {
        let _ = self.pop_element()
      }
      return
    }
    // Check for special elements (including SVG/MathML special elements)
    if is_special_element(entry.tag_name) {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      return
    }
    // SVG special elements: foreignObject, desc, title
    if entry.ns == SVG &&
      (
        entry.tag_name == "foreignObject" ||
        entry.tag_name == "desc" ||
        entry.tag_name == "title"
      ) {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      return
    }
    // MathML special elements
    if entry.ns == MathML &&
      (
        entry.tag_name == "mi" ||
        entry.tag_name == "mo" ||
        entry.tag_name == "mn" ||
        entry.tag_name == "ms" ||
        entry.tag_name == "mtext" ||
        entry.tag_name == "annotation-xml"
      ) {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      return
    }
  }
}

///|
/// 13.2.6.4.8 Text insertion mode
fn TreeBuilder::process_text_mode(self : TreeBuilder, token : Token) -> Bool {
  match token {
    Character(c) => {
      self.insert_character(c)
      false
    }
    EOF => {
      self.emit_error(EofInTag)
      let _ = self.pop_element()
      self.mode = self.original_mode
      true // Reprocess
    }
    EndTag(name~) if name == "script" => {
      // For now, just pop the script element
      let _ = self.pop_element()
      self.mode = self.original_mode
      false
    }
    EndTag(_) => {
      let _ = self.pop_element()
      self.mode = self.original_mode
      false
    }
    _ => false
  }
}

///|
/// 13.2.6.4.9 In table insertion mode
fn TreeBuilder::process_in_table_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    Character(_) => {
      let current = self.current_node()
      if current.tag_name == "table" ||
        current.tag_name == "tbody" ||
        current.tag_name == "tfoot" ||
        current.tag_name == "thead" ||
        current.tag_name == "tr" {
        self.pending_table_chars.clear()
        self.original_mode = self.mode
        self.mode = InTableText
        return true // Reprocess
      }
      self.process_in_table_anything_else(token)
    }
    Comment(text) => {
      self.insert_comment(text)
      false
    }
    DOCTYPE(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, attrs~, ..) if name == "caption" => {
      self.clear_stack_to_table_context()
      self.push_formatting_marker()
      let _ = self.insert_html_element(name, attrs)
      self.mode = InCaption
      false
    }
    StartTag(name~, attrs~, ..) if name == "colgroup" => {
      self.clear_stack_to_table_context()
      let _ = self.insert_html_element(name, attrs)
      self.mode = InColumnGroup
      false
    }
    StartTag(name~, ..) if name == "col" => {
      self.clear_stack_to_table_context()
      let _ = self.insert_html_element("colgroup", [])
      self.mode = InColumnGroup
      true // Reprocess
    }
    StartTag(name~, attrs~, ..) if name == "tbody" ||
      name == "tfoot" ||
      name == "thead" => {
      self.clear_stack_to_table_context()
      let _ = self.insert_html_element(name, attrs)
      self.mode = InTableBody
      false
    }
    StartTag(name~, ..) if name == "td" || name == "th" || name == "tr" => {
      self.clear_stack_to_table_context()
      let _ = self.insert_html_element("tbody", [])
      self.mode = InTableBody
      true // Reprocess
    }
    StartTag(name~, ..) if name == "table" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      if !self.in_table_scope("table") {
        return false
      }
      self.pop_until("table")
      self.reset_insertion_mode()
      true // Reprocess
    }
    EndTag(name~) if name == "table" => {
      if !self.in_table_scope("table") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.pop_until("table")
      self.reset_insertion_mode()
      false
    }
    EndTag(name~) if name == "body" ||
      name == "caption" ||
      name == "col" ||
      name == "colgroup" ||
      name == "html" ||
      name == "tbody" ||
      name == "td" ||
      name == "tfoot" ||
      name == "th" ||
      name == "thead" ||
      name == "tr" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, ..) if name == "style" ||
      name == "script" ||
      name == "template" => self.process_in_head_mode(token)
    EndTag(name~) if name == "template" => self.process_in_head_mode(token)
    StartTag(name~, attrs~, ..) if name == "input" => {
      let is_hidden = attrs
        .iter()
        .any(fn(a) { a.name == "type" && a.value.to_lower() == "hidden" })
      if !is_hidden {
        return self.process_in_table_anything_else(token)
      }
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      let _ = self.insert_html_element(name, attrs)
      let _ = self.pop_element()
      false
    }
    StartTag(name~, attrs~, ..) if name == "form" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      if self.has_element_in_stack("template") || self.form_element != NO_NODE {
        return false
      }
      let id = self.insert_html_element(name, attrs)
      self.form_element = id
      let _ = self.pop_element()
      false
    }
    EOF => self.process_in_body_mode(token)
    _ => self.process_in_table_anything_else(token)
  }
}

///|
/// Process anything else in table mode (foster parenting)
fn TreeBuilder::process_in_table_anything_else(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
  self.foster_parenting = true
  let result = self.process_in_body_mode(token)
  self.foster_parenting = false
  result
}

///|
/// 13.2.6.4.10 In table text insertion mode
fn TreeBuilder::process_in_table_text_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    Character('\u{0000}') => {
      self.emit_error(UnexpectedNullCharacter)
      false
    }
    Character(c) => {
      self.pending_table_chars.push(c)
      false
    }
    _ => {
      // Check if pending chars are all whitespace
      let all_whitespace = self.pending_table_chars
        .iter()
        .all(fn(c) { is_whitespace_char(c) })
      if all_whitespace {
        for c in self.pending_table_chars {
          self.insert_character(c)
        }
      } else {
        // Foster parent the characters using "anything else" rule
        // This reconstructs active formatting elements
        for c in self.pending_table_chars {
          let _ = self.process_in_table_anything_else(Character(c))
        }
      }
      self.pending_table_chars.clear()
      self.mode = self.original_mode
      true // Reprocess
    }
  }
}

///|
/// 13.2.6.4.11 In caption insertion mode
fn TreeBuilder::process_in_caption_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    EndTag(name~) if name == "caption" => {
      if !self.in_table_scope("caption") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.generate_implied_end_tags("")
      if self.current_node().tag_name != "caption" {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      }
      self.pop_until("caption")
      self.clear_active_formatting_to_marker()
      self.mode = InTable
      false
    }
    StartTag(name~, ..) if name == "caption" ||
      name == "col" ||
      name == "colgroup" ||
      name == "tbody" ||
      name == "td" ||
      name == "tfoot" ||
      name == "th" ||
      name == "thead" ||
      name == "tr" => {
      if !self.in_table_scope("caption") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.generate_implied_end_tags("")
      if self.current_node().tag_name != "caption" {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      }
      self.pop_until("caption")
      self.clear_active_formatting_to_marker()
      self.mode = InTable
      true // Reprocess
    }
    EndTag(name~) if name == "table" => {
      if !self.in_table_scope("caption") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.generate_implied_end_tags("")
      if self.current_node().tag_name != "caption" {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      }
      self.pop_until("caption")
      self.clear_active_formatting_to_marker()
      self.mode = InTable
      true // Reprocess
    }
    EndTag(name~) if name == "body" ||
      name == "col" ||
      name == "colgroup" ||
      name == "html" ||
      name == "tbody" ||
      name == "td" ||
      name == "tfoot" ||
      name == "th" ||
      name == "thead" ||
      name == "tr" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    _ => self.process_in_body_mode(token)
  }
}

///|
/// 13.2.6.4.12 In column group insertion mode
fn TreeBuilder::process_in_column_group_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    Character(c) if is_whitespace_char(c) => {
      self.insert_character(c)
      false
    }
    Comment(text) => {
      self.insert_comment(text)
      false
    }
    DOCTYPE(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, ..) if name == "html" => self.process_in_body_mode(token)
    StartTag(name~, attrs~, ..) if name == "col" => {
      let _ = self.insert_html_element(name, attrs)
      let _ = self.pop_element()
      false
    }
    EndTag(name~) if name == "colgroup" => {
      if self.current_node().tag_name != "colgroup" {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      let _ = self.pop_element()
      self.mode = InTable
      false
    }
    EndTag(name~) if name == "col" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, ..) | EndTag(name~) if name == "template" =>
      self.process_in_head_mode(token)
    EOF => self.process_in_body_mode(token)
    _ => {
      if self.current_node().tag_name != "colgroup" {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      let _ = self.pop_element()
      self.mode = InTable
      true // Reprocess
    }
  }
}

///|
/// 13.2.6.4.13 In table body insertion mode
fn TreeBuilder::process_in_table_body_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    StartTag(name~, attrs~, ..) if name == "tr" => {
      self.clear_stack_to_table_body_context()
      let _ = self.insert_html_element(name, attrs)
      self.mode = InRow
      false
    }
    StartTag(name~, ..) if name == "th" || name == "td" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      self.clear_stack_to_table_body_context()
      let _ = self.insert_html_element("tr", [])
      self.mode = InRow
      true // Reprocess
    }
    EndTag(name~) if name == "tbody" || name == "tfoot" || name == "thead" => {
      if !self.in_table_scope(name) {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.clear_stack_to_table_body_context()
      let _ = self.pop_element()
      self.mode = InTable
      false
    }
    StartTag(name~, ..) if name == "caption" ||
      name == "col" ||
      name == "colgroup" ||
      name == "tbody" ||
      name == "tfoot" ||
      name == "thead" => {
      if !self.in_table_scope("tbody") &&
        !self.in_table_scope("thead") &&
        !self.in_table_scope("tfoot") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.clear_stack_to_table_body_context()
      let _ = self.pop_element()
      self.mode = InTable
      true // Reprocess
    }
    EndTag(name~) if name == "table" => {
      if !self.in_table_scope("tbody") &&
        !self.in_table_scope("thead") &&
        !self.in_table_scope("tfoot") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.clear_stack_to_table_body_context()
      let _ = self.pop_element()
      self.mode = InTable
      true // Reprocess
    }
    EndTag(name~) if name == "body" ||
      name == "caption" ||
      name == "col" ||
      name == "colgroup" ||
      name == "html" ||
      name == "td" ||
      name == "th" ||
      name == "tr" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    _ => self.process_in_table_mode(token)
  }
}

///|
/// 13.2.6.4.14 In row insertion mode
fn TreeBuilder::process_in_row_mode(self : TreeBuilder, token : Token) -> Bool {
  match token {
    StartTag(name~, attrs~, ..) if name == "th" || name == "td" => {
      self.clear_stack_to_table_row_context()
      let _ = self.insert_html_element(name, attrs)
      self.mode = InCell
      self.push_formatting_marker()
      false
    }
    EndTag(name~) if name == "tr" => {
      if !self.in_table_scope("tr") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.clear_stack_to_table_row_context()
      let _ = self.pop_element()
      self.mode = InTableBody
      false
    }
    StartTag(name~, ..) if name == "caption" ||
      name == "col" ||
      name == "colgroup" ||
      name == "tbody" ||
      name == "tfoot" ||
      name == "thead" ||
      name == "tr" => {
      if !self.in_table_scope("tr") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.clear_stack_to_table_row_context()
      let _ = self.pop_element()
      self.mode = InTableBody
      true // Reprocess
    }
    EndTag(name~) if name == "table" => {
      if !self.in_table_scope("tr") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.clear_stack_to_table_row_context()
      let _ = self.pop_element()
      self.mode = InTableBody
      true // Reprocess
    }
    EndTag(name~) if name == "tbody" || name == "tfoot" || name == "thead" => {
      if !self.in_table_scope(name) {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      if !self.in_table_scope("tr") {
        return false
      }
      self.clear_stack_to_table_row_context()
      let _ = self.pop_element()
      self.mode = InTableBody
      true // Reprocess
    }
    EndTag(name~) if name == "body" ||
      name == "caption" ||
      name == "col" ||
      name == "colgroup" ||
      name == "html" ||
      name == "td" ||
      name == "th" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    _ => self.process_in_table_mode(token)
  }
}

///|
/// 13.2.6.4.15 In cell insertion mode
fn TreeBuilder::process_in_cell_mode(self : TreeBuilder, token : Token) -> Bool {
  match token {
    EndTag(name~) if name == "td" || name == "th" => {
      if !self.in_table_scope(name) {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.generate_implied_end_tags("")
      if self.current_node().tag_name != name {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      }
      self.pop_until(name)
      self.clear_active_formatting_to_marker()
      self.mode = InRow
      false
    }
    StartTag(name~, ..) if name == "caption" ||
      name == "col" ||
      name == "colgroup" ||
      name == "tbody" ||
      name == "td" ||
      name == "tfoot" ||
      name == "th" ||
      name == "thead" ||
      name == "tr" => {
      if !self.in_table_scope("td") && !self.in_table_scope("th") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.close_cell()
      true // Reprocess
    }
    EndTag(name~) if name == "body" ||
      name == "caption" ||
      name == "col" ||
      name == "colgroup" ||
      name == "html" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    EndTag(name~) if name == "table" ||
      name == "tbody" ||
      name == "tfoot" ||
      name == "thead" ||
      name == "tr" => {
      if !self.in_table_scope(name) {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.close_cell()
      true // Reprocess
    }
    _ => self.process_in_body_mode(token)
  }
}

///|
/// 13.2.6.4.16 In select insertion mode
fn TreeBuilder::process_in_select_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    Character('\u{0000}') => {
      self.emit_error(UnexpectedNullCharacter)
      false
    }
    Character(c) => {
      self.reconstruct_active_formatting()
      self.insert_character(c)
      false
    }
    Comment(text) => {
      self.insert_comment(text)
      false
    }
    ProcessingInstruction(target~, data~) => {
      self.insert_processing_instruction(target, data)
      false
    }
    DOCTYPE(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, ..) if name == "html" => self.process_in_body_mode(token)
    StartTag(name~, attrs~, ..) if name == "option" => {
      if self.current_node().tag_name == "option" {
        let _ = self.pop_element()
      }
      // Reconstruct formatting elements before inserting option
      self.reconstruct_active_formatting()
      let _ = self.insert_html_element(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "optgroup" => {
      if self.current_node().tag_name == "option" {
        let _ = self.pop_element()
      }
      if self.current_node().tag_name == "optgroup" {
        let _ = self.pop_element()
      }
      let _ = self.insert_html_element(name, attrs)
      false
    }
    StartTag(name~, attrs~, ..) if name == "hr" => {
      if self.current_node().tag_name == "option" {
        let _ = self.pop_element()
      }
      if self.current_node().tag_name == "optgroup" {
        let _ = self.pop_element()
      }
      let _ = self.insert_html_element(name, attrs)
      let _ = self.pop_element()
      false
    }
    EndTag(name~) if name == "optgroup" => {
      if self.current_node().tag_name == "option" &&
        self.open_elements.length() >= 2 &&
        self.open_elements[self.open_elements.length() - 2].tag_name ==
        "optgroup" {
        let _ = self.pop_element()
      }
      if self.current_node().tag_name == "optgroup" {
        let _ = self.pop_element()
      } else {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      }
      false
    }
    EndTag(name~) if name == "option" => {
      if self.current_node().tag_name == "option" {
        let _ = self.pop_element()
      } else {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      }
      false
    }
    EndTag(name~) if name == "select" => {
      if !self.in_select_scope("select") {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      self.pop_until("select")
      self.reset_insertion_mode()
      false
    }
    StartTag(name~, ..) if name == "select" => {
      // Nested select: close the current one and reset mode
      // Per spec: just pop until select, no scope check needed
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      self.pop_until("select")
      self.reset_insertion_mode()
      false
    }
    StartTag(name~, ..) if name == "input" || name == "textarea" => {
      // These elements close the select and get reprocessed
      // Note: keygen is obsolete and treated as unknown element inside select
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      self.pop_until("select")
      self.reset_insertion_mode()
      true // Reprocess
    }
    StartTag(name~, ..) | EndTag(name~) if name == "script" ||
      name == "template" => self.process_in_head_mode(token)
    StartTag(name~, attrs~, self_closing~) if name == "math" => {
      // Insert MathML element in select (browsers allow this despite spec)
      let _ = self.insert_foreign_element(name, attrs, MathML)
      if self_closing {
        let _ = self.pop_element()
      }
      false
    }
    StartTag(name~, attrs~, self_closing~) if name == "svg" => {
      // Insert SVG element in select (browsers allow this despite spec)
      let _ = self.insert_foreign_element(name, attrs, SVG)
      if self_closing {
        let _ = self.pop_element()
      }
      false
    }
    EOF => self.process_in_body_mode(token)
    StartTag(name~, attrs~, ..) if name == "plaintext" => {
      // Plaintext inside select: insert element AND switch tokenizer to PLAINTEXT
      // This makes  become literal text
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      let _ = self.insert_html_element(name, attrs)
      self.tokenizer.state = PLAINTEXT
      false
    }
    StartTag(name~, ..) if name == "caption" ||
      name == "table" ||
      name == "tbody" ||
      name == "tfoot" ||
      name == "thead" ||
      name == "tr" ||
      name == "td" ||
      name == "th" ||
      name == "col" ||
      name == "colgroup" => {
      // Table-related elements in select (not InSelectInTable) should be ignored
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, attrs~, self_closing~) => {
      // Browsers insert unknown elements inside select (against spec but matches reality)
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      self.reconstruct_active_formatting()
      let _ = self.insert_html_element(name, attrs)
      // Pop void elements immediately (img, br, etc.) or self-closing tags
      if self_closing || is_void_element_name(name) {
        let _ = self.pop_element()
      }
      // Add to active formatting list if it's a formatting element
      if is_formatting_element(name) {
        self.push_active_formatting(self.current_node().node_id, name, attrs)
      }
      false
    }
    EndTag(name~) => {
      if is_formatting_element(name) {
        let mut found_in_select = false
        for i = self.open_elements.length() - 1; i >= 0; i = i - 1 {
          let entry = self.open_elements[i]
          if entry.tag_name == "select" && entry.ns == HTML {
            break
          }
          if entry.tag_name == name && entry.ns == HTML {
            found_in_select = true
            break
          }
        }
        if found_in_select {
          self.run_adoption_agency(name)
        } else {
          self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        }
        return false
      }
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      // Find matching element in stack (only within select scope)
      for i = self.open_elements.length() - 1; i >= 0; i = i - 1 {
        let entry = self.open_elements[i]
        if entry.tag_name == "select" {
          // Hit the select element - don't look further
          break
        }
        if entry.tag_name == name && entry.ns == HTML {
          // Found matching element, pop until it
          while self.open_elements.length() > i {
            let _ = self.pop_element()
          }
          break
        }
      }
      false
    }
  }
}

///|
/// 13.2.6.4.17 In select in table insertion mode
fn TreeBuilder::process_in_select_in_table_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    StartTag(name~, ..) if name == "caption" ||
      name == "table" ||
      name == "tbody" ||
      name == "tfoot" ||
      name == "thead" ||
      name == "tr" ||
      name == "td" ||
      name == "th" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      self.pop_until("select")
      self.reset_insertion_mode()
      true // Reprocess
    }
    EndTag(name~) if name == "caption" ||
      name == "table" ||
      name == "tbody" ||
      name == "tfoot" ||
      name == "thead" ||
      name == "tr" ||
      name == "td" ||
      name == "th" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      if !self.in_table_scope(name) {
        return false
      }
      self.pop_until("select")
      self.reset_insertion_mode()
      true // Reprocess
    }
    _ => self.process_in_select_mode(token)
  }
}

///|
/// 13.2.6.4.18 In template insertion mode
fn TreeBuilder::process_in_template_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    Character(_) | Comment(_) | ProcessingInstruction(_) | DOCTYPE(_) =>
      self.process_in_body_mode(token)
    StartTag(name~, ..) if name == "base" ||
      name == "basefont" ||
      name == "bgsound" ||
      name == "link" ||
      name == "meta" ||
      name == "noframes" ||
      name == "script" ||
      name == "style" ||
      name == "template" ||
      name == "title" => self.process_in_head_mode(token)
    EndTag(name~) if name == "template" => self.process_in_head_mode(token)
    StartTag(name~, ..) if name == "caption" ||
      name == "colgroup" ||
      name == "tbody" ||
      name == "tfoot" ||
      name == "thead" => {
      let _ = self.template_modes.pop()
      self.template_modes.push(InTable)
      self.mode = InTable
      true // Reprocess
    }
    StartTag(name~, ..) if name == "col" => {
      let _ = self.template_modes.pop()
      self.template_modes.push(InColumnGroup)
      self.mode = InColumnGroup
      true // Reprocess
    }
    StartTag(name~, ..) if name == "tr" => {
      let _ = self.template_modes.pop()
      self.template_modes.push(InTableBody)
      self.mode = InTableBody
      true // Reprocess
    }
    StartTag(name~, ..) if name == "td" || name == "th" => {
      let _ = self.template_modes.pop()
      self.template_modes.push(InRow)
      self.mode = InRow
      true // Reprocess
    }
    StartTag(_) => {
      let _ = self.template_modes.pop()
      self.template_modes.push(InBody)
      self.mode = InBody
      true // Reprocess
    }
    EndTag(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    EOF => {
      if !self.has_element_in_stack("template") {
        // Stop parsing
        return false
      }
      self.emit_error(EofInTag)
      self.pop_until("template")
      self.clear_active_formatting_to_marker()
      let _ = self.template_modes.pop()
      self.reset_insertion_mode()
      true // Reprocess
    }
  }
}

///|
/// 13.2.6.4.19 After body insertion mode
fn TreeBuilder::process_after_body_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    Character(c) if is_whitespace_char(c) => self.process_in_body_mode(token)
    Comment(text) => {
      // Insert as last child of html element
      let html_id = self.open_elements[0].node_id
      let node_id = self.document.add_node(CommentNode(text))
      self.document.append_child(html_id, node_id)
      false
    }
    DOCTYPE(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, ..) if name == "html" => self.process_in_body_mode(token)
    EndTag(name~) if name == "html" => {
      // Fragment case would return here
      self.mode = AfterAfterBody
      false
    }
    EOF =>
      // Stop parsing
      false
    _ => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      self.mode = InBody
      true // Reprocess
    }
  }
}

///|
/// 13.2.6.4.20 In frameset insertion mode
fn TreeBuilder::process_in_frameset_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    Character(c) if is_whitespace_char(c) => {
      self.insert_character(c)
      false
    }
    Comment(text) => {
      self.insert_comment(text)
      false
    }
    DOCTYPE(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, ..) if name == "html" => self.process_in_body_mode(token)
    StartTag(name~, attrs~, ..) if name == "frameset" => {
      let _ = self.insert_html_element(name, attrs)
      false
    }
    EndTag(name~) if name == "frameset" => {
      if self.current_node().tag_name == "html" {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        return false
      }
      let _ = self.pop_element()
      // Fragment case check would go here
      if self.current_node().tag_name != "frameset" {
        self.mode = AfterFrameset
      }
      false
    }
    StartTag(name~, attrs~, ..) if name == "frame" => {
      let _ = self.insert_html_element(name, attrs)
      let _ = self.pop_element()
      false
    }
    StartTag(name~, ..) if name == "noframes" =>
      self.process_in_head_mode(token)
    EOF => {
      if self.current_node().tag_name != "html" {
        self.emit_error(EofInTag)
      }
      false
    }
    _ => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
  }
}

///|
/// 13.2.6.4.21 After frameset insertion mode
fn TreeBuilder::process_after_frameset_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    Character(c) if is_whitespace_char(c) => {
      self.insert_character(c)
      false
    }
    Comment(text) => {
      self.insert_comment(text)
      false
    }
    DOCTYPE(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, ..) if name == "html" => self.process_in_body_mode(token)
    EndTag(name~) if name == "html" => {
      self.mode = AfterAfterFrameset
      false
    }
    StartTag(name~, ..) if name == "noframes" =>
      self.process_in_head_mode(token)
    EOF =>
      // Stop parsing
      false
    _ => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
  }
}

///|
/// 13.2.6.4.22 After after body insertion mode
fn TreeBuilder::process_after_after_body_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    Comment(text) => {
      self.insert_comment_at_document(text)
      false
    }
    DOCTYPE(_) => self.process_in_body_mode(token)
    Character(c) if is_whitespace_char(c) => self.process_in_body_mode(token)
    StartTag(name~, ..) if name == "html" => self.process_in_body_mode(token)
    EOF =>
      // Stop parsing
      false
    _ => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      self.mode = InBody
      true // Reprocess
    }
  }
}

///|
/// 13.2.6.4.23 After after frameset insertion mode
fn TreeBuilder::process_after_after_frameset_mode(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    Comment(text) => {
      self.insert_comment_at_document(text)
      false
    }
    DOCTYPE(_) => self.process_in_body_mode(token)
    Character(c) if is_whitespace_char(c) => self.process_in_body_mode(token)
    StartTag(name~, ..) if name == "html" => self.process_in_body_mode(token)
    EOF =>
      // Stop parsing
      false
    StartTag(name~, ..) if name == "noframes" =>
      self.process_in_head_mode(token)
    _ => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
  }
}

///|
/// 13.2.6.5 Foreign content
fn TreeBuilder::process_in_foreign_content(
  self : TreeBuilder,
  token : Token,
) -> Bool {
  match token {
    Character('\u{0000}') => {
      self.emit_error(UnexpectedNullCharacter)
      self.insert_character('\u{FFFD}')
      false
    }
    Character(c) if is_whitespace_char(c) => {
      self.insert_character(c)
      false
    }
    Character(c) => {
      self.insert_character(c)
      self.frameset_ok = false
      false
    }
    Comment(text) => {
      self.insert_comment(text)
      false
    }
    DOCTYPE(_) => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      false
    }
    StartTag(name~, attrs~, self_closing~) if name == "b" ||
      name == "big" ||
      name == "blockquote" ||
      name == "body" ||
      name == "br" ||
      name == "center" ||
      name == "code" ||
      name == "dd" ||
      name == "div" ||
      name == "dl" ||
      name == "dt" ||
      name == "em" ||
      name == "embed" ||
      name == "h1" ||
      name == "h2" ||
      name == "h3" ||
      name == "h4" ||
      name == "h5" ||
      name == "h6" ||
      name == "head" ||
      name == "hr" ||
      name == "i" ||
      name == "img" ||
      name == "li" ||
      name == "listing" ||
      name == "menu" ||
      name == "meta" ||
      name == "nobr" ||
      name == "ol" ||
      name == "p" ||
      name == "pre" ||
      name == "ruby" ||
      name == "s" ||
      name == "small" ||
      name == "span" ||
      name == "strong" ||
      name == "strike" ||
      name == "sub" ||
      name == "sup" ||
      name == "table" ||
      name == "tt" ||
      name == "u" ||
      name == "ul" ||
      name == "var" => {
      self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
      // Pop until we're back in HTML namespace
      while self.open_elements.length() > 0 {
        let current = self.current_node()
        if current.ns == HTML ||
          self.is_mathml_text_integration_point(current) ||
          self.is_html_integration_point(current) {
          break
        }
        let _ = self.pop_element()
      }
      true // Reprocess
    }
    StartTag(name~, attrs~, self_closing~) if name == "font" => {
      // Check for color, face, or size attributes
      let has_special = attrs
        .iter()
        .any(fn(a) { a.name == "color" || a.name == "face" || a.name == "size" })
      if has_special {
        self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
        while self.open_elements.length() > 0 {
          let current = self.current_node()
          if current.ns == HTML ||
            self.is_mathml_text_integration_point(current) ||
            self.is_html_integration_point(current) {
            break
          }
          let _ = self.pop_element()
        }
        return true // Reprocess
      }
      // Otherwise, insert foreign element
      let current = self.current_node()
      let _ = self.insert_foreign_element(name, attrs, current.ns)
      if self_closing {
        let _ = self.pop_element()
      }
      false
    }
    StartTag(name~, attrs~, self_closing~) => {
      // Insert foreign element in current namespace
      let current = self.current_node()
      let _ = self.insert_foreign_element(name, attrs, current.ns)
      if self_closing {
        let _ = self.pop_element()
      }
      false
    }
    EndTag(name~) if name == "script" &&
      self.current_node().tag_name == "script" &&
      self.current_node().ns == SVG => {
      let _ = self.pop_element()
      // Script execution would go here
      false
    }
    EndTag(name~) => {
      // Search for matching element in stack
      // Per WHATWG spec: check HTML namespace BEFORE tag name match
      for i = self.open_elements.length() - 1; i >= 0; i = i - 1 {
        let entry = self.open_elements[i]
        // If we hit an HTML element, switch to HTML mode processing
        if entry.ns == HTML {
          // Some end tags in HTML mode can insert elements (e.g., 

,
) // For these, we need to pop foreign elements first match token { EndTag(name~) if name == "p" || name == "br" => while self.open_elements.length() > i + 1 { let _ = self.pop_element() } _ => () } return self.process_token_in_html_mode(token) } // Check for matching foreign element if entry.tag_name.to_lower() == name.to_lower() { // Pop until and including this element while self.open_elements.length() > i { let _ = self.pop_element() } return false } } false } _ => false } } // ============================================================================ // Helper functions // ============================================================================ ///| /// Check if character is whitespace (for tree builder) fn is_whitespace_char(c : Char) -> Bool { c == ' ' || c == '\t' || c == '\n' || c == '\u{0C}' || c == '\r' } ///| /// Check if tag name is a void element (no closing tag needed) fn is_void_element_name(name : String) -> Bool { match name { "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link" | "meta" | "param" | "source" | "track" | "wbr" => true _ => false } } ///| /// Check if tag name is a special element fn is_special_element(tag : String) -> Bool { match tag { "address" | "applet" | "area" | "article" | "aside" | "base" | "basefont" | "bgsound" | "blockquote" | "body" | "br" | "button" | "caption" | "center" | "col" | "colgroup" | "dd" | "details" | "dir" | "div" | "dl" | "dt" | "embed" | "fieldset" | "figcaption" | "figure" | "footer" | "form" | "frame" | "frameset" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "head" | "header" | "hgroup" | "hr" | "html" | "iframe" | "img" | "input" | "keygen" | "li" | "link" | "listing" | "main" | "marquee" | "menu" | "meta" | "nav" | "noembed" | "noframes" | "noscript" | "object" | "ol" | "p" | "param" | "plaintext" | "pre" | "script" | "search" | "section" | "select" | "source" | "style" | "summary" | "table" | "tbody" | "td" | "template" | "textarea" | "tfoot" | "th" | "thead" | "title" | "tr" | "track" | "ul" | "wbr" | "xmp" => true _ => false } } ///| /// Close a p element fn TreeBuilder::close_p_element(self : TreeBuilder) -> Unit { self.generate_implied_end_tags("p") if self.current_node().tag_name != "p" { self.emit_error(UnexpectedCharacterInUnquotedAttributeValue) } self.pop_until("p") } ///| /// Close a table cell fn TreeBuilder::close_cell(self : TreeBuilder) -> Unit { self.generate_implied_end_tags("") let current = self.current_node() if current.tag_name != "td" && current.tag_name != "th" { self.emit_error(UnexpectedCharacterInUnquotedAttributeValue) } // Pop until td or th while self.open_elements.length() > 0 { let entry = self.pop_element() if entry.tag_name == "td" || entry.tag_name == "th" { break } } self.clear_active_formatting_to_marker() self.mode = InRow } ///| /// Clear stack back to table context fn TreeBuilder::clear_stack_to_table_context(self : TreeBuilder) -> Unit { while self.open_elements.length() > 0 { let current = self.current_node() if current.tag_name == "table" || current.tag_name == "template" || current.tag_name == "html" { break } let _ = self.pop_element() } } ///| /// Clear stack back to table body context fn TreeBuilder::clear_stack_to_table_body_context(self : TreeBuilder) -> Unit { while self.open_elements.length() > 0 { let current = self.current_node() if current.tag_name == "tbody" || current.tag_name == "tfoot" || current.tag_name == "thead" || current.tag_name == "template" || current.tag_name == "html" { break } let _ = self.pop_element() } } ///| /// Clear stack back to table row context fn TreeBuilder::clear_stack_to_table_row_context(self : TreeBuilder) -> Unit { while self.open_elements.length() > 0 { let current = self.current_node() if current.tag_name == "tr" || current.tag_name == "template" || current.tag_name == "html" { break } let _ = self.pop_element() } }