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 `None` for the sentinel
205    /// (see [`CssToXpathParser::default_namespace`]).
206    default_namespace: Option<&'a str>,
207}
208
209impl<'i> selectors::parser::Parser<'i> for CssToXpathParser<'_> {
210    type Impl = CssToXpathImpl;
211    type Error = SelectorParseErrorKind<'i>;
212
213    /// Strict unless [`parse`] is retrying: a selector that fails to
214    /// parse must surface an error, never be silently dropped the way
215    /// forgiving `:is()`/`:where()` parsing would.
216    fn allow_forgiving_selectors(&self) -> bool {
217        self.forgiving
218    }
219
220    /// Enable `:is()` and `:where()`.
221    fn parse_is_and_where(&self) -> bool {
222        true
223    }
224
225    /// `:matches()` is the legacy alias for `:is()`.
226    fn is_is_alias(&self, name: &str) -> bool {
227        name.eq_ignore_ascii_case("matches")
228    }
229
230    /// Enable `:has()`. The translator restricts the arguments to
231    /// compound selectors (with an optional leading combinator).
232    fn parse_has(&self) -> bool {
233        true
234    }
235
236    /// `:nth-child(an+b of S)` / `:nth-last-child(an+b of S)`,
237    /// CSS Selectors Level 4.
238    fn parse_nth_child_of(&self) -> bool {
239        true
240    }
241
242    /// The supported non-tree-structural pseudo-classes: the "never
243    /// matches" set plus the HTML-translator overrides. Anything else
244    /// errors (see the policy note on `PseudoClass`).
245    fn parse_non_ts_pseudo_class(
246        &self,
247        location: SourceLocation,
248        name: cssparser::CowRcStr<'i>,
249    ) -> Result<PseudoClass, cssparser::ParseError<'i, Self::Error>> {
250        let pc = match_ignore_ascii_case! { &name,
251            "any-link" => PseudoClass::AnyLink,
252            "link" => PseudoClass::Link,
253            "visited" => PseudoClass::Visited,
254            "hover" => PseudoClass::Hover,
255            "active" => PseudoClass::Active,
256            "focus" => PseudoClass::Focus,
257            "focus-within" => PseudoClass::FocusWithin,
258            "focus-visible" => PseudoClass::FocusVisible,
259            "target" => PseudoClass::Target,
260            "target-within" => PseudoClass::TargetWithin,
261            "local-link" => PseudoClass::LocalLink,
262            "enabled" => PseudoClass::Enabled,
263            "disabled" => PseudoClass::Disabled,
264            "checked" => PseudoClass::Checked,
265            "required" => PseudoClass::Required,
266            "optional" => PseudoClass::Optional,
267            "read-only" => PseudoClass::ReadOnly,
268            "read-write" => PseudoClass::ReadWrite,
269            "default" => PseudoClass::Default,
270            "placeholder-shown" => PseudoClass::PlaceholderShown,
271            _ => {
272                return Err(location.new_custom_error(
273                    SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
274                ));
275            },
276        };
277        Ok(pc)
278    }
279
280    /// `:lang()` argument grammar: a comma-separated list of at least
281    /// one language range, each an ident or string optionally glued to
282    /// `*` wildcards. Whitespace is allowed only around the commas: it
283    /// is a range *terminator*, never a separator, so `:lang(en fr)` is
284    /// an error rather than two ranges, and `en *` is not the range
285    /// `en-*`. A range is assembled here, while the tokens' adjacency is
286    /// still known — the tokenizer splits `en-*` into an ident and a
287    /// delimiter. NUMBER/`+`/`-` tokens are rejected. `:dir()` is
288    /// stricter, matching its selectors-4 grammar: exactly one
289    /// identifier.
290    ///
291    /// What the assembled text *says* is not judged here: a range whose
292    /// subtags are malformed (`en-`, `*en`) is a well-formed argument
293    /// that no language tag can match, and it is the translators that
294    /// reject it — by then the range is in hand and the message can name
295    /// it (see `check_lang_range`).
296    ///
297    /// The non-standard text-content pseudo `:contains()` is deliberately
298    /// unsupported and falls through to the rejection arm, as does any
299    /// unknown functional pseudo.
300    fn parse_non_ts_functional_pseudo_class<'t>(
301        &self,
302        name: cssparser::CowRcStr<'i>,
303        parser: &mut CssParser<'i, 't>,
304        _after_part: bool,
305    ) -> Result<PseudoClass, cssparser::ParseError<'i, Self::Error>> {
306        if name.eq_ignore_ascii_case("dir") {
307            let value = match parser.next() {
308                Ok(Token::Ident(v)) => v.as_ref().to_owned(),
309                _ => {
310                    return Err(parser.new_custom_error(
311                        SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
312                    ));
313                }
314            };
315            if parser.next().is_ok() {
316                return Err(parser.new_custom_error(
317                    SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
318                ));
319            }
320            return Ok(PseudoClass::Dir(value));
321        }
322        if !name.eq_ignore_ascii_case("lang") {
323            return Err(parser.new_custom_error(
324                SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
325            ));
326        }
327
328        match parse_lang_ranges(parser) {
329            Some(ranges) => Ok(PseudoClass::Lang(ranges)),
330            None => Err(parser.new_custom_error(
331                SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name),
332            )),
333        }
334    }
335
336    /// Identity mapping: `svg|g` translates to `svg:g` — a prefix-only
337    /// namespace model with no URL maps.
338    fn namespace_for_prefix(&self, prefix: &CssString) -> Option<CssString> {
339        Some(prefix.clone())
340    }
341
342    /// The caller's default namespace prefix, or a sentinel standing in
343    /// for "none set".
344    ///
345    /// A default namespace is always reported, because without one Servo
346    /// drops the namespace component from both `e` and `*|e` (they match
347    /// identically), and the two must translate differently (`e` vs a
348    /// `local-name()` test). So with none set, plain `e` carries
349    /// `DefaultNamespace("")` — mapped to "no constraint" — while `*|e`
350    /// keeps `ExplicitAnyNamespace`. The empty string can never collide
351    /// with a real prefix (prefixes are non-empty idents, and
352    /// `namespace_for_prefix` is the identity), which is also what makes
353    /// an empty configured prefix mean "no default namespace".
354    ///
355    /// With a prefix set, Servo applies CSS Namespaces 3 for us: the
356    /// prefix reaches `DefaultNamespace` for type selectors and for the
357    /// implicit universal of a type-less compound, but not for the
358    /// featureless compounds of an `:is()` / `:where()` / `:not()`
359    /// argument, and a written `h|e` naming the same prefix collapses
360    /// onto the same component.
361    fn default_namespace(&self) -> Option<CssString> {
362        Some(CssString::from(self.default_namespace.unwrap_or("")))
363    }
364}
365
366/// The body of the `:lang()` argument grammar: the comma-separated
367/// ranges, or `None` if the arguments do not spell out at least one
368/// range. Assembling happens here rather than at translation time
369/// because only the token stream records whether two pieces were
370/// adjacent, and adjacency is the whole difference between the range
371/// `en-*` and the pair `en-`, `*`.
372///
373/// A range is any assembled text, the empty string included: `:lang("")`
374/// is the Level 4 "language not known" range, and the shapes that cannot
375/// match anything are the translators' to reject.
376fn parse_lang_ranges<'i>(parser: &mut CssParser<'i, '_>) -> Option<Vec<String>> {
377    let mut ranges: Vec<String> = Vec::new();
378    let mut current = String::new();
379    // Whether `current` has a piece yet, and whether the next piece
380    // would be adjacent to it. The two are distinct because an empty
381    // string is a piece: `:lang("" *)` has started a range even though
382    // `current` is still empty.
383    let mut started = false;
384    let mut adjacent = true;
385    loop {
386        // Whitespace and comments both terminate a range, so neither may
387        // be skipped over here.
388        let token = match parser.next_including_whitespace_and_comments() {
389            Ok(t) => t.clone(),
390            Err(_) => break, // end of the function's arguments
391        };
392        let piece = match token {
393            Token::WhiteSpace(_) | Token::Comment(_) => {
394                adjacent = false;
395                continue;
396            }
397            Token::Comma => {
398                if !started {
399                    return None; // an empty range slot: `,en`, `en,,fr`
400                }
401                ranges.push(std::mem::take(&mut current));
402                (started, adjacent) = (false, true);
403                continue;
404            }
405            Token::Ident(ref v) | Token::QuotedString(ref v) => v.as_ref().to_owned(),
406            Token::Delim('*') => "*".to_owned(),
407            _ => return None,
408        };
409        if started && !adjacent {
410            return None; // two ranges with no comma between them
411        }
412        current.push_str(&piece);
413        started = true;
414        // Adjacency is a fact about the gap between two tokens, so it is
415        // restored the moment a piece is taken: only whitespace or a
416        // comment *after* this one can separate it from the next. Leaving
417        // it false here would spread a run of whitespace over the rest of
418        // the range and reject a multi-token range after a comma, so that
419        // `:lang(*-CH, en)` parsed but `:lang(en, *-CH)` did not.
420        adjacent = true;
421    }
422    if !started {
423        return None; // no ranges at all, or a trailing comma
424    }
425    ranges.push(current);
426    Some(ranges)
427}
428
429/// The maximum functional-pseudo-class nesting depth accepted, measured
430/// as parenthesis nesting in the source selector. Both Servo's parser and
431/// this crate's translator recurse once per nesting level (as does
432/// dropping the resulting selector tree), so an unbounded depth would
433/// overflow the stack — a hard abort, not a panic, so the caller cannot
434/// catch it.
435///
436/// The value is set from the profile that costs the most stack per level,
437/// against the smallest stack the crate can be run on. An unoptimized
438/// build spends about 16 KB a level (against about 4 KB optimized), so 32
439/// levels need roughly 600 KB: a comfortable fit in the 1 MiB a library
440/// does not get to choose — the default reserve of a Windows main thread,
441/// rustc's `wasm32-unknown-unknown` stack, and whatever a thread pool
442/// hands its workers. Sizing against Rust's more generous 2 MB default
443/// for a spawned thread instead would let a debug build abort on those
444/// targets at a depth this limit promises to accept, which is the limit
445/// failing at the one job it has.
446///
447/// 32 is still far beyond any hand-written selector, and the depth counted
448/// is every parenthesis pair, including ones that do not recurse at all
449/// (`:nth-child(2n+1)`, `:lang(en)`) and ones that spend two per level
450/// (`:nth-child(2 of :is(…))`), so real selectors sit further under it
451/// than the number suggests.
452pub const MAX_NESTING_DEPTH: usize = 32;
453
454/// The facts about a selector that must be known before Servo is entered,
455/// gathered in one linear walk that skips strings, escapes, and comments.
456struct Scan {
457    /// The byte offset of the first `|` of the Level 4 column combinator
458    /// `||`, if the selector uses one. Outside strings, escapes, and
459    /// comments a doubled pipe can only be that combinator (a single `|`
460    /// occurs in namespace prefixes and `|=`, never doubled). Servo has
461    /// no column-combinator support and its parse error misreads the
462    /// second pipe as namespace syntax
463    /// (`ExplicitNamespaceUnexpectedToken`), so the construct is caught
464    /// before parsing and named properly. Column selection has no XPath
465    /// 1.0 translation anyway: column membership depends on
466    /// `colspan`/`rowspan` layout arithmetic.
467    column_combinator: Option<usize>,
468    /// The byte offset of the first `(` that opened a level deeper than
469    /// [`MAX_NESTING_DEPTH`], if any — the point at which the selector
470    /// went too deep for the parser and translator to recurse through
471    /// safely, and so the point to put a caret under. The *first* such
472    /// parenthesis, not the innermost, so the position does not move
473    /// with however much deeper the rest of the selector goes.
474    too_deep: Option<usize>,
475    /// The byte offset of the first `&` nesting selector, if the
476    /// selector uses one. Outside strings, escapes, and comments an `&`
477    /// can only be that selector: it appears in no other selector
478    /// production. This crate parses with nesting disabled — a `&` has
479    /// no meaning without the enclosing rule a selector-to-XPath
480    /// function never sees — so Servo does not recognise it as the
481    /// start of a compound and fails on whatever comes next instead,
482    /// reporting `&` as an empty selector or a dangling combinator.
483    /// Catching it here names the construct the caller actually wrote.
484    nesting_selector: Option<usize>,
485    /// The byte offset of the first `:scope` this crate cannot
486    /// translate, and which of the two ways it is out of place. Both
487    /// are lexical facts — `:scope` is unsupported inside any
488    /// functional pseudo-class argument, and at the top level anywhere
489    /// but the leftmost compound of its group — so the walk can decide
490    /// them from the parenthesis depth and whether a combinator has
491    /// been passed, and hand the translator's own check a position it
492    /// has no way to recover. See [`ScopeSite`].
493    misplaced_scope: Option<(usize, ScopeSite)>,
494    /// The byte offset of the first `:host(`, if the selector uses one.
495    /// Shadow-DOM host selection has no XPath 1.0 translation at all,
496    /// so — like `||` — the mere presence of the construct is the
497    /// error, wherever it sits. Only the functional form is looked for:
498    /// a bare `:host` is not a pseudo-class this crate's parser accepts,
499    /// so it fails to parse and never reaches translation.
500    host: Option<usize>,
501}
502
503/// Which of the two positions a [`Scan::misplaced_scope`] was found in,
504/// since they are reported as different constructs.
505#[derive(Clone, Copy, Eq, PartialEq)]
506enum ScopeSite {
507    /// Inside a functional pseudo-class argument, where an XPath 1.0
508    /// predicate cannot name the context node at all.
509    Functional,
510    /// At the top level, but not in the leftmost compound of its group
511    /// — the one place `:scope` translates, by anchoring the whole
512    /// expression on the `self::` axis instead of the caller's prefix.
513    NotLeftmost,
514}
515
516impl ScopeSite {
517    /// The construct phrase for this site, as the object of "uses …".
518    /// Worded exactly as the translator's own check words it, so the
519    /// two cannot diverge: only the position is new.
520    fn construct(self) -> &'static str {
521        match self {
522            ScopeSite::Functional => "the `:scope` pseudo-class inside a functional pseudo-class",
523            ScopeSite::NotLeftmost => "the `:scope` pseudo-class outside the leftmost compound",
524        }
525    }
526}
527
528/// How far into its selector-list group the walk has got, which is all
529/// that is needed to place a top-level `:scope`: it is supported in the
530/// leftmost compound of a group and nowhere else, so the question is
531/// only whether a combinator has been passed since the last top-level
532/// comma.
533#[derive(Default)]
534struct GroupPosition {
535    /// Whether anything that is part of a compound has been seen in
536    /// this group yet, so that leading whitespace is not a combinator.
537    content_seen: bool,
538    /// Whether whitespace has been seen after some content: a
539    /// descendant combinator if any content follows it, and nothing at
540    /// all if the group ends there.
541    space_pending: bool,
542    /// Whether a combinator — descendant, `>`, `+` or `~` — has been
543    /// passed, i.e. whether the leftmost compound is behind the walk.
544    combinator_seen: bool,
545}
546
547/// The string handling here diverges from the CSS tokenizer on one point:
548/// a newline inside a string ends it there, as a bad-string token, where
549/// this walk stays "in string" until the closing quote or the end of the
550/// input. So the walk can treat as string content — and skip — text the
551/// tokenizer reads as syntax, which for `||` only loses a nicer error
552/// message and for parentheses could undercount the depth.
553///
554/// Neither matters, because reaching that state needs a string that no
555/// newline-free closing quote follows, and cssparser turns the newline
556/// into a bad-string token that fails the parse. The skipped text is
557/// everything after that point, so nothing in it is ever parsed, let
558/// alone recursed into. Every selector that does parse is one the walk
559/// and the tokenizer agree about.
560fn scan(css: &str) -> Scan {
561    let bytes = css.as_bytes();
562    let mut i = 0;
563    let mut quote: Option<u8> = None;
564    let mut depth: usize = 0;
565    let mut brackets: usize = 0;
566    let mut group = GroupPosition::default();
567    let mut scan = Scan {
568        column_combinator: None,
569        too_deep: None,
570        nesting_selector: None,
571        misplaced_scope: None,
572        host: None,
573    };
574    while i < bytes.len() {
575        let b = bytes[i];
576        match quote {
577            Some(q) => {
578                if b == b'\\' {
579                    i += 1; // skip the escaped character
580                } else if b == q {
581                    quote = None;
582                }
583            }
584            None => {
585                // Where the walk is within its selector-list group,
586                // which only the top level has: inside a functional
587                // argument (`depth > 0`) a `:scope` is unsupported
588                // wherever it sits, and inside `[...]` nothing is a
589                // combinator — `[a~=b]`'s tilde least of all.
590                if depth == 0 && brackets == 0 {
591                    match b {
592                        b',' => group = GroupPosition::default(),
593                        b'>' | b'+' | b'~' => {
594                            group.combinator_seen = true;
595                            group.content_seen = true;
596                            group.space_pending = false;
597                        }
598                        b' ' | b'\t' | b'\n' | b'\r' | b'\x0C' => {
599                            group.space_pending |= group.content_seen;
600                        }
601                        // A comment is neither content nor whitespace:
602                        // it is removed before the selector grammar
603                        // sees it, so `a/**/b` is one compound.
604                        b'/' if bytes.get(i + 1) == Some(&b'*') => {}
605                        _ => {
606                            group.combinator_seen |= group.space_pending;
607                            group.space_pending = false;
608                            group.content_seen = true;
609                        }
610                    }
611                }
612                match b {
613                    b'\\' => i += 1, // skip the escaped character
614                    b'"' | b'\'' => quote = Some(b),
615                    b'/' if bytes.get(i + 1) == Some(&b'*') => {
616                        // Skip the comment body and its closing "*/".
617                        i += 2;
618                        while i + 1 < bytes.len() && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
619                            i += 1;
620                        }
621                        i += 1;
622                    }
623                    b'|' if bytes.get(i + 1) == Some(&b'|') => {
624                        scan.column_combinator.get_or_insert(i);
625                    }
626                    b'(' => {
627                        depth += 1;
628                        if depth > MAX_NESTING_DEPTH {
629                            scan.too_deep.get_or_insert(i);
630                        }
631                    }
632                    // Unbalanced closers are Servo's to reject, not this
633                    // walk's: just never go below zero.
634                    b')' => depth = depth.saturating_sub(1),
635                    b'[' => brackets += 1,
636                    b']' => brackets = brackets.saturating_sub(1),
637                    b'&' => {
638                        scan.nesting_selector.get_or_insert(i);
639                    }
640                    // A literal `:scope` / `:host(` here is the
641                    // pseudo-class and nothing else — an escaped colon and
642                    // a quoted or commented-out one are all skipped above,
643                    // and any other ident starting with those letters
644                    // (`:scoped`, `::scope`) is a pseudo-class this crate's
645                    // parser rejects. Both findings are only ever consulted
646                    // after the parse has succeeded, which is what makes
647                    // that last part sound.
648                    b':' if bytes[i..].starts_with(b":scope") && brackets == 0 => {
649                        let site = if depth > 0 {
650                            Some(ScopeSite::Functional)
651                        } else if group.combinator_seen {
652                            Some(ScopeSite::NotLeftmost)
653                        } else {
654                            None // the leftmost compound, where it is supported
655                        };
656                        if let Some(site) = site {
657                            scan.misplaced_scope.get_or_insert((i, site));
658                        }
659                    }
660                    b':' if bytes[i..].starts_with(b":host(") => {
661                        scan.host.get_or_insert(i);
662                    }
663                    _ => {}
664                }
665            }
666        }
667        i += 1;
668    }
669    scan
670}
671
672/// The error one parse attempt produced, before it is turned into an
673/// [`Error`] — the kind is needed to tell an empty selector apart from
674/// everything else.
675type ParseFailure<'i> = cssparser::ParseError<'i, SelectorParseErrorKind<'i>>;
676
677/// Parse a full selector list (comma-separated groups).
678///
679/// Selectors 4 gives `:is()` and `:where()` a *forgiving* argument list,
680/// of which this crate wants exactly one part: an empty list is valid and
681/// matches nothing. Dropping *invalid* arguments is not wanted — a
682/// translation library must not quietly ignore what it was handed — so
683/// the strict parse decides, and forgiving parsing is only a retry whose
684/// result is accepted when the sole thing it recovered from was an empty
685/// argument list.
686pub(crate) fn parse(
687    css: &str,
688    default_namespace: Option<&str>,
689) -> Result<SelectorList<CssToXpathImpl>, Error> {
690    let scan = scan(css);
691    if let Some(offset) = scan.column_combinator {
692        return Err(Error::unsupported_at("the `||` column combinator", offset));
693    }
694    if let Some(offset) = scan.too_deep {
695        return Err(Error::unsupported_at(
696            format!("functional pseudo-classes nested more than {MAX_NESTING_DEPTH} levels deep"),
697            offset,
698        ));
699    }
700    // Reported after the other two, so a selector with more than one
701    // problem keeps the message it had before this check existed.
702    if let Some(offset) = scan.nesting_selector {
703        return Err(Error::unsupported_at("the `&` nesting selector", offset));
704    }
705    let list = parse_lists(css, default_namespace)?;
706    // The remaining findings are constructs Servo parses happily and
707    // the translator then rejects, so they are consulted only once the
708    // parse has succeeded: a selector that is *also* invalid CSS keeps
709    // the parse error it has always reported, and the walk never has to
710    // be right about text that never parsed. What the walk adds is the
711    // position, which the translator — handed components with no source
712    // offsets — cannot recover for itself.
713    if let Some((offset, site)) = scan.misplaced_scope {
714        return Err(Error::unsupported_at(site.construct(), offset));
715    }
716    if let Some(offset) = scan.host {
717        return Err(Error::unsupported_at("the `:host` pseudo-class", offset));
718    }
719    Ok(list)
720}
721
722/// The strict parse, and the forgiving retry that only an empty `:is()`
723/// / `:where()` argument list earns.
724fn parse_lists(
725    css: &str,
726    default_namespace: Option<&str>,
727) -> Result<SelectorList<CssToXpathImpl>, Error> {
728    let strict = match parse_list(css, false, default_namespace) {
729        Ok(list) => return Ok(list),
730        Err(e) => e,
731    };
732    match parse_list(css, true, default_namespace) {
733        Ok(list) if dropped_nothing(&list) => Ok(list),
734        // The forgiving parse recovered from a genuinely invalid
735        // argument: the strict error is the one that names it, and
736        // points at it.
737        Ok(_) => Err(parse_error(css, &strict)),
738        // Both parses failed. An empty argument list is no longer an
739        // error, so a strict `EmptySelector` may well be blaming one,
740        // while the forgiving parse — which accepts those — stopped at
741        // whatever is actually wrong.
742        Err(e) if is_empty_selector(&strict) => Err(parse_error(css, &e)),
743        Err(_) => Err(parse_error(css, &strict)),
744    }
745}
746
747/// One parse of the whole selector list.
748fn parse_list<'i>(
749    css: &'i str,
750    forgiving: bool,
751    default_namespace: Option<&str>,
752) -> Result<SelectorList<CssToXpathImpl>, ParseFailure<'i>> {
753    let mut input = ParserInput::new(css);
754    let mut parser = CssParser::new(&mut input);
755    SelectorList::parse(
756        &CssToXpathParser {
757            forgiving,
758            default_namespace,
759        },
760        &mut parser,
761        ParseRelative::No,
762    )
763}
764
765fn parse_error(css: &str, e: &ParseFailure<'_>) -> Error {
766    let reported = byte_offset(css, e.location);
767    let mut kind = ParseErrorKind::from_kind(&e.kind);
768    // `EmptySelector` is what `selectors` reports whenever a compound
769    // ends up with no components, which covers both "there was nothing
770    // here" and "none of what was here parsed". Only the first is what
771    // the name says, so the second is re-reported by its cause.
772    if matches!(kind, ParseErrorKind::EmptySelector)
773        && let Some(blocked) = blocking_token(css, reported)
774    {
775        kind = blocked;
776    }
777    let offset = named_token_offset(css, reported, &kind).unwrap_or(reported);
778    Error::Parse { kind, offset }
779}
780
781/// Where the token a message echoes starts, when it is one of the two
782/// next to `reported`.
783///
784/// The position `cssparser` and `selectors` report is the parser's
785/// stopping point, which is beside the offending token rather than on
786/// it, and which of the two sides depends on where the parser that
787/// failed took its location from. Taking it before reading a token
788/// leaves the position on the whitespace in front of that token
789/// (`[a=b c]`, reported on the space); taking it after leaves it past
790/// the token (`:nth-child(foo)`, reported on the `)`), and past a
791/// function token means on its first argument (`a::part(b)`, reported
792/// on the `b`). Both are one token from what the message names, so both
793/// are offered to the kind, and the caret moves to whichever it claims.
794///
795/// The token that follows is tried first, so a position already sitting
796/// on the offending token stays where it is. `None` when neither
797/// neighbour is the one named — a kind that echoes no token at all, or
798/// a name further off than one token, as `::before`'s is — leaving the
799/// reported position, which is still the best the parser knows.
800fn named_token_offset(css: &str, reported: usize, kind: &ParseErrorKind) -> Option<usize> {
801    let mut following = None;
802    let mut preceding = None;
803    walk_tokens(css, |start, end, token| {
804        if end == reported {
805            preceding = Some((start, token.clone()));
806        }
807        // Whitespace and comments are skipped on the way forward: they
808        // are what the position lands on when it was taken before the
809        // token, and never what a message names.
810        if start >= reported && !matches!(token, Token::WhiteSpace(_) | Token::Comment(_)) {
811            following = Some((start, token.clone()));
812            return false;
813        }
814        true
815    });
816    [following, preceding]
817        .into_iter()
818        .flatten()
819        .find(|(_, token)| kind.names_token(token))
820        .map(|(start, _)| start)
821}
822
823/// Call `visit` with the byte span of every token of `css`, whitespace
824/// and comments included, until it returns `false`.
825///
826/// `cssparser`'s `Parser` skips the body of a block whose opening token
827/// it just handed back, rather than descending into it, so the walk
828/// restarts just past every opener: most of a selector's tokens sit
829/// inside `[...]` or a functional argument, and a caret has to be able
830/// to point at those.
831fn walk_tokens<'i>(css: &'i str, mut visit: impl FnMut(usize, usize, &Token<'i>) -> bool) {
832    let mut base = 0;
833    loop {
834        let mut input = ParserInput::new(&css[base..]);
835        let mut parser = CssParser::new(&mut input);
836        let restart = loop {
837            let start = base + parser.position().byte_index();
838            // The token is cloned out because reading the position
839            // again needs the parser back.
840            let Ok(token) = parser.next_including_whitespace_and_comments().cloned() else {
841                return;
842            };
843            let end = base + parser.position().byte_index();
844            if !visit(start, end, &token) {
845                return;
846            }
847            if matches!(
848                token,
849                Token::Function(_)
850                    | Token::ParenthesisBlock
851                    | Token::SquareBracketBlock
852                    | Token::CurlyBracketBlock
853            ) {
854                break end;
855            }
856        };
857        base = restart;
858    }
859}
860
861/// The token a group blamed for being empty in fact stopped at, when
862/// there was one: `#1abc` names an ID that CSS cannot spell unescaped,
863/// not an absent selector, and the same hash one compound later
864/// (`p#1abc`) is already reported that way.
865///
866/// `None` when the group really is empty — end of input, or the comma
867/// that closes it — leaving [`ParseErrorKind::EmptySelector`] to mean
868/// only what it says.
869fn blocking_token(css: &str, offset: usize) -> Option<ParseErrorKind> {
870    let mut input = ParserInput::new(css.get(offset..)?);
871    let mut parser = CssParser::new(&mut input);
872    match parser.next() {
873        Ok(Token::Comma) | Err(_) => None,
874        Ok(token) => Some(ParseErrorKind::unexpected_token(token)),
875    }
876}
877
878fn is_empty_selector(e: &ParseFailure<'_>) -> bool {
879    matches!(
880        e.kind,
881        cssparser::ParseErrorKind::Custom(SelectorParseErrorKind::EmptySelector)
882    )
883}
884
885/// Whether a forgiving parse recovered from nothing but empty `:is()` /
886/// `:where()` argument lists.
887fn dropped_nothing(list: &SelectorList<CssToXpathImpl>) -> bool {
888    list.slice()
889        .iter()
890        .all(|selector| selector.visit(&mut DroppedArgument))
891}
892
893/// Finds an argument the forgiving parse dropped. Every `visit_*` method
894/// returns `false` to stop the walk the moment one turns up, so a
895/// completed walk means there was none.
896struct DroppedArgument;
897
898impl SelectorVisitor for DroppedArgument {
899    type Impl = CssToXpathImpl;
900
901    fn visit_simple_selector(&mut self, component: &Component<CssToXpathImpl>) -> bool {
902        // The empty argument lists are skipped below, so any invalid
903        // component reaching here stands for a dropped argument.
904        !matches!(component, Component::Invalid(_))
905    }
906
907    fn visit_selector_list(
908        &mut self,
909        _list_kind: SelectorListKind,
910        list: &[Selector<CssToXpathImpl>],
911    ) -> bool {
912        if is_empty_forgiving_list(list) {
913            return true;
914        }
915        list.iter().all(|nested| nested.visit(self))
916    }
917
918    fn visit_relative_selector_list(&mut self, list: &[RelativeSelector<CssToXpathImpl>]) -> bool {
919        // `:has()` is never parsed forgivingly, but its arguments can
920        // nest `:is()`, and the default implementation does not descend.
921        list.iter().all(|relative| relative.selector.visit(self))
922    }
923}
924
925/// Whether `list` is what an empty `:is()` / `:where()` argument list
926/// parses to. Forgiving recovery replaces an argument it could not parse
927/// with a single [`Component::Invalid`] holding the source text, so an
928/// empty list is one such argument whose text holds no tokens: `:is()`,
929/// `:is( )`, `:is(/**/)`. A list of two — `:is(a,)` — is a dropped
930/// argument, not an empty list.
931pub(crate) fn is_empty_forgiving_list(list: &[Selector<CssToXpathImpl>]) -> bool {
932    let [selector] = list else {
933        return false;
934    };
935    let mut components = selector.iter_raw_match_order();
936    let Some(Component::Invalid(source)) = components.next() else {
937        return false;
938    };
939    if components.next().is_some() {
940        return false;
941    }
942    // Servo keeps the source text it could not parse, so whether the
943    // list was empty is decided on that text: nothing but whitespace and
944    // comments.
945    let mut input = ParserInput::new(source.as_str());
946    CssParser::new(&mut input).is_exhausted()
947}
948
949/// The byte offset within `css` that `location` points at.
950///
951/// A `SourceLocation` cannot be used as an index: its line is 0-indexed,
952/// its column is 1-indexed, and — the part that bites — the column
953/// counts UTF-16 code units, so a tab counts as one unit but renders as
954/// several columns, a CJK character counts as one but renders as two,
955/// and a non-BMP character counts as two but is a single character. A
956/// byte offset is what the caret renderer needs to look at the source
957/// text itself.
958fn byte_offset(css: &str, location: SourceLocation) -> usize {
959    let bytes = css.as_bytes();
960    // Walk to the start of the error's line. `\r\n`, `\r`, `\n` and `\f`
961    // are all line breaks, matching cssparser's own line counter.
962    let mut offset = 0;
963    let mut line = 0;
964    while line < location.line && offset < bytes.len() {
965        match bytes[offset] {
966            b'\r' => {
967                offset += 1;
968                if bytes.get(offset) == Some(&b'\n') {
969                    offset += 1;
970                }
971                line += 1;
972            }
973            b'\n' | b'\x0C' => {
974                offset += 1;
975                line += 1;
976            }
977            _ => offset += 1,
978        }
979    }
980    // Then across `column - 1` UTF-16 code units of that line. A column
981    // in the middle of a surrogate pair is not reachable from a token
982    // boundary, but `saturating_sub` keeps one from running away.
983    let mut units = location.column.saturating_sub(1);
984    for c in css[offset..].chars() {
985        if units == 0 {
986            break;
987        }
988        units = units.saturating_sub(c.len_utf16() as u32);
989        offset += c.len_utf8();
990    }
991    offset
992}
993
994#[cfg(test)]
995mod tests {
996    use super::*;
997
998    fn css(pc: &PseudoClass) -> String {
999        let mut s = String::new();
1000        pc.to_css(&mut s).unwrap();
1001        s
1002    }
1003
1004    #[test]
1005    fn pseudo_class_to_css_names() {
1006        assert_eq!(css(&PseudoClass::AnyLink), ":any-link");
1007        assert_eq!(css(&PseudoClass::Link), ":link");
1008        assert_eq!(css(&PseudoClass::Visited), ":visited");
1009        assert_eq!(css(&PseudoClass::Hover), ":hover");
1010        assert_eq!(css(&PseudoClass::Active), ":active");
1011        assert_eq!(css(&PseudoClass::Focus), ":focus");
1012        assert_eq!(css(&PseudoClass::FocusWithin), ":focus-within");
1013        assert_eq!(css(&PseudoClass::FocusVisible), ":focus-visible");
1014        assert_eq!(css(&PseudoClass::Target), ":target");
1015        assert_eq!(css(&PseudoClass::TargetWithin), ":target-within");
1016        assert_eq!(css(&PseudoClass::LocalLink), ":local-link");
1017        assert_eq!(css(&PseudoClass::Enabled), ":enabled");
1018        assert_eq!(css(&PseudoClass::Disabled), ":disabled");
1019        assert_eq!(css(&PseudoClass::Checked), ":checked");
1020        assert_eq!(css(&PseudoClass::Required), ":required");
1021        assert_eq!(css(&PseudoClass::Optional), ":optional");
1022    }
1023
1024    #[test]
1025    fn pseudo_class_to_css_lang() {
1026        assert_eq!(css(&PseudoClass::Lang(vec!["en".into()])), ":lang(en)");
1027        assert_eq!(
1028            css(&PseudoClass::Lang(vec!["en".into(), "fr".into()])),
1029            ":lang(en, fr)"
1030        );
1031        // A wildcard is not part of an identifier, so a range carrying
1032        // one is written as the tokens it was parsed from.
1033        assert_eq!(css(&PseudoClass::Lang(vec!["de-*".into()])), ":lang(de-*)");
1034        assert_eq!(css(&PseudoClass::Lang(vec!["*".into()])), ":lang(*)");
1035        assert_eq!(
1036            css(&PseudoClass::Lang(vec!["de-*".into(), "*".into()])),
1037            ":lang(de-*, *)"
1038        );
1039        // Values are run through `serialize_identifier`, not written raw:
1040        // a leading digit needs escaping to remain a valid CSS identifier.
1041        assert_eq!(css(&PseudoClass::Lang(vec!["1x".into()])), ":lang(\\31 x)");
1042        // The empty range has no identifier to write — an identifier
1043        // cannot be empty — so it is written as the string it was
1044        // parsed from, and stays a range on the way back in.
1045        assert_eq!(css(&PseudoClass::Lang(vec![String::new()])), ":lang(\"\")");
1046        assert_eq!(
1047            css(&PseudoClass::Lang(vec![String::new(), "en".into()])),
1048            ":lang(\"\", en)"
1049        );
1050    }
1051
1052    /// The `:lang()` argument grammar, at the level the parser decides
1053    /// it: whether a token run assembles into ranges at all.
1054    #[test]
1055    fn lang_range_grammar() {
1056        fn ranges(css: &str) -> Option<Vec<String>> {
1057            let mut input = ParserInput::new(css);
1058            let mut parser = CssParser::new(&mut input);
1059            parser.expect_function_matching("lang").ok()?;
1060            parser
1061                .parse_nested_block(|p| {
1062                    Ok::<_, cssparser::ParseError<'_, ()>>(parse_lang_ranges(p))
1063                })
1064                .ok()?
1065        }
1066        let one = |css: &str, range: &str| {
1067            assert_eq!(
1068                ranges(css).as_deref(),
1069                Some(&[range.to_owned()][..]),
1070                "{css}"
1071            );
1072        };
1073        one("lang(en)", "en");
1074        one("lang( en )", "en");
1075        one("lang(\"en\")", "en");
1076        one("lang(en-*)", "en-*");
1077        one("lang(*)", "*");
1078        one("lang(*-CH)", "*-CH");
1079        one("lang(\"en nz\")", "en nz");
1080        // The empty string is a range like any other here: what a range
1081        // *means* — the empty one included — is settled by the
1082        // translators, which is also where a malformed one is rejected.
1083        one("lang(\"\")", "");
1084        one("lang(en-)", "en-");
1085        one("lang(--x)", "--x");
1086        one("lang(en--)", "en--");
1087        one("lang(en*)", "en*");
1088        one("lang(*en)", "*en");
1089        assert_eq!(
1090            ranges("lang( en , fr )"),
1091            Some(vec!["en".to_owned(), "fr".to_owned()])
1092        );
1093        for css in [
1094            "lang()",
1095            "lang(en fr)", // whitespace is not a separator
1096            "lang(en *)",  // ... and does not build `en-*` either
1097            "lang(,)",
1098            "lang(,en)",
1099            "lang(en,)",
1100            "lang(en,,fr)",
1101            "lang(5)",
1102            "lang(-)",
1103            "lang(en/**/fr)", // a comment separates tokens as whitespace does
1104        ] {
1105            assert_eq!(ranges(css), None, "{css}");
1106        }
1107    }
1108
1109    #[test]
1110    fn pseudo_class_to_css_dir() {
1111        assert_eq!(css(&PseudoClass::Dir("ltr".into())), ":dir(ltr)");
1112    }
1113
1114    #[test]
1115    fn pseudo_class_is_active_or_hover() {
1116        assert!(PseudoClass::Active.is_active_or_hover());
1117        assert!(PseudoClass::Hover.is_active_or_hover());
1118        assert!(!PseudoClass::Focus.is_active_or_hover());
1119        assert!(!PseudoClass::Link.is_active_or_hover());
1120        assert!(!PseudoClass::Target.is_active_or_hover());
1121    }
1122
1123    #[test]
1124    fn pseudo_class_is_user_action_state() {
1125        assert!(PseudoClass::Active.is_user_action_state());
1126        assert!(PseudoClass::Hover.is_user_action_state());
1127        assert!(PseudoClass::Focus.is_user_action_state());
1128        assert!(PseudoClass::FocusWithin.is_user_action_state());
1129        assert!(PseudoClass::FocusVisible.is_user_action_state());
1130        assert!(!PseudoClass::Link.is_user_action_state());
1131        assert!(!PseudoClass::Target.is_user_action_state());
1132        assert!(!PseudoClass::Enabled.is_user_action_state());
1133        assert!(!PseudoClass::Checked.is_user_action_state());
1134    }
1135}