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
63041
pub fn matches_html_element(
52
63041
    css_path: &CssPath,
53
63041
    node_id: NodeId,
54
63041
    node_hierarchy: &NodeDataContainerRef<NodeHierarchyItem>,
55
63041
    node_data: &NodeDataContainerRef<NodeData>,
56
63041
    html_node_tree: &NodeDataContainerRef<CascadeInfo>,
57
63041
    expected_path_ending: Option<CssPathPseudoSelector>,
58
63041
) -> bool {
59
    use self::CssGroupSplitReason::*;
60

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

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

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

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

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

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

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

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

            
165
11107
    true
166
63041
}
167

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

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

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

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

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

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

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

            
247
126292
        let mut new_idx = self.current_idx;
248

            
249
126292
        if new_idx == 0 {
250
63041
            return None;
251
63251
        }
252

            
253
63251
        let mut current_path = Vec::new();
254

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

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

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

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

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

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

            
339
27652
        nodes[parent_id.index()] = parent_html_matcher;
340

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

            
346
            // Find whether this is the last element child (skip trailing text nodes)
347
37052
            let is_last_element_child = if is_text {
348
12687
                false
349
            } else {
350
24365
                let mut is_last = true;
351
24365
                let mut next = node_hierarchy[child_id].next_sibling;
352
28179
                while let Some(sib_id) = next {
353
8734
                    if !node_data[sib_id].is_text_node() {
354
4920
                        is_last = false;
355
4920
                        break;
356
3814
                    }
357
3814
                    next = node_hierarchy[sib_id].next_sibling;
358
                }
359
24365
                is_last
360
            };
361

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

            
367
37052
            nodes[child_id.index()] = child_html_matcher;
368

            
369
37052
            if !is_text {
370
24365
                element_idx += 1;
371
24365
            }
372
        }
373
    }
374

            
375
8970
    NodeDataContainer { internal: nodes }
