Skip to main content

css_ast/selector/
nth.rs

1use super::prelude::*;
2
3use crate::CSSInt;
4use css_parse::{CursorSink, Nth as NthTrait, SemanticEq, Span, ToCursors, ToSpan};
5
6#[node]
7#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
8#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
9#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
10#[derive(csskit_derives::NodeWithMetadata)]
11pub enum Nth {
12	Odd(T![Ident]),
13	Even(T![Ident]),
14	Integer(CSSInt),
15	Anb(#[metadata(skip)] i32, #[metadata(skip)] i32, #[metadata(skip)] [Cursor; 4]),
16}
17
18impl<'a> Peek<'a> for Nth {
19	const PEEK_KINDSET: KindSet = <Self as NthTrait<'a>>::NTH_KINDSET;
20}
21
22impl<'a> NthTrait<'a> for Nth {
23	fn peek_odd<I>(p: &Parser<'a, I>, c: Cursor) -> bool
24	where
25		I: Iterator<Item = Cursor> + Clone,
26	{
27		p.to_atom::<CssAtomSet>(c) == CssAtomSet::Odd
28	}
29
30	fn peek_even<I>(p: &Parser<'a, I>, c: Cursor) -> bool
31	where
32		I: Iterator<Item = Cursor> + Clone,
33	{
34		p.to_atom::<CssAtomSet>(c) == CssAtomSet::Even
35	}
36
37	fn build_odd(node: T![Ident]) -> Self {
38		Self::Odd(node)
39	}
40
41	fn build_even(node: T![Ident]) -> Self {
42		Self::Even(node)
43	}
44
45	fn build_integer(node: T![Number]) -> Self {
46		Self::Integer(CSSInt(node))
47	}
48
49	fn build_anb(a: i32, b: i32, cursors: [Cursor; 4]) -> Self {
50		Self::Anb(a, b, cursors)
51	}
52}
53
54impl<'a> Parse<'a> for Nth {
55	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
56	where
57		I: Iterator<Item = Cursor> + Clone,
58	{
59		Self::parse_nth(p)
60	}
61}
62
63impl ToCursors for Nth {
64	fn to_cursors(&self, s: &mut impl CursorSink) {
65		match self {
66			Self::Odd(c) => ToCursors::to_cursors(c, s),
67			Self::Even(c) => ToCursors::to_cursors(c, s),
68			Self::Integer(c) => ToCursors::to_cursors(c, s),
69			Self::Anb(_, _, cursors) => {
70				for c in cursors {
71					if *c != Cursor::EMPTY {
72						s.append(*c);
73					}
74				}
75			}
76		}
77	}
78}
79
80impl Nth {
81	/// Check if the given 1-based index matches this Nth pattern.
82	///
83	/// For example:
84	/// - `odd` matches indices 1, 3, 5, ...
85	/// - `even` matches indices 2, 4, 6, ...
86	/// - `3` matches only index 3
87	/// - `2n+1` matches indices 1, 3, 5, ... (same as odd)
88	/// - `3n` matches indices 3, 6, 9, ...
89	pub fn matches(&self, index: i32) -> bool {
90		match self {
91			Self::Odd(_) => index % 2 == 1,
92			Self::Even(_) => index % 2 == 0,
93			Self::Integer(n) => index == i32::from(*n),
94			Self::Anb(a, b, _) => Self::anb_matches(*a, *b, index),
95		}
96	}
97}
98
99impl SemanticEq for Nth {
100	fn semantic_eq(&self, other: &Self, source_text: &str) -> bool {
101		match (self, other) {
102			(Self::Odd(a), Self::Odd(b)) => a.semantic_eq(b, source_text),
103			(Self::Even(a), Self::Even(b)) => a.semantic_eq(b, source_text),
104			(Self::Integer(a), Self::Integer(b)) => a.semantic_eq(b, source_text),
105			(Self::Anb(a1, b1, _), Self::Anb(a2, b2, _)) => a1 == a2 && b1 == b2,
106			_ => false,
107		}
108	}
109}
110
111impl ToSpan for Nth {
112	fn to_span(&self) -> Span {
113		match self {
114			Nth::Odd(c) => c.to_span(),
115			Nth::Even(c) => c.to_span(),
116			Nth::Integer(c) => c.to_span(),
117			Nth::Anb(_, _, cursors) => {
118				let mut span = Span::DUMMY;
119				for c in cursors {
120					if *c != Cursor::EMPTY {
121						span = span + (*c).into()
122					}
123				}
124				span
125			}
126		}
127	}
128}
129
130#[cfg(test)]
131mod tests {
132	use super::*;
133	use crate::CssAtomSet;
134	use css_parse::{assert_parse, assert_parse_error, assert_parse_span};
135
136	#[test]
137	fn test_writes() {
138		assert_parse!(CssAtomSet::ATOMS, Nth, "odd");
139		assert_parse!(CssAtomSet::ATOMS, Nth, "ODD");
140		assert_parse!(CssAtomSet::ATOMS, Nth, "eVeN");
141		assert_parse!(CssAtomSet::ATOMS, Nth, "5");
142		assert_parse!(CssAtomSet::ATOMS, Nth, "n");
143		assert_parse!(CssAtomSet::ATOMS, Nth, "+n");
144		assert_parse!(CssAtomSet::ATOMS, Nth, "+N");
145		assert_parse!(CssAtomSet::ATOMS, Nth, "-n");
146		assert_parse!(CssAtomSet::ATOMS, Nth, "+5");
147		assert_parse!(CssAtomSet::ATOMS, Nth, "5n");
148		assert_parse!(CssAtomSet::ATOMS, Nth, "+5n");
149		assert_parse!(CssAtomSet::ATOMS, Nth, "-5n");
150		assert_parse!(CssAtomSet::ATOMS, Nth, "n-4");
151		assert_parse!(CssAtomSet::ATOMS, Nth, "-n-4");
152		assert_parse!(CssAtomSet::ATOMS, Nth, "+n-4");
153		assert_parse!(CssAtomSet::ATOMS, Nth, "+n+4");
154		assert_parse!(CssAtomSet::ATOMS, Nth, "+n-123456789");
155		assert_parse!(CssAtomSet::ATOMS, Nth, "2n");
156		assert_parse!(CssAtomSet::ATOMS, Nth, "2n+1");
157		assert_parse!(CssAtomSet::ATOMS, Nth, "+2n+1");
158		assert_parse!(CssAtomSet::ATOMS, Nth, "-2n+1");
159		assert_parse!(CssAtomSet::ATOMS, Nth, "-2n-1");
160		assert_parse!(CssAtomSet::ATOMS, Nth, "+2n-1");
161		assert_parse!(CssAtomSet::ATOMS, Nth, "3n+4");
162		assert_parse!(CssAtomSet::ATOMS, Nth, "3n+1");
163		assert_parse!(CssAtomSet::ATOMS, Nth, "n+ 3");
164		assert_parse!(CssAtomSet::ATOMS, Nth, "-n+3");
165
166		// Ported from https://github.com/web-platform-tests/wpt/blob/c1247636413abebe66ca11a2ca3476de771c99cb/css/selectors/parsing/parse-anplusb.html
167		assert_parse!(CssAtomSet::ATOMS, Nth, "1n+0");
168		assert_parse!(CssAtomSet::ATOMS, Nth, "n+0");
169		assert_parse!(CssAtomSet::ATOMS, Nth, "n");
170		assert_parse!(CssAtomSet::ATOMS, Nth, "-n+0");
171		assert_parse!(CssAtomSet::ATOMS, Nth, "-n");
172		assert_parse!(CssAtomSet::ATOMS, Nth, "N");
173		assert_parse!(CssAtomSet::ATOMS, Nth, "+n+3");
174		assert_parse!(CssAtomSet::ATOMS, Nth, "+n + 7 ");
175		assert_parse!(CssAtomSet::ATOMS, Nth, "N- 123");
176		assert_parse!(CssAtomSet::ATOMS, Nth, "n- 10");
177		assert_parse!(CssAtomSet::ATOMS, Nth, "-n\n- 1");
178		assert_parse!(CssAtomSet::ATOMS, Nth, " 23n\n\n+\n\n123 ");
179		// A `-` closing the unit makes b negative, whether or not a is signed or spelled out.
180		assert_parse!(CssAtomSet::ATOMS, Nth, "-n- 4");
181		assert_parse!(CssAtomSet::ATOMS, Nth, "5n- 4");
182		assert_parse!(CssAtomSet::ATOMS, Nth, "-5n- 4");
183	}
184
185	#[test]
186	fn test_spans() {
187		assert_parse_span!(
188			CssAtomSet::ATOMS,
189			Nth,
190			r#"
191			odd
192			^^^
193		"#
194		);
195		assert_parse_span!(
196			CssAtomSet::ATOMS,
197			Nth,
198			r#"
199			5
200			^
201		"#
202		);
203		assert_parse_span!(
204			CssAtomSet::ATOMS,
205			Nth,
206			r#"
207			2n
208			^^
209		"#
210		);
211		assert_parse_span!(
212			CssAtomSet::ATOMS,
213			Nth,
214			r#"
215			-2n+1 foo
216			^^^^^
217		"#
218		);
219		assert_parse_span!(
220			CssAtomSet::ATOMS,
221			Nth,
222			r#"
223			n- 10
224			^^^^^
225		"#
226		);
227	}
228
229	#[test]
230	fn test_errors() {
231		assert_parse_error!(CssAtomSet::ATOMS, Nth, "3n + -6");
232		assert_parse_error!(CssAtomSet::ATOMS, Nth, "3 n");
233		assert_parse_error!(CssAtomSet::ATOMS, Nth, "+ 2n");
234		assert_parse_error!(CssAtomSet::ATOMS, Nth, "+ 2");
235
236		// Ported from https://github.com/web-platform-tests/wpt/blob/c1247636413abebe66ca11a2ca3476de771c99cb/css/selectors/parsing/parse-anplusb.html
237		assert_parse_error!(CssAtomSet::ATOMS, Nth, "n- 1 2");
238		assert_parse_error!(CssAtomSet::ATOMS, Nth, "n-b1");
239		assert_parse_error!(CssAtomSet::ATOMS, Nth, "n-+1");
240		assert_parse_error!(CssAtomSet::ATOMS, Nth, "n-1n");
241		assert_parse_error!(CssAtomSet::ATOMS, Nth, "-n -b1");
242		assert_parse_error!(CssAtomSet::ATOMS, Nth, "-1n- b1");
243		assert_parse_error!(CssAtomSet::ATOMS, Nth, "-n-13b1");
244		assert_parse_error!(CssAtomSet::ATOMS, Nth, "-n-+1");
245		assert_parse_error!(CssAtomSet::ATOMS, Nth, "-n+n");
246		assert_parse_error!(CssAtomSet::ATOMS, Nth, "+ 1n");
247		assert_parse_error!(CssAtomSet::ATOMS, Nth, "  n +12 3");
248		assert_parse_error!(CssAtomSet::ATOMS, Nth, "  12 n ");
249		assert_parse_error!(CssAtomSet::ATOMS, Nth, "+12n-0+1");
250		assert_parse_error!(CssAtomSet::ATOMS, Nth, "+12N -- 1");
251		assert_parse_error!(CssAtomSet::ATOMS, Nth, "+12 N ");
252		assert_parse_error!(CssAtomSet::ATOMS, Nth, "+ n + 7");
253	}
254
255	// #[cfg(feature = "serde")]
256	// #[test]
257	// fn test_serializes() {
258	// 	assert_json!(Nth, "odd", { "node": [2, 1], "start": 0, "end": 3 });
259	// 	assert_json!(Nth, "3n+1", { "node": [3, 1], "start": 0, "end": 4 });
260	// }
261}