use alloc::vec::Vec;
use azul_css::css::{
AttributeMatchOp, CssAttributeSelector, CssContentGroup, CssNthChildSelector,
CssNthChildSelector::{Number, Even, Odd, Pattern}, CssPath, CssPathPseudoSelector, CssPathSelector,
};
use crate::{
dom::NodeData,
id::{NodeDataContainer, NodeDataContainerRef, NodeHierarchyRef, NodeId},
styled_dom::NodeHierarchyItem,
};
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct CascadeInfo {
pub index_in_parent: u32,
pub is_last_child: bool,
}
impl_option!(
CascadeInfo,
OptionCascadeInfo,
[Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
);
impl_vec!(CascadeInfo, CascadeInfoVec, CascadeInfoVecDestructor, CascadeInfoVecDestructorType, CascadeInfoVecSlice, OptionCascadeInfo);
impl_vec_mut!(CascadeInfo, CascadeInfoVec);
impl_vec_debug!(CascadeInfo, CascadeInfoVec);
impl_vec_partialord!(CascadeInfo, CascadeInfoVec);
impl_vec_clone!(CascadeInfo, CascadeInfoVec, CascadeInfoVecDestructor);
impl_vec_partialeq!(CascadeInfo, CascadeInfoVec);
impl CascadeInfoVec {
#[must_use] pub fn as_container(&self) -> NodeDataContainerRef<'_, CascadeInfo> {
NodeDataContainerRef {
internal: self.as_ref(),
}
}
}
#[allow(clippy::needless_pass_by_value)] #[must_use] pub fn matches_html_element(
css_path: &CssPath,
node_id: NodeId,
node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
node_data: &NodeDataContainerRef<'_, NodeData>,
html_node_tree: &NodeDataContainerRef<'_, CascadeInfo>,
expected_path_ending: Option<CssPathPseudoSelector>,
) -> bool {
use self::CssGroupSplitReason::{DirectChildren, Children, AdjacentSibling, GeneralSibling};
if css_path.selectors.is_empty() {
return false;
}
if node_data[node_id].is_anonymous() {
return false;
}
let groups: Vec<(CssContentGroup<'_>, CssGroupSplitReason)> =
CssGroupIterator::new(css_path.selectors.as_ref()).collect();
if groups.is_empty() {
return false;
}
let (ref first_group, first_reason) = groups[0];
let is_last_content_group = groups.len() == 1;
if !selector_group_matches(
first_group,
html_node_tree[node_id],
&node_data[node_id],
node_id,
expected_path_ending.as_ref(),
is_last_content_group,
) {
return false;
}
let mut current_node = node_id;
for (group_idx, (content_group, _reason)) in groups.iter().enumerate().skip(1) {
let combinator = groups[group_idx - 1].1;
let is_last = group_idx == groups.len() - 1;
match combinator {
DirectChildren => {
let parent = find_non_anonymous_parent(current_node, node_hierarchy, node_data);
match parent {
Some(p) if selector_group_matches(
content_group, html_node_tree[p], &node_data[p], p,
expected_path_ending.as_ref(), is_last,
) => { current_node = p; }
_ => return false,
}
}
Children => {
let mut ancestor = find_non_anonymous_parent(current_node, node_hierarchy, node_data);
let mut found = false;
while let Some(anc) = ancestor {
if selector_group_matches(
content_group, html_node_tree[anc], &node_data[anc], anc,
expected_path_ending.as_ref(), is_last,
) {
current_node = anc;
found = true;
break;
}
ancestor = find_non_anonymous_parent(anc, node_hierarchy, node_data);
}
if !found {
return false;
}
}
AdjacentSibling => {
let sibling = find_non_anonymous_prev_sibling(current_node, node_hierarchy, node_data);
match sibling {
Some(s) if selector_group_matches(
content_group, html_node_tree[s], &node_data[s], s,
expected_path_ending.as_ref(), is_last,
) => { current_node = s; }
_ => return false,
}
}
GeneralSibling => {
let mut sibling = find_non_anonymous_prev_sibling(current_node, node_hierarchy, node_data);
let mut found = false;
while let Some(sib) = sibling {
if selector_group_matches(
content_group, html_node_tree[sib], &node_data[sib], sib,
expected_path_ending.as_ref(), is_last,
) {
current_node = sib;
found = true;
break;
}
sibling = find_non_anonymous_prev_sibling(sib, node_hierarchy, node_data);
}
if !found {
return false;
}
}
}
}
true
}
fn find_non_anonymous_parent(
node_id: NodeId,
node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
node_data: &NodeDataContainerRef<'_, NodeData>,
) -> Option<NodeId> {
let mut next = node_hierarchy[node_id].parent_id();
while let Some(n) = next {
if !node_data[n].is_anonymous() {
return Some(n);
}
next = node_hierarchy[n].parent_id();
}
None
}
fn find_non_anonymous_prev_sibling(
node_id: NodeId,
node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
node_data: &NodeDataContainerRef<'_, NodeData>,
) -> Option<NodeId> {
let mut next = node_hierarchy[node_id].previous_sibling_id();
while let Some(n) = next {
if !node_data[n].is_anonymous() {
return Some(n);
}
next = node_hierarchy[n].previous_sibling_id();
}
None
}
#[derive(Debug)]
pub struct CssGroupIterator<'a> {
pub css_path: &'a [CssPathSelector],
current_idx: usize,
last_reason: CssGroupSplitReason,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum CssGroupSplitReason {
Children,
DirectChildren,
AdjacentSibling,
GeneralSibling,
}
impl<'a> CssGroupIterator<'a> {
#[must_use] pub const fn new(css_path: &'a [CssPathSelector]) -> Self {
let initial_len = css_path.len();
Self {
css_path,
current_idx: initial_len,
last_reason: CssGroupSplitReason::Children,
}
}
}
impl<'a> Iterator for CssGroupIterator<'a> {
type Item = (CssContentGroup<'a>, CssGroupSplitReason);
fn next(&mut self) -> Option<(CssContentGroup<'a>, CssGroupSplitReason)> {
use self::CssPathSelector::{Children, DirectChildren, AdjacentSibling, GeneralSibling};
let mut new_idx = self.current_idx;
if new_idx == 0 {
return None;
}
let mut current_path = Vec::new();
while new_idx != 0 {
match self.css_path.get(new_idx - 1)? {
Children => {
self.last_reason = CssGroupSplitReason::Children;
break;
}
DirectChildren => {
self.last_reason = CssGroupSplitReason::DirectChildren;
break;
}
AdjacentSibling => {
self.last_reason = CssGroupSplitReason::AdjacentSibling;
break;
}
GeneralSibling => {
self.last_reason = CssGroupSplitReason::GeneralSibling;
break;
}
other => current_path.push(other),
}
new_idx -= 1;
}
#[cfg(test)]
current_path.reverse();
if new_idx == 0 {
if current_path.is_empty() {
None
} else {
self.current_idx = 0;
Some((current_path, self.last_reason))
}
} else {
self.current_idx = new_idx - 1;
Some((current_path, self.last_reason))
}
}
}
#[must_use] pub fn construct_html_cascade_tree(
node_hierarchy: &NodeHierarchyRef<'_>,
node_depths_sorted: &[(usize, NodeId)],
node_data: &NodeDataContainerRef<'_, NodeData>,
) -> NodeDataContainer<CascadeInfo> {
let mut nodes = (0..node_hierarchy.len())
.map(|_| CascadeInfo {
index_in_parent: 0,
is_last_child: false,
})
.collect::<Vec<_>>();
for (_depth, parent_id) in node_depths_sorted {
let element_index_in_parent = parent_id
.preceding_siblings(node_hierarchy)
.filter(|sib_id| !node_data[*sib_id].is_text_node())
.count();
let parent_html_matcher = CascadeInfo {
index_in_parent: u32::try_from(element_index_in_parent.saturating_sub(1))
.unwrap_or(u32::MAX),
is_last_child: {
let mut is_last_element = true;
let mut next = node_hierarchy[*parent_id].next_sibling;
while let Some(sib_id) = next {
if !node_data[sib_id].is_text_node() {
is_last_element = false;
break;
}
next = node_hierarchy[sib_id].next_sibling;
}
is_last_element
},
};
nodes[parent_id.index()] = parent_html_matcher;
let mut element_idx: u32 = 0;
for child_id in parent_id.children(node_hierarchy) {
let is_text = node_data[child_id].is_text_node();
let is_last_element_child = if is_text {
false
} else {
let mut is_last = true;
let mut next = node_hierarchy[child_id].next_sibling;
while let Some(sib_id) = next {
if !node_data[sib_id].is_text_node() {
is_last = false;
break;
}
next = node_hierarchy[sib_id].next_sibling;
}
is_last
};
let child_html_matcher = CascadeInfo {
index_in_parent: element_idx,
is_last_child: is_last_element_child,
};
nodes[child_id.index()] = child_html_matcher;
if !is_text {
element_idx += 1;
}
}
}
NodeDataContainer { internal: nodes }
}
#[inline]
#[must_use] pub fn rule_ends_with(path: &CssPath, target: Option<CssPathPseudoSelector>) -> bool {
const fn is_interactive_pseudo(p: &CssPathPseudoSelector) -> bool {
matches!(
p,
CssPathPseudoSelector::Hover
| CssPathPseudoSelector::Active
| CssPathPseudoSelector::Focus
| CssPathPseudoSelector::Backdrop
| CssPathPseudoSelector::Dragging
| CssPathPseudoSelector::DragOver
)
}
let Some(last) = path.selectors.as_ref().last() else {
return false;
};
target.map_or_else(
|| match last {
CssPathSelector::PseudoSelector(p) => !is_interactive_pseudo(p),
_ => true,
},
|s| matches!(last, CssPathSelector::PseudoSelector(q) if *q == s),
)
}
fn selector_group_matches(
selectors: &[&CssPathSelector],
html_node: CascadeInfo,
node_data: &NodeData,
node_id: NodeId,
expected_path_ending: Option<&CssPathPseudoSelector>,
is_last_content_group: bool,
) -> bool {
selectors.iter().all(|selector| {
match_single_selector(
selector,
html_node,
node_data,
node_id,
expected_path_ending,
is_last_content_group,
)
})
}
fn match_single_selector(
selector: &CssPathSelector,
html_node: CascadeInfo,
node_data: &NodeData,
node_id: NodeId,
expected_path_ending: Option<&CssPathPseudoSelector>,
is_last_content_group: bool,
) -> bool {
use self::CssPathSelector::{Global, Root, Type, Class, Id, PseudoSelector, Attribute, DirectChildren, Children, AdjacentSibling, GeneralSibling};
match selector {
Global => true,
Root(range) => range.contains(node_id.index()),
Type(t) => node_data.get_node_type().get_path() == *t,
Class(c) => node_data.has_class(c.as_str()),
Id(id) => node_data.has_id(id.as_str()),
PseudoSelector(p) => {
match_pseudo_selector(p, html_node, expected_path_ending, is_last_content_group)
}
Attribute(a) => match_attribute_selector(a, node_data),
DirectChildren | Children | AdjacentSibling | GeneralSibling => false,
}
}
fn match_attribute_selector(sel: &CssAttributeSelector, node_data: &NodeData) -> bool {
let name = sel.name.as_str();
let target = sel.value.as_ref().map(azul_css::AzString::as_str);
let check = |actual: &str| -> bool {
match (&sel.op, target) {
(AttributeMatchOp::Exists, _) => true,
(AttributeMatchOp::Eq, Some(t)) => actual == t,
(AttributeMatchOp::Includes, Some(t)) => {
if t.is_empty() || t.contains(char::is_whitespace) {
return false;
}
actual.split_whitespace().any(|word| word == t)
}
(AttributeMatchOp::DashMatch, Some(t)) => {
actual == t || actual.starts_with(&alloc::format!("{t}-"))
}
(AttributeMatchOp::Prefix, Some(t)) => !t.is_empty() && actual.starts_with(t),
(AttributeMatchOp::Suffix, Some(t)) => !t.is_empty() && actual.ends_with(t),
(AttributeMatchOp::Substring, Some(t)) => !t.is_empty() && actual.contains(t),
(_, None) => false,
}
};
for attr in node_data.attributes() {
if attr.name() != name {
continue;
}
if check(attr.value().as_str()) {
return true;
}
}
false
}
fn match_pseudo_selector(
pseudo: &CssPathPseudoSelector,
html_node: CascadeInfo,
expected_path_ending: Option<&CssPathPseudoSelector>,
is_last_content_group: bool,
) -> bool {
match pseudo {
CssPathPseudoSelector::First => match_first_child(html_node),
CssPathPseudoSelector::Last => match_last_child(html_node),
CssPathPseudoSelector::NthChild(pattern) => match_nth_child(html_node, pattern),
CssPathPseudoSelector::Hover => match_interactive_pseudo(
&CssPathPseudoSelector::Hover,
expected_path_ending,
is_last_content_group,
),
CssPathPseudoSelector::Active => match_interactive_pseudo(
&CssPathPseudoSelector::Active,
expected_path_ending,
is_last_content_group,
),
CssPathPseudoSelector::Focus => match_interactive_pseudo(
&CssPathPseudoSelector::Focus,
expected_path_ending,
is_last_content_group,
),
CssPathPseudoSelector::Backdrop => match_interactive_pseudo(
&CssPathPseudoSelector::Backdrop,
expected_path_ending,
is_last_content_group,
),
CssPathPseudoSelector::Dragging => match_interactive_pseudo(
&CssPathPseudoSelector::Dragging,
expected_path_ending,
is_last_content_group,
),
CssPathPseudoSelector::DragOver => match_interactive_pseudo(
&CssPathPseudoSelector::DragOver,
expected_path_ending,
is_last_content_group,
),
CssPathPseudoSelector::Lang(lang) => {
if let Some(CssPathPseudoSelector::Lang(expected_lang)) = expected_path_ending {
return lang == expected_lang;
}
false
}
}
}
const fn match_first_child(html_node: CascadeInfo) -> bool {
html_node.index_in_parent == 0
}
const fn match_last_child(html_node: CascadeInfo) -> bool {
html_node.is_last_child
}
fn match_nth_child(html_node: CascadeInfo, pattern: &CssNthChildSelector) -> bool {
use azul_css::css::CssNthChildPattern;
let index = html_node.index_in_parent + 1;
match pattern {
Number(n) => index == *n,
Even => index.is_multiple_of(2),
Odd => index % 2 == 1,
Pattern(CssNthChildPattern {
pattern_repeat,
offset,
}) => {
if *pattern_repeat == 0 {
index == *offset
} else {
index >= *offset && (index - offset).is_multiple_of(*pattern_repeat)
}
}
}
}
fn match_interactive_pseudo(
pseudo: &CssPathPseudoSelector,
expected_path_ending: Option<&CssPathPseudoSelector>,
is_last_content_group: bool,
) -> bool {
is_last_content_group && expected_path_ending == Some(pseudo)
}
#[cfg(test)]
#[allow(clippy::pedantic, clippy::nursery, clippy::too_many_lines)]
mod autotest_generated {
use azul_css::{
css::{CssNthChildPattern, CssScopeRange, NodeTypeTag},
OptionString,
};
use super::*;
use crate::{
dom::{AttributeNameValue, AttributeType, NodeType},
id::Node,
};
fn node(
parent: Option<usize>,
prev: Option<usize>,
next: Option<usize>,
last_child: Option<usize>,
) -> Node {
Node {
parent: parent.map(NodeId::new),
previous_sibling: prev.map(NodeId::new),
next_sibling: next.map(NodeId::new),
last_child: last_child.map(NodeId::new),
}
}
fn items(nodes: &[Node]) -> Vec<NodeHierarchyItem> {
nodes.iter().map(|n| NodeHierarchyItem::from(*n)).collect()
}
fn div_with(id: Option<&str>, class: Option<&str>) -> NodeData {
let mut nd = NodeData::create_div();
if let Some(i) = id {
nd.add_id(i.into());
}
if let Some(c) = class {
nd.add_class(c.into());
}
nd
}
fn node_with_attrs(attrs: Vec<AttributeType>) -> NodeData {
let mut nd = NodeData::create_div();
nd.set_attributes(attrs.into());
nd
}
fn custom(name: &str, value: &str) -> AttributeType {
AttributeType::Custom(AttributeNameValue {
attr_name: name.into(),
value: value.into(),
})
}
fn attr_sel(name: &str, op: AttributeMatchOp, value: Option<&str>) -> CssAttributeSelector {
CssAttributeSelector {
name: name.into(),
op,
value: value.map_or(OptionString::None, |v| OptionString::Some(v.into())),
}
}
fn info(index_in_parent: u32, is_last_child: bool) -> CascadeInfo {
CascadeInfo {
index_in_parent,
is_last_child,
}
}
fn sample_hierarchy() -> Vec<Node> {
vec![
node(None, None, None, Some(5)),
node(Some(0), None, Some(2), None),
node(Some(0), Some(1), Some(3), None),
node(Some(0), Some(2), Some(5), Some(4)),
node(Some(3), None, None, None),
node(Some(0), Some(3), None, None),
]
}
fn sample_node_data() -> Vec<NodeData> {
let mut p = NodeData::create_node(NodeType::P);
p.add_class("inner".into());
vec![
NodeData::create_body(),
div_with(Some("first"), Some("a")),
NodeData::create_text("hello"),
div_with(None, Some("b")),
p,
div_with(None, Some("c")),
]
}
fn matches(
selectors: Vec<CssPathSelector>,
node_index: usize,
expected_path_ending: Option<CssPathPseudoSelector>,
) -> bool {
let hierarchy = sample_hierarchy();
let hier_items = items(&hierarchy);
let data = sample_node_data();
let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
let data_ref = NodeDataContainerRef::from_slice(&data);
let depths = hierarchy_ref.get_parents_sorted_by_depth();
let cascade = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
matches_html_element(
&CssPath::new(selectors),
NodeId::new(node_index),
&NodeDataContainerRef::from_slice(&hier_items),
&data_ref,
&cascade.as_ref(),
expected_path_ending,
)
}
#[test]
fn cascade_info_vec_as_container_preserves_every_entry() {
let v: CascadeInfoVec = vec![info(0, false), info(1, false), info(2, true)].into();
let c = v.as_container();
assert_eq!(c.len(), 3);
assert!(!c.is_empty());
assert_eq!(c[NodeId::new(0)], info(0, false));
assert_eq!(c[NodeId::new(2)], info(2, true));
assert_eq!(c.get(NodeId::new(3)), None);
assert_eq!(c.get(NodeId::new(usize::MAX)), None);
}
#[test]
fn cascade_info_vec_as_container_on_empty_vec_does_not_panic() {
let v: CascadeInfoVec = Vec::new().into();
let c = v.as_container();
assert_eq!(c.len(), 0);
assert!(c.is_empty());
assert_eq!(c.get(NodeId::new(0)), None);
}
#[test]
fn cascade_info_vec_as_container_survives_extreme_field_values() {
let v: CascadeInfoVec = vec![info(u32::MAX, true)].into();
let c = v.as_container();
assert_eq!(c[NodeId::new(0)].index_in_parent, u32::MAX);
assert!(c[NodeId::new(0)].is_last_child);
}
#[test]
fn css_group_iterator_new_records_the_path_and_yields_nothing_when_empty() {
let empty: [CssPathSelector; 0] = [];
let it = CssGroupIterator::new(&empty);
assert!(it.css_path.is_empty());
assert_eq!(CssGroupIterator::new(&empty).count(), 0);
}
#[test]
fn css_group_iterator_new_keeps_the_slice_it_was_given() {
let path = vec![
CssPathSelector::Global,
CssPathSelector::Children,
CssPathSelector::Class("x".into()),
];
let it = CssGroupIterator::new(&path);
assert_eq!(it.css_path.len(), 3);
assert_eq!(it.css_path, path.as_slice());
}
#[test]
fn css_group_iterator_splits_right_to_left_with_the_left_hand_combinator() {
let path = vec![
CssPathSelector::Type(NodeTypeTag::Body),
CssPathSelector::DirectChildren,
CssPathSelector::Class("foo".into()),
CssPathSelector::Class("main".into()),
CssPathSelector::Children,
CssPathSelector::Class("baz".into()),
];
let groups: Vec<_> = CssGroupIterator::new(&path).collect();
assert_eq!(groups.len(), 3);
assert_eq!(groups[0].0, vec![&path[5]]);
assert_eq!(groups[0].1, CssGroupSplitReason::Children);
assert_eq!(groups[1].0, vec![&path[2], &path[3]]);
assert_eq!(groups[1].1, CssGroupSplitReason::DirectChildren);
assert_eq!(groups[2].0, vec![&path[0]]);
}
#[test]
fn css_group_iterator_yields_an_empty_group_for_a_trailing_combinator() {
let path = vec![
CssPathSelector::Class("foo".into()),
CssPathSelector::DirectChildren,
];
let groups: Vec<_> = CssGroupIterator::new(&path).collect();
assert_eq!(groups.len(), 2);
assert!(
groups[0].0.is_empty(),
"the group to the right of the dangling combinator is empty"
);
assert_eq!(groups[0].1, CssGroupSplitReason::DirectChildren);
assert_eq!(groups[1].0, vec![&path[0]]);
}
#[test]
fn css_group_iterator_terminates_on_consecutive_combinators() {
let path = vec![
CssPathSelector::Class("a".into()),
CssPathSelector::Children,
CssPathSelector::DirectChildren,
CssPathSelector::Class("b".into()),
];
let groups: Vec<_> = CssGroupIterator::new(&path).collect();
assert_eq!(groups.len(), 3);
assert_eq!(groups[0].0, vec![&path[3]]);
assert!(groups[1].0.is_empty(), "the group between the two combinators is empty");
assert_eq!(groups[2].0, vec![&path[0]]);
}
#[test]
fn css_group_iterator_only_combinators_yields_only_empty_groups() {
let path = vec![
CssPathSelector::Children,
CssPathSelector::DirectChildren,
CssPathSelector::AdjacentSibling,
CssPathSelector::GeneralSibling,
];
let groups: Vec<_> = CssGroupIterator::new(&path).collect();
assert_eq!(groups.len(), 4);
assert!(groups.iter().all(|(g, _)| g.is_empty()));
}
#[test]
fn css_group_iterator_conserves_every_non_combinator_selector_on_a_huge_path() {
let mut path = Vec::new();
for i in 0..5_000u32 {
if i != 0 {
path.push(CssPathSelector::Children);
}
path.push(CssPathSelector::Class(alloc::format!(".c{i}").into()));
}
let groups: Vec<_> = CssGroupIterator::new(&path).collect();
assert_eq!(groups.len(), 5_000);
let total: usize = groups.iter().map(|(g, _)| g.len()).sum();
assert_eq!(
total, 5_000,
"every non-combinator selector must appear in exactly one group"
);
assert!(groups.iter().all(|(_, r)| *r == CssGroupSplitReason::Children));
}
#[test]
fn rule_ends_with_empty_path_is_always_false() {
let empty = CssPath::new(Vec::new());
assert!(!rule_ends_with(&empty, None));
assert!(!rule_ends_with(&empty, Some(CssPathPseudoSelector::Hover)));
assert!(!rule_ends_with(
&empty,
Some(CssPathPseudoSelector::NthChild(CssNthChildSelector::Even))
));
}
#[test]
fn rule_ends_with_none_rejects_interactive_pseudos_but_keeps_structural_ones() {
let ends_with = |s: CssPathSelector| {
rule_ends_with(&CssPath::new(vec![CssPathSelector::Class("a".into()), s]), None)
};
for p in [
CssPathPseudoSelector::Hover,
CssPathPseudoSelector::Active,
CssPathPseudoSelector::Focus,
CssPathPseudoSelector::Backdrop,
CssPathPseudoSelector::Dragging,
CssPathPseudoSelector::DragOver,
] {
assert!(
!ends_with(CssPathSelector::PseudoSelector(p.clone())),
"interactive pseudo {p:?} must not end a `None` rule"
);
}
assert!(ends_with(CssPathSelector::PseudoSelector(
CssPathPseudoSelector::First
)));
assert!(ends_with(CssPathSelector::PseudoSelector(
CssPathPseudoSelector::Last
)));
assert!(ends_with(CssPathSelector::PseudoSelector(
CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd)
)));
assert!(ends_with(CssPathSelector::Global));
assert!(ends_with(CssPathSelector::Class("b".into())));
assert!(ends_with(CssPathSelector::Type(NodeTypeTag::Div)));
assert!(ends_with(CssPathSelector::DirectChildren));
}
#[test]
fn rule_ends_with_some_target_requires_an_exact_match_on_the_last_selector() {
let hover = CssPath::new(vec![
CssPathSelector::Class("a".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
]);
assert!(rule_ends_with(&hover, Some(CssPathPseudoSelector::Hover)));
assert!(!rule_ends_with(&hover, Some(CssPathPseudoSelector::Active)));
assert!(!rule_ends_with(&hover, Some(CssPathPseudoSelector::Focus)));
let plain = CssPath::new(vec![CssPathSelector::Class("a".into())]);
assert!(!rule_ends_with(&plain, Some(CssPathPseudoSelector::Hover)));
let compound = CssPath::new(vec![
CssPathSelector::Class("a".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::First),
]);
assert!(!rule_ends_with(&compound, Some(CssPathPseudoSelector::Hover)));
assert!(rule_ends_with(&compound, Some(CssPathPseudoSelector::First)));
}
#[test]
fn rule_ends_with_compares_nth_child_and_lang_payloads() {
let nth = |s| {
CssPath::new(vec![CssPathSelector::PseudoSelector(
CssPathPseudoSelector::NthChild(s),
)])
};
assert!(rule_ends_with(
&nth(CssNthChildSelector::Number(3)),
Some(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(3)))
));
assert!(!rule_ends_with(
&nth(CssNthChildSelector::Number(3)),
Some(CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(4)))
));
assert!(!rule_ends_with(
&nth(CssNthChildSelector::Even),
Some(CssPathPseudoSelector::NthChild(CssNthChildSelector::Odd))
));
let lang = |s: &str| {
CssPath::new(vec![CssPathSelector::PseudoSelector(
CssPathPseudoSelector::Lang(s.into()),
)])
};
assert!(rule_ends_with(
&lang("zh-Hant-🎉"),
Some(CssPathPseudoSelector::Lang("zh-Hant-🎉".into()))
));
assert!(!rule_ends_with(
&lang("zh-Hant-🎉"),
Some(CssPathPseudoSelector::Lang("zh".into()))
));
assert!(!rule_ends_with(&lang(""), Some(CssPathPseudoSelector::Lang("de".into()))));
}
#[test]
fn match_first_and_last_child_read_only_their_own_field() {
assert!(match_first_child(info(0, false)));
assert!(!match_first_child(info(1, true)));
assert!(!match_first_child(info(u32::MAX, true)));
assert!(match_last_child(info(0, true)));
assert!(!match_last_child(info(0, false)));
assert!(match_last_child(info(u32::MAX, true)));
}
#[test]
fn match_nth_child_is_one_indexed() {
assert!(match_nth_child(info(0, false), &CssNthChildSelector::Number(1)));
assert!(!match_nth_child(info(0, false), &CssNthChildSelector::Number(0)));
assert!(match_nth_child(info(0, false), &CssNthChildSelector::Odd));
assert!(!match_nth_child(info(0, false), &CssNthChildSelector::Even));
assert!(match_nth_child(info(1, false), &CssNthChildSelector::Number(2)));
assert!(match_nth_child(info(1, false), &CssNthChildSelector::Even));
assert!(!match_nth_child(info(1, false), &CssNthChildSelector::Odd));
}
#[test]
fn match_nth_child_number_matches_exactly_one_index() {
for n in 1..64u32 {
for idx in 0..64u32 {
let matched = match_nth_child(info(idx, false), &CssNthChildSelector::Number(n));
assert_eq!(matched, idx + 1 == n, "nth-child({n}) vs index {idx}");
}
}
for idx in 0..64u32 {
assert!(!match_nth_child(info(idx, false), &CssNthChildSelector::Number(0)));
}
}
#[test]
fn match_nth_child_even_and_odd_partition_every_index() {
for idx in 0..1_000u32 {
let even = match_nth_child(info(idx, false), &CssNthChildSelector::Even);
let odd = match_nth_child(info(idx, false), &CssNthChildSelector::Odd);
assert_ne!(even, odd, "index {idx} must be exactly one of even/odd");
}
}
#[test]
fn match_nth_child_pattern_agrees_with_the_even_and_odd_shorthands() {
for idx in 0..256u32 {
let even_pat = CssNthChildSelector::Pattern(CssNthChildPattern {
pattern_repeat: 2,
offset: 0,
});
let odd_pat = CssNthChildSelector::Pattern(CssNthChildPattern {
pattern_repeat: 2,
offset: 1,
});
assert_eq!(
match_nth_child(info(idx, false), &even_pat),
match_nth_child(info(idx, false), &CssNthChildSelector::Even),
"2n disagrees with `even` at index {idx}"
);
assert_eq!(
match_nth_child(info(idx, false), &odd_pat),
match_nth_child(info(idx, false), &CssNthChildSelector::Odd),
"2n+1 disagrees with `odd` at index {idx}"
);
}
}
#[test]
fn match_nth_child_zero_repeat_never_divides_by_zero() {
let only_third = CssNthChildSelector::Pattern(CssNthChildPattern {
pattern_repeat: 0,
offset: 3,
});
let never = CssNthChildSelector::Pattern(CssNthChildPattern {
pattern_repeat: 0,
offset: 0,
});
for idx in 0..32u32 {
assert_eq!(match_nth_child(info(idx, false), &only_third), idx == 2);
assert!(
!match_nth_child(info(idx, false), &never),
"0n+0 must never match (index is 1-based)"
);
}
}
#[test]
fn match_nth_child_pattern_below_the_offset_does_not_underflow() {
let pat = CssNthChildSelector::Pattern(CssNthChildPattern {
pattern_repeat: 2,
offset: 5,
});
for idx in 0..4u32 {
assert!(!match_nth_child(info(idx, false), &pat), "index {idx} < offset");
}
assert!(match_nth_child(info(4, false), &pat)); assert!(!match_nth_child(info(5, false), &pat)); assert!(match_nth_child(info(6, false), &pat)); }
#[test]
fn match_nth_child_with_a_u32_max_offset_does_not_underflow() {
let pat = CssNthChildSelector::Pattern(CssNthChildPattern {
pattern_repeat: 1,
offset: u32::MAX,
});
assert!(!match_nth_child(info(0, false), &pat));
assert!(!match_nth_child(info(1_000, false), &pat));
assert!(match_nth_child(info(u32::MAX - 1, false), &pat));
}
#[cfg(feature = "std")]
#[test]
fn match_nth_child_at_u32_max_index_never_answers_wrongly() {
let even = std::panic::catch_unwind(|| {
match_nth_child(info(u32::MAX, false), &CssNthChildSelector::Even)
});
let odd = std::panic::catch_unwind(|| {
match_nth_child(info(u32::MAX, false), &CssNthChildSelector::Odd)
});
let one = std::panic::catch_unwind(|| {
match_nth_child(info(u32::MAX, false), &CssNthChildSelector::Number(1))
});
if let Ok(v) = even {
assert!(v, "2^32 is even");
}
if let Ok(v) = odd {
assert!(!v, "2^32 is not odd");
}
if let Ok(v) = one {
assert!(!v, "the 2^32-th child is not the 1st child");
}
}
#[test]
fn match_interactive_pseudo_needs_both_the_last_group_and_the_expected_ending() {
let hover = CssPathPseudoSelector::Hover;
let active = CssPathPseudoSelector::Active;
assert!(match_interactive_pseudo(&hover, Some(&hover), true));
assert!(!match_interactive_pseudo(&hover, Some(&hover), false));
assert!(!match_interactive_pseudo(&hover, Some(&active), true));
assert!(!match_interactive_pseudo(&hover, None, true));
assert!(!match_interactive_pseudo(&hover, None, false));
}
#[test]
fn match_pseudo_selector_routes_every_interactive_pseudo_through_the_gate() {
for p in [
CssPathPseudoSelector::Hover,
CssPathPseudoSelector::Active,
CssPathPseudoSelector::Focus,
CssPathPseudoSelector::Backdrop,
CssPathPseudoSelector::Dragging,
CssPathPseudoSelector::DragOver,
] {
assert!(
match_pseudo_selector(&p, info(0, false), Some(&p), true),
"{p:?} must match when it is the expected ending of the last group"
);
assert!(
!match_pseudo_selector(&p, info(0, false), Some(&p), false),
"{p:?} must not match outside the last content group"
);
assert!(
!match_pseudo_selector(&p, info(0, false), None, true),
"{p:?} must not match when no pseudo state is expected"
);
}
}
#[test]
fn match_pseudo_selector_structural_pseudos_ignore_the_expected_ending() {
let hover = CssPathPseudoSelector::Hover;
for (expected, is_last) in [(None, true), (Some(&hover), false), (Some(&hover), true)] {
assert!(match_pseudo_selector(
&CssPathPseudoSelector::First,
info(0, false),
expected,
is_last
));
assert!(!match_pseudo_selector(
&CssPathPseudoSelector::First,
info(1, false),
expected,
is_last
));
assert!(match_pseudo_selector(
&CssPathPseudoSelector::Last,
info(9, true),
expected,
is_last
));
assert!(match_pseudo_selector(
&CssPathPseudoSelector::NthChild(CssNthChildSelector::Number(10)),
info(9, true),
expected,
is_last
));
}
}
#[test]
fn match_pseudo_selector_lang_only_matches_the_expected_lang() {
let de = CssPathPseudoSelector::Lang("de".into());
let en = CssPathPseudoSelector::Lang("en".into());
assert!(match_pseudo_selector(&de, info(0, false), Some(&de), true));
assert!(!match_pseudo_selector(&de, info(0, false), Some(&en), true));
assert!(!match_pseudo_selector(&de, info(0, false), None, true));
assert!(!match_pseudo_selector(
&de,
info(0, false),
Some(&CssPathPseudoSelector::Hover),
true
));
let emoji = CssPathPseudoSelector::Lang("de-🎉".into());
assert!(match_pseudo_selector(&emoji, info(0, false), Some(&emoji), true));
assert!(!match_pseudo_selector(&emoji, info(0, false), Some(&de), true));
let empty = CssPathPseudoSelector::Lang("".into());
assert!(match_pseudo_selector(&empty, info(0, false), Some(&empty), true));
}
#[test]
fn match_attribute_selector_on_a_node_without_attributes_is_always_false() {
let nd = NodeData::create_div();
for op in [
AttributeMatchOp::Exists,
AttributeMatchOp::Eq,
AttributeMatchOp::Includes,
AttributeMatchOp::DashMatch,
AttributeMatchOp::Prefix,
AttributeMatchOp::Suffix,
AttributeMatchOp::Substring,
] {
assert!(!match_attribute_selector(&attr_sel("data-x", op, Some("v")), &nd));
assert!(!match_attribute_selector(&attr_sel("", op, None), &nd));
}
}
#[test]
fn match_attribute_selector_exists_matches_any_value_including_the_empty_one() {
let nd = node_with_attrs(vec![custom("data-x", "")]);
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Exists, None),
&nd
));
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Exists, Some("nonsense")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("data-y", AttributeMatchOp::Exists, None),
&nd
));
}
#[test]
fn match_attribute_selector_operator_without_a_value_never_matches() {
let nd = node_with_attrs(vec![custom("data-x", "value")]);
for op in [
AttributeMatchOp::Eq,
AttributeMatchOp::Includes,
AttributeMatchOp::DashMatch,
AttributeMatchOp::Prefix,
AttributeMatchOp::Suffix,
AttributeMatchOp::Substring,
] {
assert!(
!match_attribute_selector(&attr_sel("data-x", op, None), &nd),
"{op:?} with a missing target value must be rejected, not matched"
);
}
}
#[test]
fn match_attribute_selector_empty_target_never_matches_the_substring_family() {
let nd = node_with_attrs(vec![custom("data-x", "abc")]);
for op in [
AttributeMatchOp::Prefix,
AttributeMatchOp::Suffix,
AttributeMatchOp::Substring,
AttributeMatchOp::Includes,
] {
assert!(
!match_attribute_selector(&attr_sel("data-x", op, Some("")), &nd),
"{op:?} with an empty target must not match"
);
}
assert!(!match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Eq, Some("")),
&nd
));
let empty = node_with_attrs(vec![custom("data-x", "")]);
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Eq, Some("")),
&empty
));
}
#[test]
fn match_attribute_selector_includes_matches_one_of_several_class_entries() {
let mut nd = NodeData::create_div();
nd.add_class("foo".into());
nd.add_class("primary".into());
nd.add_class("bar".into());
assert!(match_attribute_selector(
&attr_sel("class", AttributeMatchOp::Includes, Some("primary")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("class", AttributeMatchOp::Includes, Some("prim")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("class", AttributeMatchOp::Includes, Some("foo bar")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("class", AttributeMatchOp::Includes, Some("\t")),
&nd
));
}
#[test]
fn match_attribute_selector_dash_match_requires_a_dash_boundary() {
let nd = node_with_attrs(vec![AttributeType::Lang("en-US".into())]);
assert!(match_attribute_selector(
&attr_sel("lang", AttributeMatchOp::DashMatch, Some("en")),
&nd
));
assert!(match_attribute_selector(
&attr_sel("lang", AttributeMatchOp::DashMatch, Some("en-US")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("lang", AttributeMatchOp::DashMatch, Some("en-U")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("lang", AttributeMatchOp::DashMatch, Some("e")),
&nd
));
let english = node_with_attrs(vec![AttributeType::Lang("english".into())]);
assert!(!match_attribute_selector(
&attr_sel("lang", AttributeMatchOp::DashMatch, Some("en")),
&english
));
}
#[test]
fn match_attribute_selector_name_matching_is_exact_and_case_sensitive() {
let nd = node_with_attrs(vec![custom("data-foo", "v")]);
assert!(match_attribute_selector(
&attr_sel("data-foo", AttributeMatchOp::Eq, Some("v")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("data-fo", AttributeMatchOp::Eq, Some("v")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("data-foo2", AttributeMatchOp::Eq, Some("v")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("DATA-FOO", AttributeMatchOp::Eq, Some("v")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("", AttributeMatchOp::Exists, None),
&nd
));
}
#[test]
fn match_attribute_selector_handles_unicode_values_on_char_boundaries() {
let nd = node_with_attrs(vec![custom("data-x", "héllo-🎉-世界")]);
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Prefix, Some("hé")),
&nd
));
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Suffix, Some("世界")),
&nd
));
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Substring, Some("🎉")),
&nd
));
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Eq, Some("héllo-🎉-世界")),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::Substring, Some("e")),
&nd
));
assert!(match_attribute_selector(
&attr_sel("data-x", AttributeMatchOp::DashMatch, Some("héllo")),
&nd
));
}
#[test]
fn match_attribute_selector_handles_huge_values() {
let huge = "ä".repeat(100_000);
let value = alloc::format!("{huge}-tail");
let nd = node_with_attrs(vec![custom("data-big", &value)]);
assert!(match_attribute_selector(
&attr_sel("data-big", AttributeMatchOp::Suffix, Some("-tail")),
&nd
));
assert!(match_attribute_selector(
&attr_sel("data-big", AttributeMatchOp::Prefix, Some("ää")),
&nd
));
assert!(match_attribute_selector(
&attr_sel("data-big", AttributeMatchOp::DashMatch, Some(huge.as_str())),
&nd
));
assert!(!match_attribute_selector(
&attr_sel("data-big", AttributeMatchOp::Eq, Some(huge.as_str())),
&nd
));
}
#[test]
fn match_single_selector_global_matches_every_node_type() {
let div = NodeData::create_div();
let text = NodeData::create_text("hello");
assert!(match_single_selector(
&CssPathSelector::Global,
info(0, false),
&div,
NodeId::new(0),
None,
true
));
assert!(match_single_selector(
&CssPathSelector::Global,
info(u32::MAX, true),
&text,
NodeId::new(usize::MAX),
None,
false
));
}
#[test]
fn match_single_selector_never_matches_a_combinator() {
let div = NodeData::create_div();
for c in [
CssPathSelector::DirectChildren,
CssPathSelector::Children,
CssPathSelector::AdjacentSibling,
CssPathSelector::GeneralSibling,
] {
assert!(!match_single_selector(
&c,
info(0, true),
&div,
NodeId::new(0),
None,
true
));
}
}
#[test]
fn match_single_selector_root_scope_range_is_inclusive_on_both_ends() {
let div = NodeData::create_div();
let sel = CssPathSelector::Root(CssScopeRange { start: 2, end: 4 });
for id in [2usize, 3, 4] {
assert!(match_single_selector(
&sel,
info(0, false),
&div,
NodeId::new(id),
None,
true
));
}
for id in [0usize, 1, 5, usize::MAX] {
assert!(!match_single_selector(
&sel,
info(0, false),
&div,
NodeId::new(id),
None,
true
));
}
}
#[test]
fn match_single_selector_root_scope_with_an_inverted_or_full_range() {
let div = NodeData::create_div();
let inverted = CssPathSelector::Root(CssScopeRange { start: 9, end: 2 });
for id in [0usize, 2, 9, usize::MAX] {
assert!(!match_single_selector(
&inverted,
info(0, false),
&div,
NodeId::new(id),
None,
true
));
}
let full = CssPathSelector::Root(CssScopeRange {
start: 0,
end: usize::MAX,
});
for id in [0usize, 1, usize::MAX] {
assert!(match_single_selector(
&full,
info(0, false),
&div,
NodeId::new(id),
None,
true
));
}
}
#[test]
fn match_single_selector_type_class_and_id() {
let mut nd = div_with(Some("first"), Some("a"));
nd.add_class("日本語-🎉".into());
let hit = |s: &CssPathSelector| {
match_single_selector(s, info(0, false), &nd, NodeId::new(0), None, true)
};
assert!(hit(&CssPathSelector::Type(NodeTypeTag::Div)));
assert!(!hit(&CssPathSelector::Type(NodeTypeTag::P)));
assert!(hit(&CssPathSelector::Class("a".into())));
assert!(hit(&CssPathSelector::Class("日本語-🎉".into())));
assert!(!hit(&CssPathSelector::Class("日本語".into())), "no prefix matching");
assert!(!hit(&CssPathSelector::Class("".into())));
assert!(hit(&CssPathSelector::Id("first".into())));
assert!(!hit(&CssPathSelector::Id("firs".into())));
assert!(!hit(&CssPathSelector::Class("first".into())));
assert!(!hit(&CssPathSelector::Id("a".into())));
}
#[test]
fn selector_group_matches_requires_every_selector_in_the_group() {
let nd = div_with(Some("first"), Some("a"));
let div = CssPathSelector::Type(NodeTypeTag::Div);
let class_a = CssPathSelector::Class("a".into());
let class_z = CssPathSelector::Class("zzz".into());
let group: Vec<&CssPathSelector> = vec![&div, &class_a];
assert!(selector_group_matches(
&group,
info(0, false),
&nd,
NodeId::new(0),
None,
true
));
let group: Vec<&CssPathSelector> = vec![&div, &class_a, &class_z];
assert!(!selector_group_matches(
&group,
info(0, false),
&nd,
NodeId::new(0),
None,
true
));
}
#[test]
fn selector_group_matches_empty_group_matches_vacuously() {
let empty: Vec<&CssPathSelector> = Vec::new();
assert!(selector_group_matches(
&empty,
info(0, false),
&NodeData::create_div(),
NodeId::new(0),
None,
true
));
}
fn anonymous_fixture() -> (Vec<Node>, Vec<NodeData>) {
let hierarchy = vec![
node(None, None, None, Some(5)),
node(Some(0), None, Some(4), Some(2)),
node(Some(1), None, None, Some(3)),
node(Some(2), None, None, None),
node(Some(0), Some(1), Some(5), None),
node(Some(0), Some(4), None, None),
];
let mut anon1 = NodeData::create_div();
anon1.set_anonymous(true);
let mut anon2 = NodeData::create_div();
anon2.set_anonymous(true);
let mut anon4 = NodeData::create_div();
anon4.set_anonymous(true);
let data = vec![
NodeData::create_body(),
anon1,
anon2,
div_with(None, Some("deep")),
anon4,
div_with(None, Some("c")),
];
(hierarchy, data)
}
#[test]
fn find_non_anonymous_parent_skips_a_chain_of_anonymous_boxes() {
let (hierarchy, data) = anonymous_fixture();
let hier_items = items(&hierarchy);
let h = NodeDataContainerRef::from_slice(&hier_items);
let d = NodeDataContainerRef::from_slice(&data);
assert_eq!(
find_non_anonymous_parent(NodeId::new(3), &h, &d),
Some(NodeId::new(0))
);
assert_eq!(
find_non_anonymous_parent(NodeId::new(5), &h, &d),
Some(NodeId::new(0))
);
assert_eq!(find_non_anonymous_parent(NodeId::new(0), &h, &d), None);
}
#[test]
fn find_non_anonymous_parent_returns_none_when_every_ancestor_is_anonymous() {
let hierarchy = vec![node(None, None, None, Some(1)), node(Some(0), None, None, None)];
let mut anon_root = NodeData::create_div();
anon_root.set_anonymous(true);
let data = vec![anon_root, NodeData::create_div()];
let hier_items = items(&hierarchy);
let h = NodeDataContainerRef::from_slice(&hier_items);
let d = NodeDataContainerRef::from_slice(&data);
assert_eq!(find_non_anonymous_parent(NodeId::new(1), &h, &d), None);
}
#[test]
fn find_non_anonymous_prev_sibling_skips_anonymous_siblings() {
let (hierarchy, data) = anonymous_fixture();
let hier_items = items(&hierarchy);
let h = NodeDataContainerRef::from_slice(&hier_items);
let d = NodeDataContainerRef::from_slice(&data);
assert_eq!(find_non_anonymous_prev_sibling(NodeId::new(5), &h, &d), None);
assert_eq!(find_non_anonymous_prev_sibling(NodeId::new(1), &h, &d), None);
assert_eq!(find_non_anonymous_prev_sibling(NodeId::new(0), &h, &d), None);
}
#[test]
fn find_non_anonymous_prev_sibling_returns_the_nearest_real_sibling() {
let hierarchy = sample_hierarchy();
let data = sample_node_data();
let hier_items = items(&hierarchy);
let h = NodeDataContainerRef::from_slice(&hier_items);
let d = NodeDataContainerRef::from_slice(&data);
assert_eq!(
find_non_anonymous_prev_sibling(NodeId::new(3), &h, &d),
Some(NodeId::new(2))
);
assert_eq!(
find_non_anonymous_prev_sibling(NodeId::new(5), &h, &d),
Some(NodeId::new(3))
);
}
#[test]
fn construct_html_cascade_tree_on_an_empty_hierarchy_is_empty() {
let hierarchy: Vec<Node> = Vec::new();
let data: Vec<NodeData> = Vec::new();
let out = construct_html_cascade_tree(
&NodeHierarchyRef::from_slice(&hierarchy),
&[],
&NodeDataContainerRef::from_slice(&data),
);
assert_eq!(out.len(), 0);
assert!(out.is_empty());
}
#[test]
fn construct_html_cascade_tree_with_no_parents_defaults_every_node() {
let hierarchy = vec![node(None, None, None, None)];
let data = vec![NodeData::create_body()];
let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
let depths = hierarchy_ref.get_parents_sorted_by_depth();
assert!(depths.is_empty());
let out = construct_html_cascade_tree(
&hierarchy_ref,
&depths,
&NodeDataContainerRef::from_slice(&data),
);
assert_eq!(out.len(), 1);
assert_eq!(out.internal[0], CascadeInfo::default());
}
#[test]
fn construct_html_cascade_tree_does_not_count_text_nodes_as_element_siblings() {
let hierarchy = sample_hierarchy();
let data = sample_node_data();
let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
let data_ref = NodeDataContainerRef::from_slice(&data);
let depths = hierarchy_ref.get_parents_sorted_by_depth();
let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
assert_eq!(out.len(), hierarchy.len(), "one CascadeInfo per node");
assert_eq!(out.internal[0], info(0, true), "root");
assert_eq!(out.internal[1], info(0, false), "div#first.a — 1st element child");
assert_eq!(out.internal[3], info(1, false), "div.b — 2nd element child (text skipped)");
assert_eq!(out.internal[4], info(0, true), "p.inner — only child of div.b");
assert_eq!(
out.internal[5],
info(2, true),
"div.c — 3rd element child and the last one"
);
assert!(!out.internal[2].is_last_child);
}
#[test]
fn construct_html_cascade_tree_ignores_trailing_text_nodes_for_is_last_child() {
let hierarchy = vec![
node(None, None, None, Some(3)),
node(Some(0), None, Some(2), None),
node(Some(0), Some(1), Some(3), None),
node(Some(0), Some(2), None, None),
];
let data = vec![
NodeData::create_body(),
NodeData::create_div(),
NodeData::create_text("a"),
NodeData::create_text("b"),
];
let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
let data_ref = NodeDataContainerRef::from_slice(&data);
let depths = hierarchy_ref.get_parents_sorted_by_depth();
let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
assert_eq!(out.internal[1], info(0, true), "trailing text must not un-last the div");
}
#[test]
fn construct_html_cascade_tree_handles_a_wide_tree() {
const CHILDREN: usize = 2_000;
let mut hierarchy = vec![node(None, None, None, Some(CHILDREN))];
let mut data = vec![NodeData::create_body()];
for i in 1..=CHILDREN {
hierarchy.push(node(
Some(0),
if i == 1 { None } else { Some(i - 1) },
if i == CHILDREN { None } else { Some(i + 1) },
None,
));
data.push(NodeData::create_div());
}
let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
let data_ref = NodeDataContainerRef::from_slice(&data);
let depths = hierarchy_ref.get_parents_sorted_by_depth();
let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
assert_eq!(out.len(), CHILDREN + 1);
for i in 1..=CHILDREN {
let expected = info(u32::try_from(i - 1).unwrap(), i == CHILDREN);
assert_eq!(out.internal[i], expected, "child {i}");
}
}
#[test]
fn construct_html_cascade_tree_handles_a_deep_chain() {
const DEPTH: usize = 1_000;
let mut hierarchy = Vec::with_capacity(DEPTH);
let mut data = Vec::with_capacity(DEPTH);
for i in 0..DEPTH {
hierarchy.push(node(
if i == 0 { None } else { Some(i - 1) },
None,
None,
if i + 1 == DEPTH { None } else { Some(i + 1) },
));
data.push(NodeData::create_div());
}
let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
let data_ref = NodeDataContainerRef::from_slice(&data);
let depths = hierarchy_ref.get_parents_sorted_by_depth();
let out = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
assert_eq!(out.len(), DEPTH);
for i in 0..DEPTH {
assert_eq!(
out.internal[i],
info(0, true),
"every node in a chain is an only child"
);
}
}
#[test]
fn matches_html_element_empty_path_never_matches() {
assert!(!matches(Vec::new(), 1, None));
assert!(!matches(Vec::new(), 0, Some(CssPathPseudoSelector::Hover)));
}
#[test]
fn matches_html_element_matches_type_class_and_id_on_the_subject() {
assert!(matches(vec![CssPathSelector::Global], 1, None));
assert!(matches(vec![CssPathSelector::Class("a".into())], 1, None));
assert!(!matches(vec![CssPathSelector::Class("a".into())], 3, None));
assert!(matches(vec![CssPathSelector::Id("first".into())], 1, None));
assert!(matches(vec![CssPathSelector::Type(NodeTypeTag::Div)], 1, None));
assert!(!matches(vec![CssPathSelector::Type(NodeTypeTag::Div)], 0, None));
assert!(matches(vec![CssPathSelector::Type(NodeTypeTag::Body)], 0, None));
let div_a = vec![
CssPathSelector::Type(NodeTypeTag::Div),
CssPathSelector::Class("a".into()),
];
assert!(matches(div_a.clone(), 1, None));
assert!(!matches(div_a, 3, None));
}
#[test]
fn matches_html_element_never_matches_an_anonymous_node() {
let (hierarchy, data) = anonymous_fixture();
let hier_items = items(&hierarchy);
let hierarchy_ref = NodeHierarchyRef::from_slice(&hierarchy);
let data_ref = NodeDataContainerRef::from_slice(&data);
let depths = hierarchy_ref.get_parents_sorted_by_depth();
let cascade = construct_html_cascade_tree(&hierarchy_ref, &depths, &data_ref);
for id in [1usize, 2, 4] {
assert!(
!matches_html_element(
&CssPath::new(vec![CssPathSelector::Global]),
NodeId::new(id),
&NodeDataContainerRef::from_slice(&hier_items),
&data_ref,
&cascade.as_ref(),
None,
),
"anonymous node {id} must not be styled"
);
}
for id in [0usize, 3, 5] {
assert!(matches_html_element(
&CssPath::new(vec![CssPathSelector::Global]),
NodeId::new(id),
&NodeDataContainerRef::from_slice(&hier_items),
&data_ref,
&cascade.as_ref(),
None,
));
}
}
#[test]
fn matches_html_element_child_combinator_is_stricter_than_the_descendant_one() {
let child = vec![
CssPathSelector::Type(NodeTypeTag::Body),
CssPathSelector::DirectChildren,
CssPathSelector::Type(NodeTypeTag::P),
];
assert!(!matches(child, 4, None));
let descendant = vec![
CssPathSelector::Type(NodeTypeTag::Body),
CssPathSelector::Children,
CssPathSelector::Type(NodeTypeTag::P),
];
assert!(matches(descendant, 4, None));
let direct = vec![
CssPathSelector::Type(NodeTypeTag::Body),
CssPathSelector::DirectChildren,
CssPathSelector::Type(NodeTypeTag::Div),
CssPathSelector::Class("b".into()),
];
assert!(matches(direct, 3, None));
}
#[test]
fn matches_html_element_descendant_combinator_walks_the_whole_ancestor_chain() {
let close = vec![
CssPathSelector::Class("b".into()),
CssPathSelector::Children,
CssPathSelector::Class("inner".into()),
];
assert!(matches(close, 4, None));
let unrelated = vec![
CssPathSelector::Class("c".into()),
CssPathSelector::Children,
CssPathSelector::Class("inner".into()),
];
assert!(!matches(unrelated, 4, None));
}
#[test]
fn matches_html_element_general_sibling_scans_all_previous_siblings() {
let general = vec![
CssPathSelector::Class("a".into()),
CssPathSelector::GeneralSibling,
CssPathSelector::Class("c".into()),
];
assert!(matches(general, 5, None));
let backwards = vec![
CssPathSelector::Class("c".into()),
CssPathSelector::GeneralSibling,
CssPathSelector::Class("a".into()),
];
assert!(!matches(backwards, 1, None));
}
#[test]
fn matches_html_element_adjacent_sibling_matches_the_immediate_element_sibling() {
let adjacent = vec![
CssPathSelector::Class("b".into()),
CssPathSelector::AdjacentSibling,
CssPathSelector::Class("c".into()),
];
assert!(matches(adjacent, 5, None));
let not_adjacent = vec![
CssPathSelector::Class("a".into()),
CssPathSelector::AdjacentSibling,
CssPathSelector::Class("c".into()),
];
assert!(!matches(not_adjacent, 5, None));
}
#[test]
fn matches_html_element_adjacent_sibling_skips_text_nodes() {
let adjacent = vec![
CssPathSelector::Class("a".into()),
CssPathSelector::AdjacentSibling,
CssPathSelector::Class("b".into()),
];
assert!(
matches(adjacent, 3, None),
"the `+` combinator must ignore non-element (text) siblings"
);
}
#[test]
fn matches_html_element_hover_on_a_single_group_path_needs_the_expected_ending() {
let hover = vec![
CssPathSelector::Class("a".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
];
assert!(matches(hover.clone(), 1, Some(CssPathPseudoSelector::Hover)));
assert!(!matches(hover.clone(), 1, None));
assert!(!matches(hover.clone(), 1, Some(CssPathPseudoSelector::Focus)));
assert!(!matches(hover, 3, Some(CssPathPseudoSelector::Hover)));
}
#[test]
fn matches_html_element_hover_on_a_descendant_path_still_matches() {
let hover_descendant = vec![
CssPathSelector::Type(NodeTypeTag::Body),
CssPathSelector::Children,
CssPathSelector::Class("a".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::Hover),
];
assert!(
matches(hover_descendant, 1, Some(CssPathPseudoSelector::Hover)),
":hover on the subject of a multi-group selector must still match"
);
}
#[test]
fn matches_html_element_structural_pseudos_work_on_the_subject() {
let first = vec![
CssPathSelector::Class("a".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::First),
];
assert!(matches(first, 1, None));
let last = vec![
CssPathSelector::Class("c".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::Last),
];
assert!(matches(last, 5, None));
let nth = vec![
CssPathSelector::Class("b".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
CssNthChildSelector::Number(2),
)),
];
assert!(matches(nth, 3, None));
let wrong_nth = vec![
CssPathSelector::Class("b".into()),
CssPathSelector::PseudoSelector(CssPathPseudoSelector::NthChild(
CssNthChildSelector::Number(3),
)),
];
assert!(!matches(wrong_nth, 3, None));
}
#[test]
fn matches_html_element_root_scope_confines_a_rule_to_its_subtree() {
let scoped = vec![
CssPathSelector::Root(CssScopeRange { start: 3, end: 4 }),
CssPathSelector::Global,
];
assert!(matches(scoped.clone(), 3, None));
assert!(matches(scoped.clone(), 4, None));
assert!(!matches(scoped.clone(), 1, None), "must not leak to a sibling");
assert!(!matches(scoped.clone(), 5, None), "must not leak to a sibling");
assert!(!matches(scoped, 0, None), "must not leak to the parent");
let node_only = vec![
CssPathSelector::Root(CssScopeRange { start: 3, end: 3 }),
CssPathSelector::Global,
];
assert!(matches(node_only.clone(), 3, None));
assert!(!matches(node_only, 4, None), "a node-only scope must not reach children");
}
#[test]
fn matches_html_element_with_a_dangling_combinator_does_not_panic() {
let dangling = vec![
CssPathSelector::Class("a".into()),
CssPathSelector::DirectChildren,
];
assert!(!matches(dangling.clone(), 4, None));
for id in 0..6 {
let _ = matches(dangling.clone(), id, None);
}
}
#[test]
fn matches_html_element_on_a_very_long_selector_chain_terminates() {
let mut path = Vec::new();
for _ in 0..500 {
path.push(CssPathSelector::Type(NodeTypeTag::Body));
path.push(CssPathSelector::Children);
}
path.push(CssPathSelector::Class("inner".into()));
assert!(!matches(path, 4, None));
}
}