1use std::collections::HashSet;
4use std::sync::{Arc, OnceLock};
5
6use crate::engine::css::parser::AttributeSelectorOperator;
7
8use super::parser::{Combinator, ComplexSelector, PseudoClass, Selector};
9
10#[derive(Debug, Clone, Default)]
18pub struct ElementInfo {
19 pub tag_name: Arc<str>,
20 pub id: Option<Arc<str>>,
21 pub classes: Arc<[Arc<str>]>,
22 pub attributes: Arc<[(Arc<str>, Arc<str>)]>,
23 pub element_index: usize,
25 pub element_count: usize,
27 pub type_index: usize,
29 pub type_count: usize,
31 pub previous_siblings: ElementChain,
35 pub has_parent: bool,
40 pub checked: bool,
44 pub lang: Option<Arc<str>>,
47 pub is_empty: bool,
51 pub hovered: bool,
53 pub active: bool,
55}
56
57#[derive(Debug)]
59struct ChainLink {
60 info: ElementInfo,
61 next: Option<Arc<ChainLink>>,
63 summary: OnceLock<Box<ChainSummary>>,
66}
67
68#[derive(Debug, Default, Clone)]
71struct ChainSummary {
72 tags: HashSet<Box<str>>,
73 ids: HashSet<Box<str>>,
74 classes: HashSet<Box<str>>,
75}
76
77impl ChainSummary {
78 fn collect(start: &ChainLink) -> Box<Self> {
79 let mut summary = match start.next.as_deref().and_then(|next| next.summary.get()) {
81 Some(cached) => (**cached).clone(),
82 None => {
83 let mut summary = Self::default();
84 let mut link = start.next.as_deref();
85 while let Some(current) = link {
86 summary.add(¤t.info);
87 link = current.next.as_deref();
88 }
89 summary
90 }
91 };
92 summary.add(&start.info);
93 Box::new(summary)
94 }
95
96 fn add(&mut self, info: &ElementInfo) {
97 self.tags.insert(info.tag_name.as_ref().into());
98 if let Some(id) = &info.id {
99 self.ids.insert(id.as_ref().into());
100 }
101 for class in info.classes.iter() {
102 self.classes.insert(class.as_ref().into());
103 }
104 }
105
106 fn could_match(&self, selector: &Selector) -> bool {
110 if let Some(tag) = &selector.tag
111 && !self.tags.contains(tag.as_str())
112 {
113 return false;
114 }
115 if let Some(id) = &selector.id
116 && !self.ids.contains(id.as_str())
117 {
118 return false;
119 }
120 selector
121 .classes
122 .iter()
123 .all(|class| self.classes.contains(class.as_str()))
124 }
125}
126
127impl ChainLink {
128 fn summary(&self) -> &ChainSummary {
130 self.summary.get_or_init(|| ChainSummary::collect(self))
131 }
132}
133
134#[derive(Debug, Clone, Default)]
139pub struct ElementChain {
140 head: Option<Arc<ChainLink>>,
141}
142
143impl ElementChain {
144 pub fn prepend(&self, info: Option<ElementInfo>) -> Self {
148 match info {
149 Some(info) => Self {
150 head: Some(Arc::new(ChainLink {
151 info,
152 next: self.head.clone(),
153 summary: OnceLock::new(),
154 })),
155 },
156 None => self.clone(),
157 }
158 }
159
160 pub fn first(&self) -> Option<&ElementInfo> {
162 self.head.as_deref().map(|link| &link.info)
163 }
164
165 pub fn from_vec(elements: Vec<ElementInfo>) -> Self {
167 let mut chain = Self::default();
168 for info in elements.into_iter().rev() {
169 chain = chain.prepend(Some(info));
170 }
171 chain
172 }
173
174 pub fn from_document_order(elements: impl IntoIterator<Item = ElementInfo>) -> Self {
178 let mut chain = Self::default();
179 for info in elements {
180 chain = chain.prepend(Some(info));
181 }
182 chain
183 }
184}
185
186#[derive(Clone, Copy)]
187struct MatchCursor<'a> {
188 link: &'a ChainLink,
189 sibling_link: Option<&'a ChainLink>,
192}
193
194fn is_checkable_input(element: &ElementInfo) -> bool {
197 if !element.tag_name.eq_ignore_ascii_case("input") {
198 return false;
199 }
200 match element
201 .attributes
202 .iter()
203 .find(|(name, _)| name.eq_ignore_ascii_case("type"))
204 {
205 Some((_, value)) => {
206 let value = value.trim();
207 value.eq_ignore_ascii_case("checkbox") || value.eq_ignore_ascii_case("radio")
208 }
209 None => false,
211 }
212}
213
214fn matches_lang(element: &ElementInfo, language: &str) -> bool {
218 let Some(tag) = element.lang.as_deref() else {
219 return false;
220 };
221 if tag.eq_ignore_ascii_case(language) {
222 return true;
223 }
224 tag.get(..language.len())
227 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(language))
228 && tag.as_bytes().get(language.len()) == Some(&b'-')
229}
230
231fn matches_an_plus_b(index: usize, a: i32, b: i32) -> bool {
232 let index = index as i128;
235 let a = i128::from(a);
236 let b = i128::from(b);
237 if a == 0 {
238 return index == b;
239 }
240 let delta = index - b;
241 delta % a == 0 && delta / a >= 0
242}
243
244impl Selector {
245 fn matches_base(&self, element: &ElementInfo) -> bool {
247 if let Some(tag) = &self.tag
249 && tag.as_str() != element.tag_name.as_ref()
250 {
251 return false;
252 }
253
254 if let Some(expected_id) = &self.id {
256 match element.id.as_deref() {
257 Some(actual_id) if actual_id == expected_id => {}
258 _ => return false,
259 }
260 }
261
262 for class in &self.classes {
264 if !element.classes.iter().any(|c| c.as_ref() == class.as_str()) {
265 return false;
266 }
267 }
268
269 for expected in &self.attributes {
270 let actual = element
271 .attributes
272 .iter()
273 .find(|(name, _)| name.eq_ignore_ascii_case(expected.name.as_str()));
274
275 match (&expected.operator, &expected.value, actual) {
276 (AttributeSelectorOperator::Exists, _, Some(_)) => {}
277 (
278 AttributeSelectorOperator::Equals,
279 Some(expected_value),
280 Some((_, actual_value)),
281 ) if actual_value.as_ref() == expected_value.as_str() => {}
282 (
283 AttributeSelectorOperator::Includes,
284 Some(expected_value),
285 Some((_, actual_value)),
286 ) if actual_value
287 .split_ascii_whitespace()
288 .any(|value| value == expected_value.as_str()) => {}
289 (
290 AttributeSelectorOperator::DashMatch,
291 Some(expected_value),
292 Some((_, actual_value)),
293 ) if actual_value.as_ref() == expected_value.as_str()
294 || actual_value
295 .strip_prefix(expected_value.as_str())
296 .is_some_and(|rest| rest.starts_with('-')) => {}
297 (
298 AttributeSelectorOperator::Prefix,
299 Some(expected_value),
300 Some((_, actual_value)),
301 ) if actual_value.starts_with(expected_value.as_str()) => {}
302 (
303 AttributeSelectorOperator::Suffix,
304 Some(expected_value),
305 Some((_, actual_value)),
306 ) if actual_value.ends_with(expected_value.as_str()) => {}
307 (
308 AttributeSelectorOperator::Substring,
309 Some(expected_value),
310 Some((_, actual_value)),
311 ) if actual_value.contains(expected_value.as_str()) => {}
312
313 _ => return false,
314 }
315 }
316
317 true
318 }
319
320 fn matches_pseudo_classes(&self, cursor: MatchCursor<'_>, is_root: bool) -> bool {
321 let element = ComplexSelector::element_at(cursor);
322 self.pseudo_classes.iter().all(|pseudo| match pseudo {
323 PseudoClass::Lang(language) => matches_lang(element, language),
324 PseudoClass::Simple(pseudo) => {
325 let has_attribute = |name: &str| {
326 element
327 .attributes
328 .iter()
329 .any(|(attribute, _)| attribute.eq_ignore_ascii_case(name))
330 };
331 let is_form_control = matches!(
332 element.tag_name.as_ref(),
333 "button" | "fieldset" | "input" | "optgroup" | "option" | "select" | "textarea"
334 );
335 if pseudo.eq_ignore_ascii_case("root") {
336 is_root
337 } else if pseudo.eq_ignore_ascii_case("defined") {
338 !element.tag_name.contains('-')
345 } else if pseudo.eq_ignore_ascii_case("link")
346 || pseudo.eq_ignore_ascii_case("any-link")
347 {
348 matches!(element.tag_name.as_ref(), "a" | "area") && has_attribute("href")
349 } else if pseudo.eq_ignore_ascii_case("disabled") {
350 is_form_control && has_attribute("disabled")
351 } else if pseudo.eq_ignore_ascii_case("enabled") {
352 is_form_control && !has_attribute("disabled")
353 } else if pseudo.eq_ignore_ascii_case("checked") {
354 is_checkable_input(element) && element.checked
358 || (element.tag_name.as_ref() == "option" && has_attribute("selected"))
359 } else if pseudo.eq_ignore_ascii_case("required") {
360 is_form_control && has_attribute("required")
361 } else if pseudo.eq_ignore_ascii_case("optional") {
362 is_form_control && !has_attribute("required")
363 } else if pseudo.eq_ignore_ascii_case("first-child") {
364 element.has_parent && element.element_index == 1
365 } else if pseudo.eq_ignore_ascii_case("last-child") {
366 element.has_parent && element.element_index == element.element_count
367 } else if pseudo.eq_ignore_ascii_case("only-child") {
368 element.has_parent && element.element_count == 1
369 } else if pseudo.eq_ignore_ascii_case("empty") {
370 element.is_empty
371 } else if pseudo.eq_ignore_ascii_case("first-of-type") {
372 element.has_parent && element.type_index == 1
373 } else if pseudo.eq_ignore_ascii_case("last-of-type") {
374 element.has_parent && element.type_index == element.type_count
375 } else if pseudo.eq_ignore_ascii_case("only-of-type") {
376 element.has_parent && element.type_count == 1
377 } else if pseudo.eq_ignore_ascii_case("hover") {
378 element.hovered
379 } else if pseudo.eq_ignore_ascii_case("active") {
380 element.active
381 } else {
382 false
383 }
384 }
385 PseudoClass::SelectorList { name, selectors } => {
386 if selectors.is_empty() {
387 return false;
388 }
389 let any_matches = selectors
390 .iter()
391 .any(|selector| selector.matches_from(cursor, 0));
392 match name.as_str() {
393 "is" | "where" => any_matches,
394 "not" => !any_matches,
395 _ => false,
396 }
397 }
398 PseudoClass::Nth { name, a, b } => {
399 let index = match name.as_str() {
400 "nth-child" => element.element_index,
401 "nth-last-child" => element
402 .element_count
403 .saturating_add(1)
404 .saturating_sub(element.element_index),
405 "nth-of-type" => element.type_index,
406 "nth-last-of-type" => element
407 .type_count
408 .saturating_add(1)
409 .saturating_sub(element.type_index),
410 _ => return false,
411 };
412 matches_an_plus_b(index, *a, *b)
413 }
414 })
415 }
416
417 fn matches_at(&self, cursor: MatchCursor<'_>) -> bool {
418 let element = ComplexSelector::element_at(cursor);
419 let is_root = cursor.sibling_link.is_none() && cursor.link.next.is_none();
420 if !self.matches_base(element) || !self.matches_pseudo_classes(cursor, is_root) {
421 return false;
422 }
423 if let Some(pseudo) = &self.pseudo_element {
424 let pseudo_lower = pseudo.to_ascii_lowercase();
427 pseudo_lower == "before" || pseudo_lower == "after"
428 } else {
429 true
430 }
431 }
432}
433
434impl ComplexSelector {
435 pub fn matches(&self, chain: &ElementChain) -> bool {
436 if self.parts.is_empty() {
437 return false;
438 }
439 let Some(link) = chain.head.as_deref() else {
440 return false;
441 };
442 self.matches_from(
443 MatchCursor {
444 link,
445 sibling_link: None,
446 },
447 0,
448 )
449 }
450
451 fn element_at(cursor: MatchCursor<'_>) -> &ElementInfo {
452 match cursor.sibling_link {
453 Some(link) => &link.info,
454 None => &cursor.link.info,
455 }
456 }
457
458 fn previous_sibling_cursor(cursor: MatchCursor<'_>) -> Option<MatchCursor<'_>> {
459 let farther = match cursor.sibling_link {
460 Some(link) => link.next.as_deref(),
461 None => cursor.link.info.previous_siblings.head.as_deref(),
462 };
463 farther.map(|link| MatchCursor {
464 link: cursor.link,
465 sibling_link: Some(link),
466 })
467 }
468
469 fn matches_from(&self, cursor: MatchCursor<'_>, selector_index: usize) -> bool {
470 let part = &self.parts[selector_index];
471
472 if !part.selector.matches_at(cursor) {
473 return false;
474 }
475
476 if selector_index + 1 == self.parts.len() {
478 return true;
479 }
480
481 match part.combinator {
482 Some(Combinator::Descendant) => {
483 let next_selector = &self.parts[selector_index + 1].selector;
484 let Some(ancestors) = cursor.link.next.as_deref() else {
485 return false;
486 };
487 if !ancestors.summary().could_match(next_selector) {
489 return false;
490 }
491 let mut ancestor = Some(ancestors);
492 while let Some(link) = ancestor {
493 if self.matches_from(
494 MatchCursor {
495 link,
496 sibling_link: None,
497 },
498 selector_index + 1,
499 ) {
500 return true;
501 }
502 ancestor = link.next.as_deref();
503 }
504 false
505 }
506 Some(Combinator::Child) => cursor.link.next.as_deref().is_some_and(|parent| {
507 self.matches_from(
508 MatchCursor {
509 link: parent,
510 sibling_link: None,
511 },
512 selector_index + 1,
513 )
514 }),
515 Some(Combinator::NextSibling) => Self::previous_sibling_cursor(cursor)
516 .is_some_and(|previous| self.matches_from(previous, selector_index + 1)),
517 Some(Combinator::SubsequentSibling) => {
518 let next_selector = &self.parts[selector_index + 1].selector;
519 let remaining = match cursor.sibling_link {
520 Some(link) => link.next.as_deref(),
521 None => cursor.link.info.previous_siblings.head.as_deref(),
522 };
523 if !remaining.is_some_and(|siblings| siblings.summary().could_match(next_selector))
525 {
526 return false;
527 }
528 let mut previous = Self::previous_sibling_cursor(cursor);
529 while let Some(candidate) = previous {
530 if self.matches_from(candidate, selector_index + 1) {
531 return true;
532 }
533 previous = Self::previous_sibling_cursor(candidate);
534 }
535 false
536 }
537 None => false,
538 }
539 }
540
541 pub fn specificity(&self) -> (u32, u32, u32) {
542 let mut a = 0; let mut b = 0; let mut c = 0; for part in &self.parts {
547 let sel = &part.selector;
548
549 if sel.id.is_some() {
550 a += 1;
551 }
552 b += (sel.classes.len() + sel.attributes.len()) as u32;
553 for pseudo in &sel.pseudo_classes {
554 match pseudo {
555 PseudoClass::Simple(_) | PseudoClass::Nth { .. } | PseudoClass::Lang(_) => {
556 b += 1
557 }
558 PseudoClass::SelectorList { name, selectors } if name == "where" => {}
559 PseudoClass::SelectorList { selectors, .. } => {
560 let nested = selectors
561 .iter()
562 .map(ComplexSelector::specificity)
563 .max()
564 .unwrap_or_default();
565 a += nested.0;
566 b += nested.1;
567 c += nested.2;
568 }
569 }
570 }
571 if sel.tag.is_some() {
572 c += 1;
573 }
574 c += u32::from(sel.pseudo_element.is_some());
575 }
576
577 (a, b, c)
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584 use crate::engine::css::parser::{
585 AttributeSelector, AttributeSelectorOperator, Parser, SelectorPart,
586 };
587
588 fn input_selector(value: Option<&str>) -> ComplexSelector {
589 ComplexSelector {
590 parts: vec![SelectorPart {
591 selector: Selector {
592 is_nesting: false,
593 tag: Some("input".into()),
594 id: None,
595 classes: Vec::<String>::new(),
596 attributes: vec![AttributeSelector {
597 name: "type".into(),
598 operator: match value {
599 Some(_) => AttributeSelectorOperator::Includes,
600 None => AttributeSelectorOperator::Exists,
601 },
602 value: value.map(Into::into),
603 }],
604 pseudo_classes: Vec::new(),
605 pseudo_element: None,
606 },
607 combinator: None,
608 }],
609 }
610 }
611
612 fn input(attributes: &[(&str, &str)]) -> ElementInfo {
613 ElementInfo {
614 tag_name: "input".into(),
615 id: None,
616 classes: Arc::default(),
617 attributes: attributes
618 .iter()
619 .map(|(name, value)| ((*name).into(), (*value).into()))
620 .collect(),
621 ..ElementInfo::default()
622 }
623 }
624
625 fn chain(elements: impl IntoIterator<Item = ElementInfo>) -> ElementChain {
626 ElementChain::from_vec(elements.into_iter().collect())
627 }
628
629 #[test]
630 fn exact_attribute_selector_requires_matching_value() {
631 let selector = input_selector(Some("hidden"));
632
633 assert!(selector.matches(&chain([input(&[("type", "hidden")])])));
634 assert!(!selector.matches(&chain([input(&[])])));
635 assert!(!selector.matches(&chain([input(&[("type", "text")])])));
636 }
637
638 #[test]
639 fn presence_attribute_selector_requires_attribute() {
640 let selector = input_selector(None);
641
642 assert!(selector.matches(&chain([input(&[("type", "text")])])));
643 assert!(!selector.matches(&chain([input(&[])])));
644 }
645
646 #[test]
647 fn child_combinator_requires_direct_parent() {
648 let stylesheet = Parser::new("main > p { color: red; }").parse().unwrap();
649 let selector = match stylesheet.children().first().unwrap().node() {
650 crate::engine::css::parser::CssNodeType::Rule { selectors } => &selectors[0],
651 _ => panic!("expected CSS rule"),
652 };
653 let paragraph = ElementInfo {
654 tag_name: "p".into(),
655 id: None,
656 classes: Arc::default(),
657 attributes: Arc::default(),
658 ..ElementInfo::default()
659 };
660 let section = ElementInfo {
661 tag_name: "section".into(),
662 id: None,
663 classes: Arc::default(),
664 attributes: Arc::default(),
665 ..ElementInfo::default()
666 };
667 let main = ElementInfo {
668 tag_name: "main".into(),
669 id: None,
670 classes: Arc::default(),
671 attributes: Arc::default(),
672 ..ElementInfo::default()
673 };
674
675 assert!(selector.matches(&chain([paragraph.clone(), main.clone()])));
676 assert!(!selector.matches(&chain([paragraph, section, main])));
677 }
678
679 fn parse_selector(source: &str) -> ComplexSelector {
680 let stylesheet = Parser::new(&format!("{source} {{ color: red; }}"))
681 .parse()
682 .unwrap();
683 match stylesheet.children().first().unwrap().node() {
684 crate::engine::css::parser::CssNodeType::Rule { selectors } => selectors[0].clone(),
685 _ => panic!("expected CSS rule"),
686 }
687 }
688
689 #[test]
690 fn root_pseudo_class_only_matches_the_root_element() {
691 let selector = parse_selector(":root");
692 let html = ElementInfo {
693 tag_name: "html".into(),
694 id: None,
695 classes: Arc::default(),
696 attributes: Arc::default(),
697 ..ElementInfo::default()
698 };
699 let body = ElementInfo {
700 tag_name: "body".into(),
701 id: None,
702 classes: Arc::default(),
703 attributes: Arc::default(),
704 ..ElementInfo::default()
705 };
706
707 assert!(selector.matches(&chain([html.clone()])));
708 assert!(!selector.matches(&chain([body, html])));
709 }
710
711 #[test]
712 fn defined_pseudo_class_matches_standard_elements_only() {
713 let html = element("html", &[], 1, 1, 1, 1);
714 let custom = element("shreddit-app", &[], 1, 1, 1, 1);
715
716 assert!(parse_selector(":defined").matches(&chain([html.clone()])));
717 assert!(!parse_selector(":defined").matches(&chain([custom.clone()])));
718
719 assert!(!parse_selector(":not(:defined)").matches(&chain([html])));
720 assert!(parse_selector(":not(:defined)").matches(&chain([custom])));
721 }
722
723 #[test]
724 fn link_pseudo_class_requires_an_href() {
725 let selector = parse_selector("a:link");
726
727 assert!(selector.matches(&chain([ElementInfo {
728 tag_name: "a".into(),
729 id: None,
730 classes: Arc::default(),
731 attributes: vec![("href".into(), "/next".into())].into(),
732 ..ElementInfo::default()
733 }])));
734 assert!(!selector.matches(&chain([ElementInfo {
735 tag_name: "a".into(),
736 id: None,
737 classes: Arc::default(),
738 attributes: Arc::default(),
739 ..ElementInfo::default()
740 }])));
741 }
742
743 fn element(
744 tag_name: &str,
745 classes: &[&str],
746 element_index: usize,
747 element_count: usize,
748 type_index: usize,
749 type_count: usize,
750 ) -> ElementInfo {
751 ElementInfo {
752 tag_name: tag_name.into(),
753 classes: classes.iter().map(|class| (*class).into()).collect(),
754 element_index,
755 element_count,
756 type_index,
757 type_count,
758 has_parent: true,
761 ..ElementInfo::default()
762 }
763 }
764
765 #[test]
766 fn sibling_combinators_match_preceding_elements() {
767 let heading = element("h2", &[], 1, 3, 1, 1);
768 let aside = element("aside", &[], 2, 3, 1, 1);
769 let mut paragraph = element("p", &[], 3, 3, 1, 1);
770 paragraph.previous_siblings = ElementChain::from_document_order([heading, aside]);
771
772 assert!(parse_selector("aside + p").matches(&chain([paragraph.clone()])));
773 assert!(!parse_selector("h2 + p").matches(&chain([paragraph.clone()])));
774 assert!(parse_selector("h2 ~ p").matches(&chain([paragraph.clone()])));
775 assert!(!parse_selector("nav ~ p").matches(&chain([paragraph])));
776 }
777
778 #[test]
779 fn descendant_combinator_matches_deep_ancestors() {
780 let span = element("span", &[], 1, 1, 1, 1);
781 let section = element("section", &[], 1, 1, 1, 1);
782 let card_body = element("body", &["card"], 1, 1, 1, 1);
783 let html = element("html", &[], 1, 1, 1, 1);
784
785 assert!(parse_selector(".card span").matches(&chain([
786 span.clone(),
787 section.clone(),
788 card_body.clone(),
789 html.clone()
790 ])));
791 assert!(parse_selector("html span").matches(&chain([span, section, card_body, html])));
792 }
793
794 #[test]
795 fn descendant_combinator_rejects_when_no_ancestor_qualifies() {
796 let span = element("span", &[], 1, 1, 1, 1);
797 let div = element("div", &[], 1, 1, 1, 1);
798 let main = ElementInfo {
799 tag_name: "main".into(),
800 id: Some("app".into()),
801 classes: vec!["root".into()].into(),
802 attributes: Arc::default(),
803 ..ElementInfo::default()
804 };
805
806 assert!(!parse_selector(".card span").matches(&chain([
808 span.clone(),
809 div.clone(),
810 main.clone()
811 ])));
812 assert!(!parse_selector("#missing span").matches(&chain([
813 span.clone(),
814 div.clone(),
815 main.clone()
816 ])));
817 assert!(!parse_selector("nav span").matches(&chain([
818 span.clone(),
819 div.clone(),
820 main.clone()
821 ])));
822
823 let mut hidden_main = main.clone();
826 hidden_main.attributes = vec![("hidden".into(), "true".into())].into();
827 assert!(parse_selector("[hidden] span").matches(&chain([
828 span.clone(),
829 div.clone(),
830 hidden_main
831 ])));
832 assert!(parse_selector("* span").matches(&chain([span, div, main])));
833 }
834
835 #[test]
836 fn subsequent_sibling_rejects_when_no_preceding_element_qualifies() {
837 let heading = element("h2", &[], 1, 2, 1, 1);
838 let mut paragraph = element("p", &[], 2, 2, 1, 1);
839 paragraph.previous_siblings = ElementChain::from_document_order([heading]);
840
841 assert!(!parse_selector(".x ~ p").matches(&chain([paragraph.clone()])));
842 assert!(!parse_selector("aside ~ p").matches(&chain([paragraph])));
843
844 let aside = element("aside", &["x"], 1, 2, 1, 1);
845 let heading = element("h2", &[], 2, 2, 1, 1);
846 let mut paragraph = element("p", &[], 3, 3, 1, 1);
847 paragraph.previous_siblings = ElementChain::from_document_order([aside, heading]);
848 assert!(parse_selector(".x ~ p").matches(&chain([paragraph])));
849 }
850
851 #[test]
852 fn structural_pseudo_classes_use_element_and_type_positions() {
853 let second_paragraph = element("p", &[], 3, 5, 2, 3);
854
855 assert!(parse_selector("p:nth-child(2n+1)").matches(&chain([second_paragraph.clone()])));
856 assert!(parse_selector("p:nth-of-type(even)").matches(&chain([second_paragraph.clone()])));
857 assert!(parse_selector("p:nth-last-child(3)").matches(&chain([second_paragraph.clone()])));
858 assert!(
859 parse_selector("p:nth-last-of-type(2)").matches(&chain([second_paragraph.clone()]))
860 );
861 assert!(parse_selector("p:nth-child(-n+3)").matches(&chain([second_paragraph.clone()])));
862 assert!(!parse_selector("p:first-child").matches(&chain([second_paragraph.clone()])));
863 assert!(!parse_selector("p:last-of-type").matches(&chain([second_paragraph])));
864 }
865
866 fn input_element(input_type: &str, checked: bool) -> ElementInfo {
867 let mut element = element("input", &[], 1, 1, 1, 1);
868 element.checked = checked;
869 if input_type != "text" {
870 let mut attributes: Vec<(Arc<str>, Arc<str>)> =
871 element.attributes.iter().cloned().collect();
872 attributes.push(("type".into(), input_type.into()));
873 element.attributes = attributes.into();
874 }
875 element
876 }
877
878 #[test]
879 fn tree_root_does_not_match_structural_pseudo_classes() {
880 let mut root = element("html", &[], 1, 1, 1, 1);
883 root.has_parent = false;
884
885 for selector in [
886 ":first-child",
887 ":last-child",
888 ":only-child",
889 ":first-of-type",
890 ":last-of-type",
891 ] {
892 assert!(
893 !parse_selector(selector).matches(&chain([root.clone()])),
894 "{selector} matched a parentless root"
895 );
896 }
897 let mut child = element("head", &[], 1, 1, 1, 1);
898 child.has_parent = true;
899 assert!(parse_selector(":first-child").matches(&chain([child])));
900 }
901
902 #[test]
903 fn empty_pseudo_class_ignores_comments_and_blank_text() {
904 let mut empty = element("p", &[], 1, 1, 1, 1);
905 empty.is_empty = true;
906 assert!(parse_selector(":empty").matches(&chain([empty.clone()])));
907
908 empty.is_empty = false;
909 assert!(!parse_selector(":empty").matches(&chain([empty])));
910 }
911
912 #[test]
913 fn lang_pseudo_class_matches_own_and_inherited_tags() {
914 let mut british = element("div", &[], 1, 1, 1, 1);
915 british.lang = Some("en-GB".into());
916 assert!(parse_selector(":lang(en)").matches(&chain([british.clone()])));
917 assert!(parse_selector(":lang(en-GB)").matches(&chain([british.clone()])));
918 assert!(!parse_selector(":lang(enx)").matches(&chain([british.clone()])));
919
920 british.lang = Some("fr".into());
921 assert!(!parse_selector(":lang(en)").matches(&chain([british.clone()])));
922
923 british.lang = Some("EN-gb".into());
925 assert!(parse_selector(":lang(en)").matches(&chain([british])));
926
927 let mut untagged = element("div", &[], 1, 1, 1, 1);
929 untagged.lang = None;
930 assert!(!parse_selector(":lang(en)").matches(&chain([untagged])));
931 }
932
933 #[test]
934 fn lang_pseudo_class_handles_non_ascii_boundaries() {
935 let mut subject = element("div", &[], 1, 1, 1, 1);
938 subject.lang = Some("é".into());
939 assert!(!parse_selector(":lang(e)").matches(&chain([subject.clone()])));
940
941 subject.lang = Some("en-日本語".into());
943 assert!(parse_selector(":lang(en)").matches(&chain([subject])));
944 }
945
946 #[test]
947 fn nth_pseudo_class_is_immune_to_index_overflow() {
948 let huge = element("p", &[], 2_147_483_649, 3_000_000_000, 1, 1);
951 assert!(parse_selector(":nth-child(odd)").matches(&chain([huge.clone()])));
952 assert!(!parse_selector(":nth-child(-n)").matches(&chain([huge])));
955 }
956
957 #[test]
958 fn checked_pseudo_class_requires_a_checkable_input() {
959 let mut checkbox = input_element("checkbox", true);
960 assert!(parse_selector(":checked").matches(&chain([checkbox.clone()])));
961
962 checkbox.checked = false;
964 checkbox.attributes = vec![("checked".into(), "".into())].into();
965 assert!(!parse_selector(":checked").matches(&chain([checkbox])));
966
967 let mut text = input_element("text", true);
969 text.checked = true;
970 assert!(!parse_selector(":checked").matches(&chain([text])));
971
972 let mut untyped = input_element("text", false);
974 untyped.checked = true;
975 assert!(!parse_selector(":checked").matches(&chain([untyped])));
976 }
977
978 #[test]
979 fn selector_list_pseudo_classes_and_multiple_pseudos_match() {
980 let visible_first = element("li", &["item"], 1, 3, 1, 3);
981 let hidden_first = element("li", &["item", "hidden"], 1, 3, 1, 3);
982 let selector = parse_selector("li.item:first-child:not(.hidden)");
983
984 assert!(selector.matches(&chain([visible_first.clone()])));
985 assert!(!selector.matches(&chain([hidden_first])));
986 assert!(parse_selector(":is(article, li.item)").matches(&chain([visible_first.clone()])));
987 assert!(parse_selector(":where(.item, .card)").matches(&chain([visible_first])));
988 }
989
990 #[test]
991 fn selector_list_pseudo_classes_follow_specificity_rules() {
992 assert_eq!(
993 parse_selector(":where(#main, .item)").specificity(),
994 (0, 0, 0)
995 );
996 assert_eq!(parse_selector(":is(#main, .item)").specificity(), (1, 0, 0));
997 assert_eq!(
998 parse_selector("li:not(.hidden):first-child").specificity(),
999 (0, 2, 1)
1000 );
1001 }
1002
1003 #[test]
1004 fn hover_and_active_pseudo_classes_match_element_state() {
1005 let selector = parse_selector("button:hover");
1006 let mut el = element("button", &[], 1, 1, 1, 1);
1007 assert!(!selector.matches(&chain([el.clone()])));
1008 el.hovered = true;
1009 assert!(selector.matches(&chain([el])));
1010
1011 let selector = parse_selector("a:active");
1012 let mut el = element("a", &[], 1, 1, 1, 1);
1013 el.attributes = vec![("href".into(), "/".into())].into();
1014 assert!(!selector.matches(&chain([el.clone()])));
1015 el.active = true;
1016 assert!(selector.matches(&chain([el])));
1017 }
1018
1019 #[test]
1020 fn hover_and_active_contribute_to_specificity() {
1021 assert_eq!(parse_selector("a:hover").specificity(), (0, 1, 1));
1022 assert_eq!(parse_selector("a:active").specificity(), (0, 1, 1));
1023 }
1024}