Skip to main content

orinium_browser/engine/css/
matcher.rs

1//! CSSセレクターマッチング処理。DOM要素とセレクターの照合を行う。
2
3use 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/// A single element's identity as the selector matcher sees it.
11///
12/// The string-bearing fields are shared through `Arc` because the same element
13/// is described many times per layout build: once when its parent builds the
14/// sibling list and again inside the ancestor/sibling [`ElementChain`] links.
15/// Cheap `Clone` keeps that repeated description from re-cloning every
16/// attribute string on the hot path.
17#[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    /// One-based index among element siblings.
24    pub element_index: usize,
25    /// Number of element siblings including this element.
26    pub element_count: usize,
27    /// One-based index among siblings with the same tag name.
28    pub type_index: usize,
29    /// Number of siblings with the same tag name.
30    pub type_count: usize,
31    /// Element siblings preceding this element in document order, nearest
32    /// first. Links are shared through `Arc`, so building it per sibling is
33    /// O(1).
34    pub previous_siblings: ElementChain,
35    /// Whether this element has a parent element.
36    ///
37    /// The tree root is an element with no parent, and structural
38    /// pseudo-classes such as `:first-child` must not match it.
39    pub has_parent: bool,
40    /// Whether a checkbox or radio is currently checked. This is live
41    /// checkedness, not the `checked` content attribute, which is
42    /// `defaultChecked` and does not affect `:checked`.
43    pub checked: bool,
44    /// The language tag in effect for this element: its own `lang` attribute, or
45    /// failing that the nearest ancestor's. `:lang()` matches against this.
46    pub lang: Option<Arc<str>>,
47    /// Whether this element is `:empty` under the Selectors 4 definition:
48    /// no element children, and no text node with non-empty data. Comments and
49    /// processing instructions do not count.
50    pub is_empty: bool,
51    /// Whether this element or an ancestor is under the pointer.
52    pub hovered: bool,
53    /// Whether this element or an ancestor is the active (pressed) target.
54    pub active: bool,
55}
56
57/// One link of an [`ElementChain`].
58#[derive(Debug)]
59struct ChainLink {
60    info: ElementInfo,
61    /// Ancestors (parent, grandparent, …), shared with the parent's chain.
62    next: Option<Arc<ChainLink>>,
63    /// Lazily built [`ChainSummary`] covering this link and every link after
64    /// it. Shared links compute it once per layout run.
65    summary: OnceLock<Box<ChainSummary>>,
66}
67
68/// Tag/id/class strings appearing anywhere along a chain, used to reject
69/// descendant and sibling walks without traversing them.
70#[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        // Reuse an already-built summary further down the chain when present.
80        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(&current.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    /// Returns `false` only when no element in the chain can satisfy the
107    /// non-structural parts of `selector`: a required tag, id or class that
108    /// appears nowhere rules every candidate out at once.
109    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    /// Summary of this link and every link after it, built on first use.
129    fn summary(&self) -> &ChainSummary {
130        self.summary.get_or_init(|| ChainSummary::collect(self))
131    }
132}
133
134/// 右(自分)→ 左(祖先)
135///
136/// Cloning is O(1) and prepending an element is O(1): descendant chains share
137/// their ancestor links through `Arc`.
138#[derive(Debug, Clone, Default)]
139pub struct ElementChain {
140    head: Option<Arc<ChainLink>>,
141}
142
143impl ElementChain {
144    /// Returns this chain extended with `info` as the innermost element.
145    ///
146    /// Passing `None` yields an equivalent chain without allocating.
147    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    /// The innermost (current) element.
161    pub fn first(&self) -> Option<&ElementInfo> {
162        self.head.as_deref().map(|link| &link.info)
163    }
164
165    /// Builds a chain from elements ordered innermost-first.
166    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    /// Builds a chain by prepending `elements` in iteration order, so the
175    /// last element becomes the nearest link. Feeding siblings in document
176    /// order therefore yields a nearest-first chain.
177    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    /// Link of the sibling being matched when a sibling combinator walks the
190    /// preceding siblings; `None` while matching the element itself.
191    sibling_link: Option<&'a ChainLink>,
192}
193
194/// Whether an element is an `input` whose type makes it a checkbox or a radio,
195/// the only inputs that can ever be `:checked`.
196fn 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        // A missing `type` attribute means `type=text`.
210        None => false,
211    }
212}
213
214/// `:lang(<language>)` matches when the element's language tag is exactly the
215/// requested one, or a subtag of it: `:lang(en)` matches `en` and `en-GB`, but
216/// not `enx`. Comparison is ASCII case-insensitive, as HTML requires.
217fn 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    // Compare the byte prefix safely: `language.len()` may not fall on a char
225    // boundary of `tag`, in which case there is no match rather than a panic.
226    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    // Widen to `i128` so neither `index - b` nor the modulo/division can
233    // overflow for large element indices or extreme `a`/`b` values.
234    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    /// Matches the non-structural portion of this selector against one element.
246    fn matches_base(&self, element: &ElementInfo) -> bool {
247        // tag
248        if let Some(tag) = &self.tag
249            && tag.as_str() != element.tag_name.as_ref()
250        {
251            return false;
252        }
253
254        // id
255        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        // class
263        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                    // Custom element tag names always contain a hyphen
339                    // (HTML spec), so an element with no hyphen is a
340                    // defined standard element. This keeps `:not(:defined)`
341                    // selectors (e.g. reddit's `visibility: hidden` on
342                    // unhydrated custom elements) from matching the whole
343                    // document tree.
344                    !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                    // Only checkboxes and radios are ever `:checked`; the
355                    // `checked` content attribute alone does not qualify,
356                    // because it only sets the default.
357                    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            // ::before / ::after pseudo-elements always match their originating
425            // element. Other pseudo-elements are not yet supported.
426            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        // セレクタが尽きた → 完全一致
477        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                // Fast-fail: no ancestor carries a required tag/id/class.
488                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                // Fast-fail: no preceding sibling carries a required tag/id/class.
524                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; // id
543        let mut b = 0; // class / attr / pseudo-class
544        let mut c = 0; // tag / pseudo-element
545
546        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            // These fixtures always describe elements that sit below a parent,
759            // which is what the structural pseudo-classes require.
760            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        // Tag, class and id requirements that appear nowhere up the chain.
807        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        // Attribute-only and universal compounds cannot be ruled out by the
824        // summary and must still walk the chain.
825        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        // A root element has no parent *element*: the document above it does not
881        // count, so `:first-child` and friends must not match it.
882        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        // Case-insensitive, per HTML.
924        british.lang = Some("EN-gb".into());
925        assert!(parse_selector(":lang(en)").matches(&chain([british])));
926
927        // A missing language never matches.
928        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        // `language.len()` lands inside a multi-byte character of the tag;
936        // this must not panic and must not match.
937        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        // A valid ASCII prefix followed by a multi-byte subtag still matches.
942        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        // An element index above `i32::MAX` previously truncated to a negative
949        // `i32`, yielding a wrong answer and risking arithmetic overflow.
950        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        // `:nth-child(-n)` has a = -1; on `i32::MIN` the modulo/division used to
953        // overflow and panic.
954        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        // Live checkedness, not the `checked` attribute, decides.
963        checkbox.checked = false;
964        checkbox.attributes = vec![("checked".into(), "".into())].into();
965        assert!(!parse_selector(":checked").matches(&chain([checkbox])));
966
967        // A text input is never `:checked`, however its state reads.
968        let mut text = input_element("text", true);
969        text.checked = true;
970        assert!(!parse_selector(":checked").matches(&chain([text])));
971
972        // A missing `type` means `type=text`.
973        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}