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