// 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.
// ============================================================================
// Adoption Agency Algorithm (WHATWG 13.2.6.4.7)
// ============================================================================
///|
/// Run the adoption agency algorithm for a formatting element
fn TreeBuilder::run_adoption_agency(
self : TreeBuilder,
tag_name : String,
) -> Unit {
// Outer loop counter
let mut outer_loop = 0
while outer_loop < 8 {
outer_loop += 1
// 1. Find formatting element
let formatting_idx = self.find_formatting_element_index(tag_name)
if formatting_idx == -1 {
// No formatting element, process as "any other end tag"
self.process_any_other_end_tag(tag_name)
return
}
let formatting_entry = match self.get_formatting_entry(formatting_idx) {
Some(entry) => entry
None => return
}
let formatting_node_id = match formatting_entry {
Element(node_id~, ..) => node_id
Marker => return
}
// 2. Check if formatting element is in stack
if !self.is_in_open_elements(formatting_node_id) {
self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
self.remove_from_active_formatting(formatting_node_id)
return
}
// 3. Check if formatting element is in scope
if !self.in_default_scope(tag_name) {
self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
return
}
// 4. Check if formatting element is current node
if self.current_node().node_id != formatting_node_id {
self.emit_error(UnexpectedCharacterInUnquotedAttributeValue)
}
// 5. Find furthest block
let furthest_block_idx = self.find_furthest_block(formatting_idx)
if furthest_block_idx == -1 {
// No furthest block - pop elements until formatting element
while self.open_elements.length() > 0 {
let entry = self.pop_element()
if entry.node_id == formatting_node_id {
break
}
}
self.remove_from_active_formatting(formatting_node_id)
return
}
// Store furthest block node_id (index becomes stale after removals)
let furthest_block_id = self.open_elements[furthest_block_idx].node_id
// 6. Get common ancestor
let common_ancestor_idx = self.find_stack_position(formatting_node_id)
if common_ancestor_idx == -1 || common_ancestor_idx == 0 {
return
}
let common_ancestor = self.open_elements[common_ancestor_idx - 1].node_id
// 7. Bookmark is the position of formatting element in the formatting list
let mut bookmark = formatting_idx
// 8. Variables for inner loop
let mut node_idx = furthest_block_idx
let mut last_node_id = furthest_block_id
// 9. Inner loop - counter limit of 3
let mut inner_loop = 0
while true {
// Increment counter first
inner_loop += 1
// Check if > 3, break
if inner_loop > 3 {
break
}
// 9.1 Move back in stack
node_idx -= 1
if node_idx < 0 {
break
}
let node = self.open_elements[node_idx]
// 9.2 Check if node is formatting element
if node.node_id == formatting_node_id {
break
}
// 9.3 Check if node is in formatting list
let node_formatting_idx = self.find_formatting_node_index(node.node_id)
if node_formatting_idx == -1 {
// Remove from stack
let _ = self.open_elements.remove(node_idx)
continue
}
// 9.4 Create replacement element
let replacement_id = self.create_element_for_formatting(
node_formatting_idx,
)
// 9.5 Replace in both lists
self.open_elements[node_idx] = {
node_id: replacement_id,
tag_name: node.tag_name,
ns: node.ns,
}
self.replace_formatting_node(node_formatting_idx, replacement_id)
// 9.6 Check if last node is furthest block
if last_node_id == furthest_block_id {
bookmark = node_formatting_idx + 1
}
// 9.7 Move last node into replacement
let parent = self.document.get_parent(last_node_id)
if parent != NO_NODE {
self.document.remove_child(parent, last_node_id)
}
self.document.append_child(replacement_id, last_node_id)
// 9.8 Set last node to current node
last_node_id = replacement_id
}
// 10. Insert last node into common ancestor
let parent = self.document.get_parent(last_node_id)
if parent != NO_NODE {
self.document.remove_child(parent, last_node_id)
}
self.insert_after_appropriate_place(common_ancestor, last_node_id)
// 11. Create element for formatting element
let new_formatting_id = self.create_element_for_formatting(formatting_idx)
// 12. Move all children of furthest block into new element
let children = self.document.get_children(furthest_block_id)
for child_id in children {
self.document.remove_child(furthest_block_id, child_id)
self.document.append_child(new_formatting_id, child_id)
}
// 13. Append new element to furthest block
self.document.append_child(furthest_block_id, new_formatting_id)
// 14. Remove old formatting element from list and insert new one
// Adjust bookmark if it's after the removed element
if formatting_idx < bookmark {
bookmark -= 1
}
self.remove_from_active_formatting(formatting_node_id)
self.insert_formatting_at(bookmark, new_formatting_id, tag_name)
// 15. Remove old formatting element from stack and insert new one
self.remove_from_stack(formatting_node_id)
self.insert_in_stack_after(furthest_block_id, new_formatting_id, tag_name)
}
}
///|
/// Find position of a node in the stack
fn TreeBuilder::find_stack_position(self : TreeBuilder, node_id : Int) -> Int {
for i, entry in self.open_elements {
if entry.node_id == node_id {
return i
}
}
-1
}
///|
/// Find formatting node index by node id
fn TreeBuilder::find_formatting_node_index(
self : TreeBuilder,
node_id : Int,
) -> Int {
for i = self.active_formatting.length() - 1; i >= 0; i = i - 1 {
match self.active_formatting[i] {
Marker => ()
Element(node_id=entry_id, ..) => if entry_id == node_id { return i }
}
}
-1
}
///|
/// Create element for a formatting entry (without inserting it)
fn TreeBuilder::create_element_for_formatting(
self : TreeBuilder,
formatting_idx : Int,
) -> Int {
let entry = self.active_formatting[formatting_idx]
match entry {
Element(tag_name~, attributes~, ..) =>
self.create_element_for_token(tag_name, attributes, HTML)
Marker => NO_NODE
}
}
///|
/// Replace node in formatting list
fn TreeBuilder::replace_formatting_node(
self : TreeBuilder,
idx : Int,
new_node_id : Int,
) -> Unit {
match self.active_formatting[idx] {
Element(tag_name~, attributes~, ..) =>
self.active_formatting[idx] = Element(
node_id=new_node_id,
tag_name~,
attributes~,
)
Marker => ()
}
}
///|
/// Insert into active formatting at specific position
fn TreeBuilder::insert_formatting_at(
self : TreeBuilder,
idx : Int,
node_id : Int,
tag_name : String,
) -> Unit {
let attrs = self.document
.get_element(node_id)
.map(fn(elem) { elem.attributes })
.unwrap_or([])
self.active_formatting.insert(
idx,
Element(node_id~, tag_name~, attributes=attrs),
)
}
///|
/// Insert in stack after a specific node
fn TreeBuilder::insert_in_stack_after(
self : TreeBuilder,
after_node : Int,
node_id : Int,
tag_name : String,
) -> Unit {
let mut insert_idx = self.open_elements.length()
for i, entry in self.open_elements {
if entry.node_id == after_node {
insert_idx = i + 1
break
}
}
self.open_elements.insert(insert_idx, { node_id, tag_name, ns: HTML, })
}
///|
/// Insert after the appropriate place in the DOM
/// The parent_id is the override target - we check if IT is table-related
fn TreeBuilder::insert_after_appropriate_place(
self : TreeBuilder,
parent_id : Int,
node_id : Int,
) -> Unit {
// Check if the target (parent_id) is a table-related element
if self.foster_parenting && self.is_table_element(parent_id) {
self.insert_node_foster_parenting(node_id)
} else {
// If parent is a template, insert into template content
let actual_parent = match self.document.get_tag_name(parent_id) {
Some("template") => {
let content = self.document.get_template_content(parent_id)
if content != NO_NODE {
content
} else {
parent_id
}
}
_ => parent_id
}
self.document.append_child(actual_parent, node_id)
}
}
///|
/// Check if a node is a table-related element that requires foster parenting
fn TreeBuilder::is_table_element(self : TreeBuilder, node_id : Int) -> Bool {
match self.document.get_tag_name(node_id) {
Some(tag) =>
tag == "table" ||
tag == "tbody" ||
tag == "tfoot" ||
tag == "thead" ||
tag == "tr"
None => false
}
}