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