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 #[cfg(feature = "shadow-dom")]
954 pub(crate) slottables: Vec<NodeId>,
955 pub(crate) selector_flags: Cell<ElementSelectorFlags>,
956}
957
958impl std::fmt::Debug for ShadowRootData {
959 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
960 formatter
961 .debug_struct("ShadowRootData")
962 .field("host", &self.host)
963 .field("mode", &self.mode)
964 .field("declarative", &self.declarative)
965 .field("clonable", &self.clonable)
966 .field("serializable", &self.serializable)
967 .field("delegates_focus", &self.delegates_focus)
968 .field("stylesheet_nodes", &self.stylesheet_nodes)
969 .field("cascade_dirty", &self.cascade_dirty)
970 .field("slots", &self.slots)
971 .finish()
972 }
973}
974
975impl ShadowRootData {
976 pub fn new(host: NodeId, mode: ShadowRootMode) -> Self {
977 Self {
978 host,
979 mode,
980 declarative: false,
981 clonable: false,
982 serializable: false,
983 delegates_focus: false,
984 stylesheet_nodes: Vec::new(),
985 cascade_data: None,
986 cascade_dirty: true,
987 slots: Vec::new(),
988 #[cfg(feature = "shadow-dom")]
989 slottables: Vec::new(),
990 selector_flags: Cell::new(ElementSelectorFlags::empty()),
991 }
992 }
993}
994
995#[derive(Debug, PartialEq, Eq)]
997pub enum MarkupNode {
998 ProcessingInstruction {
999 target: String,
1000 },
1001 Doctype {
1002 name: String,
1003 public_id: String,
1004 system_id: String,
1005 },
1006}
1007
1008#[derive(Debug, Clone)]
1010pub enum NodeData {
1011 Document(Box<DocumentData>),
1013
1014 Element(Box<ElementData>),
1016
1017 AnonymousBlock(Box<ElementData>),
1019
1020 Text(TextNodeData),
1022
1023 Comment {
1025 contents: String,
1027 },
1028
1029 DocumentFragment,
1032
1033 ShadowRoot(ShadowRootData),
1035 }
1042
1043impl NodeData {
1044 pub fn downcast_element(&self) -> Option<&ElementData> {
1045 match self {
1046 Self::Element(data) => Some(data),
1047 Self::AnonymousBlock(data) => Some(data),
1048 _ => None,
1049 }
1050 }
1051
1052 pub fn downcast_element_mut(&mut self) -> Option<&mut ElementData> {
1053 match self {
1054 Self::Element(data) => Some(data),
1055 Self::AnonymousBlock(data) => Some(data),
1056 _ => None,
1057 }
1058 }
1059
1060 pub fn is_element_with_tag_name(&self, name: &impl PartialEq<LocalName>) -> bool {
1061 let Some(elem) = self.downcast_element() else {
1062 return false;
1063 };
1064 *name == elem.name.local
1065 }
1066
1067 pub fn attrs(&self) -> Option<&[Attribute]> {
1068 Some(&self.downcast_element()?.attrs)
1069 }
1070
1071 pub fn attr(&self, name: impl PartialEq<LocalName>) -> Option<&str> {
1072 self.downcast_element()?.attr(name)
1073 }
1074
1075 pub fn has_attr(&self, name: impl PartialEq<LocalName>) -> bool {
1076 self.downcast_element()
1077 .is_some_and(|elem| elem.has_attr(name))
1078 }
1079
1080 pub fn kind(&self) -> NodeKind {
1081 match self {
1082 NodeData::Document(_) => NodeKind::Document,
1083 NodeData::Element(_) => NodeKind::Element,
1084 NodeData::AnonymousBlock(_) => NodeKind::AnonymousBlock,
1085 NodeData::Text(_) => NodeKind::Text,
1086 NodeData::Comment { .. } => NodeKind::Comment,
1087 NodeData::DocumentFragment => NodeKind::DocumentFragment,
1088 NodeData::ShadowRoot(_) => NodeKind::ShadowRoot,
1089 }
1090 }
1091
1092 pub fn shadow_root_data(&self) -> Option<&ShadowRootData> {
1093 match self {
1094 Self::ShadowRoot(data) => Some(data),
1095 _ => None,
1096 }
1097 }
1098
1099 pub fn shadow_root_data_mut(&mut self) -> Option<&mut ShadowRootData> {
1100 match self {
1101 Self::ShadowRoot(data) => Some(data),
1102 _ => None,
1103 }
1104 }
1105}
1106
1107#[derive(Debug, Clone)]
1108pub struct TextNodeData {
1109 pub content: String,
1111}
1112
1113impl TextNodeData {
1114 pub fn new(content: String) -> Self {
1115 Self { content }
1116 }
1117}
1118
1119impl Node {
1144 pub fn tree(&self) -> &crate::NodeTree {
1145 unsafe { &*self.tree }
1146 }
1147
1148 pub fn is_html_document(&self) -> bool {
1149 let document = match &self.data {
1150 NodeData::Document(data) => return data.content_type == "text/html",
1151 _ => self.owner_document.and_then(|id| self.tree().get(id)),
1152 };
1153 match document.map(|node| &node.data) {
1154 Some(NodeData::Document(data)) => data.content_type == "text/html",
1155 _ => true,
1156 }
1157 }
1158
1159 pub fn qualified_element_name(&self) -> Option<String> {
1160 let element = self.element_data()?;
1161 let name = match &element.name.prefix {
1162 Some(prefix) => format!("{prefix}:{}", element.name.local),
1163 None => element.name.local.to_string(),
1164 };
1165 Some(
1166 if self.is_html_document() && element.name.ns == markup5ever::ns!(html) {
1167 name.to_ascii_uppercase()
1168 } else {
1169 name
1170 },
1171 )
1172 }
1173
1174 #[track_caller]
1175 pub fn with(&self, id: NodeId) -> &Node {
1176 self.tree().get(id).unwrap()
1177 }
1178
1179 pub fn print_tree(&self, level: usize) {
1180 println!(
1181 "{} {} {:?} {} {:?}",
1182 " ".repeat(level),
1183 self.id,
1184 self.parent,
1185 self.node_debug_str().replace('\n', ""),
1186 self.children
1187 );
1188 for child_id in self.children.iter() {
1190 let child = self.with(*child_id);
1191 child.print_tree(level + 1)
1192 }
1193 }
1194
1195 pub fn index_of_child(&self, child_id: NodeId) -> Option<usize> {
1197 self.children.iter().position(|id| *id == child_id)
1198 }
1199
1200 pub fn child_index(&self) -> Option<usize> {
1202 let children = &self.tree()[self.parent?].children;
1203 let hint = self.child_index_hint.get();
1204 if children.get(hint) == Some(&self.id) {
1205 return Some(hint);
1206 }
1207 let index = children.iter().position(|id| *id == self.id)?;
1208 self.child_index_hint.set(index);
1209 Some(index)
1210 }
1211
1212 pub fn dom_child_at(&self, index: usize) -> Option<NodeId> {
1214 let id = *self.children.get(index)?;
1215 self.with(id).child_index_hint.set(index);
1216 Some(id)
1217 }
1218
1219 pub fn forward(&self, n: usize) -> Option<&Node> {
1221 let index = self.child_index()?.checked_add(n)?;
1222 let id = *self.tree()[self.parent?].children.get(index)?;
1223 let node = self.with(id);
1224 node.child_index_hint.set(index);
1225 Some(node)
1226 }
1227
1228 pub fn backward(&self, n: usize) -> Option<&Node> {
1229 let index = self.child_index()?.checked_sub(n)?;
1230 let id = *self.tree()[self.parent?].children.get(index)?;
1231 let node = self.with(id);
1232 node.child_index_hint.set(index);
1233 Some(node)
1234 }
1235
1236 pub fn is_element(&self) -> bool {
1237 matches!(self.data, NodeData::Element { .. })
1238 }
1239
1240 pub fn is_anonymous(&self) -> bool {
1241 matches!(self.data, NodeData::AnonymousBlock { .. })
1242 }
1243
1244 pub fn is_shadow_root(&self) -> bool {
1245 matches!(self.data, NodeData::ShadowRoot { .. })
1246 }
1247
1248 pub fn shadow_root_data(&self) -> Option<&ShadowRootData> {
1249 self.data.shadow_root_data()
1250 }
1251
1252 pub fn shadow_root_data_mut(&mut self) -> Option<&mut ShadowRootData> {
1253 self.data.shadow_root_data_mut()
1254 }
1255
1256 pub fn shadow_root_id(&self) -> Option<NodeId> {
1259 self.element_data().and_then(|el| el.shadow_root)
1260 }
1261
1262 #[inline]
1268 pub(crate) fn flattened_parent(&self) -> Option<&Node> {
1269 #[cfg(feature = "shadow-dom")]
1270 {
1271 if self.is_shadow_root() {
1272 return None;
1273 }
1274 if let Some(slot_id) = self.assigned_slot {
1275 return Some(self.with(slot_id));
1276 }
1277 let parent = self.with(self.parent?);
1278 if let Some(root) = parent.shadow_root_data() {
1279 return Some(self.with(root.host));
1280 }
1281 if parent.shadow_root_id().is_some()
1282 || parent
1283 .assigned_nodes
1284 .as_ref()
1285 .is_some_and(|nodes| !nodes.is_empty())
1286 {
1287 return None;
1288 }
1289 Some(parent)
1290 }
1291 #[cfg(not(feature = "shadow-dom"))]
1292 {
1293 self.parent.map(|id| self.with(id))
1294 }
1295 }
1296
1297 #[cfg(feature = "shadow-dom")]
1301 pub fn layout_dom_children(&self) -> &[NodeId] {
1302 match &self.flattened_children {
1303 Some(children) => children,
1304 None => &self.children,
1305 }
1306 }
1307
1308 #[cfg(not(feature = "shadow-dom"))]
1310 #[inline(always)]
1311 pub fn layout_dom_children(&self) -> &[NodeId] {
1312 &self.children
1313 }
1314
1315 pub fn is_text_node(&self) -> bool {
1316 matches!(self.data, NodeData::Text { .. })
1317 }
1318
1319 pub fn element_data(&self) -> Option<&ElementData> {
1320 match self.data {
1321 NodeData::Element(ref data) => Some(data),
1322 NodeData::AnonymousBlock(ref data) => Some(data),
1323 _ => None,
1324 }
1325 }
1326
1327 pub fn element_data_mut(&mut self) -> Option<&mut ElementData> {
1328 match self.data {
1329 NodeData::Element(ref mut data) => Some(data),
1330 NodeData::AnonymousBlock(ref mut data) => Some(data),
1331 _ => None,
1332 }
1333 }
1334
1335 pub fn text_data(&self) -> Option<&TextNodeData> {
1336 match self.data {
1337 NodeData::Text(ref data) => Some(data),
1338 _ => None,
1339 }
1340 }
1341
1342 pub fn text_data_mut(&mut self) -> Option<&mut TextNodeData> {
1343 match self.data {
1344 NodeData::Text(ref mut data) => Some(data),
1345 _ => None,
1346 }
1347 }
1348
1349 pub fn node_debug_str(&self) -> String {
1350 let mut s = String::new();
1351
1352 match &self.data {
1353 NodeData::Document(_) => write!(s, "DOCUMENT"),
1354 NodeData::DocumentFragment => write!(s, "FRAGMENT"),
1355 NodeData::Text(data) => {
1357 let bytes = data.content.as_bytes();
1358 write!(
1359 s,
1360 "TEXT {}",
1361 std::str::from_utf8(bytes.split_at(10.min(bytes.len())).0)
1362 .unwrap_or("INVALID UTF8")
1363 )
1364 }
1365 NodeData::Comment { .. } => write!(s, "COMMENT"),
1366 NodeData::AnonymousBlock(_) => write!(s, "AnonymousBlock"),
1367 NodeData::ShadowRoot(data) => write!(s, "#shadow-root ({:?})", data.mode),
1368 NodeData::Element(data) => {
1369 let name = &data.name;
1370 let class = self.attr(local_name!("class")).unwrap_or("");
1371 let id = self.attr(local_name!("id")).unwrap_or("");
1372 let display = self.display_constructed_as().to_css_string();
1373 write!(s, "<{}", name.local).unwrap();
1374 if !id.is_empty() {
1375 write!(s, " #{id}").unwrap();
1376 }
1377 if !class.is_empty() {
1378 if class.contains(' ') {
1379 write!(s, " class=\"{class}\"").unwrap()
1380 } else {
1381 write!(s, " .{class}").unwrap()
1382 }
1383 }
1384 write!(s, "> ({display})")
1385 } }
1387 .unwrap();
1388 s
1389 }
1390
1391 pub fn outer_html(&self) -> String {
1399 let mut output = String::new();
1400 self.write_outer_html(&mut output);
1401 output
1402 }
1403
1404 pub fn outer_html_pretty(&self) -> String {
1420 let mut output = String::new();
1421 self.write_outer_html_pretty(&mut output);
1422 output
1423 }
1424
1425 pub fn write_outer_html(&self, writer: &mut String) {
1426 self.write_outer_html_in_style(writer, OutputStyle::Normal, 0, None);
1427 }
1428
1429 #[cfg(feature = "svg")]
1430 pub(crate) fn write_outer_html_with_current_color(
1431 &self,
1432 writer: &mut String,
1433 current_color: &str,
1434 ) {
1435 self.write_outer_html_in_style(writer, OutputStyle::Normal, 0, Some(current_color));
1436 }
1437
1438 #[cfg(feature = "svg")]
1439 pub(crate) fn write_outer_html_with_svg_ids(
1440 &self,
1441 writer: &mut String,
1442 ids: &std::collections::HashMap<NodeId, String>,
1443 ) {
1444 self.write_outer_html_in_style_with_svg_ids(
1445 writer,
1446 OutputStyle::Normal,
1447 0,
1448 None,
1449 Some(ids),
1450 );
1451 }
1452
1453 pub fn write_outer_html_pretty(&self, writer: &mut String) {
1454 self.write_outer_html_in_style(writer, OutputStyle::Pretty, 0, None);
1455 }
1456
1457 fn write_outer_html_in_style(
1458 &self,
1459 writer: &mut String,
1460 style: OutputStyle,
1461 nesting: usize,
1462 current_color_override: Option<&str>,
1463 ) {
1464 self.write_outer_html_in_style_with_svg_ids(
1465 writer,
1466 style,
1467 nesting,
1468 current_color_override,
1469 None,
1470 );
1471 }
1472
1473 fn write_outer_html_in_style_with_svg_ids(
1474 &self,
1475 writer: &mut String,
1476 style: OutputStyle,
1477 nesting: usize,
1478 current_color_override: Option<&str>,
1479 svg_ids: Option<&std::collections::HashMap<NodeId, String>>,
1480 ) {
1481 const INDENT: &str = " ";
1482 let has_children = !self.children.is_empty();
1483 let computed_current_color = || {
1484 self.primary_styles()
1485 .map(|style| style.clone_color())
1486 .map(|color| crate::util::absolute_color_to_svg_css(&color))
1487 };
1488 let current_color = current_color_override
1489 .map(ToOwned::to_owned)
1490 .or_else(computed_current_color);
1491
1492 match &self.data {
1493 NodeData::Document(_) => {}
1494 NodeData::Comment { .. } => {}
1495 NodeData::AnonymousBlock(_) => {}
1496 NodeData::ShadowRoot(_) => {}
1497 NodeData::DocumentFragment => {}
1501 NodeData::Text(data) => {
1503 if matches!(style, OutputStyle::Pretty) {
1504 for _ in 0..nesting {
1505 writer.push_str(INDENT);
1506 }
1507 }
1508 writer.push_str(data.content.as_str());
1509 if matches!(style, OutputStyle::Pretty) {
1510 writer.push('\n');
1511 }
1512 }
1513 NodeData::Element(data) => {
1514 if matches!(style, OutputStyle::Pretty) {
1515 for _ in 0..nesting {
1516 writer.push_str(INDENT);
1517 }
1518 }
1519 let tag_name = if svg_ids.is_some()
1522 && data.name.ns.as_ref() == "http://www.w3.org/2000/svg"
1523 && data.name.local == local_name!("a")
1524 {
1525 "g"
1526 } else {
1527 data.name.local.as_ref()
1528 };
1529 let svg_id = svg_ids.and_then(|ids| ids.get(&self.id));
1530 writer.push('<');
1531 writer.push_str(tag_name);
1532 if let Some(id) = svg_id {
1533 writer.push_str(" id=\"");
1534 encode_quoted_attribute_to_string(id, writer);
1535 writer.push('"');
1536 }
1537
1538 for attr in data.attrs() {
1539 if svg_id.is_some()
1540 && attr.name.ns.as_ref().is_empty()
1541 && attr.name.local == local_name!("id")
1542 {
1543 continue;
1544 }
1545 writer.push(' ');
1546 if let Some(prefix) = attr
1547 .name
1548 .prefix
1549 .as_ref()
1550 .filter(|prefix| !prefix.is_empty())
1551 {
1552 writer.push_str(prefix);
1553 writer.push(':');
1554 }
1555 writer.push_str(&attr.name.local);
1556 writer.push_str("=\"");
1557 #[allow(clippy::unnecessary_unwrap)] if current_color.is_some() && attr.value.contains("currentColor") {
1559 let value = attr
1560 .value
1561 .replace("currentColor", current_color.as_ref().unwrap());
1562 encode_quoted_attribute_to_string(&value, writer);
1563 } else {
1564 encode_quoted_attribute_to_string(&attr.value, writer);
1565 }
1566 writer.push('"');
1567 }
1568 if !has_children {
1569 writer.push_str(" /");
1570 }
1571 writer.push('>');
1572 if matches!(style, OutputStyle::Pretty) {
1573 writer.push('\n');
1574 }
1575
1576 if has_children {
1577 for &child_id in &self.children {
1578 self.tree()[child_id].write_outer_html_in_style_with_svg_ids(
1579 writer,
1580 style,
1581 nesting + 1,
1582 current_color_override,
1583 svg_ids,
1584 );
1585 }
1586
1587 if matches!(style, OutputStyle::Pretty) {
1588 for _ in 0..nesting {
1589 writer.push_str(INDENT);
1590 }
1591 }
1592 writer.push_str("</");
1593 writer.push_str(tag_name);
1594 writer.push('>');
1595 if matches!(style, OutputStyle::Pretty) {
1596 writer.push('\n');
1597 }
1598 }
1599 }
1600 }
1601 }
1602
1603 pub fn write_html(&self, writer: &mut String) {
1611 match &self.data {
1612 NodeData::Text(data) => {
1613 let raw = self
1614 .parent
1615 .and_then(|parent| self.tree()[parent].element_data())
1616 .is_some_and(|parent| is_raw_text_element(&parent.name.local));
1617 if raw {
1618 writer.push_str(&data.content);
1619 } else {
1620 escape_html(&data.content, false, writer);
1621 }
1622 }
1623 NodeData::Comment { contents } => {
1624 writer.push_str("<!--");
1625 writer.push_str(contents);
1626 writer.push_str("-->");
1627 }
1628 NodeData::Element(data) => {
1629 writer.push('<');
1630 writer.push_str(&data.name.local);
1631 for attr in data.attrs() {
1632 writer.push(' ');
1633 if let Some(prefix) = attr
1634 .name
1635 .prefix
1636 .as_ref()
1637 .filter(|prefix| !prefix.is_empty())
1638 {
1639 writer.push_str(prefix);
1640 writer.push(':');
1641 }
1642 writer.push_str(&attr.name.local);
1643 writer.push_str("=\"");
1644 escape_html(&attr.value, true, writer);
1645 writer.push('"');
1646 }
1647 writer.push('>');
1648 if is_void_element(&data.name.local) {
1649 return;
1650 }
1651 self.write_children_html(writer);
1652 writer.push_str("</");
1653 writer.push_str(&data.name.local);
1654 writer.push('>');
1655 }
1656 NodeData::Document(_)
1657 | NodeData::DocumentFragment
1658 | NodeData::ShadowRoot(_)
1659 | NodeData::AnonymousBlock(_) => self.write_children_html(writer),
1660 }
1661 }
1662
1663 pub fn write_children_html(&self, writer: &mut String) {
1665 if let Some(contents) = self
1666 .element_data()
1667 .and_then(|element| element.template_contents)
1668 {
1669 self.tree()[contents].write_children_html(writer);
1670 return;
1671 }
1672 for &child_id in &self.children {
1673 self.tree()[child_id].write_html(writer);
1674 }
1675 }
1676
1677 pub fn attrs(&self) -> Option<&[Attribute]> {
1678 Some(&self.element_data()?.attrs)
1679 }
1680
1681 pub fn attr(&self, name: LocalName) -> Option<&str> {
1682 let attr = self
1683 .attrs()?
1684 .iter()
1685 .find(|attr| attr.name.ns == markup5ever::ns!() && attr.name.local == name)?;
1686 Some(&attr.value)
1687 }
1688
1689 pub fn primary_styles(&self) -> Option<impl Deref<Target = ServoArc<ComputedValues>>> {
1690 self.stylo_element_data_opt()
1691 .and_then(|stylo| stylo.primary_styles())
1692 }
1693
1694 pub fn text_content(&self) -> String {
1695 let mut out = String::new();
1696 self.write_text_content(&mut out);
1697 out
1698 }
1699
1700 pub fn rendered_inner_text(&self) -> String {
1704 use style::computed_values::visibility::T as Visibility;
1705
1706 enum Piece {
1707 Text(String),
1708 RequiredBreak(usize),
1709 }
1710
1711 fn text(node: &Node) -> String {
1712 let NodeData::Text(data) = &node.data else {
1713 return String::new();
1714 };
1715 if node.inline_root_ancestor().is_none() {
1717 return String::new();
1718 }
1719 let preserves = node
1720 .parent
1721 .map(|id| node.with(id))
1722 .and_then(Node::primary_styles)
1723 .is_some_and(|style| {
1724 style.get_inherited_text().white_space_collapse
1725 != style::computed_values::white_space_collapse::T::Collapse
1726 });
1727 if preserves {
1728 return data.content.to_string();
1729 }
1730 let mut result = String::with_capacity(data.content.len());
1733 let mut space = false;
1734 for ch in data.content.chars() {
1735 if matches!(ch, ' ' | '\t' | '\n' | '\r' | '\u{c}') {
1736 space = true;
1737 } else {
1738 if space {
1739 result.push(' ');
1740 space = false;
1741 }
1742 result.push(ch);
1743 }
1744 }
1745 if space {
1746 result.push(' ');
1747 }
1748 result
1749 }
1750 fn collect(node: &Node, pieces: &mut Vec<Piece>) {
1751 if node.is_element() && node.is_display_none() {
1752 return;
1753 }
1754 let visible = node
1755 .primary_styles()
1756 .is_none_or(|style| style.clone_visibility() == Visibility::Visible);
1757 if node.is_text_node() {
1758 let visible = node
1759 .parent
1760 .map(|id| node.with(id))
1761 .and_then(Node::primary_styles)
1762 .is_none_or(|style| style.clone_visibility() == Visibility::Visible);
1763 if visible {
1764 let value = text(node);
1765 if !value.is_empty() {
1766 pieces.push(Piece::Text(value));
1767 }
1768 }
1769 return;
1770 }
1771 if !node.is_element() {
1772 return;
1773 }
1774 let tag = node.element_data().expect("element").name.local.as_ref();
1775 if tag == "br" {
1776 if visible {
1777 pieces.push(Piece::Text("\n".to_owned()));
1778 }
1779 return;
1780 }
1781 let display = node.primary_styles().map(|style| style.clone_display());
1782 let block = display.is_some_and(|display| {
1783 display.outside() == DisplayOutside::Block && !display.is_contents()
1784 });
1785 let breaks = if tag == "p" { 2 } else { usize::from(block) };
1786 if breaks != 0 {
1787 pieces.push(Piece::RequiredBreak(breaks));
1788 }
1789 for child in &node.children {
1790 collect(node.with(*child), pieces);
1791 }
1792 if visible {
1793 if matches!(tag, "td" | "th") {
1794 let next_cell = node
1795 .parent
1796 .map(|parent| node.with(parent))
1797 .and_then(|parent| {
1798 parent.index_of_child(node.id).map(|index| (parent, index))
1799 })
1800 .is_some_and(|(parent, index)| {
1801 parent.children[index + 1..].iter().any(|id| {
1802 let child = parent.with(*id);
1803 child.data.is_element_with_tag_name(&local_name!("td"))
1804 || child.data.is_element_with_tag_name(&local_name!("th"))
1805 })
1806 });
1807 if next_cell {
1808 pieces.push(Piece::Text("\t".to_owned()));
1809 }
1810 } else if tag == "tr" {
1811 pieces.push(Piece::RequiredBreak(1));
1812 }
1813 }
1814 if breaks != 0 {
1815 pieces.push(Piece::RequiredBreak(breaks));
1816 }
1817 }
1818
1819 let mut current = Some(self);
1820 while let Some(node) = current {
1821 if !self.flags.is_in_document() || node.is_display_none() {
1822 return self.text_content();
1823 }
1824 current = node.parent.map(|id| node.with(id));
1825 }
1826 if self.primary_styles().is_none() {
1827 return self.text_content();
1828 }
1829 let mut pieces = Vec::new();
1830 collect(self, &mut pieces);
1831 let mut result = String::new();
1832 let mut pending = 0;
1833 for piece in pieces {
1834 match piece {
1835 Piece::RequiredBreak(count) => pending = pending.max(count),
1836 Piece::Text(value) => {
1837 if !result.is_empty() && pending != 0 {
1838 result.truncate(result.trim_end_matches(' ').len());
1839 for _ in 0..pending {
1840 result.push('\n');
1841 }
1842 }
1843 pending = 0;
1844 if result.is_empty() || result.ends_with('\n') || result.ends_with(' ') {
1845 result.push_str(value.trim_start_matches(' '));
1846 } else {
1847 result.push_str(&value);
1848 }
1849 }
1850 }
1851 }
1852 result.truncate(result.trim_end_matches(' ').len());
1853 result
1854 }
1855
1856 pub fn cssom_offset_parent(&self) -> Option<&Node> {
1859 if !self.flags.is_in_document()
1860 || self.data.is_element_with_tag_name(&local_name!("body"))
1861 || self.data.is_element_with_tag_name(&local_name!("html"))
1862 {
1863 return None;
1864 }
1865 let style = self.primary_styles()?;
1866 if style.clone_position() == Position::Fixed || style.clone_display().is_contents() {
1867 return None;
1868 }
1869 let mut current = Some(self);
1870 while let Some(node) = current {
1871 if node.is_display_none() {
1872 return None;
1873 }
1874 current = node.parent.map(|id| node.with(id));
1875 }
1876 let static_position = style.clone_position() == Position::Static;
1877 let zoom = style.effective_zoom.zoom(1.0);
1878 let mut parent = self.parent.map(|id| self.with(id));
1879 while let Some(node) = parent {
1880 if let Some(style) = node.primary_styles()
1881 && !style.clone_display().is_contents()
1882 && (node.is_offset_parent()
1883 || style.effective_zoom.zoom(1.0) != zoom
1884 || (static_position
1885 && node.data.is_element_with_tag_name(&local_name!("table"))))
1886 {
1887 return Some(node);
1888 }
1889 parent = node.parent.map(|id| node.with(id));
1890 }
1891 None
1892 }
1893
1894 pub fn write_text_content<W: Write>(&self, out: &mut W) {
1907 match &self.data {
1908 NodeData::Text(data) => {
1909 let _ = out.write_str(&data.content);
1910 }
1911 NodeData::Element(..)
1912 | NodeData::AnonymousBlock(..)
1913 | NodeData::Document(..)
1914 | NodeData::DocumentFragment
1915 | NodeData::ShadowRoot(..) => {
1916 for child_id in self.children.iter() {
1917 self.with(*child_id).write_text_content(out);
1918 }
1919 }
1920 _ => {}
1921 }
1922 }
1923
1924 pub fn flush_style_attribute(&mut self, url_extra_data: &UrlExtraData) {
1925 if let NodeData::Element(ref mut elem_data) = self.data {
1926 if let Some(guard) = elem_data.guard.clone() {
1927 elem_data.flush_style_attribute(&guard, url_extra_data);
1928 }
1929 }
1930 }
1931
1932 pub fn order(&self) -> i32 {
1933 self.primary_styles()
1934 .map(|s| match s.pseudo() {
1935 Some(PseudoElement::Before) => i32::MIN,
1936 Some(PseudoElement::After) => i32::MAX,
1937 _ => s.clone_order(),
1938 })
1939 .unwrap_or(0)
1940 }
1941
1942 pub fn z_index(&self) -> i32 {
1943 self.primary_styles()
1944 .map(|s| s.clone_z_index().integer_or(0))
1945 .unwrap_or(0)
1946 }
1947
1948 pub fn is_stacking_context_root(&self, is_flex_or_grid_item: bool) -> bool {
1950 let Some(style) = self.primary_styles() else {
1951 return false;
1952 };
1953
1954 let position = style.clone_position();
1955 let has_z_index = !style.clone_z_index().is_auto();
1956
1957 if style.clone_opacity() != 1.0 {
1958 return true;
1959 }
1960
1961 let position_based = match position {
1962 Position::Fixed | Position::Sticky => true,
1963 Position::Relative | Position::Absolute => has_z_index,
1964 Position::Static => has_z_index && is_flex_or_grid_item,
1965 };
1966 if position_based {
1967 return true;
1968 }
1969
1970 let box_styles = style.get_box();
1978 if !box_styles.transform.0.is_empty()
1979 || !matches!(box_styles.translate, Translate::None)
1980 || !matches!(box_styles.rotate, Rotate::None)
1981 || !matches!(box_styles.scale, Scale::None)
1982 {
1983 return true;
1984 }
1985
1986 if box_styles.isolation == Isolation::Isolate {
1991 return true;
1992 }
1993
1994 false
2001 }
2002
2003 pub fn hit(&self, x: f32, y: f32, scale: f64) -> Option<HitResult> {
2012 self.hit_inner(x, y, scale, &mut None)
2013 }
2014
2015 pub(crate) fn hit_inner(
2020 &self,
2021 x: f32,
2022 y: f32,
2023 scale: f64,
2024 scrollbar: &mut Option<crate::node::ScrollbarRef>,
2025 ) -> Option<HitResult> {
2026 self.hit_inner_collect(x, y, scale, scrollbar, &mut None)
2027 }
2028
2029 pub(crate) fn hit_inner_collect(
2031 &self,
2032 x: f32,
2033 y: f32,
2034 scale: f64,
2035 scrollbar: &mut Option<crate::node::ScrollbarRef>,
2036 hits: &mut Option<Vec<HitResult>>,
2037 ) -> Option<HitResult> {
2038 use style::computed_values::pointer_events::T as PointerEvents;
2039 use style::computed_values::visibility::T as Visibility;
2040
2041 if matches!(self.style().display, taffy::Display::None) {
2050 return None;
2051 }
2052
2053 if let Some(style) = self.primary_styles() {
2055 if matches!(
2056 style.clone_visibility(),
2057 Visibility::Hidden | Visibility::Collapse
2058 ) {
2059 return None;
2060 }
2061 }
2062
2063 let pointer_events_none = self
2066 .primary_styles()
2067 .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
2068
2069 let mut x = x - self.final_layout().location.x + self.scroll_offset().x as f32;
2070 let mut y = y - self.final_layout().location.y + self.scroll_offset().y as f32;
2071
2072 if let Some(t) = *self.transform() {
2073 let p = t.inverse() * kurbo::Point::new(x as f64 * scale, y as f64 * scale);
2074 x = (p.x / scale) as f32;
2075 y = (p.y / scale) as f32;
2076 }
2077
2078 let size = self.final_layout().size;
2079 let matches_self = !(x < 0.0
2080 || x > size.width + self.scroll_offset().x as f32
2081 || y < 0.0
2082 || y > size.height + self.scroll_offset().y as f32);
2083
2084 let content_size = self.final_layout().content_size;
2085 let matches_content = !(x < 0.0
2086 || x > content_size.width + self.scroll_offset().x as f32
2087 || y < 0.0
2088 || y > content_size.height + self.scroll_offset().y as f32);
2089
2090 let matches_hoisted_content = match &self.stacking_context {
2091 Some(sc) => {
2092 let content_area = sc.content_area;
2093 x >= content_area.left + self.scroll_offset().x as f32
2094 && x <= content_area.right + self.scroll_offset().x as f32
2095 && y >= content_area.top + self.scroll_offset().y as f32
2096 && y <= content_area.bottom + self.scroll_offset().y as f32
2097 }
2098 None => false,
2099 };
2100
2101 let overflow = *self.scrollable_overflow();
2104
2105 let matches_overflow = x >= (overflow.x0 / scale) as f32
2106 && x <= (overflow.x1 / scale) as f32
2107 && y >= (overflow.y0 / scale) as f32
2108 && y <= (overflow.y1 / scale) as f32;
2109
2110 if !matches_self && !matches_content && !matches_hoisted_content && !matches_overflow {
2111 return None;
2112 }
2113
2114 if matches_self
2117 && let Some(sb) = self.scrollbar_at_local(
2118 (x - self.scroll_offset().x as f32) as f64,
2119 (y - self.scroll_offset().y as f32) as f64,
2120 )
2121 {
2122 *scrollbar = Some(sb);
2123 }
2124
2125 if matches_hoisted_content {
2130 if let Some(hoisted) = &self.stacking_context {
2131 for hoisted_child in hoisted.pos_z_hoisted_children().rev() {
2132 if hoisted_child.clips.iter().any(|clip| {
2133 x < clip.left || x > clip.right || y < clip.top || y > clip.bottom
2134 }) {
2135 continue;
2136 }
2137 let x = x - hoisted_child.position.x;
2138 let y = y - hoisted_child.position.y;
2139 if let Some(hit) = self
2140 .with(hoisted_child.node_id)
2141 .hit_inner_collect(x, y, scale, scrollbar, hits)
2142 {
2143 return Some(hit);
2144 }
2145 }
2146 }
2147 }
2148
2149 for child_id in self.paint_children.borrow().iter().flatten().rev() {
2151 if let Some(hit) = self
2152 .with(*child_id)
2153 .hit_inner_collect(x, y, scale, scrollbar, hits)
2154 {
2155 return Some(hit);
2156 }
2157 }
2158
2159 if matches_hoisted_content {
2161 if let Some(hoisted) = &self.stacking_context {
2162 for hoisted_child in hoisted.neg_z_hoisted_children().rev() {
2163 if hoisted_child.clips.iter().any(|clip| {
2164 x < clip.left || x > clip.right || y < clip.top || y > clip.bottom
2165 }) {
2166 continue;
2167 }
2168 let x = x - hoisted_child.position.x;
2169 let y = y - hoisted_child.position.y;
2170 if let Some(hit) = self
2171 .with(hoisted_child.node_id)
2172 .hit_inner_collect(x, y, scale, scrollbar, hits)
2173 {
2174 return Some(hit);
2175 }
2176 }
2177 }
2178 }
2179
2180 #[cfg(feature = "svg")]
2182 if matches_self && let Some(hit) = self.hit_inline_svg(x, y, hits) {
2183 return Some(hit);
2184 }
2185
2186 if self.flags.is_inline_root() {
2188 x -= self.final_layout().padding.left + self.final_layout().border.left;
2189 y -= self.final_layout().padding.top + self.final_layout().border.top;
2190 let element_data = &self.element_data().unwrap();
2191 if let Some(ild) = element_data.inline_layout_data.as_ref() {
2192 let layout = &ild.layout;
2193 let scale = layout.scale();
2194
2195 if let Some((cluster, _side)) =
2196 Cluster::from_point_exact(layout, x * scale, y * scale)
2197 {
2198 let style_index = cluster.glyphs().next()?.style_index();
2199 let node_id = layout.styles()[style_index].brush.id;
2200 let text_pointer_events_none = self
2201 .with(node_id)
2202 .primary_styles()
2203 .is_some_and(|style| style.clone_pointer_events() == PointerEvents::None);
2204 if !text_pointer_events_none {
2205 let hit = HitResult {
2206 node_id,
2207 x,
2208 y,
2209 is_text: true,
2210 };
2211 if let Some(hits) = hits.as_mut() {
2212 hits.push(hit);
2213 } else {
2214 return Some(hit);
2215 }
2216 }
2217 }
2218 }
2219 }
2220
2221 if matches_self && !pointer_events_none {
2223 let hit = HitResult {
2224 node_id: self.id,
2225 x,
2226 y,
2227 is_text: false,
2228 };
2229 if let Some(hits) = hits.as_mut() {
2230 hits.push(hit);
2231 } else {
2232 return Some(hit);
2233 }
2234 }
2235
2236 None
2237 }
2238
2239 pub fn inline_root_ancestor(&self) -> Option<&Node> {
2242 let mut node = self;
2243 loop {
2244 if node.flags.is_inline_root() {
2245 return Some(node);
2246 }
2247 let id = node.layout_parent.get()?;
2248 node = self.with(id);
2249 }
2250 }
2251
2252 pub fn text_offset_at_point(&self, x: f32, y: f32) -> Option<usize> {
2256 if !self.flags.is_inline_root() {
2257 return None;
2258 }
2259
2260 let element_data = self.element_data()?;
2261 let inline_layout = element_data.inline_layout_data.as_ref()?;
2262 let layout = &inline_layout.layout;
2263 let scale = layout.scale();
2264
2265 let (cluster, side) = Cluster::from_point(layout, x * scale, y * scale)?;
2267
2268 let is_leading = side == ClusterSide::Left;
2273 let offset = if cluster.is_rtl() {
2274 if is_leading {
2275 cluster.text_range().end
2276 } else {
2277 cluster.text_range().start
2278 }
2279 } else {
2280 if is_leading || cluster.is_line_break() == Some(BreakReason::Explicit) {
2282 cluster.text_range().start
2283 } else {
2284 cluster.text_range().end
2285 }
2286 };
2287
2288 Some(offset)
2289 }
2290
2291 pub fn text_range_at_point(
2301 &self,
2302 x: f32,
2303 y: f32,
2304 granularity: TextGranularity,
2305 ) -> Option<Range<usize>> {
2306 if !self.flags.is_inline_root() {
2307 return None;
2308 }
2309
2310 let element_data = self.element_data()?;
2311 let inline_layout = element_data.inline_layout_data.as_ref()?;
2312 let layout = &inline_layout.layout;
2313 let scale = layout.scale();
2314 let (x, y) = (x * scale, y * scale);
2315
2316 Cluster::from_point(layout, x, y)?;
2321
2322 let selection = match granularity {
2323 TextGranularity::Word => Selection::word_from_point(layout, x, y),
2324 TextGranularity::Line => Selection::hard_line_from_point(layout, x, y),
2328 };
2329
2330 let range = selection.text_range();
2331 if range.is_empty() { None } else { Some(range) }
2332 }
2333
2334 pub fn absolute_position(&self, x: f32, y: f32) -> crate::util::Point<f32> {
2336 let x = x + self.final_layout().location.x;
2339 let y = y + self.final_layout().location.y;
2340
2341 self.layout_parent
2343 .get()
2344 .and_then(|id| self.tree().get(id))
2345 .map(|parent| {
2346 parent.absolute_position(
2347 x - parent.scroll_offset().x as f32,
2348 y - parent.scroll_offset().y as f32,
2349 )
2350 })
2351 .unwrap_or(crate::util::Point { x, y })
2352 }
2353
2354 fn is_offset_parent(&self) -> bool {
2357 let Some(styles) = self.primary_styles() else {
2358 return false;
2359 };
2360 if styles.get_box().position != Position::Static {
2361 return true;
2362 }
2363 self.data.is_element_with_tag_name(&local_name!("body"))
2364 || self.data.is_element_with_tag_name(&local_name!("td"))
2365 || self.data.is_element_with_tag_name(&local_name!("th"))
2366 }
2367
2368 pub fn offset_parent(&self) -> Option<&Node> {
2376 let mut node = self;
2377 loop {
2378 node = self.with(node.layout_parent.get()?);
2379 if node.is_offset_parent() {
2380 return Some(node);
2381 }
2382 }
2383 }
2384
2385 pub fn offset_top_left(&self) -> crate::util::Point<f32> {
2388 let mut x = 0.0;
2389 let mut y = 0.0;
2390 let mut current = self;
2391 loop {
2392 let layout = current.final_layout();
2393 x += layout.location.x;
2394 y += layout.location.y;
2395
2396 let Some(parent_id) = current.layout_parent.get() else {
2397 break;
2398 };
2399 let parent = self.with(parent_id);
2400 if parent.is_offset_parent() {
2401 let border = parent.final_layout().border;
2402 x -= border.left;
2403 y -= border.top;
2404 break;
2405 }
2406 current = parent;
2407 }
2408 crate::util::Point { x, y }
2409 }
2410
2411 pub fn synthetic_click_event(&self, mods: Modifiers) -> DomEventData {
2413 DomEventData::Click(self.synthetic_click_event_data(mods))
2414 }
2415
2416 pub fn synthetic_click_event_data(&self, mods: Modifiers) -> BlitzPointerEvent {
2417 let absolute_position = self.absolute_position(0.0, 0.0);
2418 let x = absolute_position.x + (self.final_layout().size.width / 2.0);
2419 let y = absolute_position.y + (self.final_layout().size.height / 2.0);
2420
2421 BlitzPointerEvent {
2422 id: BlitzPointerId::Mouse,
2423 is_primary: true,
2424 coords: PointerCoords {
2425 page_x: x,
2426 page_y: y,
2427
2428 screen_x: x,
2430 screen_y: y,
2431 client_x: x,
2432 client_y: y,
2433 },
2434 mods,
2435 button: Default::default(),
2436 buttons: Default::default(),
2437 details: Default::default(),
2438 element: Default::default(),
2439 active_pointers: Default::default(),
2440 }
2441 }
2442}
2443
2444impl PartialEq for Node {
2446 fn eq(&self, other: &Self) -> bool {
2447 self.id == other.id
2448 }
2449}
2450
2451impl Eq for Node {}
2452
2453impl std::fmt::Debug for Node {
2454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2455 f.debug_struct("NodeData")
2457 .field("parent", &self.parent)
2458 .field("id", &self.id)
2459 .field("is_inline_root", &self.flags.is_inline_root())
2460 .field("children", &self.children)
2461 .field("layout_children", &self.layout_children.borrow())
2462 .field("node", &self.data)
2464 .field("stylo_element_data", &self.stylo_element_data_opt())
2465 .finish()
2468 }
2469}
2470
2471fn is_raw_text_element(name: &LocalName) -> bool {
2474 matches!(
2475 name.as_ref(),
2476 "style" | "script" | "xmp" | "iframe" | "noembed" | "noframes" | "plaintext" | "noscript"
2477 )
2478}
2479
2480fn is_void_element(name: &LocalName) -> bool {
2482 matches!(
2483 name.as_ref(),
2484 "area"
2485 | "base"
2486 | "basefont"
2487 | "bgsound"
2488 | "br"
2489 | "col"
2490 | "embed"
2491 | "frame"
2492 | "hr"
2493 | "img"
2494 | "input"
2495 | "keygen"
2496 | "link"
2497 | "meta"
2498 | "param"
2499 | "source"
2500 | "track"
2501 | "wbr"
2502 )
2503}
2504
2505fn escape_html(text: &str, attribute: bool, writer: &mut String) {
2508 for ch in text.chars() {
2509 match ch {
2510 '&' => writer.push_str("&"),
2511 '\u{a0}' => writer.push_str(" "),
2512 '"' if attribute => writer.push_str("""),
2513 '<' if !attribute => writer.push_str("<"),
2514 '>' if !attribute => writer.push_str(">"),
2515 _ => writer.push(ch),
2516 }
2517 }
2518}
2519
2520#[cfg(test)]
2521mod test {
2522 use style_dom::ElementState;
2523
2524 use crate::{Attribute, BaseDocument, DocumentConfig, ElementData, NodeData, qual_name};
2525
2526 #[test]
2527 fn absolute_position_tolerates_a_detached_layout_parent() {
2528 let mut document = BaseDocument::new(DocumentConfig::default());
2529 let parent = document.create_node(NodeData::Element(Box::new(ElementData::new(
2530 qual_name!("section"),
2531 vec![],
2532 ))));
2533 let child = document.create_node(NodeData::Element(Box::new(ElementData::new(
2534 qual_name!("button"),
2535 vec![],
2536 ))));
2537 document
2538 .get_node(child)
2539 .unwrap()
2540 .layout_parent
2541 .set(Some(parent));
2542
2543 document.remove_node_from_tree(parent);
2548
2549 assert_eq!(
2550 document
2551 .get_node(child)
2552 .unwrap()
2553 .absolute_position(4.0, 7.0),
2554 crate::util::Point { x: 4.0, y: 7.0 },
2555 );
2556 }
2557
2558 #[test]
2559 fn create_node_with_disabled_attr() {
2560 let mut document = BaseDocument::new(DocumentConfig::default());
2561 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2562 qual_name!("button"),
2563 vec![Attribute {
2564 name: qual_name!("disabled"),
2565 value: "".into(),
2566 }],
2567 ))));
2568 let node = document.get_node(node).unwrap();
2569
2570 assert!(
2571 node.element_state().contains(ElementState::DISABLED),
2572 "form node is disabled"
2573 );
2574 assert!(
2575 !node.element_state().contains(ElementState::ENABLED),
2576 "form node is not enabled"
2577 );
2578 }
2579
2580 #[test]
2581 fn ignore_disabled_attr_content() {
2582 let mut document = BaseDocument::new(DocumentConfig::default());
2583 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2584 qual_name!("button"),
2585 vec![Attribute {
2586 name: qual_name!("disabled"),
2587 value: "false".into(),
2588 }],
2589 ))));
2590 let node = document.get_node(node).unwrap();
2591
2592 assert!(
2593 node.element_state().contains(ElementState::DISABLED),
2594 "form node is disabled"
2595 );
2596 assert!(
2597 !node.element_state().contains(ElementState::ENABLED),
2598 "form node is not enabled"
2599 );
2600 }
2601
2602 #[test]
2603 fn create_node_with_ignored_disable() {
2604 let mut document = BaseDocument::new(DocumentConfig::default());
2605 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2606 qual_name!("a"),
2607 vec![Attribute {
2608 name: qual_name!("disabled"),
2609 value: "".into(),
2610 }],
2611 ))));
2612 let node = document.get_node(node).unwrap();
2613
2614 assert!(
2615 !node.element_state().contains(ElementState::DISABLED),
2616 "Non form node cannot be disabled"
2617 );
2618 assert!(
2619 !node.element_state().contains(ElementState::ENABLED),
2620 "Non form node cannot be enabled"
2621 );
2622 }
2623
2624 #[test]
2625 fn create_empty_enabled_node() {
2626 let mut document = BaseDocument::new(DocumentConfig::default());
2627 let node = document.create_node(NodeData::Element(Box::new(ElementData::new(
2628 qual_name!("button"),
2629 vec![],
2630 ))));
2631 let node = document.get_node(node).unwrap();
2632
2633 assert!(
2634 node.element_state().contains(ElementState::ENABLED),
2635 "Button should be enabled by default"
2636 );
2637 }
2638}