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