1use alloc::vec::Vec;
8
9use azul_css::css::{
10 AttributeMatchOp, CssAttributeSelector, CssContentGroup, CssNthChildSelector,
11 CssNthChildSelector::{Even, Number, Odd, Pattern},
12 CssPath, CssPathPseudoSelector, CssPathSelector,
13};
14
15use crate::{
16 dom::NodeData,
17 id::{NodeDataContainer, NodeDataContainerRef, NodeHierarchyRef, NodeId},
18 styled_dom::NodeHierarchyItem,
19};
20
21#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
23#[repr(C)]
24pub struct CascadeInfo {
25 pub index_in_parent: u32,
26 pub is_last_child: bool,
27}
28
29impl_option!(
30 CascadeInfo,
31 OptionCascadeInfo,
32 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
33);
34
35impl_vec!(
36 CascadeInfo,
37 CascadeInfoVec,
38 CascadeInfoVecDestructor,
39 CascadeInfoVecDestructorType,
40 CascadeInfoVecSlice,
41 OptionCascadeInfo
42);
43impl_vec_mut!(CascadeInfo, CascadeInfoVec);
44impl_vec_debug!(CascadeInfo, CascadeInfoVec);
45impl_vec_partialord!(CascadeInfo, CascadeInfoVec);
46impl_vec_clone!(CascadeInfo, CascadeInfoVec, CascadeInfoVecDestructor);
47impl_vec_partialeq!(CascadeInfo, CascadeInfoVec);
48
49impl CascadeInfoVec {
50 #[must_use]
51 pub fn as_container(&self) -> NodeDataContainerRef<'_, CascadeInfo> {
52 NodeDataContainerRef {
53 internal: self.as_ref(),
54 }
55 }
56}
57
58#[allow(clippy::needless_pass_by_value)] #[allow(clippy::too_many_lines)] #[must_use]
63pub fn matches_html_element(
64 css_path: &CssPath,
65 node_id: NodeId,
66 node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
67 node_data: &NodeDataContainerRef<'_, NodeData>,
68 html_node_tree: &NodeDataContainerRef<'_, CascadeInfo>,
69 expected_path_ending: Option<CssPathPseudoSelector>,
70) -> bool {
71 use self::CssGroupSplitReason::{AdjacentSibling, Children, DirectChildren, GeneralSibling};
72
73 if css_path.selectors.is_empty() {
74 return false;
75 }
76
77 if node_data[node_id].is_anonymous() {
80 return false;
81 }
82
83 let groups: Vec<(CssContentGroup<'_>, CssGroupSplitReason)> =
85 CssGroupIterator::new(css_path.selectors.as_ref()).collect();
86
87 if groups.is_empty() {
88 return false;
89 }
90
91 let (ref first_group, first_reason) = groups[0];
93 let is_last_content_group = true;
98 if !selector_group_matches(
99 first_group,
100 html_node_tree[node_id],
101 &node_data[node_id],
102 node_id,
103 expected_path_ending.as_ref(),
104 is_last_content_group,
105 ) {
106 return false;
107 }
108
109 let mut current_node = node_id;
112
113 for (group_idx, (content_group, _reason)) in groups.iter().enumerate().skip(1) {
114 let combinator = groups[group_idx - 1].1;
116 let is_last = group_idx == groups.len() - 1;
117
118 match combinator {
119 DirectChildren => {
120 let parent = find_non_anonymous_parent(current_node, node_hierarchy, node_data);
122 match parent {
123 Some(p)
124 if selector_group_matches(
125 content_group,
126 html_node_tree[p],
127 &node_data[p],
128 p,
129 expected_path_ending.as_ref(),
130 is_last,
131 ) =>
132 {
133 current_node = p;
134 }
135 _ => return false,
136 }
137 }
138 Children => {
139 let mut ancestor =
141 find_non_anonymous_parent(current_node, node_hierarchy, node_data);
142 let mut found = false;
143 while let Some(anc) = ancestor {
144 if selector_group_matches(
145 content_group,
146 html_node_tree[anc],
147 &node_data[anc],
148 anc,
149 expected_path_ending.as_ref(),
150 is_last,
151 ) {
152 current_node = anc;
153 found = true;
154 break;
155 }
156 ancestor = find_non_anonymous_parent(anc, node_hierarchy, node_data);
157 }
158 if !found {
159 return false;
160 }
161 }
162 AdjacentSibling => {
163 let sibling =
165 find_non_anonymous_prev_sibling(current_node, node_hierarchy, node_data);
166 match sibling {
167 Some(s)
168 if selector_group_matches(
169 content_group,
170 html_node_tree[s],
171 &node_data[s],
172 s,
173 expected_path_ending.as_ref(),
174 is_last,
175 ) =>
176 {
177 current_node = s;
178 }
179 _ => return false,
180 }
181 }
182 GeneralSibling => {
183 let mut sibling =
185 find_non_anonymous_prev_sibling(current_node, node_hierarchy, node_data);
186 let mut found = false;
187 while let Some(sib) = sibling {
188 if selector_group_matches(
189 content_group,
190 html_node_tree[sib],
191 &node_data[sib],
192 sib,
193 expected_path_ending.as_ref(),
194 is_last,
195 ) {
196 current_node = sib;
197 found = true;
198 break;
199 }
200 sibling = find_non_anonymous_prev_sibling(sib, node_hierarchy, node_data);
201 }
202 if !found {
203 return false;
204 }
205 }
206 }
207 }
208
209 true
210}
211
212fn find_non_anonymous_parent(
214 node_id: NodeId,
215 node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
216 node_data: &NodeDataContainerRef<'_, NodeData>,
217) -> Option<NodeId> {
218 let mut next = node_hierarchy[node_id].parent_id();
219 while let Some(n) = next {
220 if !node_data[n].is_anonymous() {
221 return Some(n);
222 }
223 next = node_hierarchy[n].parent_id();
224 }
225 None
226}
227
228fn find_non_anonymous_prev_sibling(
235 node_id: NodeId,
236 node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
237 node_data: &NodeDataContainerRef<'_, NodeData>,
238) -> Option<NodeId> {
239 let mut next = node_hierarchy[node_id].previous_sibling_id();
240 while let Some(n) = next {
241 if !node_data[n].is_anonymous() && !node_data[n].is_text_node() {
242 return Some(n);
243 }
244 next = node_hierarchy[n].previous_sibling_id();
245 }
246 None
247}
248
249#[derive(Debug)]
262pub struct CssGroupIterator<'a> {
263 pub css_path: &'a [CssPathSelector],
264 current_idx: usize,
265 last_reason: CssGroupSplitReason,
266}
267
268#[derive(Debug, Copy, Clone, PartialEq, Eq)]
269pub enum CssGroupSplitReason {
270 Children,
272 DirectChildren,
274 AdjacentSibling,
276 GeneralSibling,
278}
279
280impl<'a> CssGroupIterator<'a> {
281 #[must_use]
282 pub const fn new(css_path: &'a [CssPathSelector]) -> Self {
283 let initial_len = css_path.len();
284 Self {
285 css_path,
286 current_idx: initial_len,
287 last_reason: CssGroupSplitReason::Children,
288 }
289 }
290}
291
292impl<'a> Iterator for CssGroupIterator<'a> {
293 type Item = (CssContentGroup<'a>, CssGroupSplitReason);
294
295 fn next(&mut self) -> Option<(CssContentGroup<'a>, CssGroupSplitReason)> {
296 use self::CssPathSelector::{AdjacentSibling, Children, DirectChildren, GeneralSibling};
297
298 let mut new_idx = self.current_idx;
299
300 if new_idx == 0 {
301 return None;
302 }
303
304 let mut current_path = Vec::new();
305
306 while new_idx != 0 {
307 match self.css_path.get(new_idx - 1)? {
308 Children => {
309 self.last_reason = CssGroupSplitReason::Children;
310 break;
311 }
312 DirectChildren => {
313 self.last_reason = CssGroupSplitReason::DirectChildren;
314 break;
315 }
316 AdjacentSibling => {
317 self.last_reason = CssGroupSplitReason::AdjacentSibling;
318 break;
319 }
320 GeneralSibling => {
321 self.last_reason = CssGroupSplitReason::GeneralSibling;
322 break;
323 }
324 other => current_path.push(other),
325 }
326 new_idx -= 1;
327 }
328
329 #[cfg(test)]
332 current_path.reverse();
333
334 if new_idx == 0 {
335 if current_path.is_empty() {
336 None
337 } else {
338 self.current_idx = 0;
340 Some((current_path, self.last_reason))
341 }
342 } else {
343 self.current_idx = new_idx - 1;
345 Some((current_path, self.last_reason))
346 }
347 }
348}
349
350#[must_use]
351pub fn construct_html_cascade_tree(
352 node_hierarchy: &NodeHierarchyRef<'_>,
353 node_depths_sorted: &[(usize, NodeId)],
354 node_data: &NodeDataContainerRef<'_, NodeData>,
355) -> NodeDataContainer<CascadeInfo> {
356 let mut nodes = (0..node_hierarchy.len())
357 .map(|_| CascadeInfo {
358 index_in_parent: 0,
359 is_last_child: false,
360 })
361 .collect::<Vec<_>>();
362
363 for (_depth, parent_id) in node_depths_sorted {
364 let element_index_in_parent = parent_id
370 .preceding_siblings(node_hierarchy)
371 .filter(|sib_id| !node_data[*sib_id].is_text_node())
372 .count();
373
374 let parent_html_matcher = CascadeInfo {
375 index_in_parent: u32::try_from(element_index_in_parent.saturating_sub(1))
376 .unwrap_or(u32::MAX),
377 is_last_child: {
379 let mut is_last_element = true;
380 let mut next = node_hierarchy[*parent_id].next_sibling;
381 while let Some(sib_id) = next {
382 if !node_data[sib_id].is_text_node() {
383 is_last_element = false;
384 break;
385 }
386 next = node_hierarchy[sib_id].next_sibling;
387 }
388 is_last_element
389 },
390 };
391
392 nodes[parent_id.index()] = parent_html_matcher;
393
394 let mut element_idx: u32 = 0;
396 for child_id in parent_id.children(node_hierarchy) {
397 let is_text = node_data[child_id].is_text_node();
398
399 let is_last_element_child = if is_text {
401 false
402 } else {
403 let mut is_last = true;
404 let mut next = node_hierarchy[child_id].next_sibling;
405 while let Some(sib_id) = next {
406 if !node_data[sib_id].is_text_node() {
407 is_last = false;
408 break;
409 }
410 next = node_hierarchy[sib_id].next_sibling;
411 }
412 is_last
413 };
414
415 let child_html_matcher = CascadeInfo {
416 index_in_parent: element_idx,
417 is_last_child: is_last_element_child,
418 };
419
420 nodes[child_id.index()] = child_html_matcher;
421
422 if !is_text {
423 element_idx += 1;
424 }
425 }
426 }
427
428 NodeDataContainer { internal: nodes }
429}
430
431#[inline]
437#[must_use]
438pub fn rule_ends_with(path: &CssPath, target: Option<CssPathPseudoSelector>) -> bool {
439 const fn is_interactive_pseudo(p: &CssPathPseudoSelector) -> bool {
442 matches!(
443 p,
444 CssPathPseudoSelector::Hover
445 | CssPathPseudoSelector::Active
446 | CssPathPseudoSelector::Focus
447 | CssPathPseudoSelector::SeatFocus
448 | CssPathPseudoSelector::Backdrop
449 | CssPathPseudoSelector::Dragging
450 | CssPathPseudoSelector::DragOver
451 | CssPathPseudoSelector::Placeholder
452 )
453 }
454
455 let Some(last) = path.selectors.as_ref().last() else {
456 return false;
457 };
458 target.map_or_else(
459 || match last {
460 CssPathSelector::PseudoSelector(p) => !is_interactive_pseudo(p),
463 _ => true,
464 },
465 |s| matches!(last, CssPathSelector::PseudoSelector(q) if *q == s),
466 )
467}
468
469fn selector_group_matches(
474 selectors: &[&CssPathSelector],
475 html_node: CascadeInfo,
476 node_data: &NodeData,
477 node_id: NodeId,
478 expected_path_ending: Option<&CssPathPseudoSelector>,
479 is_last_content_group: bool,
480) -> bool {
481 let node_scoped_to_self = selectors.iter().any(|s| {
487 matches!(s, CssPathSelector::Root(r)
488 if r.start == r.end && r.start == node_id.index())
489 });
490 selectors.iter().all(|selector| {
491 match_single_selector(
492 selector,
493 html_node,
494 node_data,
495 node_id,
496 expected_path_ending,
497 is_last_content_group,
498 node_scoped_to_self,
499 )
500 })
501}
502
503fn match_single_selector(
505 selector: &CssPathSelector,
506 html_node: CascadeInfo,
507 node_data: &NodeData,
508 node_id: NodeId,
509 expected_path_ending: Option<&CssPathPseudoSelector>,
510 is_last_content_group: bool,
511 node_scoped_to_self: bool,
512) -> bool {
513 use self::CssPathSelector::{
514 AdjacentSibling, Attribute, Children, Class, DirectChildren, GeneralSibling, Global, Id,
515 PseudoSelector, Root, Type,
516 };
517
518 match selector {
519 Global => !node_data.is_text_node() || node_scoped_to_self,
529 Root(range) => range.contains(node_id.index()),
541 Type(t) => node_data.get_node_type().get_path() == *t,
542 Class(c) => node_data.has_class(c.as_str()),
543 Id(id) => node_data.has_id(id.as_str()),
544 PseudoSelector(CssPathPseudoSelector::Root) => node_id.index() == 0,
548 PseudoSelector(p) => {
549 match_pseudo_selector(p, html_node, expected_path_ending, is_last_content_group)
550 }
551 Attribute(a) => match_attribute_selector(a, node_data),
552 DirectChildren | Children | AdjacentSibling | GeneralSibling => false,
553 }
554}
555
556fn match_attribute_selector(sel: &CssAttributeSelector, node_data: &NodeData) -> bool {
563 let name = sel.name.as_str();
564 let target = sel.value.as_ref().map(azul_css::AzString::as_str);
565
566 let check = |actual: &str| -> bool {
567 match (&sel.op, target) {
568 (AttributeMatchOp::Exists, _) => true,
569 (AttributeMatchOp::Eq, Some(t)) => actual == t,
570 (AttributeMatchOp::Includes, Some(t)) => {
571 if t.is_empty() || t.contains(char::is_whitespace) {
572 return false;
573 }
574 actual.split_whitespace().any(|word| word == t)
575 }
576 (AttributeMatchOp::DashMatch, Some(t)) => {
577 actual == t || actual.starts_with(&alloc::format!("{t}-"))
578 }
579 (AttributeMatchOp::Prefix, Some(t)) => !t.is_empty() && actual.starts_with(t),
580 (AttributeMatchOp::Suffix, Some(t)) => !t.is_empty() && actual.ends_with(t),
581 (AttributeMatchOp::Substring, Some(t)) => !t.is_empty() && actual.contains(t),
582 (_, None) => false,
584 }
585 };
586
587 for attr in node_data.attributes() {
588 if attr.name() != name {
589 continue;
590 }
591 if check(attr.value().as_str()) {
592 return true;
593 }
594 }
595
596 false
597}
598
599fn match_pseudo_selector(
601 pseudo: &CssPathPseudoSelector,
602 html_node: CascadeInfo,
603 expected_path_ending: Option<&CssPathPseudoSelector>,
604 is_last_content_group: bool,
605) -> bool {
606 match pseudo {
607 CssPathPseudoSelector::First => match_first_child(html_node),
608 CssPathPseudoSelector::Last => match_last_child(html_node),
609 CssPathPseudoSelector::NthChild(pattern) => match_nth_child(html_node, pattern),
610 CssPathPseudoSelector::Hover => match_interactive_pseudo(
611 &CssPathPseudoSelector::Hover,
612 expected_path_ending,
613 is_last_content_group,
614 ),
615 CssPathPseudoSelector::Active => match_interactive_pseudo(
616 &CssPathPseudoSelector::Active,
617 expected_path_ending,
618 is_last_content_group,
619 ),
620 CssPathPseudoSelector::Focus => match_interactive_pseudo(
621 &CssPathPseudoSelector::Focus,
622 expected_path_ending,
623 is_last_content_group,
624 ),
625 CssPathPseudoSelector::SeatFocus => match_interactive_pseudo(
626 &CssPathPseudoSelector::SeatFocus,
627 expected_path_ending,
628 is_last_content_group,
629 ),
630 CssPathPseudoSelector::Backdrop => match_interactive_pseudo(
631 &CssPathPseudoSelector::Backdrop,
632 expected_path_ending,
633 is_last_content_group,
634 ),
635 CssPathPseudoSelector::Dragging => match_interactive_pseudo(
636 &CssPathPseudoSelector::Dragging,
637 expected_path_ending,
638 is_last_content_group,
639 ),
640 CssPathPseudoSelector::DragOver => match_interactive_pseudo(
641 &CssPathPseudoSelector::DragOver,
642 expected_path_ending,
643 is_last_content_group,
644 ),
645 CssPathPseudoSelector::Lang(lang) => {
646 if let Some(CssPathPseudoSelector::Lang(expected_lang)) = expected_path_ending {
649 return lang == expected_lang;
650 }
651 false
653 }
654 CssPathPseudoSelector::Placeholder => match_interactive_pseudo(
658 &CssPathPseudoSelector::Placeholder,
659 expected_path_ending,
660 is_last_content_group,
661 ),
662 CssPathPseudoSelector::Root => false,
665 }
666}
667
668const fn match_first_child(html_node: CascadeInfo) -> bool {
670 html_node.index_in_parent == 0
671}
672
673const fn match_last_child(html_node: CascadeInfo) -> bool {
675 html_node.is_last_child
676}
677
678fn match_nth_child(html_node: CascadeInfo, pattern: &CssNthChildSelector) -> bool {
680 use azul_css::css::CssNthChildPattern;
681
682 let index = html_node.index_in_parent + 1;
684
685 match pattern {
686 Number(n) => index == *n,
687 Even => index.is_multiple_of(2),
688 Odd => index % 2 == 1,
689 Pattern(CssNthChildPattern {
690 pattern_repeat,
691 offset,
692 }) => {
693 if *pattern_repeat == 0 {
694 index == *offset
695 } else {
696 index >= *offset && (index - offset).is_multiple_of(*pattern_repeat)
697 }
698 }
699 }
700}
701
702fn match_interactive_pseudo(
705 pseudo: &CssPathPseudoSelector,
706 expected_path_ending: Option<&CssPathPseudoSelector>,
707 is_last_content_group: bool,
708) -> bool {
709 is_last_content_group && expected_path_ending == Some(pseudo)
710}
711
712#[cfg(test)]
713#[path = "style_test.rs"]
714mod style_test;