1use crate::Document;
2use crate::layout::damage::HoistedPaintChildren;
3use bitflags::bitflags;
4use blitz_traits::events::{
5 BlitzPointerEvent, BlitzPointerId, DomEventData, HitResult, PointerCoords,
6};
7use blitz_traits::node_id::NodeId;
8use blitz_traits::shell::ShellProvider;
9use euclid::{Point2D, Rect, Size2D};
10use html_escape::encode_quoted_attribute_to_string;
11use keyboard_types::Modifiers;
12use kurbo::{Affine, Rect as KurboRect};
13use markup5ever::{LocalName, local_name};
14use parley::{BreakReason, Cluster, ClusterSide, Selection};
15use selectors::matching::ElementSelectorFlags;
16use std::cell::{Cell, RefCell};
17use std::fmt::Write;
18use std::ops::{Deref, Range};
19use std::sync::Arc;
20use std::sync::atomic::{AtomicBool, Ordering};
21use style::Atom;
22use style::computed_values::isolation::T as Isolation;
23use style::invalidation::element::restyle_hints::RestyleHint;
24use style::properties::ComputedValues;
25use style::properties::generated::longhands::position::computed_value::T as Position;
26use style::selector_parser::{PseudoElement, RestyleDamage};
27use style::servo_arc::Arc as ServoArc;
28use style::shared_lock::SharedRwLock;
29use style::stylesheets::UrlExtraData;
30use style::values::computed::CSSPixelLength;
31use style::values::computed::Display as StyloDisplay;
32use style::values::computed::Rotate;
33use style::values::generics::transform::{Scale, Translate};
34use style::values::specified::box_::{DisplayInside, DisplayOutside};
35use style_dom::ElementState;
36use style_traits::values::ToCss;
37use taffy::{
38 Cache,
39 prelude::{Layout, Style},
40};
41use thin_vec::ThinVec;
42
43use super::stylo_data::StyloData;
44use super::{Attribute, DocumentData, ElementData};
45
46#[derive(Clone, Copy)]
47enum OutputStyle {
48 Normal,
49 Pretty,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum DisplayOuter {
54 Block,
55 Inline,
56 None,
57}
58
59bitflags! {
60 #[derive(Clone, Copy, PartialEq)]
61 pub struct NodeFlags: u32 {
62 const IS_INLINE_ROOT = 0b00000001;
64 const IS_TABLE_ROOT = 0b00000010;
66 const IS_IN_DOCUMENT = 0b00000100;
68 const IS_PARSER_INERT_SCRIPT = 0b00001000;
70 }
71}
72
73impl NodeFlags {
74 #[inline(always)]
75 pub fn is_inline_root(&self) -> bool {
76 self.contains(Self::IS_INLINE_ROOT)
77 }
78
79 #[inline(always)]
80 pub fn is_table_root(&self) -> bool {
81 self.contains(Self::IS_TABLE_ROOT)
82 }
83
84 #[inline(always)]
85 pub fn is_in_document(&self) -> bool {
86 self.contains(Self::IS_IN_DOCUMENT)
87 }
88
89 #[inline(always)]
90 pub fn reset_construction_flags(&mut self) {
91 self.remove(Self::IS_INLINE_ROOT);
92 self.remove(Self::IS_TABLE_ROOT);
93 }
94}
95
96pub struct Node {
97 tree: *mut crate::NodeTree,
99
100 pub id: NodeId,
102 pub parent: Option<NodeId>,
104 child_index_hint: Cell<usize>,
110 pub owner_document: Option<NodeId>,
113 pub markup: Option<Arc<MarkupNode>>,
115 pub custom_element_subtree_count: usize,
117 pub children: ThinVec<NodeId>,
119 pub layout_parent: Cell<Option<NodeId>>,
121 pub layout_children: RefCell<Option<ThinVec<NodeId>>>,
123 pub anonymous_blocks: ThinVec<NodeId>,
129 pub paint_children: RefCell<Option<ThinVec<NodeId>>>,
131 pub stacking_context: Option<Box<HoistedPaintChildren>>,
132
133 #[cfg(feature = "shadow-dom")]
139 pub flattened_children: Option<Vec<NodeId>>,
140 #[cfg(feature = "shadow-dom")]
142 pub assigned_slot: Option<NodeId>,
143 #[cfg(feature = "shadow-dom")]
145 pub assigned_nodes: Option<Vec<NodeId>>,
146 pub(crate) children_to_process: std::sync::atomic::AtomicIsize,
147
148 pub flags: NodeFlags,
150
151 pub data: NodeData,
158}
159
160unsafe impl Send for Node {}
161unsafe impl Sync for Node {}
162
163macro_rules! universal_accessors {
166 ($($(#[$meta:meta])* $field:ident / $field_mut:ident : $ty:ty),* $(,)?) => {
167 impl Node {
168 $(
169 $(#[$meta])*
170 #[inline]
171 pub fn $field(&self) -> &$ty {
172 match &self.data {
173 NodeData::Element(data) | NodeData::AnonymousBlock(data) => &data.$field,
174 NodeData::Document(data) => &data.$field,
175 _ => panic!(concat!("`", stringify!($field), "` is not available on this node kind")),
176 }
177 }
178
179 $(#[$meta])*
180 #[inline]
181 pub fn $field_mut(&mut self) -> &mut $ty {
182 match &mut self.data {
183 NodeData::Element(data) | NodeData::AnonymousBlock(data) => &mut data.$field,
184 NodeData::Document(data) => &mut data.$field,
185 _ => panic!(concat!("`", stringify!($field), "` is not available on this node kind")),
186 }
187 }
188 )*
189 }
190 };
191}
192
193universal_accessors! {
194 stylo_element_data / stylo_element_data_mut: StyloData,
195 style / style_mut: Style<Atom>,
196 style_source / style_source_mut: Option<ServoArc<ComputedValues>>,
197 subtree_hoists / subtree_hoists_mut: bool,
198 unrounded_layout / unrounded_layout_mut: Layout,
202 final_layout / final_layout_mut: Layout,
203 scroll_offset / scroll_offset_mut: crate::Point<f64>,
204 scrollable_overflow / scrollable_overflow_mut: KurboRect,
205 transform / transform_mut: Option<Affine>,
206 display_constructed_as / display_constructed_as_mut: StyloDisplay,
207 element_state / element_state_mut: ElementState,
210 snapshot_handled / snapshot_handled_mut: AtomicBool,
211 }
216
217impl Node {
218 pub fn selector_flags(&self) -> &Cell<ElementSelectorFlags> {
219 match &self.data {
220 NodeData::Element(data) | NodeData::AnonymousBlock(data) => &data.selector_flags,
221 NodeData::Document(data) => &data.selector_flags,
222 NodeData::ShadowRoot(data) => &data.selector_flags,
223 _ => panic!("selector flags require a parent node"),
224 }
225 }
226
227 pub fn selector_flags_mut(&mut self) -> &mut Cell<ElementSelectorFlags> {
228 match &mut self.data {
229 NodeData::Element(data) | NodeData::AnonymousBlock(data) => &mut data.selector_flags,
230 NodeData::Document(data) => &mut data.selector_flags,
231 NodeData::ShadowRoot(data) => &mut data.selector_flags,
232 _ => panic!("selector flags require a parent node"),
233 }
234 }
235
236 #[inline]
244 pub fn cache(&self) -> &Cache {
245 match &self.data {
246 NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.cache(),
247 NodeData::Document(data) => data.cache(),
248 _ => panic!("`cache` is not available on this node kind"),
249 }
250 }
251
252 #[inline]
254 pub fn cache_mut(&mut self) -> &mut Cache {
255 match &mut self.data {
256 NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.cache_mut(),
257 NodeData::Document(data) => data.cache_mut(),
258 _ => panic!("`cache_mut` is not available on this node kind"),
259 }
260 }
261
262 #[inline]
264 pub fn cache_release(&mut self) {
265 match &mut self.data {
266 NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.cache_release(),
267 NodeData::Document(data) => data.cache_release(),
268 _ => {}
269 }
270 }
271
272 #[inline]
278 pub fn style_source_opt(&self) -> Option<&ServoArc<ComputedValues>> {
279 match &self.data {
280 NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.style_source.as_ref(),
281 NodeData::Document(data) => data.style_source.as_ref(),
282 _ => None,
283 }
284 }
285
286 #[inline]
287 pub fn stylo_element_data_opt(&self) -> Option<&StyloData> {
288 match &self.data {
289 NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
290 Some(&data.stylo_element_data)
291 }
292 NodeData::Document(data) => Some(&data.stylo_element_data),
293 _ => None,
294 }
295 }
296
297 #[inline]
298 pub fn stylo_element_data_opt_mut(&mut self) -> Option<&mut StyloData> {
299 match &mut self.data {
300 NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
301 Some(&mut data.stylo_element_data)
302 }
303 NodeData::Document(data) => Some(&mut data.stylo_element_data),
304 _ => None,
305 }
306 }
307
308 #[inline]
311 fn dirty_descendants_flag(&self) -> Option<&AtomicBool> {
312 match &self.data {
313 NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
314 Some(&data.dirty_descendants)
315 }
316 NodeData::Document(data) => Some(&data.dirty_descendants),
317 _ => None,
318 }
319 }
320
321 #[inline]
324 pub fn guard(&self) -> &SharedRwLock {
325 let guard = match &self.data {
326 NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.guard.as_ref(),
327 NodeData::Document(data) => data.guard.as_ref(),
328 _ => None,
329 };
330 guard.expect("`guard` is not available on this node kind")
331 }
332
333 #[inline]
334 pub fn has_snapshot(&self) -> bool {
335 match &self.data {
336 NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.has_snapshot,
337 NodeData::Document(data) => data.has_snapshot,
338 _ => false,
339 }
340 }
341
342 #[inline]
343 pub fn set_has_snapshot(&mut self, value: bool) {
344 match &mut self.data {
345 NodeData::Element(data) | NodeData::AnonymousBlock(data) => data.has_snapshot = value,
346 NodeData::Document(data) => data.has_snapshot = value,
347 _ => {}
348 }
349 }
350
351 #[inline]
352 pub fn before(&self) -> Option<NodeId> {
353 self.element_data().and_then(|data| data.before)
354 }
355
356 #[inline]
357 pub fn after(&self) -> Option<NodeId> {
358 self.element_data().and_then(|data| data.after)
359 }
360}
361
362impl Node {
363 pub(crate) fn new(
364 tree: *mut crate::NodeTree,
365 id: NodeId,
366 guard: SharedRwLock,
367 mut data: NodeData,
368 ) -> Self {
369 match &mut data {
372 NodeData::Element(data) | NodeData::AnonymousBlock(data) => {
373 data.guard = Some(guard);
374 }
375 NodeData::Document(data) => data.guard = Some(guard),
376 _ => {}
377 }
378
379 Self {
380 tree,
381
382 id,
383 parent: None,
384 child_index_hint: Cell::new(0),
385 owner_document: None,
386 markup: None,
387 custom_element_subtree_count: 0,
388 children: ThinVec::new(),
389 layout_parent: Cell::new(None),
390 layout_children: RefCell::new(None),
391 anonymous_blocks: ThinVec::new(),
392 paint_children: RefCell::new(None),
393 stacking_context: None,
394 #[cfg(feature = "shadow-dom")]
395 flattened_children: None,
396 #[cfg(feature = "shadow-dom")]
397 assigned_slot: None,
398 #[cfg(feature = "shadow-dom")]
399 assigned_nodes: None,
400 children_to_process: std::sync::atomic::AtomicIsize::new(0),
401
402 flags: NodeFlags::empty(),
403 data,
404 }
405 }
406
407 pub fn set_transform(&mut self, scale: f32) -> Option<Affine> {
408 let transform = self.primary_styles().and_then(|s| {
409 let size = self.final_layout().size;
410 let reference_box = Rect::new(
411 Point2D::new(CSSPixelLength::new(0.0), CSSPixelLength::new(0.0)),
412 Size2D::new(
413 CSSPixelLength::new(size.width),
414 CSSPixelLength::new(size.height),
415 ),
416 );
417 crate::resolve_2d_transform(s.get_box(), reference_box).map(|t| {
420 let scale = scale as f64;
421 let [m11, m12, m21, m22, m41, m42] = t.as_coeffs();
422 Affine::new([m11, m12, m21, m22, m41 * scale, m42 * scale])
423 })
424 });
425
426 *self.transform_mut() = transform;
427 transform
428 }
429
430 pub fn pe_by_index(&self, index: usize) -> Option<NodeId> {
431 match index {
432 0 => self.after(),
433 1 => self.before(),
434 _ => panic!("Invalid pseudo element index"),
435 }
436 }
437
438 pub fn set_pe_by_index(&mut self, index: usize, value: Option<NodeId>) {
439 let Some(data) = self.element_data_mut() else {
440 return;
441 };
442 match index {
443 0 => data.after = value,
444 1 => data.before = value,
445 _ => panic!("Invalid pseudo element index"),
446 }
447 }
448
449 pub(crate) fn display_style(&self) -> Option<StyloDisplay> {
450 Some(self.primary_styles().as_ref()?.clone_display())
451 }
452
453 pub fn computed_style_properties(&self) -> Option<Vec<(&'static str, String)>> {
470 let style = self.primary_styles()?;
471 let layout = *self.final_layout();
472 let (used_width, used_height) = if matches!(
473 style.clone_box_sizing(),
474 style::computed_values::box_sizing::T::BorderBox
475 ) {
476 (layout.size.width, layout.size.height)
477 } else {
478 (
479 (layout.size.width
480 - layout.padding.left
481 - layout.padding.right
482 - layout.border.left
483 - layout.border.right)
484 .max(0.0),
485 (layout.size.height
486 - layout.padding.top
487 - layout.padding.bottom
488 - layout.border.top
489 - layout.border.bottom)
490 .max(0.0),
491 )
492 };
493 Some(vec![
494 ("display", style.clone_display().to_css_string()),
495 ("position", style.clone_position().to_css_string()),
496 ("visibility", style.clone_visibility().to_css_string()),
497 ("opacity", style.clone_opacity().to_css_string()),
498 ("color", style.clone_color().to_css_string()),
499 (
500 "background-color",
501 style.clone_background_color().to_css_string(),
502 ),
503 (
504 "font-size",
505 format!("{}px", style.clone_font_size().computed_size().px()),
506 ),
507 ("font-weight", style.clone_font_weight().to_css_string()),
508 ("font-style", style.clone_font_style().to_css_string()),
509 ("font-family", style.clone_font_family().to_css_string()),
510 ("text-align", style.clone_text_align().to_css_string()),
511 ("overflow-x", style.clone_overflow_x().to_css_string()),
512 ("overflow-y", style.clone_overflow_y().to_css_string()),
513 ("z-index", style.clone_z_index().to_css_string()),
514 ("box-sizing", style.clone_box_sizing().to_css_string()),
515 (
516 "flex-direction",
517 style.clone_flex_direction().to_css_string(),
518 ),
519 (
520 "justify-content",
521 style.clone_justify_content().to_css_string(),
522 ),
523 ("align-items", style.clone_align_items().to_css_string()),
524 ("width", format!("{}px", used_width)),
533 ("height", format!("{}px", used_height)),
534 ("padding-top", format!("{}px", layout.padding.top)),
540 ("padding-right", format!("{}px", layout.padding.right)),
541 ("padding-bottom", format!("{}px", layout.padding.bottom)),
542 ("padding-left", format!("{}px", layout.padding.left)),
543 ("border-top-width", format!("{}px", layout.border.top)),
544 ("border-right-width", format!("{}px", layout.border.right)),
545 ("border-bottom-width", format!("{}px", layout.border.bottom)),
546 ("border-left-width", format!("{}px", layout.border.left)),
547 ("margin-top", format!("{}px", layout.margin.top)),
548 ("margin-right", format!("{}px", layout.margin.right)),
549 ("margin-bottom", format!("{}px", layout.margin.bottom)),
550 ("margin-left", format!("{}px", layout.margin.left)),
551 ])
552 }
553
554 pub fn diagnostic_computed_style(&self) -> Option<Vec<(&'static str, String)>> {
555 let style = self.primary_styles()?;
556 Some(vec![
557 ("display", style.clone_display().to_css_string()),
558 ("color", style.clone_color().to_css_string()),
559 (
560 "background-color",
561 style.clone_background_color().to_css_string(),
562 ),
563 (
564 "font-size",
565 format!("{}px", style.clone_font_size().computed_size().px()),
566 ),
567 ("width", style.clone_width().to_css_string()),
568 ])
569 }
570
571 pub fn is_display_none(&self) -> bool {
573 self.display_style()
574 .is_some_and(|display| display.is_none())
575 }
576
577 pub fn is_or_contains_block(&self) -> bool {
578 let style = self.primary_styles();
579 let style = style.as_ref();
580
581 let position = style
583 .map(|s| s.clone_position())
584 .unwrap_or(Position::Relative);
585 let is_in_flow = matches!(
586 position,
587 Position::Static | Position::Relative | Position::Sticky
588 );
589 if !is_in_flow {
590 return false;
591 }
592 let is_floating = style
595 .map(|s| s.clone_float().is_floating())
596 .unwrap_or(false);
597 if is_floating {
598 return false;
599 }
600 let display = style
601 .map(|s| s.clone_display())
602 .unwrap_or(StyloDisplay::inline());
603 match display.outside() {
604 DisplayOutside::None => false,
605 DisplayOutside::Block => true,
606 _ => {
607 if display.inside() == DisplayInside::Flow {
608 self.children
609 .iter()
610 .copied()
611 .any(|child_id| self.tree()[child_id].is_or_contains_block())
612 } else {
613 false
614 }
615 }
616 }
617 }
618
619 pub fn is_whitespace_node(&self) -> bool {
620 match &self.data {
621 NodeData::Text(data) => data.content.chars().all(|c| c.is_ascii_whitespace()),
622 _ => false,
623 }
624 }
625
626 pub fn is_focussable(&self) -> bool {
627 self.data
628 .downcast_element()
629 .map(|el| el.is_focussable)
630 .unwrap_or(false)
631 }
632
633 pub fn set_restyle_hint(&mut self, hint: RestyleHint) {
634 if let Some(stylo_element_data) = self.stylo_element_data_opt_mut() {
635 let mut element_data = stylo_element_data.ensure_init_mut();
639 element_data.hint.insert(hint);
640 }
641 self.mark_ancestors_dirty();
644 }
645
646 pub fn has_dirty_descendants(&self) -> bool {
648 self.dirty_descendants_flag()
649 .is_some_and(|flag| flag.load(Ordering::Relaxed))
650 }
651
652 pub fn set_dirty_descendants(&self) {
654 if let Some(flag) = self.dirty_descendants_flag() {
655 flag.store(true, Ordering::Relaxed);
656 }
657 }
658
659 pub fn unset_dirty_descendants(&self) {
661 if let Some(flag) = self.dirty_descendants_flag() {
662 flag.store(false, Ordering::Relaxed);
663 }
664 }
665
666 pub(crate) fn mark_style_attr_updated(&mut self) {
668 if let Some(stylo_element_data) = self.stylo_element_data_opt_mut() {
669 if let Some(mut data) = stylo_element_data.get_mut() {
670 data.hint |= RestyleHint::RESTYLE_STYLE_ATTRIBUTE;
671 }
672 }
673 self.set_dirty_descendants();
674 self.mark_ancestors_dirty();
675 }
676
677 pub fn mark_ancestors_dirty(&self) {
681 let mut current = self
684 .shadow_root_data()
685 .map(|root| self.with(root.host))
686 .or_else(|| self.flattened_parent());
687
688 while let Some(parent) = current {
689 if let Some(flag) = parent.dirty_descendants_flag() {
690 flag.store(true, Ordering::Relaxed);
694 }
695 current = parent.flattened_parent();
696 }
697 }
698
699 pub fn damage(&self) -> Option<RestyleDamage> {
706 self.stylo_element_data_opt()
707 .and_then(|stylo| stylo.get().map(|data| data.damage))
708 }
709
710 pub fn set_damage(&mut self, damage: RestyleDamage) {
711 if let Some(stylo) = self.stylo_element_data_opt_mut() {
712 if let Some(mut data) = stylo.get_mut() {
713 data.damage = damage;
714 }
715 }
716 }
717
718 pub fn insert_damage(&mut self, damage: RestyleDamage) {
719 if let Some(stylo) = self.stylo_element_data_opt_mut() {
720 if let Some(mut data) = stylo.get_mut() {
721 data.damage |= damage;
722 }
723 }
724 }
725
726 pub fn remove_damage(&mut self, damage: RestyleDamage) {
727 if let Some(stylo) = self.stylo_element_data_opt_mut() {
728 if let Some(mut data) = stylo.get_mut() {
729 data.damage.remove(damage);
730 }
731 }
732 }
733
734 pub fn clear_damage_mut(&mut self) {
735 if let Some(stylo) = self.stylo_element_data_opt_mut() {
736 if let Some(mut data) = stylo.get_mut() {
737 data.damage = RestyleDamage::empty();
738 }
739 }
740 }
741
742 pub fn hover(&mut self) {
743 if let Some(data) = self.element_data_mut() {
744 data.element_state.insert(ElementState::HOVER);
745 }
746 self.set_restyle_hint(RestyleHint::restyle_subtree());
747 }
748
749 pub fn unhover(&mut self) {
750 if let Some(data) = self.element_data_mut() {
751 data.element_state.remove(ElementState::HOVER);
752 }
753 self.set_restyle_hint(RestyleHint::restyle_subtree());
754 }
755
756 pub fn is_hovered(&self) -> bool {
757 self.element_data()
758 .is_some_and(|data| data.element_state.contains(ElementState::HOVER))
759 }
760
761 pub fn focus(&mut self, shell_provider: Arc<dyn ShellProvider>) {
762 if let Some(data) = self.element_data_mut() {
763 data.element_state
764 .insert(ElementState::FOCUS | ElementState::FOCUSRING);
765 }
766 self.set_restyle_hint(RestyleHint::restyle_subtree());
767
768 if self
770 .element_data()
771 .and_then(|elem| elem.text_input_data())
772 .is_some()
773 {
774 shell_provider.set_ime_enabled(true);
775 let mut pos = self.absolute_position(0.0, 0.0);
776 pos.x += self.final_layout().content_box_x();
777 pos.y += self.final_layout().content_box_y();
778 let width = self.final_layout().content_box_width();
779 let height = self.final_layout().content_box_height();
780 shell_provider.set_ime_cursor_area(pos.x, pos.y, width, height);
781 }
782 }
783
784 pub fn blur(&mut self, shell_provider: Arc<dyn ShellProvider>) {
785 if let Some(data) = self.element_data_mut() {
786 data.element_state
787 .remove(ElementState::FOCUS | ElementState::FOCUSRING);
788 }
789 self.set_restyle_hint(RestyleHint::restyle_subtree());
790
791 if self
793 .element_data()
794 .and_then(|elem| elem.text_input_data())
795 .is_some()
796 {
797 shell_provider.set_ime_enabled(false);
798 }
799 }
800
801 pub fn is_focussed(&self) -> bool {
802 self.element_data()
803 .is_some_and(|data| data.element_state.contains(ElementState::FOCUS))
804 }
805
806 pub fn active(&mut self) {
807 if let Some(data) = self.element_data_mut() {
808 data.element_state.insert(ElementState::ACTIVE);
809 }
810 self.set_restyle_hint(RestyleHint::restyle_subtree());
811 }
812
813 pub fn unactive(&mut self) {
814 if let Some(data) = self.element_data_mut() {
815 data.element_state.remove(ElementState::ACTIVE);
816 }
817 self.set_restyle_hint(RestyleHint::restyle_subtree());
818 }
819
820 pub fn is_active(&self) -> bool {
821 self.element_data()
822 .is_some_and(|data| data.element_state.contains(ElementState::ACTIVE))
823 }
824
825 pub fn disable(&mut self) {
828 if let Some(data) = self.element_data_mut() {
829 if data.can_be_disabled() {
830 data.element_state.insert(ElementState::DISABLED);
831 data.element_state.remove(ElementState::ENABLED);
832 }
833 }
834 self.set_restyle_hint(RestyleHint::restyle_subtree());
835 }
836
837 pub fn enable(&mut self) {
840 if let Some(data) = self.element_data_mut() {
841 if data.can_be_disabled() {
842 data.element_state.insert(ElementState::ENABLED);
843 data.element_state.remove(ElementState::DISABLED);
844 }
845 }
846 self.set_restyle_hint(RestyleHint::restyle_subtree());
847 }
848
849 pub fn subdoc(&self) -> Option<&dyn Document> {
850 self.element_data().and_then(|el| el.sub_doc_data())
851 }
852
853 pub fn subdoc_mut(&mut self) -> Option<&mut dyn Document> {
854 self.element_data_mut().and_then(|el| el.sub_doc_data_mut())
855 }
856
857 pub fn text_input_v_centering_offset(&self, scale: f64) -> f64 {
858 if let Some(input_data) = self
861 .data
862 .downcast_element()
863 .and_then(|el| el.text_input_data())
864 {
865 if !input_data.is_multiline {
866 let content_box_height = self.final_layout().content_box_height();
867 let input_height = input_data.editor.try_layout().unwrap().height() / scale as f32;
868 let y_offset = ((content_box_height - input_height) / 2.0).max(0.0);
869
870 return y_offset as f64;
871 }
872 }
873
874 0.0
875 }
876}
877
878#[derive(Debug, Clone, Copy, PartialEq)]
879pub enum NodeKind {
880 Document,
881 Element,
882 AnonymousBlock,
883 Text,
884 Comment,
885 DocumentFragment,
886 ShadowRoot,
887}
888
889#[derive(Debug, Clone, Copy, PartialEq, Eq)]
895pub enum TextGranularity {
896 Word,
898 Line,
900}
901
902impl TextGranularity {
903 pub fn from_click_count(count: u16) -> Option<Self> {
906 match count {
907 0 | 1 => None,
908 2 => Some(Self::Word),
909 _ => Some(Self::Line),
910 }
911 }
912}
913
914#[derive(Debug, Clone, Copy, PartialEq, Eq)]
918pub enum ShadowRootMode {
919 Open,
922 Closed,
925}
926
927#[derive(Clone)]
935pub struct ShadowRootData {
936 pub host: NodeId,
938 pub mode: ShadowRootMode,
940 pub declarative: bool,
942 pub clonable: bool,
944 pub serializable: bool,
946 pub delegates_focus: bool,
948 pub stylesheet_nodes: Vec<NodeId>,
950 pub(crate) cascade_data: Option<ServoArc<style::stylist::CascadeData>>,
951 pub(crate) cascade_dirty: bool,
952 pub(crate) slots: Vec<NodeId>,
953 pub(crate) slottables: Vec<NodeId>,
954 pub(crate) selector_flags: Cell<ElementSelectorFlags>,
955}
956
957impl std::fmt::Debug for ShadowRootData {
958 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
959 formatter
960 .debug_struct("ShadowRootData")
961 .field("host", &self.host)
962 .field("mode", &self.mode)
963 .field("declarative", &self.declarative)
964 .field("clonable", &self.clonable)
965 .field("serializable", &self.serializable)
966 .field("delegates_focus", &self.delegates_focus)
967 .field("stylesheet_nodes", &self.stylesheet_nodes)
968 .field("cascade_dirty", &self.cascade_dirty)
969 .field("slots", &self.slots)
970 .finish()
971 }
972}
973
974impl ShadowRootData {
975 pub fn new(host: NodeId, mode: ShadowRootMode) -> Self {
976 Self {
977 host,
978 mode,
979 declarative: false,
980 clonable: false,
981 serializable: false,
982 delegates_focus: false,
983 stylesheet_nodes: Vec::new(),
984 cascade_data: None,
985 cascade_dirty: true,
986 slots: Vec::new(),
987 slottables: Vec::new(),
988 selector_flags: Cell::new(ElementSelectorFlags::empty()),
989 }
990 }
991}
992
993#[derive(Debug, PartialEq, Eq)]
995pub enum MarkupNode {
996 ProcessingInstruction {
997 target: String,
998 },
999 Doctype {
1000 name: String,
1001 public_id: String,
1002 system_id: String,
1003 },
1004}
1005
1006#[derive(Debug, Clone)]
1008pub enum NodeData {
1009 Document(Box<DocumentData>),
1011
1012 Element(Box<ElementData>),
1014
1015 AnonymousBlock(Box<ElementData>),
1017
1018 Text(TextNodeData),
1020
1021 Comment {
1023 contents: String,
1025 },
1026
1027 DocumentFragment,
1030
1031 ShadowRoot(ShadowRootData),
1033 }
1040
1041impl NodeData {
1042 pub fn downcast_element(&self) -> Option<&ElementData> {
1043 match self {
1044 Self::Element(data) => Some(data),
1045 Self::AnonymousBlock(data) => Some(data),
1046 _ => None,
1047 }
1048 }
1049
1050 pub fn downcast_element_mut(&mut self) -> Option<&mut ElementData> {
1051 match self {
1052 Self::Element(data) => Some(data),
1053 Self::AnonymousBlock(data) => Some(data),
1054 _ => None,
1055 }
1056 }
1057
1058 pub fn is_element_with_tag_name(&self, name: &impl PartialEq<LocalName>) -> bool {
1059 let Some(elem) = self.downcast_element() else {
1060 return false;
1061 };
1062 *name == elem.name.local
1063 }
1064
1065 pub fn attrs(&self) -> Option<&[Attribute]> {
1066 Some(&self.downcast_element()?.attrs)
1067 }
1068
1069 pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
1070 self.downcast_element()?.attr(name)
1071 }
1072
1073 pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
1074 self.downcast_element()
1075 .is_some_and(|elem| elem.has_attr(name))
1076 }
1077
1078 pub fn kind(&self) -> NodeKind {
1079 match self {
1080 NodeData::Document(_) => NodeKind::Document,
1081 NodeData::Element(_) => NodeKind::Element,
1082 NodeData::AnonymousBlock(_) => NodeKind::AnonymousBlock,
1083 NodeData::Text(_) => NodeKind::Text,
1084 NodeData::Comment { .. } => NodeKind::Comment,
1085 NodeData::DocumentFragment => NodeKind::DocumentFragment,
1086 NodeData::ShadowRoot(_) => NodeKind::ShadowRoot,
1087 }
1088 }
1089
1090 pub fn shadow_root_data(&self) -> Option<&ShadowRootData> {
1091 match self {
1092 Self::ShadowRoot(data) => Some(data),
1093 _ => None,
1094 }
1095 }
1096
1097 pub fn shadow_root_data_mut(&mut self) -> Option<&mut ShadowRootData> {
1098 match self {
1099 Self::ShadowRoot(data) => Some(data),
1100 _ => None,
1101 }
1102 }
1103}
1104
1105#[derive(Debug, Clone)]
1106pub struct TextNodeData {
1107 pub content: String,
1109}
1110
1111impl TextNodeData {
1112 pub fn new(content: String) -> Self {
1113 Self { content }
1114 }
1115}
1116
1117impl Node {
1142 pub fn tree(&self) -> &crate::NodeTree {
1143 unsafe { &*self.tree }
1144 }
1145
1146 pub fn is_html_document(&self) -> bool {
1147 let document = match &self.data {
1148 NodeData::Document(data) => return data.content_type == "text/html",
1149 _ => self.owner_document.and_then(|id| self.tree().get(id)),
1150 };
1151 match document.map(|node| &node.data) {
1152 Some(NodeData::Document(data)) => data.content_type == "text/html",
1153 _ => true,
1154 }
1155 }
1156
1157 pub fn qualified_element_name(&self) -> Option<String> {
1158 let element = self.element_data()?;
1159 let name = match &element.name.prefix {
1160 Some(prefix) => format!("{prefix}:{}", element.name.local),
1161 None => element.name.local.to_string(),
1162 };
1163 Some(
1164 if self.is_html_document() && element.name.ns == markup5ever::ns!(html) {
1165 name.to_ascii_uppercase()
1166 } else {
1167 name
1168 },
1169 )
1170 }
1171
1172 #[track_caller]
1173 pub fn with(&self, id: NodeId) -> &Node {
1174 self.tree().get(id).unwrap()
1175 }
1176
1177 pub fn print_tree(&self, level: usize) {
1178 println!(
1179 "{} {} {:?} {} {:?}",
1180 " ".repeat(level),
1181 self.id,
1182 self.parent,
1183 self.node_debug_str().replace('\n', ""),
1184 self.children
1185 );
1186 for child_id in self.children.iter() {
1188 let child = self.with(*child_id);
1189 child.print_tree(level + 1)
1190 }
1191 }
1192
1193 pub fn index_of_child(&self, child_id: NodeId) -> Option<usize> {
1195 self.children.iter().position(|id| *id == child_id)
1196 }
1197
1198 pub fn child_index(&self) -> Option<usize> {
1200 let children = &self.tree()[self.parent?].children;
1201 let hint = self.child_index_hint.get();
1202 if children.get(hint) == Some(&self.id) {
1203 return Some(hint);
1204 }
1205 let index = children.iter().position(|id| *id == self.id)?;
1206 self.child_index_hint.set(index);
1207 Some(index)
1208 }
1209
1210 pub fn dom_child_at(&self, index: usize) -> Option<NodeId> {
1212 let id = *self.children.get(index)?;
1213 self.with(id).child_index_hint.set(index);
1214 Some(id)
1215 }
1216
1217 pub fn forward(&self, n: usize) -> Option<&Node> {
1219 let index = self.child_index()?.checked_add(n)?;
1220 let id = *self.tree()[self.parent?].children.get(index)?;
1221 let node = self.with(id);
1222 node.child_index_hint.set(index);
1223 Some(node)
1224 }
1225
1226 pub fn backward(&self, n: usize) -> Option<&Node> {
1227 let index = self.child_index()?.checked_sub(n)?;
1228 let id = *self.tree()[self.parent?].children.get(index)?;
1229 let node = self.with(id);
1230 node.child_index_hint.set(index);
1231 Some(node)
1232 }
1233
1234 pub fn is_element(&self) -> bool {
1235 matches!(self.data, NodeData::Element { .. })
1236 }
1237
1238 pub fn is_anonymous(&self) -> bool {
1239 matches!(self.data, NodeData::AnonymousBlock { .. })
1240 }
1241
1242 pub fn is_shadow_root(&self) -> bool {
1243 matches!(self.data, NodeData::ShadowRoot { .. })
1244 }
1245
1246 pub fn shadow_root_data(&self) -> Option<&ShadowRootData> {
1247 self.data.shadow_root_data()
1248 }
1249
1250 pub fn shadow_root_data_mut(&mut self) -> Option<&mut ShadowRootData> {
1251 self.data.shadow_root_data_mut()
1252 }
1253
1254 pub fn shadow_root_id(&self) -> Option<NodeId> {
1257 self.element_data().and_then(|el| el.shadow_root)
1258 }
1259
1260 #[inline]
1266 pub(crate) fn flattened_parent(&self) -> Option<&Node> {
1267 #[cfg(feature = "shadow-dom")]
1268 {
1269 if self.is_shadow_root() {
1270 return None;
1271 }
1272 if let Some(slot_id) = self.assigned_slot {
1273 return Some(self.with(slot_id));
1274 }
1275 let parent = self.with(self.parent?);
1276 if let Some(root) = parent.shadow_root_data() {
1277 return Some(self.with(root.host));
1278 }
1279 if parent.shadow_root_id().is_some()
1280 || parent
1281 .assigned_nodes
1282 .as_ref()
1283 .is_some_and(|nodes| !nodes.is_empty())
1284 {
1285 return None;
1286 }
1287 Some(parent)
1288 }
1289 #[cfg(not(feature = "shadow-dom"))]
1290 {
1291 self.parent.map(|id| self.with(id))
1292 }
1293 }
1294
1295 #[cfg(feature = "shadow-dom")]
1299 pub fn layout_dom_children(&self) -> &[NodeId] {
1300 match &self.flattened_children {
1301 Some(children) => children,
1302 None => &self.children,
1303 }
1304 }
1305
1306 #[cfg(not(feature = "shadow-dom"))]
1308 #[inline(always)]
1309 pub fn layout_dom_children(&self) -> &[NodeId] {
1310 &self.children
1311 }
1312
1313 pub fn is_text_node(&self) -> bool {
1314 matches!(self.data, NodeData::Text { .. })
1315 }
1316
1317 pub fn element_data(&self) -> Option<&ElementData> {
1318 match self.data {
1319 NodeData::Element(ref data) => Some(data),
1320 NodeData::AnonymousBlock(ref data) => Some(data),
1321 _ => None,
1322 }
1323 }
1324
1325 pub fn element_data_mut(&mut self) -> Option<&mut ElementData> {
1326 match self.data {
1327 NodeData::Element(ref mut data) => Some(data),
1328 NodeData::AnonymousBlock(ref mut data) => Some(data),
1329 _ => None,
1330 }
1331 }
1332
1333 pub fn text_data(&self) -> Option<&TextNodeData> {
1334 match self.data {
1335 NodeData::Text(ref data) => Some(data),
1336 _ => None,
1337 }
1338 }
1339
1340 pub fn text_data_mut(&mut self) -> Option<&mut TextNodeData> {
1341 match self.data {
1342 NodeData::Text(ref mut data) => Some(data),
1343 _ => None,
1344 }
1345 }
1346
1347 pub fn node_debug_str(&self) -> String {
1348 let mut s = String::new();
1349
1350 match &self.data {
1351 NodeData::Document(_) => write!(s, "DOCUMENT"),
1352 NodeData::DocumentFragment => write!(s, "FRAGMENT"),
1353 NodeData::Text(data) => {
1355 let bytes = data.content.as_bytes();
1356 write!(
1357 s,
1358 "TEXT {}",
1359 std::str::from_utf8(bytes.split_at(10.min(bytes.len())).0)
1360 .unwrap_or("INVALID UTF8")
1361 )
1362 }
1363 NodeData::Comment { .. } => write!(s, "COMMENT"),
1364 NodeData::AnonymousBlock(_) => write!(s, "AnonymousBlock"),
1365 NodeData::ShadowRoot(data) => write!(s, "#shadow-root ({:?})", data.mode),
1366 NodeData::Element(data) => {
1367 let name = &data.name;
1368 let class = self.attr(local_name!("class")).unwrap_or("");
1369 let id = self.attr(local_name!("id")).unwrap_or("");
1370 let display = self.display_constructed_as().to_css_string();
1371 write!(s, "<{}", name.local).unwrap();
1372 if !id.is_empty() {
1373 write!(s, " #{id}").unwrap();
1374 }
1375 if !class.is_empty() {
1376 if class.contains(' ') {
1377 write!(s, " class=\"{class}\"").unwrap()
1378 } else {
1379 write!(s, " .{class}").unwrap()
1380 }
1381 }
1382 write!(s, "> ({display})")
1383 } }
1385 .unwrap();
1386 s
1387 }
1388
1389 pub fn outer_html(&self) -> String {
1397 let mut output = String::new();
1398 self.write_outer_html(&mut output);
1399 output
1400 }
1401
1402 pub fn outer_html_pretty(&self) -> String {
1418 let mut output = String::new();
1419 self.write_outer_html_pretty(&mut output);
1420 output
1421 }
1422
1423 pub fn write_outer_html(&self, writer: &mut String) {
1424 self.write_outer_html_in_style(writer, OutputStyle::Normal, 0, None);
1425 }
1426
1427 #[cfg(feature = "svg")]
1428 pub(crate) fn write_outer_html_with_current_color(
1429 &self,
1430 writer: &mut String,
1431 current_color: &str,
1432 ) {
1433 self.write_outer_html_in_style(writer, OutputStyle::Normal, 0, Some(current_color));
1434 }
1435
1436 #[cfg(feature = "svg")]
1437 pub(crate) fn write_outer_html_with_svg_ids(
1438 &self,
1439 writer: &mut String,
1440 ids: &std::collections::HashMap<NodeId, String>,
1441 ) {
1442 self.write_outer_html_in_style_with_svg_ids(
1443 writer,
1444 OutputStyle::Normal,
1445 0,
1446 None,
1447 Some(ids),
1448 );
1449 }
1450
1451 pub fn write_outer_html_pretty(&self, writer: &mut String) {
1452 self.write_outer_html_in_style(writer, OutputStyle::Pretty, 0, None);
1453 }
1454
1455 fn write_outer_html_in_style(
1456 &self,
1457 writer: &mut String,
1458 style: OutputStyle,
1459 nesting: usize,
1460 current_color_override: Option<&str>,
1461 ) {
1462 self.write_outer_html_in_style_with_svg_ids(
1463 writer,
1464 style,
1465 nesting,
1466 current_color_override,
1467 None,
1468 );
1469 }
1470
1471 fn write_outer_html_in_style_with_svg_ids(
1472 &self,
1473 writer: &mut String,
1474 style: OutputStyle,
1475 nesting: usize,
1476 current_color_override: Option<&str>,
1477 svg_ids: Option<&std::collections::HashMap<NodeId, String>>,
1478 ) {
1479 const INDENT: &str = " ";
1480 let has_children = !self.children.is_empty();
1481 let computed_current_color = || {
1482 self.primary_styles()
1483 .map(|style| style.clone_color())
1484 .map(|color| crate::util::absolute_color_to_svg_css(&color))
1485 };
1486 let current_color = current_color_override
1487 .map(ToOwned::to_owned)
1488 .or_else(computed_current_color);
1489
1490 match &self.data {
1491 NodeData::Document(_) => {}
1492 NodeData::Comment { .. } => {}
1493 NodeData::AnonymousBlock(_) => {}
1494 NodeData::ShadowRoot(_) => {}
1495 NodeData::DocumentFragment => {}
1499 NodeData::Text(data) => {
1501 if matches!(style, OutputStyle::Pretty) {
1502 for _ in 0..nesting {
1503 writer.push_str(INDENT);
1504 }
1505 }
1506 writer.push_str(data.content.as_str());
1507 if matches!(style, OutputStyle::Pretty) {
1508 writer.push('\n');
1509 }
1510 }
1511 NodeData::Element(data) => {
1512 if matches!(style, OutputStyle::Pretty) {
1513 for _ in 0..nesting {
1514 writer.push_str(INDENT);
1515 }
1516 }
1517 let tag_name = if svg_ids.is_some()
1520 && data.name.ns.as_ref() == "http://www.w3.org/2000/svg"
1521 && data.name.local == local_name!("a")
1522 {
1523 "g"
1524 } else {
1525 data.name.local.as_ref()
1526 };
1527 let svg_id = svg_ids.and_then(|ids| ids.get(&self.id));
1528 writer.push('<');
1529 writer.push_str(tag_name);
1530 if let Some(id) = svg_id {
1531 writer.push_str(" id=\"");
1532 encode_quoted_attribute_to_string(id, writer);
1533 writer.push('"');
1534 }
1535
1536 for attr in data.attrs() {
1537 if svg_id.is_some()
1538 && attr.name.ns.as_ref().is_empty()
1539 && attr.name.local == local_name!("id")
1540 {
1541 continue;
1542 }
1543 writer.push(' ');
1544 if let Some(prefix) = attr
1545 .name
1546 .prefix
1547 .as_ref()
1548 .filter(|prefix| !prefix.is_empty())
1549 {
1550 writer.push_str(prefix);
1551 writer.push(':');
1552 }
1553 writer.push_str(&attr.name.local);
1554 writer.push_str("=\"");
1555 #[allow(clippy::unnecessary_unwrap)] if current_color.is_some() && attr.value.contains("currentColor") {
1557 let value = attr
1558 .value
1559 .replace("currentColor", current_color.as_ref().unwrap());
1560 encode_quoted_attribute_to_string(&value, writer);
1561 } else {
1562 encode_quoted_attribute_to_string(&attr.value, writer);
1563 }
1564 writer.push('"');
1565 }
1566 if !has_children {
1567 writer.push_str(" /");
1568 }
1569 writer.push('>');
1570 if matches!(style, OutputStyle::Pretty) {
1571 writer.push('\n');
1572 }
1573
1574 if has_children {
1575 for &child_id in &self.children {
1576 self.tree()[child_id].write_outer_html_in_style_with_svg_ids(
1577 writer,
1578 style,
1579 nesting + 1,
1580 current_color_override,
1581 svg_ids,
1582 );
1583 }
1584
1585 if matches!(style, OutputStyle::Pretty) {
1586 for _ in 0..nesting {
1587 writer.push_str(INDENT);
1588 }
1589 }
1590 writer.push_str("</");
1591 writer.push_str(tag_name);
1592 writer.push('>');
1593 if matches!(style, OutputStyle::Pretty) {
1594 writer.push('\n');
1595 }
1596 }
1597 }
1598 }
1599 }
1600
1601 pub fn write_html(&self, writer: &mut String) {
1609 match &self.data {
1610 NodeData::Text(data) => {
1611 let raw = self
1612 .parent
1613 .and_then(|parent| self.tree()[parent].element_data())
1614 .is_some_and(|parent| is_raw_text_element(&parent.name.local));
1615 if raw {
1616 writer.push_str(&data.content);
1617 } else {
1618 escape_html(&data.content, false, writer);
1619 }
1620 }
1621 NodeData::Comment { contents } => {
1622 writer.push_str("<!--");
1623 writer.push_str(contents);
1624 writer.push_str("-->");
1625 }
1626 NodeData::Element(data) => {
1627 writer.push('<');
1628 writer.push_str(&data.name.local);
1629 for attr in data.attrs() {
1630 writer.push(' ');
1631 if let Some(prefix) = attr
1632 .name
1633 .prefix
1634 .as_ref()
1635 .filter(|prefix| !prefix.is_empty())
1636 {
1637 writer.push_str(prefix);
1638 writer.push(':');
1639 }
1640 writer.push_str(&attr.name.local);
1641 writer.push_str("=\"");
1642 escape_html(&attr.value, true, writer);
1643 writer.push('"');
1644 }
1645 writer.push('>');
1646 if is_void_element(&data.name.local) {
1647 return;
1648 }
1649 self.write_children_html(writer);
1650 writer.push_str("</");
1651 writer.push_str(&data.name.local);
1652 writer.push('>');
1653 }
1654 NodeData::Document(_)
1655 | NodeData::DocumentFragment
1656 | NodeData::ShadowRoot(_)
1657 | NodeData::AnonymousBlock(_) => self.write_children_html(writer),
1658 }
1659 }
1660
1661 pub fn write_children_html(&self, writer: &mut String) {
1663 if let Some(contents) = self
1664 .element_data()
1665 .and_then(|element| element.template_contents)
1666 {
1667 self.tree()[contents].write_children_html(writer);
1668 return;
1669 }
1670 for &child_id in &self.children {
1671 self.tree()[child_id].write_html(writer);
1672 }
1673 }
1674
1675 pub fn attrs(&self) -> Option<&[Attribute]> {
1676 Some(&self.element_data()?.attrs)
1677 }
1678
1679 pub fn attr(&self, name: LocalName) -> Option<&str> {
1680 let attr = self
1681 .attrs()?
1682 .iter()
1683 .find(|attr| attr.name.ns == markup5ever::ns!() && attr.name.local == name)?;
1684 Some(&attr.value)
1685 }
1686
1687 pub fn primary_styles(&self) -> Option<impl Deref<Target = ServoArc<ComputedValues>>> {
1688 self.stylo_element_data_opt()
1689 .and_then(|stylo| stylo.primary_styles())
1690 }
1691
1692 pub fn text_content(&self) -> String {
1693 let mut out = String::new();
1694 self.write_text_content(&mut out);
1695 out
1696 }
1697
1698 pub fn rendered_inner_text(&self) -> String {
1702 use style::computed_values::visibility::T as Visibility;
1703
1704 enum Piece {
1705 Text(String),
1706 RequiredBreak(usize),
1707 }
1708
1709 fn text(node: &Node) -> String {
1710 let NodeData::Text(data) = &node.data else {
1711 return String::new();
1712 };
1713 if node.inline_root_ancestor().is_none() {
1715 return String::new();
1716 }
1717 let preserves = node
1718 .parent
1719 .map(|id| node.with(id))
1720 .and_then(Node::primary_styles)
1721 .is_some_and(|style| {
1722 style.get_inherited_text().white_space_collapse
1723 != style::computed_values::white_space_collapse::T::Collapse
1724 });
1725 if preserves {
1726 return data.content.to_string();
1727 }
1728 let mut result = String::with_capacity(data.content.len());
1731 let mut space = false;
1732 for ch in data.content.chars() {
1733 if matches!(ch, ' ' | '\t' | '\n' | '\r' | '\u{c}') {
1734 space = true;
1735 } else {
1736 if space {
1737 result.push(' ');
1738 space = false;
1739 }
1740 result.push(ch);
1741 }
1742 }
1743 if space {
1744 result.push(' ');
1745 }
1746 result
1747 }
1748 fn collect(node: &Node, pieces: &mut Vec<Piece>) {
1749 if node.is_element() && node.is_display_none() {
1750 return;
1751 }
1752 let visible = node
1753 .primary_styles()
1754 .is_none_or(|style| style.clone_visibility() == Visibility::Visible);
1755 if node.is_text_node() {
1756 let visible = node
1757 .parent
1758 .map(|id| node.with(id))
1759 .and_then(Node::primary_styles)
1760 .is_none_or(|style| style.clone_visibility() == Visibility::Visible);
1761 if visible {
1762 let value = text(node);
1763 if !value.is_empty() {
1764 pieces.push(Piece::Text(value));
1765 }
1766 }
1767 return;
1768 }
1769 if !node.is_element() {
1770 return;
1771 }
1772 let tag = node.element_data().expect("element").name.local.as_ref();
1773 if tag == "br" {
1774 if visible {
1775 pieces.push(Piece::Text("\n".to_owned()));
1776 }
1777 return;
1778 }
1779 let display = node.primary_styles().map(|style| style.clone_display());
1780 let block = display.is_some_and(|display| {
1781 display.outside() == DisplayOutside::Block && !display.is_contents()
1782 });
1783 let breaks = if tag == "p" { 2 } else { usize::from(block) };
1784 if breaks != 0 {
1785 pieces.push(Piece::RequiredBreak(breaks));
1786 }
1787 for child in &node.children {
1788 collect(node.with(*child), pieces);
1789 }
1790 if visible {
1791 if matches!(tag, "td" | "th") {
1792 let next_cell = node
1793 .parent
1794 .map(|parent| node.with(parent))
1795 .and_then(|parent| {
1796 parent.index_of_child(node.id).map(|index| (parent, index))
1797 })
1798 .is_some_and(|(parent, index)| {
1799 parent.children[index + 1..].iter().any(|id| {
1800 let child = parent.with(*id);
1801 child.data.is_element_with_tag_name(&local_name!("td"))
1802 || child.data.is_element_with_tag_name(&local_name!("th"))
1803 })
1804 });
1805 if next_cell {
1806 pieces.push(Piece::Text("\t".to_owned()));
1807 }
1808 } else if tag == "tr" {
1809 pieces.push(Piece::RequiredBreak(1));
1810 }
1811 }
1812 if breaks != 0 {
1813 pieces.push(Piece::RequiredBreak(breaks));
1814 }
1815 }
1816
1817 let mut current = Some(self);
1818 while let Some(node) = current {
1819 if !self.flags.is_in_document() || node.is_display_none() {
1820 return self.text_content();
1821 }
1822 current = node.parent.map(|id| node.with(id));
1823 }
1824 if self.primary_styles().is_none() {
1825 return self.text_content();
1826 }
1827 let mut pieces = Vec::new();
1828 collect(self, &mut pieces);
1829 let mut result = String::new();
1830 let mut pending = 0;
1831 for piece in pieces {
1832 match piece {
1833 Piece::RequiredBreak(count) => pending = pending.max(count),
1834 Piece::Text(value) => {
1835 if !result.is_empty() && pending != 0 {
1836 result.truncate(result.trim_end_matches(' ').len());
1837 for _ in 0..pending {
1838 result.push('\n');
1839 }
1840 }
1841 pending = 0;
1842 if result.is_empty() || result.ends_with('\n') || result.ends_with(' ') {
1843 result.push_str(value.trim_start_matches(' '));
1844 } else {
1845 result.push_str(&value);
1846 }
1847 }
1848 }
1849 }
1850 result.truncate(result.trim_end_matches(' ').len());
1851 result
1852 }
1853
1854 pub fn cssom_offset_parent(&self) -> Option<&Node> {
1857 if !self.flags.is_in_document()
1858 || self.data.is_element_with_tag_name(&local_name!("body"))
1859 || self.data.is_element_with_tag_name(&local_name!("html"))
1860 {
1861 return None;
1862 }
1863 let style = self.primary_styles()?;
1864 if style.clone_position() == Position::Fixed || style.clone_display().is_contents() {
1865 return None;
1866 }
1867 let mut current = Some(self);
1868 while let Some(node) = current {
1869 if node.is_display_none() {
1870 return None;
1871 }
1872 current = node.parent.map(|id| node.with(id));
1873 }
1874 let static_position = style.clone_position() == Position::Static;
1875 let zoom = style.effective_zoom.zoom(1.0);
1876 let mut parent = self.parent.map(|id| self.with(id));
1877 while let Some(node) = parent {
1878 if let Some(style) = node.primary_styles()
1879 && !style.clone_display().is_contents()
1880 && (node.is_offset_parent()
1881 || style.effective_zoom.zoom(1.0) != zoom
1882 || (static_position
1883 && node.data.is_element_with_tag_name(&local_name!("table"))))
1884 {
1885 return Some(node);
1886 }
1887 parent = node.parent.map(|id| node.with(id));
1888 }
1889 None
1890 }
1891
1892 pub fn write_text_content<W: Write>(&self, out: &mut W) {
1905 match &self.data {
1906 NodeData::Text(data) => {
1907 let _ = out.write_str(&data.content);
1908 }
1909 NodeData::Element(..)
1910 | NodeData::AnonymousBlock(..)
1911 | NodeData::Document(..)
1912 | NodeData::DocumentFragment
1913 | NodeData::ShadowRoot(..) => {
1914 for child_id in self.children.iter() {
1915 self.with(*child_id).write_text_content(out);
1916 }
1917 }
1918 _ => {}
1919 }
1920 }
1921
1922 pub fn flush_style_attribute(&mut self, url_extra_data: &UrlExtraData) {
1923 if let NodeData::Element(ref mut elem_data) = self.data {
1924 if let Some(guard) = elem_data.guard.clone() {
1925 elem_data.flush_style_attribute(&guard, url_extra_data);
1926 }
1927 }
1928 }
1929
1930 pub fn order(&self) -> i32 {
1931 self.primary_styles()
1932 .map(|s| match s.pseudo() {
1933 Some(PseudoElement::Before) => i32::MIN,
1934 Some(PseudoElement::After) => i32::MAX,
1935 _ => s.clone_order(),
1936 })
1937 .unwrap_or(0)
1938 }
1939
1940 pub fn z_index(&self) -> i32 {
1941 self.primary_styles()
1942 .map(|s| s.clone_z_index().integer_or(0))
1943 .unwrap_or(0)
1944 }
1945
1946 pub fn is_stacking_context_root(&self, is_flex_or_grid_item: bool) -> bool {
1948 let Some(style) = self.primary_styles() else {
1949 return false;
1950 };
1951
1952 let position = style.clone_position();
1953 let has_z_index = !style.clone_z_index().is_auto();
1954
1955 if style.clone_opacity() != 1.0 {
1956 return true;
1957 }
1958
1959 let position_based = match position {
1960 Position::Fixed | Position::Sticky => true,
1961 Position::Relative | Position::Absolute => has_z_index,
1962 Position::Static => has_z_index && is_flex_or_grid_item,
1963 };
1964 if position_based {
1965 return true;
1966 }
1967
1968 let box_styles = style.get_box();
1976 if !box_styles.transform.0.is_empty()
1977 || !matches!(box_styles.translate, Translate::None)
1978 || !matches!(box_styles.rotate, Rotate::None)
1979 || !matches!(box_styles.scale, Scale::None)
1980 {
1981 return true;
1982 }
1983
1984 if box_styles.isolation == Isolation::Isolate {
1989 return true;
1990 }
1991
1992 false
1999 }
2000
2001 pub fn hit(&self, x: f32, y: f32, scale: f64) -> Option<HitResult> {
2010 self.hit_inner(x, y, scale, &mut None)
2011 }
2012
2013 pub(crate) fn hit_inner(
2018 &self,
2019 x: f32,
2020 y: f32,
2021 scale: f64,
2022 scrollbar: &mut Option<crate::node::ScrollbarRef>,
2023 ) -> Option<HitResult> {
2024 self.hit_inner_collect(x, y, scale, scrollbar, &mut None)
2025 }
2026
2027 pub(crate) fn hit_inner_collect(
2029 &self,
2030 x: f32,
2031 y: f32,
2032 scale: f64,
2033 scrollbar: &mut Option<crate::node::ScrollbarRef>,
2034 hits: &mut Option<Vec<HitResult>>,
2035 ) -> Option<HitResult> {
2036 use style::computed_values::pointer_events::T as PointerEvents;
2037 use style::computed_values::visibility::T as Visibility;
2038
2039 if matches!(self.style().display, taffy::Display::None) {
2048 return None;
2049 }
2050
2051 if let Some(style) = self.primary_styles() {
2053 if matches!(
2054 style.clone_visibility(),
2055 Visibility::Hidden | Visibility::Collapse
2056 ) {
2057 return None;
2058 }
2059 }
2060
2061 let pointer_events_none = self
2064 .primary_styles()
2065 .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
2066
2067 let mut x = x - self.final_layout().location.x + self.scroll_offset().x as f32;
2068 let mut y = y - self.final_layout().location.y + self.scroll_offset().y as f32;
2069
2070 if let Some(t) = *self.transform() {
2071 let p = t.inverse() * kurbo::Point::new(x as f64 * scale, y as f64 * scale);
2072 x = (p.x / scale) as f32;
2073 y = (p.y / scale) as f32;
2074 }
2075
2076 let size = self.final_layout().size;
2077 let matches_self = !(x < 0.0
2078 || x > size.width + self.scroll_offset().x as f32
2079 || y < 0.0
2080 || y > size.height + self.scroll_offset().y as f32);
2081
2082 let content_size = self.final_layout().content_size;
2083 let matches_content = !(x < 0.0
2084 || x > content_size.width + self.scroll_offset().x as f32
2085 || y < 0.0
2086 || y > content_size.height + self.scroll_offset().y as f32);
2087
2088 let matches_hoisted_content = match &self.stacking_context {
2089 Some(sc) => {
2090 let content_area = sc.content_area;
2091 x >= content_area.left + self.scroll_offset().x as f32
2092 && x <= content_area.right + self.scroll_offset().x as f32
2093 && y >= content_area.top + self.scroll_offset().y as f32
2094 && y <= content_area.bottom + self.scroll_offset().y as f32
2095 }
2096 None => false,
2097 };
2098
2099 let overflow = *self.scrollable_overflow();
2102
2103 let matches_overflow = x >= (overflow.x0 / scale) as f32
2104 && x <= (overflow.x1 / scale) as f32
2105 && y >= (overflow.y0 / scale) as f32
2106 && y <= (overflow.y1 / scale) as f32;
2107
2108 if !matches_self && !matches_content && !matches_hoisted_content && !matches_overflow {
2109 return None;
2110 }
2111
2112 if matches_self
2115 && let Some(sb) = self.scrollbar_at_local(
2116 (x - self.scroll_offset().x as f32) as f64,
2117 (y - self.scroll_offset().y as f32) as f64,
2118 )
2119 {
2120 *scrollbar = Some(sb);
2121 }
2122
2123 if matches_hoisted_content {
2128 if let Some(hoisted) = &self.stacking_context {
2129 for hoisted_child in hoisted.pos_z_hoisted_children().rev() {
2130 if hoisted_child.clips.iter().any(|clip| {
2131 x < clip.left || x > clip.right || y < clip.top || y > clip.bottom
2132 }) {
2133 continue;
2134 }
2135 let x = x - hoisted_child.position.x;
2136 let y = y - hoisted_child.position.y;
2137 if let Some(hit) = self
2138 .with(hoisted_child.node_id)
2139 .hit_inner_collect(x, y, scale, scrollbar, hits)
2140 {
2141 return Some(hit);
2142 }
2143 }
2144 }
2145 }
2146
2147 for child_id in self.paint_children.borrow().iter().flatten().rev() {
2149 if let Some(hit) = self
2150 .with(*child_id)
2151 .hit_inner_collect(x, y, scale, scrollbar, hits)
2152 {
2153 return Some(hit);
2154 }
2155 }
2156
2157 if matches_hoisted_content {
2159 if let Some(hoisted) = &self.stacking_context {
2160 for hoisted_child in hoisted.neg_z_hoisted_children().rev() {
2161 if hoisted_child.clips.iter().any(|clip| {
2162 x < clip.left || x > clip.right || y < clip.top || y > clip.bottom
2163 }) {
2164 continue;
2165 }
2166 let x = x - hoisted_child.position.x;
2167 let y = y - hoisted_child.position.y;
2168 if let Some(hit) = self
2169 .with(hoisted_child.node_id)
2170 .hit_inner_collect(x, y, scale, scrollbar, hits)
2171 {
2172 return Some(hit);
2173 }
2174 }
2175 }
2176 }
2177
2178 #[cfg(feature = "svg")]
2180 if matches_self && let Some(hit) = self.hit_inline_svg(x, y, hits) {
2181 return Some(hit);
2182 }
2183
2184 if self.flags.is_inline_root() {
2186 x -= self.final_layout().padding.left + self.final_layout().border.left;
2187 y -= self.final_layout().padding.top + self.final_layout().border.top;
2188 let element_data = &self.element_data().unwrap();
2189 if let Some(ild) = element_data.inline_layout_data.as_ref() {
2190 let layout = &ild.layout;
2191 let scale = layout.scale();
2192
2193 if let Some((cluster, _side)) =
2194 Cluster::from_point_exact(layout, x * scale, y * scale)
2195 {
2196 let style_index = cluster.glyphs().next()?.style_index();
2197 let node_id = layout.styles()[style_index].brush.id;
2198 let text_pointer_events_none = self
2199 .with(node_id)
2200 .primary_styles()
2201 .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
2202 if !text_pointer_events_none {
2203 let hit = HitResult {
2204 node_id,
2205 x,
2206 y,
2207 is_text: true,
2208 };
2209 if let Some(hits) = hits.as_mut() {
2210 hits.push(hit);
2211 } else {
2212 return Some(hit);
2213 }
2214 }
2215 }
2216 }
2217 }
2218
2219 if matches_self && !pointer_events_none {
2221 let hit = HitResult {
2222 node_id: self.id,
2223 x,
2224 y,
2225 is_text: false,
2226 };
2227 if let Some(hits) = hits.as_mut() {
2228 hits.push(hit);
2229 } else {
2230 return Some(hit);
2231 }
2232 }
2233
2234 None
2235 }
2236
2237 pub fn inline_root_ancestor(&self) -> Option<&Node> {
2240 let mut node = self;
2241 loop {
2242 if node.flags.is_inline_root() {
2243 return Some(node);
2244 }
2245 let id = node.layout_parent.get()?;
2246 node = self.with(id);
2247 }
2248 }
2249
2250 pub fn text_offset_at_point(&self, x: f32, y: f32) -> Option<usize> {
2254 if !self.flags.is_inline_root() {
2255 return None;
2256 }
2257
2258 let element_data = self.element_data()?;
2259 let inline_layout = element_data.inline_layout_data.as_ref()?;
2260 let layout = &inline_layout.layout;
2261 let scale = layout.scale();
2262
2263 let (cluster, side) = Cluster::from_point(layout, x * scale, y * scale)?;
2265
2266 let is_leading = side == ClusterSide::Left;
2271 let offset = if cluster.is_rtl() {
2272 if is_leading {
2273 cluster.text_range().end
2274 } else {
2275 cluster.text_range().start
2276 }
2277 } else {
2278 if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) {
2280 cluster.text_range().start
2281 } else {
2282 cluster.text_range().end
2283 }
2284 };
2285
2286 Some(offset)
2287 }
2288
2289 pub fn text_range_at_point(
2299 &self,
2300 x: f32,
2301 y: f32,
2302 granularity: TextGranularity,
2303 ) -> Option<Range<usize>> {
2304 if !self.flags.is_inline_root() {
2305 return None;
2306 }
2307
2308 let element_data = self.element_data()?;
2309 let inline_layout = element_data.inline_layout_data.as_ref()?;
2310 let layout = &inline_layout.layout;
2311 let scale = layout.scale();
2312 let (x, y) = (x * scale, y * scale);
2313
2314 Cluster::from_point(layout, x, y)?;
2319
2320 let selection = match granularity {
2321 TextGranularity::Word => Selection::word_from_point(layout, x, y),
2322 TextGranularity::Line => Selection::hard_line_from_point(layout, x, y),
2326 };
2327
2328 let range = selection.text_range();
2329 if range.is_empty() { None } else { Some(range) }
2330 }
2331
2332 pub fn absolute_position(&self, x: f32, y: f32) -> crate::util::Point<f32> {
2334 let x = x + self.final_layout().location.x;
2337 let y = y + self.final_layout().location.y;
2338
2339 self.layout_parent
2341 .get()
2342 .and_then(|id| self.tree().get(id))
2343 .map(|parent| {
2344 parent.absolute_position(
2345 x - parent.scroll_offset().x as f32,
2346 y - parent.scroll_offset().y as f32,
2347 )
2348 })
2349 .unwrap_or(crate::util::Point { x, y })
2350 }
2351
2352 fn is_offset_parent(&self) -> bool {
2355 let Some(styles) = self.primary_styles() else {
2356 return false;
2357 };
2358 if styles.get_box().position != Position::Static {
2359 return true;
2360 }
2361 self.data.is_element_with_tag_name(&local_name!("body"))
2362 || self.data.is_element_with_tag_name(&local_name!("td"))
2363 || self.data.is_element_with_tag_name(&local_name!("th"))
2364 }
2365
2366 pub fn offset_parent(&self) -> Option<&Node> {
2374 let mut node = self;
2375 loop {
2376 node = self.with(node.layout_parent.get()?);
2377 if node.is_offset_parent() {
2378 return Some(node);
2379 }
2380 }
2381 }
2382
2383 pub fn offset_top_left(&self) -> crate::util::Point<f32> {
2386 let mut x = 0.0;
2387 let mut y = 0.0;
2388 let mut current = self;
2389 loop {
2390 let layout = current.final_layout();
2391 x += layout.location.x;
2392 y += layout.location.y;
2393
2394 let Some(parent_id) = current.layout_parent.get() else {
2395 break;
2396 };
2397 let parent = self.with(parent_id);
2398 if parent.is_offset_parent() {
2399 let border = parent.final_layout().border;
2400 x -= border.left;
2401 y -= border.top;
2402 break;
2403 }
2404 current = parent;
2405 }
2406 crate::util::Point { x, y }
2407 }
2408
2409 pub fn synthetic_click_event(&self, mods: Modifiers) -> DomEventData {
2411 DomEventData::Click(self.synthetic_click_event_data(mods))
2412 }
2413
2414 pub fn synthetic_click_event_data(&self, mods: Modifiers) -> BlitzPointerEvent {
2415 let absolute_position = self.absolute_position(0.0, 0.0);
2416 let x = absolute_position.x + (self.final_layout().size.width / 2.0);
2417 let y = absolute_position.y + (self.final_layout().size.height / 2.0);
2418
2419 BlitzPointerEvent {
2420 id: BlitzPointerId::Mouse,
2421 is_primary: true,
2422 coords: PointerCoords {
2423 page_x: x,
2424 page_y: y,
2425
2426 screen_x: x,
2428 screen_y: y,
2429 client_x: x,
2430 client_y: y,
2431 },
2432 mods,
2433 button: Default::default(),
2434 buttons: Default::default(),
2435 details: Default::default(),
2436 element: Default::default(),
2437 active_pointers: Default::default(),
2438 }
2439 }
2440}
2441
2442impl PartialEq for Node {
2444 fn eq(&self, other: &Self) -> bool {
2445 self.id == other.id
2446 }
2447}
2448
2449impl Eq for Node {}
2450
2451impl std::fmt::Debug for Node {
2452 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2453 f.debug_struct("NodeData")
2455 .field("parent", &self.parent)
2456 .field("id", &self.id)
2457 .field("is_inline_root", &self.flags.is_inline_root())
2458 .field("children", &self.children)
2459 .field("layout_children", &self.layout_children.borrow())
2460 .field("node", &self.data)
2462 .field("stylo_element_data", &self.stylo_element_data_opt())
2463 .finish()
2466 }
2467}
2468
2469fn is_raw_text_element(name: &LocalName) -> bool {
2472 matches!(
2473 name.as_ref(),
2474 "style" | "script" | "xmp" | "iframe" | "noembed" | "noframes" | "plaintext" | "noscript"
2475 )
2476}
2477
2478fn is_void_element(name: &LocalName) -> bool {
2480 matches!(
2481 name.as_ref(),
2482 "area"
2483 | "base"
2484 | "basefont"
2485 | "bgsound"
2486 | "br"
2487 | "col"
2488 | "embed"
2489 | "frame"
2490 | "hr"
2491 | "img"
2492 | "input"
2493 | "keygen"
2494 | "link"
2495 | "meta"
2496 | "param"
2497 | "source"
2498 | "track"
2499 | "wbr"
2500 )
2501}
2502
2503fn escape_html(text: &str, attribute: bool, writer: &mut String) {
2506 for ch in text.chars() {
2507 match ch {
2508 '&' => writer.push_str("&"),
2509 '\u{a0}' => writer.push_str(" "),
2510 '"' if attribute => writer.push_str("""),
2511 '<' if !attribute => writer.push_str("<"),
2512 '>' if !attribute => writer.push_str(">"),
2513 _ => writer.push(ch),
2514 }
2515 }
2516}
2517
2518#[cfg(test)]
2519mod test {
2520 use style_dom::ElementState;
2521
2522 use crate::{Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, qual_name};
2523
2524 #[test]
2525 fn absolute_position_tolerates_a_detached_layout_parent() {
2526 let mut document = BaseDocument::new(DocumentConfig::default());
2527 let parent = document.create_node(NodeData::Element(Box::new(ElementData::new(
2528 qual_name!("section"),
2529 vec![],
2530 ))));
2531 let child = document.create_node(NodeData::Element(Box::new(ElementData::new(
2532 qual_name!("button"),
2533 vec![],
2534 ))));
2535 document
2536 .get_node(child)
2537 .unwrap()
2538 .layout_parent
2539 .set(Some(parent));
2540
2541 document.remove_node_from_tree(parent);
2546
2547 assert_eq!(
2548 document
2549 .get_node(child)
2550 .unwrap()
2551 .absolute_position(4.0, 7.0),
2552 crate::util::Point { x: 4.0, y: 7.0 },
2553 );
2554 }
2555
2556 #[test]
2557 fn create_node_with_disabled_attr() {
2558 let mut document = BaseDocument::new(DocumentConfig::default());
2559 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2560 qual_name!("button"),
2561 vec![Attribute {
2562 name: qual_name!("disabled"),
2563 value: "".into(),
2564 }],
2565 ))));
2566 let node = document.get_node(node).unwrap();
2567
2568 assert!(
2569 node.element_state().contains(ElementState::DISABLED),
2570 "form node is disabled"
2571 );
2572 assert!(
2573 !node.element_state().contains(ElementState::ENABLED),
2574 "form node is not enabled"
2575 );
2576 }
2577
2578 #[test]
2579 fn ignore_disabled_attr_content() {
2580 let mut document = BaseDocument::new(DocumentConfig::default());
2581 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2582 qual_name!("button"),
2583 vec![Attribute {
2584 name: qual_name!("disabled"),
2585 value: "false".into(),
2586 }],
2587 ))));
2588 let node = document.get_node(node).unwrap();
2589
2590 assert!(
2591 node.element_state().contains(ElementState::DISABLED),
2592 "form node is disabled"
2593 );
2594 assert!(
2595 !node.element_state().contains(ElementState::ENABLED),
2596 "form node is not enabled"
2597 );
2598 }
2599
2600 #[test]
2601 fn create_node_with_ignored_disable() {
2602 let mut document = BaseDocument::new(DocumentConfig::default());
2603 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2604 qual_name!("a"),
2605 vec![Attribute {
2606 name: qual_name!("disabled"),
2607 value: "".into(),
2608 }],
2609 ))));
2610 let node = document.get_node(node).unwrap();
2611
2612 assert!(
2613 !node.element_state().contains(ElementState::DISABLED),
2614 "Non form node cannot be disabled"
2615 );
2616 assert!(
2617 !node.element_state().contains(ElementState::ENABLED),
2618 "Non form node cannot be enabled"
2619 );
2620 }
2621
2622 #[test]
2623 fn create_empty_enabled_node() {
2624 let mut document = BaseDocument::new(DocumentConfig::default());
2625 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2626 qual_name!("button"),
2627 vec![],
2628 ))));
2629 let node = document.get_node(node).unwrap();
2630
2631 assert!(
2632 node.element_state().contains(ElementState::ENABLED),
2633 "Button should be enabled by default"
2634 );
2635 }
2636}