///|
/// Core DOM Primitives
///
/// These are the fundamental DOM operations that all higher layers use.
/// No serialization, direct function calls only.
///
/// Design principles:
/// 1. Each operation is atomic and stateless
/// 2. No implicit side effects
/// 3. Suitable for WASM export
/// 4. Can be batched by higher layers
///|
/// DOM Tree - the core document structure
/// This is the single source of truth for all DOM operations
pub struct DomTree {
/// Next available node ID
mut next_id : Int
/// All nodes indexed by ID
nodes : Map[Int, DomNode]
/// Root document node ID
mut root_id : Int
/// Currently focused node (for input)
mut focused_node : Int?
/// Mutation queue for batching changes
mutations : MutationQueue
}
///|
/// Internal DOM node representation
struct DomNode {
mut node_type : NodeType
mut tag_name : String
mut text_content : String
attributes : Map[String, String]
mut custom_states : Array[String]
mut form_value : String?
mut form_state_value : String?
mut parent_id : Int?
children : Array[Int]
mut shadow_root_id : Int?
mut host_id : Int?
mut shadow_mode : String?
mut shadow_delegates_focus : Bool
mut shadow_slot_assignment : String
mut shadow_clonable : Bool
mut shadow_serializable : Bool
/// For elements: computed style cache
mut cached_rect : Rect?
/// Cached selector projection of this node's class list and attribute array,
/// computed lazily and invalidated when attributes change. Lets querySelector
/// reuse them across queries instead of re-splitting / rebuilding per query.
mut sel_classes : Array[String]?
mut sel_attrs : Array[@selector.Attribute]?
}
///|
fn DomNode::new(
node_type : NodeType,
tag_name : String,
text_content? : String = "",
) -> DomNode {
{
node_type,
tag_name,
text_content,
attributes: {},
custom_states: [],
form_value: None,
form_state_value: None,
parent_id: None,
children: [],
shadow_root_id: None,
host_id: None,
shadow_mode: None,
shadow_delegates_focus: false,
shadow_slot_assignment: "named",
shadow_clonable: false,
shadow_serializable: false,
cached_rect: None,
sel_classes: None,
sel_attrs: None,
}
}
///|
fn DomNode::new_document_fragment() -> DomNode {
DomNode::new(DocumentFragment, "#document-fragment")
}
///|
/// Create a new empty DOM tree
pub fn DomTree::new() -> DomTree {
let tree : DomTree = {
next_id: 1,
nodes: {},
root_id: 0,
focused_node: None,
mutations: MutationQueue::new(),
}
tree.nodes[0] = DomNode::new(Document, "#document")
tree.root_id = 0
tree
}
// =============================================================================
// Node Creation
// =============================================================================
///|
/// Create an element node
pub fn DomTree::create_element(self : DomTree, tag_name : String) -> NodeId {
self.allocate_node(DomNode::new(Element, tag_name))
}
///|
/// Create a text node
pub fn DomTree::create_text(self : DomTree, content : String) -> NodeId {
self.allocate_node(DomNode::new(Text, "#text", text_content=content))
}
///|
/// Create a comment node
pub fn DomTree::create_comment(self : DomTree, content : String) -> NodeId {
self.allocate_node(DomNode::new(Comment, "#comment", text_content=content))
}
///|
pub fn DomTree::create_runtime_node(
self : DomTree,
id : Int,
node_type : NodeType,
tag_name? : String = "",
text_content? : String = "",
) -> Result[NodeId, CoreError] {
let node = match node_type {
Element => DomNode::new(Element, tag_name)
Text => DomNode::new(Text, "#text", text_content~)
Comment => DomNode::new(Comment, "#comment", text_content~)
DocumentFragment => DomNode::new_document_fragment()
_ => return Err(InvalidOperation(message="unsupported runtime node type"))
}
self.allocate_node_with_id(id, node)
}
///|
fn DomTree::allocate_node(self : DomTree, node : DomNode) -> NodeId {
let id = self.next_id
self.next_id += 1
self.nodes[id] = node
NodeId(id)
}
///|
fn DomTree::allocate_node_with_id(
self : DomTree,
id : Int,
node : DomNode,
) -> Result[NodeId, CoreError] {
if id <= 0 {
return Err(InvalidOperation(message="node id must be positive"))
}
if self.nodes.contains(id) {
return Err(InvalidOperation(message="node id already exists"))
}
if id >= self.next_id {
self.next_id = id + 1
}
self.nodes[id] = node
Ok(NodeId(id))
}
// =============================================================================
// Tree Manipulation
// =============================================================================
///|
/// Append child to parent
pub fn DomTree::append_child(
self : DomTree,
parent : NodeId,
child : NodeId,
) -> Result[Unit, CoreError] {
let parent_id = parent.to_int()
let child_id = child.to_int()
match (self.nodes.get(parent_id), self.nodes.get(child_id)) {
(Some(p), Some(c)) => {
// Remove from old parent if any
match c.parent_id {
Some(old_parent_id) =>
match self.nodes.get(old_parent_id) {
Some(old_parent) => {
let idx = old_parent.children.search(child_id)
match idx {
Some(i) => {
let _ = old_parent.children.remove(i)
// Record removal from old parent
self.mutations.record_child_removed(
NodeId(old_parent_id),
child,
)
}
None => ()
}
}
None => ()
}
None => ()
}
// Set new parent
c.parent_id = Some(parent_id)
// Add to new parent's children
p.children.push(child_id)
// Record mutation
self.mutations.record_child_added(parent, child)
// Invalidate layout cache
self.invalidate_layout(parent_id)
Ok(())
}
(None, _) => Err(NodeNotFound(node_id=parent))
(_, None) => Err(NodeNotFound(node_id=child))
}
}
///|
/// Insert child before reference node
pub fn DomTree::insert_before(
self : DomTree,
parent : NodeId,
child : NodeId,
reference : NodeId?,
) -> Result[Unit, CoreError] {
let parent_id = parent.to_int()
let child_id = child.to_int()
match self.nodes.get(parent_id) {
None => return Err(NodeNotFound(node_id=parent))
Some(p) =>
match self.nodes.get(child_id) {
None => return Err(NodeNotFound(node_id=child))
Some(c) => {
// Remove from old parent
match c.parent_id {
Some(old_parent_id) =>
match self.nodes.get(old_parent_id) {
Some(old_parent) => {
let idx = old_parent.children.search(child_id)
match idx {
Some(i) => {
let _ = old_parent.children.remove(i)
// Record removal from old parent
self.mutations.record_child_removed(
NodeId(old_parent_id),
child,
)
}
None => ()
}
}
None => ()
}
None => ()
}
// Set new parent
c.parent_id = Some(parent_id)
// Insert at position
match reference {
Some(ref_node) => {
let ref_id = ref_node.to_int()
let idx = p.children.search(ref_id)
match idx {
Some(i) => p.children.insert(i, child_id)
None => p.children.push(child_id) // Reference not found, append
}
}
None => p.children.push(child_id) // No reference, append
}
// Record mutation
self.mutations.record_child_added(parent, child)
self.invalidate_layout(parent_id)
Ok(())
}
}
}
}
///|
/// Remove child from parent
pub fn DomTree::remove_child(
self : DomTree,
parent : NodeId,
child : NodeId,
) -> Result[Unit, CoreError] {
let parent_id = parent.to_int()
let child_id = child.to_int()
match (self.nodes.get(parent_id), self.nodes.get(child_id)) {
(Some(p), Some(c)) => {
let idx = p.children.search(child_id)
match idx {
Some(i) => {
let _ = p.children.remove(i)
c.parent_id = None
// Record mutation
self.mutations.record_child_removed(parent, child)
self.invalidate_layout(parent_id)
Ok(())
}
None => Err(InvalidOperation(message="Child not found in parent"))
}
}
(None, _) => Err(NodeNotFound(node_id=parent))
(_, None) => Err(NodeNotFound(node_id=child))
}
}
// =============================================================================
// Attribute Operations
// =============================================================================
///|
/// Set attribute on element
pub fn DomTree::set_attribute(
self : DomTree,
node : NodeId,
name : String,
value : String,
) -> Result[Unit, CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => {
let old_value = n.attributes.get(name)
n.attributes[name] = value
n.sel_classes = None
n.sel_attrs = None
// Record mutation
self.mutations.record_attribute(
node,
name,
old_value~,
new_value=Some(value),
)
self.invalidate_layout(id)
Ok(())
}
None => Err(NodeNotFound(node_id=node))
}
}
///|
/// Get attribute from element
pub fn DomTree::get_attribute(
self : DomTree,
node : NodeId,
name : String,
) -> Result[String?, CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => Ok(n.attributes.get(name))
None => Err(NodeNotFound(node_id=node))
}
}
///|
/// Remove attribute from element
pub fn DomTree::remove_attribute(
self : DomTree,
node : NodeId,
name : String,
) -> Result[Unit, CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => {
let old_value = n.attributes.get(name)
n.attributes.remove(name)
n.sel_classes = None
n.sel_attrs = None
// Record mutation
self.mutations.record_attribute(node, name, old_value~)
self.invalidate_layout(id)
Ok(())
}
None => Err(NodeNotFound(node_id=node))
}
}
///|
/// Get all attributes
pub fn DomTree::get_attributes(
self : DomTree,
node : NodeId,
) -> Result[Array[(String, String)], CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => {
let attrs : Array[(String, String)] = []
for name, value in n.attributes {
attrs.push((name, value))
}
Ok(attrs)
}
None => Err(NodeNotFound(node_id=node))
}
}
///|
fn normalize_custom_states(states : Array[String]) -> Array[String] {
let normalized : Array[String] = []
for state in states {
if state.is_empty() {
continue
}
if normalized.search(state) is None {
normalized.push(state)
}
}
normalized
}
///|
pub fn DomTree::set_custom_states(
self : DomTree,
node : NodeId,
states : Array[String],
) -> Result[Unit, CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => {
if n.node_type != Element {
return Err(
InvalidOperation(message="Custom states can only be set on elements"),
)
}
n.custom_states = normalize_custom_states(states)
self.invalidate_layout(id)
Ok(())
}
None => Err(NodeNotFound(node_id=node))
}
}
///|
pub fn DomTree::get_custom_states(
self : DomTree,
node : NodeId,
) -> Result[Array[String], CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => Ok(n.custom_states.copy())
None => Err(NodeNotFound(node_id=node))
}
}
///|
pub fn DomTree::set_form_associated_state(
self : DomTree,
node : NodeId,
form_value : String?,
form_state_value : String?,
) -> Result[Unit, CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => {
if n.node_type != Element {
return Err(
InvalidOperation(
message="Form-associated state can only be set on elements",
),
)
}
n.form_value = form_value
n.form_state_value = form_state_value
Ok(())
}
None => Err(NodeNotFound(node_id=node))
}
}
///|
pub fn DomTree::get_form_associated_state(
self : DomTree,
node : NodeId,
) -> Result[(String?, String?), CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => Ok((n.form_value, n.form_state_value))
None => Err(NodeNotFound(node_id=node))
}
}
// =============================================================================
// Node Information
// =============================================================================
///|
/// Get node info
pub fn DomTree::get_node_info(
self : DomTree,
node : NodeId,
) -> Result[NodeInfo, CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) =>
Ok({
node_id: node,
node_type: n.node_type,
node_name: n.tag_name,
node_value: n.text_content,
child_count: n.children.length(),
})
None => Err(NodeNotFound(node_id=node))
}
}
///|
/// Get tag name
pub fn DomTree::get_tag_name(
self : DomTree,
node : NodeId,
) -> Result[String, CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => Ok(n.tag_name)
None => Err(NodeNotFound(node_id=node))
}
}
///|
/// Get text content
pub fn DomTree::get_text_content(
self : DomTree,
node : NodeId,
) -> Result[String, CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) =>
match n.node_type {
Text | Comment => Ok(n.text_content)
_ => {
// For elements, collect all descendant text
let buf = StringBuilder::new()
self.collect_text_content(id, buf)
Ok(buf.to_string())
}
}
None => Err(NodeNotFound(node_id=node))
}
}
///|
/// Set text content
pub fn DomTree::set_text_content(
self : DomTree,
node : NodeId,
content : String,
) -> Result[Unit, CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => {
match n.node_type {
Text | Comment => {
let old_value = n.text_content
n.text_content = content
// Record mutation
self.mutations.record_character_data(
node,
old_value=Some(old_value),
new_value=Some(content),
)
}
_ => {
// For elements, remove all children and add text node
// Record removal of existing children
for child_id in n.children {
self.mutations.record_child_removed(node, NodeId(child_id))
}
n.children.clear()
let text_node = self.create_text(content)
let _ = self.append_child(node, text_node)
}
}
self.invalidate_layout(id)
Ok(())
}
None => Err(NodeNotFound(node_id=node))
}
}
///|
/// Collect text content recursively
fn DomTree::collect_text_content(
self : DomTree,
node_id : Int,
buf : StringBuilder,
) -> Unit {
match self.nodes.get(node_id) {
Some(n) =>
match n.node_type {
Text => buf.write_string(n.text_content)
_ =>
for child_id in n.children {
self.collect_text_content(child_id, buf)
}
}
None => ()
}
}
// =============================================================================
// Tree Traversal
// =============================================================================
///|
/// Get document root
pub fn DomTree::get_document(self : DomTree) -> NodeId {
NodeId(self.root_id)
}
///|
/// Get parent node
pub fn DomTree::get_parent(
self : DomTree,
node : NodeId,
) -> Result[NodeId?, CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => Ok(n.parent_id.map(fn(x) { NodeId(x) }))
None => Err(NodeNotFound(node_id=node))
}
}
///|
/// Get child nodes
pub fn DomTree::get_children(
self : DomTree,
node : NodeId,
) -> Result[Array[NodeId], CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => Ok(n.children.map(fn(x) { NodeId(x) }))
None => Err(NodeNotFound(node_id=node))
}
}
///|
/// Get first child
pub fn DomTree::get_first_child(
self : DomTree,
node : NodeId,
) -> Result[NodeId?, CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => Ok(n.children.get(0).map(NodeId::from_int))
None => Err(NodeNotFound(node_id=node))
}
}
///|
/// Get next sibling
pub fn DomTree::get_next_sibling(
self : DomTree,
node : NodeId,
) -> Result[NodeId?, CoreError] {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) =>
match n.parent_id {
Some(parent_id) =>
match self.nodes.get(parent_id) {
Some(parent) => {
let idx = parent.children.search(id)
match idx {
Some(i) if i + 1 < parent.children.length() =>
Ok(Some(NodeId(parent.children[i + 1])))
_ => Ok(None)
}
}
None => Ok(None)
}
None => Ok(None)
}
None => Err(NodeNotFound(node_id=node))
}
}
// =============================================================================
// Layout Cache
// =============================================================================
///|
/// Invalidate layout cache for node and ancestors
fn DomTree::invalidate_layout(self : DomTree, node_id : Int) -> Unit {
match self.nodes.get(node_id) {
Some(n) => {
n.cached_rect = None
match self.get_invalidation_parent_id(node_id) {
Some(parent_id) => self.invalidate_layout(parent_id)
None => ()
}
}
None => ()
}
}
///|
/// Set cached rect (called by layout engine)
pub fn DomTree::set_cached_rect(
self : DomTree,
node : NodeId,
rect : Rect,
) -> Unit {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => n.cached_rect = Some(rect)
None => ()
}
}
///|
/// Get cached rect
pub fn DomTree::get_cached_rect(self : DomTree, node : NodeId) -> Rect? {
let id = node.to_int()
match self.nodes.get(id) {
Some(n) => n.cached_rect
None => None
}
}
// =============================================================================
// Focus Management
// =============================================================================
///|
/// Set focused element
pub fn DomTree::set_focus(self : DomTree, node : NodeId?) -> Unit {
self.focused_node = node.map(fn(x) { x.to_int() })
}
///|
/// Get focused element
pub fn DomTree::get_focus(self : DomTree) -> NodeId? {
self.focused_node.map(fn(x) { NodeId(x) })
}
// =============================================================================
// Mutation Queue API
// =============================================================================
///|
/// Enable or disable mutation recording. Off by default: records exist only to
/// feed the incremental-layout bridge, so a standalone tree (querySelector /
/// one-shot build) records nothing. The bridge enables it on the trees it owns.
pub fn DomTree::set_mutation_recording(self : DomTree, on : Bool) -> Unit {
self.mutations.set_recording(on)
}
///|
/// Check if there are pending mutations
pub fn DomTree::has_pending_mutations(self : DomTree) -> Bool {
!self.mutations.is_empty()
}
///|
/// Get number of pending mutations
pub fn DomTree::pending_mutation_count(self : DomTree) -> Int {
self.mutations.length()
}
///|
/// Flush pending mutations and return optimized records
/// Returns (records, result) where records is the optimized mutation list
/// and result contains statistics about the flush operation
pub fn DomTree::flush_mutations(
self : DomTree,
) -> (Array[MutationRecord], FlushResult) {
self.mutations.flush()
}
///|
/// Flush mutations with a custom handler for each record
/// Useful for applying mutations to other systems (e.g., LayoutTree)
pub fn[T] DomTree::flush_mutations_with(
self : DomTree,
handler : (MutationRecord) -> T,
) -> (Array[T], FlushResult) {
self.mutations.flush_with(handler)
}
///|
/// Clear all pending mutations without processing
pub fn DomTree::clear_mutations(self : DomTree) -> Unit {
self.mutations.clear()
}
///|
/// Get direct access to the mutation queue (for advanced use cases)
pub fn DomTree::get_mutation_queue(self : DomTree) -> MutationQueue {
self.mutations
}