Skip to main content

hpx_browser/
dom.rs

1use ahash::AHashSet;
2use blitz_dom::{
3    Attribute as BlitzAttribute, BaseDocument, DocumentConfig, ElementData as BlitzElementData,
4    Node as BlitzNode, NodeData as BlitzNodeData, QualName as H5QualName, ns,
5};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub struct NodeId(pub(crate) u64);
9
10impl NodeId {
11    #[must_use]
12    pub fn from_raw(v: u32) -> Self {
13        // JS-side raw IDs are slot indices (low 32 bits) with no version.
14        // Reconstruct a blitz NodeId using only the index; the version is
15        // resolved at lookup time by `BaseDocument::get_node` (returns None
16        // for stale versions).
17        Self(u64::from(v))
18    }
19
20    #[must_use]
21    pub fn to_raw(self) -> u32 {
22        // Expose only the slot index to JS. The version lives in the high
23        // 32 bits and is an internal detail of the blitz DOM.
24        #[expect(clippy::cast_possible_truncation, reason = "extracting slot index")]
25        {
26            self.0 as u32
27        }
28    }
29
30    /// Convert to `f64` for JS interop. Carries the full 64-bit value
31    /// (slot index + version) so versioned NodeIds round-trip through JS
32    /// numbers, which can represent integers up to 2^53 exactly. Use this
33    /// instead of [`to_raw`](Self::to_raw) when the version must survive a
34    /// hop through JavaScript (e.g. the document node has a non-zero
35    /// version and `from_raw(0)` would not resolve).
36    #[must_use]
37    pub fn to_f64(self) -> f64 {
38        self.0 as f64
39    }
40
41    /// Reconstruct a NodeId from an `f64` produced by [`to_f64`](Self::to_f64).
42    #[must_use]
43    pub fn from_f64(v: f64) -> Self {
44        Self(v as u64)
45    }
46
47    /// Convert to the blitz-dom node id (`usize`).
48    pub(crate) fn to_blitz(self) -> usize {
49        self.0 as usize
50    }
51
52    /// Wrap a blitz-dom node id (`usize`) in our local newtype.
53    pub(crate) fn from_blitz(id: usize) -> Self {
54        Self(id as u64)
55    }
56}
57
58/// Iterator over the children of a node, reading directly from the
59/// underlying `BaseDocument` children vec without allocating.
60pub struct ChildrenIter<'a> {
61    iter: std::slice::Iter<'a, usize>,
62}
63
64impl<'a> Iterator for ChildrenIter<'a> {
65    type Item = NodeId;
66    fn next(&mut self) -> Option<NodeId> {
67        self.iter.next().map(|&id| NodeId::from_blitz(id))
68    }
69    fn size_hint(&self) -> (usize, Option<usize>) {
70        self.iter.size_hint()
71    }
72}
73
74impl<'a> DoubleEndedIterator for ChildrenIter<'a> {
75    fn next_back(&mut self) -> Option<NodeId> {
76        self.iter.next_back().map(|&id| NodeId::from_blitz(id))
77    }
78}
79
80impl<'a> ExactSizeIterator for ChildrenIter<'a> {}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub struct QualName {
84    pub ns: Option<String>,
85    pub local: String,
86}
87
88impl QualName {
89    pub fn new(local: impl Into<String>) -> Self {
90        Self {
91            ns: None,
92            local: local.into(),
93        }
94    }
95
96    pub fn with_ns(ns: impl Into<String>, local: impl Into<String>) -> Self {
97        Self {
98            ns: Some(ns.into()),
99            local: local.into(),
100        }
101    }
102}
103
104impl QualName {
105    fn to_h5(&self) -> H5QualName {
106        let ns = match &self.ns {
107            Some(ns) => ns.as_str().into(),
108            None => ns!(html),
109        };
110        H5QualName::new(None, ns, self.local.as_str().into())
111    }
112
113    fn from_h5(qn: &H5QualName) -> Self {
114        let ns_str = qn.ns.to_string();
115        let ns = if ns_str.is_empty() || ns_str == "http://www.w3.org/1999/xhtml" {
116            None
117        } else {
118            Some(ns_str)
119        };
120        QualName {
121            ns,
122            local: qn.local.to_string(),
123        }
124    }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct Attribute {
129    pub name: QualName,
130    pub value: String,
131}
132
133impl Attribute {
134    pub(crate) fn to_blitz(&self) -> BlitzAttribute {
135        BlitzAttribute {
136            name: self.name.to_h5(),
137            value: self.value.clone(),
138        }
139    }
140}
141
142#[derive(Debug, Clone)]
143pub enum NodeData {
144    Document,
145    DocumentType {
146        name: String,
147        public_id: String,
148        system_id: String,
149    },
150    Element(ElementData),
151    Text(String),
152    Comment(String),
153    ProcessingInstruction {
154        target: String,
155        data: String,
156    },
157    DocumentFragment,
158    ShadowRoot {
159        mode: ShadowRootMode,
160        host: NodeId,
161    },
162}
163
164#[derive(Debug, Clone)]
165pub struct ElementData {
166    pub name: QualName,
167    pub attrs: Vec<Attribute>,
168    pub shadow_root: Option<NodeId>,
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum ShadowRootMode {
173    Open,
174    Closed,
175}
176
177#[derive(Debug, Clone)]
178pub struct Node {
179    pub id: NodeId,
180    pub data: NodeData,
181    pub parent: Option<NodeId>,
182    pub first_child: Option<NodeId>,
183    pub last_child: Option<NodeId>,
184    pub prev_sibling: Option<NodeId>,
185    pub next_sibling: Option<NodeId>,
186}
187
188impl Node {
189    pub fn is_element(&self) -> bool {
190        matches!(self.data, NodeData::Element(_))
191    }
192
193    pub fn as_element(&self) -> Option<&ElementData> {
194        match &self.data {
195            NodeData::Element(data) => Some(data),
196            _ => None,
197        }
198    }
199
200    pub fn as_element_mut(&mut self) -> Option<&mut ElementData> {
201        match &mut self.data {
202            NodeData::Element(data) => Some(data),
203            _ => None,
204        }
205    }
206
207    pub fn as_text(&self) -> Option<&str> {
208        match &self.data {
209            NodeData::Text(t) => Some(t),
210            _ => None,
211        }
212    }
213
214    pub fn is_element_with_tag(&self, tag: &str) -> bool {
215        match &self.data {
216            NodeData::Element(e) => e.name.local.eq_ignore_ascii_case(tag),
217            _ => false,
218        }
219    }
220}
221
222/// Lightweight borrowed reference to a DOM node that reads directly from the
223/// underlying `BaseDocument`. Does **not** allocate `Vec<Attribute>`, `String`
224/// for `QualName`, or compute sibling positions — unlike [`Dom::get`].
225#[derive(Clone, Copy)]
226pub struct NodeRef<'a> {
227    dom: &'a Dom,
228    id: NodeId,
229}
230
231impl<'a> NodeRef<'a> {
232    pub fn id(&self) -> NodeId {
233        self.id
234    }
235
236    fn blitz_data(&self) -> Option<&'a BlitzNodeData> {
237        self.dom.inner.get_node(self.id.to_blitz()).map(|n| &n.data)
238    }
239
240    pub fn is_element(&self) -> bool {
241        matches!(
242            self.blitz_data(),
243            Some(BlitzNodeData::Element(_)) | Some(BlitzNodeData::AnonymousBlock(_))
244        )
245    }
246
247    pub fn is_text(&self) -> bool {
248        matches!(self.blitz_data(), Some(BlitzNodeData::Text(_)))
249    }
250
251    pub fn is_comment(&self) -> bool {
252        matches!(self.blitz_data(), Some(BlitzNodeData::Comment))
253    }
254
255    pub fn is_document(&self) -> bool {
256        matches!(self.blitz_data(), Some(BlitzNodeData::Document))
257    }
258
259    pub fn text(&self) -> Option<&'a str> {
260        match self.blitz_data() {
261            Some(BlitzNodeData::Text(t)) => Some(&t.content),
262            _ => None,
263        }
264    }
265
266    pub fn tag_name(&self) -> Option<&'a str> {
267        match self.blitz_data() {
268            Some(BlitzNodeData::Element(e)) | Some(BlitzNodeData::AnonymousBlock(e)) => {
269                Some(&e.name.local)
270            }
271            _ => None,
272        }
273    }
274
275    pub fn get_attr(&self, name: &str) -> Option<&'a str> {
276        match self.blitz_data() {
277            Some(BlitzNodeData::Element(e)) | Some(BlitzNodeData::AnonymousBlock(e)) => e
278                .attrs
279                .iter()
280                .find(|a| &*a.name.local == name)
281                .map(|a| a.value.as_str()),
282            _ => None,
283        }
284    }
285
286    pub fn has_class(&self, class: &str) -> bool {
287        self.get_attr("class")
288            .is_some_and(|v| v.split_whitespace().any(|c| c == class))
289    }
290
291    pub fn first_child(&self) -> Option<NodeId> {
292        self.dom
293            .inner
294            .get_node(self.id.to_blitz())
295            .and_then(|n| n.children.first().map(|&c| NodeId::from_blitz(c)))
296    }
297
298    pub fn next_sibling(&self) -> Option<NodeId> {
299        let node = self.dom.inner.get_node(self.id.to_blitz())?;
300        let parent_id = node.parent?;
301        let parent = self.dom.inner.get_node(parent_id)?;
302        let pos = parent
303            .children
304            .iter()
305            .position(|&c| c == self.id.to_blitz())?;
306        parent.children.get(pos + 1).map(|&c| NodeId::from_blitz(c))
307    }
308
309    pub fn parent(&self) -> Option<NodeId> {
310        self.dom
311            .inner
312            .get_node(self.id.to_blitz())
313            .and_then(|n| n.parent.map(NodeId::from_blitz))
314    }
315
316    pub fn node_type(&self) -> u32 {
317        match self.blitz_data() {
318            Some(BlitzNodeData::Element(_)) | Some(BlitzNodeData::AnonymousBlock(_)) => 1,
319            Some(BlitzNodeData::Text(_)) => 3,
320            Some(BlitzNodeData::Comment) => 8,
321            Some(BlitzNodeData::Document) => 9,
322            None => 0,
323        }
324    }
325}
326
327impl<'a> std::fmt::Debug for NodeRef<'a> {
328    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
329        match self.blitz_data() {
330            Some(BlitzNodeData::Element(e)) | Some(BlitzNodeData::AnonymousBlock(e)) => {
331                write!(f, "<{}", e.name.local)?;
332                for attr in e.attrs.iter() {
333                    write!(f, " {}=\"{}\"", attr.name.local, attr.value)?;
334                }
335                write!(f, ">")
336            }
337            Some(BlitzNodeData::Text(t)) => write!(f, "Text({:?})", t.content),
338            Some(BlitzNodeData::Comment) => write!(f, "Comment"),
339            Some(BlitzNodeData::Document) => write!(f, "Document"),
340            None => write!(f, "NodeRef(<invalid {}>)", self.id.0),
341        }
342    }
343}
344
345pub struct Dom {
346    inner: BaseDocument,
347}
348
349const WALK_LIMIT: usize = 2_000_000;
350const ANCESTOR_LIMIT: usize = 10_000;
351
352impl Dom {
353    pub fn new() -> Self {
354        let mut config = DocumentConfig::default();
355        config.style_threading = blitz_dom::StyleThreading::Sequential;
356        let inner = BaseDocument::new(config);
357        Self { inner }
358    }
359
360    pub fn from_base(inner: BaseDocument) -> Self {
361        Self { inner }
362    }
363
364    pub fn document(&self) -> NodeId {
365        NodeId::from_blitz(self.inner.root_node().id)
366    }
367
368    pub fn inner(&self) -> &BaseDocument {
369        &self.inner
370    }
371
372    pub fn inner_mut(&mut self) -> &mut BaseDocument {
373        &mut self.inner
374    }
375
376    /// Returns a lightweight borrowed reference to a node without allocating.
377    pub fn node_ref(&self, id: NodeId) -> NodeRef<'_> {
378        NodeRef { dom: self, id }
379    }
380
381    /// Returns an iterator over the children of a node, reading directly
382    /// from the `BaseDocument` children vec without allocating.
383    pub fn children_of(&self, id: NodeId) -> ChildrenIter<'_> {
384        let slice = self
385            .inner
386            .get_node(id.to_blitz())
387            .map(|n| n.children.as_slice())
388            .unwrap_or(&[]);
389        ChildrenIter { iter: slice.iter() }
390    }
391
392    #[cfg_attr(feature = "hotpath", hotpath::measure)]
393    pub fn get(&self, id: NodeId) -> Option<Node> {
394        let blitz_node = self.inner.get_node(id.to_blitz())?;
395        Some(self.convert_node(blitz_node))
396    }
397
398    pub fn get_mut(&mut self, id: NodeId) -> Option<&mut BaseDocument> {
399        self.inner.get_node_mut(id.to_blitz())?;
400        Some(&mut self.inner)
401    }
402
403    pub fn len(&self) -> usize {
404        self.inner.tree().len()
405    }
406
407    pub fn is_empty(&self) -> bool {
408        self.inner.tree().is_empty()
409    }
410
411    pub fn create_element(&mut self, name: QualName, attrs: Vec<Attribute>) -> NodeId {
412        let blitz_attrs: Vec<BlitzAttribute> = attrs.iter().map(|a| a.to_blitz()).collect();
413        let h5_name = name.to_h5();
414        let elem_data = BlitzElementData::new(h5_name, blitz_attrs);
415        let id = self.inner.create_node(BlitzNodeData::Element(elem_data));
416        NodeId::from_blitz(id)
417    }
418
419    pub fn create_text(&mut self, text: String) -> NodeId {
420        let id = self.inner.create_text_node(&text);
421        NodeId::from_blitz(id)
422    }
423
424    pub fn create_comment(&mut self, _text: String) -> NodeId {
425        let id = self.inner.create_node(BlitzNodeData::Comment);
426        NodeId::from_blitz(id)
427    }
428
429    pub fn create_document_fragment(&mut self) -> NodeId {
430        let id = self.inner.create_node(BlitzNodeData::Document);
431        NodeId::from_blitz(id)
432    }
433
434    pub fn create_shadow_root(&mut self, _host: NodeId, _mode: ShadowRootMode) -> NodeId {
435        let id = self.inner.create_node(BlitzNodeData::Document);
436        NodeId::from_blitz(id)
437    }
438
439    pub fn allocate_pi(&mut self, _target: String, _data: String) -> NodeId {
440        let id = self.inner.create_node(BlitzNodeData::Comment);
441        NodeId::from_blitz(id)
442    }
443
444    pub fn create_doctype(
445        &mut self,
446        _name: String,
447        _public_id: String,
448        _system_id: String,
449    ) -> NodeId {
450        let id = self.inner.create_node(BlitzNodeData::Document);
451        NodeId::from_blitz(id)
452    }
453
454    pub fn append_child(&mut self, parent: NodeId, child: NodeId) {
455        if self.inner.get_node(parent.to_blitz()).is_none()
456            || self.inner.get_node(child.to_blitz()).is_none()
457        {
458            return;
459        }
460        self.detach(child);
461        if let Some(parent_node) = self.inner.get_node_mut(parent.to_blitz()) {
462            parent_node.children.push(child.to_blitz());
463        }
464        if let Some(child_node) = self.inner.get_node_mut(child.to_blitz()) {
465            child_node.parent = Some(parent.to_blitz());
466        }
467    }
468
469    pub fn insert_before(&mut self, parent: NodeId, child: NodeId, reference: NodeId) {
470        if self.inner.get_node(parent.to_blitz()).is_none()
471            || self.inner.get_node(child.to_blitz()).is_none()
472            || self.inner.get_node(reference.to_blitz()).is_none()
473        {
474            return;
475        }
476        self.detach(child);
477        if let Some(parent_node) = self.inner.get_node_mut(parent.to_blitz()) {
478            if let Some(idx) = parent_node
479                .children
480                .iter()
481                .position(|&id| id == reference.to_blitz())
482            {
483                parent_node.children.insert(idx, child.to_blitz());
484            } else {
485                parent_node.children.push(child.to_blitz());
486            }
487        }
488        if let Some(child_node) = self.inner.get_node_mut(child.to_blitz()) {
489            child_node.parent = Some(parent.to_blitz());
490        }
491    }
492
493    pub fn detach(&mut self, id: NodeId) {
494        let parent_id = match self.inner.get_node(id.to_blitz()) {
495            Some(n) => n.parent,
496            None => return,
497        };
498        if let Some(pid) = parent_id {
499            if let Some(parent) = self.inner.get_node_mut(pid) {
500                parent.children.retain(|&c| c != id.to_blitz());
501            }
502        }
503        if let Some(node) = self.inner.get_node_mut(id.to_blitz()) {
504            node.parent = None;
505        }
506    }
507
508    pub fn remove(&mut self, id: NodeId) {
509        self.detach(id);
510        let children: Vec<usize> = self
511            .inner
512            .get_node(id.to_blitz())
513            .map(|n| n.children.to_vec())
514            .unwrap_or_default();
515        for child_id in children {
516            self.remove(NodeId::from_blitz(child_id));
517        }
518    }
519
520    pub fn reparent_children(&mut self, source: NodeId, target: NodeId) {
521        let children: Vec<usize> = self
522            .inner
523            .get_node(source.to_blitz())
524            .map(|n| n.children.to_vec())
525            .unwrap_or_default();
526        for child_id in children {
527            self.append_child(target, NodeId::from_blitz(child_id));
528        }
529    }
530
531    pub fn children(&self, parent: NodeId) -> Vec<NodeId> {
532        self.children_of(parent).collect()
533    }
534
535    pub fn child_elements(&self, parent: NodeId) -> Vec<NodeId> {
536        self.children_of(parent)
537            .filter(|&id| NodeRef { dom: self, id }.is_element())
538            .collect()
539    }
540
541    #[cfg_attr(feature = "hotpath", hotpath::measure)]
542    pub fn text_content(&self, id: NodeId) -> String {
543        let mut result = String::new();
544        self.collect_text(id, &mut result);
545        result
546    }
547
548    #[cfg_attr(feature = "hotpath", hotpath::measure)]
549    fn collect_text(&self, root: NodeId, result: &mut String) {
550        let mut stack: Vec<NodeId> = vec![root];
551        let mut visited: AHashSet<NodeId> = AHashSet::with_capacity(64);
552        let mut steps: usize = 0;
553        while let Some(id) = stack.pop() {
554            if !visited.insert(id) {
555                continue;
556            }
557            steps += 1;
558            if steps > WALK_LIMIT {
559                break;
560            }
561            let nr = NodeRef { dom: self, id };
562            if let Some(text) = nr.text() {
563                result.push_str(text);
564            } else {
565                stack.extend(self.children_of(id).rev());
566            }
567        }
568    }
569
570    pub fn set_text_content(&mut self, id: NodeId, text: &str) {
571        let children: Vec<NodeId> = self.children(id);
572        for child in children {
573            self.remove(child);
574        }
575        if !text.is_empty() {
576            let text_id = self.create_text(text.to_string());
577            self.append_child(id, text_id);
578        }
579    }
580
581    pub fn get_element_by_id(&self, id_value: &str) -> Option<NodeId> {
582        self.find_element(self.document(), &|nr| nr.get_attr("id") == Some(id_value))
583    }
584
585    pub fn get_elements_by_tag_name(&self, root: NodeId, tag: &str) -> Vec<NodeId> {
586        let mut results = Vec::new();
587        self.collect_elements(
588            root,
589            &|nr| nr.tag_name().is_some_and(|t| t.eq_ignore_ascii_case(tag)),
590            &mut results,
591        );
592        results
593    }
594
595    pub fn get_elements_by_class_name(&self, root: NodeId, class: &str) -> Vec<NodeId> {
596        let mut results = Vec::new();
597        self.collect_elements(root, &|nr| nr.has_class(class), &mut results);
598        results
599    }
600
601    pub fn serialize_html(&self, id: NodeId) -> String {
602        let mut out = String::new();
603        self.serialize_node(id, &mut out);
604        out
605    }
606
607    pub fn serialize_inner_html(&self, id: NodeId) -> String {
608        let mut out = String::new();
609        if self.inner.get_node(id.to_blitz()).is_none() {
610            return out;
611        }
612        for c in self.children_of(id) {
613            self.serialize_node(c, &mut out);
614        }
615        out
616    }
617
618    #[cfg_attr(feature = "hotpath", hotpath::measure)]
619    fn serialize_node(&self, root: NodeId, out: &mut String) {
620        enum SerWork {
621            Open(NodeId),
622            Close(NodeId),
623        }
624        const VOID_ELEMENTS: &[&str] = &[
625            "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
626            "source", "track", "wbr",
627        ];
628
629        let mut stack: Vec<SerWork> = vec![SerWork::Open(root)];
630        let mut visited: AHashSet<NodeId> = AHashSet::with_capacity(64);
631        let mut steps: usize = 0;
632        while let Some(work) = stack.pop() {
633            match work {
634                SerWork::Close(id) => {
635                    if let Some(blitz_node) = self.inner.get_node(id.to_blitz()) {
636                        if let BlitzNodeData::Element(e) | BlitzNodeData::AnonymousBlock(e) =
637                            &blitz_node.data
638                        {
639                            out.push_str("</");
640                            out.push_str(&e.name.local);
641                            out.push('>');
642                        }
643                    }
644                }
645                SerWork::Open(id) => {
646                    if !visited.insert(id) {
647                        continue;
648                    }
649                    steps += 1;
650                    if steps > WALK_LIMIT {
651                        break;
652                    }
653                    let blitz_node = match self.inner.get_node(id.to_blitz()) {
654                        Some(n) => n,
655                        None => continue,
656                    };
657                    match &blitz_node.data {
658                        BlitzNodeData::Element(e) | BlitzNodeData::AnonymousBlock(e) => {
659                            out.push('<');
660                            out.push_str(&e.name.local);
661                            for attr in e.attrs.iter() {
662                                out.push(' ');
663                                out.push_str(&attr.name.local);
664                                out.push_str("=\"");
665                                out.push_str(
666                                    &attr.value.replace('&', "&amp;").replace('"', "&quot;"),
667                                );
668                                out.push('"');
669                            }
670                            out.push('>');
671                            let tag: &str = &e.name.local;
672                            if !VOID_ELEMENTS.contains(&tag) {
673                                stack.push(SerWork::Close(id));
674                            }
675                            for c in self.children_of(id).rev() {
676                                stack.push(SerWork::Open(c));
677                            }
678                        }
679                        BlitzNodeData::Text(t) => {
680                            out.push_str(
681                                &t.content
682                                    .replace('&', "&amp;")
683                                    .replace('<', "&lt;")
684                                    .replace('>', "&gt;"),
685                            );
686                        }
687                        BlitzNodeData::Comment => {
688                            out.push_str("<!--");
689                            out.push_str("-->");
690                        }
691                        BlitzNodeData::Document => {
692                            for c in self.children_of(id).rev() {
693                                stack.push(SerWork::Open(c));
694                            }
695                        }
696                    }
697                }
698            }
699        }
700    }
701
702    pub fn merge_subtree(&mut self, source: &Dom, source_root: NodeId) -> NodeId {
703        fn create_from(this: &mut Dom, source: &Dom, src_id: NodeId) -> Option<NodeId> {
704            let src = source.get(src_id)?;
705            Some(match &src.data {
706                NodeData::Element(elem) => {
707                    this.create_element(elem.name.clone(), elem.attrs.clone())
708                }
709                NodeData::Text(t) => this.create_text(t.clone()),
710                NodeData::Comment(t) => this.create_comment(t.clone()),
711                NodeData::DocumentFragment | NodeData::Document => this.create_document_fragment(),
712                _ => this.create_document_fragment(),
713            })
714        }
715
716        let new_root = match create_from(self, source, source_root) {
717            Some(id) => id,
718            None => return self.create_document_fragment(),
719        };
720
721        let mut queue: Vec<(NodeId, NodeId)> = Vec::new();
722        let mut visited: AHashSet<NodeId> = AHashSet::with_capacity(64);
723        visited.insert(source_root);
724
725        for c in source.children_of(source_root) {
726            queue.push((c, new_root));
727        }
728
729        let mut steps: usize = 0;
730        let mut i = 0usize;
731        while i < queue.len() {
732            let (src_id, dest_parent) = queue[i];
733            i += 1;
734            steps += 1;
735            if steps > WALK_LIMIT {
736                break;
737            }
738            if !visited.insert(src_id) {
739                continue;
740            }
741            let new_id = match create_from(self, source, src_id) {
742                Some(id) => id,
743                None => continue,
744            };
745            self.append_child(dest_parent, new_id);
746            for c in source.children_of(src_id) {
747                queue.push((c, new_id));
748            }
749        }
750
751        new_root
752    }
753
754    pub fn node_type(&self, id: NodeId) -> u32 {
755        NodeRef { dom: self, id }.node_type()
756    }
757
758    fn find_element(
759        &self,
760        root: NodeId,
761        predicate: &dyn Fn(&NodeRef<'_>) -> bool,
762    ) -> Option<NodeId> {
763        let mut stack: Vec<NodeId> = self.children_of(root).rev().collect();
764        let mut visited: AHashSet<NodeId> = AHashSet::with_capacity(64);
765        let mut steps: usize = 0;
766        while let Some(id) = stack.pop() {
767            if !visited.insert(id) {
768                continue;
769            }
770            steps += 1;
771            if steps > WALK_LIMIT {
772                break;
773            }
774            let nr = NodeRef { dom: self, id };
775            if predicate(&nr) {
776                return Some(id);
777            }
778            stack.extend(self.children_of(id).rev());
779        }
780        None
781    }
782
783    fn collect_elements(
784        &self,
785        root: NodeId,
786        predicate: &dyn Fn(&NodeRef<'_>) -> bool,
787        results: &mut Vec<NodeId>,
788    ) {
789        let mut stack: Vec<NodeId> = self.children_of(root).rev().collect();
790        let mut visited: AHashSet<NodeId> = AHashSet::with_capacity(64);
791        let mut steps: usize = 0;
792        while let Some(id) = stack.pop() {
793            if !visited.insert(id) {
794                continue;
795            }
796            steps += 1;
797            if steps > WALK_LIMIT {
798                break;
799            }
800            let nr = NodeRef { dom: self, id };
801            if predicate(&nr) {
802                results.push(id);
803            }
804            stack.extend(self.children_of(id).rev());
805        }
806    }
807
808    #[cfg_attr(feature = "hotpath", hotpath::measure)]
809    fn convert_node(&self, blitz_node: &BlitzNode) -> Node {
810        let id = NodeId::from_blitz(blitz_node.id);
811        let parent = blitz_node.parent.map(NodeId::from_blitz);
812        let children: Vec<NodeId> = blitz_node
813            .children
814            .iter()
815            .map(|&c| NodeId::from_blitz(c))
816            .collect();
817        let first_child = children.first().copied();
818        let last_child = children.last().copied();
819        let prev_sibling = blitz_node
820            .parent
821            .and_then(|pid| self.inner.get_node(pid))
822            .and_then(|parent| {
823                let pos = parent.children.iter().position(|&c| c == blitz_node.id)?;
824                if pos > 0 {
825                    parent.children.get(pos - 1).map(|&c| NodeId::from_blitz(c))
826                } else {
827                    None
828                }
829            });
830        let next_sibling = blitz_node
831            .parent
832            .and_then(|pid| self.inner.get_node(pid))
833            .and_then(|parent| {
834                let pos = parent.children.iter().position(|&c| c == blitz_node.id)?;
835                parent.children.get(pos + 1).map(|&c| NodeId::from_blitz(c))
836            });
837        let data = match &blitz_node.data {
838            BlitzNodeData::Document => NodeData::Document,
839            BlitzNodeData::Element(e) => {
840                let name = QualName::from_h5(&e.name);
841                let attrs = e
842                    .attrs
843                    .iter()
844                    .map(|a| Attribute {
845                        name: QualName::from_h5(&a.name),
846                        value: a.value.clone(),
847                    })
848                    .collect();
849                NodeData::Element(ElementData {
850                    name,
851                    attrs,
852                    shadow_root: None,
853                })
854            }
855            BlitzNodeData::AnonymousBlock(e) => {
856                let name = QualName::from_h5(&e.name);
857                let attrs = e
858                    .attrs
859                    .iter()
860                    .map(|a| Attribute {
861                        name: QualName::from_h5(&a.name),
862                        value: a.value.clone(),
863                    })
864                    .collect();
865                NodeData::Element(ElementData {
866                    name,
867                    attrs,
868                    shadow_root: None,
869                })
870            }
871            BlitzNodeData::Text(t) => NodeData::Text(t.content.clone()),
872            BlitzNodeData::Comment => NodeData::Comment(String::new()),
873        };
874        Node {
875            id,
876            data,
877            parent,
878            first_child,
879            last_child,
880            prev_sibling,
881            next_sibling,
882        }
883    }
884}
885
886impl Default for Dom {
887    fn default() -> Self {
888        Self::new()
889    }
890}
891
892impl std::fmt::Debug for Dom {
893    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
894        let mut s = f.debug_struct("Dom");
895        s.field("len", &self.inner.tree().len());
896        s.finish()
897    }
898}
899
900#[derive(Clone)]
901pub struct DomElement<'a> {
902    pub dom: &'a Dom,
903    pub id: NodeId,
904}
905
906impl<'a> DomElement<'a> {
907    pub fn new(dom: &'a Dom, id: NodeId) -> Option<Self> {
908        let nr = NodeRef { dom, id };
909        if !nr.is_element() {
910            return None;
911        }
912        Some(Self { dom, id })
913    }
914
915    pub fn node_id(&self) -> NodeId {
916        self.id
917    }
918
919    fn nr(&self) -> NodeRef<'a> {
920        NodeRef {
921            dom: self.dom,
922            id: self.id,
923        }
924    }
925
926    pub fn local_name(&self) -> &'a str {
927        self.nr().tag_name().unwrap_or("")
928    }
929
930    pub fn id(&self) -> Option<&'a str> {
931        self.nr().get_attr("id")
932    }
933
934    pub fn has_class(&self, name: &str) -> bool {
935        self.nr().has_class(name)
936    }
937
938    pub fn has_attribute(&self, name: &str) -> bool {
939        self.nr().get_attr(name).is_some()
940    }
941
942    pub fn attr(&self, name: &str) -> Option<&'a str> {
943        self.nr().get_attr(name)
944    }
945}
946
947impl<'a> std::fmt::Debug for DomElement<'a> {
948    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
949        let nr = self.nr();
950        write!(f, "<{}", nr.tag_name().unwrap_or("?"))?;
951        if let Some(BlitzNodeData::Element(e)) | Some(BlitzNodeData::AnonymousBlock(e)) =
952            nr.blitz_data()
953        {
954            for attr in e.attrs.iter() {
955                write!(f, " {}=\"{}\"", attr.name.local, attr.value)?;
956            }
957        }
958        write!(f, ">")
959    }
960}