Skip to main content

css_to_xpath/parser/
mod.rs

1//! `SelectorImpl` and `Parser` implementations bridging Servo's `selectors`
2//! crate to this crate's translator.
3
4mod impls;
5
6use cssparser::{Parser as CssParser, ToCss, Token, match_ignore_ascii_case};
7use selectors::parser::{
8    Component, NonTSPseudoClass, ParseRelative, PseudoElement, RelativeSelector, Selector,
9    SelectorImpl, SelectorList, SelectorParseErrorKind,
10};
11use selectors::visitor::{SelectorListKind, SelectorVisitor};
12use std::fmt;
13
14pub(crate) use impls::CssString;
15
16use crate::translate::error::{Error, ParseErrorKind};
17
18#[derive(Clone, Debug)]
19pub(crate) struct CssToXpathImpl;
20
21impl SelectorImpl for CssToXpathImpl {
22    type ExtraMatchingData<'a> = ();
23    type AttrValue = CssString;
24    type Identifier = CssString;
25    type LocalName = CssString;
26    type NamespaceUrl = CssString;
27    type NamespacePrefix = CssString;
28    type BorrowedNamespaceUrl = str;
29    type BorrowedLocalName = str;
30    type NonTSPseudoClass = PseudoClass;
31    type PseudoElement = NeverPseudoElement;
32}
33
34/// The non-tree-structural pseudo-classes the translators know.
35/// Everything here is the "never matches" set under the generic
36/// translator; the HTML translator overrides `:checked`, `:link`,
37/// `:enabled`, `:disabled`, the form-state family (`:read-only`,
38/// `:read-write`, `:default`, `:placeholder-shown`), and `:lang()`. Any
39/// other pseudo name is rejected at parse time (tree-structural pseudos
40/// are parsed natively by Servo and never reach this type).
41///
42/// Policy for what belongs here versus erroring: pseudo-classes whose
43/// semantics rest on user or runtime state a static document cannot have
44/// (the user-action, link, and target families) parse and never match, as
45/// does `:dir()`, whose *resolved* directionality needs the bidi
46/// algorithm rather than the document tree (see `apply_pseudo_class`).
47/// Names that are unknown, or whose semantics rest on machinery outside
48/// the document tree that a static translation would have to guess at
49/// (`:valid` and the constraint-validation family, `:indeterminate`,
50/// whose checkbox state is IDL-only and whose radio-group arm XPath 1.0
51/// cannot express, `:defined`), error instead, so typos and genuinely
52/// missing features stay loud.
53#[derive(Clone, Debug, Eq, PartialEq)]
54pub(crate) enum PseudoClass {
55    AnyLink,
56    Link,
57    Visited,
58    Hover,
59    Active,
60    Focus,
61    FocusWithin,
62    FocusVisible,
63    Target,
64    TargetWithin,
65    LocalLink,
66    Enabled,
67    Disabled,
68    Checked,
69    Required,
70    Optional,
71    ReadOnly,
72    ReadWrite,
73    Default,
74    PlaceholderShown,
75    /// The comma-separated language ranges of `:lang()`, each
76    /// reassembled from the tokens it was spelled with (see
77    /// [`is_valid_lang_range`]).
78    Lang(Vec<String>),
79    /// The single identifier of `:dir()`, kept only so the selector can
80    /// be serialized back: the translation never matches whatever it
81    /// says, so `:dir(rtl)` and `:dir(foo)` translate alike. Selectors 4
82    /// defines `ltr` and `rtl`; any other identifier is accepted rather
83    /// than rejected, since no value can change the output.
84    Dir(String),
85}
86
87impl PseudoClass {
88    fn name(&self) -> &'static str {
89        match self {
90            PseudoClass::AnyLink => "any-link",
91            PseudoClass::Link => "link",
92            PseudoClass::Visited => "visited",
93            PseudoClass::Hover => "hover",
94            PseudoClass::Active => "active",
95            PseudoClass::Focus => "focus",
96            PseudoClass::FocusWithin => "focus-within",
97            PseudoClass::FocusVisible => "focus-visible",
98            PseudoClass::Target => "target",
99            PseudoClass::TargetWithin => "target-within",
100            PseudoClass::LocalLink => "local-link",
101            PseudoClass::Enabled => "enabled",
102            PseudoClass::Disabled => "disabled",
103            PseudoClass::Checked => "checked",
104            PseudoClass::Required => "required",
105            PseudoClass::Optional => "optional",
106            PseudoClass::ReadOnly => "read-only",
107            PseudoClass::ReadWrite => "read-write",
108            PseudoClass::Default => "default",
109            PseudoClass::PlaceholderShown => "placeholder-shown",
110            PseudoClass::Lang(_) => "lang",
111            PseudoClass::Dir(_) => "dir",
112        }
113    }
114}
115
116impl ToCss for PseudoClass {
117    fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
118        dest.write_char(':')?;
119        dest.write_str(self.name())?;
120        match self {
121            PseudoClass::Lang(ranges) => {
122                dest.write_char('(')?;
123                for (i, range) in ranges.iter().enumerate() {
124                    if i > 0 {
125                        dest.write_str(", ")?;
126                    }
127                    // A range is written back as the token sequence it
128                    // was parsed from: `*` cannot be part of an
129                    // identifier, so the pieces around it are serialized
130                    // separately (`en-*` as the ident `en-` then `*`).
131                    // The empty range has no such token — an identifier
132                    // cannot be empty — and is written as the string it
133                    // has to be written as.
134                    if range.is_empty() {
135                        dest.write_str("\"\"")?;
136                    }
137                    for (j, piece) in range.split('*').enumerate() {
138                        if j > 0 {
139                            dest.write_char('*')?;
140                        }
141                        if !piece.is_empty() {
142                            cssparser::serialize_identifier(piece, dest)?;
143                        }
144                    }
145                }
146                dest.write_char(')')
147            }
148            PseudoClass::Dir(value) => {
149                dest.write_char('(')?;
150                cssparser::serialize_identifier(value, dest)?;
151                dest.write_char(')')
152            }
153            _ => Ok(()),
154        }
155    }
156}
157
158impl NonTSPseudoClass for PseudoClass {
159    fn is_active_or_hover(&self) -> bool {
160        matches!(self, PseudoClass::Active | PseudoClass::Hover)
161    }
162
163    fn is_user_action_state(&self) -> bool {
164        matches!(
165            self,
166            PseudoClass::Active
167                | PseudoClass::Hover
168                | PseudoClass::Focus
169                | PseudoClass::FocusWithin
170                | PseudoClass::FocusVisible
171        )
172    }
173}
174
175/// Uninhabited: `parse_pseudo_element` is left at its erroring default, so
176/// `::before` etc. fail to parse — pseudo-elements are not supported.
177#[derive(Clone, Debug, Eq, PartialEq)]
178pub(crate) enum NeverPseudoElement {}
179
180impl ToCss for NeverPseudoElement {
181    // The standard way to write a total match on an uninhabited type.
182    // `&self` here can never be a live reference, so clippy's warning
183    // about dereferencing one describes a call that cannot happen.
184    #[allow(clippy::uninhabited_references)]
185    fn to_css<W: fmt::Write>(&self, _dest: &mut W) -> fmt::Result {
186        match *self {}
187    }
188}
189
190impl PseudoElement for NeverPseudoElement {}
191
192pub(crate) struct CssToXpathParser<'a> {
193    /// Whether Servo may recover from an invalid `:is()` / `:where()`
194    /// argument instead of failing the whole parse. Only the retry in
195    /// [`parse`] sets this, and it then rejects every recovery bar the
196    /// empty argument list.
197    forgiving: bool,
198    /// The caller's default namespace prefix, or the empty sentinel
199    /// (see [`CssToXpathParser::default_namespace`]). Built once per
200    /// parse, since Servo asks for it once per compound.
201    default_namespace: &'a CssString,
202    /// When this parse is one of the truncated probes [`locate`] makes,
203    /// the length of the probe's input. A functional pseudo-class whose
204    /// arguments run out right there was cut short by the truncation,
205    /// not written wrong, so it reports end of input as Servo's own
206    /// constructs do: an error that a longer probe could still undo
207    /// must not read as one already made.
208    probe_end: Option<usize>,
209}
210
211/// Why a functional pseudo-class was rejected.
212#[derive(Clone, Copy, Debug, Eq, PartialEq)]
213enum Rejected {
214    /// Its arguments ended before they said enough: `:dir()`,
215    /// `:lang(en,`.
216    Incomplete,
217    /// Something in it is wrong as written: an unknown name, or an
218    /// argument token that cannot be where it is.
219    Invalid,
220}
221
222impl CssToXpathParser<'_> {
223    /// The error for a functional pseudo-class rejected for `why`, with
224    /// `parser` left where its arguments were rejected.
225    fn functional_error(
226        &self,
227        parser: &CssParser<'_>,
228        why: Rejected,
229    ) -> cssparser::ParseError<SelectorParseErrorKind> {
230        // Arguments that ran out at the end of a probe's input, rather
231        // than at their `)`, were cut short by the probe.
232        if why == Rejected::Incomplete && self.probe_end == Some(parser.position().byte_index()) {
233            return cssparser::ParseError::from_basic_kind(
234                cssparser::BasicParseErrorKind::EndOfInput,
235            );
236        }
237        cssparser::ParseError::custom(SelectorParseErrorKind::UnsupportedPseudoClassOrElement)
238    }
239}
240
241impl<'i> selectors::parser::Parser<'i> for CssToXpathParser<'_> {
242    type Impl = CssToXpathImpl;
243    type Error = SelectorParseErrorKind;
244
245    /// Strict unless [`parse`] is retrying: a selector that fails to
246    /// parse must surface an error, never be silently dropped the way
247    /// forgiving `:is()`/`:where()` parsing would.
248    fn allow_forgiving_selectors(&self) -> bool {
249        self.forgiving
250    }
251
252    /// Enable `:is()` and `:where()`.
253    fn parse_is_and_where(&self) -> bool {
254        true
255    }
256
257    /// `:matches()` is the legacy alias for `:is()`.
258    fn is_is_alias(&self, name: &str) -> bool {
259        name.eq_ignore_ascii_case("matches")
260    }
261
262    /// Enable `:has()`. The translator restricts the arguments to
263    /// compound selectors (with an optional leading combinator).
264    fn parse_has(&self) -> bool {
265        true
266    }
267
268    /// `:nth-child(an+b of S)` / `:nth-last-child(an+b of S)`,
269    /// CSS Selectors Level 4.
270    fn parse_nth_child_of(&self) -> bool {
271        true
272    }
273
274    /// The supported non-tree-structural pseudo-classes: the "never
275    /// matches" set plus the HTML-translator overrides. Anything else
276    /// errors (see the policy note on `PseudoClass`).
277    fn parse_non_ts_pseudo_class(
278        &self,
279        name: cssparser::CowRcStr<'i>,
280    ) -> Result<PseudoClass, cssparser::ParseError<Self::Error>> {
281        let pc = match_ignore_ascii_case! { &name,
282            "any-link" => PseudoClass::AnyLink,
283            "link" => PseudoClass::Link,
284            "visited" => PseudoClass::Visited,
285            "hover" => PseudoClass::Hover,
286            "active" => PseudoClass::Active,
287            "focus" => PseudoClass::Focus,
288            "focus-within" => PseudoClass::FocusWithin,
289            "focus-visible" => PseudoClass::FocusVisible,
290            "target" => PseudoClass::Target,
291            "target-within" => PseudoClass::TargetWithin,
292            "local-link" => PseudoClass::LocalLink,
293            "enabled" => PseudoClass::Enabled,
294            "disabled" => PseudoClass::Disabled,
295            "checked" => PseudoClass::Checked,
296            "required" => PseudoClass::Required,
297            "optional" => PseudoClass::Optional,
298            "read-only" => PseudoClass::ReadOnly,
299            "read-write" => PseudoClass::ReadWrite,
300            "default" => PseudoClass::Default,
301            "placeholder-shown" => PseudoClass::PlaceholderShown,
302            _ => {
303                return Err(cssparser::ParseError::custom(
304                    SelectorParseErrorKind::UnsupportedPseudoClassOrElement,
305                ));
306            },
307        };
308        Ok(pc)
309    }
310
311    /// `:lang()` argument grammar: a comma-separated list of at least
312    /// one language range, each an ident or string optionally glued to
313    /// `*` wildcards. Whitespace is allowed only around the commas: it
314    /// is a range *terminator*, never a separator, so `:lang(en fr)` is
315    /// an error rather than two ranges, and `en *` is not the range
316    /// `en-*`. A range is assembled here, while the tokens' adjacency is
317    /// still known — the tokenizer splits `en-*` into an ident and a
318    /// delimiter. NUMBER/`+`/`-` tokens are rejected. `:dir()` is
319    /// stricter, matching its selectors-4 grammar: exactly one
320    /// identifier.
321    ///
322    /// What the assembled text *says* is not judged here: a range whose
323    /// subtags are malformed (`en-`, `*en`) is a well-formed argument
324    /// that no language tag can match, and it is the translators that
325    /// reject it — by then the range is in hand and the message can name
326    /// it (see `check_lang_range`).
327    ///
328    /// The non-standard text-content pseudo `:contains()` is deliberately
329    /// unsupported and falls through to the rejection arm, as does any
330    /// unknown functional pseudo.
331    fn parse_non_ts_functional_pseudo_class(
332        &self,
333        name: cssparser::CowRcStr<'i>,
334        parser: &mut CssParser<'i>,
335        _after_part: bool,
336    ) -> Result<PseudoClass, cssparser::ParseError<Self::Error>> {
337        if name.eq_ignore_ascii_case("dir") {
338            let value = match parser.next() {
339                Ok(Token::Ident(v)) => v.as_ref().to_owned(),
340                Ok(_) => return Err(self.functional_error(parser, Rejected::Invalid)),
341                Err(_) => return Err(self.functional_error(parser, Rejected::Incomplete)),
342            };
343            if parser.next().is_ok() {
344                return Err(self.functional_error(parser, Rejected::Invalid));
345            }
346            return Ok(PseudoClass::Dir(value));
347        }
348        if !name.eq_ignore_ascii_case("lang") {
349            return Err(self.functional_error(parser, Rejected::Invalid));
350        }
351
352        parse_lang_ranges(parser)
353            .map(PseudoClass::Lang)
354            .map_err(|why| self.functional_error(parser, why))
355    }
356
357    /// Identity mapping: `svg|g` translates to `svg:g` — a prefix-only
358    /// namespace model with no URL maps.
359    fn namespace_for_prefix(&self, prefix: &CssString) -> Option<CssString> {
360        Some(prefix.clone())
361    }
362
363    /// The caller's default namespace prefix, or a sentinel standing in
364    /// for "none set".
365    ///
366    /// A default namespace is always reported, because without one Servo
367    /// drops the namespace component from both `e` and `*|e` (they match
368    /// identically), and the two must translate differently (`e` vs a
369    /// `local-name()` test). So with none set, plain `e` carries
370    /// `DefaultNamespace("")` — mapped to "no constraint" — while `*|e`
371    /// keeps `ExplicitAnyNamespace`. The empty string can never collide
372    /// with a real prefix (prefixes are non-empty idents, and
373    /// `namespace_for_prefix` is the identity), which is also what makes
374    /// an empty configured prefix mean "no default namespace".
375    ///
376    /// With a prefix set, Servo applies CSS Namespaces 3 for us: the
377    /// prefix reaches `DefaultNamespace` for type selectors and for the
378    /// implicit universal of a type-less compound, but not for the
379    /// featureless compounds of an `:is()` / `:where()` / `:not()`
380    /// argument, and a written `h|e` naming the same prefix collapses
381    /// onto the same component.
382    fn default_namespace(&self) -> Option<CssString> {
383        Some(self.default_namespace.clone())
384    }
385}
386
387/// The body of the `:lang()` argument grammar: the comma-separated
388/// ranges, or why the arguments do not spell out at least one range.
389/// Assembling happens here rather than at translation time because only
390/// the token stream records whether two pieces were adjacent, and
391/// adjacency is the whole difference between the range `en-*` and the
392/// pair `en-`, `*`.
393///
394/// A range is any assembled text, the empty string included: `:lang("")`
395/// is the Level 4 "language not known" range, and the shapes that cannot
396/// match anything are the translators' to reject.
397fn parse_lang_ranges(parser: &mut CssParser<'_>) -> Result<Vec<String>, Rejected> {
398    let mut ranges: Vec<String> = Vec::new();
399    let mut current = String::new();
400    // Whether `current` has a piece yet, and whether the next piece
401    // would be adjacent to it. The two are distinct because an empty
402    // string is a piece: `:lang("" *)` has started a range even though
403    // `current` is still empty.
404    let mut started = false;
405    let mut adjacent = true;
406    // Whitespace and comments both terminate a range, so neither may be
407    // skipped over here. The loop ends with the function's arguments.
408    while let Ok(token) = parser.next_including_whitespace_and_comments().cloned() {
409        let piece = match token {
410            Token::WhiteSpace(_) | Token::Comment(_) => {
411                adjacent = false;
412                continue;
413            }
414            Token::Comma => {
415                if !started {
416                    // an empty range slot: `,en`, `en,,fr`
417                    return Err(Rejected::Invalid);
418                }
419                ranges.push(std::mem::take(&mut current));
420                (started, adjacent) = (false, true);
421                continue;
422            }
423            Token::Ident(ref v) | Token::QuotedString(ref v) => v.as_ref().to_owned(),
424            Token::Delim('*') => "*".to_owned(),
425            _ => return Err(Rejected::Invalid),
426        };
427        if started && !adjacent {
428            // two ranges with no comma between them
429            return Err(Rejected::Invalid);
430        }
431        current.push_str(&piece);
432        started = true;
433        // Adjacency is a fact about the gap between two tokens, so it is
434        // restored the moment a piece is taken: only whitespace or a
435        // comment *after* this one can separate it from the next. Leaving
436        // it false here would spread a run of whitespace over the rest of
437        // the range and reject a multi-token range after a comma, so that
438        // `:lang(*-CH, en)` parsed but `:lang(en, *-CH)` did not.
439        adjacent = true;
440    }
441    if !started {
442        // no ranges at all, or a trailing comma
443        return Err(Rejected::Incomplete);
444    }
445    ranges.push(current);
446    Ok(ranges)
447}
448
449/// The maximum functional-pseudo-class nesting depth accepted, measured
450/// as parenthesis nesting in the source selector. Both Servo's parser and
451/// this crate's translator recurse once per nesting level (as does
452/// dropping the resulting selector tree), so an unbounded depth would
453/// overflow the stack — a hard abort, not a panic, so the caller cannot
454/// catch it.
455///
456/// The value is set from the profile that costs the most stack per level,
457/// against the smallest stack the crate can be run on. An unoptimized
458/// build spends about 16 KB a level (against about 4 KB optimized), so 32
459/// levels need roughly 600 KB: a comfortable fit in the 1 MiB a library
460/// does not get to choose — the default reserve of a Windows main thread,
461/// rustc's `wasm32-unknown-unknown` stack, and whatever a thread pool
462/// hands its workers. Sizing against Rust's more generous 2 MB default
463/// for a spawned thread instead would let a debug build abort on those
464/// targets at a depth this limit promises to accept, which is the limit
465/// failing at the one job it has.
466///
467/// 32 is still far beyond any hand-written selector, and the depth counted
468/// is every parenthesis pair, including ones that do not recurse at all
469/// (`:nth-child(2n+1)`, `:lang(en)`) and ones that spend two per level
470/// (`:nth-child(2 of :is(…))`), so real selectors sit further under it
471/// than the number suggests.
472pub const MAX_NESTING_DEPTH: usize = 32;
473
474/// The facts about a selector that must be known before Servo is entered,
475/// gathered in one linear walk that skips strings, escapes, and comments.
476struct Scan {
477    /// The byte offset of the first `|` of the Level 4 column combinator
478    /// `||`, if the selector uses one. Outside strings, escapes, and
479    /// comments a doubled pipe can only be that combinator (a single `|`
480    /// occurs in namespace prefixes and `|=`, never doubled). Servo has
481    /// no column-combinator support and its parse error misreads the
482    /// second pipe as namespace syntax
483    /// (`ExplicitNamespaceUnexpectedToken`), so the construct is caught
484    /// before parsing and named properly. Column selection has no XPath
485    /// 1.0 translation anyway: column membership depends on
486    /// `colspan`/`rowspan` layout arithmetic.
487    column_combinator: Option<usize>,
488    /// The byte offset of the first `(` that opened a level deeper than
489    /// [`MAX_NESTING_DEPTH`], if any — the point at which the selector
490    /// went too deep for the parser and translator to recurse through
491    /// safely, and so the point to put a caret under. The *first* such
492    /// parenthesis, not the innermost, so the position does not move
493    /// with however much deeper the rest of the selector goes.
494    too_deep: Option<usize>,
495    /// The byte offset of the first `&` nesting selector, if the
496    /// selector uses one. Outside strings, escapes, and comments an `&`
497    /// can only be that selector: it appears in no other selector
498    /// production. This crate parses with nesting disabled — a `&` has
499    /// no meaning without the enclosing rule a selector-to-XPath
500    /// function never sees — so Servo does not recognise it as the
501    /// start of a compound and fails on whatever comes next instead,
502    /// reporting `&` as an empty selector or a dangling combinator.
503    /// Catching it here names the construct the caller actually wrote.
504    nesting_selector: Option<usize>,
505    /// The byte offset of the first `:scope` this crate cannot
506    /// translate, and which of the two ways it is out of place. Both
507    /// are lexical facts — `:scope` is unsupported inside any
508    /// functional pseudo-class argument, and at the top level anywhere
509    /// but the leftmost compound of its group — so the walk can decide
510    /// them from the parenthesis depth and whether a combinator has
511    /// been passed, and hand the translator's own check a position it
512    /// has no way to recover. See [`ScopeSite`].
513    misplaced_scope: Option<(usize, ScopeSite)>,
514    /// The byte offset of the first `:host(`, if the selector uses one.
515    /// Shadow-DOM host selection has no XPath 1.0 translation at all,
516    /// so — like `||` — the mere presence of the construct is the
517    /// error, wherever it sits. Only the functional form is looked for:
518    /// a bare `:host` is not a pseudo-class this crate's parser accepts,
519    /// so it fails to parse and never reaches translation.
520    host: Option<usize>,
521}
522
523/// Which of the two positions a [`Scan::misplaced_scope`] was found in,
524/// since they are reported as different constructs.
525#[derive(Clone, Copy, Eq, PartialEq)]
526enum ScopeSite {
527    /// Inside a functional pseudo-class argument, where an XPath 1.0
528    /// predicate cannot name the context node at all.
529    Functional,
530    /// At the top level, but not in the leftmost compound of its group
531    /// — the one place `:scope` translates, by anchoring the whole
532    /// expression on the `self::` axis instead of the caller's prefix.
533    NotLeftmost,
534}
535
536impl ScopeSite {
537    /// The construct phrase for this site, as the object of "uses …".
538    /// Worded exactly as the translator's own check words it, so the
539    /// two cannot diverge: only the position is new.
540    fn construct(self) -> &'static str {
541        match self {
542            ScopeSite::Functional => "the `:scope` pseudo-class inside a functional pseudo-class",
543            ScopeSite::NotLeftmost => "the `:scope` pseudo-class outside the leftmost compound",
544        }
545    }
546}
547
548/// How far into its selector-list group the walk has got, which is all
549/// that is needed to place a top-level `:scope`: it is supported in the
550/// leftmost compound of a group and nowhere else, so the question is
551/// only whether a combinator has been passed since the last top-level
552/// comma.
553#[derive(Default)]
554struct GroupPosition {
555    /// Whether anything that is part of a compound has been seen in
556    /// this group yet, so that leading whitespace is not a combinator.
557    content_seen: bool,
558    /// Whether whitespace has been seen after some content: a
559    /// descendant combinator if any content follows it, and nothing at
560    /// all if the group ends there.
561    space_pending: bool,
562    /// Whether a combinator — descendant, `>`, `+` or `~` — has been
563    /// passed, i.e. whether the leftmost compound is behind the walk.
564    combinator_seen: bool,
565}
566
567/// The string handling here diverges from the CSS tokenizer on one point:
568/// a newline inside a string ends it there, as a bad-string token, where
569/// this walk stays "in string" until the closing quote or the end of the
570/// input. So the walk can treat as string content — and skip — text the
571/// tokenizer reads as syntax, which for `||` only loses a nicer error
572/// message and for parentheses could undercount the depth.
573///
574/// Neither matters, because reaching that state needs a string that no
575/// newline-free closing quote follows, and cssparser turns the newline
576/// into a bad-string token that fails the parse. The skipped text is
577/// everything after that point, so nothing in it is ever parsed, let
578/// alone recursed into. Every selector that does parse is one the walk
579/// and the tokenizer agree about.
580fn scan(css: &str) -> Scan {
581    let bytes = css.as_bytes();
582    let mut i = 0;
583    let mut quote: Option<u8> = None;
584    let mut depth: usize = 0;
585    let mut brackets: usize = 0;
586    let mut group = GroupPosition::default();
587    let mut scan = Scan {
588        column_combinator: None,
589        too_deep: None,
590        nesting_selector: None,
591        misplaced_scope: None,
592        host: None,
593    };
594    while i < bytes.len() {
595        let b = bytes[i];
596        match quote {
597            Some(q) => {
598                if b == b'\\' {
599                    i += 1; // skip the escaped character
600                } else if b == q {
601                    quote = None;
602                }
603            }
604            None => {
605                // Where the walk is within its selector-list group,
606                // which only the top level has: inside a functional
607                // argument (`depth > 0`) a `:scope` is unsupported
608                // wherever it sits, and inside `[...]` nothing is a
609                // combinator — `[a~=b]`'s tilde least of all.
610                if depth == 0 && brackets == 0 {
611                    match b {
612                        b',' => group = GroupPosition::default(),
613                        b'>' | b'+' | b'~' => {
614                            group.combinator_seen = true;
615                            group.content_seen = true;
616                            group.space_pending = false;
617                        }
618                        b' ' | b'\t' | b'\n' | b'\r' | b'\x0C' => {
619                            group.space_pending |= group.content_seen;
620                        }
621                        // A comment is neither content nor whitespace:
622                        // it is removed before the selector grammar
623                        // sees it, so `a/**/b` is one compound.
624                        b'/' if bytes.get(i + 1) == Some(&b'*') => {}
625                        _ => {
626                            group.combinator_seen |= group.space_pending;
627                            group.space_pending = false;
628                            group.content_seen = true;
629                        }
630                    }
631                }
632                match b {
633                    b'\\' => i += 1, // skip the escaped character
634                    b'"' | b'\'' => quote = Some(b),
635                    b'/' if bytes.get(i + 1) == Some(&b'*') => {
636                        // Skip the comment body and its closing "*/".
637                        i += 2;
638                        while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
639                            i += 1;
640                        }
641                        i += 1;
642                    }
643                    b'|' if bytes.get(i + 1) == Some(&b'|') => {
644                        scan.column_combinator.get_or_insert(i);
645                    }
646                    b'(' => {
647                        depth += 1;
648                        if depth > MAX_NESTING_DEPTH {
649                            scan.too_deep.get_or_insert(i);
650                        }
651                    }
652                    // Unbalanced closers are Servo's to reject, not this
653                    // walk's: just never go below zero.
654                    b')' => depth = depth.saturating_sub(1),
655                    b'[' => brackets += 1,
656                    b']' => brackets = brackets.saturating_sub(1),
657                    b'&' => {
658                        scan.nesting_selector.get_or_insert(i);
659                    }
660                    // A literal `:scope` / `:host(` here is the
661                    // pseudo-class and nothing else — an escaped colon and
662                    // a quoted or commented-out one are all skipped above,
663                    // and any other ident starting with those letters
664                    // (`:scoped`, `::scope`) is a pseudo-class this crate's
665                    // parser rejects. Both findings are only ever consulted
666                    // after the parse has succeeded, which is what makes
667                    // that last part sound.
668                    b':' if bytes[i..].starts_with(b":scope") && brackets == 0 => {
669                        let site = if depth > 0 {
670                            Some(ScopeSite::Functional)
671                        } else if group.combinator_seen {
672                            Some(ScopeSite::NotLeftmost)
673                        } else {
674                            None // the leftmost compound, where it is supported
675                        };
676                        if let Some(site) = site {
677                            scan.misplaced_scope.get_or_insert((i, site));
678                        }
679                    }
680                    b':' if bytes[i..].starts_with(b":host(") => {
681                        scan.host.get_or_insert(i);
682                    }
683                    _ => {}
684                }
685            }
686        }
687        i += 1;
688    }
689    scan
690}
691
692/// The error one parse attempt produced, before it is turned into an
693/// [`Error`] — the kind is needed to tell an empty selector apart from
694/// everything else.
695type ParseFailure = cssparser::ParseError<SelectorParseErrorKind>;
696
697/// One way of parsing a selector list: strictly or forgivingly, under
698/// the caller's default namespace. A failed attempt is located by
699/// repeating it on truncations of the input (see [`locate`]), and a
700/// repeat has to be the same attempt to fail the same way, so the two
701/// settings travel together.
702#[derive(Clone, Copy)]
703struct Attempt<'a> {
704    forgiving: bool,
705    default_namespace: &'a CssString,
706}
707
708impl Attempt<'_> {
709    /// One parse of the whole selector list.
710    fn parse(self, css: &str) -> Result<SelectorList<CssToXpathImpl>, ParseFailure> {
711        self.run(css, None)
712    }
713
714    /// One parse of a truncation of the selector, for [`locate`].
715    fn probe(self, css: &str) -> Result<(), ParseFailure> {
716        self.run(css, Some(css.len())).map(drop)
717    }
718
719    fn run(
720        self,
721        css: &str,
722        probe_end: Option<usize>,
723    ) -> Result<SelectorList<CssToXpathImpl>, ParseFailure> {
724        SelectorList::parse(
725            &CssToXpathParser {
726                forgiving: self.forgiving,
727                default_namespace: self.default_namespace,
728                probe_end,
729            },
730            &mut CssParser::new(css),
731            ParseRelative::No,
732        )
733    }
734}
735
736/// Parse a full selector list (comma-separated groups).
737///
738/// Selectors 4 gives `:is()` and `:where()` a *forgiving* argument list,
739/// of which this crate wants exactly one part: an empty list is valid and
740/// matches nothing. Dropping *invalid* arguments is not wanted — a
741/// translation library must not quietly ignore what it was handed — so
742/// the strict parse decides, and forgiving parsing is only a retry whose
743/// result is accepted when the sole thing it recovered from was an empty
744/// argument list.
745pub(crate) fn parse(
746    css: &str,
747    default_namespace: Option<&str>,
748) -> Result<SelectorList<CssToXpathImpl>, Error> {
749    let scan = scan(css);
750    if let Some(offset) = scan.column_combinator {
751        return Err(Error::unsupported_at("the `||` column combinator", offset));
752    }
753    if let Some(offset) = scan.too_deep {
754        return Err(Error::unsupported_at(
755            format!("functional pseudo-classes nested more than {MAX_NESTING_DEPTH} levels deep"),
756            offset,
757        ));
758    }
759    // Reported after the other two, so a selector with more than one
760    // problem keeps the message it had before this check existed.
761    if let Some(offset) = scan.nesting_selector {
762        return Err(Error::unsupported_at("the `&` nesting selector", offset));
763    }
764    let list = parse_lists(css, default_namespace)?;
765    // The remaining findings are constructs Servo parses happily and
766    // the translator then rejects, so they are consulted only once the
767    // parse has succeeded: a selector that is *also* invalid CSS keeps
768    // the parse error it has always reported, and the walk never has to
769    // be right about text that never parsed. What the walk adds is the
770    // position, which the translator — handed components with no source
771    // offsets — cannot recover for itself.
772    if let Some((offset, site)) = scan.misplaced_scope {
773        return Err(Error::unsupported_at(site.construct(), offset));
774    }
775    if let Some(offset) = scan.host {
776        return Err(Error::unsupported_at("the `:host` pseudo-class", offset));
777    }
778    Ok(list)
779}
780
781/// The strict parse, and the forgiving retry that only an empty `:is()`
782/// / `:where()` argument list earns.
783fn parse_lists(
784    css: &str,
785    default_namespace: Option<&str>,
786) -> Result<SelectorList<CssToXpathImpl>, Error> {
787    let default_namespace = CssString::from(default_namespace.unwrap_or(""));
788    let strict = Attempt {
789        forgiving: false,
790        default_namespace: &default_namespace,
791    };
792    let forgiving = Attempt {
793        forgiving: true,
794        ..strict
795    };
796    let strict_error = match strict.parse(css) {
797        Ok(list) => return Ok(list),
798        Err(e) => e,
799    };
800    // The strict parse stops at the first error in source order, and an
801    // empty argument list is reported as `EmptySelector`. Any other
802    // strict error therefore sits before, or is, something the forgiving
803    // parse cannot accept cleanly either, and every outcome of the retry
804    // below would report it unchanged — so the retry is skipped.
805    if !is_empty_selector(&strict_error) {
806        return Err(parse_error(css, strict, &strict_error));
807    }
808    match forgiving.parse(css) {
809        Ok(list) if dropped_nothing(&list) => Ok(list),
810        // The forgiving parse recovered from a genuinely invalid
811        // argument: the strict error is the one that names it, and
812        // points at it.
813        Ok(_) => Err(parse_error(css, strict, &strict_error)),
814        // Both parses failed. An empty argument list is no longer an
815        // error, so a strict `EmptySelector` may well be blaming one,
816        // while the forgiving parse — which accepts those — stopped at
817        // whatever is actually wrong.
818        Err(e) if is_empty_selector(&strict_error) => Err(parse_error(css, forgiving, &e)),
819        Err(_) => Err(parse_error(css, strict, &strict_error)),
820    }
821}
822
823/// `e`, the error `attempt` failed `css` with, as this crate reports it.
824fn parse_error(css: &str, attempt: Attempt<'_>, e: &ParseFailure) -> Error {
825    let failure = locate(css, attempt, &e.kind);
826    let mut kind = ParseErrorKind::from_kind(&e.kind, failure.token.as_ref());
827    // A kind that echoes the token points at it; one that echoes nothing
828    // points where the parse stopped.
829    let offset = match kind {
830        ParseErrorKind::InvalidPosition | ParseErrorKind::Other(_) => failure.stopped,
831        _ => failure.at,
832    };
833    // `EmptySelector` is what `selectors` reports whenever a compound
834    // ends up with no components, which covers both "there was nothing
835    // here" and "none of what was here parsed". Only the first is what
836    // the name says, so the second is re-reported by its cause.
837    if matches!(kind, ParseErrorKind::EmptySelector)
838        && let Some(blocked) = blocking_token(css, offset)
839    {
840        kind = blocked;
841    }
842    Error::Parse { kind, offset }
843}
844
845/// Whether running out of input is itself enough to fail a parse with
846/// `kind`: a construct left open, a group with nothing in it, or a
847/// combinator with nothing after it. Every other kind needs a token
848/// that should not be where it is.
849fn is_truncation_kind(kind: &cssparser::ParseErrorKind<SelectorParseErrorKind>) -> bool {
850    matches!(
851        kind,
852        cssparser::ParseErrorKind::Basic(cssparser::BasicParseErrorKind::EndOfInput)
853            | cssparser::ParseErrorKind::Custom(
854                SelectorParseErrorKind::EmptySelector | SelectorParseErrorKind::DanglingCombinator
855            )
856    )
857}
858
859/// Where a parse failed, as [`locate`] finds it.
860struct Failure<'i> {
861    /// The byte offset of the token the parse failed on, or of where it
862    /// ran out.
863    at: usize,
864    /// The byte offset the parse had got to when it failed: past the
865    /// token it failed on, unless it only looked at that token.
866    stopped: usize,
867    /// The token the parse failed on, when it failed on one rather than
868    /// by running out.
869    token: Option<Token<'i>>,
870}
871
872/// One token of the selector, and the byte span it was written at.
873struct Spanned<'i> {
874    start: usize,
875    end: usize,
876    token: Token<'i>,
877}
878
879/// Where a failed `attempt` at `css` went wrong.
880///
881/// Neither `cssparser` nor `selectors` reports a position, so it is
882/// recovered by repeating the attempt on truncations of the input. Servo
883/// reads a selector once, left to right, and fails at the first thing it
884/// cannot use, so a truncation that keeps that thing fails the same way
885/// — and the shortest such truncation ends on it. A truncation that
886/// stops short of it can still fail, but only by running out of input,
887/// which Servo reports as one of the [truncation
888/// kinds](is_truncation_kind) whatever it was in the middle of.
889///
890/// So a failure of any other kind is located by a binary search for the
891/// shortest truncation, cut at a token boundary, that fails with a kind
892/// other than those. A failure of one of those kinds is itself a running
893/// out, of the input or of a block or a comma-separated group, and is
894/// located among the places that can happen instead (see
895/// [`locate_truncation`]).
896fn locate<'i>(
897    css: &'i str,
898    attempt: Attempt<'_>,
899    kind: &cssparser::ParseErrorKind<SelectorParseErrorKind>,
900) -> Failure<'i> {
901    let tokens = tokens(css);
902    let ran_out = |at| Failure {
903        at,
904        stopped: at,
905        token: None,
906    };
907    if is_truncation_kind(kind) {
908        return ran_out(locate_truncation(css, attempt, &tokens, kind));
909    }
910    let Some(last) = tokens.len().checked_sub(1) else {
911        return ran_out(css.len());
912    };
913    // The whole selector is known to fail, so the search is over the
914    // truncations before it, which only ever make the answer earlier.
915    let mut i = tokens[..last].partition_point(|t| match attempt.probe(&css[..t.end]) {
916        Ok(()) => true,
917        Err(e) => is_truncation_kind(&e.kind),
918    });
919    // A string or URL that a newline left bad is a good one if the
920    // input ends before the newline, so the truncation that keeps it
921    // whole succeeds, and the search lands on what follows it.
922    if i > 0 && matches!(tokens[i - 1].token, Token::BadString(_) | Token::BadUrl(_)) {
923        i -= 1;
924    }
925    if let cssparser::ParseErrorKind::Custom(
926        SelectorParseErrorKind::UnsupportedPseudoClassOrElement,
927    ) = kind
928    {
929        i = pseudo_name(&tokens, i);
930    }
931    // A token after whitespace can be one the parser only looked at,
932    // to see whether the whitespace was a descendant combinator, and
933    // then put back: a failure on that combinator stops before it. Any
934    // other combinator is read, and a failure on it stops past it.
935    let combinator = matches!(tokens[i].token, Token::Delim('>' | '+' | '~'));
936    let after_whitespace = tokens[..i]
937        .iter()
938        .rev()
939        .find(|t| !matches!(t.token, Token::Comment(_)))
940        .filter(|t| !combinator && matches!(t.token, Token::WhiteSpace(_)));
941    Failure {
942        at: tokens[i].start,
943        stopped: after_whitespace.map_or(tokens[i].end, |t| t.end),
944        token: Some(tokens[i].token.clone()),
945    }
946}
947
948/// The truncation-kind counterpart of [`locate`].
949///
950/// A block, a comma-separated group, and the input itself all end the
951/// same way as far as the parser inside them is concerned: there is
952/// nothing more to read. So the end that failed is found the way
953/// [`locate`] finds a token, among the truncations just before each
954/// `,`, `)` and `]` and at the end of the input — the first of which to
955/// fail is the one the error ran out at.
956///
957/// That end is the position of an [`EndOfInput`]. An empty compound
958/// (`EmptySelector`, `DanglingCombinator`) is the end too when nothing
959/// came between the compound's start and it (`a > , b`), but when
960/// something did, the compound was not empty but unusable (`a > #1abc`),
961/// and the position is that of the token the parser could not use. That
962/// token is found by another binary search, for the shortest truncation
963/// that fails as an empty compound even with a compound added: before
964/// the token, a truncation that fails that way is only waiting for a
965/// compound, and a `*` completes it; from the token on, nothing can.
966///
967/// [`EndOfInput`]: cssparser::BasicParseErrorKind::EndOfInput
968fn locate_truncation(
969    css: &str,
970    attempt: Attempt<'_>,
971    tokens: &[Spanned<'_>],
972    kind: &cssparser::ParseErrorKind<SelectorParseErrorKind>,
973) -> usize {
974    let ends: Vec<usize> = tokens
975        .iter()
976        .filter(|t| {
977            matches!(
978                t.token,
979                Token::Comma | Token::CloseParenthesis | Token::CloseSquareBracket
980            )
981        })
982        .map(|t| t.start)
983        .collect();
984    let end = ends
985        .get(ends.partition_point(|&end| attempt.probe(&css[..end]).is_ok()))
986        .copied()
987        .unwrap_or(css.len());
988    if matches!(kind, cssparser::ParseErrorKind::Basic(_)) {
989        return end;
990    }
991    // Whitespace and comments are skipped rather than used, so they are
992    // never the token the parser could not use.
993    let candidates: Vec<&Spanned<'_>> = tokens
994        .iter()
995        .take_while(|t| t.end <= end)
996        .filter(|t| !matches!(t.token, Token::WhiteSpace(_) | Token::Comment(_)))
997        .collect();
998    let unusable = candidates.partition_point(|t| {
999        let cut = &css[..t.end];
1000        let awaits_compound = attempt.probe(cut).is_err_and(|e| {
1001            matches!(
1002                e.kind,
1003                cssparser::ParseErrorKind::Custom(
1004                    SelectorParseErrorKind::EmptySelector
1005                        | SelectorParseErrorKind::DanglingCombinator
1006                )
1007            )
1008        });
1009        // The `*` goes in behind a comment, which the parser skips but
1010        // which keeps it from running into what precedes it: straight
1011        // after a `/` it would open a comment of its own.
1012        !(awaits_compound && attempt.probe(&format!("{cut}/**/*")).is_err())
1013    });
1014    candidates.get(unusable).map_or(end, |t| t.start)
1015}
1016
1017/// The token naming the pseudo-class or pseudo-element that failed at
1018/// `tokens[i]`: that token itself when it is a name after a colon
1019/// (`:frobnicate`, `::before`, `:contains(`), and otherwise the function
1020/// whose arguments it is in — a functional pseudo-class fails on its
1021/// arguments, and the message names the function.
1022fn pseudo_name(tokens: &[Spanned<'_>], i: usize) -> usize {
1023    // Comments between the colon and the name are skipped by the
1024    // parser, so they are skipped here too.
1025    let after_colon = tokens[..i]
1026        .iter()
1027        .rev()
1028        .find(|t| !matches!(t.token, Token::Comment(_)))
1029        .is_some_and(|t| matches!(t.token, Token::Colon));
1030    if after_colon && matches!(tokens[i].token, Token::Ident(_) | Token::Function(_)) {
1031        return i;
1032    }
1033    // The blocks open before `tokens[i]`, innermost last, so a closing
1034    // `tokens[i]` counts as inside the block it closes.
1035    let mut open = Vec::new();
1036    for (j, t) in tokens[..i].iter().enumerate() {
1037        match t.token {
1038            Token::Function(_)
1039            | Token::ParenthesisBlock
1040            | Token::SquareBracketBlock
1041            | Token::CurlyBracketBlock => open.push(j),
1042            Token::CloseParenthesis | Token::CloseSquareBracket | Token::CloseCurlyBracket => {
1043                open.pop();
1044            }
1045            _ => {}
1046        }
1047    }
1048    // The argument that failed can sit in a block of its own
1049    // (`:lang([`), so the name is the innermost *function* around it.
1050    open.into_iter()
1051        .rev()
1052        .find(|&j| matches!(tokens[j].token, Token::Function(_)))
1053        .unwrap_or(i)
1054}
1055
1056/// Every token of `css`, whitespace and comments included, with the
1057/// byte span it was written at.
1058///
1059/// `cssparser`'s `Parser` skips the body of a block whose opening token
1060/// it just handed back, rather than descending into it, so the walk
1061/// restarts just past every opener: most of a selector's tokens sit
1062/// inside `[...]` or a functional argument, and a caret has to be able
1063/// to point at those.
1064fn tokens(css: &str) -> Vec<Spanned<'_>> {
1065    let mut tokens = Vec::new();
1066    let mut base = 0;
1067    loop {
1068        let mut parser = CssParser::new(&css[base..]);
1069        let restart = loop {
1070            let start = base + parser.position().byte_index();
1071            // The token is cloned out because reading the position
1072            // again needs the parser back.
1073            let Ok(token) = parser.next_including_whitespace_and_comments().cloned() else {
1074                return tokens;
1075            };
1076            let end = base + parser.position().byte_index();
1077            let opens_block = matches!(
1078                token,
1079                Token::Function(_)
1080                    | Token::ParenthesisBlock
1081                    | Token::SquareBracketBlock
1082                    | Token::CurlyBracketBlock
1083            );
1084            tokens.push(Spanned { start, end, token });
1085            if opens_block {
1086                break end;
1087            }
1088        };
1089        base = restart;
1090    }
1091}
1092
1093/// The token a group blamed for being empty in fact stopped at, when
1094/// there was one: `#1abc` names an ID that CSS cannot spell unescaped,
1095/// not an absent selector, and the same hash one compound later
1096/// (`p#1abc`) is already reported that way.
1097///
1098/// `None` when the group really is empty — end of input, or the comma
1099/// that closes it — leaving [`ParseErrorKind::EmptySelector`] to mean
1100/// only what it says.
1101fn blocking_token(css: &str, offset: usize) -> Option<ParseErrorKind> {
1102    let mut parser = CssParser::new(css.get(offset..)?);
1103    match parser.next() {
1104        Ok(Token::Comma) | Err(_) => None,
1105        Ok(token) => Some(ParseErrorKind::unexpected_token(token)),
1106    }
1107}
1108
1109fn is_empty_selector(e: &ParseFailure) -> bool {
1110    matches!(
1111        e.kind,
1112        cssparser::ParseErrorKind::Custom(SelectorParseErrorKind::EmptySelector)
1113    )
1114}
1115
1116/// Whether a forgiving parse recovered from nothing but empty `:is()` /
1117/// `:where()` argument lists.
1118fn dropped_nothing(list: &SelectorList<CssToXpathImpl>) -> bool {
1119    list.slice()
1120        .iter()
1121        .all(|selector| selector.visit(&mut DroppedArgument))
1122}
1123
1124/// Finds an argument the forgiving parse dropped. Every `visit_*` method
1125/// returns `false` to stop the walk the moment one turns up, so a
1126/// completed walk means there was none.
1127struct DroppedArgument;
1128
1129impl SelectorVisitor for DroppedArgument {
1130    type Impl = CssToXpathImpl;
1131
1132    fn visit_simple_selector(&mut self, component: &Component<CssToXpathImpl>) -> bool {
1133        // The empty argument lists are skipped below, so any invalid
1134        // component reaching here stands for a dropped argument.
1135        !matches!(component, Component::Invalid(_))
1136    }
1137
1138    fn visit_selector_list(
1139        &mut self,
1140        _list_kind: SelectorListKind,
1141        list: &[Selector<CssToXpathImpl>],
1142    ) -> bool {
1143        if is_empty_forgiving_list(list) {
1144            return true;
1145        }
1146        list.iter().all(|nested| nested.visit(self))
1147    }
1148
1149    fn visit_relative_selector_list(&mut self, list: &[RelativeSelector<CssToXpathImpl>]) -> bool {
1150        // `:has()` is never parsed forgivingly, but its arguments can
1151        // nest `:is()`, and the default implementation does not descend.
1152        list.iter().all(|relative| relative.selector.visit(self))
1153    }
1154}
1155
1156/// Whether `list` is what an empty `:is()` / `:where()` argument list
1157/// parses to. Forgiving recovery replaces an argument it could not parse
1158/// with a single [`Component::Invalid`] holding the source text, so an
1159/// empty list is one such argument whose text holds no tokens: `:is()`,
1160/// `:is( )`, `:is(/**/)`. A list of two — `:is(a,)` — is a dropped
1161/// argument, not an empty list.
1162pub(crate) fn is_empty_forgiving_list(list: &[Selector<CssToXpathImpl>]) -> bool {
1163    let [selector] = list else {
1164        return false;
1165    };
1166    let mut components = selector.iter_raw_match_order();
1167    let Some(Component::Invalid(source)) = components.next() else {
1168        return false;
1169    };
1170    if components.next().is_some() {
1171        return false;
1172    }
1173    // Servo keeps the source text it could not parse, so whether the
1174    // list was empty is decided on that text: nothing but whitespace and
1175    // comments.
1176    CssParser::new(source.as_str()).is_exhausted()
1177}
1178
1179#[cfg(test)]
1180mod tests {
1181    use super::*;
1182
1183    fn css(pc: &PseudoClass) -> String {
1184        let mut s = String::new();
1185        pc.to_css(&mut s).unwrap();
1186        s
1187    }
1188
1189    #[test]
1190    fn pseudo_class_to_css_names() {
1191        assert_eq!(css(&PseudoClass::AnyLink), ":any-link");
1192        assert_eq!(css(&PseudoClass::Link), ":link");
1193        assert_eq!(css(&PseudoClass::Visited), ":visited");
1194        assert_eq!(css(&PseudoClass::Hover), ":hover");
1195        assert_eq!(css(&PseudoClass::Active), ":active");
1196        assert_eq!(css(&PseudoClass::Focus), ":focus");
1197        assert_eq!(css(&PseudoClass::FocusWithin), ":focus-within");
1198        assert_eq!(css(&PseudoClass::FocusVisible), ":focus-visible");
1199        assert_eq!(css(&PseudoClass::Target), ":target");
1200        assert_eq!(css(&PseudoClass::TargetWithin), ":target-within");
1201        assert_eq!(css(&PseudoClass::LocalLink), ":local-link");
1202        assert_eq!(css(&PseudoClass::Enabled), ":enabled");
1203        assert_eq!(css(&PseudoClass::Disabled), ":disabled");
1204        assert_eq!(css(&PseudoClass::Checked), ":checked");
1205        assert_eq!(css(&PseudoClass::Required), ":required");
1206        assert_eq!(css(&PseudoClass::Optional), ":optional");
1207    }
1208
1209    #[test]
1210    fn pseudo_class_to_css_lang() {
1211        assert_eq!(css(&PseudoClass::Lang(vec!["en".into()])), ":lang(en)");
1212        assert_eq!(
1213            css(&PseudoClass::Lang(vec!["en".into(), "fr".into()])),
1214            ":lang(en, fr)"
1215        );
1216        // A wildcard is not part of an identifier, so a range carrying
1217        // one is written as the tokens it was parsed from.
1218        assert_eq!(css(&PseudoClass::Lang(vec!["de-*".into()])), ":lang(de-*)");
1219        assert_eq!(css(&PseudoClass::Lang(vec!["*".into()])), ":lang(*)");
1220        assert_eq!(
1221            css(&PseudoClass::Lang(vec!["de-*".into(), "*".into()])),
1222            ":lang(de-*, *)"
1223        );
1224        // Values are run through `serialize_identifier`, not written raw:
1225        // a leading digit needs escaping to remain a valid CSS identifier.
1226        assert_eq!(css(&PseudoClass::Lang(vec!["1x".into()])), ":lang(\\31 x)");
1227        // The empty range has no identifier to write — an identifier
1228        // cannot be empty — so it is written as the string it was
1229        // parsed from, and stays a range on the way back in.
1230        assert_eq!(css(&PseudoClass::Lang(vec![String::new()])), ":lang(\"\")");
1231        assert_eq!(
1232            css(&PseudoClass::Lang(vec![String::new(), "en".into()])),
1233            ":lang(\"\", en)"
1234        );
1235    }
1236
1237    /// The `:lang()` argument grammar, at the level the parser decides
1238    /// it: whether a token run assembles into ranges at all.
1239    #[test]
1240    fn lang_range_grammar() {
1241        fn ranges(css: &str) -> Option<Vec<String>> {
1242            let mut parser = CssParser::new(css);
1243            parser.expect_function_matching("lang").ok()?;
1244            parser
1245                .parse_nested_block(|p| {
1246                    Ok::<_, cssparser::ParseError<()>>(parse_lang_ranges(p).ok())
1247                })
1248                .ok()?
1249        }
1250        let one = |css: &str, range: &str| {
1251            assert_eq!(
1252                ranges(css).as_deref(),
1253                Some(&[range.to_owned()][..]),
1254                "{css}"
1255            );
1256        };
1257        one("lang(en)", "en");
1258        one("lang( en )", "en");
1259        one("lang(\"en\")", "en");
1260        one("lang(en-*)", "en-*");
1261        one("lang(*)", "*");
1262        one("lang(*-CH)", "*-CH");
1263        one("lang(\"en nz\")", "en nz");
1264        // The empty string is a range like any other here: what a range
1265        // *means* — the empty one included — is settled by the
1266        // translators, which is also where a malformed one is rejected.
1267        one("lang(\"\")", "");
1268        one("lang(en-)", "en-");
1269        one("lang(--x)", "--x");
1270        one("lang(en--)", "en--");
1271        one("lang(en*)", "en*");
1272        one("lang(*en)", "*en");
1273        assert_eq!(
1274            ranges("lang( en , fr )"),
1275            Some(vec!["en".to_owned(), "fr".to_owned()])
1276        );
1277        for css in [
1278            "lang()",
1279            "lang(en fr)", // whitespace is not a separator
1280            "lang(en *)",  // ... and does not build `en-*` either
1281            "lang(,)",
1282            "lang(,en)",
1283            "lang(en,)",
1284            "lang(en,,fr)",
1285            "lang(5)",
1286            "lang(-)",
1287            "lang(en/**/fr)", // a comment separates tokens as whitespace does
1288        ] {
1289            assert_eq!(ranges(css), None, "{css}");
1290        }
1291    }
1292
1293    #[test]
1294    fn pseudo_class_to_css_dir() {
1295        assert_eq!(css(&PseudoClass::Dir("ltr".into())), ":dir(ltr)");
1296    }
1297
1298    #[test]
1299    fn pseudo_class_is_active_or_hover() {
1300        assert!(PseudoClass::Active.is_active_or_hover());
1301        assert!(PseudoClass::Hover.is_active_or_hover());
1302        assert!(!PseudoClass::Focus.is_active_or_hover());
1303        assert!(!PseudoClass::Link.is_active_or_hover());
1304        assert!(!PseudoClass::Target.is_active_or_hover());
1305    }
1306
1307    #[test]
1308    fn pseudo_class_is_user_action_state() {
1309        assert!(PseudoClass::Active.is_user_action_state());
1310        assert!(PseudoClass::Hover.is_user_action_state());
1311        assert!(PseudoClass::Focus.is_user_action_state());
1312        assert!(PseudoClass::FocusWithin.is_user_action_state());
1313        assert!(PseudoClass::FocusVisible.is_user_action_state());
1314        assert!(!PseudoClass::Link.is_user_action_state());
1315        assert!(!PseudoClass::Target.is_user_action_state());
1316        assert!(!PseudoClass::Enabled.is_user_action_state());
1317        assert!(!PseudoClass::Checked.is_user_action_state());
1318    }
1319}
1320
1321/// Pins the early exit in [`parse_lists`]: it skips the forgiving retry
1322/// on the grounds that the retry cannot change the result, so the result
1323/// is checked against the implementation that always made it.
1324#[cfg(test)]
1325mod early_exit_tests {
1326    use super::*;
1327    use proptest::prelude::*;
1328
1329    const CORPUS: &str = include_str!("../../tests/corpus/selectors.txt");
1330
1331    /// `parse_lists` as it was before the early exit: the forgiving
1332    /// retry after every strict failure.
1333    fn reference(
1334        css: &str,
1335        default_namespace: Option<&str>,
1336    ) -> Result<SelectorList<CssToXpathImpl>, Error> {
1337        let default_namespace = CssString::from(default_namespace.unwrap_or(""));
1338        let strict = Attempt {
1339            forgiving: false,
1340            default_namespace: &default_namespace,
1341        };
1342        let forgiving = Attempt {
1343            forgiving: true,
1344            ..strict
1345        };
1346        let strict_error = match strict.parse(css) {
1347            Ok(list) => return Ok(list),
1348            Err(e) => e,
1349        };
1350        match forgiving.parse(css) {
1351            Ok(list) if dropped_nothing(&list) => Ok(list),
1352            Ok(_) => Err(parse_error(css, strict, &strict_error)),
1353            Err(e) if is_empty_selector(&strict_error) => Err(parse_error(css, forgiving, &e)),
1354            Err(_) => Err(parse_error(css, strict, &strict_error)),
1355        }
1356    }
1357
1358    fn assert_agrees(css: &str) -> Result<(), TestCaseError> {
1359        let shown =
1360            |r: Result<SelectorList<CssToXpathImpl>, Error>| r.map(|list| list.to_css_string());
1361        prop_assert_eq!(
1362            shown(parse_lists(css, None)),
1363            shown(reference(css, None)),
1364            "{:?}",
1365            css
1366        );
1367        Ok(())
1368    }
1369
1370    /// Characters that make up selector syntax, weighted towards the
1371    /// ones that open, close and separate the constructs at issue.
1372    const PIECES: &[&str] = &[
1373        ":is(",
1374        ":where(",
1375        ":not(",
1376        ":has(",
1377        ":nth-child(",
1378        "::before",
1379        ":foo",
1380        "(",
1381        ")",
1382        "[",
1383        "]",
1384        ",",
1385        " ",
1386        ">",
1387        "+",
1388        "~",
1389        "|",
1390        "*",
1391        "=",
1392        "\"",
1393        "'",
1394        "\\",
1395        "/**/",
1396        "/*",
1397        "#",
1398        ".",
1399        "a",
1400        "b",
1401        "1",
1402        "-",
1403        "&",
1404        "{",
1405        "}",
1406        "\u{e9}",
1407    ];
1408
1409    fn piece() -> impl Strategy<Value = String> {
1410        proptest::sample::select(PIECES).prop_map(str::to_owned)
1411    }
1412
1413    fn corpus_line() -> impl Strategy<Value = String> {
1414        let lines: Vec<&'static str> = CORPUS.lines().collect();
1415        proptest::sample::select(lines).prop_map(str::to_owned)
1416    }
1417
1418    /// A corpus selector with a few pieces spliced in at arbitrary
1419    /// character positions, and a few characters removed.
1420    fn mutated() -> impl Strategy<Value = String> {
1421        (
1422            corpus_line(),
1423            proptest::collection::vec((any::<proptest::sample::Index>(), piece()), 0..4),
1424            proptest::collection::vec(any::<proptest::sample::Index>(), 0..3),
1425        )
1426            .prop_map(|(line, inserts, removals)| {
1427                let mut chars: Vec<char> = line.chars().collect();
1428                for index in removals {
1429                    if !chars.is_empty() {
1430                        chars.remove(index.index(chars.len()));
1431                    }
1432                }
1433                for (index, piece) in inserts {
1434                    let at = index.index(chars.len() + 1);
1435                    chars.splice(at..at, piece.chars());
1436                }
1437                chars.into_iter().collect()
1438            })
1439    }
1440
1441    #[test]
1442    fn corpus_agrees() {
1443        for line in CORPUS.lines() {
1444            assert_agrees(line).unwrap();
1445        }
1446    }
1447
1448    proptest! {
1449        #![proptest_config(ProptestConfig { cases: 4096, ..ProptestConfig::default() })]
1450
1451        #[test]
1452        fn mutated_corpus_agrees(css in mutated()) {
1453            assert_agrees(&css)?;
1454        }
1455
1456        #[test]
1457        fn assembled_pieces_agree(pieces in proptest::collection::vec(piece(), 0..12)) {
1458            assert_agrees(&pieces.concat())?;
1459        }
1460    }
1461}