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
27pub 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#[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 pub fn node_ref(&self, id: NodeId) -> NodeRef<'_> {
344 NodeRef { dom: self, id }
345 }
346
347 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('&', "&").replace('"', """),
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('&', "&")
647 .replace('<', "<")
648 .replace('>', ">"),
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}