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