css_parse/traits/
semantic_eq.rs

1use css_lexer::{Cursor, Kind};
2
3/// Trait for semantic equality comparison that ignores source positions and whitespace.
4///
5/// This trait provides semantic comparison for CSS AST nodes, comparing their structural
6/// content and meaning rather than their exact representation in source code. Two nodes
7/// are semantically equal if they represent the same CSS construct, regardless of source
8/// position or trivia.
9pub trait SemanticEq {
10	/// Returns `true` if `self` and `other` are semantically equal.
11	fn semantic_eq(&self, other: &Self) -> bool;
12}
13
14// Implement for Cursor - compare tokens without considering source offset
15impl SemanticEq for Cursor {
16	fn semantic_eq(&self, other: &Self) -> bool {
17		// For Delims, we ignore associated whitespace rules since
18		// those are formatting hints, not semantic content
19		match self.token().kind() {
20			Kind::Delim => {
21				self.token().with_associated_whitespace(css_lexer::AssociatedWhitespaceRules::none())
22					== other.token().with_associated_whitespace(css_lexer::AssociatedWhitespaceRules::none())
23			}
24			_ => self.token() == other.token(),
25		}
26	}
27}
28
29impl<T> SemanticEq for Option<T>
30where
31	T: SemanticEq,
32{
33	fn semantic_eq(&self, s: &Self) -> bool {
34		match (self, s) {
35			(Some(a), Some(b)) => a.semantic_eq(b),
36			(None, None) => true,
37			(_, _) => false,
38		}
39	}
40}
41
42impl<'a, T> SemanticEq for bumpalo::collections::Vec<'a, T>
43where
44	T: SemanticEq,
45{
46	fn semantic_eq(&self, s: &Self) -> bool {
47		if self.len() != s.len() {
48			return false;
49		}
50		for i in 0..self.len() {
51			if !self[i].semantic_eq(&s[i]) {
52				return false;
53			}
54		}
55		true
56	}
57}
58
59macro_rules! impl_tuple {
60		($($T:ident [ $A:ident, $B:ident ]),+) => {
61        impl<$($T),*> SemanticEq for ($($T),*)
62        where
63            $($T: SemanticEq,)*
64        {
65            fn semantic_eq(&self, o: &Self) -> bool {
66                let ($($A),*) = self;
67                let ($($B),*) = o;
68                $($A.semantic_eq(&$B))&&*
69            }
70        }
71    };
72}
73
74impl_tuple!(A[sa,oa], B[sb,ob]);
75impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc]);
76impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc], D[sd,od]);
77impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc], D[sd,od], E[se,oe]);
78impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc], D[sd,od], E[se,oe], F[sf,of]);
79impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc], D[sd,od], E[se,oe], F[sf,of], G[sg,og]);
80impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc], D[sd,od], E[se,oe], F[sf,of], G[sg,og], H[sh,oh]);
81impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc], D[sd,od], E[se,oe], F[sf,of], G[sg,og], H[sh,oh], I[si,oi]);
82impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc], D[sd,od], E[se,oe], F[sf,of], G[sg,og], H[sh,oh], I[si,oi], J[sj,oj]);
83impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc], D[sd,od], E[se,oe], F[sf,of], G[sg,og], H[sh,oh], I[si,oi], J[sj,oj], K[sk,ok]);
84impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc], D[sd,od], E[se,oe], F[sf,of], G[sg,og], H[sh,oh], I[si,oi], J[sj,oj], K[sk,ok], L[sl,ol]);
85
86#[cfg(test)]
87mod tests {
88	use super::*;
89	use crate::{ComponentValues, Parse, Parser, ToCursors};
90	use bumpalo::Bump;
91	use css_lexer::EmptyAtomSet;
92
93	fn parse<'a, T: Parse<'a> + ToCursors>(bump: &'a Bump, source: &'a str) -> T {
94		let lexer = css_lexer::Lexer::new(&EmptyAtomSet::ATOMS, source);
95		let mut parser = Parser::new(bump, source, lexer);
96		let result = parser.parse_entirely::<T>();
97		result.output.unwrap()
98	}
99
100	#[test]
101	fn test_cursor_semantic_eq_ignores_offset() {
102		let token = css_lexer::Token::COMMA;
103		let cursor1 = Cursor::new(css_lexer::SourceOffset(0), token);
104		let cursor2 = Cursor::new(css_lexer::SourceOffset(100), token);
105
106		// Should be semantically equal despite different offsets
107		assert!(cursor1.semantic_eq(&cursor2));
108
109		// Standard PartialEq should distinguish them
110		assert_ne!(cursor1, cursor2);
111	}
112
113	#[test]
114	fn test_component_values_ignores_whitespace() {
115		let source1 = "1px solid red";
116		let source2 = "1px  solid  red"; // Extra whitespace
117
118		let bump = Bump::new();
119		let values1 = parse::<ComponentValues>(&bump, source1);
120		let values2 = parse::<ComponentValues>(&bump, source2);
121
122		// Semantically equal despite whitespace
123		assert!(values1.semantic_eq(&values2));
124	}
125
126	#[test]
127	fn test_component_values_different_values() {
128		let source1 = "1px solid red";
129		let source2 = "2px solid red";
130
131		let bump = Bump::new();
132		let values1 = parse::<ComponentValues>(&bump, source1);
133		let values2 = parse::<ComponentValues>(&bump, source2);
134
135		// Should NOT be equal due to different values
136		assert!(!values1.semantic_eq(&values2));
137	}
138}