Skip to main content

css_ast/selector/
mod.rs

1use crate::{
2	CssMetadata,
3	specificity::{Specificity, ToSpecificity},
4};
5use bumpalo::collections::Vec;
6use css_parse::{
7	CompoundSelector as CompoundSelectorTrait, Cursor, NodeMetadata, NodeWithMetadata, Parse, Parser,
8	Result as ParserResult, SelectorComponent as SelectorComponentTrait, T, syntax::CommaSeparated,
9};
10use csskit_derives::*;
11
12mod attribute;
13mod class;
14mod combinator;
15mod functional_pseudo_class;
16mod functional_pseudo_element;
17mod moz;
18mod ms;
19mod namespace;
20mod nth;
21mod o;
22mod pseudo_class;
23mod pseudo_element;
24mod tag;
25mod webkit;
26
27pub use attribute::*;
28pub use class::*;
29pub use combinator::*;
30pub use functional_pseudo_class::*;
31pub use functional_pseudo_element::*;
32pub use moz::*;
33pub use ms::*;
34pub use namespace::*;
35pub use nth::*;
36pub use o::*;
37pub use pseudo_class::*;
38pub use pseudo_element::*;
39pub use tag::*;
40pub use webkit::*;
41
42/// Represents a list of [CompoundSelectors][CompoundSelector], such as `body, dialog:modal`.
43///
44/// ```md
45/// <selector-list>
46///  │├─╭─ <compound-selector> ─╮─ "," ─╭─╮─┤│
47///     │                       ╰───────╯ │
48///     ╰─────────────────────────────────╯
49/// ```
50#[derive(Peek, Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
51#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
52#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
53pub struct SelectorList<'a>(pub CommaSeparated<'a, CompoundSelector<'a>>);
54
55impl<'a> NodeWithMetadata<CssMetadata> for SelectorList<'a> {
56	fn self_metadata(&self) -> CssMetadata {
57		CssMetadata::default().with_size(self.0.len().min(u16::MAX as usize) as u16)
58	}
59
60	fn metadata(&self) -> CssMetadata {
61		self.self_metadata()
62	}
63}
64
65#[derive(Peek, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
66#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
67#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
68#[derive(csskit_derives::NodeWithMetadata)]
69pub struct CompoundSelector<'a>(pub Vec<'a, SelectorComponent<'a>>);
70
71impl<'a> CompoundSelectorTrait<'a> for CompoundSelector<'a> {
72	type SelectorComponent = SelectorComponent<'a>;
73}
74
75impl<'a> Parse<'a> for CompoundSelector<'a> {
76	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
77	where
78		I: Iterator<Item = Cursor> + Clone,
79	{
80		Ok(Self(Self::parse_compound_selector(p)?))
81	}
82}
83
84pub type ComplexSelector<'a> = SelectorList<'a>;
85pub type ForgivingSelector<'a> = SelectorList<'a>;
86pub type RelativeSelector<'a> = SelectorList<'a>;
87
88#[derive(
89	Peek, Parse, ToCursors, IntoCursor, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
90)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
92#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
93#[derive(csskit_derives::NodeWithMetadata)]
94pub struct Id(T![Hash]);
95
96#[derive(
97	Peek, Parse, ToCursors, IntoCursor, ToSpan, SemanticEq, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
98)]
99#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
100#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(self))]
101#[derive(csskit_derives::NodeWithMetadata)]
102pub struct Wildcard(T![*]);
103
104// This encapsulates all `simple-selector` subtypes (e.g. `wq-name`,
105// `id-selector`) into one enum, as it makes parsing and visiting much more
106// practical.
107#[derive(Peek, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
109#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit(children))]
110#[derive(csskit_derives::NodeWithMetadata)]
111pub enum SelectorComponent<'a> {
112	Id(Id),
113	Class(Class),
114	Tag(Tag),
115	Wildcard(Wildcard),
116	Combinator(Combinator),
117	Attribute(Attribute),
118	PseudoClass(PseudoClass),
119	PseudoElement(PseudoElement),
120	FunctionalPseudoElement(FunctionalPseudoElement<'a>),
121	LegacyPseudoElement(LegacyPseudoElement),
122	FunctionalPseudoClass(FunctionalPseudoClass<'a>),
123	Namespace(Namespace),
124}
125
126impl<'a> Parse<'a> for SelectorComponent<'a> {
127	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
128	where
129		I: Iterator<Item = Cursor> + Clone,
130	{
131		Self::parse_selector_component(p)
132	}
133}
134
135impl<'a> ToSpecificity for SelectorComponent<'a> {
136	fn specificity(&self) -> Specificity {
137		match self {
138			Self::Id(_) => Specificity(1, 0, 0),
139			Self::Class(_) | Self::Attribute(_) | Self::PseudoClass(_) => Specificity(0, 1, 0),
140			Self::Tag(_) | Self::PseudoElement(_) | Self::LegacyPseudoElement(_) => Specificity(0, 0, 1),
141			Self::FunctionalPseudoElement(_) => Specificity(0, 0, 1),
142			Self::Combinator(_) | Self::Namespace(_) | Self::Wildcard(_) => Specificity(0, 0, 0),
143			Self::FunctionalPseudoClass(f) => f.specificity(),
144		}
145	}
146}
147
148impl<'a> ToSpecificity for CompoundSelector<'a> {
149	fn specificity(&self) -> Specificity {
150		self.0.iter().map(ToSpecificity::specificity).sum()
151	}
152}
153
154impl<'a> ToSpecificity for SelectorList<'a> {
155	fn specificity(&self) -> Specificity {
156		(&self.0).into_iter().map(|(s, _)| s.specificity()).max().unwrap_or_default()
157	}
158}
159
160impl<'a> SelectorComponentTrait<'a> for SelectorComponent<'a> {
161	type Wildcard = Wildcard;
162	type Id = Id;
163	type Type = Tag;
164	type PseudoClass = PseudoClass;
165	type PseudoElement = PseudoElement;
166	type LegacyPseudoElement = LegacyPseudoElement;
167	type Class = Class;
168	type NsType = Namespace;
169	type Combinator = Combinator;
170	type Attribute = Attribute;
171	type FunctionalPseudoClass = FunctionalPseudoClass<'a>;
172	type FunctionalPseudoElement = FunctionalPseudoElement<'a>;
173
174	fn build_wildcard(node: Wildcard) -> Self {
175		Self::Wildcard(node)
176	}
177
178	fn build_id(node: Id) -> Self {
179		Self::Id(node)
180	}
181
182	fn build_class(node: Class) -> Self {
183		Self::Class(node)
184	}
185
186	fn build_type(node: Tag) -> Self {
187		Self::Tag(node)
188	}
189
190	fn build_pseudo_class(node: PseudoClass) -> Self {
191		Self::PseudoClass(node)
192	}
193
194	fn build_pseudo_element(node: PseudoElement) -> Self {
195		Self::PseudoElement(node)
196	}
197
198	fn build_legacy_pseudo_element(node: LegacyPseudoElement) -> Self {
199		Self::LegacyPseudoElement(node)
200	}
201
202	fn build_ns_type(node: Namespace) -> Self {
203		Self::Namespace(node)
204	}
205
206	fn build_combinator(node: Combinator) -> Self {
207		Self::Combinator(node)
208	}
209
210	fn build_attribute(node: Attribute) -> Self {
211		Self::Attribute(node)
212	}
213
214	fn build_functional_pseudo_class(node: FunctionalPseudoClass<'a>) -> Self {
215		Self::FunctionalPseudoClass(node)
216	}
217
218	fn build_functional_pseudo_element(node: FunctionalPseudoElement<'a>) -> Self {
219		Self::FunctionalPseudoElement(node)
220	}
221}
222
223#[cfg(test)]
224mod tests {
225	use super::*;
226	use crate::{CssAtomSet, specificity::ToSpecificity};
227	use bumpalo::Bump;
228	use css_lexer::Lexer;
229	use css_parse::Parser;
230	use css_parse::assert_parse;
231
232	#[test]
233	fn size_test() {
234		assert_eq!(std::mem::size_of::<SelectorList>(), 32);
235		assert_eq!(std::mem::size_of::<ComplexSelector>(), 32);
236		assert_eq!(std::mem::size_of::<ForgivingSelector>(), 32);
237		assert_eq!(std::mem::size_of::<RelativeSelector>(), 32);
238		assert_eq!(std::mem::size_of::<SelectorComponent>(), 128);
239		assert_eq!(std::mem::size_of::<LegacyPseudoElement>(), 28);
240		assert_eq!(std::mem::size_of::<Combinator>(), 28);
241	}
242
243	#[test]
244	fn test_writes() {
245		assert_parse!(CssAtomSet::ATOMS, SelectorList, ":root");
246		assert_parse!(CssAtomSet::ATOMS, SelectorList, "body,body");
247		assert_parse!(CssAtomSet::ATOMS, SelectorList, ".body .body");
248		assert_parse!(CssAtomSet::ATOMS, SelectorList, "*");
249		assert_parse!(CssAtomSet::ATOMS, SelectorList, "[attr|='foo']");
250		assert_parse!(CssAtomSet::ATOMS, SelectorList, "*|x");
251		assert_parse!(CssAtomSet::ATOMS, SelectorList, "* x");
252		assert_parse!(CssAtomSet::ATOMS, SelectorList, "a b");
253		assert_parse!(CssAtomSet::ATOMS, SelectorList, "  a b");
254		assert_parse!(CssAtomSet::ATOMS, SelectorList, "body [attr|='foo']");
255		assert_parse!(CssAtomSet::ATOMS, SelectorList, "*|x :focus-within");
256		assert_parse!(CssAtomSet::ATOMS, SelectorList, ".foo[attr*=\"foo\"]");
257		assert_parse!(CssAtomSet::ATOMS, SelectorList, "a > b");
258		assert_parse!(CssAtomSet::ATOMS, SelectorList, ".foo[attr*=\"foo\"] > *");
259		assert_parse!(CssAtomSet::ATOMS, SelectorList, ".foo[attr*=\"foo\"] > * + *");
260		assert_parse!(CssAtomSet::ATOMS, SelectorList, ":after");
261		assert_parse!(CssAtomSet::ATOMS, SelectorList, "::after");
262		assert_parse!(CssAtomSet::ATOMS, SelectorList, ":before");
263		assert_parse!(CssAtomSet::ATOMS, SelectorList, "::before");
264		assert_parse!(CssAtomSet::ATOMS, SelectorList, "::before:focus:target:right:playing:popover-open:blank");
265		assert_parse!(CssAtomSet::ATOMS, SelectorList, ":dir(ltr)");
266		assert_parse!(CssAtomSet::ATOMS, SelectorList, "tr:nth-child(n-1):state(foo)");
267		// assert_parse!(CssAtomSet::ATOMS, SelectorList, " /**/ .foo");
268		assert_parse!(CssAtomSet::ATOMS, SelectorList, ":lang(en-gb,en-us)");
269		assert_parse!(CssAtomSet::ATOMS, SelectorList, "& .foo");
270		assert_parse!(CssAtomSet::ATOMS, SelectorList, "&:hover");
271		assert_parse!(CssAtomSet::ATOMS, SelectorList, ".foo &:hover");
272		assert_parse!(CssAtomSet::ATOMS, SelectorList, ".foo & & &");
273		assert_parse!(CssAtomSet::ATOMS, SelectorList, ".class&");
274		assert_parse!(CssAtomSet::ATOMS, SelectorList, "&&");
275		assert_parse!(CssAtomSet::ATOMS, SelectorList, "& + .foo,&.bar");
276		assert_parse!(CssAtomSet::ATOMS, SelectorList, ":state(foo)&");
277		assert_parse!(CssAtomSet::ATOMS, SelectorList, ":heading(1)");
278		assert_parse!(CssAtomSet::ATOMS, SelectorList, ":heading(1,2,3)");
279		// Non Standard
280		assert_parse!(CssAtomSet::ATOMS, SelectorList, "::-moz-focus-inner");
281		assert_parse!(
282			CssAtomSet::ATOMS,
283			SelectorList,
284			"::-moz-list-bullet::-webkit-scrollbar::-ms-clear:-ms-input-placeholder::-o-scrollbar:-o-prefocus"
285		);
286		assert_parse!(CssAtomSet::ATOMS, SelectorList, "button:-moz-focusring");
287		assert_parse!(CssAtomSet::ATOMS, SelectorList, "::view-transition-group(*)");
288		assert_parse!(CssAtomSet::ATOMS, SelectorList, "::view-transition-new(thing.foo.bar.baz)");
289	}
290
291	#[test]
292	#[cfg(feature = "visitable")]
293	fn test_visits() {
294		use crate::assert_visits;
295		assert_visits!(".foo", CompoundSelector, Class);
296		assert_visits!("#bar", CompoundSelector, Id);
297		assert_visits!(".foo", SelectorList, CompoundSelector, Class);
298		assert_visits!(".foo, #bar", SelectorList, CompoundSelector, Class, CompoundSelector, Id);
299		assert_visits!(".foo#bar", CompoundSelector, Class, Id);
300		assert_visits!(".foo.bar", CompoundSelector, Class, Class);
301		assert_visits!(".foo", CompoundSelector, Class);
302		assert_visits!(".foo#bar", CompoundSelector, Class, Id);
303		assert_visits!(".foo", CompoundSelector, Class);
304		assert_visits!("*.foo#bar", CompoundSelector, Wildcard, Class, Id);
305		assert_visits!(".foo .bar", CompoundSelector, Class, Combinator, Class);
306		assert_visits!(".foo ", CompoundSelector, Class);
307		assert_visits!("a > b", CompoundSelector, Tag, HtmlTag, Combinator, Tag, HtmlTag);
308		assert_visits!("a>b", CompoundSelector, Tag, HtmlTag, Combinator, Tag, HtmlTag);
309		assert_visits!("a + b", CompoundSelector, Tag, HtmlTag, Combinator, Tag, HtmlTag);
310		assert_visits!("a ~ b", CompoundSelector, Tag, HtmlTag, Combinator, Tag, HtmlTag);
311		assert_visits!(".foo > .bar + .baz", CompoundSelector, Class, Combinator, Class, Combinator, Class);
312	}
313
314	#[test]
315	#[should_panic]
316	#[cfg(feature = "visitable")]
317	fn test_assert_visits_fails() {
318		use crate::assert_visits;
319		assert_visits!(".foo", CompoundSelector, visit_id<Id>);
320	}
321
322	macro_rules! assert_specificity {
323		($sel:literal, $a:literal, $b:literal, $c:literal) => {{
324			let bump = Bump::default();
325			let lexer = Lexer::new(&CssAtomSet::ATOMS, $sel);
326			let mut parser = Parser::new(&bump, $sel, lexer);
327			let result = parser.parse_entirely::<SelectorList>().with_trivia();
328			assert!(result.errors.is_empty(), "parse failed for {:?}: {:?}", $sel, result.errors);
329			let s = result.output.unwrap().specificity();
330			assert_eq!(
331				s,
332				Specificity($a, $b, $c),
333				"selector {:?}: expected ({},{},{}) got ({},{},{})",
334				$sel,
335				$a,
336				$b,
337				$c,
338				s.0,
339				s.1,
340				s.2
341			);
342		}};
343	}
344
345	#[test]
346	fn test_specificity_arithmetic() {
347		assert_eq!(Specificity(0, 1, 0) + Specificity(0, 1, 0), Specificity(0, 2, 0));
348		assert_eq!(Specificity(1, 0, 0) + Specificity(0, 1, 0), Specificity(1, 1, 0));
349		assert_eq!(Specificity(255, 0, 0) + Specificity(1, 0, 0), Specificity(255, 0, 0));
350	}
351
352	#[test]
353	fn test_specificity() {
354		assert_specificity!("#foo", 1, 0, 0);
355		assert_specificity!(".foo", 0, 1, 0);
356		assert_specificity!(".a.b.c", 0, 3, 0);
357		assert_specificity!("div", 0, 0, 1);
358		assert_specificity!(":hover", 0, 1, 0);
359		assert_specificity!("::before", 0, 0, 1);
360		assert_specificity!(":before", 0, 0, 1);
361		assert_specificity!("[href]", 0, 1, 0);
362		assert_specificity!("*", 0, 0, 0);
363		assert_specificity!("a.foo", 0, 1, 1);
364		assert_specificity!("a.foo:hover", 0, 2, 1);
365		assert_specificity!("#a.b", 1, 1, 0);
366		assert_specificity!(":where(.a.b)", 0, 0, 0);
367		assert_specificity!(":is(.a, #b)", 1, 0, 0);
368		assert_specificity!(":not(.a, .b)", 0, 1, 0);
369		assert_specificity!("a:has(.b)", 0, 1, 1);
370		assert_specificity!(":nth-child(2)", 0, 1, 0);
371		assert_specificity!(":nth-of-type(2n+1)", 0, 1, 0);
372		assert_specificity!(".a, #b", 1, 0, 0);
373	}
374
375	#[test]
376	fn test_specificity_complex() {
377		assert_specificity!("nav ul li:nth-child(even) a:not([href^='#'])", 0, 2, 4);
378		assert_specificity!("button:only-of-type:enabled:active:hover", 0, 4, 1);
379		assert_specificity!("table tr:not(:first-child):hover td:nth-child(2n+1)", 0, 3, 3);
380		assert_specificity!("input[type='checkbox'][checked]:indeterminate + label", 0, 3, 2);
381	}
382}