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