Skip to main content

css_parse/traits/
semantic_eq.rs

1use crate::Vec;
2use allocator_api2::alloc::Allocator;
3use css_lexer::{AssociatedWhitespaceRules, Cursor};
4
5/// Trait for semantic equality comparison that ignores source positions and whitespace.
6///
7/// This trait provides semantic comparison for CSS AST nodes, comparing their structural
8/// content and meaning rather than their exact representation in source code. Two nodes
9/// are semantically equal if they represent the same CSS construct, regardless of source
10/// position or trivia.
11pub trait SemanticEq {
12	/// Returns `true` if `self` and `other` are semantically equal.
13	fn semantic_eq(&self, other: &Self) -> bool;
14}
15
16// Implement for Cursor - compare tokens without considering source offset
17impl SemanticEq for Cursor {
18	fn semantic_eq(&self, other: &Self) -> bool {
19		// Associated whitespace rules are formatting hints, not semantic content, so ignore
20		// them. `with_associated_whitespace` is a no-op for kinds that don't carry such rules
21		// (only "delim-like" kinds do: Delim, Colon, Semicolon, Comma, and the paren/curly/
22		// square brackets), so this is safe to apply unconditionally.
23		self.token().with_associated_whitespace(AssociatedWhitespaceRules::none())
24			== other.token().with_associated_whitespace(AssociatedWhitespaceRules::none())
25	}
26}
27
28impl<T> SemanticEq for Option<T>
29where
30	T: SemanticEq,
31{
32	fn semantic_eq(&self, s: &Self) -> bool {
33		match (self, s) {
34			(Some(a), Some(b)) => a.semantic_eq(b),
35			(None, None) => true,
36			(_, _) => false,
37		}
38	}
39}
40
41impl<'a, T, A: Allocator> SemanticEq for Vec<'a, T, A>
42where
43	T: SemanticEq,
44{
45	fn semantic_eq(&self, s: &Self) -> bool {
46		if self.len() != s.len() {
47			return false;
48		}
49		for i in 0..self.len() {
50			if !self[i].semantic_eq(&s[i]) {
51				return false;
52			}
53		}
54		true
55	}
56}
57
58macro_rules! impl_tuple {
59		($($T:ident [ $A:ident, $B:ident ]),+) => {
60        impl<$($T),*> SemanticEq for ($($T),*)
61        where
62            $($T: SemanticEq,)*
63        {
64            fn semantic_eq(&self, o: &Self) -> bool {
65                let ($($A),*) = self;
66                let ($($B),*) = o;
67                $($A.semantic_eq(&$B))&&*
68            }
69        }
70    };
71}
72
73impl_tuple!(A[sa,oa], B[sb,ob]);
74impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc]);
75impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc], D[sd,od]);
76impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc], D[sd,od], E[se,oe]);
77impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc], D[sd,od], E[se,oe], F[sf,of]);
78impl_tuple!(A[sa,oa], B[sb,ob], C[sc,oc], D[sd,od], E[se,oe], F[sf,of], G[sg,og]);
79impl_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]);
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], I[si,oi]);
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], J[sj,oj]);
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], K[sk,ok]);
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], L[sl,ol]);
84
85#[cfg(test)]
86mod tests {
87	use super::*;
88	use crate::Arena;
89	use crate::{ComponentValues, Parse, Parser, ToCursors};
90	use css_lexer::EmptyAtomSet;
91
92	fn parse<'a, T: Parse<'a> + ToCursors>(alloc: &'a Arena, source: &'a str) -> T {
93		let lexer = css_lexer::Lexer::new(&EmptyAtomSet::ATOMS, source);
94		let mut parser = Parser::new(alloc, source, lexer);
95		let result = parser.parse_entirely::<T>();
96		result.output.unwrap()
97	}
98
99	#[test]
100	fn test_cursor_semantic_eq_ignores_offset() {
101		let token = css_lexer::Token::COMMA;
102		let cursor1 = Cursor::new(css_lexer::SourceOffset(0), token);
103		let cursor2 = Cursor::new(css_lexer::SourceOffset(100), token);
104
105		// Should be semantically equal despite different offsets
106		assert!(cursor1.semantic_eq(&cursor2));
107
108		// Standard PartialEq should distinguish them
109		assert_ne!(cursor1, cursor2);
110	}
111
112	#[test]
113	fn test_cursor_semantic_eq_ignores_associated_whitespace_for_all_delim_like_kinds() {
114		// Colon, Semicolon, Comma, and the paren/curly/square brackets share Delim's bit
115		// layout and can also carry associated-whitespace formatting hints. Those hints are
116		// not semantic content, so two tokens differing only by them must compare equal.
117		for token in [
118			css_lexer::Token::COLON,
119			css_lexer::Token::SEMICOLON,
120			css_lexer::Token::COMMA,
121			css_lexer::Token::LEFT_PAREN,
122			css_lexer::Token::RIGHT_PAREN,
123			css_lexer::Token::LEFT_CURLY,
124			css_lexer::Token::RIGHT_CURLY,
125			css_lexer::Token::LEFT_SQUARE,
126			css_lexer::Token::RIGHT_SQUARE,
127		] {
128			let plain = Cursor::new(css_lexer::SourceOffset(0), token);
129			let with_whitespace_rule = Cursor::new(
130				css_lexer::SourceOffset(0),
131				token.with_associated_whitespace(css_lexer::AssociatedWhitespaceRules::EnforceBefore),
132			);
133			assert!(
134				plain.semantic_eq(&with_whitespace_rule),
135				"{:?} should be semantic_eq regardless of associated whitespace",
136				token.kind()
137			);
138		}
139	}
140
141	#[test]
142	fn test_component_values_ignores_whitespace() {
143		let source1 = "1px solid red";
144		let source2 = "1px  solid  red"; // Extra whitespace
145
146		let alloc = Arena::new();
147		let values1 = parse::<ComponentValues>(&alloc, source1);
148		let values2 = parse::<ComponentValues>(&alloc, source2);
149
150		// Semantically equal despite whitespace
151		assert!(values1.semantic_eq(&values2));
152	}
153
154	#[test]
155	fn test_component_values_different_values() {
156		let source1 = "1px solid red";
157		let source2 = "2px solid red";
158
159		let alloc = Arena::new();
160		let values1 = parse::<ComponentValues>(&alloc, source1);
161		let values2 = parse::<ComponentValues>(&alloc, source2);
162
163		// Should NOT be equal due to different values
164		assert!(!values1.semantic_eq(&values2));
165	}
166}