376
8970
}
377

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

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

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

            
441
/// Matches a single CSS selector against a DOM node.
442
63142
fn match_single_selector(
443
63142
    selector: &CssPathSelector,
444
63142
    html_node: &CascadeInfo,
445
63142
    node_data: &NodeData,
446
63142
    expected_path_ending: &Option<CssPathPseudoSelector>,
447
63142
    is_last_content_group: bool,
448
63142
) -> bool {
449
    use self::CssPathSelector::*;
450

            
451
63142
    match selector {
452
        Global => true,
453
16388
        Type(t) => node_data.get_node_type().get_path() == *t,
454
46129
        Class(c) => node_data.has_class(c.as_str()),
455
336
        Id(id) => node_data.has_id(id.as_str()),
456
        PseudoSelector(p) => {
457
            match_pseudo_selector(p, html_node, expected_path_ending, is_last_content_group)
458
        }
459
289
        Attribute(a) => match_attribute_selector(a, node_data),
460
        DirectChildren | Children | AdjacentSibling | GeneralSibling => false,
461
    }
462
63142
}
463

            
464
/// Matches an attribute selector (`[name]`, `[name="v"]`, `[name~="v"]`, ...) against a node.
465
///
466
/// Some attributes (notably `class`) are stored as multiple separate entries in
467
/// `node_data.attributes()` rather than a single space-joined string. We collect
468
/// every matching value and treat the matcher as "any value satisfies the op",
469
/// so that `[class~="primary"]` matches a node with classes `foo primary bar`.
470
289
fn match_attribute_selector(sel: &CssAttributeSelector, node_data: &NodeData) -> bool {
471
289
    let name = sel.name.as_str();
472
289
    let target = sel.value.as_ref().map(|v| v.as_str());
473

            
474
289
    let check = |actual: &str| -> bool {
475
289
        match (&sel.op, target) {
476
17
            (AttributeMatchOp::Exists, _) => true,
477
51
            (AttributeMatchOp::Eq, Some(t)) => actual == t,
478
51
            (AttributeMatchOp::Includes, Some(t)) => {
479
51
                if t.is_empty() || t.contains(char::is_whitespace) {
480
                    return false;
481
51
                }
482
51
                actual.split_whitespace().any(|word| word == t)
483
            }
484
51
            (AttributeMatchOp::DashMatch, Some(t)) => {
485
51
                actual == t || actual.starts_with(&alloc::format!("{}-", t))
486
            }
487
51
            (AttributeMatchOp::Prefix, Some(t)) => !t.is_empty() && actual.starts_with(t),
488
34
            (AttributeMatchOp::Suffix, Some(t)) => !t.is_empty() && actual.ends_with(t),
489
34
            (AttributeMatchOp::Substring, Some(t)) => !t.is_empty() && actual.contains(t),
490
            // Operator with a missing value (parser should reject these — be defensive).
491
            (_, None) => false,
492
        }
493
289
    };
494

            
495
289
    for attr in node_data.attributes().iter() {
496
289
        if attr.name() != name {
497
            continue;
498
289
        }
499
289
        if check(attr.value().as_str()) {
500
153
            return true;
501
136
        }
502
    }
503

            
504
136
    false
505
289
}
506

            
507
/// Matches a pseudo-selector (:first, :last, :nth-child, :hover, etc.) against a node.
508
fn match_pseudo_selector(
509
    pseudo: &CssPathPseudoSelector,
510
    html_node: &CascadeInfo,
511
    expected_path_ending: &Option<CssPathPseudoSelector>,
512
    is_last_content_group: bool,
513
) -> bool {
514
    match pseudo {
515
        CssPathPseudoSelector::First => match_first_child(html_node),
516
        CssPathPseudoSelector::Last => match_last_child(html_node),
517
        CssPathPseudoSelector::NthChild(pattern) => match_nth_child(html_node, pattern),
518
        CssPathPseudoSelector::Hover => match_interactive_pseudo(
519
            &CssPathPseudoSelector::Hover,
520
            expected_path_ending,
521
            is_last_content_group,
522
        ),
523
        CssPathPseudoSelector::Active => match_interactive_pseudo(
524
            &CssPathPseudoSelector::Active,
525
            expected_path_ending,
526
            is_last_content_group,
527
        ),
528
        CssPathPseudoSelector::Focus => match_interactive_pseudo(
529
            &CssPathPseudoSelector::Focus,
530
            expected_path_ending,
531
            is_last_content_group,
532
        ),
533
        CssPathPseudoSelector::Backdrop => match_interactive_pseudo(
534
            &CssPathPseudoSelector::Backdrop,
535
            expected_path_ending,
536
            is_last_content_group,
537
        ),
538
        CssPathPseudoSelector::Dragging => match_interactive_pseudo(
539
            &CssPathPseudoSelector::Dragging,
540
            expected_path_ending,
541
            is_last_content_group,
542
        ),
543
        CssPathPseudoSelector::DragOver => match_interactive_pseudo(
544
            &CssPathPseudoSelector::DragOver,
545
            expected_path_ending,
546
            is_last_content_group,
547
        ),
548
        CssPathPseudoSelector::Lang(lang) => {
549
            // :lang() is matched via DynamicSelector at runtime, not during CSS cascade
550
            // During cascade, we just check if this is the expected ending
551
            if let Some(expected) = expected_path_ending {
552
                if let CssPathPseudoSelector::Lang(expected_lang) = expected {
553
                    return lang == expected_lang;
554
                }
555
            }
556
            // If not specifically looking for :lang, it doesn't match structurally
557
            false
558
        }
559
    }
560
}
561

            
562
/// Returns true if the node is the first child of its parent.
563
fn match_first_child(html_node: &CascadeInfo) -> bool {
564
    html_node.index_in_parent == 0
565
}
566

            
567
/// Returns true if the node is the last child of its parent.
568
fn match_last_child(html_node: &CascadeInfo) -> bool {
569
    html_node.is_last_child
570
}
571

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

            
576
    // nth-child is 1-indexed, index_in_parent is 0-indexed
577
    let index = html_node.index_in_parent + 1;
578

            
579
    match pattern {
580
        Number(n) => index == *n,
581
        Even => index % 2 == 0,
582
        Odd => index % 2 == 1,
583
        Pattern(CssNthChildPattern {
584
            pattern_repeat,
585
            offset,
586
        }) => {
587
            if *pattern_repeat == 0 {
588
                index == *offset
589
            } else {
590
                index >= *offset && ((index - offset) % pattern_repeat == 0)
591
            }
592
        }
593
    }
594
}
595

            
596
/// Matches interactive pseudo-selectors (:hover, :active, :focus).
597
/// These only apply if they appear in the last content group of the CSS path.
598
fn match_interactive_pseudo(
599
    pseudo: &CssPathPseudoSelector,
600
    expected_path_ending: &Option<CssPathPseudoSelector>,
601
    is_last_content_group: bool,
602
) -> bool {
603
    is_last_content_group && expected_path_ending.as_ref() == Some(pseudo)
604
}