css_ast/selector/
nth.rs

1use crate::{CSSInt, diagnostics};
2use css_parse::{
3	Cursor, CursorSink, Kind, KindSet, Parse, Parser, Peek, Result as ParserResult, Span, T, ToCursors, ToSpan,
4	keyword_set,
5};
6use csskit_derives::Visitable;
7
8keyword_set!(pub enum NthKeyword {
9	Odd: "odd",
10	Even: "even",
11});
12
13#[derive(Visitable, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
15#[visit(self)]
16pub enum Nth {
17	Odd(T![Ident]),
18	Even(T![Ident]),
19	Integer(CSSInt),
20	Anb(i32, i32, [Cursor; 4]),
21}
22
23impl<'a> Peek<'a> for Nth {
24	fn peek(p: &Parser<'a>, c: Cursor) -> bool {
25		<T![Number]>::peek(p, c) || NthKeyword::peek(p, c)
26	}
27}
28
29impl<'a> Parse<'a> for Nth {
30	fn parse(p: &mut Parser<'a>) -> ParserResult<Self> {
31		let mut c: Cursor;
32		if p.peek::<T![Number]>() {
33			let number = p.parse::<CSSInt>()?;
34			return Ok(Self::Integer(number));
35		} else if let Some(kw) = p.parse_if_peek::<NthKeyword>()? {
36			match kw {
37				NthKeyword::Even(ident) => return Ok(Self::Even(ident)),
38				NthKeyword::Odd(ident) => return Ok(Self::Odd(ident)),
39			}
40		} else {
41			c = p.parse::<T![Any]>()?.into();
42		}
43
44		let a;
45		let mut b_sign = 0;
46		let mut cursors = [c, Cursor::EMPTY, Cursor::EMPTY, Cursor::EMPTY];
47
48		if c == '+' {
49			let skip = p.set_skip(KindSet::NONE);
50			let next = p.parse::<T![Any]>();
51			p.set_skip(skip);
52			c = next?.into();
53			debug_assert!(cursors[1] == Cursor::EMPTY);
54			cursors[1] = c;
55		}
56		if !matches!(c.token().kind(), Kind::Number | Kind::Dimension | Kind::Ident) {
57			Err(diagnostics::Unexpected(c))?
58		}
59		if c.token().is_float() {
60			Err(diagnostics::ExpectedInt(c))?
61		}
62
63		match p.parse_str_lower(c) {
64			"n-" => {
65				b_sign = -1;
66				a = if c.token().is_int() { c.token().value() as i32 } else { 1 };
67			}
68			anb => {
69				let mut chars = anb.chars();
70				let mut char = chars.next();
71				a = if c.token().is_int() {
72					c.token().value() as i32
73				} else if char == Some('-') {
74					char = chars.next();
75					-1
76				} else {
77					1
78				};
79				if !matches!(char, Some('n') | Some('N')) {
80					Err(diagnostics::Unexpected(c))?
81				}
82				if let Ok(b) = chars.as_str().parse::<i32>() {
83					return Ok(Self::Anb(a, b, cursors));
84				} else if !chars.as_str().is_empty() {
85					Err(diagnostics::Unexpected(c))?
86				}
87			}
88		}
89
90		if b_sign == 0 {
91			if p.peek::<T![+]>() {
92				b_sign = 1;
93				c = p.parse::<T![+]>()?.into();
94				debug_assert!(cursors[2] == Cursor::EMPTY);
95				cursors[2] = c;
96			} else if p.peek::<T![-]>() {
97				b_sign = -1;
98				c = p.parse::<T![-]>()?.into();
99				debug_assert!(cursors[2] == Cursor::EMPTY);
100				cursors[2] = c;
101			}
102		}
103
104		let b = if p.peek::<T![Number]>() {
105			c = p.parse::<T![Number]>()?.into();
106			debug_assert!(cursors[3] == Cursor::EMPTY);
107			cursors[3] = c;
108			if c.token().is_float() {
109				Err(diagnostics::ExpectedInt(c))?
110			}
111			if c.token().has_sign() && b_sign != 0 {
112				Err(diagnostics::ExpectedUnsigned(c))?
113			}
114			if b_sign == 0 {
115				b_sign = 1;
116			}
117			let i = c.token().value();
118			(i.abs() as i32) * b_sign
119		} else {
120			0
121		};
122		Ok(Self::Anb(a, b, cursors))
123	}
124}
125
126impl ToCursors for Nth {
127	fn to_cursors(&self, s: &mut impl CursorSink) {
128		match self {
129			Self::Odd(c) => ToCursors::to_cursors(c, s),
130			Self::Even(c) => ToCursors::to_cursors(c, s),
131			Self::Integer(c) => ToCursors::to_cursors(c, s),
132			Self::Anb(_, _, cursors) => {
133				for c in cursors {
134					if *c != Cursor::EMPTY {
135						s.append(*c);
136					}
137				}
138			}
139		}
140	}
141}
142
143impl ToSpan for Nth {
144	fn to_span(&self) -> Span {
145		match self {
146			Nth::Odd(c) => c.to_span(),
147			Nth::Even(c) => c.to_span(),
148			Nth::Integer(c) => c.to_span(),
149			Nth::Anb(_, _, cursors) => {
150				let mut span = Span::ZERO;
151				for c in cursors {
152					if *c != Cursor::EMPTY {
153						span = span + (*c).into()
154					}
155				}
156				span
157			}
158		}
159	}
160}
161
162#[cfg(test)]
163mod tests {
164	use super::*;
165	use css_parse::{assert_parse, assert_parse_error};
166
167	#[test]
168	fn size_test() {
169		assert_eq!(std::mem::size_of::<Nth>(), 60);
170	}
171
172	#[test]
173	fn test_writes() {
174		assert_parse!(Nth, "odd");
175		assert_parse!(Nth, "ODD");
176		assert_parse!(Nth, "eVeN");
177		assert_parse!(Nth, "5");
178		assert_parse!(Nth, "n");
179		assert_parse!(Nth, "+n");
180		assert_parse!(Nth, "+N");
181		assert_parse!(Nth, "-n");
182		assert_parse!(Nth, "+5");
183		assert_parse!(Nth, "5n");
184		assert_parse!(Nth, "+5n");
185		assert_parse!(Nth, "-5n");
186		assert_parse!(Nth, "n-4");
187		assert_parse!(Nth, "-n-4");
188		assert_parse!(Nth, "+n-4");
189		assert_parse!(Nth, "+n+4");
190		assert_parse!(Nth, "+n-123456789");
191		assert_parse!(Nth, "2n");
192		assert_parse!(Nth, "2n+1");
193		assert_parse!(Nth, "+2n+1");
194		assert_parse!(Nth, "-2n+1");
195		assert_parse!(Nth, "-2n-1");
196		assert_parse!(Nth, "+2n-1");
197		assert_parse!(Nth, "3n+4");
198		assert_parse!(Nth, "3n+1");
199		assert_parse!(Nth, "n+ 3");
200		assert_parse!(Nth, "-n+3");
201
202		// Ported from https://github.com/web-platform-tests/wpt/blob/c1247636413abebe66ca11a2ca3476de771c99cb/css/selectors/parsing/parse-anplusb.html
203		assert_parse!(Nth, "1n+0");
204		assert_parse!(Nth, "n+0");
205		assert_parse!(Nth, "n");
206		assert_parse!(Nth, "-n+0");
207		assert_parse!(Nth, "-n");
208		assert_parse!(Nth, "N");
209		assert_parse!(Nth, "+n+3");
210		assert_parse!(Nth, "+n + 7 ", "+n+ 7");
211		assert_parse!(Nth, "N- 123");
212		assert_parse!(Nth, "n- 10");
213		assert_parse!(Nth, "-n\n- 1", "-n - 1");
214		assert_parse!(Nth, " 23n\n\n+\n\n123 ", "23n+ 123");
215	}
216
217	#[test]
218	fn test_errors() {
219		assert_parse_error!(Nth, "3n + -6");
220		assert_parse_error!(Nth, "3 n");
221		assert_parse_error!(Nth, "+ 2n");
222		assert_parse_error!(Nth, "+ 2");
223
224		// Ported from https://github.com/web-platform-tests/wpt/blob/c1247636413abebe66ca11a2ca3476de771c99cb/css/selectors/parsing/parse-anplusb.html
225		assert_parse_error!(Nth, "n- 1 2");
226		assert_parse_error!(Nth, "n-b1");
227		assert_parse_error!(Nth, "n-+1");
228		assert_parse_error!(Nth, "n-1n");
229		assert_parse_error!(Nth, "-n -b1");
230		assert_parse_error!(Nth, "-1n- b1");
231		assert_parse_error!(Nth, "-n-13b1");
232		assert_parse_error!(Nth, "-n-+1");
233		assert_parse_error!(Nth, "-n+n");
234		assert_parse_error!(Nth, "+ 1n");
235		assert_parse_error!(Nth, "  n +12 3");
236		assert_parse_error!(Nth, "  12 n ");
237		assert_parse_error!(Nth, "+12n-0+1");
238		assert_parse_error!(Nth, "+12N -- 1");
239		assert_parse_error!(Nth, "+12 N ");
240		assert_parse_error!(Nth, "+ n + 7");
241	}
242
243	// #[cfg(feature = "serde")]
244	// #[test]
245	// fn test_serializes() {
246	// 	assert_json!(Nth, "odd", { "node": [2, 1], "start": 0, "end": 3 });
247	// 	assert_json!(Nth, "3n+1", { "node": [3, 1], "start": 0, "end": 4 });
248	// }
249}