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 = true;
87 if !selector_group_matches(
88 first_group,
89 html_node_tree[node_id],
90 &node_data[node_id],
91 node_id,
92 expected_path_ending.as_ref(),
93 is_last_content_group,
94 ) {
95 return false;
96 }
97
98 let mut current_node = node_id;
101
102 for (group_idx, (content_group, _reason)) in groups.iter().enumerate().skip(1) {
103 let combinator = groups[group_idx - 1].1;
105 let is_last = group_idx == groups.len() - 1;
106
107 match combinator {
108 DirectChildren => {
109 let parent = find_non_anonymous_parent(current_node, node_hierarchy, node_data);
111 match parent {
112 Some(p) if selector_group_matches(
113 content_group, html_node_tree[p], &node_data[p], p,
114 expected_path_ending.as_ref(), is_last,
115 ) => { current_node = p; }
116 _ => return false,
117 }
118 }
119 Children => {
120 let mut ancestor = find_non_anonymous_parent(current_node, node_hierarchy, node_data);
122 let mut found = false;
123 while let Some(anc) = ancestor {
124 if selector_group_matches(
125 content_group, html_node_tree[anc], &node_data[anc], anc,
126 expected_path_ending.as_ref(), is_last,
127 ) {
128 current_node = anc;
129 found = true;
130 break;
131 }
132 ancestor = find_non_anonymous_parent(anc, node_hierarchy, node_data);
133 }
134 if !found {
135 return false;
136 }
137 }
138 AdjacentSibling => {
139 let sibling = find_non_anonymous_prev_sibling(current_node, node_hierarchy, node_data);
141 match sibling {
142 Some(s) if selector_group_matches(
143 content_group, html_node_tree[s], &node_data[s], s,
144 expected_path_ending.as_ref(), is_last,
145 ) => { current_node = s; }
146 _ => return false,
147 }
148 }
149 GeneralSibling => {
150 let mut sibling = find_non_anonymous_prev_sibling(current_node, node_hierarchy, node_data);
152 let mut found = false;
153 while let Some(sib) = sibling {
154 if selector_group_matches(
155 content_group, html_node_tree[sib], &node_data[sib], sib,
156 expected_path_ending.as_ref(), is_last,
157 ) {
158 current_node = sib;
159 found = true;
160 break;
161 }
162 sibling = find_non_anonymous_prev_sibling(sib, node_hierarchy, node_data);
163 }
164 if !found {
165 return false;
166 }
167 }
168 }
169 }
170
171 true
172}
173
174fn find_non_anonymous_parent(
176 node_id: NodeId,
177 node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
178 node_data: &NodeDataContainerRef<'_, NodeData>,
179) -> Option<NodeId> {
180 let mut next = node_hierarchy[node_id].parent_id();
181 while let Some(n) = next {
182 if !node_data[n].is_anonymous() {
183 return Some(n);
184 }
185 next = node_hierarchy[n].parent_id();
186 }
187 None
188}
189
190fn find_non_anonymous_prev_sibling(
197 node_id: NodeId,
198 node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
199 node_data: &NodeDataContainerRef<'_, NodeData>,
200) -> Option<NodeId> {
201 let mut next = node_hierarchy[node_id].previous_sibling_id();
202 while let Some(n) = next {
203 if !node_data[n].is_anonymous() && !node_data[n].is_text_node() {
204 return Some(n);
205 }
206 next = node_hierarchy[n].previous_sibling_id();
207 }
208 None
209}
210
211#[derive(Debug)]
224pub struct CssGroupIterator<'a> {
225 pub css_path: &'a [CssPathSelector],
226 current_idx: usize,
227 last_reason: CssGroupSplitReason,
228}
229
230#[derive(Debug, Copy, Clone, PartialEq, Eq)]
231pub enum CssGroupSplitReason {
232 Children,
234 DirectChildren,
236 AdjacentSibling,
238 GeneralSibling,
240}
241
242impl<'a> CssGroupIterator<'a> {
243 #[must_use] pub const fn new(css_path: &'a [CssPathSelector]) -> Self {
244 let initial_len = css_path.len();
245 Self {
246 css_path,
247 current_idx: initial_len,
248 last_reason: CssGroupSplitReason::Children,
249 }
250 }
251}
252
253impl<'a> Iterator for CssGroupIterator<'a> {
254 type Item = (CssContentGroup<'a>, CssGroupSplitReason);
255
256 fn next(&mut self) -> Option<(CssContentGroup<'a>, CssGroupSplitReason)> {
257 use self::CssPathSelector::{Children, DirectChildren, AdjacentSibling, GeneralSibling};
258
259 let mut new_idx = self.current_idx;
260
261 if new_idx == 0 {
262 return None;
263 }
264
265 let mut current_path = Vec::new();
266
267 while new_idx != 0 {
268 match self.css_path.get(new_idx - 1)? {
269 Children => {
270 self.last_reason = CssGroupSplitReason::Children;
271 break;
272 }
273 DirectChildren => {
274 self.last_reason = CssGroupSplitReason::DirectChildren;
275 break;
276 }
277 AdjacentSibling => {
278 self.last_reason = CssGroupSplitReason::AdjacentSibling;
279 break;
280 }
281 GeneralSibling => {
282 self.last_reason = CssGroupSplitReason::GeneralSibling;
283 break;
284 }
285 other => current_path.push(other),
286 }
287 new_idx -= 1;
288 }
289
290 #[cfg(test)]
293 current_path.reverse();
294
295 if new_idx == 0 {
296 if current_path.is_empty() {
297 None
298 } else {
299 self.current_idx = 0;
301 Some((current_path, self.last_reason))
302 }
303 } else {
304 self.current_idx = new_idx - 1;
306 Some((current_path, self.last_reason))
307 }
308 }
309}
310
311#[must_use] pub fn construct_html_cascade_tree(
312 node_hierarchy: &NodeHierarchyRef<'_>,
313 node_depths_sorted: &[(usize, NodeId)],
314 node_data: &NodeDataContainerRef<'_, NodeData>,
315) -> NodeDataContainer<CascadeInfo> {
316 let mut nodes = (0..node_hierarchy.len())
317 .map(|_| CascadeInfo {
318 index_in_parent: 0,
319 is_last_child: false,
320 })
321 .collect::<Vec<_>>();
322
323 for (_depth, parent_id) in node_depths_sorted {
324 let element_index_in_parent = parent_id
330 .preceding_siblings(node_hierarchy)
331 .filter(|sib_id| !node_data[*sib_id].is_text_node())
332 .count();
333
334 let parent_html_matcher = CascadeInfo {
335 index_in_parent: u32::try_from(element_index_in_parent.saturating_sub(1))
336 .unwrap_or(u32::MAX),
337 is_last_child: {
339 let mut is_last_element = true;
340 let mut next = node_hierarchy[*parent_id].next_sibling;
341 while let Some(sib_id) = next {
342 if !node_data[sib_id].is_text_node() {
343 is_last_element = false;
344 break;
345 }
346 next = node_hierarchy[sib_id].next_sibling;
347 }
348 is_last_element
349 },
350 };
351
352 nodes[parent_id.index()] = parent_html_matcher;
353
354 let mut element_idx: u32 = 0;
356 for child_id in parent_id.children(node_hierarchy) {
357 let is_text = node_data[child_id].is_text_node();
358
359 let is_last_element_child = if is_text {
361 false
362 } else {
363 let mut is_last = true;
364 let mut next = node_hierarchy[child_id].next_sibling;
365 while let Some(sib_id) = next {
366 if !node_data[sib_id].is_text_node() {
367 is_last = false;
368 break;
369 }
370 next = node_hierarchy[sib_id].next_sibling;
371 }
372 is_last
373 };
374
375 let child_html_matcher = CascadeInfo {
376 index_in_parent: element_idx,
377 is_last_child: is_last_element_child,
378 };
379
380 nodes[child_id.index()] = child_html_matcher;
381
382 if !is_text {
383 element_idx += 1;
384 }
385 }
386 }
387
388 NodeDataContainer { internal: nodes }
389}
390
391#[inline]
397#[must_use] pub fn rule_ends_with(path: &CssPath, target: Option<CssPathPseudoSelector>) -> bool {
398 const fn is_interactive_pseudo(p: &CssPathPseudoSelector) -> bool {
401 matches!(
402 p,
403 CssPathPseudoSelector::Hover
404 | CssPathPseudoSelector::Active
405 | CssPathPseudoSelector::Focus
406 | CssPathPseudoSelector::Backdrop
407 | CssPathPseudoSelector::Dragging
408 | CssPathPseudoSelector::DragOver
409 )
410 }
411
412 let Some(last) = path.selectors.as_ref().last() else {
413 return false;
414 };
415 target.map_or_else(
416 || match last {
417 CssPathSelector::PseudoSelector(p) => !is_interactive_pseudo(p),
420 _ => true,
421 },
422 |s| matches!(last, CssPathSelector::PseudoSelector(q) if *q == s),
423 )
424}
425
426fn selector_group_matches(
431 selectors: &[&CssPathSelector],
432 html_node: CascadeInfo,
433 node_data: &NodeData,
434 node_id: NodeId,
435 expected_path_ending: Option<&CssPathPseudoSelector>,
436 is_last_content_group: bool,
437) -> bool {
438 selectors.iter().all(|selector| {
439 match_single_selector(
440 selector,
441 html_node,
442 node_data,
443 node_id,
444 expected_path_ending,
445 is_last_content_group,
446 )
447 })
448}
449
450fn match_single_selector(
452 selector: &CssPathSelector,
453 html_node: CascadeInfo,
454 node_data: &NodeData,
455 node_id: NodeId,
456 expected_path_ending: Option<&CssPathPseudoSelector>,
457 is_last_content_group: bool,
458) -> bool {
459 use self::CssPathSelector::{Global, Root, Type, Class, Id, PseudoSelector, Attribute, DirectChildren, Children, AdjacentSibling, GeneralSibling};
460
461 match selector {
462 Global => true,
463 Root(range) => range.contains(node_id.index()),
475 Type(t) => node_data.get_node_type().get_path() == *t,
476 Class(c) => node_data.has_class(c.as_str()),
477 Id(id) => node_data.has_id(id.as_str()),
478 PseudoSelector(CssPathPseudoSelector::Root) => node_id.index() == 0,
482 PseudoSelector(p) => {
483 match_pseudo_selector(p, html_node, expected_path_ending, is_last_content_group)
484 }
485 Attribute(a) => match_attribute_selector(a, node_data),
486 DirectChildren | Children | AdjacentSibling | GeneralSibling => false,
487 }
488}
489
490fn match_attribute_selector(sel: &CssAttributeSelector, node_data: &NodeData) -> bool {
497 let name = sel.name.as_str();
498 let target = sel.value.as_ref().map(azul_css::AzString::as_str);
499
500 let check = |actual: &str| -> bool {
501 match (&sel.op, target) {
502 (AttributeMatchOp::Exists, _) => true,
503 (AttributeMatchOp::Eq, Some(t)) => actual == t,
504 (AttributeMatchOp::Includes, Some(t)) => {
505 if t.is_empty() || t.contains(char::is_whitespace) {
506 return false;
507 }
508 actual.split_whitespace().any(|word| word == t)
509 }
510 (AttributeMatchOp::DashMatch, Some(t)) => {
511 actual == t || actual.starts_with(&alloc::format!("{t}-"))
512 }
513 (AttributeMatchOp::Prefix, Some(t)) => !t.is_empty() && actual.starts_with(t),
514 (AttributeMatchOp::Suffix, Some(t)) => !t.is_empty() && actual.ends_with(t),
515 (AttributeMatchOp::Substring, Some(t)) => !t.is_empty() && actual.contains(t),
516 (_, None) => false,
518 }
519 };
520
521 for attr in node_data.attributes() {
522 if attr.name() != name {
523 continue;
524 }
525 if check(attr.value().as_str()) {
526 return true;
527 }
528 }
529
530 false
531}
532
533fn match_pseudo_selector(
535 pseudo: &CssPathPseudoSelector,
536 html_node: CascadeInfo,
537 expected_path_ending: Option<&CssPathPseudoSelector>,
538 is_last_content_group: bool,
539) -> bool {
540 match pseudo {
541 CssPathPseudoSelector::First => match_first_child(html_node),
542 CssPathPseudoSelector::Last => match_last_child(html_node),
543 CssPathPseudoSelector::NthChild(pattern) => match_nth_child(html_node, pattern),
544 CssPathPseudoSelector::Hover => match_interactive_pseudo(
545 &CssPathPseudoSelector::Hover,
546 expected_path_ending,
547 is_last_content_group,
548 ),
549 CssPathPseudoSelector::Active => match_interactive_pseudo(
550 &CssPathPseudoSelector::Active,
551 expected_path_ending,
552 is_last_content_group,
553 ),
554 CssPathPseudoSelector::Focus => match_interactive_pseudo(
555 &CssPathPseudoSelector::Focus,
556 expected_path_ending,
557 is_last_content_group,
558 ),
559 CssPathPseudoSelector::Backdrop => match_interactive_pseudo(
560 &CssPathPseudoSelector::Backdrop,
561 expected_path_ending,
562 is_last_content_group,
563 ),
564 CssPathPseudoSelector::Dragging => match_interactive_pseudo(
565 &CssPathPseudoSelector::Dragging,
566 expected_path_ending,
567 is_last_content_group,
568 ),
569 CssPathPseudoSelector::DragOver => match_interactive_pseudo(
570 &CssPathPseudoSelector::DragOver,
571 expected_path_ending,
572 is_last_content_group,
573 ),
574 CssPathPseudoSelector::Lang(lang) => {
575 if let Some(CssPathPseudoSelector::Lang(expected_lang)) = expected_path_ending {
578 return lang == expected_lang;
579 }
580 false
582 }
583 CssPathPseudoSelector::Root => false,
586 }
587}
588
589const fn match_first_child(html_node: CascadeInfo) -> bool {
591 html_node.index_in_parent == 0
592}
593
594const fn match_last_child(html_node: CascadeInfo) -> bool {
596 html_node.is_last_child
597}
598
599fn match_nth_child(html_node: CascadeInfo, pattern: &CssNthChildSelector) -> bool {
601 use azul_css::css::CssNthChildPattern;
602
603 let index = html_node.index_in_parent + 1;
605
606 match pattern {
607 Number(n) => index == *n,
608 Even => index.is_multiple_of(2),
609 Odd => index % 2 == 1,
610 Pattern(CssNthChildPattern {
611 pattern_repeat,
612 offset,
613 }) => {
614 if *pattern_repeat == 0 {
615 index == *offset
616 } else {
617 index >= *offset && (index - offset).is_multiple_of(*pattern_repeat)
618 }
619 }
620 }
621}
622
623fn match_interactive_pseudo(
626 pseudo: &CssPathPseudoSelector,
627 expected_path_ending: Option<&CssPathPseudoSelector>,
628 is_last_content_group: bool,
629) -> bool {
630 is_last_content_group && expected_path_ending == Some(pseudo)
631}
632
633#[cfg(test)]
634#[allow(clippy::pedantic, clippy::nursery, clippy::too_many_lines)]
635mod autotest_generated {
636 use azul_css::{
637 css::{CssNthChildPattern, CssScopeRange, NodeTypeTag},
638 OptionString,
639 };
640
641 use super::*;
642 use crate::{
643 dom::{AttributeNameValue, AttributeType, NodeType},
644 id::Node,
645 };
646
647 fn node(
652 parent: Option<usize>,
653 prev: Option<usize>,
654 next: Option<usize>,
655 last_child: Option<usize>,
656 ) -> Node {
657 Node {
658 parent: parent.map(NodeId::new),
659 previous_sibling: prev.map(NodeId::new),
660 next_sibling: next.map(NodeId::new),
661 last_child: last_child.map(NodeId::new),
662 }
663 }
664
665 fn items(nodes: &[Node]) -> Vec<NodeHierarchyItem> {
666 nodes.iter().map(|n| NodeHierarchyItem::from(*n)).collect()
667 }
668
669 fn div_with(id: Option<&str>, class: Option<&str>) -> NodeData {
670 let mut nd = NodeData::create_div();
671 if let Some(i) = id {
672 nd.add_id(i.into());
673 }
674 if let Some(c) = class {
675 nd.add_class(c.into());
676 }
677 nd
678 }
679
680 fn node_with_attrs(attrs: Vec<AttributeType>) -> NodeData {
681 let mut nd = NodeData::create_div();
682 nd.set_attributes(attrs.into());
683 nd
684 }
685
686 fn custom(name: &str, value: &str) -> AttributeType {
687 AttributeType::Custom(AttributeNameValue {
688 attr_name: name.into(),
689 value: value.into(),
690 })
691 }
692
693 fn attr_sel(name: &str, op: AttributeMatchOp, value: Option<&str>) -> CssAttributeSelector {
694 CssAttributeSelector {
695 name: name.into(),
696 op,
697 value: value.map_or(OptionString::None, |v| OptionString::Some(v.into())),
698 }
699 }
700
701 fn info(index_in_parent: u32, is_last_child: bool) -> CascadeInfo {
702 CascadeInfo {
703 index_in_parent,
704 is_last_child,
705 }
706 }
707
708 fn sample_hierarchy() -> Vec<Node> {
721 vec![
722 node(None, None, None, Some(5)),
723 node(Some(0), None, Some(2), None),
724 node(Some(0), Some(1), Some(3), None),
725 node(Some(0), Some(2), Some(5), Some(4)),
726 node(Some(3), None, None, None),
727 node(Some(0), Some(3), None, None),
728 ]
729 }
730
731 fn sample_node_data() -> Vec<NodeData> {
732 let mut p = NodeData::create_node(NodeType::P);
733 p.add_class("inner".into());
734 vec![
735 NodeData::create_body(),
736 div_with(Some("first"), Some("a")),
737 NodeData::create_text("hello"),
738 div_with(None, Some("b")),
739 p,
740 div_with(None, Some("c")),
741 ]
742 }
743
744 fn matches(
746 selectors: Vec<CssPathSelector>,
747 node_index: usize,
748 expected_path_ending: Option<CssPathPseudoSelector>,
749 ) -> bool {
750 let hierarchy = sample_hierarchy();
751 let hier_items = items(&hierarchy);
752 let data = sample_node_data();
753
754 let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
755 let data_ref = NodeDataContainerRef::from_slice(&data);
756 let depths = hierarchy_ref.get_parents_sorted_by_depth();
757 let cascade = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
758
759 matches_html_element(
760 &CssPath::new(selectors),
761 NodeId::new(node_index),
762 &NodeDataContainerRef::from_slice(&hier_items),
763 &data_ref,
764 &cascade.as_ref(),
765 expected_path_ending,
766 )
767 }
768
769 #[test]
774 fn cascade_info_vec_as_container_preserves_every_entry() {
775 let v: CascadeInfoVec = vec![info(0, false), info(1, false), info(2, true)].into();
776 let c = v.as_container();
777
778 assert_eq!(c.len(), 3);
779 assert!(!c.is_empty());
780 assert_eq!(c[NodeId::new(0)], info(0, false));
781 assert_eq!(c[NodeId::new(2)], info(2, true));
782 assert_eq!(c.get(NodeId::new(3)), None);
784 assert_eq!(c.get(NodeId::new(usize::MAX)), None);
785 }
786
787 #[test]
788 fn cascade_info_vec_as_container_on_empty_vec_does_not_panic() {
789 let v: CascadeInfoVec = Vec::new().into();
790 let c = v.as_container();
791 assert_eq!(c.len(), 0);
792 assert!(c.is_empty());
793 assert_eq!(c.get(NodeId::new(0)), None);
794 }
795
796 #[test]
797 fn cascade_info_vec_as_container_survives_extreme_field_values() {
798 let v: CascadeInfoVec = vec![info(u32::MAX, true)].into();
799 let c = v.as_container();
800 assert_eq!(c[NodeId::new(0)].index_in_parent, u32::MAX);
801 assert!(c[NodeId::new(0)].is_last_child);
802 }
803
804 #[test]
809 fn css_group_iterator_new_records_the_path_and_yields_nothing_when_empty() {
810 let empty: [CssPathSelector; 0] = [];
811 let it = CssGroupIterator::new(&empty);
812 assert!(it.css_path.is_empty());
813 assert_eq!(CssGroupIterator::new(&empty).count(), 0);
814 }
815
816 #[test]
817 fn css_group_iterator_new_keeps_the_slice_it_was_given() {
818 let path = vec![
819 CssPathSelector::Global,
820 CssPathSelector::Children,
821 CssPathSelector::Class("x".into()),
822 ];
823 let it = CssGroupIterator::new(&path);
824 assert_eq!(it.css_path.len(), 3);
825 assert_eq!(it.css_path, path.as_slice());
826 }
827
828 #[test]
829 fn css_group_iterator_splits_right_to_left_with_the_left_hand_combinator() {
830 let path = vec![
832 CssPathSelector::Type(NodeTypeTag::Body),
833 CssPathSelector::DirectChildren,
834 CssPathSelector::Class("foo".into()),
835 CssPathSelector::Class("main".into()),
836 CssPathSelector::Children,
837 CssPathSelector::Class("baz".into()),
838 ];
839
840 let groups: Vec<_> = CssGroupIterator::new(&path).collect();
841 assert_eq!(groups.len(), 3);
842
843 assert_eq!(groups[0].0, vec![&path[5]]);
845 assert_eq!(groups[0].1, CssGroupSplitReason::Children);
846
847 assert_eq!(groups[1].0, vec![&path[2], &path[3]]);
848 assert_eq!(groups[1].1, CssGroupSplitReason::DirectChildren);
849
850 assert_eq!(groups[2].0, vec![&path[0]]);
851 }
856
857 #[test]
858 fn css_group_iterator_yields_an_empty_group_for_a_trailing_combinator() {
859 let path = vec![
861 CssPathSelector::Class("foo".into()),
862 CssPathSelector::DirectChildren,
863 ];
864 let groups: Vec<_> = CssGroupIterator::new(&path).collect();
865
866 assert_eq!(groups.len(), 2);
867 assert!(
868 groups[0].0.is_empty(),
869 "the group to the right of the dangling combinator is empty"
870 );
871 assert_eq!(groups[0].1, CssGroupSplitReason::DirectChildren);
872 assert_eq!(groups[1].0, vec![&path[0]]);
873 }
874
875 #[test]
876 fn css_group_iterator_terminates_on_consecutive_combinators() {
877 let path = vec![
880 CssPathSelector::Class("a".into()),
881 CssPathSelector::Children,
882 CssPathSelector::DirectChildren,
883 CssPathSelector::Class("b".into()),
884 ];
885 let groups: Vec<_> = CssGroupIterator::new(&path).collect();
886
887 assert_eq!(groups.len(), 3);
888 assert_eq!(groups[0].0, vec![&path[3]]);
889 assert!(groups[1].0.is_empty(), "the group between the two combinators is empty");
890 assert_eq!(groups[2].0, vec![&path[0]]);
891 }
892
893 #[test]
894 fn css_group_iterator_only_combinators_yields_only_empty_groups() {
895 let path = vec![
896 CssPathSelector::Children,
897 CssPathSelector::DirectChildren,
898 CssPathSelector::AdjacentSibling,
899 CssPathSelector::GeneralSibling,
900 ];
901 let groups: Vec<_> = CssGroupIterator::new(&path).collect();
902
903 assert_eq!(groups.len(), 4);
906 assert!(groups.iter().all(|(g, _)| g.is_empty()));
907 }
908
909 #[test]
910 fn css_group_iterator_conserves_every_non_combinator_selector_on_a_huge_path() {
911 let mut path = Vec::new();
914 for i in 0..5_000u32 {
915 if i != 0 {
916 path.push(CssPathSelector::Children);
917 }
918 path.push(CssPathSelector::Class(alloc::format!(".c{i}").into()));
919 }
920
921 let groups: Vec<_> = CssGroupIterator::new(&path).collect();
922 assert_eq!(groups.len(), 5_000);
923 let total: usize = groups.iter().map(|(g, _)| g.len()).sum();
924 assert_eq!(
925 total, 5_000,
926 "every non-combinator selector must appear in exactly one group"
927 );
928 assert!(groups.iter().all(|(_, r)| *r == CssGroupSplitReason::Children));
929 }
930
931 #[test]
936 fn rule_ends_with_empty_path_is_always_false() {
937 let empty = CssPath::new(Vec::new());
938 assert!(!rule_ends_with(&empty, None));
939 assert!(!rule_ends_with(&empty, Some(CssPathPseudoSelector::Hover)));
940 assert!(!rule_ends_with(
941 &empty,
942 Some(CssPathPseudoSelector::NthChild(CssNthChildSelector::Even))
943 ));
944 }
945
946 #[test]
947 fn rule_ends_with_none_rejects_interactive_pseudos_but_keeps_structural_ones() {
948 let ends_with = |s: CssPathSelector| {
949 rule_ends_with(&CssPath::new(vec![CssPathSelector::Class("a".into()), s]), None)
950 };
951
952 for p in [
954 CssPathPseudoSelector::Hover,
955 CssPathPseudoSelector::Active,
956 CssPathPseudoSelector::Focus,
957 CssPathPseudoSelector::Backdrop,
958 CssPathPseudoSelector::Dragging,
959 CssPathPseudoSelector::DragOver,
960 ] {
961 assert!(
962 !ends_with(CssPathSelector::PseudoSelector(p.clone())),
963 "interactive pseudo {p:?} must not end a `None` rule"
964 );
965 }
966
967 assert!(ends_with(CssPathSelector::PseudoSelector(
969 CssPathPseudoSelector::First
970 )));
971 assert!(ends_with(CssPathSelector::PseudoSelector(
972 CssPathPseudoSelector::Last
973 )));
974 assert!(ends_with(CssPathSelector::PseudoSelector(
975 CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)
976 )));
977 assert!(ends_with(CssPathSelector::Global));
979 assert!(ends_with(CssPathSelector::Class("b".into())));
980 assert!(ends_with(CssPathSelector::Type(NodeTypeTag::Div)));
981 assert!(ends_with(CssPathSelector::DirectChildren));
982 }
983
984 #[test]
985 fn rule_ends_with_some_target_requires_an_exact_match_on_the_last_selector() {
986 let hover = CssPath::new(vec![
987 CssPathSelector::Class("a".into()),
988 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
989 ]);
990 assert!(rule_ends_with(&hover, Some(CssPathPseudoSelector::Hover)));
991 assert!(!rule_ends_with(&hover, Some(CssPathPseudoSelector::Active)));
992 assert!(!rule_ends_with(&hover, Some(CssPathPseudoSelector::Focus)));
993
994 let plain = CssPath::new(vec![CssPathSelector::Class("a".into())]);
996 assert!(!rule_ends_with(&plain, Some(CssPathPseudoSelector::Hover)));
997
998 let compound = CssPath::new(vec![
1001 CssPathSelector::Class("a".into()),
1002 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
1003 CssPathSelector::PseudoSelector(CssPathPseudoSelector::First),
1004 ]);
1005 assert!(!rule_ends_with(&compound, Some(CssPathPseudoSelector::Hover)));
1006 assert!(rule_ends_with(&compound, Some(CssPathPseudoSelector::First)));
1007 }
1008
1009 #[test]
1010 fn rule_ends_with_compares_nth_child_and_lang_payloads() {
1011 let nth = |s| {
1012 CssPath::new(vec![CssPathSelector::PseudoSelector(
1013 CssPathPseudoSelector::NthChild(s),
1014 )])
1015 };
1016 assert!(rule_ends_with(
1017 &nth(CssNthChildSelector::Number(3)),
1018 Some(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(3)))
1019 ));
1020 assert!(!rule_ends_with(
1021 &nth(CssNthChildSelector::Number(3)),
1022 Some(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(4)))
1023 ));
1024 assert!(!rule_ends_with(
1025 &nth(CssNthChildSelector::Even),
1026 Some(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd))
1027 ));
1028
1029 let lang = |s: &str| {
1031 CssPath::new(vec![CssPathSelector::PseudoSelector(
1032 CssPathPseudoSelector::Lang(s.into()),
1033 )])
1034 };
1035 assert!(rule_ends_with(
1036 &lang("zh-Hant-🎉"),
1037 Some(CssPathPseudoSelector::Lang("zh-Hant-🎉".into()))
1038 ));
1039 assert!(!rule_ends_with(
1040 &lang("zh-Hant-🎉"),
1041 Some(CssPathPseudoSelector::Lang("zh".into()))
1042 ));
1043 assert!(!rule_ends_with(&lang(""), Some(CssPathPseudoSelector::Lang("de".into()))));
1044 }
1045
1046 #[test]
1051 fn match_first_and_last_child_read_only_their_own_field() {
1052 assert!(match_first_child(info(0, false)));
1053 assert!(!match_first_child(info(1, true)));
1054 assert!(!match_first_child(info(u32::MAX, true)));
1055
1056 assert!(match_last_child(info(0, true)));
1057 assert!(!match_last_child(info(0, false)));
1058 assert!(match_last_child(info(u32::MAX, true)));
1059 }
1060
1061 #[test]
1066 fn match_nth_child_is_one_indexed() {
1067 assert!(match_nth_child(info(0, false), &CssNthChildSelector::Number(1)));
1069 assert!(!match_nth_child(info(0, false), &CssNthChildSelector::Number(0)));
1070 assert!(match_nth_child(info(0, false), &CssNthChildSelector::Odd));
1071 assert!(!match_nth_child(info(0, false), &CssNthChildSelector::Even));
1072
1073 assert!(match_nth_child(info(1, false), &CssNthChildSelector::Number(2)));
1074 assert!(match_nth_child(info(1, false), &CssNthChildSelector::Even));
1075 assert!(!match_nth_child(info(1, false), &CssNthChildSelector::Odd));
1076 }
1077
1078 #[test]
1079 fn match_nth_child_number_matches_exactly_one_index() {
1080 for n in 1..64u32 {
1081 for idx in 0..64u32 {
1082 let matched = match_nth_child(info(idx, false), &CssNthChildSelector::Number(n));
1083 assert_eq!(matched, idx + 1 == n, "nth-child({n}) vs index {idx}");
1084 }
1085 }
1086 for idx in 0..64u32 {
1088 assert!(!match_nth_child(info(idx, false), &CssNthChildSelector::Number(0)));
1089 }
1090 }
1091
1092 #[test]
1093 fn match_nth_child_even_and_odd_partition_every_index() {
1094 for idx in 0..1_000u32 {
1095 let even = match_nth_child(info(idx, false), &CssNthChildSelector::Even);
1096 let odd = match_nth_child(info(idx, false), &CssNthChildSelector::Odd);
1097 assert_ne!(even, odd, "index {idx} must be exactly one of even/odd");
1098 }
1099 }
1100
1101 #[test]
1102 fn match_nth_child_pattern_agrees_with_the_even_and_odd_shorthands() {
1103 for idx in 0..256u32 {
1105 let even_pat = CssNthChildSelector::Pattern(CssNthChildPattern {
1106 pattern_repeat: 2,
1107 offset: 0,
1108 });
1109 let odd_pat = CssNthChildSelector::Pattern(CssNthChildPattern {
1110 pattern_repeat: 2,
1111 offset: 1,
1112 });
1113 assert_eq!(
1114 match_nth_child(info(idx, false), &even_pat),
1115 match_nth_child(info(idx, false), &CssNthChildSelector::Even),
1116 "2n disagrees with `even` at index {idx}"
1117 );
1118 assert_eq!(
1119 match_nth_child(info(idx, false), &odd_pat),
1120 match_nth_child(info(idx, false), &CssNthChildSelector::Odd),
1121 "2n+1 disagrees with `odd` at index {idx}"
1122 );
1123 }
1124 }
1125
1126 #[test]
1127 fn match_nth_child_zero_repeat_never_divides_by_zero() {
1128 let only_third = CssNthChildSelector::Pattern(CssNthChildPattern {
1130 pattern_repeat: 0,
1131 offset: 3,
1132 });
1133 let never = CssNthChildSelector::Pattern(CssNthChildPattern {
1134 pattern_repeat: 0,
1135 offset: 0,
1136 });
1137
1138 for idx in 0..32u32 {
1139 assert_eq!(match_nth_child(info(idx, false), &only_third), idx == 2);
1140 assert!(
1141 !match_nth_child(info(idx, false), &never),
1142 "0n+0 must never match (index is 1-based)"
1143 );
1144 }
1145 }
1146
1147 #[test]
1148 fn match_nth_child_pattern_below_the_offset_does_not_underflow() {
1149 let pat = CssNthChildSelector::Pattern(CssNthChildPattern {
1152 pattern_repeat: 2,
1153 offset: 5,
1154 });
1155 for idx in 0..4u32 {
1156 assert!(!match_nth_child(info(idx, false), &pat), "index {idx} < offset");
1157 }
1158 assert!(match_nth_child(info(4, false), &pat)); assert!(!match_nth_child(info(5, false), &pat)); assert!(match_nth_child(info(6, false), &pat)); }
1162
1163 #[test]
1164 fn match_nth_child_with_a_u32_max_offset_does_not_underflow() {
1165 let pat = CssNthChildSelector::Pattern(CssNthChildPattern {
1166 pattern_repeat: 1,
1167 offset: u32::MAX,
1168 });
1169 assert!(!match_nth_child(info(0, false), &pat));
1170 assert!(!match_nth_child(info(1_000, false), &pat));
1171 assert!(match_nth_child(info(u32::MAX - 1, false), &pat));
1174 }
1175
1176 #[cfg(feature = "std")]
1183 #[test]
1184 fn match_nth_child_at_u32_max_index_never_answers_wrongly() {
1185 let even = std::panic::catch_unwind(|| {
1187 match_nth_child(info(u32::MAX, false), &CssNthChildSelector::Even)
1188 });
1189 let odd = std::panic::catch_unwind(|| {
1190 match_nth_child(info(u32::MAX, false), &CssNthChildSelector::Odd)
1191 });
1192 let one = std::panic::catch_unwind(|| {
1193 match_nth_child(info(u32::MAX, false), &CssNthChildSelector::Number(1))
1194 });
1195
1196 if let Ok(v) = even {
1199 assert!(v, "2^32 is even");
1200 }
1201 if let Ok(v) = odd {
1202 assert!(!v, "2^32 is not odd");
1203 }
1204 if let Ok(v) = one {
1205 assert!(!v, "the 2^32-th child is not the 1st child");
1206 }
1207 }
1208
1209 #[test]
1214 fn match_interactive_pseudo_needs_both_the_last_group_and_the_expected_ending() {
1215 let hover = CssPathPseudoSelector::Hover;
1216 let active = CssPathPseudoSelector::Active;
1217
1218 assert!(match_interactive_pseudo(&hover, Some(&hover), true));
1219 assert!(!match_interactive_pseudo(&hover, Some(&hover), false));
1220 assert!(!match_interactive_pseudo(&hover, Some(&active), true));
1221 assert!(!match_interactive_pseudo(&hover, None, true));
1222 assert!(!match_interactive_pseudo(&hover, None, false));
1223 }
1224
1225 #[test]
1226 fn match_pseudo_selector_routes_every_interactive_pseudo_through_the_gate() {
1227 for p in [
1228 CssPathPseudoSelector::Hover,
1229 CssPathPseudoSelector::Active,
1230 CssPathPseudoSelector::Focus,
1231 CssPathPseudoSelector::Backdrop,
1232 CssPathPseudoSelector::Dragging,
1233 CssPathPseudoSelector::DragOver,
1234 ] {
1235 assert!(
1236 match_pseudo_selector(&p, info(0, false), Some(&p), true),
1237 "{p:?} must match when it is the expected ending of the last group"
1238 );
1239 assert!(
1240 !match_pseudo_selector(&p, info(0, false), Some(&p), false),
1241 "{p:?} must not match outside the last content group"
1242 );
1243 assert!(
1244 !match_pseudo_selector(&p, info(0, false), None, true),
1245 "{p:?} must not match when no pseudo state is expected"
1246 );
1247 }
1248 }
1249
1250 #[test]
1251 fn match_pseudo_selector_structural_pseudos_ignore_the_expected_ending() {
1252 let hover = CssPathPseudoSelector::Hover;
1253
1254 for (expected, is_last) in [(None, true), (Some(&hover), false), (Some(&hover), true)] {
1256 assert!(match_pseudo_selector(
1257 &CssPathPseudoSelector::First,
1258 info(0, false),
1259 expected,
1260 is_last
1261 ));
1262 assert!(!match_pseudo_selector(
1263 &CssPathPseudoSelector::First,
1264 info(1, false),
1265 expected,
1266 is_last
1267 ));
1268 assert!(match_pseudo_selector(
1269 &CssPathPseudoSelector::Last,
1270 info(9, true),
1271 expected,
1272 is_last
1273 ));
1274 assert!(match_pseudo_selector(
1275 &CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(10)),
1276 info(9, true),
1277 expected,
1278 is_last
1279 ));
1280 }
1281 }
1282
1283 #[test]
1284 fn match_pseudo_selector_lang_only_matches_the_expected_lang() {
1285 let de = CssPathPseudoSelector::Lang("de".into());
1286 let en = CssPathPseudoSelector::Lang("en".into());
1287
1288 assert!(match_pseudo_selector(&de, info(0, false), Some(&de), true));
1289 assert!(!match_pseudo_selector(&de, info(0, false), Some(&en), true));
1290 assert!(!match_pseudo_selector(&de, info(0, false), None, true));
1291 assert!(!match_pseudo_selector(
1292 &de,
1293 info(0, false),
1294 Some(&CssPathPseudoSelector::Hover),
1295 true
1296 ));
1297
1298 let emoji = CssPathPseudoSelector::Lang("de-🎉".into());
1300 assert!(match_pseudo_selector(&emoji, info(0, false), Some(&emoji), true));
1301 assert!(!match_pseudo_selector(&emoji, info(0, false), Some(&de), true));
1302 let empty = CssPathPseudoSelector::Lang("".into());
1303 assert!(match_pseudo_selector(&empty, info(0, false), Some(&empty), true));
1304 }
1305
1306 #[test]
1311 fn match_attribute_selector_on_a_node_without_attributes_is_always_false() {
1312 let nd = NodeData::create_div();
1313 for op in [
1314 AttributeMatchOp::Exists,
1315 AttributeMatchOp::Eq,
1316 AttributeMatchOp::Includes,
1317 AttributeMatchOp::DashMatch,
1318 AttributeMatchOp::Prefix,
1319 AttributeMatchOp::Suffix,
1320 AttributeMatchOp::Substring,
1321 ] {
1322 assert!(!match_attribute_selector(&attr_sel("data-x", op, Some("v")), &nd));
1323 assert!(!match_attribute_selector(&attr_sel("", op, None), &nd));
1324 }
1325 }
1326
1327 #[test]
1328 fn match_attribute_selector_exists_matches_any_value_including_the_empty_one() {
1329 let nd = node_with_attrs(vec![custom("data-x", "")]);
1330 assert!(match_attribute_selector(
1331 &attr_sel("data-x", AttributeMatchOp::Exists, None),
1332 &nd
1333 ));
1334 assert!(match_attribute_selector(
1336 &attr_sel("data-x", AttributeMatchOp::Exists, Some("nonsense")),
1337 &nd
1338 ));
1339 assert!(!match_attribute_selector(
1340 &attr_sel("data-y", AttributeMatchOp::Exists, None),
1341 &nd
1342 ));
1343 }
1344
1345 #[test]
1346 fn match_attribute_selector_operator_without_a_value_never_matches() {
1347 let nd = node_with_attrs(vec![custom("data-x", "value")]);
1348 for op in [
1349 AttributeMatchOp::Eq,
1350 AttributeMatchOp::Includes,
1351 AttributeMatchOp::DashMatch,
1352 AttributeMatchOp::Prefix,
1353 AttributeMatchOp::Suffix,
1354 AttributeMatchOp::Substring,
1355 ] {
1356 assert!(
1357 !match_attribute_selector(&attr_sel("data-x", op, None), &nd),
1358 "{op:?} with a missing target value must be rejected, not matched"
1359 );
1360 }
1361 }
1362
1363 #[test]
1364 fn match_attribute_selector_empty_target_never_matches_the_substring_family() {
1365 let nd = node_with_attrs(vec![custom("data-x", "abc")]);
1368 for op in [
1369 AttributeMatchOp::Prefix,
1370 AttributeMatchOp::Suffix,
1371 AttributeMatchOp::Substring,
1372 AttributeMatchOp::Includes,
1373 ] {
1374 assert!(
1375 !match_attribute_selector(&attr_sel("data-x", op, Some("")), &nd),
1376 "{op:?} with an empty target must not match"
1377 );
1378 }
1379 assert!(!match_attribute_selector(
1381 &attr_sel("data-x", AttributeMatchOp::Eq, Some("")),
1382 &nd
1383 ));
1384 let empty = node_with_attrs(vec![custom("data-x", "")]);
1385 assert!(match_attribute_selector(
1386 &attr_sel("data-x", AttributeMatchOp::Eq, Some("")),
1387 &empty
1388 ));
1389 }
1390
1391 #[test]
1392 fn match_attribute_selector_includes_matches_one_of_several_class_entries() {
1393 let mut nd = NodeData::create_div();
1396 nd.add_class("foo".into());
1397 nd.add_class("primary".into());
1398 nd.add_class("bar".into());
1399
1400 assert!(match_attribute_selector(
1401 &attr_sel("class", AttributeMatchOp::Includes, Some("primary")),
1402 &nd
1403 ));
1404 assert!(!match_attribute_selector(
1405 &attr_sel("class", AttributeMatchOp::Includes, Some("prim")),
1406 &nd
1407 ));
1408 assert!(!match_attribute_selector(
1410 &attr_sel("class", AttributeMatchOp::Includes, Some("foo bar")),
1411 &nd
1412 ));
1413 assert!(!match_attribute_selector(
1414 &attr_sel("class", AttributeMatchOp::Includes, Some("\t")),
1415 &nd
1416 ));
1417 }
1418
1419 #[test]
1420 fn match_attribute_selector_dash_match_requires_a_dash_boundary() {
1421 let nd = node_with_attrs(vec![AttributeType::Lang("en-US".into())]);
1422
1423 assert!(match_attribute_selector(
1424 &attr_sel("lang", AttributeMatchOp::DashMatch, Some("en")),
1425 &nd
1426 ));
1427 assert!(match_attribute_selector(
1428 &attr_sel("lang", AttributeMatchOp::DashMatch, Some("en-US")),
1429 &nd
1430 ));
1431 assert!(!match_attribute_selector(
1433 &attr_sel("lang", AttributeMatchOp::DashMatch, Some("en-U")),
1434 &nd
1435 ));
1436 assert!(!match_attribute_selector(
1437 &attr_sel("lang", AttributeMatchOp::DashMatch, Some("e")),
1438 &nd
1439 ));
1440
1441 let english = node_with_attrs(vec![AttributeType::Lang("english".into())]);
1443 assert!(!match_attribute_selector(
1444 &attr_sel("lang", AttributeMatchOp::DashMatch, Some("en")),
1445 &english
1446 ));
1447 }
1448
1449 #[test]
1450 fn match_attribute_selector_name_matching_is_exact_and_case_sensitive() {
1451 let nd = node_with_attrs(vec![custom("data-foo", "v")]);
1452 assert!(match_attribute_selector(
1453 &attr_sel("data-foo", AttributeMatchOp::Eq, Some("v")),
1454 &nd
1455 ));
1456 assert!(!match_attribute_selector(
1457 &attr_sel("data-fo", AttributeMatchOp::Eq, Some("v")),
1458 &nd
1459 ));
1460 assert!(!match_attribute_selector(
1461 &attr_sel("data-foo2", AttributeMatchOp::Eq, Some("v")),
1462 &nd
1463 ));
1464 assert!(!match_attribute_selector(
1465 &attr_sel("DATA-FOO", AttributeMatchOp::Eq, Some("v")),
1466 &nd
1467 ));
1468 assert!(!match_attribute_selector(
1469 &attr_sel("", AttributeMatchOp::Exists, None),
1470 &nd
1471 ));
1472 }
1473
1474 #[test]
1475 fn match_attribute_selector_handles_unicode_values_on_char_boundaries() {
1476 let nd = node_with_attrs(vec![custom("data-x", "héllo-🎉-世界")]);
1478
1479 assert!(match_attribute_selector(
1480 &attr_sel("data-x", AttributeMatchOp::Prefix, Some("hé")),
1481 &nd
1482 ));
1483 assert!(match_attribute_selector(
1484 &attr_sel("data-x", AttributeMatchOp::Suffix, Some("世界")),
1485 &nd
1486 ));
1487 assert!(match_attribute_selector(
1488 &attr_sel("data-x", AttributeMatchOp::Substring, Some("🎉")),
1489 &nd
1490 ));
1491 assert!(match_attribute_selector(
1492 &attr_sel("data-x", AttributeMatchOp::Eq, Some("héllo-🎉-世界")),
1493 &nd
1494 ));
1495 assert!(!match_attribute_selector(
1497 &attr_sel("data-x", AttributeMatchOp::Substring, Some("e")),
1498 &nd
1499 ));
1500 assert!(match_attribute_selector(
1502 &attr_sel("data-x", AttributeMatchOp::DashMatch, Some("héllo")),
1503 &nd
1504 ));
1505 }
1506
1507 #[test]
1508 fn match_attribute_selector_handles_huge_values() {
1509 let huge = "ä".repeat(100_000);
1510 let value = alloc::format!("{huge}-tail");
1511 let nd = node_with_attrs(vec![custom("data-big", &value)]);
1512
1513 assert!(match_attribute_selector(
1514 &attr_sel("data-big", AttributeMatchOp::Suffix, Some("-tail")),
1515 &nd
1516 ));
1517 assert!(match_attribute_selector(
1518 &attr_sel("data-big", AttributeMatchOp::Prefix, Some("ää")),
1519 &nd
1520 ));
1521 assert!(match_attribute_selector(
1522 &attr_sel("data-big", AttributeMatchOp::DashMatch, Some(huge.as_str())),
1523 &nd
1524 ));
1525 assert!(!match_attribute_selector(
1526 &attr_sel("data-big", AttributeMatchOp::Eq, Some(huge.as_str())),
1527 &nd
1528 ));
1529 }
1530
1531 #[test]
1536 fn match_single_selector_global_matches_every_node_type() {
1537 let div = NodeData::create_div();
1538 let text = NodeData::create_text("hello");
1539 assert!(match_single_selector(
1540 &CssPathSelector::Global,
1541 info(0, false),
1542 &div,
1543 NodeId::new(0),
1544 None,
1545 true
1546 ));
1547 assert!(match_single_selector(
1548 &CssPathSelector::Global,
1549 info(u32::MAX, true),
1550 &text,
1551 NodeId::new(usize::MAX),
1552 None,
1553 false
1554 ));
1555 }
1556
1557 #[test]
1558 fn match_single_selector_never_matches_a_combinator() {
1559 let div = NodeData::create_div();
1562 for c in [
1563 CssPathSelector::DirectChildren,
1564 CssPathSelector::Children,
1565 CssPathSelector::AdjacentSibling,
1566 CssPathSelector::GeneralSibling,
1567 ] {
1568 assert!(!match_single_selector(
1569 &c,
1570 info(0, true),
1571 &div,
1572 NodeId::new(0),
1573 None,
1574 true
1575 ));
1576 }
1577 }
1578
1579 #[test]
1580 fn match_single_selector_root_scope_range_is_inclusive_on_both_ends() {
1581 let div = NodeData::create_div();
1582 let sel = CssPathSelector::Root(CssScopeRange { start: 2, end: 4 });
1583
1584 for id in [2usize, 3, 4] {
1585 assert!(match_single_selector(
1586 &sel,
1587 info(0, false),
1588 &div,
1589 NodeId::new(id),
1590 None,
1591 true
1592 ));
1593 }
1594 for id in [0usize, 1, 5, usize::MAX] {
1595 assert!(!match_single_selector(
1596 &sel,
1597 info(0, false),
1598 &div,
1599 NodeId::new(id),
1600 None,
1601 true
1602 ));
1603 }
1604 }
1605
1606 #[test]
1607 fn match_single_selector_root_scope_with_an_inverted_or_full_range() {
1608 let div = NodeData::create_div();
1609
1610 let inverted = CssPathSelector::Root(CssScopeRange { start: 9, end: 2 });
1612 for id in [0usize, 2, 9, usize::MAX] {
1613 assert!(!match_single_selector(
1614 &inverted,
1615 info(0, false),
1616 &div,
1617 NodeId::new(id),
1618 None,
1619 true
1620 ));
1621 }
1622
1623 let full = CssPathSelector::Root(CssScopeRange {
1625 start: 0,
1626 end: usize::MAX,
1627 });
1628 for id in [0usize, 1, usize::MAX] {
1629 assert!(match_single_selector(
1630 &full,
1631 info(0, false),
1632 &div,
1633 NodeId::new(id),
1634 None,
1635 true
1636 ));
1637 }
1638 }
1639
1640 #[test]
1641 fn match_single_selector_type_class_and_id() {
1642 let mut nd = div_with(Some("first"), Some("a"));
1643 nd.add_class("日本語-🎉".into());
1644
1645 let hit = |s: &CssPathSelector| {
1646 match_single_selector(s, info(0, false), &nd, NodeId::new(0), None, true)
1647 };
1648
1649 assert!(hit(&CssPathSelector::Type(NodeTypeTag::Div)));
1650 assert!(!hit(&CssPathSelector::Type(NodeTypeTag::P)));
1651 assert!(hit(&CssPathSelector::Class("a".into())));
1652 assert!(hit(&CssPathSelector::Class("日本語-🎉".into())));
1653 assert!(!hit(&CssPathSelector::Class("日本語".into())), "no prefix matching");
1654 assert!(!hit(&CssPathSelector::Class("".into())));
1655 assert!(hit(&CssPathSelector::Id("first".into())));
1656 assert!(!hit(&CssPathSelector::Id("firs".into())));
1657 assert!(!hit(&CssPathSelector::Class("first".into())));
1659 assert!(!hit(&CssPathSelector::Id("a".into())));
1660 }
1661
1662 #[test]
1663 fn selector_group_matches_requires_every_selector_in_the_group() {
1664 let nd = div_with(Some("first"), Some("a"));
1665 let div = CssPathSelector::Type(NodeTypeTag::Div);
1666 let class_a = CssPathSelector::Class("a".into());
1667 let class_z = CssPathSelector::Class("zzz".into());
1668
1669 let group: Vec<&CssPathSelector> = vec![&div, &class_a];
1670 assert!(selector_group_matches(
1671 &group,
1672 info(0, false),
1673 &nd,
1674 NodeId::new(0),
1675 None,
1676 true
1677 ));
1678
1679 let group: Vec<&CssPathSelector> = vec![&div, &class_a, &class_z];
1680 assert!(!selector_group_matches(
1681 &group,
1682 info(0, false),
1683 &nd,
1684 NodeId::new(0),
1685 None,
1686 true
1687 ));
1688 }
1689
1690 #[test]
1691 fn selector_group_matches_empty_group_matches_vacuously() {
1692 let empty: Vec<&CssPathSelector> = Vec::new();
1696 assert!(selector_group_matches(
1697 &empty,
1698 info(0, false),
1699 &NodeData::create_div(),
1700 NodeId::new(0),
1701 None,
1702 true
1703 ));
1704 }
1705
1706 fn anonymous_fixture() -> (Vec<Node>, Vec<NodeData>) {
1719 let hierarchy = vec![
1720 node(None, None, None, Some(5)),
1721 node(Some(0), None, Some(4), Some(2)),
1722 node(Some(1), None, None, Some(3)),
1723 node(Some(2), None, None, None),
1724 node(Some(0), Some(1), Some(5), None),
1725 node(Some(0), Some(4), None, None),
1726 ];
1727 let mut anon1 = NodeData::create_div();
1728 anon1.set_anonymous(true);
1729 let mut anon2 = NodeData::create_div();
1730 anon2.set_anonymous(true);
1731 let mut anon4 = NodeData::create_div();
1732 anon4.set_anonymous(true);
1733 let data = vec![
1734 NodeData::create_body(),
1735 anon1,
1736 anon2,
1737 div_with(None, Some("deep")),
1738 anon4,
1739 div_with(None, Some("c")),
1740 ];
1741 (hierarchy, data)
1742 }
1743
1744 #[test]
1745 fn find_non_anonymous_parent_skips_a_chain_of_anonymous_boxes() {
1746 let (hierarchy, data) = anonymous_fixture();
1747 let hier_items = items(&hierarchy);
1748 let h = NodeDataContainerRef::from_slice(&hier_items);
1749 let d = NodeDataContainerRef::from_slice(&data);
1750
1751 assert_eq!(
1753 find_non_anonymous_parent(NodeId::new(3), &h, &d),
1754 Some(NodeId::new(0))
1755 );
1756 assert_eq!(
1758 find_non_anonymous_parent(NodeId::new(5), &h, &d),
1759 Some(NodeId::new(0))
1760 );
1761 assert_eq!(find_non_anonymous_parent(NodeId::new(0), &h, &d), None);
1763 }
1764
1765 #[test]
1766 fn find_non_anonymous_parent_returns_none_when_every_ancestor_is_anonymous() {
1767 let hierarchy = vec![node(None, None, None, Some(1)), node(Some(0), None, None, None)];
1769 let mut anon_root = NodeData::create_div();
1770 anon_root.set_anonymous(true);
1771 let data = vec![anon_root, NodeData::create_div()];
1772 let hier_items = items(&hierarchy);
1773 let h = NodeDataContainerRef::from_slice(&hier_items);
1774 let d = NodeDataContainerRef::from_slice(&data);
1775
1776 assert_eq!(find_non_anonymous_parent(NodeId::new(1), &h, &d), None);
1777 }
1778
1779 #[test]
1780 fn find_non_anonymous_prev_sibling_skips_anonymous_siblings() {
1781 let (hierarchy, data) = anonymous_fixture();
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!(find_non_anonymous_prev_sibling(NodeId::new(5), &h, &d), None);
1789 assert_eq!(find_non_anonymous_prev_sibling(NodeId::new(1), &h, &d), None);
1791 assert_eq!(find_non_anonymous_prev_sibling(NodeId::new(0), &h, &d), None);
1792 }
1793
1794 #[test]
1795 fn find_non_anonymous_prev_sibling_returns_the_nearest_real_sibling() {
1796 let hierarchy = sample_hierarchy();
1797 let data = sample_node_data();
1798 let hier_items = items(&hierarchy);
1799 let h = NodeDataContainerRef::from_slice(&hier_items);
1800 let d = NodeDataContainerRef::from_slice(&data);
1801
1802 assert_eq!(
1807 find_non_anonymous_prev_sibling(NodeId::new(3), &h, &d),
1808 Some(NodeId::new(1))
1809 );
1810 assert_eq!(
1811 find_non_anonymous_prev_sibling(NodeId::new(5), &h, &d),
1812 Some(NodeId::new(3))
1813 );
1814 }
1815
1816 #[test]
1821 fn construct_html_cascade_tree_on_an_empty_hierarchy_is_empty() {
1822 let hierarchy: Vec<Node> = Vec::new();
1823 let data: Vec<NodeData> = Vec::new();
1824 let out = construct_html_cascade_tree(
1825 &NodeHierarchyRef::from_slice(&hierarchy),
1826 &[],
1827 &NodeDataContainerRef::from_slice(&data),
1828 );
1829 assert_eq!(out.len(), 0);
1830 assert!(out.is_empty());
1831 }
1832
1833 #[test]
1834 fn construct_html_cascade_tree_with_no_parents_defaults_every_node() {
1835 let hierarchy = vec![node(None, None, None, None)];
1838 let data = vec![NodeData::create_body()];
1839 let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
1840 let depths = hierarchy_ref.get_parents_sorted_by_depth();
1841 assert!(depths.is_empty());
1842
1843 let out = construct_html_cascade_tree(
1844 &hierarchy_ref,
1845 &depths,
1846 &NodeDataContainerRef::from_slice(&data),
1847 );
1848 assert_eq!(out.len(), 1);
1849 assert_eq!(out.internal[0], CascadeInfo::default());
1850 }
1851
1852 #[test]
1853 fn construct_html_cascade_tree_does_not_count_text_nodes_as_element_siblings() {
1854 let hierarchy = sample_hierarchy();
1855 let data = sample_node_data();
1856 let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
1857 let data_ref = NodeDataContainerRef::from_slice(&data);
1858 let depths = hierarchy_ref.get_parents_sorted_by_depth();
1859
1860 let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
1861
1862 assert_eq!(out.len(), hierarchy.len(), "one CascadeInfo per node");
1863 assert_eq!(out.internal[0], info(0, true), "root");
1864 assert_eq!(out.internal[1], info(0, false), "div#first.a — 1st element child");
1865 assert_eq!(out.internal[3], info(1, false), "div.b — 2nd element child (text skipped)");
1866 assert_eq!(out.internal[4], info(0, true), "p.inner — only child of div.b");
1867 assert_eq!(
1868 out.internal[5],
1869 info(2, true),
1870 "div.c — 3rd element child and the last one"
1871 );
1872 assert!(!out.internal[2].is_last_child);
1874 }
1875
1876 #[test]
1877 fn construct_html_cascade_tree_ignores_trailing_text_nodes_for_is_last_child() {
1878 let hierarchy = vec![
1880 node(None, None, None, Some(3)),
1881 node(Some(0), None, Some(2), None),
1882 node(Some(0), Some(1), Some(3), None),
1883 node(Some(0), Some(2), None, None),
1884 ];
1885 let data = vec![
1886 NodeData::create_body(),
1887 NodeData::create_div(),
1888 NodeData::create_text("a"),
1889 NodeData::create_text("b"),
1890 ];
1891 let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
1892 let data_ref = NodeDataContainerRef::from_slice(&data);
1893 let depths = hierarchy_ref.get_parents_sorted_by_depth();
1894
1895 let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
1896 assert_eq!(out.internal[1], info(0, true), "trailing text must not un-last the div");
1897 }
1898
1899 #[test]
1900 fn construct_html_cascade_tree_handles_a_wide_tree() {
1901 const CHILDREN: usize = 2_000;
1902
1903 let mut hierarchy = vec![node(None, None, None, Some(CHILDREN))];
1904 let mut data = vec![NodeData::create_body()];
1905 for i in 1..=CHILDREN {
1906 hierarchy.push(node(
1907 Some(0),
1908 if i == 1 { None } else { Some(i - 1) },
1909 if i == CHILDREN { None } else { Some(i + 1) },
1910 None,
1911 ));
1912 data.push(NodeData::create_div());
1913 }
1914
1915 let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
1916 let data_ref = NodeDataContainerRef::from_slice(&data);
1917 let depths = hierarchy_ref.get_parents_sorted_by_depth();
1918 let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
1919
1920 assert_eq!(out.len(), CHILDREN + 1);
1921 for i in 1..=CHILDREN {
1922 let expected = info(u32::try_from(i - 1).unwrap(), i == CHILDREN);
1923 assert_eq!(out.internal[i], expected, "child {i}");
1924 }
1925 }
1926
1927 #[test]
1928 fn construct_html_cascade_tree_handles_a_deep_chain() {
1929 const DEPTH: usize = 1_000;
1930
1931 let mut hierarchy = Vec::with_capacity(DEPTH);
1932 let mut data = Vec::with_capacity(DEPTH);
1933 for i in 0..DEPTH {
1934 hierarchy.push(node(
1935 if i == 0 { None } else { Some(i - 1) },
1936 None,
1937 None,
1938 if i + 1 == DEPTH { None } else { Some(i + 1) },
1939 ));
1940 data.push(NodeData::create_div());
1941 }
1942
1943 let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
1944 let data_ref = NodeDataContainerRef::from_slice(&data);
1945 let depths = hierarchy_ref.get_parents_sorted_by_depth();
1946 let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
1947
1948 assert_eq!(out.len(), DEPTH);
1949 for i in 0..DEPTH {
1950 assert_eq!(
1951 out.internal[i],
1952 info(0, true),
1953 "every node in a chain is an only child"
1954 );
1955 }
1956 }
1957
1958 #[test]
1963 fn matches_html_element_empty_path_never_matches() {
1964 assert!(!matches(Vec::new(), 1, None));
1965 assert!(!matches(Vec::new(), 0, Some(CssPathPseudoSelector::Hover)));
1966 }
1967
1968 #[test]
1969 fn matches_html_element_matches_type_class_and_id_on_the_subject() {
1970 assert!(matches(vec![CssPathSelector::Global], 1, None));
1971 assert!(matches(vec![CssPathSelector::Class("a".into())], 1, None));
1972 assert!(!matches(vec![CssPathSelector::Class("a".into())], 3, None));
1973 assert!(matches(vec![CssPathSelector::Id("first".into())], 1, None));
1974 assert!(matches(vec![CssPathSelector::Type(NodeTypeTag::Div)], 1, None));
1975 assert!(!matches(vec![CssPathSelector::Type(NodeTypeTag::Div)], 0, None));
1976 assert!(matches(vec![CssPathSelector::Type(NodeTypeTag::Body)], 0, None));
1977
1978 let div_a = vec![
1980 CssPathSelector::Type(NodeTypeTag::Div),
1981 CssPathSelector::Class("a".into()),
1982 ];
1983 assert!(matches(div_a.clone(), 1, None));
1984 assert!(!matches(div_a, 3, None));
1985 }
1986
1987 #[test]
1988 fn matches_html_element_never_matches_an_anonymous_node() {
1989 let (hierarchy, data) = anonymous_fixture();
1990 let hier_items = items(&hierarchy);
1991 let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
1992 let data_ref = NodeDataContainerRef::from_slice(&data);
1993 let depths = hierarchy_ref.get_parents_sorted_by_depth();
1994 let cascade = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
1995
1996 for id in [1usize, 2, 4] {
1998 assert!(
1999 !matches_html_element(
2000 &CssPath::new(vec![CssPathSelector::Global]),
2001 NodeId::new(id),
2002 &NodeDataContainerRef::from_slice(&hier_items),
2003 &data_ref,
2004 &cascade.as_ref(),
2005 None,
2006 ),
2007 "anonymous node {id} must not be styled"
2008 );
2009 }
2010 for id in [0usize, 3, 5] {
2011 assert!(matches_html_element(
2012 &CssPath::new(vec![CssPathSelector::Global]),
2013 NodeId::new(id),
2014 &NodeDataContainerRef::from_slice(&hier_items),
2015 &data_ref,
2016 &cascade.as_ref(),
2017 None,
2018 ));
2019 }
2020 }
2021
2022 #[test]
2023 fn matches_html_element_child_combinator_is_stricter_than_the_descendant_one() {
2024 let child = vec![
2026 CssPathSelector::Type(NodeTypeTag::Body),
2027 CssPathSelector::DirectChildren,
2028 CssPathSelector::Type(NodeTypeTag::P),
2029 ];
2030 assert!(!matches(child, 4, None));
2031
2032 let descendant = vec![
2034 CssPathSelector::Type(NodeTypeTag::Body),
2035 CssPathSelector::Children,
2036 CssPathSelector::Type(NodeTypeTag::P),
2037 ];
2038 assert!(matches(descendant, 4, None));
2039
2040 let direct = vec![
2042 CssPathSelector::Type(NodeTypeTag::Body),
2043 CssPathSelector::DirectChildren,
2044 CssPathSelector::Type(NodeTypeTag::Div),
2045 CssPathSelector::Class("b".into()),
2046 ];
2047 assert!(matches(direct, 3, None));
2048 }
2049
2050 #[test]
2051 fn matches_html_element_descendant_combinator_walks_the_whole_ancestor_chain() {
2052 let close = vec![
2054 CssPathSelector::Class("b".into()),
2055 CssPathSelector::Children,
2056 CssPathSelector::Class("inner".into()),
2057 ];
2058 assert!(matches(close, 4, None));
2059
2060 let unrelated = vec![
2062 CssPathSelector::Class("c".into()),
2063 CssPathSelector::Children,
2064 CssPathSelector::Class("inner".into()),
2065 ];
2066 assert!(!matches(unrelated, 4, None));
2067 }
2068
2069 #[test]
2070 fn matches_html_element_general_sibling_scans_all_previous_siblings() {
2071 let general = vec![
2074 CssPathSelector::Class("a".into()),
2075 CssPathSelector::GeneralSibling,
2076 CssPathSelector::Class("c".into()),
2077 ];
2078 assert!(matches(general, 5, None));
2079
2080 let backwards = vec![
2082 CssPathSelector::Class("c".into()),
2083 CssPathSelector::GeneralSibling,
2084 CssPathSelector::Class("a".into()),
2085 ];
2086 assert!(!matches(backwards, 1, None));
2087 }
2088
2089 #[test]
2090 fn matches_html_element_adjacent_sibling_matches_the_immediate_element_sibling() {
2091 let adjacent = vec![
2093 CssPathSelector::Class("b".into()),
2094 CssPathSelector::AdjacentSibling,
2095 CssPathSelector::Class("c".into()),
2096 ];
2097 assert!(matches(adjacent, 5, None));
2098
2099 let not_adjacent = vec![
2101 CssPathSelector::Class("a".into()),
2102 CssPathSelector::AdjacentSibling,
2103 CssPathSelector::Class("c".into()),
2104 ];
2105 assert!(!matches(not_adjacent, 5, None));
2106 }
2107
2108 #[test]
2115 fn matches_html_element_adjacent_sibling_skips_text_nodes() {
2116 let adjacent = vec![
2118 CssPathSelector::Class("a".into()),
2119 CssPathSelector::AdjacentSibling,
2120 CssPathSelector::Class("b".into()),
2121 ];
2122 assert!(
2123 matches(adjacent, 3, None),
2124 "the `+` combinator must ignore non-element (text) siblings"
2125 );
2126 }
2127
2128 #[test]
2129 fn matches_html_element_hover_on_a_single_group_path_needs_the_expected_ending() {
2130 let hover = vec![
2131 CssPathSelector::Class("a".into()),
2132 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
2133 ];
2134 assert!(matches(hover.clone(), 1, Some(CssPathPseudoSelector::Hover)));
2135 assert!(!matches(hover.clone(), 1, None));
2137 assert!(!matches(hover.clone(), 1, Some(CssPathPseudoSelector::Focus)));
2139 assert!(!matches(hover, 3, Some(CssPathPseudoSelector::Hover)));
2141 }
2142
2143 #[test]
2152 fn matches_html_element_hover_on_a_descendant_path_still_matches() {
2153 let hover_descendant = vec![
2155 CssPathSelector::Type(NodeTypeTag::Body),
2156 CssPathSelector::Children,
2157 CssPathSelector::Class("a".into()),
2158 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
2159 ];
2160 assert!(
2161 matches(hover_descendant, 1, Some(CssPathPseudoSelector::Hover)),
2162 ":hover on the subject of a multi-group selector must still match"
2163 );
2164 }
2165
2166 #[test]
2167 fn matches_html_element_structural_pseudos_work_on_the_subject() {
2168 let first = vec![
2170 CssPathSelector::Class("a".into()),
2171 CssPathSelector::PseudoSelector(CssPathPseudoSelector::First),
2172 ];
2173 assert!(matches(first, 1, None));
2174
2175 let last = vec![
2176 CssPathSelector::Class("c".into()),
2177 CssPathSelector::PseudoSelector(CssPathPseudoSelector::Last),
2178 ];
2179 assert!(matches(last, 5, None));
2180
2181 let nth = vec![
2183 CssPathSelector::Class("b".into()),
2184 CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
2185 CssNthChildSelector::Number(2),
2186 )),
2187 ];
2188 assert!(matches(nth, 3, None));
2189
2190 let wrong_nth = vec![
2191 CssPathSelector::Class("b".into()),
2192 CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
2193 CssNthChildSelector::Number(3),
2194 )),
2195 ];
2196 assert!(!matches(wrong_nth, 3, None));
2197 }
2198
2199 #[test]
2200 fn matches_html_element_root_scope_confines_a_rule_to_its_subtree() {
2201 let scoped = vec![
2203 CssPathSelector::Root(CssScopeRange { start: 3, end: 4 }),
2204 CssPathSelector::Global,
2205 ];
2206 assert!(matches(scoped.clone(), 3, None));
2207 assert!(matches(scoped.clone(), 4, None));
2208 assert!(!matches(scoped.clone(), 1, None), "must not leak to a sibling");
2209 assert!(!matches(scoped.clone(), 5, None), "must not leak to a sibling");
2210 assert!(!matches(scoped, 0, None), "must not leak to the parent");
2211
2212 let node_only = vec![
2214 CssPathSelector::Root(CssScopeRange { start: 3, end: 3 }),
2215 CssPathSelector::Global,
2216 ];
2217 assert!(matches(node_only.clone(), 3, None));
2218 assert!(!matches(node_only, 4, None), "a node-only scope must not reach children");
2219 }
2220
2221 #[test]
2222 fn matches_html_element_with_a_dangling_combinator_does_not_panic() {
2223 let dangling = vec![
2227 CssPathSelector::Class("a".into()),
2228 CssPathSelector::DirectChildren,
2229 ];
2230 assert!(!matches(dangling.clone(), 4, None));
2231 for id in 0..6 {
2233 let _ = matches(dangling.clone(), id, None);
2234 }
2235 }
2236
2237 #[test]
2238 fn matches_html_element_on_a_very_long_selector_chain_terminates() {
2239 let mut path = Vec::new();
2242 for _ in 0..500 {
2243 path.push(CssPathSelector::Type(NodeTypeTag::Body));
2244 path.push(CssPathSelector::Children);
2245 }
2246 path.push(CssPathSelector::Class("inner".into()));
2247 assert!(!matches(path, 4, None));
2248 }
2249}