///|
/// Insertion Mode Handlers for HTML5 Tree Construction
/// Based on WHATWG HTML5 Parsing Specification
///|
/// Handle initial insertion mode
fn TreeBuilder::handle_initial(self : TreeBuilder, token : Token) -> Unit {
match token {
Token::Doctype(_) => self.insertion_mode = InsertionMode::BeforeHtml
Token::Character(c) if tok_is_whitespace(c) => () // Ignore whitespace
Token::Characters(s) if is_whitespace_only(s) => () // Ignore whitespace
_ => {
self.insertion_mode = InsertionMode::BeforeHtml
self.reprocess_token(token)
}
}
}
///|
/// Handle before html insertion mode
fn TreeBuilder::handle_before_html(self : TreeBuilder, token : Token) -> Unit {
match token {
Token::Doctype(_) => () // Ignore
Token::Character(c) if tok_is_whitespace(c) => () // Ignore whitespace
Token::Characters(s) if is_whitespace_only(s) => () // Ignore whitespace
Token::StartTag(tag, attrs, _) if tag == "html" => {
let elem = create_element(tag, attrs)
self.open_elements.push(elem)
self.insertion_mode = InsertionMode::BeforeHead
}
_ => {
let elem = create_element("html", {})
self.open_elements.push(elem)
self.insertion_mode = InsertionMode::BeforeHead
self.reprocess_token(token)
}
}
}
///|
/// Handle before head insertion mode
fn TreeBuilder::handle_before_head(self : TreeBuilder, token : Token) -> Unit {
match token {
Token::Character(c) if tok_is_whitespace(c) => () // Ignore whitespace
Token::Characters(s) if is_whitespace_only(s) => () // Ignore whitespace
Token::StartTag(tag, attrs, _) if tag == "head" => {
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.head_element = Some(elem)
self.insertion_mode = InsertionMode::InHead
}
Token::EndTag(tag) if tag == "head" ||
tag == "body" ||
tag == "html" ||
tag == "br" => {
let elem = create_element("head", {})
self.insert_element(elem)
self.head_element = Some(elem)
self.insertion_mode = InsertionMode::InHead
self.reprocess_token(token)
}
Token::EndTag(_) => () // Parse error, ignore
_ => {
let elem = create_element("head", {})
self.insert_element(elem)
self.head_element = Some(elem)
self.insertion_mode = InsertionMode::InHead
self.reprocess_token(token)
}
}
}
///|
/// Handle in head insertion mode
fn TreeBuilder::handle_in_head(self : TreeBuilder, token : Token) -> Unit {
match token {
Token::Character(c) if tok_is_whitespace(c) => self.insert_character(c)
Token::Characters(s) if is_whitespace_only(s) => self.insert_characters(s)
Token::StartTag(tag, attrs, self_closing) =>
match tag {
"html" => self.handle_in_body(Token::StartTag(tag, attrs, self_closing))
"base" | "basefont" | "bgsound" | "link" | "meta" => {
let elem = create_element(tag, attrs)
self.insert_element(elem)
let _ = self.open_elements.pop()
// Void element
}
"title" => {
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.tokenizer.enter_raw_text_mode(tag)
self.original_insertion_mode = Some(self.insertion_mode)
self.insertion_mode = InsertionMode::Text
}
"style" | "script" => {
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.tokenizer.enter_raw_text_mode(tag)
self.original_insertion_mode = Some(self.insertion_mode)
self.insertion_mode = InsertionMode::Text
}
"template" => self.start_template(tag, attrs)
"head" => () // Parse error, ignore
_ => {
// Pop head and switch to after head
let _ = self.open_elements.pop()
self.insertion_mode = InsertionMode::AfterHead
self.reprocess_token(Token::StartTag(tag, attrs, self_closing))
}
}
Token::EndTag(tag) =>
match tag {
"head" => {
let _ = self.open_elements.pop()
self.insertion_mode = InsertionMode::AfterHead
}
"body" | "html" | "br" => {
let _ = self.open_elements.pop()
self.insertion_mode = InsertionMode::AfterHead
self.reprocess_token(Token::EndTag(tag))
}
"template" => self.end_template()
_ => () // Parse error, ignore
}
_ => {
let _ = self.open_elements.pop()
self.insertion_mode = InsertionMode::AfterHead
self.reprocess_token(token)
}
}
}
///|
/// `` start tag (HTML "in head" rules): insert the element, push a
/// formatting marker, and switch into the in-template insertion mode. Template
/// content is parsed as a real (but inert — UA display:none) DOM subtree; the
/// element is a scope boundary so its content cannot leak end tags into the
/// surrounding document.
fn TreeBuilder::start_template(
self : TreeBuilder,
tag : String,
attrs : Map[String, String],
) -> Unit {
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.active_formatting.push(FormattingElement::Marker)
self.frameset_ok = false
self.template_insertion_modes.push(InsertionMode::InTemplate)
self.insertion_mode = InsertionMode::InTemplate
}
///|
/// `` end tag (HTML "in head" rules): pop the template subtree and
/// restore the surrounding insertion mode. Reached uniformly via
/// process_token_single so it closes from any insertion mode the template's
/// content may have switched into.
fn TreeBuilder::end_template(self : TreeBuilder) -> Unit {
if !self.has_element_in_scope("template") {
return // Parse error, ignore
}
self.flush_pending_text()
self.generate_implied_end_tags()
self.pop_until("template")
self.clear_active_formatting_to_marker()
if self.template_insertion_modes.length() > 0 {
self.template_insertion_modes.pop() |> ignore
}
self.reset_insertion_mode()
}
///|
/// Handle after head insertion mode
fn TreeBuilder::handle_after_head(self : TreeBuilder, token : Token) -> Unit {
match token {
Token::Character(c) if tok_is_whitespace(c) => self.insert_character(c)
Token::Characters(s) if is_whitespace_only(s) => self.insert_characters(s)
Token::StartTag(tag, attrs, _) if tag == "body" => {
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.insertion_mode = InsertionMode::InBody
}
Token::StartTag(tag, _, _) if tag == "head" => () // Parse error, ignore
Token::EndTag(tag) if tag != "body" && tag != "html" && tag != "br" => () // Parse error, ignore
_ => {
let elem = create_element("body", {})
self.insert_element(elem)
self.insertion_mode = InsertionMode::InBody
self.reprocess_token(token)
}
}
}
///|
/// Handle in body insertion mode - this is the main handler
fn TreeBuilder::handle_in_body(self : TreeBuilder, token : Token) -> Unit {
match token {
Token::Character(c) => {
self.reconstruct_active_formatting_elements()
self.insert_character(c)
}
Token::Characters(s) => {
self.reconstruct_active_formatting_elements()
self.insert_characters(s)
}
Token::Comment(_) => () // Ignore comments for now
Token::Doctype(_) => () // Parse error, ignore
Token::StartTag(tag, attrs, self_closing) =>
self.handle_in_body_start_tag(tag, attrs, self_closing)
Token::EndTag(tag) => self.handle_in_body_end_tag(tag)
Token::EOF => ()
}
}
///|
/// Handle start tags in body
fn TreeBuilder::handle_in_body_start_tag(
self : TreeBuilder,
tag : String,
attrs : Map[String, String],
self_closing : Bool,
) -> Unit {
match tag {
"html" =>
// Merge attributes to first html element
()
"head" =>
// Parse error, ignore - we already have a head
()
"template" => self.start_template(tag, attrs)
"base"
| "basefont"
| "bgsound"
| "link"
| "meta"
| "noframes"
| "script"
| "style"
| "title" =>
match tag {
"base" | "basefont" | "bgsound" | "link" | "meta" => {
let elem = create_element(tag, attrs)
self.insert_into_head(elem, false)
}
"noframes" | "script" | "style" | "title" => {
let elem = create_element(tag, attrs)
self.insert_into_head(elem, true)
self.tokenizer.enter_raw_text_mode(tag)
self.original_insertion_mode = Some(InsertionMode::InBody)
self.insertion_mode = InsertionMode::Text
}
_ => ()
}
"body" =>
// Merge attributes if already have body
()
"address"
| "article"
| "aside"
| "blockquote"
| "center"
| "details"
| "dialog"
| "dir"
| "div"
| "dl"
| "fieldset"
| "figcaption"
| "figure"
| "footer"
| "header"
| "hgroup"
| "main"
| "menu"
| "nav"
| "ol"
| "p"
| "section"
| "summary"
| "ul" => {
if self.has_element_in_button_scope("p") {
self.close_p_element()
}
let elem = create_element(tag, attrs)
self.insert_element(elem)
}
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => {
if self.has_element_in_button_scope("p") {
self.close_p_element()
}
// If current is heading, pop it (implicit close)
match self.current_node() {
Some(e) if e.tag == "h1" ||
e.tag == "h2" ||
e.tag == "h3" ||
e.tag == "h4" ||
e.tag == "h5" ||
e.tag == "h6" => {
let _ = self.open_elements.pop()
}
_ => ()
}
let elem = create_element(tag, attrs)
self.insert_element(elem)
}
"pre" | "listing" => {
if self.has_element_in_button_scope("p") {
self.close_p_element()
}
let elem = create_element(tag, attrs)
self.insert_element(elem)
}
"form" => {
if self.has_element_in_button_scope("p") {
self.close_p_element()
}
let elem = create_element(tag, attrs)
self.insert_element(elem)
}
"li" => {
self.frameset_ok = false
self.close_li_if_needed()
if self.has_element_in_button_scope("p") {
self.close_p_element()
}
let elem = create_element(tag, attrs)
self.insert_element(elem)
}
"dd" | "dt" => {
self.frameset_ok = false
self.close_dd_dt_if_needed()
if self.has_element_in_button_scope("p") {
self.close_p_element()
}
let elem = create_element(tag, attrs)
self.insert_element(elem)
}
"plaintext" => {
if self.has_element_in_button_scope("p") {
self.close_p_element()
}
let elem = create_element(tag, attrs)
self.insert_element(elem)
}
"button" => {
if self.has_element_in_scope("button") {
self.generate_implied_end_tags()
self.pop_until("button")
}
let elem = create_element(tag, attrs)
self.insert_element(elem)
}
"a" => {
// Check for active anchor and close if needed
if self.has_active_formatting_element("a") {
self.run_adoption_agency("a")
}
self.reconstruct_active_formatting_elements()
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.active_formatting.push(FormattingElement::Element(elem))
}
"b"
| "big"
| "code"
| "em"
| "font"
| "i"
| "s"
| "small"
| "strike"
| "strong"
| "tt"
| "u" => {
self.reconstruct_active_formatting_elements()
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.active_formatting.push(FormattingElement::Element(elem))
}
"nobr" => {
if self.has_element_in_scope("nobr") {
// Run adoption agency
self.pop_until("nobr")
}
self.reconstruct_active_formatting_elements()
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.active_formatting.push(FormattingElement::Element(elem))
}
"table" => {
if self.has_element_in_button_scope("p") {
self.close_p_element()
}
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.frameset_ok = false
self.insertion_mode = InsertionMode::InTable
}
"area"
| "br"
| "embed"
| "img"
| "keygen"
| "wbr"
| "input"
| "param"
| "source"
| "track"
| "hr" => {
let elem = create_element(tag, attrs)
self.insert_element(elem)
let _ = self.open_elements.pop()
// Void element
}
"image" => {
// Treated as
let elem = create_element("img", attrs)
self.insert_element(elem)
let _ = self.open_elements.pop()
}
"textarea" => {
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.original_insertion_mode = Some(self.insertion_mode)
self.insertion_mode = InsertionMode::Text
}
"select" => {
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.insertion_mode = InsertionMode::InSelect
}
"optgroup" | "option" => {
if self.current_node() is Some(e) && e.tag == "option" {
let _ = self.open_elements.pop()
}
let elem = create_element(tag, attrs)
self.insert_element(elem)
}
"caption"
| "col"
| "colgroup"
| "tbody"
| "td"
| "tfoot"
| "th"
| "thead"
| "tr" =>
// These are table-only elements, ignore if not in table context
()
_ => {
self.reconstruct_active_formatting_elements()
let elem = create_element(tag, attrs)
self.insert_element(elem)
if self_closing {
let _ = self.open_elements.pop()
}
}
}
}
///|
/// Handle end tags in body
fn TreeBuilder::handle_in_body_end_tag(
self : TreeBuilder,
tag : String,
) -> Unit {
// Flush any pending text before processing end tag
// This ensures text content is added to the current element before it's popped
self.flush_pending_text()
match tag {
"body" => {
if !self.has_element_in_scope("body") {
return // Parse error, ignore
}
self.insertion_mode = InsertionMode::AfterBody
}
"html" => {
if !self.has_element_in_scope("body") {
return // Parse error, ignore
}
self.insertion_mode = InsertionMode::AfterBody
self.reprocess_token(Token::EndTag("html"))
}
"address"
| "article"
| "aside"
| "blockquote"
| "button"
| "center"
| "details"
| "dialog"
| "dir"
| "div"
| "dl"
| "fieldset"
| "figcaption"
| "figure"
| "footer"
| "header"
| "hgroup"
| "listing"
| "main"
| "menu"
| "nav"
| "ol"
| "pre"
| "section"
| "summary"
| "ul" => {
if !self.has_element_in_scope(tag) {
return // Parse error, ignore
}
self.generate_implied_end_tags()
self.pop_until(tag)
}
"form" => {
// A stray "" with no form in scope is a parse error and must be
// ignored; otherwise pop_until below would clear the whole open-elements
// stack (dropping body content).
if !self.has_element_in_scope("form") {
return
}
self.generate_implied_end_tags()
self.pop_until("form")
}
"p" => {
if !self.has_element_in_button_scope("p") {
// Create an implicit p element
let elem = create_element("p", {})
self.insert_element(elem)
}
self.close_p_element()
}
"li" => {
if !self.has_element_in_list_scope("li") {
return // Parse error, ignore
}
self.generate_implied_end_tags_except("li")
self.pop_until("li")
}
"dd" | "dt" => {
if !self.has_element_in_scope(tag) {
return // Parse error, ignore
}
self.generate_implied_end_tags_except(tag)
self.pop_until(tag)
}
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => {
if !self.has_heading_in_scope() {
return // Parse error, ignore
}
self.generate_implied_end_tags()
// Pop until any heading
while !self.open_elements.is_empty() {
match self.current_node() {
Some(e) if e.tag == "h1" ||
e.tag == "h2" ||
e.tag == "h3" ||
e.tag == "h4" ||
e.tag == "h5" ||
e.tag == "h6" => {
let _ = self.open_elements.pop()
break
}
_ => {
let _ = self.open_elements.pop()
}
}
}
}
"a"
| "b"
| "big"
| "code"
| "em"
| "font"
| "i"
| "nobr"
| "s"
| "small"
| "strike"
| "strong"
| "tt"
| "u" => self.run_adoption_agency(tag)
"br" => {
// is treated as
let elem = create_element("br", {})
self.insert_element(elem)
let _ = self.open_elements.pop()
}
_ =>
// Any other end tag
self.any_other_end_tag(tag)
}
}
///|
/// Handle any other end tag in body
fn TreeBuilder::any_other_end_tag(self : TreeBuilder, tag : String) -> Unit {
for i = self.open_elements.length() - 1; i >= 0; i = i - 1 {
let node = self.open_elements[i]
if node.tag == tag {
self.generate_implied_end_tags_except(tag)
// Pop until this node
while self.open_elements.length() > i + 1 {
let _ = self.open_elements.pop()
}
let _ = self.open_elements.pop() // Pop the matched node
return
}
if is_special_element(node.tag) {
return // Parse error, ignore
}
}
}
///|
/// Handle in table insertion mode
fn TreeBuilder::handle_in_table(self : TreeBuilder, token : Token) -> Unit {
match token {
Token::Character(_) | Token::Characters(_) => {
// Enable foster parenting for text in table
self.pending_table_chars = []
self.original_insertion_mode = Some(self.insertion_mode)
self.insertion_mode = InsertionMode::InTableText
self.reprocess_token(token)
}
Token::Comment(_) => () // Ignore for now
Token::Doctype(_) => () // Parse error, ignore
Token::StartTag(tag, attrs, self_closing) =>
match tag {
"caption" => {
self.clear_stack_to_table_context()
self.active_formatting.push(FormattingElement::Marker)
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.insertion_mode = InsertionMode::InCaption
}
"colgroup" => {
self.clear_stack_to_table_context()
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.insertion_mode = InsertionMode::InColumnGroup
}
"col" => {
self.clear_stack_to_table_context()
let elem = create_element("colgroup", {})
self.insert_element(elem)
self.insertion_mode = InsertionMode::InColumnGroup
self.reprocess_token(Token::StartTag(tag, attrs, self_closing))
}
"tbody" | "tfoot" | "thead" => {
self.clear_stack_to_table_context()
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.insertion_mode = InsertionMode::InTableBody
}
"td" | "th" | "tr" => {
self.clear_stack_to_table_context()
let elem = create_element("tbody", {})
self.insert_element(elem)
self.insertion_mode = InsertionMode::InTableBody
self.reprocess_token(Token::StartTag(tag, attrs, self_closing))
}
"table" =>
// Close current table and reprocess
if self.has_element_in_table_scope("table") {
self.pop_until("table")
self.reset_insertion_mode()
self.reprocess_token(Token::StartTag(tag, attrs, self_closing))
}
"style" | "script" | "template" =>
self.handle_in_head(Token::StartTag(tag, attrs, self_closing))
"input" =>
// Check if type is hidden
match attrs.get("type") {
Some(t) if t.to_lower() == "hidden" => {
let elem = create_element(tag, attrs)
self.insert_element(elem)
let _ = self.open_elements.pop()
}
_ => {
// Foster parent
self.foster_parenting = true
self.handle_in_body(Token::StartTag(tag, attrs, self_closing))
self.foster_parenting = false
}
}
"form" => () // Parse error, ignore
_ => {
// Foster parent
self.foster_parenting = true
self.handle_in_body(Token::StartTag(tag, attrs, self_closing))
self.foster_parenting = false
}
}
Token::EndTag(tag) =>
match tag {
"table" => {
if !self.has_element_in_table_scope("table") {
return // Parse error, ignore
}
self.pop_until("table")
self.reset_insertion_mode()
}
"body"
| "caption"
| "col"
| "colgroup"
| "html"
| "tbody"
| "td"
| "tfoot"
| "th"
| "thead"
| "tr" => () // Parse error, ignore
"template" => self.handle_in_head(Token::EndTag(tag))
_ => {
// Foster parent
self.foster_parenting = true
self.handle_in_body(Token::EndTag(tag))
self.foster_parenting = false
}
}
Token::EOF => self.handle_in_body(token)
}
}
///|
/// Handle in table text
fn TreeBuilder::handle_in_table_text(self : TreeBuilder, token : Token) -> Unit {
match token {
Token::Character(c) => self.pending_table_chars.push(c)
Token::Characters(s) =>
for c in s {
self.pending_table_chars.push(c)
}
_ => {
// Flush pending table characters
let mut has_non_space = false
for c in self.pending_table_chars {
if !tok_is_whitespace(c) {
has_non_space = true
break
}
}
if has_non_space {
// Foster parent all pending chars
self.foster_parenting = true
for c in self.pending_table_chars {
self.insert_character(c)
}
self.foster_parenting = false
} else {
// Insert as regular characters
for c in self.pending_table_chars {
self.insert_character(c)
}
}
self.pending_table_chars = []
match self.original_insertion_mode {
Some(mode) => self.insertion_mode = mode
None => self.insertion_mode = InsertionMode::InTable
}
self.reprocess_token(token)
}
}
}
///|
/// Handle in table body
fn TreeBuilder::handle_in_table_body(self : TreeBuilder, token : Token) -> Unit {
match token {
Token::StartTag(tag, attrs, self_closing) =>
match tag {
"tr" => {
self.clear_stack_to_table_body_context()
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.insertion_mode = InsertionMode::InRow
}
"th" | "td" => {
self.clear_stack_to_table_body_context()
let elem = create_element("tr", {})
self.insert_element(elem)
self.insertion_mode = InsertionMode::InRow
self.reprocess_token(Token::StartTag(tag, attrs, self_closing))
}
"caption" | "col" | "colgroup" | "tbody" | "tfoot" | "thead" => {
if !self.has_element_in_table_scope("tbody") &&
!self.has_element_in_table_scope("thead") &&
!self.has_element_in_table_scope("tfoot") {
return // Parse error, ignore
}
self.clear_stack_to_table_body_context()
let _ = self.open_elements.pop() // Pop tbody/thead/tfoot
self.insertion_mode = InsertionMode::InTable
self.reprocess_token(Token::StartTag(tag, attrs, self_closing))
}
_ => self.handle_in_table(Token::StartTag(tag, attrs, self_closing))
}
Token::EndTag(tag) =>
match tag {
"tbody" | "tfoot" | "thead" => {
if !self.has_element_in_table_scope(tag) {
return // Parse error, ignore
}
self.clear_stack_to_table_body_context()
let _ = self.open_elements.pop()
self.insertion_mode = InsertionMode::InTable
}
"table" => {
if !self.has_element_in_table_scope("tbody") &&
!self.has_element_in_table_scope("thead") &&
!self.has_element_in_table_scope("tfoot") {
return // Parse error, ignore
}
self.clear_stack_to_table_body_context()
let _ = self.open_elements.pop()
self.insertion_mode = InsertionMode::InTable
self.reprocess_token(Token::EndTag(tag))
}
"body" | "caption" | "col" | "colgroup" | "html" | "td" | "th" | "tr" =>
() // Parse error, ignore
_ => self.handle_in_table(Token::EndTag(tag))
}
_ => self.handle_in_table(token)
}
}
///|
/// Handle in row
fn TreeBuilder::handle_in_row(self : TreeBuilder, token : Token) -> Unit {
match token {
Token::StartTag(tag, attrs, self_closing) =>
match tag {
"th" | "td" => {
self.clear_stack_to_table_row_context()
let elem = create_element(tag, attrs)
self.insert_element(elem)
self.insertion_mode = InsertionMode::InCell
self.active_formatting.push(FormattingElement::Marker)
}
"caption" | "col" | "colgroup" | "tbody" | "tfoot" | "thead" | "tr" => {
if !self.has_element_in_table_scope("tr") {
return // Parse error, ignore
}
self.clear_stack_to_table_row_context()
let _ = self.open_elements.pop() // Pop tr
self.insertion_mode = InsertionMode::InTableBody
self.reprocess_token(Token::StartTag(tag, attrs, self_closing))
}
_ => self.handle_in_table(Token::StartTag(tag, attrs, self_closing))
}
Token::EndTag(tag) =>
match tag {
"tr" => {
if !self.has_element_in_table_scope("tr") {
return // Parse error, ignore
}
self.clear_stack_to_table_row_context()
let _ = self.open_elements.pop()
self.insertion_mode = InsertionMode::InTableBody
}
"table" => {
if !self.has_element_in_table_scope("tr") {
return // Parse error, ignore
}
self.clear_stack_to_table_row_context()
let _ = self.open_elements.pop()
self.insertion_mode = InsertionMode::InTableBody
self.reprocess_token(Token::EndTag(tag))
}
"tbody" | "tfoot" | "thead" => {
if !self.has_element_in_table_scope(tag) {
return // Parse error, ignore
}
if !self.has_element_in_table_scope("tr") {
return // Ignore
}
self.clear_stack_to_table_row_context()
let _ = self.open_elements.pop()
self.insertion_mode = InsertionMode::InTableBody
self.reprocess_token(Token::EndTag(tag))
}
"body" | "caption" | "col" | "colgroup" | "html" | "td" | "th" => () // Parse error, ignore
_ => self.handle_in_table(Token::EndTag(tag))
}
_ => self.handle_in_table(token)
}
}
///|
fn is_cell_closing_start_tag(tag : String) -> Bool {
match tag {
"caption"
| "col"
| "colgroup"
| "tbody"
| "td"
| "tfoot"
| "th"
| "thead"
| "tr" => true
_ => false
}
}
///|
fn is_cell_ignored_end_tag(tag : String) -> Bool {
match tag {
"body" | "caption" | "col" | "colgroup" | "html" => true
_ => false
}
}
///|
fn is_cell_scope_end_tag(tag : String) -> Bool {
match tag {
"table" | "tbody" | "tfoot" | "thead" | "tr" => true
_ => false
}
}
///|
/// Handle in cell
fn TreeBuilder::handle_in_cell(self : TreeBuilder, token : Token) -> Unit {
match token {
Token::EndTag(tag) if tag == "td" || tag == "th" => {
if !self.has_element_in_table_scope(tag) {
return // Parse error, ignore
}
self.generate_implied_end_tags()
self.pop_until(tag)
self.clear_active_formatting_to_marker()
self.insertion_mode = InsertionMode::InRow
}
Token::StartTag(tag, attrs, self_closing) if is_cell_closing_start_tag(tag) => {
if !self.has_element_in_table_scope("td") &&
!self.has_element_in_table_scope("th") {
return // Parse error, ignore
}
self.close_cell()
self.reprocess_token(Token::StartTag(tag, attrs, self_closing))
}
Token::EndTag(tag) if is_cell_ignored_end_tag(tag) => () // Parse error, ignore
Token::EndTag(tag) if is_cell_scope_end_tag(tag) => {
if !self.has_element_in_table_scope(tag) {
return // Parse error, ignore
}
self.close_cell()
self.reprocess_token(Token::EndTag(tag))
}
_ => self.handle_in_body(token)
}
}
///|
fn is_caption_closing_start_tag(tag : String) -> Bool {
match tag {
"caption"
| "col"
| "colgroup"
| "tbody"
| "td"
| "tfoot"
| "th"
| "thead"
| "tr" => true
_ => false
}
}
///|
fn is_caption_ignored_end_tag(tag : String) -> Bool {
match tag {
"body"
| "col"
| "colgroup"
| "html"
| "tbody"
| "td"
| "tfoot"
| "th"
| "thead"
| "tr" => true
_ => false
}
}
///|
/// Handle in caption
fn TreeBuilder::handle_in_caption(self : TreeBuilder, token : Token) -> Unit {
match token {
Token::EndTag(tag) if tag == "caption" => {
if !self.has_element_in_table_scope("caption") {
return // Parse error, ignore
}
self.generate_implied_end_tags()
self.pop_until("caption")
self.clear_active_formatting_to_marker()
self.insertion_mode = InsertionMode::InTable
}
Token::StartTag(tag, attrs, self_closing) if is_caption_closing_start_tag(
tag,
) => {
if !self.has_element_in_table_scope("caption") {
return // Parse error, ignore
}
self.generate_implied_end_tags()
self.pop_until("caption")
self.clear_active_formatting_to_marker()
self.insertion_mode = InsertionMode::InTable
self.reprocess_token(Token::StartTag(tag, attrs, self_closing))
}
Token::EndTag(tag) if tag == "table" => {
if !self.has_element_in_table_scope("caption") {
return // Parse error, ignore
}
self.generate_implied_end_tags()
self.pop_until("caption")
self.clear_active_formatting_to_marker()
self.insertion_mode = InsertionMode::InTable
self.reprocess_token(Token::EndTag(tag))
}
Token::EndTag(tag) if is_caption_ignored_end_tag(tag) => () // Parse error, ignore
_ => self.handle_in_body(token)
}
}
///|
/// Handle in column group
fn TreeBuilder::handle_in_column_group(
self : TreeBuilder,
token : Token,
) -> Unit {
match token {
Token::Character(c) if tok_is_whitespace(c) => self.insert_character(c)
Token::Characters(s) if is_whitespace_only(s) => self.insert_characters(s)
Token::Comment(_) => () // Ignore
Token::Doctype(_) => () // Parse error, ignore
Token::StartTag(tag, attrs, _) if tag == "html" =>
self.handle_in_body(Token::StartTag(tag, attrs, false))
Token::StartTag(tag, attrs, _) if tag == "col" => {
let elem = create_element(tag, attrs)
self.insert_element(elem)
let _ = self.open_elements.pop()
// Void element
}
Token::StartTag(tag, attrs, self_closing) if tag == "template" =>
self.handle_in_head(Token::StartTag(tag, attrs, self_closing))
Token::EndTag(tag) if tag == "colgroup" => {
match self.current_node() {
Some(e) if e.tag != "colgroup" => return // Parse error, ignore
_ => ()
}
let _ = self.open_elements.pop()
self.insertion_mode = InsertionMode::InTable
}
Token::EndTag(tag) if tag == "col" => () // Parse error, ignore
Token::EndTag(tag) if tag == "template" =>
self.handle_in_head(Token::EndTag(tag))
Token::EOF => self.handle_in_body(token)
_ => {
match self.current_node() {
Some(e) if e.tag != "colgroup" => return // Parse error, ignore
_ => ()
}
let _ = self.open_elements.pop()
self.insertion_mode = InsertionMode::InTable
self.reprocess_token(token)
}
}
}
///|
/// Handle after body
fn TreeBuilder::handle_after_body(self : TreeBuilder, token : Token) -> Unit {
match token {
Token::Character(c) if tok_is_whitespace(c) =>
self.handle_in_body(Token::Character(c))
Token::Characters(s) if is_whitespace_only(s) =>
self.handle_in_body(Token::Characters(s))
Token::Comment(_) => () // Ignore
Token::Doctype(_) => () // Parse error, ignore
Token::StartTag(tag, _, _) if tag == "html" => self.handle_in_body(token)
Token::EndTag(tag) if tag == "html" =>
self.insertion_mode = InsertionMode::AfterAfterBody
Token::EOF => () // Stop
_ => {
self.insertion_mode = InsertionMode::InBody
self.reprocess_token(token)
}
}
}
///|
/// Helper: check if p is in button scope
fn TreeBuilder::has_element_in_button_scope(
self : TreeBuilder,
tag : String,
) -> Bool {
for i = self.open_elements.length() - 1; i >= 0; i = i - 1 {
let elem = self.open_elements[i]
if elem.tag == tag {
return true
}
if is_scope_boundary(elem.tag) || elem.tag == "button" {
return false
}
}
false
}
///|
/// Helper: check if element is in list scope
fn TreeBuilder::has_element_in_list_scope(
self : TreeBuilder,
tag : String,
) -> Bool {
for i = self.open_elements.length() - 1; i >= 0; i = i - 1 {
let elem = self.open_elements[i]
if elem.tag == tag {
return true
}
if is_scope_boundary(elem.tag) || elem.tag == "ol" || elem.tag == "ul" {
return false
}
}
false
}
///|
/// Helper: check if any heading is in scope
fn TreeBuilder::has_heading_in_scope(self : TreeBuilder) -> Bool {
for i = self.open_elements.length() - 1; i >= 0; i = i - 1 {
let elem = self.open_elements[i]
match elem.tag {
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => return true
_ => ()
}
if is_scope_boundary(elem.tag) {
return false
}
}
false
}
///|
/// Helper: close p element
fn TreeBuilder::close_p_element(self : TreeBuilder) -> Unit {
self.generate_implied_end_tags_except("p")
self.pop_until("p")
}
///|
/// Helper: close li if needed
fn TreeBuilder::close_li_if_needed(self : TreeBuilder) -> Unit {
for i = self.open_elements.length() - 1; i >= 0; i = i - 1 {
let elem = self.open_elements[i]
if elem.tag == "li" {
self.generate_implied_end_tags_except("li")
self.pop_until("li")
return
}
if is_special_element(elem.tag) &&
elem.tag != "address" &&
elem.tag != "div" &&
elem.tag != "p" {
return
}
}
}
///|
/// Helper: close dd/dt if needed
fn TreeBuilder::close_dd_dt_if_needed(self : TreeBuilder) -> Unit {
for i = self.open_elements.length() - 1; i >= 0; i = i - 1 {
let elem = self.open_elements[i]
if elem.tag == "dd" || elem.tag == "dt" {
self.generate_implied_end_tags_except(elem.tag)
self.pop_until(elem.tag)
return
}
if is_special_element(elem.tag) &&
elem.tag != "address" &&
elem.tag != "div" &&
elem.tag != "p" {
return
}
}
}
///|
/// Helper: remove element from active formatting
fn TreeBuilder::remove_from_active_formatting(
self : TreeBuilder,
tag : String,
) -> Unit {
for i = self.active_formatting.length() - 1; i >= 0; i = i - 1 {
match self.active_formatting[i] {
FormattingElement::Element(e) if e.tag == tag => {
let _ = self.active_formatting.remove(i)
return
}
_ => ()
}
}
}
///|
fn TreeBuilder::has_active_formatting_element(
self : TreeBuilder,
tag : String,
) -> Bool {
for i = self.active_formatting.length() - 1; i >= 0; i = i - 1 {
match self.active_formatting[i] {
FormattingElement::Element(e) if e.tag == tag => return true
FormattingElement::Marker => return false
_ => ()
}
}
false
}
///|
fn TreeBuilder::has_open_element_match(
self : TreeBuilder,
element : Element,
) -> Bool {
for open in self.open_elements {
if same_tree_builder_element(open, element) {
return true
}
}
false
}
///|
fn TreeBuilder::reconstruct_active_formatting_elements(
self : TreeBuilder,
) -> Unit {
if self.active_formatting.is_empty() {
return
}
let mut entry_index = self.active_formatting.length() - 1
match self.active_formatting[entry_index] {
FormattingElement::Marker => return
FormattingElement::Element(e) if self.has_open_element_match(e) => return
_ => ()
}
while entry_index > 0 {
entry_index = entry_index - 1
match self.active_formatting[entry_index] {
FormattingElement::Marker => {
entry_index = entry_index + 1
break
}
FormattingElement::Element(e) if self.has_open_element_match(e) => {
entry_index = entry_index + 1
break
}
_ => ()
}
}
while entry_index < self.active_formatting.length() {
match self.active_formatting[entry_index] {
FormattingElement::Element(e) => {
let reconstructed = clone_element_without_children(e)
self.insert_element(reconstructed)
self.active_formatting[entry_index] = FormattingElement::Element(
reconstructed,
)
}
FormattingElement::Marker => ()
}
entry_index = entry_index + 1
}
}
///|
fn TreeBuilder::find_open_element_index(
self : TreeBuilder,
tag : String,
) -> Int {
for i = self.open_elements.length() - 1; i >= 0; i = i - 1 {
if self.open_elements[i].tag == tag {
return i
}
}
-1
}
///|
fn TreeBuilder::find_furthest_special_after(
self : TreeBuilder,
stack_index : Int,
) -> Int {
for i = stack_index + 1; i < self.open_elements.length(); i = i + 1 {
if is_special_element(self.open_elements[i].tag) {
return i
}
}
-1
}
///|
fn same_tree_builder_element(a : Element, b : Element) -> Bool {
a.tag == b.tag && a.id == b.id
}
///|
fn remove_last_matching_child(parent : Element, target : Element) -> Unit {
for i = parent.children.length() - 1; i >= 0; i = i - 1 {
match parent.children[i] {
Node::Element(e) if same_tree_builder_element(e, target) => {
let _ = parent.children.remove(i)
return
}
_ => ()
}
}
}
///|
fn insert_after_matching_child(
parent : Element,
anchor : Element,
child : Element,
) -> Unit {
for i = parent.children.length() - 1; i >= 0; i = i - 1 {
match parent.children[i] {
Node::Element(e) if same_tree_builder_element(e, anchor) => {
parent.children.insert(i + 1, Node::Element(child))
return
}
_ => ()
}
}
parent.children.push(Node::Element(child))
}
///|
fn clone_element_without_children(element : Element) -> Element {
{
tag: element.tag,
id: element.id,
classes: element.classes,
style: element.style,
attributes: element.attributes,
children: [],
}
}
///|
fn wrap_children_in_formatting_clone(
block : Element,
formatting : Element,
) -> Unit {
let reconstructed = clone_element_without_children(formatting)
for child in block.children {
reconstructed.children.push(child)
}
block.children.clear()
block.children.push(Node::Element(reconstructed))
}
///|
fn TreeBuilder::reparent_formatting_around_block(
self : TreeBuilder,
formatting_index : Int,
block_index : Int,
) -> Unit {
if formatting_index <= 0 ||
block_index <= formatting_index ||
block_index >= self.open_elements.length() {
return
}
let common_ancestor = self.open_elements[formatting_index - 1]
let formatting = self.open_elements[formatting_index]
let block = self.open_elements[block_index]
let block_parent = self.open_elements[block_index - 1]
remove_last_matching_child(block_parent, block)
wrap_children_in_formatting_clone(block, formatting)
insert_after_matching_child(common_ancestor, formatting, block)
let _ = self.open_elements.remove(formatting_index)
}
///|
fn TreeBuilder::run_adoption_agency(self : TreeBuilder, tag : String) -> Unit {
let mut attempt = 0
while attempt < 8 {
attempt = attempt + 1
let formatting_index = self.find_open_element_index(tag)
if formatting_index < 0 {
self.any_other_end_tag(tag)
return
}
if !self.has_element_in_scope(tag) {
self.remove_from_active_formatting(tag)
return
}
match self.current_node() {
Some(e) if e.tag == tag => {
let _ = self.open_elements.pop()
self.remove_from_active_formatting(tag)
return
}
_ => ()
}
let furthest_block_index = self.find_furthest_special_after(
formatting_index,
)
if furthest_block_index < 0 {
self.pop_until(tag)
self.remove_from_active_formatting(tag)
return
}
self.reparent_formatting_around_block(
formatting_index, furthest_block_index,
)
self.remove_from_active_formatting(tag)
return
}
}
///|
/// Helper: clear stack to table context
fn TreeBuilder::clear_stack_to_table_context(self : TreeBuilder) -> Unit {
while !self.open_elements.is_empty() {
match self.current_node() {
Some(e) if e.tag == "table" || e.tag == "template" || e.tag == "html" =>
return
_ => {
let _ = self.open_elements.pop()
}
}
}
}
///|
fn is_table_body_context(tag : String) -> Bool {
match tag {
"tbody" | "tfoot" | "thead" | "template" | "html" => true
_ => false
}
}
///|
/// Helper: clear stack to table body context
fn TreeBuilder::clear_stack_to_table_body_context(self : TreeBuilder) -> Unit {
while !self.open_elements.is_empty() {
match self.current_node() {
Some(e) if is_table_body_context(e.tag) => return
_ => {
let _ = self.open_elements.pop()
}
}
}
}
///|
/// Helper: clear stack to table row context
fn TreeBuilder::clear_stack_to_table_row_context(self : TreeBuilder) -> Unit {
while !self.open_elements.is_empty() {
match self.current_node() {
Some(e) if e.tag == "tr" || e.tag == "template" || e.tag == "html" =>
return
_ => {
let _ = self.open_elements.pop()
}
}
}
}
///|
/// Helper: clear active formatting to marker
fn TreeBuilder::clear_active_formatting_to_marker(self : TreeBuilder) -> Unit {
while !self.active_formatting.is_empty() {
match self.active_formatting.pop() {
Some(FormattingElement::Marker) => return
_ => ()
}
}
}
///|
/// Helper: close cell
fn TreeBuilder::close_cell(self : TreeBuilder) -> Unit {
// Preserve pending text (e.g. ) inside the current cell
// before implicit td/th closing via table insertion modes.
self.flush_pending_text()
self.generate_implied_end_tags()
// Pop until td or th
while !self.open_elements.is_empty() {
match self.current_node() {
Some(e) if e.tag == "td" || e.tag == "th" => {
let _ = self.open_elements.pop()
break
}
_ => {
let _ = self.open_elements.pop()
}
}
}
self.clear_active_formatting_to_marker()
self.insertion_mode = InsertionMode::InRow
}
///|
/// Helper: reset insertion mode appropriately
fn TreeBuilder::reset_insertion_mode(self : TreeBuilder) -> Unit {
for i = self.open_elements.length() - 1; i >= 0; i = i - 1 {
let node = self.open_elements[i]
let last = i == 0
match node.tag {
"select" => {
self.insertion_mode = InsertionMode::InSelect
return
}
"td" | "th" if !last => {
self.insertion_mode = InsertionMode::InCell
return
}
"tr" => {
self.insertion_mode = InsertionMode::InRow
return
}
"tbody" | "thead" | "tfoot" => {
self.insertion_mode = InsertionMode::InTableBody
return
}
"caption" => {
self.insertion_mode = InsertionMode::InCaption
return
}
"colgroup" => {
self.insertion_mode = InsertionMode::InColumnGroup
return
}
"table" => {
self.insertion_mode = InsertionMode::InTable
return
}
"template" => {
// Use the current template insertion mode (top of the stack); default
// to InTemplate when, defensively, the stack is empty.
self.insertion_mode = if self.template_insertion_modes.length() > 0 {
self.template_insertion_modes[self.template_insertion_modes.length() -
1]
} else {
InsertionMode::InTemplate
}
return
}
"head" | "body" => {
self.insertion_mode = InsertionMode::InBody
return
}
"frameset" => {
self.insertion_mode = InsertionMode::InFrameset
return
}
"html" => {
self.insertion_mode = InsertionMode::BeforeHead
return
}
_ => ()
}
if last {
self.insertion_mode = InsertionMode::InBody
return
}
}
}