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('&', "&").replace('"', """),
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('&', "&")
516 .replace('<', "<")
517 .replace('>', ">"),
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}