Skip to main content

css_ast/selector/
mod.rs

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