css_ast/selector/
nth.rs

1use crate::{CSSInt, CssAtomSet, CssDiagnostic};
2use css_parse::{
3	Cursor, CursorSink, Diagnostic, Kind, KindSet, Parse, Parser, Peek, Result as ParserResult, SemanticEq, Span, T,
4	ToCursors, ToSpan,
5};
6
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(i32, i32, [Cursor; 4]),
16}
17
18impl<'a> Peek<'a> for Nth {
19	fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
20	where
21		I: Iterator<Item = Cursor> + Clone,
22	{
23		<T![Number]>::peek(p, c) || <T![Ident]>::peek(p, c)
24	}
25}
26
27impl<'a> Parse<'a> for Nth {
28	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
29	where
30		I: Iterator<Item = Cursor> + Clone,
31	{
32		if p.peek::<CSSInt>() {
33			return Ok(Self::Integer(p.parse::<CSSInt>()?));
34		} else if p.peek::<T![Ident]>() {
35			let peek_cursor = p.peek_n(1);
36			let atom = p.to_atom::<CssAtomSet>(peek_cursor);
37			if atom == CssAtomSet::Odd {
38				let ident = p.parse::<T![Ident]>()?;
39				return Ok(Self::Odd(ident));
40			} else if atom == CssAtomSet::Even {
41				let ident = p.parse::<T![Ident]>()?;
42				return Ok(Self::Even(ident));
43			}
44		}
45
46		let mut c = p.next();
47
48		let a;
49		let mut b_sign = 0;
50		let mut cursors = [c, Cursor::EMPTY, Cursor::EMPTY, Cursor::EMPTY];
51
52		if c == '+' {
53			let skip = p.set_skip(KindSet::NONE);
54			c = p.next();
55			p.set_skip(skip);
56			debug_assert!(cursors[1] == Cursor::EMPTY);
57			cursors[1] = c;
58		}
59		if !matches!(c.token().kind(), Kind::Number | Kind::Dimension | Kind::Ident) {
60			Err(Diagnostic::new(c, Diagnostic::unexpected))?
61		}
62		if c.token().is_float() {
63			Err(Diagnostic::new(c, Diagnostic::expected_int))?
64		}
65
66		if p.equals_atom(c, &CssAtomSet::_NDash) {
67			b_sign = -1;
68			a = if c.token().is_int() { c.token().value() as i32 } else { 1 };
69		} else {
70			let source_cursor = p.to_source_cursor(c);
71			let anb = source_cursor.parse(p.bump());
72			let mut chars = anb.chars();
73			let mut char = chars.next();
74			a = if c.token().is_int() {
75				c.token().value() as i32
76			} else if char == Some('-') {
77				char = chars.next();
78				-1
79			} else {
80				1
81			};
82			if !matches!(char, Some('n') | Some('N')) {
83				Err(Diagnostic::new(c, Diagnostic::unexpected))?
84			}
85			if let Ok(b) = chars.as_str().parse::<i32>() {
86				return Ok(Self::Anb(a, b, cursors));
87			} else if !chars.as_str().is_empty() {
88				Err(Diagnostic::new(c, Diagnostic::unexpected))?
89			}
90		}
91
92		if b_sign == 0 {
93			if p.peek::<T![+]>() {
94				b_sign = 1;
95				c = p.parse::<T![+]>()?.into();
96				debug_assert!(cursors[2] == Cursor::EMPTY);
97				cursors[2] = c;
98			} else if p.peek::<T![-]>() {
99				b_sign = -1;
100				c = p.parse::<T![-]>()?.into();
101				debug_assert!(cursors[2] == Cursor::EMPTY);
102				cursors[2] = c;
103			}
104		}
105
106		let b = if p.peek::<T![Number]>() {
107			c = p.parse::<T![Number]>()?.into();
108			debug_assert!(cursors[3] == Cursor::EMPTY);
109			if c.token().is_float() {
110				Err(Diagnostic::new(c, Diagnostic::expected_int))?
111			}
112			if c.token().has_sign() && b_sign != 0 {
113				Err(Diagnostic::new(c, Diagnostic::expected_unsigned))?
114			}
115			// If the number has a sign (like +1 or -1), mark it as required for minification
116			if c.token().has_sign() {
117				c = c.with_sign_required();
118			}
119			cursors[3] = c;
120			if b_sign == 0 {
121				b_sign = 1;
122			}
123			let i = c.token().value();
124			(i.abs() as i32) * b_sign
125		} else {
126			0
127		};
128		Ok(Self::Anb(a, b, cursors))
129	}
130}
131
132impl ToCursors for Nth {
133	fn to_cursors(&self, s: &mut impl CursorSink) {
134		match self {
135			Self::Odd(c) => ToCursors::to_cursors(c, s),
136			Self::Even(c) => ToCursors::to_cursors(c, s),
137			Self::Integer(c) => ToCursors::to_cursors(c, s),
138			Self::Anb(_, _, cursors) => {
139				for c in cursors {
140					if *c != Cursor::EMPTY {
141						s.append(*c);
142					}
143				}
144			}
145		}
146	}
147}
148
149impl Nth {
150	/// Check if the given 1-based index matches this Nth pattern.
151	///
152	/// For example:
153	/// - `odd` matches indices 1, 3, 5, ...
154	/// - `even` matches indices 2, 4, 6, ...
155	/// - `3` matches only index 3
156	/// - `2n+1` matches indices 1, 3, 5, ... (same as odd)
157	/// - `3n` matches indices 3, 6, 9, ...
158	pub fn matches(&self, index: i32) -> bool {
159		match self {
160			Self::Odd(_) => index % 2 == 1,
161			Self::Even(_) => index % 2 == 0,
162			Self::Integer(n) => index == i32::from(*n),
163			Self::Anb(a, b, _) => {
164				if *a == 0 {
165					// 0n+b just matches index b
166					index == *b
167				} else {
168					// Check if (index - b) / a is a non-negative integer
169					let diff = index - b;
170					diff % a == 0 && diff / a >= 0
171				}
172			}
173		}
174	}
175}
176
177impl SemanticEq for Nth {
178	fn semantic_eq(&self, other: &Self) -> bool {
179		match (self, other) {
180			(Self::Odd(a), Self::Odd(b)) => a.semantic_eq(b),
181			(Self::Even(a), Self::Even(b)) => a.semantic_eq(b),
182			(Self::Integer(a), Self::Integer(b)) => a.semantic_eq(b),
183			(Self::Anb(a1, b1, _), Self::Anb(a2, b2, _)) => a1 == a2 && b1 == b2,
184			_ => false,
185		}
186	}
187}
188
189impl ToSpan for Nth {
190	fn to_span(&self) -> Span {
191		match self {
192			Nth::Odd(c) => c.to_span(),
193			Nth::Even(c) => c.to_span(),
194			Nth::Integer(c) => c.to_span(),
195			Nth::Anb(_, _, cursors) => {
196				let mut span = Span::ZERO;
197				for c in cursors {
198					if *c != Cursor::EMPTY {
199						span = span + (*c).into()
200					}
201				}
202				span
203			}
204		}
205	}
206}
207
208#[cfg(test)]
209mod tests {
210	use super::*;
211	use crate::CssAtomSet;
212	use css_parse::{assert_parse, assert_parse_error};
213
214	#[test]
215	fn size_test() {
216		assert_eq!(std::mem::size_of::<Nth>(), 60);
217	}
218
219	#[test]
220	fn test_writes() {
221		assert_parse!(CssAtomSet::ATOMS, Nth, "odd");
222		assert_parse!(CssAtomSet::ATOMS, Nth, "ODD");
223		assert_parse!(CssAtomSet::ATOMS, Nth, "eVeN");
224		assert_parse!(CssAtomSet::ATOMS, Nth, "5");
225		assert_parse!(CssAtomSet::ATOMS, Nth, "n");
226		assert_parse!(CssAtomSet::ATOMS, Nth, "+n");
227		assert_parse!(CssAtomSet::ATOMS, Nth, "+N");
228		assert_parse!(CssAtomSet::ATOMS, Nth, "-n");
229		assert_parse!(CssAtomSet::ATOMS, Nth, "+5");
230		assert_parse!(CssAtomSet::ATOMS, Nth, "5n");
231		assert_parse!(CssAtomSet::ATOMS, Nth, "+5n");
232		assert_parse!(CssAtomSet::ATOMS, Nth, "-5n");
233		assert_parse!(CssAtomSet::ATOMS, Nth, "n-4");
234		assert_parse!(CssAtomSet::ATOMS, Nth, "-n-4");
235		assert_parse!(CssAtomSet::ATOMS, Nth, "+n-4");
236		assert_parse!(CssAtomSet::ATOMS, Nth, "+n+4");
237		assert_parse!(CssAtomSet::ATOMS, Nth, "+n-123456789");
238		assert_parse!(CssAtomSet::ATOMS, Nth, "2n");
239		assert_parse!(CssAtomSet::ATOMS, Nth, "2n+1");
240		assert_parse!(CssAtomSet::ATOMS, Nth, "+2n+1");
241		assert_parse!(CssAtomSet::ATOMS, Nth, "-2n+1");
242		assert_parse!(CssAtomSet::ATOMS, Nth, "-2n-1");
243		assert_parse!(CssAtomSet::ATOMS, Nth, "+2n-1");
244		assert_parse!(CssAtomSet::ATOMS, Nth, "3n+4");
245		assert_parse!(CssAtomSet::ATOMS, Nth, "3n+1");
246		assert_parse!(CssAtomSet::ATOMS, Nth, "n+ 3");
247		assert_parse!(CssAtomSet::ATOMS, Nth, "-n+3");
248
249		// Ported from https://github.com/web-platform-tests/wpt/blob/c1247636413abebe66ca11a2ca3476de771c99cb/css/selectors/parsing/parse-anplusb.html
250		assert_parse!(CssAtomSet::ATOMS, Nth, "1n+0");
251		assert_parse!(CssAtomSet::ATOMS, Nth, "n+0");
252		assert_parse!(CssAtomSet::ATOMS, Nth, "n");
253		assert_parse!(CssAtomSet::ATOMS, Nth, "-n+0");
254		assert_parse!(CssAtomSet::ATOMS, Nth, "-n");
255		assert_parse!(CssAtomSet::ATOMS, Nth, "N");
256		assert_parse!(CssAtomSet::ATOMS, Nth, "+n+3");
257		assert_parse!(CssAtomSet::ATOMS, Nth, "+n + 7 ");
258		assert_parse!(CssAtomSet::ATOMS, Nth, "N- 123");
259		assert_parse!(CssAtomSet::ATOMS, Nth, "n- 10");
260		assert_parse!(CssAtomSet::ATOMS, Nth, "-n\n- 1");
261		assert_parse!(CssAtomSet::ATOMS, Nth, " 23n\n\n+\n\n123 ");
262	}
263
264	#[test]
265	fn test_errors() {
266		assert_parse_error!(CssAtomSet::ATOMS, Nth, "3n + -6");
267		assert_parse_error!(CssAtomSet::ATOMS, Nth, "3 n");
268		assert_parse_error!(CssAtomSet::ATOMS, Nth, "+ 2n");
269		assert_parse_error!(CssAtomSet::ATOMS, Nth, "+ 2");
270
271		// Ported from https://github.com/web-platform-tests/wpt/blob/c1247636413abebe66ca11a2ca3476de771c99cb/css/selectors/parsing/parse-anplusb.html
272		assert_parse_error!(CssAtomSet::ATOMS, Nth, "n- 1 2");
273		assert_parse_error!(CssAtomSet::ATOMS, Nth, "n-b1");
274		assert_parse_error!(CssAtomSet::ATOMS, Nth, "n-+1");
275		assert_parse_error!(CssAtomSet::ATOMS, Nth, "n-1n");
276		assert_parse_error!(CssAtomSet::ATOMS, Nth, "-n -b1");
277		assert_parse_error!(CssAtomSet::ATOMS, Nth, "-1n- b1");
278		assert_parse_error!(CssAtomSet::ATOMS, Nth, "-n-13b1");
279		assert_parse_error!(CssAtomSet::ATOMS, Nth, "-n-+1");
280		assert_parse_error!(CssAtomSet::ATOMS, Nth, "-n+n");
281		assert_parse_error!(CssAtomSet::ATOMS, Nth, "+ 1n");
282		assert_parse_error!(CssAtomSet::ATOMS, Nth, "  n +12 3");
283		assert_parse_error!(CssAtomSet::ATOMS, Nth, "  12 n ");
284		assert_parse_error!(CssAtomSet::ATOMS, Nth, "+12n-0+1");
285		assert_parse_error!(CssAtomSet::ATOMS, Nth, "+12N -- 1");
286		assert_parse_error!(CssAtomSet::ATOMS, Nth, "+12 N ");
287		assert_parse_error!(CssAtomSet::ATOMS, Nth, "+ n + 7");
288	}
289
290	// #[cfg(feature = "serde")]
291	// #[test]
292	// fn test_serializes() {
293	// 	assert_json!(Nth, "odd", { "node": [2, 1], "start": 0, "end": 3 });
294	// 	assert_json!(Nth, "3n+1", { "node": [3, 1], "start": 0, "end": 4 });
295	// }
296}