Skip to main content

hpx_browser/
dom.rs

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