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#[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#[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, ":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 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}