css_ast/selector/
functional_pseudo_class.rs

1use crate::CssAtomSet;
2use css_lexer::Kind;
3use css_parse::{CommaSeparated, Cursor, Diagnostic, Parse, Parser, Peek, Result as ParserResult, State, T};
4use csskit_derives::{Parse, Peek, SemanticEq, ToCursors, ToSpan};
5
6use super::{ForgivingSelector, Nth, RelativeSelector, SelectorList};
7
8macro_rules! apply_functional_pseudo_class {
9	($macro: ident) => {
10		$macro! {
11			Dir(DirPseudoFunction) CssAtomSet::Dir,
12			Has(HasPseudoFunction<'a>) CssAtomSet::Has,
13			Heading(HeadingPseudoFunction<'a>) CssAtomSet::Heading,
14			Host(HostPseudoFunction<'a>) CssAtomSet::Host,
15			HostContext(HostContextPseudoFunction<'a>) CssAtomSet::HostContext,
16			Is(IsPseudoFunction<'a>) CssAtomSet::Is,
17			Lang(LangPseudoFunction<'a>) CssAtomSet::Lang,
18			Not(NotPseudoFunction<'a>) CssAtomSet::Not,
19			NthChild(NthChildPseudoFunction) CssAtomSet::NthChild,
20			NthCol(NthColPseudoFunction) CssAtomSet::NthCol,
21			NthLastChild(NthLastChildPseudoFunction) CssAtomSet::NthLastChild,
22			NthLastCol(NthLastColPseudoFunction) CssAtomSet::NthLastCol,
23			NthLastOfType(NthLastOfTypePseudoFunction) CssAtomSet::NthLastOfType,
24			NthOfType(NthOfTypePseudoFunction) CssAtomSet::NthOfType,
25			State(StatePseudoFunction) CssAtomSet::State,
26			Where(WherePseudoFunction<'a>) CssAtomSet::Where,
27		}
28	};
29}
30
31macro_rules! define_functional_pseudo_class {
32	( $($ident: ident($ty: ty) $pat: pat $(,)*)+ ) => {
33		#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
34		#[derive( ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
35		#[cfg_attr(feature = "css_feature_data", derive(::csskit_derives::ToCSSFeature), css_feature("css.selectors"))]
36		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
37		pub enum FunctionalPseudoClass<'a> {
38			$($ident($ty),)+
39		}
40	}
41}
42apply_functional_pseudo_class!(define_functional_pseudo_class);
43
44impl<'a> Peek<'a> for FunctionalPseudoClass<'a> {
45	fn peek<I>(p: &Parser<'a, I>, c: css_lexer::Cursor) -> bool
46	where
47		I: Iterator<Item = Cursor> + Clone,
48	{
49		<T![:]>::peek(p, c) && p.peek_n(2) == Kind::Function
50	}
51}
52
53impl<'a> Parse<'a> for FunctionalPseudoClass<'a> {
54	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
55	where
56		I: Iterator<Item = Cursor> + Clone,
57	{
58		macro_rules! match_keyword {
59			( $($ident: ident($ty: ty) $pat: pat $(,)*)+ ) => {
60				match p.to_atom::<CssAtomSet>(p.peek_n(2)) {
61					CssAtomSet::Has if p.is(State::DisallowRelativeSelector) => {
62						Err(Diagnostic::new(p.next(), Diagnostic::unexpected))?
63					}
64					$($pat => p.parse::<$ty>().map(Self::$ident),)+
65					_ => Err(Diagnostic::new(p.next(), Diagnostic::unexpected))?
66				}
67			}
68		}
69		apply_functional_pseudo_class!(match_keyword)
70	}
71}
72
73#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
75#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
76#[derive(csskit_derives::NodeWithMetadata)]
77pub struct DirPseudoFunction {
78	pub colon: T![:],
79	#[atom(CssAtomSet::Dir)]
80	pub function: T![Function],
81	pub value: DirValue,
82	pub close: Option<T![')']>,
83}
84
85#[derive(Parse, Peek, ToSpan, ToCursors, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
87#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(skip))]
88#[derive(csskit_derives::NodeWithMetadata)]
89pub enum DirValue {
90	#[atom(CssAtomSet::Rtl)]
91	Rtl(T![Ident]),
92	#[atom(CssAtomSet::Ltr)]
93	Ltr(T![Ident]),
94}
95
96#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
98#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
99#[derive(csskit_derives::NodeWithMetadata)]
100pub struct HasPseudoFunction<'a> {
101	#[cfg_attr(feature = "visitable", visit(skip))]
102	pub colon: T![:],
103	#[cfg_attr(feature = "visitable", visit(skip))]
104	#[atom(CssAtomSet::Has)]
105	pub function: T![Function],
106	#[parse(state = State::DisallowRelativeSelector)]
107	pub value: RelativeSelector<'a>,
108	#[cfg_attr(feature = "visitable", visit(skip))]
109	pub close: Option<T![')']>,
110}
111
112#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
114#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
115#[derive(csskit_derives::NodeWithMetadata)]
116pub struct HostPseudoFunction<'a> {
117	#[cfg_attr(feature = "visitable", visit(skip))]
118	pub colon: T![:],
119	#[cfg_attr(feature = "visitable", visit(skip))]
120	#[atom(CssAtomSet::Host)]
121	pub function: T![Function],
122	pub value: SelectorList<'a>,
123	#[cfg_attr(feature = "visitable", visit(skip))]
124	pub close: Option<T![')']>,
125}
126
127#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
128#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
129#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
130#[derive(csskit_derives::NodeWithMetadata)]
131pub struct HostContextPseudoFunction<'a> {
132	#[cfg_attr(feature = "visitable", visit(skip))]
133	pub colon: T![:],
134	#[cfg_attr(feature = "visitable", visit(skip))]
135	#[atom(CssAtomSet::HostContext)]
136	pub function: T![Function],
137	pub value: SelectorList<'a>,
138	#[cfg_attr(feature = "visitable", visit(skip))]
139	pub close: Option<T![')']>,
140}
141
142#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
144#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
145#[derive(csskit_derives::NodeWithMetadata)]
146pub struct IsPseudoFunction<'a> {
147	#[cfg_attr(feature = "visitable", visit(skip))]
148	pub colon: T![:],
149	#[cfg_attr(feature = "visitable", visit(skip))]
150	#[atom(CssAtomSet::Is)]
151	pub function: T![Function],
152	pub value: ForgivingSelector<'a>,
153	#[cfg_attr(feature = "visitable", visit(skip))]
154	pub close: Option<T![')']>,
155}
156
157#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
158#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
159#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
160#[derive(csskit_derives::NodeWithMetadata)]
161pub struct LangPseudoFunction<'a> {
162	pub colon: T![:],
163	#[atom(CssAtomSet::Lang)]
164	pub function: T![Function],
165	pub value: LangValues<'a>,
166	pub close: Option<T![')']>,
167}
168
169#[derive(ToSpan, Parse, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
170#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
171pub struct LangValues<'a>(pub CommaSeparated<'a, LangValue>);
172
173#[derive(Parse, ToSpan, Peek, ToCursors, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
174#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
175pub enum LangValue {
176	Ident(T![Ident]),
177	String(T![String]),
178}
179
180#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
181#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
182#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
183#[derive(csskit_derives::NodeWithMetadata)]
184pub struct NotPseudoFunction<'a> {
185	#[cfg_attr(feature = "visitable", visit(skip))]
186	pub colon: T![:],
187	#[cfg_attr(feature = "visitable", visit(skip))]
188	#[atom(CssAtomSet::Not)]
189	pub function: T![Function],
190	pub value: SelectorList<'a>,
191	#[cfg_attr(feature = "visitable", visit(skip))]
192	pub close: Option<T![')']>,
193}
194
195#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
196#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
197#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
198#[derive(csskit_derives::NodeWithMetadata)]
199pub struct NthChildPseudoFunction {
200	#[cfg_attr(feature = "visitable", visit(skip))]
201	pub colon: T![:],
202	#[cfg_attr(feature = "visitable", visit(skip))]
203	#[atom(CssAtomSet::NthChild)]
204	pub function: T![Function],
205	pub value: Nth,
206	#[cfg_attr(feature = "visitable", visit(skip))]
207	pub close: Option<T![')']>,
208}
209
210#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
211#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
212#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
213#[derive(csskit_derives::NodeWithMetadata)]
214pub struct NthColPseudoFunction {
215	#[cfg_attr(feature = "visitable", visit(skip))]
216	pub colon: T![:],
217	#[cfg_attr(feature = "visitable", visit(skip))]
218	#[atom(CssAtomSet::NthCol)]
219	pub function: T![Function],
220	pub value: Nth,
221	#[cfg_attr(feature = "visitable", visit(skip))]
222	pub close: Option<T![')']>,
223}
224
225#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
226#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
227#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
228#[derive(csskit_derives::NodeWithMetadata)]
229pub struct NthLastChildPseudoFunction {
230	#[cfg_attr(feature = "visitable", visit(skip))]
231	pub colon: T![:],
232	#[cfg_attr(feature = "visitable", visit(skip))]
233	#[atom(CssAtomSet::NthLastChild)]
234	pub function: T![Function],
235	pub value: Nth,
236	#[cfg_attr(feature = "visitable", visit(skip))]
237	pub close: Option<T![')']>,
238}
239
240#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
241#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
242#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
243#[derive(csskit_derives::NodeWithMetadata)]
244pub struct NthLastColPseudoFunction {
245	#[cfg_attr(feature = "visitable", visit(skip))]
246	pub colon: T![:],
247	#[cfg_attr(feature = "visitable", visit(skip))]
248	#[atom(CssAtomSet::NthLastCol)]
249	pub function: T![Function],
250	pub value: Nth,
251	#[cfg_attr(feature = "visitable", visit(skip))]
252	pub close: Option<T![')']>,
253}
254
255#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
256#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
257#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
258#[derive(csskit_derives::NodeWithMetadata)]
259pub struct NthLastOfTypePseudoFunction {
260	#[cfg_attr(feature = "visitable", visit(skip))]
261	pub colon: T![:],
262	#[cfg_attr(feature = "visitable", visit(skip))]
263	#[atom(CssAtomSet::NthLastOfType)]
264	pub function: T![Function],
265	pub value: Nth,
266	#[cfg_attr(feature = "visitable", visit(skip))]
267	pub close: Option<T![')']>,
268}
269
270#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
271#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
272#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
273#[derive(csskit_derives::NodeWithMetadata)]
274pub struct NthOfTypePseudoFunction {
275	#[cfg_attr(feature = "visitable", visit(skip))]
276	pub colon: T![:],
277	#[cfg_attr(feature = "visitable", visit(skip))]
278	#[atom(CssAtomSet::NthOfType)]
279	pub function: T![Function],
280	pub value: Nth,
281	#[cfg_attr(feature = "visitable", visit(skip))]
282	pub close: Option<T![')']>,
283}
284
285#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
286#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
287#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
288#[derive(csskit_derives::NodeWithMetadata)]
289pub struct WherePseudoFunction<'a> {
290	#[cfg_attr(feature = "visitable", visit(skip))]
291	pub colon: T![:],
292	#[cfg_attr(feature = "visitable", visit(skip))]
293	#[atom(CssAtomSet::Where)]
294	pub function: T![Function],
295	pub value: ForgivingSelector<'a>,
296	#[cfg_attr(feature = "visitable", visit(skip))]
297	pub close: Option<T![')']>,
298}
299
300#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
301#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
302#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
303#[derive(csskit_derives::NodeWithMetadata)]
304pub struct StatePseudoFunction {
305	pub colon: T![:],
306	#[atom(CssAtomSet::State)]
307	pub function: T![Function],
308	pub value: T![Ident],
309	pub close: Option<T![')']>,
310}
311
312#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
313#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
314#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
315#[derive(csskit_derives::NodeWithMetadata)]
316pub struct HeadingPseudoFunction<'a> {
317	pub colon: T![:],
318	#[atom(CssAtomSet::Heading)]
319	pub function: T![Function],
320	pub value: CommaSeparated<'a, Nth>,
321	pub close: Option<T![')']>,
322}
323
324#[cfg(test)]
325mod tests {
326	use super::*;
327	use crate::selector::SelectorList;
328	use css_parse::{assert_parse, assert_parse_error};
329
330	#[test]
331	fn size_test() {
332		assert_eq!(std::mem::size_of::<FunctionalPseudoClass>(), 104);
333		assert_eq!(std::mem::size_of::<DirValue>(), 16);
334	}
335
336	#[test]
337	fn test_has_parses() {
338		assert_parse!(CssAtomSet::ATOMS, SelectorList, ":has(.foo)");
339		assert_parse!(CssAtomSet::ATOMS, SelectorList, ":has(> .foo)");
340		assert_parse!(CssAtomSet::ATOMS, SelectorList, ":has(+ .sibling)");
341		assert_parse!(CssAtomSet::ATOMS, SelectorList, ":has(:is(.a,.b))");
342		assert_parse!(CssAtomSet::ATOMS, SelectorList, ":has(:not(.foo))");
343		assert_parse!(CssAtomSet::ATOMS, SelectorList, ":has(:where(.foo))");
344	}
345
346	#[test]
347	fn test_nested_has_disallowed() {
348		// Nested :has() is invalid CSS - :has() cannot contain :has()
349		assert_parse_error!(CssAtomSet::ATOMS, SelectorList, ":has(:has(.foo))");
350		assert_parse_error!(CssAtomSet::ATOMS, SelectorList, ":has(:has(:has(.foo)))");
351		assert_parse_error!(CssAtomSet::ATOMS, SelectorList, ":has(.bar :has(.foo))");
352		assert_parse_error!(CssAtomSet::ATOMS, SelectorList, ":has(:is(:has(.foo)))");
353		assert_parse_error!(CssAtomSet::ATOMS, SelectorList, ":has(:not(:has(.foo)))");
354		assert_parse_error!(CssAtomSet::ATOMS, SelectorList, ":has(:where(:has(.foo)))");
355	}
356}