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        // SAFETY: DomElement::new() validated the node exists; this holds for the lifetime.
828        match self.dom.get(self.id) {
829            Some(n) => n,
830            None => unreachable!(
831                "DomElement::node invariant: node must exist (validated in constructor)"
832            ),
833        }
834    }
835
836    fn element_data(&self) -> &ElementData {
837        // SAFETY: DomElement::new() validated the node is an element; this holds for the lifetime.
838        match self.node().as_element() {
839            Some(d) => d,
840            None => unreachable!(
841                "DomElement::element_data invariant: node must be element (validated in constructor)"
842            ),
843        }
844    }
845}
846
847impl<'a> std::fmt::Debug for DomElement<'a> {
848    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
849        let data = self.element_data();
850        write!(f, "<{}", data.name.local)?;
851        for attr in &data.attrs {
852            write!(f, " {}=\"{}\"", attr.name.local, attr.value)?;
853        }
854        write!(f, ">")
855    }
856}
857
858impl<'a> Element for DomElement<'a> {
859    fn local_name(&self) -> &str {
860        &self.element_data().name.local
861    }
862
863    fn namespace(&self) -> Option<&str> {
864        self.element_data().name.ns.as_deref()
865    }
866
867    fn id(&self) -> Option<&str> {
868        self.element_data()
869            .attrs
870            .iter()
871            .find(|a| a.name.local == "id")
872            .map(|a| a.value.as_str())
873    }
874
875    fn has_class(&self, name: &str) -> bool {
876        self.element_data()
877            .attrs
878            .iter()
879            .find(|a| a.name.local == "class")
880            .is_some_and(|a| a.value.split_whitespace().any(|c| c == name))
881    }
882
883    fn has_attribute(&self, name: &str) -> bool {
884        self.element_data()
885            .attrs
886            .iter()
887            .any(|a| a.name.local.eq_ignore_ascii_case(name))
888    }
889
890    fn attribute_value(&self, name: &str) -> Option<&str> {
891        self.element_data()
892            .attrs
893            .iter()
894            .find(|a| a.name.local.eq_ignore_ascii_case(name))
895            .map(|a| a.value.as_str())
896    }
897
898    fn parent_element(&self) -> Option<Self> {
899        let mut parent_id = self.node().parent?;
900        loop {
901            let parent = self.dom.get(parent_id)?;
902            if parent.is_element() {
903                return Some(DomElement {
904                    dom: self.dom,
905                    id: parent_id,
906                });
907            }
908            parent_id = parent.parent?;
909        }
910    }
911
912    fn prev_sibling_element(&self) -> Option<Self> {
913        let mut sib_id = self.node().prev_sibling?;
914        loop {
915            let sib = self.dom.get(sib_id)?;
916            if sib.is_element() {
917                return Some(DomElement {
918                    dom: self.dom,
919                    id: sib_id,
920                });
921            }
922            sib_id = sib.prev_sibling?;
923        }
924    }
925
926    fn next_sibling_element(&self) -> Option<Self> {
927        let mut sib_id = self.node().next_sibling?;
928        loop {
929            let sib = self.dom.get(sib_id)?;
930            if sib.is_element() {
931                return Some(DomElement {
932                    dom: self.dom,
933                    id: sib_id,
934                });
935            }
936            sib_id = sib.next_sibling?;
937        }
938    }
939
940    fn first_child_element(&self) -> Option<Self> {
941        let mut child_id = self.node().first_child?;
942        loop {
943            let child = self.dom.get(child_id)?;
944            if child.is_element() {
945                return Some(DomElement {
946                    dom: self.dom,
947                    id: child_id,
948                });
949            }
950            child_id = child.next_sibling?;
951        }
952    }
953
954    fn last_child_element(&self) -> Option<Self> {
955        let mut child_id = self.node().last_child?;
956        loop {
957            let child = self.dom.get(child_id)?;
958            if child.is_element() {
959                return Some(DomElement {
960                    dom: self.dom,
961                    id: child_id,
962                });
963            }
964            child_id = child.prev_sibling?;
965        }
966    }
967
968    fn is_root(&self) -> bool {
969        match self.node().parent {
970            Some(parent_id) => self
971                .dom
972                .get(parent_id)
973                .is_some_and(|n| matches!(n.data, NodeData::Document)),
974            None => false,
975        }
976    }
977
978    fn is_empty(&self) -> bool {
979        let mut child_id = self.node().first_child;
980        while let Some(id) = child_id {
981            if let Some(child) = self.dom.get(id) {
982                match &child.data {
983                    NodeData::Element(_) => return false,
984                    NodeData::Text(t) if !t.is_empty() => return false,
985                    _ => {}
986                }
987                child_id = child.next_sibling;
988            } else {
989                break;
990            }
991        }
992        true
993    }
994
995    fn is_link(&self) -> bool {
996        let name = self.local_name();
997        (name == "a" || name == "area") && self.has_attribute("href")
998    }
999}
1000
1001// ---------------------------------------------------------------------------
1002// Tests
1003// ---------------------------------------------------------------------------
1004
1005#[cfg(test)]
1006mod tests {
1007    use super::*;
1008
1009    fn build_test_dom() -> Dom {
1010        let mut dom = Dom::new();
1011        let html = dom.create_element(QualName::new("html"), vec![]);
1012        dom.append_child(NodeId::DOCUMENT, html);
1013
1014        let body = dom.create_element(QualName::new("body"), vec![]);
1015        dom.append_child(html, body);
1016
1017        let div = dom.create_element(
1018            QualName::new("div"),
1019            vec![
1020                Attribute {
1021                    name: QualName::new("id"),
1022                    value: "main".to_string(),
1023                },
1024                Attribute {
1025                    name: QualName::new("class"),
1026                    value: "container active".to_string(),
1027                },
1028            ],
1029        );
1030        dom.append_child(body, div);
1031
1032        let p = dom.create_element(QualName::new("p"), vec![]);
1033        dom.append_child(div, p);
1034
1035        let text = dom.create_text("Hello world".to_string());
1036        dom.append_child(p, text);
1037
1038        dom
1039    }
1040
1041    #[test]
1042    fn new_dom_has_document() {
1043        let dom = Dom::new();
1044        assert_eq!(dom.len(), 1);
1045        assert!(dom.get(NodeId::DOCUMENT).is_some());
1046    }
1047
1048    #[test]
1049    fn create_and_append() {
1050        let mut dom = Dom::new();
1051        let div = dom.create_element(QualName::new("div"), vec![]);
1052        dom.append_child(NodeId::DOCUMENT, div);
1053
1054        assert_eq!(dom.children(NodeId::DOCUMENT), vec![div]);
1055        assert_eq!(dom.get(div).unwrap().parent, Some(NodeId::DOCUMENT));
1056    }
1057
1058    #[test]
1059    fn append_multiple_children() {
1060        let mut dom = Dom::new();
1061        let a = dom.create_element(QualName::new("a"), vec![]);
1062        let b = dom.create_element(QualName::new("b"), vec![]);
1063        let c = dom.create_element(QualName::new("c"), vec![]);
1064        dom.append_child(NodeId::DOCUMENT, a);
1065        dom.append_child(NodeId::DOCUMENT, b);
1066        dom.append_child(NodeId::DOCUMENT, c);
1067
1068        assert_eq!(dom.children(NodeId::DOCUMENT), vec![a, b, c]);
1069        assert_eq!(dom.get(a).unwrap().next_sibling, Some(b));
1070        assert_eq!(dom.get(b).unwrap().prev_sibling, Some(a));
1071        assert_eq!(dom.get(b).unwrap().next_sibling, Some(c));
1072    }
1073
1074    #[test]
1075    fn insert_before_test() {
1076        let mut dom = Dom::new();
1077        let a = dom.create_element(QualName::new("a"), vec![]);
1078        let c = dom.create_element(QualName::new("c"), vec![]);
1079        dom.append_child(NodeId::DOCUMENT, a);
1080        dom.append_child(NodeId::DOCUMENT, c);
1081
1082        let b = dom.create_element(QualName::new("b"), vec![]);
1083        dom.insert_before(NodeId::DOCUMENT, b, c);
1084
1085        assert_eq!(dom.children(NodeId::DOCUMENT), vec![a, b, c]);
1086    }
1087
1088    #[test]
1089    fn detach_test() {
1090        let mut dom = Dom::new();
1091        let a = dom.create_element(QualName::new("a"), vec![]);
1092        let b = dom.create_element(QualName::new("b"), vec![]);
1093        let c = dom.create_element(QualName::new("c"), vec![]);
1094        dom.append_child(NodeId::DOCUMENT, a);
1095        dom.append_child(NodeId::DOCUMENT, b);
1096        dom.append_child(NodeId::DOCUMENT, c);
1097
1098        dom.detach(b);
1099        assert_eq!(dom.children(NodeId::DOCUMENT), vec![a, c]);
1100        assert_eq!(dom.get(a).unwrap().next_sibling, Some(c));
1101        assert_eq!(dom.get(c).unwrap().prev_sibling, Some(a));
1102    }
1103
1104    #[test]
1105    fn remove_recycles() {
1106        let mut dom = Dom::new();
1107        let a = dom.create_element(QualName::new("a"), vec![]);
1108        dom.append_child(NodeId::DOCUMENT, a);
1109        dom.remove(a);
1110
1111        let b = dom.create_element(QualName::new("b"), vec![]);
1112        assert_eq!(b.0, a.0);
1113    }
1114
1115    #[test]
1116    fn text_content_test() {
1117        let mut dom = Dom::new();
1118        let div = dom.create_element(QualName::new("div"), vec![]);
1119        let text1 = dom.create_text("Hello ".to_string());
1120        let span = dom.create_element(QualName::new("span"), vec![]);
1121        let text2 = dom.create_text("world".to_string());
1122
1123        dom.append_child(NodeId::DOCUMENT, div);
1124        dom.append_child(div, text1);
1125        dom.append_child(div, span);
1126        dom.append_child(span, text2);
1127
1128        assert_eq!(dom.text_content(div), "Hello world");
1129    }
1130
1131    #[test]
1132    fn child_elements_skips_text() {
1133        let mut dom = Dom::new();
1134        let div = dom.create_element(QualName::new("div"), vec![]);
1135        let text = dom.create_text("hello".to_string());
1136        let span = dom.create_element(QualName::new("span"), vec![]);
1137
1138        dom.append_child(NodeId::DOCUMENT, div);
1139        dom.append_child(div, text);
1140        dom.append_child(div, span);
1141
1142        assert_eq!(dom.child_elements(div), vec![span]);
1143    }
1144
1145    #[test]
1146    fn reparent_children_test() {
1147        let mut dom = Dom::new();
1148        let source = dom.create_element(QualName::new("source"), vec![]);
1149        let target = dom.create_element(QualName::new("target"), vec![]);
1150        let a = dom.create_element(QualName::new("a"), vec![]);
1151        let b = dom.create_element(QualName::new("b"), vec![]);
1152
1153        dom.append_child(NodeId::DOCUMENT, source);
1154        dom.append_child(NodeId::DOCUMENT, target);
1155        dom.append_child(source, a);
1156        dom.append_child(source, b);
1157
1158        dom.reparent_children(source, target);
1159
1160        assert!(dom.children(source).is_empty());
1161        assert_eq!(dom.children(target), vec![a, b]);
1162    }
1163
1164    #[test]
1165    fn node_id_raw_roundtrip() {
1166        let id = NodeId::from_raw(42);
1167        assert_eq!(id.to_raw(), 42);
1168    }
1169
1170    #[test]
1171    fn set_text_content_test() {
1172        let mut dom = Dom::new();
1173        let div = dom.create_element(QualName::new("div"), vec![]);
1174        let span = dom.create_element(QualName::new("span"), vec![]);
1175        let text = dom.create_text("old".to_string());
1176        dom.append_child(NodeId::DOCUMENT, div);
1177        dom.append_child(div, span);
1178        dom.append_child(span, text);
1179
1180        dom.set_text_content(div, "new text");
1181        assert_eq!(dom.text_content(div), "new text");
1182        assert_eq!(dom.child_elements(div).len(), 0);
1183    }
1184
1185    #[test]
1186    fn get_element_by_id_test() {
1187        let mut dom = Dom::new();
1188        let div = dom.create_element(
1189            QualName::new("div"),
1190            vec![Attribute {
1191                name: QualName::new("id"),
1192                value: "main".to_string(),
1193            }],
1194        );
1195        dom.append_child(NodeId::DOCUMENT, div);
1196
1197        assert_eq!(dom.get_element_by_id("main"), Some(div));
1198        assert_eq!(dom.get_element_by_id("nonexistent"), None);
1199    }
1200
1201    #[test]
1202    fn get_elements_by_tag_name_test() {
1203        let mut dom = Dom::new();
1204        let div = dom.create_element(QualName::new("div"), vec![]);
1205        let p1 = dom.create_element(QualName::new("p"), vec![]);
1206        let p2 = dom.create_element(QualName::new("p"), vec![]);
1207        let span = dom.create_element(QualName::new("span"), vec![]);
1208        dom.append_child(NodeId::DOCUMENT, div);
1209        dom.append_child(div, p1);
1210        dom.append_child(div, span);
1211        dom.append_child(div, p2);
1212
1213        let ps = dom.get_elements_by_tag_name(NodeId::DOCUMENT, "p");
1214        assert_eq!(ps, vec![p1, p2]);
1215    }
1216
1217    #[test]
1218    fn get_elements_by_class_name_test() {
1219        let mut dom = Dom::new();
1220        let a = dom.create_element(
1221            QualName::new("div"),
1222            vec![Attribute {
1223                name: QualName::new("class"),
1224                value: "foo bar".to_string(),
1225            }],
1226        );
1227        let b = dom.create_element(
1228            QualName::new("div"),
1229            vec![Attribute {
1230                name: QualName::new("class"),
1231                value: "baz".to_string(),
1232            }],
1233        );
1234        dom.append_child(NodeId::DOCUMENT, a);
1235        dom.append_child(NodeId::DOCUMENT, b);
1236
1237        assert_eq!(
1238            dom.get_elements_by_class_name(NodeId::DOCUMENT, "foo"),
1239            vec![a]
1240        );
1241        assert_eq!(
1242            dom.get_elements_by_class_name(NodeId::DOCUMENT, "baz"),
1243            vec![b]
1244        );
1245    }
1246
1247    #[test]
1248    fn serialize_html_basic() {
1249        let mut dom = Dom::new();
1250        let div = dom.create_element(
1251            QualName::new("div"),
1252            vec![Attribute {
1253                name: QualName::new("id"),
1254                value: "main".to_string(),
1255            }],
1256        );
1257        let text = dom.create_text("Hello".to_string());
1258        dom.append_child(NodeId::DOCUMENT, div);
1259        dom.append_child(div, text);
1260
1261        assert_eq!(dom.serialize_html(div), "<div id=\"main\">Hello</div>");
1262    }
1263
1264    #[test]
1265    fn serialize_inner_html_test() {
1266        let mut dom = Dom::new();
1267        let div = dom.create_element(QualName::new("div"), vec![]);
1268        let span = dom.create_element(QualName::new("span"), vec![]);
1269        let text = dom.create_text("hi".to_string());
1270        dom.append_child(NodeId::DOCUMENT, div);
1271        dom.append_child(div, span);
1272        dom.append_child(span, text);
1273
1274        assert_eq!(dom.serialize_inner_html(div), "<span>hi</span>");
1275    }
1276
1277    #[test]
1278    fn merge_subtree_test() {
1279        let mut source = Dom::new();
1280        let div = source.create_element(QualName::new("div"), vec![]);
1281        let text = source.create_text("copied".to_string());
1282        source.append_child(NodeId::DOCUMENT, div);
1283        source.append_child(div, text);
1284
1285        let mut target = Dom::new();
1286        let parent = target.create_element(QualName::new("body"), vec![]);
1287        target.append_child(NodeId::DOCUMENT, parent);
1288
1289        let new_div = target.merge_subtree(&source, div);
1290        target.append_child(parent, new_div);
1291
1292        assert_eq!(target.text_content(new_div), "copied");
1293        assert_eq!(
1294            target
1295                .get(new_div)
1296                .and_then(|n| n.as_element())
1297                .map(|e| e.name.local.as_str()),
1298            Some("div")
1299        );
1300    }
1301
1302    #[test]
1303    fn append_child_rejects_self_cycle() {
1304        let mut dom = Dom::new();
1305        let div = dom.create_element(QualName::new("div"), vec![]);
1306        dom.append_child(NodeId::DOCUMENT, div);
1307        dom.append_child(div, div);
1308        assert_eq!(dom.children(NodeId::DOCUMENT), vec![div]);
1309        assert!(dom.children(div).is_empty());
1310        assert_eq!(dom.get(div).unwrap().parent, Some(NodeId::DOCUMENT));
1311    }
1312
1313    #[test]
1314    fn append_child_rejects_ancestor_cycle() {
1315        let mut dom = Dom::new();
1316        let outer = dom.create_element(QualName::new("outer"), vec![]);
1317        let inner = dom.create_element(QualName::new("inner"), vec![]);
1318        dom.append_child(NodeId::DOCUMENT, outer);
1319        dom.append_child(outer, inner);
1320        dom.append_child(inner, outer);
1321        assert_eq!(dom.children(NodeId::DOCUMENT), vec![outer]);
1322        assert_eq!(dom.children(outer), vec![inner]);
1323        assert!(dom.children(inner).is_empty());
1324    }
1325
1326    #[test]
1327    fn node_type_test() {
1328        let mut dom = Dom::new();
1329        assert_eq!(dom.node_type(NodeId::DOCUMENT), 9);
1330        let el = dom.create_element(QualName::new("div"), vec![]);
1331        assert_eq!(dom.node_type(el), 1);
1332        let text = dom.create_text("hi".to_string());
1333        assert_eq!(dom.node_type(text), 3);
1334        let comment = dom.create_comment("c".to_string());
1335        assert_eq!(dom.node_type(comment), 8);
1336    }
1337
1338    #[test]
1339    fn element_local_name() {
1340        let dom = build_test_dom();
1341        let html_el = DomElement::new(&dom, dom.children(NodeId::DOCUMENT)[0]).unwrap();
1342        assert_eq!(html_el.local_name(), "html");
1343    }
1344
1345    #[test]
1346    fn element_id_and_class() {
1347        let dom = build_test_dom();
1348        let html = dom.children(NodeId::DOCUMENT)[0];
1349        let body = dom.children(html)[0];
1350        let div = dom.children(body)[0];
1351        let el = DomElement::new(&dom, div).unwrap();
1352
1353        assert_eq!(el.id(), Some("main"));
1354        assert!(el.has_class("container"));
1355        assert!(el.has_class("active"));
1356        assert!(!el.has_class("inactive"));
1357    }
1358
1359    #[test]
1360    fn parent_and_children() {
1361        let dom = build_test_dom();
1362        let html = dom.children(NodeId::DOCUMENT)[0];
1363        let body = dom.children(html)[0];
1364        let div = dom.children(body)[0];
1365        let p = dom.children(div)[0];
1366
1367        let p_el = DomElement::new(&dom, p).unwrap();
1368        let parent = p_el.parent_element().unwrap();
1369        assert_eq!(parent.local_name(), "div");
1370
1371        let div_el = DomElement::new(&dom, div).unwrap();
1372        let first_child = div_el.first_child_element().unwrap();
1373        assert_eq!(first_child.local_name(), "p");
1374    }
1375
1376    #[test]
1377    fn is_root_test() {
1378        let dom = build_test_dom();
1379        let html = dom.children(NodeId::DOCUMENT)[0];
1380        let body = dom.children(html)[0];
1381
1382        let html_el = DomElement::new(&dom, html).unwrap();
1383        assert!(html_el.is_root());
1384
1385        let body_el = DomElement::new(&dom, body).unwrap();
1386        assert!(!body_el.is_root());
1387    }
1388
1389    #[test]
1390    fn is_empty_test() {
1391        let dom = build_test_dom();
1392        let html = dom.children(NodeId::DOCUMENT)[0];
1393        let body = dom.children(html)[0];
1394        let div = dom.children(body)[0];
1395
1396        let div_el = DomElement::new(&dom, div).unwrap();
1397        assert!(!div_el.is_empty());
1398    }
1399
1400    // BDD: "Parse simple HTML" scenario
1401    #[test]
1402    fn bdd_parse_simple_html() {
1403        let mut dom = Dom::new();
1404        let html = dom.create_element(QualName::new("html"), vec![]);
1405        let body = dom.create_element(QualName::new("body"), vec![]);
1406        let h1 = dom.create_element(QualName::new("h1"), vec![]);
1407        let text = dom.create_text("Hello".to_string());
1408
1409        dom.append_child(NodeId::DOCUMENT, html);
1410        dom.append_child(html, body);
1411        dom.append_child(body, h1);
1412        dom.append_child(h1, text);
1413
1414        assert!(dom.get(NodeId::DOCUMENT).is_some());
1415        let h1_children = dom.children(h1);
1416        assert_eq!(h1_children.len(), 1);
1417        assert_eq!(dom.text_content(h1), "Hello");
1418    }
1419
1420    // BDD: "Query elements by selector" scenario
1421    #[test]
1422    fn bdd_query_elements_by_tag() {
1423        let mut dom = Dom::new();
1424        let div = dom.create_element(
1425            QualName::new("div"),
1426            vec![Attribute {
1427                name: QualName::new("class"),
1428                value: "content".to_string(),
1429            }],
1430        );
1431        let p1 = dom.create_element(QualName::new("p"), vec![]);
1432        let t1 = dom.create_text("First".to_string());
1433        let p2 = dom.create_element(QualName::new("p"), vec![]);
1434        let t2 = dom.create_text("Second".to_string());
1435
1436        dom.append_child(NodeId::DOCUMENT, div);
1437        dom.append_child(div, p1);
1438        dom.append_child(p1, t1);
1439        dom.append_child(div, p2);
1440        dom.append_child(p2, t2);
1441
1442        let ps = dom.get_elements_by_tag_name(NodeId::DOCUMENT, "p");
1443        assert_eq!(ps.len(), 2);
1444        assert_eq!(dom.text_content(ps[0]), "First");
1445    }
1446
1447    // BDD: "Mutate DOM tree" scenario
1448    #[test]
1449    fn bdd_mutate_dom_tree() {
1450        let mut dom = Dom::new();
1451        let div = dom.create_element(QualName::new("div"), vec![]);
1452        let span = dom.create_element(QualName::new("span"), vec![]);
1453        let old_text = dom.create_text("Old".to_string());
1454
1455        dom.append_child(NodeId::DOCUMENT, div);
1456        dom.append_child(div, span);
1457        dom.append_child(span, old_text);
1458
1459        let p = dom.create_element(QualName::new("p"), vec![]);
1460        let new_text = dom.create_text("New".to_string());
1461        dom.append_child(div, p);
1462        dom.append_child(p, new_text);
1463
1464        assert_eq!(dom.children(div).len(), 2);
1465    }
1466}