1
//! DOM tree to CSS style tree cascading.
2
//!
3
//! Implements CSS selector matching (`matches_html_element`) and cascade-info
4
//! construction (`construct_html_cascade_tree`). Used by `styled_dom` and
5
//! `prop_cache` to resolve which CSS rules apply to each DOM node.
6

            
7
use alloc::vec::Vec;
8

            
9
use azul_css::css::{
10
    AttributeMatchOp, CssAttributeSelector, CssContentGroup, CssNthChildSelector,
11
    CssNthChildSelector::*, CssPath, CssPathPseudoSelector, CssPathSelector,
12
};
13

            
14
use crate::{
15
    dom::NodeData,
16
    id::{NodeDataContainer, NodeDataContainerRef, NodeHierarchyRef, NodeId},
17
    styled_dom::NodeHierarchyItem,
18
};
19

            
20
/// Has all the necessary information about the style CSS path
21
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22
#[repr(C)]
23
pub struct CascadeInfo {
24
    pub index_in_parent: u32,
25
    pub is_last_child: bool,
26
}
27

            
28
impl_option!(
29
    CascadeInfo,
30
    OptionCascadeInfo,
31
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
32
);
33

            
34
impl_vec!(CascadeInfo, CascadeInfoVec, CascadeInfoVecDestructor, CascadeInfoVecDestructorType, CascadeInfoVecSlice, OptionCascadeInfo);
35
impl_vec_mut!(CascadeInfo, CascadeInfoVec);
36
impl_vec_debug!(CascadeInfo, CascadeInfoVec);
37
impl_vec_partialord!(CascadeInfo, CascadeInfoVec);
38
impl_vec_clone!(CascadeInfo, CascadeInfoVec, CascadeInfoVecDestructor);
39
impl_vec_partialeq!(CascadeInfo, CascadeInfoVec);
40

            
41
impl CascadeInfoVec {
42
    pub fn as_container<'a>(&'a self) -> NodeDataContainerRef<'a, CascadeInfo> {
43
        NodeDataContainerRef {
44
            internal: self.as_ref(),
45
        }
46
    }
