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