Skip to main content

hpx_browser/
dom.rs

1//! Arena-allocated DOM tree with Shadow DOM support.
2//!
3//! - Arena allocation: O(1) node access via `NodeId`, no `Rc<RefCell<>>`
4//! - `NodeId` is `Copy` — lightweight handles
5//! - Tree mutations: `append_child`, `insert_before`, `detach`, `remove`, `reparent_children`
6//! - Implements [`crate::css_selectors::Element`] for selector matching
7
8use std::collections::HashSet;
9
10use crate::css_selectors::Element;
11
12// ---------------------------------------------------------------------------
13// NodeId
14// ---------------------------------------------------------------------------
15
16/// Opaque, lightweight handle to a node in the DOM arena.
17/// `Copy + Eq + Hash` so it can be used as a key/value everywhere.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub struct NodeId(pub(crate) usize);
20
21impl NodeId {
22    /// The document node is always at index 0.
23    pub const DOCUMENT: NodeId = NodeId(0);
24
25    /// Create a `NodeId` from a raw `u32` (for JS interop).
26    #[must_use]
27    pub fn from_raw(v: u32) -> Self {
28        Self(v as usize)
29    }
30
31    /// Get the raw `u32` value (for JS interop).
32    #[must_use]
33    pub fn to_raw(self) -> u32 {
34        self.0 as u32
35    }
36}
37
38// ---------------------------------------------------------------------------
39// QualName / Attribute
40// ---------------------------------------------------------------------------
41
42/// A qualified name (namespace + local name).
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct QualName {
45    pub ns: Option<String>,
46    pub local: String,
47}
48
49impl QualName {
50    pub fn new(local: impl Into<String>) -> Self {
51        Self {
52            ns: None,
53            local: local.into(),
54        }
55    }
56
57    pub fn with_ns(ns: impl Into<String>, local: impl Into<String>) -> Self {
58        Self {
59            ns: Some(ns.into()),
60            local: local.into(),
61        }
62    }
63}
64
65/// An HTML/XML attribute.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct Attribute {
68    pub name: QualName,
69    pub value: String,
70}
71
72// ---------------------------------------------------------------------------
73// NodeData / ElementData / Node
74// ---------------------------------------------------------------------------
75
76/// Node data — the payload for each node in the arena.
77#[derive(Debug, Clone)]
78pub enum NodeData {
79    Document,
80    DocumentType {
81        name: String,
82        public_id: String,
83        system_id: String,
84    },
85    Element(ElementData),
86    Text(String),
87    Comment(String),
88    ProcessingInstruction {
89        target: String,
90        data: String,
91    },
92    DocumentFragment,
93    ShadowRoot {
94        mode: ShadowRootMode,
95        host: NodeId,
96    },
97}
98
99#[derive(Debug, Clone)]
100pub struct ElementData {
101    pub name: QualName,
102    pub attrs: Vec<Attribute>,
103    pub shadow_root: Option<NodeId>,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub enum ShadowRootMode {
108    Open,
109    Closed,
110}
111
112/// A node in the arena — intrusive linked list of siblings + parent/child pointers.
113#[derive(Debug, Clone)]
114pub struct Node {
115    pub id: NodeId,
116    pub data: NodeData,
117    pub parent: Option<NodeId>,
118    pub first_child: Option<NodeId>,
119    pub last_child: Option<NodeId>,
120    pub prev_sibling: Option<NodeId>,
121    pub next_sibling: Option<NodeId>,
122}
123
124impl Node {
125    pub fn new(id: NodeId, data: NodeData) -> Self {
126        Self {
127            id,
128            data,
129            parent: None,
130            first_child: None,
131            last_child: None,
132            prev_sibling: None,
133            next_sibling: None,
134        }
135    }
136
137    pub fn is_element(&self) -> bool {
138        matches!(self.data, NodeData::Element(_))
139    }
140
141    pub fn as_element(&self) -> Option<&ElementData> {
142        match &self.data {
143            NodeData::Element(data) => Some(data),
144            _ => None,
145        }
146    }
147
148    pub fn as_element_mut(&mut self) -> Option<&mut ElementData> {
149        match &mut self.data {
150            NodeData::Element(data) => Some(data),
151            _ => None,
152        }
153    }
154
155    pub fn as_text(&self) -> Option<&str> {
156        match &self.data {
157            NodeData::Text(t) => Some(t),
158            _ => None,
159        }
160    }
161}
162
163// ---------------------------------------------------------------------------
164// Dom — the arena
165// ---------------------------------------------------------------------------
166
167/// Arena-allocated DOM tree. All nodes live in a flat `Vec`, referenced by `NodeId`.
168#[derive(Debug)]
169pub struct Dom {
170    nodes: Vec<Option<Node>>,
171    free_list: Vec<usize>,
172}
173
174/// Tripwire for tree-walking helpers — bounds iterative DFS/BFS to avoid
175/// runaway on pathological/corrupt trees.
176const WALK_LIMIT: usize = 2_000_000;
177
178/// Bound for ancestor walks used by cycle assertions at mutation sites.
179const ANCESTOR_LIMIT: usize = 10_000;
180
181impl Dom {
182    /// Create a new DOM with a Document node at index 0.
183    pub fn new() -> Self {
184        let doc_node = Node::new(NodeId::DOCUMENT, NodeData::Document);
185        Self {
186            nodes: vec![Some(doc_node)],
187            free_list: Vec::new(),
188        }
189    }
190
191    /// Get the document node ID.
192    pub fn document(&self) -> NodeId {
193        NodeId::DOCUMENT
194    }
195
196    /// Get a reference to a node by ID.
197    pub fn get(&self, id: NodeId) -> Option<&Node> {
198        self.nodes.get(id.0).and_then(|n| n.as_ref())
199    }
200
201    /// Get a mutable reference to a node by ID.
202    pub fn get_mut(&mut self, id: NodeId) -> Option<&mut Node> {
203        self.nodes.get_mut(id.0).and_then(|n| n.as_mut())
204    }
205
206    /// Total number of live nodes.
207    pub fn len(&self) -> usize {
208        self.nodes.iter().filter(|n| n.is_some()).count()
209    }
210
211    pub fn is_empty(&self) -> bool {
212        self.len() == 0
213    }
214
215    // --- Node creation ---
216
217    fn allocate(&mut self, data: NodeData) -> NodeId {
218        if let Some(idx) = self.free_list.pop() {
219            let id = NodeId(idx);
220            self.nodes[idx] = Some(Node::new(id, data));
221            id
222        } else {
223            let id = NodeId(self.nodes.len());
224            self.nodes.push(Some(Node::new(id, data)));
225            id
226        }
227    }
228
229    pub fn create_element(&mut self, name: QualName, attrs: Vec<Attribute>) -> NodeId {
230        self.allocate(NodeData::Element(ElementData {
231            name,
232            attrs,
233            shadow_root: None,
234        }))
235    }
236
237    pub fn create_text(&mut self, text: String) -> NodeId {
238        self.allocate(NodeData::Text(text))
239    }
240
241    pub fn create_comment(&mut self, text: String) -> NodeId {
242        self.allocate(NodeData::Comment(text))
243    }
244
245    pub fn create_document_fragment(&mut self) -> NodeId {
246        self.allocate(NodeData::DocumentFragment)
247    }
248
249    /// Create a shadow root and attach it to the given host element.
250    pub fn create_shadow_root(&mut self, host: NodeId, mode: ShadowRootMode) -> NodeId {
251        let shadow_id = self.allocate(NodeData::ShadowRoot { mode, host });
252        if let Some(node) = self.get_mut(host) {
253            if let Some(elem) = node.as_element_mut() {
254                elem.shadow_root = Some(shadow_id);
255            }
256        }
257        shadow_id
258    }
259
260    pub fn allocate_pi(&mut self, target: String, data: String) -> NodeId {
261        self.allocate(NodeData::ProcessingInstruction { target, data })
262    }
263
264    pub fn create_doctype(&mut self, name: String, public_id: String, system_id: String) -> NodeId {
265        self.allocate(NodeData::DocumentType {
266            name,
267            public_id,
268            system_id,
269        })
270    }
271
272    // --- Tree mutation (all O(1)) ---
273
274    /// Returns `true` if `candidate` is `target` or an ancestor of `target`.
275    fn is_ancestor_or_self(&self, candidate: NodeId, target: NodeId) -> bool {
276        if candidate == target {
277            return true;
278        }
279        let mut current = self.get(target).and_then(|n| n.parent);
280        let mut depth = 0usize;
281        while let Some(id) = current {
282            if id == candidate {
283                return true;
284            }
285            depth += 1;
286            if depth > ANCESTOR_LIMIT {
287                break;
288            }
289            current = self.get(id).and_then(|n| n.parent);
290        }
291        false
292    }
293
294    /// Append `child` as the last child of `parent`.
295    ///
296    /// Cycle-safe: if `child` is an ancestor of `parent`, the mutation is a no-op.
297    pub fn append_child(&mut self, parent: NodeId, child: NodeId) {
298        if self.get(parent).is_none() || self.get(child).is_none() {
299            return;
300        }
301        if self.is_ancestor_or_self(child, parent) {
302            return;
303        }
304        self.detach(child);
305
306        let old_last = self.get(parent).and_then(|n| n.last_child);
307
308        if let Some(node) = self.get_mut(child) {
309            node.parent = Some(parent);
310            node.prev_sibling = old_last;
311            node.next_sibling = None;
312        }
313
314        if let Some(old_last_id) = old_last {
315            if let Some(node) = self.get_mut(old_last_id) {
316                node.next_sibling = Some(child);
317            }
318        } else if let Some(node) = self.get_mut(parent) {
319            node.first_child = Some(child);
320        }
321
322        if let Some(node) = self.get_mut(parent) {
323            node.last_child = Some(child);
324        }
325    }
326
327    /// Insert `child` before `reference` (which must be a child of `parent`).
328    pub fn insert_before(&mut self, parent: NodeId, child: NodeId, reference: NodeId) {
329        if self.get(parent).is_none() || self.get(child).is_none() || self.get(reference).is_none()
330        {
331            return;
332        }
333        if self.is_ancestor_or_self(child, parent) {
334            return;
335        }
336        self.detach(child);
337
338        let prev = self.get(reference).and_then(|n| n.prev_sibling);
339
340        if let Some(node) = self.get_mut(child) {
341            node.parent = Some(parent);
342            node.prev_sibling = prev;
343            node.next_sibling = Some(reference);
344        }
345
346        if let Some(node) = self.get_mut(reference) {
347            node.prev_sibling = Some(child);
348        }
349
350        if let Some(prev_id) = prev {
351            if let Some(node) = self.get_mut(prev_id) {
352                node.next_sibling = Some(child);
353            }
354        } else if let Some(node) = self.get_mut(parent) {
355            node.first_child = Some(child);
356        }
357    }
358
359    /// Detach a node from its parent (but keep it in the arena).
360    pub fn detach(&mut self, id: NodeId) {
361        let (parent, prev, next) = match self.get(id) {
362            Some(node) => (node.parent, node.prev_sibling, node.next_sibling),
363            None => return,
364        };
365
366        if let Some(prev_id) = prev {
367            if let Some(node) = self.get_mut(prev_id) {
368                node.next_sibling = next;
369            }
370        } else if let Some(parent_id) = parent {
371            if let Some(node) = self.get_mut(parent_id) {
372                node.first_child = next;
373            }
374        }
375
376        if let Some(next_id) = next {
377            if let Some(node) = self.get_mut(next_id) {
378                node.prev_sibling = prev;
379            }
380        } else if let Some(parent_id) = parent {
381            if let Some(node) = self.get_mut(parent_id) {
382                node.last_child = prev;
383            }
384        }
385
386        if let Some(node) = self.get_mut(id) {
387            node.parent = None;
388            node.prev_sibling = None;
389            node.next_sibling = None;
390        }
391    }
392
393    /// Remove a node from the arena entirely (recycles its slot).
394    pub fn remove(&mut self, id: NodeId) {
395        self.detach(id);
396        if id.0 < self.nodes.len() {
397            self.nodes[id.0] = None;
398            self.free_list.push(id.0);
399        }
400    }
401
402    /// Move all children of `source` to become children of `target`.
403    pub fn reparent_children(&mut self, source: NodeId, target: NodeId) {
404        loop {
405            let child = self.get(source).and_then(|n| n.first_child);
406            match child {
407                Some(child_id) => self.append_child(target, child_id),
408                None => break,
409            }
410        }
411    }
412
413    // --- Traversal helpers ---
414
415    /// Iterate over child node IDs.
416    pub fn children(&self, parent: NodeId) -> Vec<NodeId> {
417        let mut result = Vec::new();
418        let mut current = self.get(parent).and_then(|n| n.first_child);
419        while let Some(id) = current {
420            result.push(id);
421            current = self.get(id).and_then(|n| n.next_sibling);
422        }
423        result
424    }
425
426    /// Iterate over child element node IDs (skip text/comment nodes).
427    pub fn child_elements(&self, parent: NodeId) -> Vec<NodeId> {
428        self.children(parent)
429            .into_iter()
430            .filter(|id| self.get(*id).is_some_and(|n| n.is_element()))
431            .collect()
432    }
433
434    /// Get text content of a subtree.
435    pub fn text_content(&self, id: NodeId) -> String {
436        let mut result = String::new();
437        self.collect_text(id, &mut result);
438        result
439    }
440
441    /// Iterative pre-order DFS that concatenates text-node payloads in document order.
442    fn collect_text(&self, root: NodeId, result: &mut String) {
443        let mut stack: Vec<NodeId> = vec![root];
444        let mut visited: HashSet<NodeId> = HashSet::with_capacity(64);
445        let mut steps: usize = 0;
446        while let Some(id) = stack.pop() {
447            if !visited.insert(id) {
448                continue;
449            }
450            steps += 1;
451            if steps > WALK_LIMIT {
452                break;
453            }
454            let node = match self.get(id) {
455                Some(n) => n,
456                None => continue,
457            };
458            match &node.data {
459                NodeData::Text(t) => result.push_str(t),
460                _ => {
461                    let mut kids: Vec<NodeId> = Vec::new();
462                    let mut child = node.first_child;
463                    while let Some(c) = child {
464                        kids.push(c);
465                        child = self.get(c).and_then(|n| n.next_sibling);
466                    }
467                    stack.extend(kids.into_iter().rev());
468                }
469            }
470        }
471    }
472
473    /// Set text content: remove all children, add a single text node.
474    pub fn set_text_content(&mut self, id: NodeId, text: &str) {
475        let children: Vec<NodeId> = self.children(id);
476        for child in children {
477            self.remove(child);
478        }
479        if !text.is_empty() {
480            let text_id = self.create_text(text.to_string());
481            self.append_child(id, text_id);
482        }
483    }
484
485    /// Find an element by ID attribute (tree walk from document root).
486    pub fn get_element_by_id(&self, id_value: &str) -> Option<NodeId> {
487        self.find_element(NodeId::DOCUMENT, &|node| {
488            node.as_element()
489                .and_then(|e| e.attrs.iter().find(|a| a.name.local == "id"))
490                .is_some_and(|a| a.value == id_value)
491        })
492    }
493
494    /// Find elements by tag name (case-insensitive).
495    pub fn get_elements_by_tag_name(&self, root: NodeId, tag: &str) -> Vec<NodeId> {
496        let mut results = Vec::new();
497        self.collect_elements(
498            root,
499            &|node| {
500                node.as_element()
501                    .is_some_and(|e| e.name.local.eq_ignore_ascii_case(tag))
502            },
503            &mut results,
504        );
505        results
506    }
507
508    /// Find elements by class name.
509    pub fn get_elements_by_class_name(&self, root: NodeId, class: &str) -> Vec<NodeId> {
510        let mut results = Vec::new();
511        self.collect_elements(
512            root,
513            &|node| {
514                node.as_element()
515                    .and_then(|e| e.attrs.iter().find(|a| a.name.local == "class"))
516                    .is_some_and(|a| a.value.split_whitespace().any(|c| c == class))
517            },
518            &mut results,
519        );
520        results
521    }
522
523    /// Serialize a subtree as HTML.
524    pub fn serialize_html(&self, id: NodeId) -> String {
525        let mut out = String::new();
526        self.serialize_node(id, &mut out);
527        out
528    }
529
530    /// Serialize children of a node as HTML (for innerHTML getter).
531    pub fn serialize_inner_html(&self, id: NodeId) -> String {
532        let mut out = String::new();
533        let mut kids: Vec<NodeId> = Vec::new();
534        let mut child = self.get(id).and_then(|n| n.first_child);
535        while let Some(child_id) = child {
536            kids.push(child_id);
537            child = self.get(child_id).and_then(|n| n.next_sibling);
538        }
539        for c in kids {
540            self.serialize_node(c, &mut out);
541        }
542        out
543    }
544
545    /// Iterative serializer using an explicit work-stack.
546    fn serialize_node(&self, root: NodeId, out: &mut String) {
547        enum SerWork {
548            Open(NodeId),
549            Close(String),
550        }
551        const VOID_ELEMENTS: &[&str] = &[
552            "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
553            "source", "track", "wbr",
554        ];
555
556        let mut stack: Vec<SerWork> = vec![SerWork::Open(root)];
557        let mut visited: HashSet<NodeId> = HashSet::with_capacity(64);
558        let mut steps: usize = 0;
559        while let Some(work) = stack.pop() {
560            match work {
561                SerWork::Close(s) => out.push_str(&s),
562                SerWork::Open(id) => {
563                    if !visited.insert(id) {
564                        continue;
565                    }
566                    steps += 1;
567                    if steps > WALK_LIMIT {
568                        break;
569                    }
570                    let node = match self.get(id) {
571                        Some(n) => n,
572                        None => continue,
573                    };
574                    match &node.data {
575                        NodeData::Element(elem) => {
576                            out.push('<');
577                            out.push_str(&elem.name.local);
578                            for attr in &elem.attrs {
579                                out.push(' ');
580                                out.push_str(&attr.name.local);
581                                out.push_str("=\"");
582                                out.push_str(
583                                    &attr.value.replace('&', "&amp;").replace('"', "&quot;"),
584                                );
585                                out.push('"');
586                            }
587                            out.push('>');
588
589                            let is_void = VOID_ELEMENTS.contains(&elem.name.local.as_str());
590                            if !is_void {
591                                stack.push(SerWork::Close(format!("</{}>", elem.name.local)));
592                            }
593                            let mut kids: Vec<NodeId> = Vec::new();
594                            let mut child = node.first_child;
595                            while let Some(c) = child {
596                                kids.push(c);
597                                child = self.get(c).and_then(|n| n.next_sibling);
598                            }
599                            for c in kids.into_iter().rev() {
600                                stack.push(SerWork::Open(c));
601                            }
602                        }
603                        NodeData::Text(text) => {
604                            out.push_str(
605                                &text
606                                    .replace('&', "&amp;")
607                                    .replace('<', "&lt;")
608                                    .replace('>', "&gt;"),
609                            );
610                        }
611                        NodeData::Comment(text) => {
612                            out.push_str("<!--");
613                            out.push_str(text);
614                            out.push_str("-->");
615                        }
616                        NodeData::DocumentType { name, .. } => {
617                            out.push_str("<!DOCTYPE ");
618                            out.push_str(name);
619                            out.push('>');
620                        }
621                        NodeData::Document | NodeData::DocumentFragment => {
622                            let mut kids: Vec<NodeId> = Vec::new();
623                            let mut child = node.first_child;
624                            while let Some(c) = child {
625                                kids.push(c);
626                                child = self.get(c).and_then(|n| n.next_sibling);
627                            }
628                            for c in kids.into_iter().rev() {
629                                stack.push(SerWork::Open(c));
630                            }
631                        }
632                        _ => {}
633                    }
634                }
635            }
636        }
637    }
638
639    /// Copy a subtree from another `Dom` into this one. Returns the new root `NodeId`.
640    pub fn merge_subtree(&mut self, source: &Dom, source_root: NodeId) -> NodeId {
641        fn create_from(this: &mut Dom, source: &Dom, src_id: NodeId) -> Option<NodeId> {
642            let src = source.get(src_id)?;
643            Some(match &src.data {
644                NodeData::Element(elem) => {
645                    this.create_element(elem.name.clone(), elem.attrs.clone())
646                }
647                NodeData::Text(t) => this.create_text(t.clone()),
648                NodeData::Comment(t) => this.create_comment(t.clone()),
649                NodeData::DocumentFragment | NodeData::Document => this.create_document_fragment(),
650                _ => this.create_document_fragment(),
651            })
652        }
653
654        let new_root = match create_from(self, source, source_root) {
655            Some(id) => id,
656            None => return self.create_document_fragment(),
657        };
658
659        let mut queue: Vec<(NodeId, NodeId)> = Vec::new();
660        let mut visited: HashSet<NodeId> = HashSet::with_capacity(64);
661        visited.insert(source_root);
662
663        let mut child = source.get(source_root).and_then(|n| n.first_child);
664        while let Some(c) = child {
665            queue.push((c, new_root));
666            child = source.get(c).and_then(|n| n.next_sibling);
667        }
668
669        let mut steps: usize = 0;
670        let mut i = 0usize;
671        while i < queue.len() {
672            let (src_id, dest_parent) = queue[i];
673            i += 1;
674            steps += 1;
675            if steps > WALK_LIMIT {
676                break;
677            }
678            if !visited.insert(src_id) {
679                continue;
680            }
681            let new_id = match create_from(self, source, src_id) {
682                Some(id) => id,
683                None => continue,
684            };
685            self.append_child(dest_parent, new_id);
686            let mut child = source.get(src_id).and_then(|n| n.first_child);
687            while let Some(c) = child {
688                queue.push((c, new_id));
689                child = source.get(c).and_then(|n| n.next_sibling);
690            }
691        }
692
693        new_root
694    }
695
696    /// Get the node type number (matches DOM spec `nodeType`).
697    pub fn node_type(&self, id: NodeId) -> u32 {
698        match self.get(id).map(|n| &n.data) {
699            Some(NodeData::Element(_)) => 1,
700            Some(NodeData::Text(_)) => 3,
701            Some(NodeData::ProcessingInstruction { .. }) => 7,
702            Some(NodeData::Comment(_)) => 8,
703            Some(NodeData::Document) => 9,
704            Some(NodeData::DocumentType { .. }) => 10,
705            Some(NodeData::DocumentFragment) => 11,
706            Some(NodeData::ShadowRoot { .. }) => 11,
707            None => 0,
708        }
709    }
710
711    // --- Internal helpers ---
712
713    /// Iterative pre-order DFS returning the first descendant matching `predicate`.
714    fn find_element(&self, root: NodeId, predicate: &dyn Fn(&Node) -> bool) -> Option<NodeId> {
715        let mut stack: Vec<NodeId> = Vec::new();
716        let mut child = self.get(root).and_then(|n| n.first_child);
717        let mut seed: Vec<NodeId> = Vec::new();
718        while let Some(c) = child {
719            seed.push(c);
720            child = self.get(c).and_then(|n| n.next_sibling);
721        }
722        stack.extend(seed.into_iter().rev());
723
724        let mut visited: HashSet<NodeId> = HashSet::with_capacity(64);
725        let mut steps: usize = 0;
726        while let Some(id) = stack.pop() {
727            if !visited.insert(id) {
728                continue;
729            }
730            steps += 1;
731            if steps > WALK_LIMIT {
732                break;
733            }
734            let node = match self.get(id) {
735                Some(n) => n,
736                None => continue,
737            };
738            if predicate(node) {
739                return Some(id);
740            }
741            let mut kids: Vec<NodeId> = Vec::new();
742            let mut child = node.first_child;
743            while let Some(c) = child {
744                kids.push(c);
745                child = self.get(c).and_then(|n| n.next_sibling);
746            }
747            stack.extend(kids.into_iter().rev());
748        }
749        None
750    }
751
752    /// Iterative pre-order DFS pushing every matching descendant into `results`.
753    fn collect_elements(
754        &self,
755        root: NodeId,
756        predicate: &dyn Fn(&Node) -> bool,
757        results: &mut Vec<NodeId>,
758    ) {
759        let mut stack: Vec<NodeId> = Vec::new();
760        let mut seed: Vec<NodeId> = Vec::new();
761        let mut child = self.get(root).and_then(|n| n.first_child);
762        while let Some(c) = child {
763            seed.push(c);
764            child = self.get(c).and_then(|n| n.next_sibling);
765        }
766        stack.extend(seed.into_iter().rev());
767
768        let mut visited: HashSet<NodeId> = HashSet::with_capacity(64);
769        let mut steps: usize = 0;
770        while let Some(id) = stack.pop() {
771            if !visited.insert(id) {
772                continue;
773            }
774            steps += 1;
775            if steps > WALK_LIMIT {
776                break;
777            }
778            let node = match self.get(id) {
779                Some(n) => n,
780                None => continue,
781            };
782            if predicate(node) {
783                results.push(id);
784            }
785            let mut kids: Vec<NodeId> = Vec::new();
786            let mut child = node.first_child;
787            while let Some(c) = child {
788                kids.push(c);
789                child = self.get(c).and_then(|n| n.next_sibling);
790            }
791            stack.extend(kids.into_iter().rev());
792        }
793    }
794}
795
796impl Default for Dom {
797    fn default() -> Self {
798        Self::new()
799    }
800}
801
802// ---------------------------------------------------------------------------
803// DomElement — lightweight wrapper implementing css_selectors::Element
804// ---------------------------------------------------------------------------
805
806/// A DOM element wrapper that implements [`crate::css_selectors::Element`].
807///
808/// This is a lightweight handle: it borrows the `Dom` and holds a `NodeId`.
809/// Created on-the-fly for selector matching.
810#[derive(Clone)]
811pub struct DomElement<'a> {
812    pub dom: &'a Dom,
813    pub id: NodeId,
814}
815
816impl<'a> DomElement<'a> {
817    pub fn new(dom: &'a Dom, id: NodeId) -> Option<Self> {
818        let node = dom.get(id)?;
819        node.is_element().then_some(Self { dom, id })
820    }
821
822    pub fn node_id(&self) -> NodeId {
823        self.id
824    }
825
826    fn node(&self) -> &Node {
827        // ponytail: safe — constructor validated node exists and is element
828        match self.dom.get(self.id) {
829            Some(n) => n,
830            None => unreachable!(),
831        }
832    }
833
834    fn element_data(&self) -> &ElementData {
835        // ponytail: safe — constructor validated node is an element
836        match self.node().as_element() {
837            Some(d) => d,
838            None => unreachable!(),
839        }
840    }
841}
842
843impl<'a> std::fmt::Debug for DomElement<'a> {
844    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
845        let data = self.element_data();
846        write!(f, "<{}", data.name.local)?;
847        for attr in &data.attrs {
848            write!(f, " {}=\"{}\"", attr.name.local, attr.value)?;
849        }
850        write!(f, ">")
851    }
852}
853
854impl<'a> Element for DomElement<'a> {
855    fn local_name(&self) -> &str {
856        &self.element_data().name.local
857    }
858
859    fn namespace(&self) -> Option<&str> {
860        self.element_data().name.ns.as_deref()
861    }
862
863    fn id(&self) -> Option<&str> {
864        self.element_data()
865            .attrs
866            .iter()
867            .find(|a| a.name.local == "id")
868            .map(|a| a.value.as_str())
869    }
870
871    fn has_class(&self, name: &str) -> bool {
872        self.element_data()
873            .attrs
874            .iter()
875            .find(|a| a.name.local == "class")
876            .is_some_and(|a| a.value.split_whitespace().any(|c| c == name))
877    }
878
879    fn has_attribute(&self, name: &str) -> bool {
880        self.element_data()
881            .attrs
882            .iter()
883            .any(|a| a.name.local.eq_ignore_ascii_case(name))
884    }
885
886    fn attribute_value(&self, name: &str) -> Option<&str> {
887        self.element_data()
888            .attrs
889            .iter()
890            .find(|a| a.name.local.eq_ignore_ascii_case(name))
891            .map(|a| a.value.as_str())
892    }
893
894    fn parent_element(&self) -> Option<Self> {
895        let mut parent_id = self.node().parent?;
896        loop {
897            let parent = self.dom.get(parent_id)?;
898            if parent.is_element() {
899                return Some(DomElement {
900                    dom: self.dom,
901                    id: parent_id,
902                });
903            }
904            parent_id = parent.parent?;
905        }
906    }
907
908    fn prev_sibling_element(&self) -> Option<Self> {
909        let mut sib_id = self.node().prev_sibling?;
910        loop {
911            let sib = self.dom.get(sib_id)?;
912            if sib.is_element() {
913                return Some(DomElement {
914                    dom: self.dom,
915                    id: sib_id,
916                });
917            }
918            sib_id = sib.prev_sibling?;
919        }
920    }
921
922    fn next_sibling_element(&self) -> Option<Self> {
923        let mut sib_id = self.node().next_sibling?;
924        loop {
925            let sib = self.dom.get(sib_id)?;
926            if sib.is_element() {
927                return Some(DomElement {
928                    dom: self.dom,
929                    id: sib_id,
930                });
931            }
932            sib_id = sib.next_sibling?;
933        }
934    }
935
936    fn first_child_element(&self) -> Option<Self> {
937        let mut child_id = self.node().first_child?;
938        loop {
939            let child = self.dom.get(child_id)?;
940            if child.is_element() {
941                return Some(DomElement {
942                    dom: self.dom,
943                    id: child_id,
944                });
945            }
946            child_id = child.next_sibling?;
947        }
948    }
949
950    fn last_child_element(&self) -> Option<Self> {
951        let mut child_id = self.node().last_child?;
952        loop {
953            let child = self.dom.get(child_id)?;
954            if child.is_element() {
955                return Some(DomElement {
956                    dom: self.dom,
957                    id: child_id,
958                });
959            }
960            child_id = child.prev_sibling?;
961        }
962    }
963
964    fn is_root(&self) -> bool {
965        match self.node().parent {
966            Some(parent_id) => self
967                .dom
968                .get(parent_id)
969                .is_some_and(|n| matches!(n.data, NodeData::Document)),
970            None => false,
971        }
972    }
973
974    fn is_empty(&self) -> bool {
975        let mut child_id = self.node().first_child;
976        while let Some(id) = child_id {
977            if let Some(child) = self.dom.get(id) {
978                match &child.data {
979                    NodeData::Element(_) => return false,
980                    NodeData::Text(t) if !t.is_empty() => return false,
981                    _ => {}
982                }
983                child_id = child.next_sibling;
984            } else {
985                break;
986            }
987        }
988        true
989    }
990
991    fn is_link(&self) -> bool {
992        let name = self.local_name();
993        (name == "a" || name == "area") && self.has_attribute("href")
994    }
995}
996
997// ---------------------------------------------------------------------------
998// Tests
999// ---------------------------------------------------------------------------
1000
1001#[cfg(test)]
1002mod tests {
1003    use super::*;
1004
1005    fn build_test_dom() -> Dom {
1006        let mut dom = Dom::new();
1007        let html = dom.create_element(QualName::new("html"), vec![]);
1008        dom.append_child(NodeId::DOCUMENT, html);
1009
1010        let body = dom.create_element(QualName::new("body"), vec![]);
1011        dom.append_child(html, body);
1012
1013        let div = dom.create_element(
1014            QualName::new("div"),
1015            vec![
1016                Attribute {
1017                    name: QualName::new("id"),
1018                    value: "main".to_string(),
1019                },
1020                Attribute {
1021                    name: QualName::new("class"),
1022                    value: "container active".to_string(),
1023                },
1024            ],
1025        );
1026        dom.append_child(body, div);
1027
1028        let p = dom.create_element(QualName::new("p"), vec![]);
1029        dom.append_child(div, p);
1030
1031        let text = dom.create_text("Hello world".to_string());
1032        dom.append_child(p, text);
1033
1034        dom
1035    }
1036
1037    #[test]
1038    fn new_dom_has_document() {
1039        let dom = Dom::new();
1040        assert_eq!(dom.len(), 1);
1041        assert!(dom.get(NodeId::DOCUMENT).is_some());
1042    }
1043
1044    #[test]
1045    fn create_and_append() {
1046        let mut dom = Dom::new();
1047        let div = dom.create_element(QualName::new("div"), vec![]);
1048        dom.append_child(NodeId::DOCUMENT, div);
1049
1050        assert_eq!(dom.children(NodeId::DOCUMENT), vec![div]);
1051        assert_eq!(dom.get(div).unwrap().parent, Some(NodeId::DOCUMENT));
1052    }
1053
1054    #[test]
1055    fn append_multiple_children() {
1056        let mut dom = Dom::new();
1057        let a = dom.create_element(QualName::new("a"), vec![]);
1058        let b = dom.create_element(QualName::new("b"), vec![]);
1059        let c = dom.create_element(QualName::new("c"), vec![]);
1060        dom.append_child(NodeId::DOCUMENT, a);
1061        dom.append_child(NodeId::DOCUMENT, b);
1062        dom.append_child(NodeId::DOCUMENT, c);
1063
1064        assert_eq!(dom.children(NodeId::DOCUMENT), vec![a, b, c]);
1065        assert_eq!(dom.get(a).unwrap().next_sibling, Some(b));
1066        assert_eq!(dom.get(b).unwrap().prev_sibling, Some(a));
1067        assert_eq!(dom.get(b).unwrap().next_sibling, Some(c));
1068    }
1069
1070    #[test]
1071    fn insert_before_test() {
1072        let mut dom = Dom::new();
1073        let a = dom.create_element(QualName::new("a"), vec![]);
1074        let c = dom.create_element(QualName::new("c"), vec![]);
1075        dom.append_child(NodeId::DOCUMENT, a);
1076        dom.append_child(NodeId::DOCUMENT, c);
1077
1078        let b = dom.create_element(QualName::new("b"), vec![]);
1079        dom.insert_before(NodeId::DOCUMENT, b, c);
1080
1081        assert_eq!(dom.children(NodeId::DOCUMENT), vec![a, b, c]);
1082    }
1083
1084    #[test]
1085    fn detach_test() {
1086        let mut dom = Dom::new();
1087        let a = dom.create_element(QualName::new("a"), vec![]);
1088        let b = dom.create_element(QualName::new("b"), vec![]);
1089        let c = dom.create_element(QualName::new("c"), vec![]);
1090        dom.append_child(NodeId::DOCUMENT, a);
1091        dom.append_child(NodeId::DOCUMENT, b);
1092        dom.append_child(NodeId::DOCUMENT, c);
1093
1094        dom.detach(b);
1095        assert_eq!(dom.children(NodeId::DOCUMENT), vec![a, c]);
1096        assert_eq!(dom.get(a).unwrap().next_sibling, Some(c));
1097        assert_eq!(dom.get(c).unwrap().prev_sibling, Some(a));
1098    }
1099
1100    #[test]
1101    fn remove_recycles() {
1102        let mut dom = Dom::new();
1103        let a = dom.create_element(QualName::new("a"), vec![]);
1104        dom.append_child(NodeId::DOCUMENT, a);
1105        dom.remove(a);
1106
1107        let b = dom.create_element(QualName::new("b"), vec![]);
1108        assert_eq!(b.0, a.0);
1109    }
1110
1111    #[test]
1112    fn text_content_test() {
1113        let mut dom = Dom::new();
1114        let div = dom.create_element(QualName::new("div"), vec![]);
1115        let text1 = dom.create_text("Hello ".to_string());
1116        let span = dom.create_element(QualName::new("span"), vec![]);
1117        let text2 = dom.create_text("world".to_string());
1118
1119        dom.append_child(NodeId::DOCUMENT, div);
1120        dom.append_child(div, text1);
1121        dom.append_child(div, span);
1122        dom.append_child(span, text2);
1123
1124        assert_eq!(dom.text_content(div), "Hello world");
1125    }
1126
1127    #[test]
1128    fn child_elements_skips_text() {
1129        let mut dom = Dom::new();
1130        let div = dom.create_element(QualName::new("div"), vec![]);
1131        let text = dom.create_text("hello".to_string());
1132        let span = dom.create_element(QualName::new("span"), vec![]);
1133
1134        dom.append_child(NodeId::DOCUMENT, div);
1135        dom.append_child(div, text);
1136        dom.append_child(div, span);
1137
1138        assert_eq!(dom.child_elements(div), vec![span]);
1139    }
1140
1141    #[test]
1142    fn reparent_children_test() {
1143        let mut dom = Dom::new();
1144        let source = dom.create_element(QualName::new("source"), vec![]);
1145        let target = dom.create_element(QualName::new("target"), vec![]);
1146        let a = dom.create_element(QualName::new("a"), vec![]);
1147        let b = dom.create_element(QualName::new("b"), vec![]);
1148
1149        dom.append_child(NodeId::DOCUMENT, source);
1150        dom.append_child(NodeId::DOCUMENT, target);
1151        dom.append_child(source, a);
1152        dom.append_child(source, b);
1153
1154        dom.reparent_children(source, target);
1155
1156        assert!(dom.children(source).is_empty());
1157        assert_eq!(dom.children(target), vec![a, b]);
1158    }
1159
1160    #[test]
1161    fn node_id_raw_roundtrip() {
1162        let id = NodeId::from_raw(42);
1163        assert_eq!(id.to_raw(), 42);
1164    }
1165
1166    #[test]
1167    fn set_text_content_test() {
1168        let mut dom = Dom::new();
1169        let div = dom.create_element(QualName::new("div"), vec![]);
1170        let span = dom.create_element(QualName::new("span"), vec![]);
1171        let text = dom.create_text("old".to_string());
1172        dom.append_child(NodeId::DOCUMENT, div);
1173        dom.append_child(div, span);
1174        dom.append_child(span, text);
1175
1176        dom.set_text_content(div, "new text");
1177        assert_eq!(dom.text_content(div), "new text");
1178        assert_eq!(dom.child_elements(div).len(), 0);
1179    }
1180
1181    #[test]
1182    fn get_element_by_id_test() {
1183        let mut dom = Dom::new();
1184        let div = dom.create_element(
1185            QualName::new("div"),
1186            vec![Attribute {
1187                name: QualName::new("id"),
1188                value: "main".to_string(),
1189            }],
1190        );
1191        dom.append_child(NodeId::DOCUMENT, div);
1192
1193        assert_eq!(dom.get_element_by_id("main"), Some(div));
1194        assert_eq!(dom.get_element_by_id("nonexistent"), None);
1195    }
1196
1197    #[test]
1198    fn get_elements_by_tag_name_test() {
1199        let mut dom = Dom::new();
1200        let div = dom.create_element(QualName::new("div"), vec![]);
1201        let p1 = dom.create_element(QualName::new("p"), vec![]);
1202        let p2 = dom.create_element(QualName::new("p"), vec![]);
1203        let span = dom.create_element(QualName::new("span"), vec![]);
1204        dom.append_child(NodeId::DOCUMENT, div);
1205        dom.append_child(div, p1);
1206        dom.append_child(div, span);
1207        dom.append_child(div, p2);
1208
1209        let ps = dom.get_elements_by_tag_name(NodeId::DOCUMENT, "p");
1210        assert_eq!(ps, vec![p1, p2]);
1211    }
1212
1213    #[test]
1214    fn get_elements_by_class_name_test() {
1215        let mut dom = Dom::new();
1216        let a = dom.create_element(
1217            QualName::new("div"),
1218            vec![Attribute {
1219                name: QualName::new("class"),
1220                value: "foo bar".to_string(),
1221            }],
1222        );
1223        let b = dom.create_element(
1224            QualName::new("div"),
1225            vec![Attribute {
1226                name: QualName::new("class"),
1227                value: "baz".to_string(),
1228            }],
1229        );
1230        dom.append_child(NodeId::DOCUMENT, a);
1231        dom.append_child(NodeId::DOCUMENT, b);
1232
1233        assert_eq!(
1234            dom.get_elements_by_class_name(NodeId::DOCUMENT, "foo"),
1235            vec![a]
1236        );
1237        assert_eq!(
1238            dom.get_elements_by_class_name(NodeId::DOCUMENT, "baz"),
1239            vec![b]
1240        );
1241    }
1242
1243    #[test]
1244    fn serialize_html_basic() {
1245        let mut dom = Dom::new();
1246        let div = dom.create_element(
1247            QualName::new("div"),
1248            vec![Attribute {
1249                name: QualName::new("id"),
1250                value: "main".to_string(),
1251            }],
1252        );
1253        let text = dom.create_text("Hello".to_string());
1254        dom.append_child(NodeId::DOCUMENT, div);
1255        dom.append_child(div, text);
1256
1257        assert_eq!(dom.serialize_html(div), "<div id=\"main\">Hello</div>");
1258    }
1259
1260    #[test]
1261    fn serialize_inner_html_test() {
1262        let mut dom = Dom::new();
1263        let div = dom.create_element(QualName::new("div"), vec![]);
1264        let span = dom.create_element(QualName::new("span"), vec![]);
1265        let text = dom.create_text("hi".to_string());
1266        dom.append_child(NodeId::DOCUMENT, div);
1267        dom.append_child(div, span);
1268        dom.append_child(span, text);
1269
1270        assert_eq!(dom.serialize_inner_html(div), "<span>hi</span>");
1271    }
1272
1273    #[test]
1274    fn merge_subtree_test() {
1275        let mut source = Dom::new();
1276        let div = source.create_element(QualName::new("div"), vec![]);
1277        let text = source.create_text("copied".to_string());
1278        source.append_child(NodeId::DOCUMENT, div);
1279        source.append_child(div, text);
1280
1281        let mut target = Dom::new();
1282        let parent = target.create_element(QualName::new("body"), vec![]);
1283        target.append_child(NodeId::DOCUMENT, parent);
1284
1285        let new_div = target.merge_subtree(&source, div);
1286        target.append_child(parent, new_div);
1287
1288        assert_eq!(target.text_content(new_div), "copied");
1289        assert_eq!(
1290            target
1291                .get(new_div)
1292                .and_then(|n| n.as_element())
1293                .map(|e| e.name.local.as_str()),
1294            Some("div")
1295        );
1296    }
1297
1298    #[test]
1299    fn append_child_rejects_self_cycle() {
1300        let mut dom = Dom::new();
1301        let div = dom.create_element(QualName::new("div"), vec![]);
1302        dom.append_child(NodeId::DOCUMENT, div);
1303        dom.append_child(div, div);
1304        assert_eq!(dom.children(NodeId::DOCUMENT), vec![div]);
1305        assert!(dom.children(div).is_empty());
1306        assert_eq!(dom.get(div).unwrap().parent, Some(NodeId::DOCUMENT));
1307    }
1308
1309    #[test]
1310    fn append_child_rejects_ancestor_cycle() {
1311        let mut dom = Dom::new();
1312        let outer = dom.create_element(QualName::new("outer"), vec![]);
1313        let inner = dom.create_element(QualName::new("inner"), vec![]);
1314        dom.append_child(NodeId::DOCUMENT, outer);
1315        dom.append_child(outer, inner);
1316        dom.append_child(inner, outer);
1317        assert_eq!(dom.children(NodeId::DOCUMENT), vec![outer]);
1318        assert_eq!(dom.children(outer), vec![inner]);
1319        assert!(dom.children(inner).is_empty());
1320    }
1321
1322    #[test]
1323    fn node_type_test() {
1324        let mut dom = Dom::new();
1325        assert_eq!(dom.node_type(NodeId::DOCUMENT), 9);
1326        let el = dom.create_element(QualName::new("div"), vec![]);
1327        assert_eq!(dom.node_type(el), 1);
1328        let text = dom.create_text("hi".to_string());
1329        assert_eq!(dom.node_type(text), 3);
1330        let comment = dom.create_comment("c".to_string());
1331        assert_eq!(dom.node_type(comment), 8);
1332    }
1333
1334    #[test]
1335    fn element_local_name() {
1336        let dom = build_test_dom();
1337        let html_el = DomElement::new(&dom, dom.children(NodeId::DOCUMENT)[0]).unwrap();
1338        assert_eq!(html_el.local_name(), "html");
1339    }
1340
1341    #[test]
1342    fn element_id_and_class() {
1343        let dom = build_test_dom();
1344        let html = dom.children(NodeId::DOCUMENT)[0];
1345        let body = dom.children(html)[0];
1346        let div = dom.children(body)[0];
1347        let el = DomElement::new(&dom, div).unwrap();
1348
1349        assert_eq!(el.id(), Some("main"));
1350        assert!(el.has_class("container"));
1351        assert!(el.has_class("active"));
1352        assert!(!el.has_class("inactive"));
1353    }
1354
1355    #[test]
1356    fn parent_and_children() {
1357        let dom = build_test_dom();
1358        let html = dom.children(NodeId::DOCUMENT)[0];
1359        let body = dom.children(html)[0];
1360        let div = dom.children(body)[0];
1361        let p = dom.children(div)[0];
1362
1363        let p_el = DomElement::new(&dom, p).unwrap();
1364        let parent = p_el.parent_element().unwrap();
1365        assert_eq!(parent.local_name(), "div");
1366
1367        let div_el = DomElement::new(&dom, div).unwrap();
1368        let first_child = div_el.first_child_element().unwrap();
1369        assert_eq!(first_child.local_name(), "p");
1370    }
1371
1372    #[test]
1373    fn is_root_test() {
1374        let dom = build_test_dom();
1375        let html = dom.children(NodeId::DOCUMENT)[0];
1376        let body = dom.children(html)[0];
1377
1378        let html_el = DomElement::new(&dom, html).unwrap();
1379        assert!(html_el.is_root());
1380
1381        let body_el = DomElement::new(&dom, body).unwrap();
1382        assert!(!body_el.is_root());
1383    }
1384
1385    #[test]
1386    fn is_empty_test() {
1387        let dom = build_test_dom();
1388        let html = dom.children(NodeId::DOCUMENT)[0];
1389        let body = dom.children(html)[0];
1390        let div = dom.children(body)[0];
1391
1392        let div_el = DomElement::new(&dom, div).unwrap();
1393        assert!(!div_el.is_empty());
1394    }
1395
1396    // BDD: "Parse simple HTML" scenario
1397    #[test]
1398    fn bdd_parse_simple_html() {
1399        let mut dom = Dom::new();
1400        let html = dom.create_element(QualName::new("html"), vec![]);
1401        let body = dom.create_element(QualName::new("body"), vec![]);
1402        let h1 = dom.create_element(QualName::new("h1"), vec![]);
1403        let text = dom.create_text("Hello".to_string());
1404
1405        dom.append_child(NodeId::DOCUMENT, html);
1406        dom.append_child(html, body);
1407        dom.append_child(body, h1);
1408        dom.append_child(h1, text);
1409
1410        assert!(dom.get(NodeId::DOCUMENT).is_some());
1411        let h1_children = dom.children(h1);
1412        assert_eq!(h1_children.len(), 1);
1413        assert_eq!(dom.text_content(h1), "Hello");
1414    }
1415
1416    // BDD: "Query elements by selector" scenario
1417    #[test]
1418    fn bdd_query_elements_by_tag() {
1419        let mut dom = Dom::new();
1420        let div = dom.create_element(
1421            QualName::new("div"),
1422            vec![Attribute {
1423                name: QualName::new("class"),
1424                value: "content".to_string(),
1425            }],
1426        );
1427        let p1 = dom.create_element(QualName::new("p"), vec![]);
1428        let t1 = dom.create_text("First".to_string());
1429        let p2 = dom.create_element(QualName::new("p"), vec![]);
1430        let t2 = dom.create_text("Second".to_string());
1431
1432        dom.append_child(NodeId::DOCUMENT, div);
1433        dom.append_child(div, p1);
1434        dom.append_child(p1, t1);
1435        dom.append_child(div, p2);
1436        dom.append_child(p2, t2);
1437
1438        let ps = dom.get_elements_by_tag_name(NodeId::DOCUMENT, "p");
1439        assert_eq!(ps.len(), 2);
1440        assert_eq!(dom.text_content(ps[0]), "First");
1441    }
1442
1443    // BDD: "Mutate DOM tree" scenario
1444    #[test]
1445    fn bdd_mutate_dom_tree() {
1446        let mut dom = Dom::new();
1447        let div = dom.create_element(QualName::new("div"), vec![]);
1448        let span = dom.create_element(QualName::new("span"), vec![]);
1449        let old_text = dom.create_text("Old".to_string());
1450
1451        dom.append_child(NodeId::DOCUMENT, div);
1452        dom.append_child(div, span);
1453        dom.append_child(span, old_text);
1454
1455        let p = dom.create_element(QualName::new("p"), vec![]);
1456        let new_text = dom.create_text("New".to_string());
1457        dom.append_child(div, p);
1458        dom.append_child(p, new_text);
1459
1460        assert_eq!(dom.children(div).len(), 2);
1461    }
1462}