1use alloc::vec::Vec;
8
9use azul_css::css::{
10 AttributeMatchOp, CssAttributeSelector, CssContentGroup, CssNthChildSelector,
11 CssNthChildSelector::{Number, Even, Odd, Pattern}, CssPath, CssPathPseudoSelector, CssPathSelector,
12};
13
14use crate::{
15 dom::NodeData,
16 id::{NodeDataContainer, NodeDataContainerRef, NodeHierarchyRef, NodeId},
17 styled_dom::NodeHierarchyItem,
18};
19
20#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22#[repr(C)]
23pub struct CascadeInfo {
24 pub index_in_parent: u32,
25 pub is_last_child: bool,
26}
27
28impl_option!(
29 CascadeInfo,
30 OptionCascadeInfo,
31 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
32);
33
34impl_vec!(CascadeInfo, CascadeInfoVec, CascadeInfoVecDestructor, CascadeInfoVecDestructorType, CascadeInfoVecSlice, OptionCascadeInfo);
35impl_vec_mut!(CascadeInfo, CascadeInfoVec);
36impl_vec_debug!(CascadeInfo, CascadeInfoVec);
37impl_vec_partialord!(CascadeInfo, CascadeInfoVec);
38impl_vec_clone!(CascadeInfo, CascadeInfoVec, CascadeInfoVecDestructor);
39impl_vec_partialeq!(CascadeInfo, CascadeInfoVec);
40
41impl CascadeInfoVec {
42 #[must_use] pub fn as_container(&self) -> NodeDataContainerRef<'_, CascadeInfo> {
43 NodeDataContainerRef {
44 internal: self.as_ref(),
45 }
46 }
47}
48
49#[allow(clippy::needless_pass_by_value)] #[must_use] pub fn matches_html_element(
53 css_path: &CssPath,
54 node_id: NodeId,
55 node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
56 node_data: &NodeDataContainerRef<'_, NodeData>,
57 html_node_tree: &NodeDataContainerRef<'_, CascadeInfo>,
58 expected_path_ending: Option<CssPathPseudoSelector>,
59) -> bool {
60 use self::CssGroupSplitReason::{DirectChildren, Children, AdjacentSibling, GeneralSibling};
61
62 if css_path.selectors.is_empty() {
63 return false;
64 }
65
66 if node_data[node_id].is_anonymous() {
69 return false;
70 }
71
72 let groups: Vec<(CssContentGroup<'_>, CssGroupSplitReason)> =
74 CssGroupIterator::new(css_path.selectors.as_ref()).collect();
75
76 if groups.is_empty() {
77 return false;
78 }
79
80 let (ref first_group, first_reason) = groups[0];
82 let is_last_content_group = groups.len() == 1;
83 if !selector_group_matches(
84 first_group,
85 html_node_tree[node_id],
86 &node_data[node_id],
87 node_id,
88 expected_path_ending.as_ref(),
89 is_last_content_group,
90 ) {
91 return false;
92 }
93
94 let mut current_node = node_id;
97
98 for (group_idx, (content_group, _reason)) in groups.iter().enumerate().skip(1) {
99 let combinator = groups[group_idx - 1].1;
101 let is_last = group_idx == groups.len() - 1;
102
103 match combinator {
104 DirectChildren => {
105 let parent = find_non_anonymous_parent(current_node, node_hierarchy, node_data);
107 match parent {
108 Some(p) if selector_group_matches(
109 content_group, html_node_tree[p], &node_data[p], p,
110 expected_path_ending.as_ref(), is_last,
111 ) => { current_node = p; }
112 _ => return false,
113 }
114 }
115 Children => {
116 let mut ancestor = find_non_anonymous_parent(current_node, node_hierarchy, node_data);
118 let mut found = false;
119 while let Some(anc) = ancestor {
120 if selector_group_matches(
121 content_group, html_node_tree[anc], &node_data[anc], anc,
122 expected_path_ending.as_ref(), is_last,
123 ) {
124 current_node = anc;
125 found = true;
126 break;
127 }
128 ancestor = find_non_anonymous_parent(anc, node_hierarchy, node_data);
129 }
130 if !found {
131 return false;
132 }
133 }
134 AdjacentSibling => {
135 let sibling = find_non_anonymous_prev_sibling(current_node, node_hierarchy, node_data);
137 match sibling {
138 Some(s) if selector_group_matches(
139 content_group, html_node_tree[s], &node_data[s], s,
140 expected_path_ending.as_ref(), is_last,
141 ) => { current_node = s; }
142 _ => return false,
143 }
144 }
145 GeneralSibling => {
146 let mut sibling = find_non_anonymous_prev_sibling(current_node, node_hierarchy, node_data);
148 let mut found = false;
149 while let Some(sib) = sibling {
150 if selector_group_matches(
151 content_group, html_node_tree[sib], &node_data[sib], sib,
152 expected_path_ending.as_ref(), is_last,
153 ) {
154 current_node = sib;
155 found = true;
156 break;
157 }
158 sibling = find_non_anonymous_prev_sibling(sib, node_hierarchy, node_data);
159 }
160 if !found {
161 return false;
162 }
163 }
164 }
165 }
166
167 true
168}
169
170fn find_non_anonymous_parent(
172 node_id: NodeId,
173 node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
174 node_data: &NodeDataContainerRef<'_, NodeData>,
175) -> Option<NodeId> {
176 let mut next = node_hierarchy[node_id].parent_id();
177 while let Some(n) = next {
178 if !node_data[n].is_anonymous() {
179 return Some(n);
180 }
181 next = node_hierarchy[n].parent_id();
182 }
183 None
184}
185
186fn find_non_anonymous_prev_sibling(
188 node_id: NodeId,
189 node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
190 node_data: &NodeDataContainerRef<'_, NodeData>,
191) -> Option<NodeId> {
192 let mut next = node_hierarchy[node_id].previous_sibling_id();
193 while let Some(n) = next {
194 if !node_data[n].is_anonymous() {
195 return Some(n);
196 }
197 next = node_hierarchy[n].previous_sibling_id();
198 }
199 None
200}
201
202#[derive(Debug)]
215pub struct CssGroupIterator<'a> {
216 pub css_path: &'a [CssPathSelector],
217 current_idx: usize,
218 last_reason: CssGroupSplitReason,
219}
220
221#[derive(Debug, Copy, Clone, PartialEq, Eq)]
222pub enum CssGroupSplitReason {
223 Children,
225 DirectChildren,
227 AdjacentSibling,
229 GeneralSibling,
231}
232
233impl<'a> CssGroupIterator<'a> {
234 #[must_use] pub const fn new(css_path: &'a [CssPathSelector]) -> Self {
235 let initial_len = css_path.len();
236 Self {
237 css_path,
238 current_idx: initial_len,
239 last_reason: CssGroupSplitReason::Children,
240 }
241 }
242}
243
244impl<'a> Iterator for CssGroupIterator<'a> {
245 type Item = (CssContentGroup<'a>, CssGroupSplitReason);
246
247 fn next(&mut self) -> Option<(CssContentGroup<'a>, CssGroupSplitReason)> {
248 use self::CssPathSelector::{Children, DirectChildren, AdjacentSibling, GeneralSibling};
249
250 let mut new_idx = self.current_idx;
251
252 if new_idx == 0 {
253 return None;
254 }
255
256 let mut current_path = Vec::new();
257
258 while new_idx != 0 {
259 match self.css_path.get(new_idx - 1)? {
260 Children => {
261 self.last_reason = CssGroupSplitReason::Children;
262 break;
263 }
264 DirectChildren => {
265 self.last_reason = CssGroupSplitReason::DirectChildren;
266 break;
267 }
268 AdjacentSibling => {
269 self.last_reason = CssGroupSplitReason::AdjacentSibling;
270 break;
271 }
272 GeneralSibling => {
273 self.last_reason = CssGroupSplitReason::GeneralSibling;
274 break;
275 }
276 other => current_path.push(other),
277 }
278 new_idx -= 1;
279 }
280
281 #[cfg(test)]
284 current_path.reverse();
285
286 if new_idx == 0 {
287 if current_path.is_empty() {
288 None
289 } else {
290 self.current_idx = 0;
292 Some((current_path, self.last_reason))
293 }
294 } else {
295 self.current_idx = new_idx - 1;
297 Some((current_path, self.last_reason))
298 }
299 }
300}
301
302#[must_use] pub fn construct_html_cascade_tree(
303 node_hierarchy: &NodeHierarchyRef<'_>,
304 node_depths_sorted: &[(usize, NodeId)],
305 node_data: &NodeDataContainerRef<'_, NodeData>,
306) -> NodeDataContainer<CascadeInfo> {
307 let mut nodes = (0..node_hierarchy.len())
308 .map(|_| CascadeInfo {
309 index_in_parent: 0,
310 is_last_child: false,
311 })
312 .collect::<Vec<_>>();
313
314 for (_depth, parent_id) in node_depths_sorted {
315 let element_index_in_parent = parent_id
321 .preceding_siblings(node_hierarchy)
322 .filter(|sib_id| !node_data[*sib_id].is_text_node())
323 .count();
324
325 let parent_html_matcher = CascadeInfo {
326 index_in_parent: u32::try_from(element_index_in_parent.saturating_sub(1))
327 .unwrap_or(u32::MAX),
328 is_last_child: {
330 let mut is_last_element = true;
331 let mut next = node_hierarchy[*parent_id].next_sibling;
332 while let Some(sib_id) = next {
333 if !node_data[sib_id].is_text_node() {
334 is_last_element = false;
335 break;
336 }
337 next = node_hierarchy[sib_id].next_sibling;
338 }
339 is_last_element
340 },
341 };
342
343 nodes[parent_id.index()] = parent_html_matcher;
344
345 let mut element_idx: u32 = 0;
347 for child_id in parent_id.children(node_hierarchy) {
348 let is_text = node_data[child_id].is_text_node();
349
350 let is_last_element_child = if is_text {
352 false
353 } else {
354 let mut is_last = true;
355 let mut next = node_hierarchy[child_id].next_sibling;
356 while let Some(sib_id) = next {
357 if !node_data[sib_id].is_text_node() {
358 is_last = false;
359 break;
360 }
361 next = node_hierarchy[sib_id].next_sibling;
362 }
363 is_last
364 };
365
366 let child_html_matcher = CascadeInfo {
367 index_in_parent: element_idx,
368 is_last_child: is_last_element_child,
369 };
370
371 nodes[child_id.index()] = child_html_matcher;
372
373 if !is_text {
374 element_idx += 1;
375 }
376 }
377 }
378
379 NodeDataContainer { internal: nodes }
380}
381
382#[inline]
388#[must_use] pub fn rule_ends_with(path: &CssPath, target: Option<CssPathPseudoSelector>) -> bool {
389 const fn is_interactive_pseudo(p: &CssPathPseudoSelector) -> bool {
392 matches!(
393 p,
394 CssPathPseudoSelector::Hover
395 | CssPathPseudoSelector::Active
396 | CssPathPseudoSelector::Focus
397 | CssPathPseudoSelector::Backdrop
398 | CssPathPseudoSelector::Dragging
399 | CssPathPseudoSelector::DragOver
400 )
401 }
402
403 let Some(last) = path.selectors.as_ref().last() else {
404 return false;
405 };
406 target.map_or_else(
407 || match last {
408 CssPathSelector::PseudoSelector(p) => !is_interactive_pseudo(p),
411 _ => true,
412 },
413 |s| matches!(last, CssPathSelector::PseudoSelector(q) if *q == s),
414 )
415}
416
417fn selector_group_matches(
422 selectors: &[&CssPathSelector],
423 html_node: CascadeInfo,
424 node_data: &NodeData,
425 node_id: NodeId,
426 expected_path_ending: Option<&CssPathPseudoSelector>,
427 is_last_content_group: bool,
428) -> bool {
429 selectors.iter().all(|selector| {
430 match_single_selector(
431 selector,
432 html_node,
433 node_data,
434 node_id,
435 expected_path_ending,
436 is_last_content_group,
437 )
438 })
439}
440
441fn match_single_selector(
443 selector: &CssPathSelector,
444 html_node: CascadeInfo,
445 node_data: &NodeData,
446 node_id: NodeId,
447 expected_path_ending: Option<&CssPathPseudoSelector>,
448 is_last_content_group: bool,
449) -> bool {
450 use self::CssPathSelector::{Global, Root, Type, Class, Id, PseudoSelector, Attribute, DirectChildren, Children, AdjacentSibling, GeneralSibling};
451
452 match selector {
453 Global => true,
454 Root(range) => range.contains(node_id.index()),
466 Type(t) => node_data.get_node_type().get_path() == *t,
467 Class(c) => node_data.has_class(c.as_str()),
468 Id(id) => node_data.has_id(id.as_str()),
469 PseudoSelector(p) => {
470 match_pseudo_selector(p, html_node, expected_path_ending, is_last_content_group)
471 }
472 Attribute(a) => match_attribute_selector(a, node_data),
473 DirectChildren | Children | AdjacentSibling | GeneralSibling => false,
474 }
475}
476
477fn match_attribute_selector(sel: &CssAttributeSelector, node_data: &NodeData) -> bool {
484 let name = sel.name.as_str();
485 let target = sel.value.as_ref().map(azul_css::AzString::as_str);
486
487 let check = |actual: &str| -> bool {
488 match (&sel.op, target) {
489 (AttributeMatchOp::Exists, _) => true,
490 (AttributeMatchOp::Eq, Some(t)) => actual == t,
491 (AttributeMatchOp::Includes, Some(t)) => {
492 if t.is_empty() || t.contains(char::is_whitespace) {
493 return false;
494 }
495 actual.split_whitespace().any(|word| word == t)
496 }
497 (AttributeMatchOp::DashMatch, Some(t)) => {
498 actual == t || actual.starts_with(&alloc::format!("{t}-"))
499 }
500 (AttributeMatchOp::Prefix, Some(t)) => !t.is_empty() && actual.starts_with(t),
501 (AttributeMatchOp::Suffix, Some(t)) => !t.is_empty() && actual.ends_with(t),
502 (AttributeMatchOp::Substring, Some(t)) => !t.is_empty() && actual.contains(t),
503 (_, None) => false,
505 }
506 };
507
508 for attr in node_data.attributes() {
509 if attr.name() != name {
510 continue;
511 }
512 if check(attr.value().as_str()) {
513 return true;
514 }
515 }
516
517 false
518}
519
520fn match_pseudo_selector(
522 pseudo: &CssPathPseudoSelector,
523 html_node: CascadeInfo,
524 expected_path_ending: Option<&CssPathPseudoSelector>,
525 is_last_content_group: bool,
526) -> bool {
527 match pseudo {
528 CssPathPseudoSelector::First => match_first_child(html_node),
529 CssPathPseudoSelector::Last => match_last_child(html_node),
530 CssPathPseudoSelector::NthChild(pattern) => match_nth_child(html_node, pattern),
531 CssPathPseudoSelector::Hover => match_interactive_pseudo(
532 &CssPathPseudoSelector::Hover,
533 expected_path_ending,
534 is_last_content_group,
535 ),
536 CssPathPseudoSelector::Active => match_interactive_pseudo(
537 &CssPathPseudoSelector::Active,
538 expected_path_ending,
539 is_last_content_group,
540 ),
541 CssPathPseudoSelector::Focus => match_interactive_pseudo(
542 &CssPathPseudoSelector::Focus,
543 expected_path_ending,
544 is_last_content_group,
545 ),
546 CssPathPseudoSelector::Backdrop => match_interactive_pseudo(
547 &CssPathPseudoSelector::Backdrop,
548 expected_path_ending,
549 is_last_content_group,
550 ),
551 CssPathPseudoSelector::Dragging => match_interactive_pseudo(
552 &CssPathPseudoSelector::Dragging,
553 expected_path_ending,
554 is_last_content_group,
555 ),
556 CssPathPseudoSelector::DragOver => match_interactive_pseudo(
557 &CssPathPseudoSelector::DragOver,
558 expected_path_ending,
559 is_last_content_group,
560 ),
561 CssPathPseudoSelector::Lang(lang) => {
562 if let Some(CssPathPseudoSelector::Lang(expected_lang)) = expected_path_ending {
565 return lang == expected_lang;
566 }
567 false
569 }
570 }
571}
572
573const fn match_first_child(html_node: CascadeInfo) -> bool {
575 html_node.index_in_parent == 0
576}
577
578const fn match_last_child(html_node: CascadeInfo) -> bool {
580 html_node.is_last_child
581}
582
583fn match_nth_child(html_node: CascadeInfo, pattern: &CssNthChildSelector) -> bool {
585 use azul_css::css::CssNthChildPattern;
586
587 let index = html_node.index_in_parent + 1;
589
590 match pattern {
591 Number(n) => index == *n,
592 Even => index.is_multiple_of(2),
593 Odd => index % 2 == 1,
594 Pattern(CssNthChildPattern {
595 pattern_repeat,
596 offset,
597 }) => {
598 if *pattern_repeat == 0 {
599 index == *offset
600 } else {
601 index >= *offset && (index - offset).is_multiple_of(*pattern_repeat)
602 }
603 }
604 }
605}
606
607fn match_interactive_pseudo(
610 pseudo: &CssPathPseudoSelector,
611 expected_path_ending: Option<&CssPathPseudoSelector>,
612 is_last_content_group: bool,
613) -> bool {
614 is_last_content_group && expected_path_ending == Some(pseudo)
615}
616
617#[cfg(test)]
618#[allow(clippy::pedantic, clippy::nursery, clippy::too_many_lines)]
619mod autotest_generated {
620 use azul_css::{
621 css::{CssNthChildPattern, CssScopeRange, NodeTypeTag},
622 OptionString,
623 };
624
625 use super::*;
626 use crate::{
627 dom::{AttributeNameValue, AttributeType, NodeType},
628 id::Node,
629 };
630
631 fn node(
636 parent: Option<usize>,
637 prev: Option<usize>,
638 next: Option<usize>,
639 last_child: Option<usize>,
640 ) -> Node {
641 Node {
642 parent: parent.map(NodeId::new),
643 previous_sibling: prev.map(NodeId::new),
644 next_sibling: next.map(NodeId::new),
645 last_child: last_child.map(NodeId::new),
646 }
647 }
648
649 fn items(nodes: &[Node]) -> Vec<NodeHierarchyItem> {
650 nodes.iter().map(|n| NodeHierarchyItem::from(*n)).collect()
651 }
652
653 fn div_with(id: Option<&str>, class: Option<&str>) -> NodeData {
654 let mut nd = NodeData::create_div();
655 if let Some(i) = id {
656 nd.add_id(i.into());
657 }
658 if let Some(c) = class {
659 nd.add_class(c.into());
660 }
661 nd
662 }
663
664 fn node_with_attrs(attrs: Vec<AttributeType>) -> NodeData {
665 let mut nd = NodeData::create_div();
666 nd.set_attributes(attrs.into());
667 nd
668 }
669
670 fn custom(name: &str, value: &str) -> AttributeType {
671 AttributeType::Custom(AttributeNameValue {
672 attr_name: name.into(),
673 value: value.into(),
674 })
675 }
676
677 fn attr_sel(name: &str, op: AttributeMatchOp, value: Option<&str>) -> CssAttributeSelector {
678 CssAttributeSelector {
679 name: name.into(),
680 op,
681 value: value.map_or(OptionString::None, |v| OptionString::Some(v.into())),
682 }
683 }
684
685 fn info(index_in_parent: u32, is_last_child: bool) -> CascadeInfo {
686 CascadeInfo {
687 index_in_parent,
688 is_last_child,
689 }
690 }
691
692 fn sample_hierarchy() -> Vec<Node> {
705 vec![
706 node(None, None, None, Some(5)),
707 node(Some(0), None, Some(2), None),
708 node(Some(0), Some(1), Some(3), None),
709 node(Some(0), Some(2), Some(5), Some(4)),
710 node(Some(3), None, None, None),
711 node(Some(0), Some(3), None, None),
712 ]
713 }
714
715 fn sample_node_data() -> Vec<NodeData> {
716 let mut p = NodeData::create_node(NodeType::P);
717 p.add_class("inner".into());
718 vec![
719 NodeData::create_body(),
720 div_with(Some("first"), Some("a")),
721 NodeData::create_text("hello"),
722 div_with(None, Some("b")),
723 p,
724 div_with(None, Some("c")),
725 ]
726 }
727
728 fn matches(
730 selectors: Vec<CssPathSelector>,
731 node_index: usize,
732 expected_path_ending: Option<CssPathPseudoSelector>,
733 ) -> bool {
734 let hierarchy = sample_hierarchy();
735 let hier_items = items(&hierarchy);
736 let data = sample_node_data();
737
738 let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
739 let data_ref = NodeDataContainerRef::from_slice(&data);
740 let depths = hierarchy_ref.get_parents_sorted_by_depth();
741 let cascade = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
742
743 matches_html_element(
744 &CssPath::new(selectors),
745 NodeId::new(node_index),
746 &NodeDataContainerRef::from_slice(&hier_items),
747 &data_ref,
748 &cascade.as_ref(),
749 expected_path_ending,
750 )
751 }
752
753 #[test]
758 fn cascade_info_vec_as_container_preserves_every_entry() {
759 let v: CascadeInfoVec = vec![info(0, false), info(1, false), info(2, true)].into();
760 let c = v.as_container();
761
762 assert_eq!(c.len(), 3);
763 assert!(!c.is_empty());
764 assert_eq!(c[NodeId::new(0)], info(0, false));
765 assert_eq!(c[NodeId::new(2)], info(2, true));
766 assert_eq!(c.get(NodeId::new(3)), None);
768 assert_eq!(c.get(NodeId::new(usize::MAX)), None);
769 }
770
771 #[test]
772 fn cascade_info_vec_as_container_on_empty_vec_does_not_panic() {
773 let v: CascadeInfoVec = Vec::new().into();
774 let c = v.as_container();
775 assert_eq!(c.len(), 0);
776 assert!(c.is_empty());
777 assert_eq!(c.get(NodeId::new(0)), None);
778 }
779
780 #[test]
781 fn cascade_info_vec_as_container_survives_extreme_field_values() {
782 let v: CascadeInfoVec = vec![info(u32::MAX, true)].into();
783 let c = v.as_container();
784 assert_eq!(c[NodeId::new(0)].index_in_parent, u32::MAX);
785 assert!(c[NodeId::new(0)].is_last_child);
786 }
787
788 #[test]
793 fn css_group_iterator_new_records_the_path_and_yields_nothing_when_empty() {
794 let empty: [CssPathSelector; 0] = [];
795 let it = CssGroupIterator::new(&empty);
796 assert!(it.css_path.is_empty());
797 assert_eq!(CssGroupIterator::new(&empty).count(), 0);
798 }
799
800 #[test]
801 fn css_group_iterator_new_keeps_the_slice_it_was_given() {
802 let path = vec![
803 CssPathSelector::Global,
804 CssPathSelector::Children,
805 CssPathSelector::Class("x".into()),
806 ];
807 let it = CssGroupIterator::new(&path);
808 assert_eq!(it.css_path.len(), 3);
809 assert_eq!(it.css_path, path.as_slice());
810 }
811
812 #[test]
813 fn css_group_iterator_splits_right_to_left_with_the_left_hand_combinator() {
814 let path = vec![
816 CssPathSelector::Type(NodeTypeTag::Body),
817 CssPathSelector::DirectChildren,
818 CssPathSelector::Class("foo".into()),
819 CssPathSelector::Class("main".into()),
820 CssPathSelector::Children,
821 CssPathSelector::Class("baz".into()),
822 ];
823
824 let groups: Vec<_> = CssGroupIterator::new(&path).collect();
825 assert_eq!(groups.len(), 3);
826
827 assert_eq!(groups[0].0, vec![&path[5]]);
829 assert_eq!(groups[0].1, CssGroupSplitReason::Children);
830
831 assert_eq!(groups[1].0, vec![&path[2], &path[3]]);
832 assert_eq!(groups[1].1, CssGroupSplitReason::DirectChildren);
833
834 assert_eq!(groups[2].0, vec![&path[0]]);
835 }
840
841 #[test]
842 fn css_group_iterator_yields_an_empty_group_for_a_trailing_combinator() {
843 let path = vec![
845 CssPathSelector::Class("foo".into()),
846 CssPathSelector::DirectChildren,
847 ];
848 let groups: Vec<_> = CssGroupIterator::new(&path).collect();
849
850 assert_eq!(groups.len(), 2);
851 assert!(
852 groups[0].0.is_empty(),
853 "the group to the right of the dangling combinator is empty"
854 );
855 assert_eq!(groups[0].1, CssGroupSplitReason::DirectChildren);
856 assert_eq!(groups[1].0, vec![&path[0]]);
857 }
858
859 #[test]
860 fn css_group_iterator_terminates_on_consecutive_combinators() {
861 let path = vec![
864 CssPathSelector::Class("a".into()),
865 CssPathSelector::Children,
866 CssPathSelector::DirectChildren,
867 CssPathSelector::Class("b".into()),
868 ];
869 let groups: Vec<_> = CssGroupIterator::new(&path).collect();
870
871 assert_eq!(groups.len(), 3);
872 assert_eq!(groups[0].0, vec![&path[3]]);
873 assert!(groups[1].0.is_empty(), "the group between the two combinators is empty");
874 assert_eq!(groups[2].0, vec![&path[0]]);
875 }
876
877 #[test]
878 fn css_group_iterator_only_combinators_yields_only_empty_groups() {
879 let path = vec![
880 CssPathSelector::Children,
881 CssPathSelector::DirectChildren,
882 CssPathSelector::AdjacentSibling,
883 CssPathSelector::GeneralSibling,
884 ];
885 let groups: Vec<_> = CssGroupIterator::new(&path).collect();
886
887 assert_eq!(groups.len(), 4);
890 assert!(groups.iter().all(|(g, _)| g.is_empty()));
891 }
892
893 #[test]
894 fn css_group_iterator_conserves_every_non_combinator_selector_on_a_huge_path() {
895 let mut path = Vec::new();
898 for i in 0..5_000u32 {
899 if i != 0 {
900 path.push(CssPathSelector::Children);
901 }
902 path.push(CssPathSelector::Class(alloc::format!(".c{i}").into()));
903 }
904
905 let groups: Vec<_> = CssGroupIterator::new(&path).collect();
906 assert_eq!(groups.len(), 5_000);
907 let total: usize = groups.iter().map(|(g, _)| g.len()).sum();
908 assert_eq!(
909 total, 5_000,
910 "every non-combinator selector must appear in exactly one group"
911 );
912 assert!(groups.iter().all(|(_, r)| *r == CssGroupSplitReason::Children));
913 }
914
915 #[test]
920 fn rule_ends_with_empty_path_is_always_false() {
921 let empty = CssPath::new(Vec::new());
922 assert!(!rule_ends_with(&empty, None));
923 assert!(!rule_ends_with(&empty, Some(CssPathPseudoSelector::Hover)));
924 assert!(!rule_ends_with(
925 &empty,
926 Some(CssPathPseudoSelector::NthChild(CssNthChildSelector::Even))
927 ));
928 }
929
930 #[test]
931 fn rule_ends_with_none_rejects_interactive_pseudos_but_keeps_structural_ones() {
932 let ends_with = |s: CssPathSelector| {
933 rule_ends_with(&CssPath::new(vec![CssPathSelector::Class("a".into()), s]), None)
934 };
935
936 for p in [
938 CssPathPseudoSelector::Hover,
939 CssPathPseudoSelector::Active,
940 CssPathPseudoSelector::Focus,
941 CssPathPseudoSelector::Backdrop,
942 CssPathPseudoSelector::Dragging,
943 CssPathPseudoSelector::DragOver,
944 ] {
945 assert!(
946 !ends_with(CssPathSelector::PseudoSelector(p.clone())),
947 "interactive pseudo {p:?} must not end a `None` rule"
948 );
949 }
950
951 assert!(ends_with(CssPathSelector::PseudoSelector(
953 CssPathPseudoSelector::First
954 )));
955 assert!(ends_with(CssPathSelector::PseudoSelector(
956 CssPathPseudoSelector::Last
957 )));
958 assert!(ends_with(CssPathSelector::PseudoSelector(
959 CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)
960 )));
961 assert!(ends_with(CssPathSelector::Global));
963 assert!(ends_with(CssPathSelector::Class("b".into())));
964 assert!(ends_with(CssPathSelector::Type(NodeTypeTag::Div)));
965 assert!(ends_with(CssPathSelector::DirectChildren));
966 }
967
968 #[test]
969 fn rule_ends_with_some_target_requires_an_exact_match_on_the_last_selector() {
970 let hover = CssPath::new(vec![
971 CssPathSelector::Class("a".into()),
972 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
973 ]);
974 assert!(rule_ends_with(&hover, Some(CssPathPseudoSelector::Hover)));
975 assert!(!rule_ends_with(&hover, Some(CssPathPseudoSelector::Active)));
976 assert!(!rule_ends_with(&hover, Some(CssPathPseudoSelector::Focus)));
977
978 let plain = CssPath::new(vec![CssPathSelector::Class("a".into())]);
980 assert!(!rule_ends_with(&plain, Some(CssPathPseudoSelector::Hover)));
981
982 let compound = CssPath::new(vec![
985 CssPathSelector::Class("a".into()),
986 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
987 CssPathSelector::PseudoSelector(CssPathPseudoSelector::First),
988 ]);
989 assert!(!rule_ends_with(&compound, Some(CssPathPseudoSelector::Hover)));
990 assert!(rule_ends_with(&compound, Some(CssPathPseudoSelector::First)));
991 }
992
993 #[test]
994 fn rule_ends_with_compares_nth_child_and_lang_payloads() {
995 let nth = |s| {
996 CssPath::new(vec![CssPathSelector::PseudoSelector(
997 CssPathPseudoSelector::NthChild(s),
998 )])
999 };
1000 assert!(rule_ends_with(
1001 &nth(CssNthChildSelector::Number(3)),
1002 Some(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(3)))
1003 ));
1004 assert!(!rule_ends_with(
1005 &nth(CssNthChildSelector::Number(3)),
1006 Some(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(4)))
1007 ));
1008 assert!(!rule_ends_with(
1009 &nth(CssNthChildSelector::Even),
1010 Some(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd))
1011 ));
1012
1013 let lang = |s: &str| {
1015 CssPath::new(vec![CssPathSelector::PseudoSelector(
1016 CssPathPseudoSelector::Lang(s.into()),
1017 )])
1018 };
1019 assert!(rule_ends_with(
1020 &lang("zh-Hant-🎉"),
1021 Some(CssPathPseudoSelector::Lang("zh-Hant-🎉".into()))
1022 ));
1023 assert!(!rule_ends_with(
1024 &lang("zh-Hant-🎉"),
1025 Some(CssPathPseudoSelector::Lang("zh".into()))
1026 ));
1027 assert!(!rule_ends_with(&lang(""), Some(CssPathPseudoSelector::Lang("de".into()))));
1028 }
1029
1030 #[test]
1035 fn match_first_and_last_child_read_only_their_own_field() {
1036 assert!(match_first_child(info(0, false)));
1037 assert!(!match_first_child(info(1, true)));
1038 assert!(!match_first_child(info(u32::MAX, true)));
1039
1040 assert!(match_last_child(info(0, true)));
1041 assert!(!match_last_child(info(0, false)));
1042 assert!(match_last_child(info(u32::MAX, true)));
1043 }
1044
1045 #[test]
1050 fn match_nth_child_is_one_indexed() {
1051 assert!(match_nth_child(info(0, false), &CssNthChildSelector::Number(1)));
1053 assert!(!match_nth_child(info(0, false), &CssNthChildSelector::Number(0)));
1054 assert!(match_nth_child(info(0, false), &CssNthChildSelector::Odd));
1055 assert!(!match_nth_child(info(0, false), &CssNthChildSelector::Even));
1056
1057 assert!(match_nth_child(info(1, false), &CssNthChildSelector::Number(2)));
1058 assert!(match_nth_child(info(1, false), &CssNthChildSelector::Even));
1059 assert!(!match_nth_child(info(1, false), &CssNthChildSelector::Odd));
1060 }
1061
1062 #[test]
1063 fn match_nth_child_number_matches_exactly_one_index() {
1064 for n in 1..64u32 {
1065 for idx in 0..64u32 {
1066 let matched = match_nth_child(info(idx, false), &CssNthChildSelector::Number(n));
1067 assert_eq!(matched, idx + 1 == n, "nth-child({n}) vs index {idx}");
1068 }
1069 }
1070 for idx in 0..64u32 {
1072 assert!(!match_nth_child(info(idx, false), &CssNthChildSelector::Number(0)));
1073 }
1074 }
1075
1076 #[test]
1077 fn match_nth_child_even_and_odd_partition_every_index() {
1078 for idx in 0..1_000u32 {
1079 let even = match_nth_child(info(idx, false), &CssNthChildSelector::Even);
1080 let odd = match_nth_child(info(idx, false), &CssNthChildSelector::Odd);
1081 assert_ne!(even, odd, "index {idx} must be exactly one of even/odd");
1082 }
1083 }
1084
1085 #[test]
1086 fn match_nth_child_pattern_agrees_with_the_even_and_odd_shorthands() {
1087 for idx in 0..256u32 {
1089 let even_pat = CssNthChildSelector::Pattern(CssNthChildPattern {
1090 pattern_repeat: 2,
1091 offset: 0,
1092 });
1093 let odd_pat = CssNthChildSelector::Pattern(CssNthChildPattern {
1094 pattern_repeat: 2,
1095 offset: 1,
1096 });
1097 assert_eq!(
1098 match_nth_child(info(idx, false), &even_pat),
1099 match_nth_child(info(idx, false), &CssNthChildSelector::Even),
1100 "2n disagrees with `even` at index {idx}"
1101 );
1102 assert_eq!(
1103 match_nth_child(info(idx, false), &odd_pat),
1104 match_nth_child(info(idx, false), &CssNthChildSelector::Odd),
1105 "2n+1 disagrees with `odd` at index {idx}"
1106 );
1107 }
1108 }
1109
1110 #[test]
1111 fn match_nth_child_zero_repeat_never_divides_by_zero() {
1112 let only_third = CssNthChildSelector::Pattern(CssNthChildPattern {
1114 pattern_repeat: 0,
1115 offset: 3,
1116 });
1117 let never = CssNthChildSelector::Pattern(CssNthChildPattern {
1118 pattern_repeat: 0,
1119 offset: 0,
1120 });
1121
1122 for idx in 0..32u32 {
1123 assert_eq!(match_nth_child(info(idx, false), &only_third), idx == 2);
1124 assert!(
1125 !match_nth_child(info(idx, false), &never),
1126 "0n+0 must never match (index is 1-based)"
1127 );
1128 }
1129 }
1130
1131 #[test]
1132 fn match_nth_child_pattern_below_the_offset_does_not_underflow() {
1133 let pat = CssNthChildSelector::Pattern(CssNthChildPattern {
1136 pattern_repeat: 2,
1137 offset: 5,
1138 });
1139 for idx in 0..4u32 {
1140 assert!(!match_nth_child(info(idx, false), &pat), "index {idx} < offset");
1141 }
1142 assert!(match_nth_child(info(4, false), &pat)); assert!(!match_nth_child(info(5, false), &pat)); assert!(match_nth_child(info(6, false), &pat)); }
1146
1147 #[test]
1148 fn match_nth_child_with_a_u32_max_offset_does_not_underflow() {
1149 let pat = CssNthChildSelector::Pattern(CssNthChildPattern {
1150 pattern_repeat: 1,
1151 offset: u32::MAX,
1152 });
1153 assert!(!match_nth_child(info(0, false), &pat));
1154 assert!(!match_nth_child(info(1_000, false), &pat));
1155 assert!(match_nth_child(info(u32::MAX - 1, false), &pat));
1158 }
1159
1160 #[cfg(feature = "std")]
1167 #[test]
1168 fn match_nth_child_at_u32_max_index_never_answers_wrongly() {
1169 let even = std::panic::catch_unwind(|| {
1171 match_nth_child(info(u32::MAX, false), &CssNthChildSelector::Even)
1172 });
1173 let odd = std::panic::catch_unwind(|| {
1174 match_nth_child(info(u32::MAX, false), &CssNthChildSelector::Odd)
1175 });
1176 let one = std::panic::catch_unwind(|| {
1177 match_nth_child(info(u32::MAX, false), &CssNthChildSelector::Number(1))
1178 });
1179
1180 if let Ok(v) = even {
1183 assert!(v, "2^32 is even");
1184 }
1185 if let Ok(v) = odd {
1186 assert!(!v, "2^32 is not odd");
1187 }
1188 if let Ok(v) = one {
1189 assert!(!v, "the 2^32-th child is not the 1st child");
1190 }
1191 }
1192
1193 #[test]
1198 fn match_interactive_pseudo_needs_both_the_last_group_and_the_expected_ending() {
1199 let hover = CssPathPseudoSelector::Hover;
1200 let active = CssPathPseudoSelector::Active;
1201
1202 assert!(match_interactive_pseudo(&hover, Some(&hover), true));
1203 assert!(!match_interactive_pseudo(&hover, Some(&hover), false));
1204 assert!(!match_interactive_pseudo(&hover, Some(&active), true));
1205 assert!(!match_interactive_pseudo(&hover, None, true));
1206 assert!(!match_interactive_pseudo(&hover, None, false));
1207 }
1208
1209 #[test]
1210 fn match_pseudo_selector_routes_every_interactive_pseudo_through_the_gate() {
1211 for p in [
1212 CssPathPseudoSelector::Hover,
1213 CssPathPseudoSelector::Active,
1214 CssPathPseudoSelector::Focus,
1215 CssPathPseudoSelector::Backdrop,
1216 CssPathPseudoSelector::Dragging,
1217 CssPathPseudoSelector::DragOver,
1218 ] {
1219 assert!(
1220 match_pseudo_selector(&p, info(0, false), Some(&p), true),
1221 "{p:?} must match when it is the expected ending of the last group"
1222 );
1223 assert!(
1224 !match_pseudo_selector(&p, info(0, false), Some(&p), false),
1225 "{p:?} must not match outside the last content group"
1226 );
1227 assert!(
1228 !match_pseudo_selector(&p, info(0, false), None, true),
1229 "{p:?} must not match when no pseudo state is expected"
1230 );
1231 }
1232 }
1233
1234 #[test]
1235 fn match_pseudo_selector_structural_pseudos_ignore_the_expected_ending() {
1236 let hover = CssPathPseudoSelector::Hover;
1237
1238 for (expected, is_last) in [(None, true), (Some(&hover), false), (Some(&hover), true)] {
1240 assert!(match_pseudo_selector(
1241 &CssPathPseudoSelector::First,
1242 info(0, false),
1243 expected,
1244 is_last
1245 ));
1246 assert!(!match_pseudo_selector(
1247 &CssPathPseudoSelector::First,
1248 info(1, false),
1249 expected,
1250 is_last
1251 ));
1252 assert!(match_pseudo_selector(
1253 &CssPathPseudoSelector::Last,
1254 info(9, true),
1255 expected,
1256 is_last
1257 ));
1258 assert!(match_pseudo_selector(
1259 &CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(10)),
1260 info(9, true),
1261 expected,
1262 is_last
1263 ));
1264 }
1265 }
1266
1267 #[test]
1268 fn match_pseudo_selector_lang_only_matches_the_expected_lang() {
1269 let de = CssPathPseudoSelector::Lang("de".into());
1270 let en = CssPathPseudoSelector::Lang("en".into());
1271
1272 assert!(match_pseudo_selector(&de, info(0, false), Some(&de), true));
1273 assert!(!match_pseudo_selector(&de, info(0, false), Some(&en), true));
1274 assert!(!match_pseudo_selector(&de, info(0, false), None, true));
1275 assert!(!match_pseudo_selector(
1276 &de,
1277 info(0, false),
1278 Some(&CssPathPseudoSelector::Hover),
1279 true
1280 ));
1281
1282 let emoji = CssPathPseudoSelector::Lang("de-🎉".into());
1284 assert!(match_pseudo_selector(&emoji, info(0, false), Some(&emoji), true));
1285 assert!(!match_pseudo_selector(&emoji, info(0, false), Some(&de), true));
1286 let empty = CssPathPseudoSelector::Lang("".into());
1287 assert!(match_pseudo_selector(&empty, info(0, false), Some(&empty), true));
1288 }
1289
1290 #[test]
1295 fn match_attribute_selector_on_a_node_without_attributes_is_always_false() {
1296 let nd = NodeData::create_div();
1297 for op in [
1298 AttributeMatchOp::Exists,
1299 AttributeMatchOp::Eq,
1300 AttributeMatchOp::Includes,
1301 AttributeMatchOp::DashMatch,
1302 AttributeMatchOp::Prefix,
1303 AttributeMatchOp::Suffix,
1304 AttributeMatchOp::Substring,
1305 ] {
1306 assert!(!match_attribute_selector(&attr_sel("data-x", op, Some("v")), &nd));
1307 assert!(!match_attribute_selector(&attr_sel("", op, None), &nd));
1308 }
1309 }
1310
1311 #[test]
1312 fn match_attribute_selector_exists_matches_any_value_including_the_empty_one() {
1313 let nd = node_with_attrs(vec![custom("data-x", "")]);
1314 assert!(match_attribute_selector(
1315 &attr_sel("data-x", AttributeMatchOp::Exists, None),
1316 &nd
1317 ));
1318 assert!(match_attribute_selector(
1320 &attr_sel("data-x", AttributeMatchOp::Exists, Some("nonsense")),
1321 &nd
1322 ));
1323 assert!(!match_attribute_selector(
1324 &attr_sel("data-y", AttributeMatchOp::Exists, None),
1325 &nd
1326 ));
1327 }
1328
1329 #[test]
1330 fn match_attribute_selector_operator_without_a_value_never_matches() {
1331 let nd = node_with_attrs(vec![custom("data-x", "value")]);
1332 for op in [
1333 AttributeMatchOp::Eq,
1334 AttributeMatchOp::Includes,
1335 AttributeMatchOp::DashMatch,
1336 AttributeMatchOp::Prefix,
1337 AttributeMatchOp::Suffix,
1338 AttributeMatchOp::Substring,
1339 ] {
1340 assert!(
1341 !match_attribute_selector(&attr_sel("data-x", op, None), &nd),
1342 "{op:?} with a missing target value must be rejected, not matched"
1343 );
1344 }
1345 }
1346
1347 #[test]
1348 fn match_attribute_selector_empty_target_never_matches_the_substring_family() {
1349 let nd = node_with_attrs(vec![custom("data-x", "abc")]);
1352 for op in [
1353 AttributeMatchOp::Prefix,
1354 AttributeMatchOp::Suffix,
1355 AttributeMatchOp::Substring,
1356 AttributeMatchOp::Includes,
1357 ] {
1358 assert!(
1359 !match_attribute_selector(&attr_sel("data-x", op, Some("")), &nd),
1360 "{op:?} with an empty target must not match"
1361 );
1362 }
1363 assert!(!match_attribute_selector(
1365 &attr_sel("data-x", AttributeMatchOp::Eq, Some("")),
1366 &nd
1367 ));
1368 let empty = node_with_attrs(vec![custom("data-x", "")]);
1369 assert!(match_attribute_selector(
1370 &attr_sel("data-x", AttributeMatchOp::Eq, Some("")),
1371 &empty
1372 ));
1373 }
1374
1375 #[test]
1376 fn match_attribute_selector_includes_matches_one_of_several_class_entries() {
1377 let mut nd = NodeData::create_div();
1380 nd.add_class("foo".into());
1381 nd.add_class("primary".into());
1382 nd.add_class("bar".into());
1383
1384 assert!(match_attribute_selector(
1385 &attr_sel("class", AttributeMatchOp::Includes, Some("primary")),
1386 &nd
1387 ));
1388 assert!(!match_attribute_selector(
1389 &attr_sel("class", AttributeMatchOp::Includes, Some("prim")),
1390 &nd
1391 ));
1392 assert!(!match_attribute_selector(
1394 &attr_sel("class", AttributeMatchOp::Includes, Some("foo bar")),
1395 &nd
1396 ));
1397 assert!(!match_attribute_selector(
1398 &attr_sel("class", AttributeMatchOp::Includes, Some("\t")),
1399 &nd
1400 ));
1401 }
1402
1403 #[test]
1404 fn match_attribute_selector_dash_match_requires_a_dash_boundary() {
1405 let nd = node_with_attrs(vec![AttributeType::Lang("en-US".into())]);
1406
1407 assert!(match_attribute_selector(
1408 &attr_sel("lang", AttributeMatchOp::DashMatch, Some("en")),
1409 &nd
1410 ));
1411 assert!(match_attribute_selector(
1412 &attr_sel("lang", AttributeMatchOp::DashMatch, Some("en-US")),
1413 &nd
1414 ));
1415 assert!(!match_attribute_selector(
1417 &attr_sel("lang", AttributeMatchOp::DashMatch, Some("en-U")),
1418 &nd
1419 ));
1420 assert!(!match_attribute_selector(
1421 &attr_sel("lang", AttributeMatchOp::DashMatch, Some("e")),
1422 &nd
1423 ));
1424
1425 let english = node_with_attrs(vec![AttributeType::Lang("english".into())]);
1427 assert!(!match_attribute_selector(
1428 &attr_sel("lang", AttributeMatchOp::DashMatch, Some("en")),
1429 &english
1430 ));
1431 }
1432
1433 #[test]
1434 fn match_attribute_selector_name_matching_is_exact_and_case_sensitive() {
1435 let nd = node_with_attrs(vec![custom("data-foo", "v")]);
1436 assert!(match_attribute_selector(
1437 &attr_sel("data-foo", AttributeMatchOp::Eq, Some("v")),
1438 &nd
1439 ));
1440 assert!(!match_attribute_selector(
1441 &attr_sel("data-fo", AttributeMatchOp::Eq, Some("v")),
1442 &nd
1443 ));
1444 assert!(!match_attribute_selector(
1445 &attr_sel("data-foo2", AttributeMatchOp::Eq, Some("v")),
1446 &nd
1447 ));
1448 assert!(!match_attribute_selector(
1449 &attr_sel("DATA-FOO", AttributeMatchOp::Eq, Some("v")),
1450 &nd
1451 ));
1452 assert!(!match_attribute_selector(
1453 &attr_sel("", AttributeMatchOp::Exists, None),
1454 &nd
1455 ));
1456 }
1457
1458 #[test]
1459 fn match_attribute_selector_handles_unicode_values_on_char_boundaries() {
1460 let nd = node_with_attrs(vec![custom("data-x", "héllo-🎉-世界")]);
1462
1463 assert!(match_attribute_selector(
1464 &attr_sel("data-x", AttributeMatchOp::Prefix, Some("hé")),
1465 &nd
1466 ));
1467 assert!(match_attribute_selector(
1468 &attr_sel("data-x", AttributeMatchOp::Suffix, Some("世界")),
1469 &nd
1470 ));
1471 assert!(match_attribute_selector(
1472 &attr_sel("data-x", AttributeMatchOp::Substring, Some("🎉")),
1473 &nd
1474 ));
1475 assert!(match_attribute_selector(
1476 &attr_sel("data-x", AttributeMatchOp::Eq, Some("héllo-🎉-世界")),
1477 &nd
1478 ));
1479 assert!(!match_attribute_selector(
1481 &attr_sel("data-x", AttributeMatchOp::Substring, Some("e")),
1482 &nd
1483 ));
1484 assert!(match_attribute_selector(
1486 &attr_sel("data-x", AttributeMatchOp::DashMatch, Some("héllo")),
1487 &nd
1488 ));
1489 }
1490
1491 #[test]
1492 fn match_attribute_selector_handles_huge_values() {
1493 let huge = "ä".repeat(100_000);
1494 let value = alloc::format!("{huge}-tail");
1495 let nd = node_with_attrs(vec![custom("data-big", &value)]);
1496
1497 assert!(match_attribute_selector(
1498 &attr_sel("data-big", AttributeMatchOp::Suffix, Some("-tail")),
1499 &nd
1500 ));
1501 assert!(match_attribute_selector(
1502 &attr_sel("data-big", AttributeMatchOp::Prefix, Some("ää")),
1503 &nd
1504 ));
1505 assert!(match_attribute_selector(
1506 &attr_sel("data-big", AttributeMatchOp::DashMatch, Some(huge.as_str())),
1507 &nd
1508 ));
1509 assert!(!match_attribute_selector(
1510 &attr_sel("data-big", AttributeMatchOp::Eq, Some(huge.as_str())),
1511 &nd
1512 ));
1513 }
1514
1515 #[test]
1520 fn match_single_selector_global_matches_every_node_type() {
1521 let div = NodeData::create_div();
1522 let text = NodeData::create_text("hello");
1523 assert!(match_single_selector(
1524 &CssPathSelector::Global,
1525 info(0, false),
1526 &div,
1527 NodeId::new(0),
1528 None,
1529 true
1530 ));
1531 assert!(match_single_selector(
1532 &CssPathSelector::Global,
1533 info(u32::MAX, true),
1534 &text,
1535 NodeId::new(usize::MAX),
1536 None,
1537 false
1538 ));
1539 }
1540
1541 #[test]
1542 fn match_single_selector_never_matches_a_combinator() {
1543 let div = NodeData::create_div();
1546 for c in [
1547 CssPathSelector::DirectChildren,
1548 CssPathSelector::Children,
1549 CssPathSelector::AdjacentSibling,
1550 CssPathSelector::GeneralSibling,
1551 ] {
1552 assert!(!match_single_selector(
1553 &c,
1554 info(0, true),
1555 &div,
1556 NodeId::new(0),
1557 None,
1558 true
1559 ));
1560 }
1561 }
1562
1563 #[test]
1564 fn match_single_selector_root_scope_range_is_inclusive_on_both_ends() {
1565 let div = NodeData::create_div();
1566 let sel = CssPathSelector::Root(CssScopeRange { start: 2, end: 4 });
1567
1568 for id in [2usize, 3, 4] {
1569 assert!(match_single_selector(
1570 &sel,
1571 info(0, false),
1572 &div,
1573 NodeId::new(id),
1574 None,
1575 true
1576 ));
1577 }
1578 for id in [0usize, 1, 5, usize::MAX] {
1579 assert!(!match_single_selector(
1580 &sel,
1581 info(0, false),
1582 &div,
1583 NodeId::new(id),
1584 None,
1585 true
1586 ));
1587 }
1588 }
1589
1590 #[test]
1591 fn match_single_selector_root_scope_with_an_inverted_or_full_range() {
1592 let div = NodeData::create_div();
1593
1594 let inverted = CssPathSelector::Root(CssScopeRange { start: 9, end: 2 });
1596 for id in [0usize, 2, 9, usize::MAX] {
1597 assert!(!match_single_selector(
1598 &inverted,
1599 info(0, false),
1600 &div,
1601 NodeId::new(id),
1602 None,
1603 true
1604 ));
1605 }
1606
1607 let full = CssPathSelector::Root(CssScopeRange {
1609 start: 0,
1610 end: usize::MAX,
1611 });
1612 for id in [0usize, 1, usize::MAX] {
1613 assert!(match_single_selector(
1614 &full,
1615 info(0, false),
1616 &div,
1617 NodeId::new(id),
1618 None,
1619 true
1620 ));
1621 }
1622 }
1623
1624 #[test]
1625 fn match_single_selector_type_class_and_id() {
1626 let mut nd = div_with(Some("first"), Some("a"));
1627 nd.add_class("日本語-🎉".into());
1628
1629 let hit = |s: &CssPathSelector| {
1630 match_single_selector(s, info(0, false), &nd, NodeId::new(0), None, true)
1631 };
1632
1633 assert!(hit(&CssPathSelector::Type(NodeTypeTag::Div)));
1634 assert!(!hit(&CssPathSelector::Type(NodeTypeTag::P)));
1635 assert!(hit(&CssPathSelector::Class("a".into())));
1636 assert!(hit(&CssPathSelector::Class("日本語-🎉".into())));
1637 assert!(!hit(&CssPathSelector::Class("日本語".into())), "no prefix matching");
1638 assert!(!hit(&CssPathSelector::Class("".into())));
1639 assert!(hit(&CssPathSelector::Id("first".into())));
1640 assert!(!hit(&CssPathSelector::Id("firs".into())));
1641 assert!(!hit(&CssPathSelector::Class("first".into())));
1643 assert!(!hit(&CssPathSelector::Id("a".into())));
1644 }
1645
1646 #[test]
1647 fn selector_group_matches_requires_every_selector_in_the_group() {
1648 let nd = div_with(Some("first"), Some("a"));
1649 let div = CssPathSelector::Type(NodeTypeTag::Div);
1650 let class_a = CssPathSelector::Class("a".into());
1651 let class_z = CssPathSelector::Class("zzz".into());
1652
1653 let group: Vec<&CssPathSelector> = vec![&div, &class_a];
1654 assert!(selector_group_matches(
1655 &group,
1656 info(0, false),
1657 &nd,
1658 NodeId::new(0),
1659 None,
1660 true
1661 ));
1662
1663 let group: Vec<&CssPathSelector> = vec![&div, &class_a, &class_z];
1664 assert!(!selector_group_matches(
1665 &group,
1666 info(0, false),
1667 &nd,
1668 NodeId::new(0),
1669 None,
1670 true
1671 ));
1672 }
1673
1674 #[test]
1675 fn selector_group_matches_empty_group_matches_vacuously() {
1676 let empty: Vec<&CssPathSelector> = Vec::new();
1680 assert!(selector_group_matches(
1681 &empty,
1682 info(0, false),
1683 &NodeData::create_div(),
1684 NodeId::new(0),
1685 None,
1686 true
1687 ));
1688 }
1689
1690 fn anonymous_fixture() -> (Vec<Node>, Vec<NodeData>) {
1703 let hierarchy = vec![
1704 node(None, None, None, Some(5)),
1705 node(Some(0), None, Some(4), Some(2)),
1706 node(Some(1), None, None, Some(3)),
1707 node(Some(2), None, None, None),
1708 node(Some(0), Some(1), Some(5), None),
1709 node(Some(0), Some(4), None, None),
1710 ];
1711 let mut anon1 = NodeData::create_div();
1712 anon1.set_anonymous(true);
1713 let mut anon2 = NodeData::create_div();
1714 anon2.set_anonymous(true);
1715 let mut anon4 = NodeData::create_div();
1716 anon4.set_anonymous(true);
1717 let data = vec![
1718 NodeData::create_body(),
1719 anon1,
1720 anon2,
1721 div_with(None, Some("deep")),
1722 anon4,
1723 div_with(None, Some("c")),
1724 ];
1725 (hierarchy, data)
1726 }
1727
1728 #[test]
1729 fn find_non_anonymous_parent_skips_a_chain_of_anonymous_boxes() {
1730 let (hierarchy, data) = anonymous_fixture();
1731 let hier_items = items(&hierarchy);
1732 let h = NodeDataContainerRef::from_slice(&hier_items);
1733 let d = NodeDataContainerRef::from_slice(&data);
1734
1735 assert_eq!(
1737 find_non_anonymous_parent(NodeId::new(3), &h, &d),
1738 Some(NodeId::new(0))
1739 );
1740 assert_eq!(
1742 find_non_anonymous_parent(NodeId::new(5), &h, &d),
1743 Some(NodeId::new(0))
1744 );
1745 assert_eq!(find_non_anonymous_parent(NodeId::new(0), &h, &d), None);
1747 }
1748
1749 #[test]
1750 fn find_non_anonymous_parent_returns_none_when_every_ancestor_is_anonymous() {
1751 let hierarchy = vec![node(None, None, None, Some(1)), node(Some(0), None, None, None)];
1753 let mut anon_root = NodeData::create_div();
1754 anon_root.set_anonymous(true);
1755 let data = vec![anon_root, NodeData::create_div()];
1756 let hier_items = items(&hierarchy);
1757 let h = NodeDataContainerRef::from_slice(&hier_items);
1758 let d = NodeDataContainerRef::from_slice(&data);
1759
1760 assert_eq!(find_non_anonymous_parent(NodeId::new(1), &h, &d), None);
1761 }
1762
1763 #[test]
1764 fn find_non_anonymous_prev_sibling_skips_anonymous_siblings() {
1765 let (hierarchy, data) = anonymous_fixture();
1766 let hier_items = items(&hierarchy);
1767 let h = NodeDataContainerRef::from_slice(&hier_items);
1768 let d = NodeDataContainerRef::from_slice(&data);
1769
1770 assert_eq!(find_non_anonymous_prev_sibling(NodeId::new(5), &h, &d), None);
1773 assert_eq!(find_non_anonymous_prev_sibling(NodeId::new(1), &h, &d), None);
1775 assert_eq!(find_non_anonymous_prev_sibling(NodeId::new(0), &h, &d), None);
1776 }
1777
1778 #[test]
1779 fn find_non_anonymous_prev_sibling_returns_the_nearest_real_sibling() {
1780 let hierarchy = sample_hierarchy();
1781 let data = sample_node_data();
1782 let hier_items = items(&hierarchy);
1783 let h = NodeDataContainerRef::from_slice(&hier_items);
1784 let d = NodeDataContainerRef::from_slice(&data);
1785
1786 assert_eq!(
1790 find_non_anonymous_prev_sibling(NodeId::new(3), &h, &d),
1791 Some(NodeId::new(2))
1792 );
1793 assert_eq!(
1794 find_non_anonymous_prev_sibling(NodeId::new(5), &h, &d),
1795 Some(NodeId::new(3))
1796 );
1797 }
1798
1799 #[test]
1804 fn construct_html_cascade_tree_on_an_empty_hierarchy_is_empty() {
1805 let hierarchy: Vec<Node> = Vec::new();
1806 let data: Vec<NodeData> = Vec::new();
1807 let out = construct_html_cascade_tree(
1808 &NodeHierarchyRef::from_slice(&hierarchy),
1809 &[],
1810 &NodeDataContainerRef::from_slice(&data),
1811 );
1812 assert_eq!(out.len(), 0);
1813 assert!(out.is_empty());
1814 }
1815
1816 #[test]
1817 fn construct_html_cascade_tree_with_no_parents_defaults_every_node() {
1818 let hierarchy = vec![node(None, None, None, None)];
1821 let data = vec![NodeData::create_body()];
1822 let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
1823 let depths = hierarchy_ref.get_parents_sorted_by_depth();
1824 assert!(depths.is_empty());
1825
1826 let out = construct_html_cascade_tree(
1827 &hierarchy_ref,
1828 &depths,
1829 &NodeDataContainerRef::from_slice(&data),
1830 );
1831 assert_eq!(out.len(), 1);
1832 assert_eq!(out.internal[0], CascadeInfo::default());
1833 }
1834
1835 #[test]
1836 fn construct_html_cascade_tree_does_not_count_text_nodes_as_element_siblings() {
1837 let hierarchy = sample_hierarchy();
1838 let data = sample_node_data();
1839 let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
1840 let data_ref = NodeDataContainerRef::from_slice(&data);
1841 let depths = hierarchy_ref.get_parents_sorted_by_depth();
1842
1843 let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
1844
1845 assert_eq!(out.len(), hierarchy.len(), "one CascadeInfo per node");
1846 assert_eq!(out.internal[0], info(0, true), "root");
1847 assert_eq!(out.internal[1], info(0, false), "div#first.a — 1st element child");
1848 assert_eq!(out.internal[3], info(1, false), "div.b — 2nd element child (text skipped)");
1849 assert_eq!(out.internal[4], info(0, true), "p.inner — only child of div.b");
1850 assert_eq!(
1851 out.internal[5],
1852 info(2, true),
1853 "div.c — 3rd element child and the last one"
1854 );
1855 assert!(!out.internal[2].is_last_child);
1857 }
1858
1859 #[test]
1860 fn construct_html_cascade_tree_ignores_trailing_text_nodes_for_is_last_child() {
1861 let hierarchy = vec![
1863 node(None, None, None, Some(3)),
1864 node(Some(0), None, Some(2), None),
1865 node(Some(0), Some(1), Some(3), None),
1866 node(Some(0), Some(2), None, None),
1867 ];
1868 let data = vec![
1869 NodeData::create_body(),
1870 NodeData::create_div(),
1871 NodeData::create_text("a"),
1872 NodeData::create_text("b"),
1873 ];
1874 let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
1875 let data_ref = NodeDataContainerRef::from_slice(&data);
1876 let depths = hierarchy_ref.get_parents_sorted_by_depth();
1877
1878 let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
1879 assert_eq!(out.internal[1], info(0, true), "trailing text must not un-last the div");
1880 }
1881
1882 #[test]
1883 fn construct_html_cascade_tree_handles_a_wide_tree() {
1884 const CHILDREN: usize = 2_000;
1885
1886 let mut hierarchy = vec![node(None, None, None, Some(CHILDREN))];
1887 let mut data = vec![NodeData::create_body()];
1888 for i in 1..=CHILDREN {
1889 hierarchy.push(node(
1890 Some(0),
1891 if i == 1 { None } else { Some(i - 1) },
1892 if i == CHILDREN { None } else { Some(i + 1) },
1893 None,
1894 ));
1895 data.push(NodeData::create_div());
1896 }
1897
1898 let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
1899 let data_ref = NodeDataContainerRef::from_slice(&data);
1900 let depths = hierarchy_ref.get_parents_sorted_by_depth();
1901 let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
1902
1903 assert_eq!(out.len(), CHILDREN + 1);
1904 for i in 1..=CHILDREN {
1905 let expected = info(u32::try_from(i - 1).unwrap(), i == CHILDREN);
1906 assert_eq!(out.internal[i], expected, "child {i}");
1907 }
1908 }
1909
1910 #[test]
1911 fn construct_html_cascade_tree_handles_a_deep_chain() {
1912 const DEPTH: usize = 1_000;
1913
1914 let mut hierarchy = Vec::with_capacity(DEPTH);
1915 let mut data = Vec::with_capacity(DEPTH);
1916 for i in 0..DEPTH {
1917 hierarchy.push(node(
1918 if i == 0 { None } else { Some(i - 1) },
1919 None,
1920 None,
1921 if i + 1 == DEPTH { None } else { Some(i + 1) },
1922 ));
1923 data.push(NodeData::create_div());
1924 }
1925
1926 let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
1927 let data_ref = NodeDataContainerRef::from_slice(&data);
1928 let depths = hierarchy_ref.get_parents_sorted_by_depth();
1929 let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
1930
1931 assert_eq!(out.len(), DEPTH);
1932 for i in 0..DEPTH {
1933 assert_eq!(
1934 out.internal[i],
1935 info(0, true),
1936 "every node in a chain is an only child"
1937 );
1938 }
1939 }
1940
1941 #[test]
1946 fn matches_html_element_empty_path_never_matches() {
1947 assert!(!matches(Vec::new(), 1, None));
1948 assert!(!matches(Vec::new(), 0, Some(CssPathPseudoSelector::Hover)));
1949 }
1950
1951 #[test]
1952 fn matches_html_element_matches_type_class_and_id_on_the_subject() {
1953 assert!(matches(vec![CssPathSelector::Global], 1, None));
1954 assert!(matches(vec![CssPathSelector::Class("a".into())], 1, None));
1955 assert!(!matches(vec![CssPathSelector::Class("a".into())], 3, None));
1956 assert!(matches(vec![CssPathSelector::Id("first".into())], 1, None));
1957 assert!(matches(vec![CssPathSelector::Type(NodeTypeTag::Div)], 1, None));
1958 assert!(!matches(vec![CssPathSelector::Type(NodeTypeTag::Div)], 0, None));
1959 assert!(matches(vec![CssPathSelector::Type(NodeTypeTag::Body)], 0, None));
1960
1961 let div_a = vec![
1963 CssPathSelector::Type(NodeTypeTag::Div),
1964 CssPathSelector::Class("a".into()),
1965 ];
1966 assert!(matches(div_a.clone(), 1, None));
1967 assert!(!matches(div_a, 3, None));
1968 }
1969
1970 #[test]
1971 fn matches_html_element_never_matches_an_anonymous_node() {
1972 let (hierarchy, data) = anonymous_fixture();
1973 let hier_items = items(&hierarchy);
1974 let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
1975 let data_ref = NodeDataContainerRef::from_slice(&data);
1976 let depths = hierarchy_ref.get_parents_sorted_by_depth();
1977 let cascade = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
1978
1979 for id in [1usize, 2, 4] {
1981 assert!(
1982 !matches_html_element(
1983 &CssPath::new(vec![CssPathSelector::Global]),
1984 NodeId::new(id),
1985 &NodeDataContainerRef::from_slice(&hier_items),
1986 &data_ref,
1987 &cascade.as_ref(),
1988 None,
1989 ),
1990 "anonymous node {id} must not be styled"
1991 );
1992 }
1993 for id in [0usize, 3, 5] {
1994 assert!(matches_html_element(
1995 &CssPath::new(vec![CssPathSelector::Global]),
1996 NodeId::new(id),
1997 &NodeDataContainerRef::from_slice(&hier_items),
1998 &data_ref,
1999 &cascade.as_ref(),
2000 None,
2001 ));
2002 }
2003 }
2004
2005 #[test]
2006 fn matches_html_element_child_combinator_is_stricter_than_the_descendant_one() {
2007 let child = vec![
2009 CssPathSelector::Type(NodeTypeTag::Body),
2010 CssPathSelector::DirectChildren,
2011 CssPathSelector::Type(NodeTypeTag::P),
2012 ];
2013 assert!(!matches(child, 4, None));
2014
2015 let descendant = vec![
2017 CssPathSelector::Type(NodeTypeTag::Body),
2018 CssPathSelector::Children,
2019 CssPathSelector::Type(NodeTypeTag::P),
2020 ];
2021 assert!(matches(descendant, 4, None));
2022
2023 let direct = vec![
2025 CssPathSelector::Type(NodeTypeTag::Body),
2026 CssPathSelector::DirectChildren,
2027 CssPathSelector::Type(NodeTypeTag::Div),
2028 CssPathSelector::Class("b".into()),
2029 ];
2030 assert!(matches(direct, 3, None));
2031 }
2032
2033 #[test]
2034 fn matches_html_element_descendant_combinator_walks_the_whole_ancestor_chain() {
2035 let close = vec![
2037 CssPathSelector::Class("b".into()),
2038 CssPathSelector::Children,
2039 CssPathSelector::Class("inner".into()),
2040 ];
2041 assert!(matches(close, 4, None));
2042
2043 let unrelated = vec![
2045 CssPathSelector::Class("c".into()),
2046 CssPathSelector::Children,
2047 CssPathSelector::Class("inner".into()),
2048 ];
2049 assert!(!matches(unrelated, 4, None));
2050 }
2051
2052 #[test]
2053 fn matches_html_element_general_sibling_scans_all_previous_siblings() {
2054 let general = vec![
2057 CssPathSelector::Class("a".into()),
2058 CssPathSelector::GeneralSibling,
2059 CssPathSelector::Class("c".into()),
2060 ];
2061 assert!(matches(general, 5, None));
2062
2063 let backwards = vec![
2065 CssPathSelector::Class("c".into()),
2066 CssPathSelector::GeneralSibling,
2067 CssPathSelector::Class("a".into()),
2068 ];
2069 assert!(!matches(backwards, 1, None));
2070 }
2071
2072 #[test]
2073 fn matches_html_element_adjacent_sibling_matches_the_immediate_element_sibling() {
2074 let adjacent = vec![
2076 CssPathSelector::Class("b".into()),
2077 CssPathSelector::AdjacentSibling,
2078 CssPathSelector::Class("c".into()),
2079 ];
2080 assert!(matches(adjacent, 5, None));
2081
2082 let not_adjacent = vec![
2084 CssPathSelector::Class("a".into()),
2085 CssPathSelector::AdjacentSibling,
2086 CssPathSelector::Class("c".into()),
2087 ];
2088 assert!(!matches(not_adjacent, 5, None));
2089 }
2090
2091 #[test]
2098 fn matches_html_element_adjacent_sibling_skips_text_nodes() {
2099 let adjacent = vec![
2101 CssPathSelector::Class("a".into()),
2102 CssPathSelector::AdjacentSibling,
2103 CssPathSelector::Class("b".into()),
2104 ];
2105 assert!(
2106 matches(adjacent, 3, None),
2107 "the `+` combinator must ignore non-element (text) siblings"
2108 );
2109 }
2110
2111 #[test]
2112 fn matches_html_element_hover_on_a_single_group_path_needs_the_expected_ending() {
2113 let hover = vec![
2114 CssPathSelector::Class("a".into()),
2115 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
2116 ];
2117 assert!(matches(hover.clone(), 1, Some(CssPathPseudoSelector::Hover)));
2118 assert!(!matches(hover.clone(), 1, None));
2120 assert!(!matches(hover.clone(), 1, Some(CssPathPseudoSelector::Focus)));
2122 assert!(!matches(hover, 3, Some(CssPathPseudoSelector::Hover)));
2124 }
2125
2126 #[test]
2135 fn matches_html_element_hover_on_a_descendant_path_still_matches() {
2136 let hover_descendant = vec![
2138 CssPathSelector::Type(NodeTypeTag::Body),
2139 CssPathSelector::Children,
2140 CssPathSelector::Class("a".into()),
2141 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
2142 ];
2143 assert!(
2144 matches(hover_descendant, 1, Some(CssPathPseudoSelector::Hover)),
2145 ":hover on the subject of a multi-group selector must still match"
2146 );
2147 }
2148
2149 #[test]
2150 fn matches_html_element_structural_pseudos_work_on_the_subject() {
2151 let first = vec![
2153 CssPathSelector::Class("a".into()),
2154 CssPathSelector::PseudoSelector(CssPathPseudoSelector::First),
2155 ];
2156 assert!(matches(first, 1, None));
2157
2158 let last = vec![
2159 CssPathSelector::Class("c".into()),
2160 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Last),
2161 ];
2162 assert!(matches(last, 5, None));
2163
2164 let nth = vec![
2166 CssPathSelector::Class("b".into()),
2167 CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
2168 CssNthChildSelector::Number(2),
2169 )),
2170 ];
2171 assert!(matches(nth, 3, None));
2172
2173 let wrong_nth = vec![
2174 CssPathSelector::Class("b".into()),
2175 CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
2176 CssNthChildSelector::Number(3),
2177 )),
2178 ];
2179 assert!(!matches(wrong_nth, 3, None));
2180 }
2181
2182 #[test]
2183 fn matches_html_element_root_scope_confines_a_rule_to_its_subtree() {
2184 let scoped = vec![
2186 CssPathSelector::Root(CssScopeRange { start: 3, end: 4 }),
2187 CssPathSelector::Global,
2188 ];
2189 assert!(matches(scoped.clone(), 3, None));
2190 assert!(matches(scoped.clone(), 4, None));
2191 assert!(!matches(scoped.clone(), 1, None), "must not leak to a sibling");
2192 assert!(!matches(scoped.clone(), 5, None), "must not leak to a sibling");
2193 assert!(!matches(scoped, 0, None), "must not leak to the parent");
2194
2195 let node_only = vec![
2197 CssPathSelector::Root(CssScopeRange { start: 3, end: 3 }),
2198 CssPathSelector::Global,
2199 ];
2200 assert!(matches(node_only.clone(), 3, None));
2201 assert!(!matches(node_only, 4, None), "a node-only scope must not reach children");
2202 }
2203
2204 #[test]
2205 fn matches_html_element_with_a_dangling_combinator_does_not_panic() {
2206 let dangling = vec![
2210 CssPathSelector::Class("a".into()),
2211 CssPathSelector::DirectChildren,
2212 ];
2213 assert!(!matches(dangling.clone(), 4, None));
2214 for id in 0..6 {
2216 let _ = matches(dangling.clone(), id, None);
2217 }
2218 }
2219
2220 #[test]
2221 fn matches_html_element_on_a_very_long_selector_chain_terminates() {
2222 let mut path = Vec::new();
2225 for _ in 0..500 {
2226 path.push(CssPathSelector::Type(NodeTypeTag::Body));
2227 path.push(CssPathSelector::Children);
2228 }
2229 path.push(CssPathSelector::Class("inner".into()));
2230 assert!(!matches(path, 4, None));
2231 }
2232}