47
}
48

            
49
/// Returns if the style CSS path matches the DOM node (i.e. if the DOM node should be styled by
50
/// that element)
51
779468
pub fn matches_html_element(
52
779468
    css_path: &CssPath,
53
779468
    node_id: NodeId,
54
779468
    node_hierarchy: &NodeDataContainerRef<NodeHierarchyItem>,
55
779468
    node_data: &NodeDataContainerRef<NodeData>,
56
779468
    html_node_tree: &NodeDataContainerRef<CascadeInfo>,
57
779468
    expected_path_ending: Option<CssPathPseudoSelector>,
58
779468
) -> bool {
59
    use self::CssGroupSplitReason::*;
60

            
61
779468
    if css_path.selectors.is_empty() {
62
        return false;
63
779468
    }
64

            
65
    // Skip anonymous nodes - they are not part of the original DOM tree
66
    // and should not participate in CSS selector matching
67
779468
    if node_data[node_id].is_anonymous() {
68
        return false;
69
779468
    }
70

            
71
    // Collect all selector groups (processed right-to-left from the CSS path).
72
779468
    let groups: Vec<(CssContentGroup<'_>, CssGroupSplitReason)> =
73
779468
        CssGroupIterator::new(css_path.selectors.as_ref()).collect();
74

            
75
779468
    if groups.is_empty() {
76
        return false;
77
779468
    }
78

            
79
    // The rightmost group must match the target node directly.
80
779468
    let (ref first_group, first_reason) = groups[0];
81
779468
    let is_last_content_group = groups.len() == 1;
82
779468
    if !selector_group_matches(
83
779468
        first_group,
84
779468
        &html_node_tree[node_id],
85
779468
        &node_data[node_id],
86
779468
        node_id,
87
779468
        &expected_path_ending,
88
779468
        is_last_content_group,
89
779468
    ) {
90
740744
        return false;
91
38724
    }
92

            
93
    // Navigate from the target node upward/sideways through the DOM,
94
    // matching each remaining selector group with its combinator.
95
38724
    let mut current_node = node_id;
96

            
97
38724
    for (group_idx, (content_group, _reason)) in groups.iter().enumerate().skip(1) {
98
        // The combinator comes from the PREVIOUS group's reason
99
51
        let combinator = groups[group_idx - 1].1;
100
51
        let is_last = group_idx == groups.len() - 1;
101

            
102
51
        match combinator {
103
            DirectChildren => {
104
                // Parent must match directly (child combinator `>`)
105
                let parent = find_non_anonymous_parent(current_node, node_hierarchy, node_data);
106
                match parent {
107
                    Some(p) if selector_group_matches(
108
                        content_group, &html_node_tree[p], &node_data[p], p,
109
                        &expected_path_ending, is_last,
110
                    ) => { current_node = p; }
111
                    _ => return false,
112
                }
113
            }
114
            Children => {
115
                // Search up ancestor chain for a match (descendant combinator ` `)
116
51
                let mut ancestor = find_non_anonymous_parent(current_node, node_hierarchy, node_data);
117
51
                let mut found = false;
118
102
                while let Some(anc) = ancestor {
119
102
                    if selector_group_matches(
120
102
                        content_group, &html_node_tree[anc], &node_data[anc], anc,
121
102
                        &expected_path_ending, is_last,
122
                    ) {
123
51
                        current_node = anc;
124
51
                        found = true;
125
51
                        break;
126
51
                    }
127
51
                    ancestor = find_non_anonymous_parent(anc, node_hierarchy, node_data);
128
                }
129
51
                if !found {
130
                    return false;
131
51
                }
132
            }
133
            AdjacentSibling => {
134
                // Immediate previous sibling must match (adjacent sibling `+`)
135
                let sibling = find_non_anonymous_prev_sibling(current_node, node_hierarchy, node_data);
136
                match sibling {
137
                    Some(s) if selector_group_matches(
138
                        content_group, &html_node_tree[s], &node_data[s], s,
139
                        &expected_path_ending, is_last,
140
                    ) => { current_node = s; }
141
                    _ => return false,
142
                }
143
            }
144
            GeneralSibling => {
145
                // Search previous siblings for a match (general sibling `~`)
146
                let mut sibling = find_non_anonymous_prev_sibling(current_node, node_hierarchy, node_data);
147
                let mut found = false;
148
                while let Some(sib) = sibling {
149
                    if selector_group_matches(
150
                        content_group, &html_node_tree[sib], &node_data[sib], sib,
151
                        &expected_path_ending, is_last,
152
                    ) {
153
                        current_node = sib;
154
                        found = true;
155
                        break;
156
                    }
157
                    sibling = find_non_anonymous_prev_sibling(sib, node_hierarchy, node_data);
158
                }
159
                if !found {
160
                    return false;
161
                }
162
            }
163
        }
164
    }
165

            
166
38724
    true
167
779468
}
168

            
169
/// Find the first non-anonymous parent of a node.
170
102
fn find_non_anonymous_parent(
171
102
    node_id: NodeId,
172
102
    node_hierarchy: &NodeDataContainerRef<NodeHierarchyItem>,
173
102
    node_data: &NodeDataContainerRef<NodeData>,
174
102
) -> Option<NodeId> {
175
102
    let mut next = node_hierarchy[node_id].parent_id();
176
102
    while let Some(n) = next {
177
102
        if !node_data[n].is_anonymous() {
178
102
            return Some(n);
179
        }
180
        next = node_hierarchy[n].parent_id();
181
    }
182
    None
183
102
}
184

            
185
/// Find the first non-anonymous previous sibling of a node.
186
fn find_non_anonymous_prev_sibling(
187
    node_id: NodeId,
188
    node_hierarchy: &NodeDataContainerRef<NodeHierarchyItem>,
189
    node_data: &NodeDataContainerRef<NodeData>,
190
) -> Option<NodeId> {
191
    let mut next = node_hierarchy[node_id].previous_sibling_id();
192
    while let Some(n) = next {
193
        if !node_data[n].is_anonymous() {
194
            return Some(n);
195
        }
196
        next = node_hierarchy[n].previous_sibling_id();
197
    }
198
    None
199
}
200

            
201
/// A CSS group is a group of css selectors in a path that specify the rule that a
202
/// certain node has to match, i.e. "div.main.foo" has to match three requirements:
203
///
204
/// - the node has to be of type div
205
/// - the node has to have the class "main"
206
/// - the node has to have the class "foo"
207
///
208
/// If any of these requirements are not met, the CSS block is discarded.
209
///
210
/// The CssGroupIterator splits the CSS path into semantic blocks, i.e.:
211
///
212
/// "body > .foo.main > #baz" will be split into ["body", ".foo.main" and "#baz"]
213
pub struct CssGroupIterator<'a> {
214
    pub css_path: &'a [CssPathSelector],
215
    current_idx: usize,
216
    last_reason: CssGroupSplitReason,
217
}
218

            
219
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
220
pub enum CssGroupSplitReason {
221
    /// ".foo .main" - match any children
222
    Children,
223
    /// ".foo > .main" - match only direct children
224
    DirectChildren,
225
    /// ".foo + .main" - match adjacent sibling (immediately preceding)
226
    AdjacentSibling,
227
    /// ".foo ~ .main" - match general sibling (any preceding sibling)
228
    GeneralSibling,
229
}
230

            
231
impl<'a> CssGroupIterator<'a> {
232
779468
    pub fn new(css_path: &'a [CssPathSelector]) -> Self {
233
779468
        let initial_len = css_path.len();
234
779468
        Self {
235
779468
            css_path,
236
779468
            current_idx: initial_len,
237
779468
            last_reason: CssGroupSplitReason::Children,
238
779468
        }
239
779468
    }
240
}
241

            
242
impl<'a> Iterator for CssGroupIterator<'a> {
243
    type Item = (CssContentGroup<'a>, CssGroupSplitReason);
244

            
245
1559191
    fn next(&mut self) -> Option<(CssContentGroup<'a>, CssGroupSplitReason)> {
246
        use self::CssPathSelector::*;
247

            
248
1559191
        let mut new_idx = self.current_idx;
249

            
250
1559191
        if new_idx == 0 {
251
779468
            return None;
252
779723
        }
253

            
254
779723
        let mut current_path = Vec::new();
255

            
256
2293729
        while new_idx != 0 {
257
1514261
            match self.css_path.get(new_idx - 1)? {
258
                Children => {
259
255
                    self.last_reason = CssGroupSplitReason::Children;
260
255
                    break;
261
                }
262
                DirectChildren => {
263
                    self.last_reason = CssGroupSplitReason::DirectChildren;
264
                    break;
265
                }
266
                AdjacentSibling => {
267
                    self.last_reason = CssGroupSplitReason::AdjacentSibling;
268
                    break;
269
                }
270
                GeneralSibling => {
271
                    self.last_reason = CssGroupSplitReason::GeneralSibling;
272
                    break;
273
                }
274
1514006
                other => current_path.push(other),
275
            }
276
1514006
            new_idx -= 1;
277
        }
278

            
279
        // NOTE: Order inside of a ContentGroup is not important
280
        // for matching elements, only important for testing
281
        #[cfg(test)]
282
4
        current_path.reverse();
283

            
284
779723
        if new_idx == 0 {
285
779468
            if current_path.is_empty() {
286
                None
287
            } else {
288
                // Last element of path
289
779468
                self.current_idx = 0;
290
779468
                Some((current_path, self.last_reason))
291
            }
292
        } else {
293
            // skip the "Children | DirectChildren" element itself
294
255
            self.current_idx = new_idx - 1;
295
255
            Some((current_path, self.last_reason))
296
        }
297
1559191
    }
298
}
299

            
300
12360
pub fn construct_html_cascade_tree(
301
12360
    node_hierarchy: &NodeHierarchyRef,
302
12360
    node_depths_sorted: &[(usize, NodeId)],
303
12360
    node_data: &NodeDataContainerRef<NodeData>,
304
12360
) -> NodeDataContainer<CascadeInfo> {
305
12360
    let mut nodes = (0..node_hierarchy.len())
306
12360
        .map(|_| CascadeInfo {
307
            index_in_parent: 0,
308
            is_last_child: false,
309
85507
        })
310
12360
        .collect::<Vec<_>>();
311

            
312
61786
    for (_depth, parent_id) in node_depths_sorted {
313
        // Per CSS Selectors Level 4 §13: "Standalone text and other non-element
314
        // nodes are not counted when calculating the position of an element in
315
        // the list of children of its parent."
316
        //
317
        // We count only element siblings when computing index_in_parent.
318
49426
        let element_index_in_parent = parent_id
319
49426
            .preceding_siblings(node_hierarchy)
320
143858
            .filter(|sib_id| !node_data[*sib_id].is_text_node())
321
49426
            .count();
322

            
323
49426
        let parent_html_matcher = CascadeInfo {
324
49426
            index_in_parent: (element_index_in_parent.saturating_sub(1)) as u32,
325
            // Necessary for :last selectors — find last element sibling
326
            is_last_child: {
327
49426
                let mut is_last_element = true;
328
49426
                let mut next = node_hierarchy[*parent_id].next_sibling;
329
50943
                while let Some(sib_id) = next {
330
16332
                    if !node_data[sib_id].is_text_node() {
331
14815
                        is_last_element = false;
332
14815
                        break;
333
1517
                    }
334
1517
                    next = node_hierarchy[sib_id].next_sibling;
335
                }
336
49426
                is_last_element
337
            },
338
        };
339

            
340
49426
        nodes[parent_id.index()] = parent_html_matcher;
341

            
342
        // Count only element children for index_in_parent
343
49426
        let mut element_idx: u32 = 0;
344
73147
        for child_id in parent_id.children(node_hierarchy) {
345
73147
            let is_text = node_data[child_id].is_text_node();
346

            
347
            // Find whether this is the last element child (skip trailing text nodes)
348
73147
            let is_last_element_child = if is_text {
349
28368
                false
350
            } else {
351
44779
                let mut is_last = true;
352
44779
                let mut next = node_hierarchy[child_id].next_sibling;
353
49611
                while let Some(sib_id) = next {
354
22557
                    if !node_data[sib_id].is_text_node() {
355
17725
                        is_last = false;
356
17725
                        break;
357
4832
                    }
358
4832
                    next = node_hierarchy[sib_id].next_sibling;
359
                }
360
44779
                is_last
361
            };
362

            
363
73147
            let child_html_matcher = CascadeInfo {
364
73147
                index_in_parent: element_idx,
365
73147
                is_last_child: is_last_element_child,
366
73147
            };
367

            
368
73147
            nodes[child_id.index()] = child_html_matcher;
369

            
370
73147
            if !is_text {
371
44779
                element_idx += 1;
372
44779
            }
373
        }
374
    }
375

            
376
12360
    NodeDataContainer { internal: nodes }
377
12360
}
378

            
379
/// Checks whether the last selector in `path` matches the given pseudo-selector `target`.
380
///
381
/// Known limitation: this only inspects the final selector in the path, so compound
382
/// selectors like `div:hover:first-child` may not be filtered correctly when `target`
383
/// is `None` — only the very last pseudo-selector is tested.
384
#[inline]
385
1002570
pub fn rule_ends_with(path: &CssPath, target: Option<CssPathPseudoSelector>) -> bool {
386
    // Helper to check if a pseudo-selector is "interactive" (requires user interaction state)
387
    // vs "structural" (based on DOM structure only)
388
18360
    fn is_interactive_pseudo(p: &CssPathPseudoSelector) -> bool {
389
        matches!(
390
18360
            p,
391
            CssPathPseudoSelector::Hover
392
                | CssPathPseudoSelector::Active
393
                | CssPathPseudoSelector::Focus
394
                | CssPathPseudoSelector::Backdrop
395
                | CssPathPseudoSelector::Dragging
396
                | CssPathPseudoSelector::DragOver
397
        )
398
18360
    }
399

            
400
1002570
    match target {
401
788691
        None => match path.selectors.as_ref().last() {
402
            None => false,
403
788691
            Some(q) => match q {
404
                // Only reject interactive pseudo-selectors (hover, active, focus)
405
                // Structural pseudo-selectors (nth-child, first, last) should be allowed
406
18360
                CssPathSelector::PseudoSelector(p) => !is_interactive_pseudo(p),
407
770331
                _ => true,
408
            },
409
        },
410
213879
        Some(s) => match path.selectors.as_ref().last() {
411
            None => false,
412
213879
            Some(q) => match q {
413
22185
                CssPathSelector::PseudoSelector(q) => *q == s,
414
191694
                _ => false,
415
            },
416
        },
417
    }
418
1002570
}
419

            
420
/// Matches a single group of CSS selectors against a DOM node.
421
///
422
/// Returns true if all selectors in the group match the given node.
423
/// Combinator selectors (>, +, ~, space) should not appear in the group.
424
779570
fn selector_group_matches(
425
779570
    selectors: &[&CssPathSelector],
426
779570
    html_node: &CascadeInfo,
427
779570
    node_data: &NodeData,
428
779570
    node_id: NodeId,
429
779570
    expected_path_ending: &Option<CssPathPseudoSelector>,
430
779570
    is_last_content_group: bool,
431
779570
) -> bool {
432
1464562
    selectors.iter().all(|selector| {
433
1464562
        match_single_selector(
434
1464562
            selector,
435
1464562
            html_node,
436
1464562
            node_data,
437
1464562
            node_id,
438
1464562
            expected_path_ending,
439
1464562
            is_last_content_group,
440
        )
441
1464562
    })
442
779570
}
443

            
444
/// Matches a single CSS selector against a DOM node.
445
1464562
fn match_single_selector(
446
1464562
    selector: &CssPathSelector,
447
1464562
    html_node: &CascadeInfo,
448
1464562
    node_data: &NodeData,
449
1464562
    node_id: NodeId,
450
1464562
    expected_path_ending: &Option<CssPathPseudoSelector>,
451
1464562
    is_last_content_group: bool,
452
1464562
) -> bool {
453
    use self::CssPathSelector::*;
454

            
455
1464562
    match selector {
456
659384
        Global => true,
457
        // `Root(range)` (scope marker, #47): matches any node WITHIN the subtree
458
        // range `[start, end]`. The range is chosen when the scope is pushed
459
        // (`CssPath::push_front_scope`):
460
        //  - a bare-decl `with_css` rule (`* { … }`) is scoped node-only (`[start,
461
        //    start]`) → inline-style semantics: it applies to the OWNER only, so a
462
        //    non-root `background` can't leak to descendants/siblings (#47 leak fix).
463
        //  - a component rule with a real selector (`.menu-item`, from
464
        //    `add_component_css`) is scoped to the whole subtree (`[start, end]`) so
465
        //    its selector matches descendants of the owner (a menu container styling
466
        //    its `.menu-item` children). Compounded with the rest of the path,
467
        //    `[Root(range), Class(x)]` means "a node in range that also matches `.x`".
468
666613
        Root(range) => range.contains(node_id.index()),
469
19850
        Type(t) => node_data.get_node_type().get_path() == *t,
470
99624
        Class(c) => node_data.has_class(c.as_str()),
471
408
        Id(id) => node_data.has_id(id.as_str()),
472
18360
        PseudoSelector(p) => {
473
18360
            match_pseudo_selector(p, html_node, expected_path_ending, is_last_content_group)
474
        }
475
323
        Attribute(a) => match_attribute_selector(a, node_data),
476
        DirectChildren | Children | AdjacentSibling | GeneralSibling => false,
477
    }
478
1464562
}
479

            
480
/// Matches an attribute selector (`[name]`, `[name="v"]`, `[name~="v"]`, ...) against a node.
481
///
482
/// Some attributes (notably `class`) are stored as multiple separate entries in
483
/// `node_data.attributes()` rather than a single space-joined string. We collect
484
/// every matching value and treat the matcher as "any value satisfies the op",
485
/// so that `[class~="primary"]` matches a node with classes `foo primary bar`.
486
323
fn match_attribute_selector(sel: &CssAttributeSelector, node_data: &NodeData) -> bool {
487
323
    let name = sel.name.as_str();
488
323
    let target = sel.value.as_ref().map(|v| v.as_str());
489

            
490
323
    let check = |actual: &str| -> bool {
491
323
        match (&sel.op, target) {
492
19
            (AttributeMatchOp::Exists, _) => true,
493
57
            (AttributeMatchOp::Eq, Some(t)) => actual == t,
494
57
            (AttributeMatchOp::Includes, Some(t)) => {
495
57
                if t.is_empty() || t.contains(char::is_whitespace) {
496
                    return false;
497
57
                }
498
57
                actual.split_whitespace().any(|word| word == t)
499
            }
500
57
            (AttributeMatchOp::DashMatch, Some(t)) => {
501
57
                actual == t || actual.starts_with(&alloc::format!("{}-", t))
502
            }
503
57
            (AttributeMatchOp::Prefix, Some(t)) => !t.is_empty() && actual.starts_with(t),
504
38
            (AttributeMatchOp::Suffix, Some(t)) => !t.is_empty() && actual.ends_with(t),
505
38
            (AttributeMatchOp::Substring, Some(t)) => !t.is_empty() && actual.contains(t),
506
            // Operator with a missing value (parser should reject these — be defensive).
507
            (_, None) => false,
508
        }
509
323
    };
510

            
511
323
    for attr in node_data.attributes().iter() {
512
323
        if attr.name() != name {
513
            continue;
514
323
        }
515
323
        if check(attr.value().as_str()) {
516
171
            return true;
517
152
        }
518
    }
519

            
520
152
    false
521
323
}
522

            
523
/// Matches a pseudo-selector (:first, :last, :nth-child, :hover, etc.) against a node.
524
18360
fn match_pseudo_selector(
525
18360
    pseudo: &CssPathPseudoSelector,
526
18360
    html_node: &CascadeInfo,
527
18360
    expected_path_ending: &Option<CssPathPseudoSelector>,
528
18360
    is_last_content_group: bool,
529
18360
) -> bool {
530
18360
    match pseudo {
531
        CssPathPseudoSelector::First => match_first_child(html_node),
532
        CssPathPseudoSelector::Last => match_last_child(html_node),
533
        CssPathPseudoSelector::NthChild(pattern) => match_nth_child(html_node, pattern),
534
18360
        CssPathPseudoSelector::Hover => match_interactive_pseudo(
535
18360
            &CssPathPseudoSelector::Hover,
536
18360
            expected_path_ending,
537
18360
            is_last_content_group,
538
        ),
539
        CssPathPseudoSelector::Active => match_interactive_pseudo(
540
            &CssPathPseudoSelector::Active,
541
            expected_path_ending,
542
            is_last_content_group,
543
        ),
544
        CssPathPseudoSelector::Focus => match_interactive_pseudo(
545
            &CssPathPseudoSelector::Focus,
546
            expected_path_ending,
547
            is_last_content_group,
548
        ),
549
        CssPathPseudoSelector::Backdrop => match_interactive_pseudo(
550
            &CssPathPseudoSelector::Backdrop,
551
            expected_path_ending,
552
            is_last_content_group,
553
        ),
554
        CssPathPseudoSelector::Dragging => match_interactive_pseudo(
555
            &CssPathPseudoSelector::Dragging,
556
            expected_path_ending,
557
            is_last_content_group,
558
        ),
559
        CssPathPseudoSelector::DragOver => match_interactive_pseudo(
560
            &CssPathPseudoSelector::DragOver,
561
            expected_path_ending,
562
            is_last_content_group,
563
        ),
564
        CssPathPseudoSelector::Lang(lang) => {
565
            // :lang() is matched via DynamicSelector at runtime, not during CSS cascade
566
            // During cascade, we just check if this is the expected ending
567
            if let Some(expected) = expected_path_ending {
568
                if let CssPathPseudoSelector::Lang(expected_lang) = expected {
569
                    return lang == expected_lang;
570
                }
571
            }
572
            // If not specifically looking for :lang, it doesn't match structurally
573
            false
574
        }
575
    }
576
18360
}
577

            
578
/// Returns true if the node is the first child of its parent.
579
fn match_first_child(html_node: &CascadeInfo) -> bool {
580
    html_node.index_in_parent == 0
581
}
582

            
583
/// Returns true if the node is the last child of its parent.
584
fn match_last_child(html_node: &CascadeInfo) -> bool {
585
    html_node.is_last_child
586
}
587

            
588
/// Matches :nth-child(n), :nth-child(even), :nth-child(odd), or :nth-child(An+B) patterns.
589
fn match_nth_child(html_node: &CascadeInfo, pattern: &CssNthChildSelector) -> bool {
590
    use azul_css::css::CssNthChildPattern;
591

            
592
    // nth-child is 1-indexed, index_in_parent is 0-indexed
593
    let index = html_node.index_in_parent + 1;
594

            
595
    match pattern {
596
        Number(n) => index == *n,
597
        Even => index % 2 == 0,
598
        Odd => index % 2 == 1,
599
        Pattern(CssNthChildPattern {
600
            pattern_repeat,
601
            offset,
602
        }) => {
603
            if *pattern_repeat == 0 {
604
                index == *offset
605
            } else {
606
                index >= *offset && ((index - offset) % pattern_repeat == 0)
607
            }
608
        }
609
    }
610
}
611

            
612
/// Matches interactive pseudo-selectors (:hover, :active, :focus).
613
/// These only apply if they appear in the last content group of the CSS path.
614
18360
fn match_interactive_pseudo(
615
18360
    pseudo: &CssPathPseudoSelector,
616
18360
    expected_path_ending: &Option<CssPathPseudoSelector>,
617
18360
    is_last_content_group: bool,
618
18360
) -> bool {
619
18360
    is_last_content_group && expected_path_ending.as_ref() == Some(pseudo)
620
18360
}