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