Skip to main content

css_ast/selector/
functional_pseudo_class.rs

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