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