css_to_xpath/translate/mod.rs
1//! Translation from Servo's parsed selector representation to XPath.
2
3pub(crate) mod error;
4mod generic;
5mod ncname;
6mod nth;
7mod pseudo;
8mod xpath_expr;
9
10pub use error::{Error, ParseErrorKind};
11pub use nth::{MAX_NTH_OF_BYTES, MAX_NTH_OF_DEPTH};
12
13use std::borrow::Cow;
14use std::fmt;
15
16use selectors::attr::{NamespaceConstraint, ParsedAttrSelectorOperation, ParsedCaseSensitivity};
17use selectors::parser::{Combinator, Component, RelativeSelector, Selector};
18
19use crate::parser::{self, CssToXpathImpl};
20use generic::{attrib_equals, attrib_includes, attrib_operator};
21use ncname::is_ncname;
22use pseudo::LangSource;
23use xpath_expr::{Condition, XPathExpr, is_safe_name};
24
25/// Which translator family the pseudo-class overrides come from: generic
26/// or HTML (both `html` and `xhtml` use the HTML overrides; they differ
27/// in name casing and in the `:lang()` language source).
28#[derive(Clone, Copy, PartialEq, Eq)]
29pub(crate) enum Kind {
30 Generic,
31 Html,
32}
33
34/// The translator flavour: which pseudo-class overrides, name-casing
35/// rules, and `:lang()` language source to apply.
36///
37/// [`Html`](Mode::Html) and [`Xhtml`](Mode::Xhtml) share the HTML
38/// pseudo-class overrides; only `Html` ASCII-lowercases element and
39/// attribute names and folds HTML's legacy case-insensitive attribute
40/// values, and only `Xhtml` reads `xml:lang`.
41///
42/// Pseudo-classes with no static equivalent (`:hover`, `:visited`,
43/// `:focus`, `:dir()`, …) translate to an unmatchable `[0]` in every
44/// flavour, rather than erroring.
45///
46/// The enum is deliberately exhaustive: these three are the document
47/// flavours CSS selector matching distinguishes, and callers benefit
48/// more from exhaustive `match` than the crate would from room to add a
49/// fourth.
50#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
51pub enum Mode {
52 /// Plain CSS/XPath semantics: case-sensitive names, no
53 /// HTML-specific pseudo-classes, and `:lang()` via XPath's own
54 /// `lang()` function.
55 ///
56 /// This is the [`Default`], as the flavour that assumes least about
57 /// the document.
58 #[default]
59 Generic,
60 /// An HTML document, as an HTML parser leaves it: element and
61 /// attribute names are ASCII-lowercased, HTML's legacy
62 /// case-insensitive attribute values (`type`, `rel`, …) compare
63 /// without regard to case, and `:link`, `:checked`,
64 /// `:disabled`/`:enabled`, `:required`/`:optional` and `:lang()`
65 /// take their static HTML meaning over the elements HTML defines
66 /// them for. `:lang()` reads the nearest `@lang` ancestor.
67 Html,
68 /// XHTML: the same HTML pseudo-class semantics as [`Mode::Html`],
69 /// but case is preserved (XHTML is XML, so both names and those
70 /// attribute values are case-sensitive) and `:lang()` reads
71 /// `xml:lang` as well as `lang`, preferring `xml:lang` when both sit
72 /// on the nearest ancestor.
73 Xhtml,
74}
75
76impl Mode {
77 /// The mode's lowercase name: `"generic"`, `"html"` or `"xhtml"`.
78 ///
79 /// The inverse of the [`FromStr`](std::str::FromStr) impl, which
80 /// accepts these three names in any ASCII case.
81 #[must_use]
82 pub fn as_str(self) -> &'static str {
83 match self {
84 Mode::Generic => "generic",
85 Mode::Html => "html",
86 Mode::Xhtml => "xhtml",
87 }
88 }
89}
90
91impl fmt::Display for Mode {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 f.write_str(self.as_str())
94 }
95}
96
97impl std::str::FromStr for Mode {
98 type Err = ParseModeError;
99
100 /// Parse `"generic"`, `"html"` or `"xhtml"`, ignoring ASCII case, so
101 /// that a mode read from a CLI flag or a config file needs no
102 /// hand-written `match` in every caller.
103 ///
104 /// Nothing else is accepted — not an abbreviation, not `"xml"` — and
105 /// the name is compared as-is, so surrounding whitespace is the
106 /// caller's to trim.
107 fn from_str(s: &str) -> Result<Self, Self::Err> {
108 for mode in [Mode::Generic, Mode::Html, Mode::Xhtml] {
109 if s.eq_ignore_ascii_case(mode.as_str()) {
110 return Ok(mode);
111 }
112 }
113 Err(ParseModeError)
114 }
115}
116
117/// The error [`Mode`]'s [`FromStr`](std::str::FromStr) impl returns: the
118/// string named no mode.
119///
120/// It carries no payload — the offending string is the one the caller
121/// just passed in, and echoing it back would only make the message
122/// unbounded.
123#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
124pub struct ParseModeError;
125
126impl fmt::Display for ParseModeError {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 f.write_str("expected one of `generic`, `html` or `xhtml`")
129 }
130}
131
132impl std::error::Error for ParseModeError {}
133
134/// A reusable translator for one [`Mode`], optionally carrying a
135/// default namespace prefix.
136///
137/// Every flavour difference — which pseudo-class overrides apply,
138/// whether names are ASCII-lowercased, where `:lang()` reads from — is
139/// derived from the mode by the private accessors below. Casing is
140/// applied here in the translator, never via Servo's parser settings.
141///
142/// [`Default`] is [`Mode::Generic`] with no default namespace: the plain
143/// translator, the same as `Translator::new(Mode::Generic)`.
144#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
145pub struct Translator {
146 mode: Mode,
147 /// The prefix an unprefixed type selector is qualified with, set by
148 /// [`Translator::with_default_namespace_prefix`]. `None` leaves such
149 /// a selector unprefixed, so it matches the null namespace only.
150 default_namespace: Option<Cow<'static, str>>,
151}
152
153/// The namespace constraint on a type or attribute selector: none
154/// written, any, explicitly none, or a specific prefix.
155#[derive(Clone, Copy)]
156enum NsConstraint<'a> {
157 /// No namespace separator written (`e`, `[foo]`).
158 None,
159 /// `*|e`, `[*|foo]`: any namespace, including none.
160 Any,
161 /// `|e`, `[|foo]`: explicitly no namespace.
162 ExplicitNone,
163 /// `ns|e`, `[ns|foo]`: a specific prefix (identity-mapped, no URL).
164 Prefix(&'a str),
165}
166
167impl Translator {
168 /// Build a translator for one of the three [`Mode`] flavours, with
169 /// no default namespace — see
170 /// [`Translator::with_default_namespace_prefix`].
171 ///
172 /// The result is immutable and holds no per-selector state, so a
173 /// single translator can be reused for any number of translations.
174 #[must_use]
175 pub const fn new(mode: Mode) -> Self {
176 Translator {
177 mode,
178 default_namespace: None,
179 }
180 }
181
182 /// Put unprefixed type selectors in a default namespace, the way a
183 /// stylesheet's `@namespace url(…)` does.
184 ///
185 /// This crate never sees namespace URLs, so the default namespace is
186 /// named by the prefix the emitted XPath should use — one the
187 /// caller's namespace map binds, exactly as a written `h|p` prefix
188 /// would be:
189 ///
190 /// ```
191 /// use css_to_xpath::{Mode, Translator};
192 ///
193 /// let t = Translator::new(Mode::Xhtml).with_default_namespace_prefix("h");
194 /// assert_eq!(t.css_to_xpath("body > p", "").unwrap(), "h:body/h:p");
195 /// assert_eq!(t.css_to_xpath("p:is(a, b)", "").unwrap(), "h:p[self::h:a or self::h:b]");
196 /// ```
197 ///
198 /// The semantics are the CSS Namespaces 3 ones. A default namespace
199 /// applies to type selectors and to the implicit universal selector
200 /// of a compound that has none (`.c` becomes `h:*[…]`, `*` becomes
201 /// `h:*`), but never to attribute selectors — an unprefixed
202 /// attribute name has no namespace by definition. `|e` still means
203 /// "no namespace" and `*|e` still means "any namespace"; and, per
204 /// Selectors Level 4, the implicit universal selector of an
205 /// `:is()` / `:where()` / `:not()` argument is *not* qualified, so
206 /// `:is(p)` picks up the default namespace but `:is(.c)` does not.
207 ///
208 /// The prefix is checked exactly as a written one is, when the
209 /// translation reaches it: one that is not a usable XPath name is an
210 /// [`Error`] rather than a guess. An empty prefix means no default
211 /// namespace.
212 #[must_use]
213 pub fn with_default_namespace_prefix(mut self, prefix: impl Into<Cow<'static, str>>) -> Self {
214 self.default_namespace = Some(prefix.into());
215 self
216 }
217
218 /// The [`Mode`] this translator was built for.
219 #[must_use]
220 pub const fn mode(&self) -> Mode {
221 self.mode
222 }
223
224 /// The default namespace prefix set by
225 /// [`Translator::with_default_namespace_prefix`], if any.
226 #[must_use]
227 pub fn default_namespace_prefix(&self) -> Option<&str> {
228 self.default_namespace.as_deref()
229 }
230
231 /// Which pseudo-class overrides apply: `Xhtml` shares HTML's.
232 pub(crate) const fn kind(&self) -> Kind {
233 match self.mode {
234 Mode::Generic => Kind::Generic,
235 Mode::Html | Mode::Xhtml => Kind::Html,
236 }
237 }
238
239 /// Whether element names are ASCII-lowercased: only an HTML parser
240 /// does that, and only to elements it knows are HTML.
241 pub(crate) const fn lower_case_element_names(&self) -> bool {
242 matches!(self.mode, Mode::Html)
243 }
244
245 /// Whether attribute names are ASCII-lowercased. Kept apart from
246 /// [`Translator::lower_case_element_names`] because the two answer
247 /// different questions, even though today's modes agree on both.
248 pub(crate) const fn lower_case_attribute_names(&self) -> bool {
249 matches!(self.mode, Mode::Html)
250 }
251
252 /// Whether the target document is an HTML document, which is what
253 /// makes HTML's legacy case-insensitive attribute values fold (see
254 /// `apply_case_flag`). Only `Mode::Html` sets it: `Mode::Xhtml` is
255 /// XML, where those attributes compare case-sensitively. It is kept
256 /// apart from [`Translator::lower_case_attribute_names`] because the
257 /// two answer different questions — how a name is spelled versus how
258 /// a value is compared.
259 pub(crate) const fn html_document(&self) -> bool {
260 matches!(self.mode, Mode::Html)
261 }
262
263 /// Where `:lang()` reads an element's language from.
264 pub(crate) const fn lang_source(&self) -> LangSource {
265 match self.mode {
266 Mode::Generic => LangSource::XmlLang,
267 Mode::Html => LangSource::Lang,
268 Mode::Xhtml => LangSource::Both,
269 }
270 }
271
272 /// Translate comma-separated selector groups, each prefixed, joined
273 /// with " | ".
274 ///
275 /// `prefix` is prepended verbatim to every selector-group branch, so
276 /// it must end in something a node test can follow: an axis
277 /// (`"descendant-or-self::"`, [`crate::DESCENDANT_OR_SELF`]) or a
278 /// step separator (`"//"`, [`crate::WHOLE_DOCUMENT`]). Pass `""` for
279 /// a bare relative expression. Nothing validates it — a prefix like
280 /// `"/html/body "` yields `/html/body div`, which XPath reads as a
281 /// division, not a path.
282 ///
283 /// A selector group anchored on `:scope` ignores `prefix` and
284 /// anchors on the `self::` axis instead, since `:scope` names the
285 /// context node the XPath is evaluated from.
286 ///
287 /// # Errors
288 ///
289 /// Returns an [`Error`] when the selector is syntactically invalid
290 /// or uses an unsupported construct.
291 pub fn css_to_xpath(&self, css: &str, prefix: &str) -> Result<String, Error> {
292 let list = parser::parse(css, self.default_namespace_prefix())?;
293 let mut out = String::new();
294 for (i, sel) in list.slice().iter().enumerate() {
295 if i > 0 {
296 out.push_str(" | ");
297 }
298 self.selector_to_xpath(sel, prefix, &mut out)?;
299 }
300 Ok(out)
301 }
302
303 /// Translate one selector group onto the end of `out`, folding from
304 /// the leftmost compound rightwards.
305 ///
306 /// The expression is built on `out`'s own buffer, so nothing already
307 /// written is copied. On error, what `out` holds is unspecified.
308 fn selector_to_xpath(
309 &self,
310 selector: &Selector<CssToXpathImpl>,
311 prefix: &str,
312 out: &mut String,
313 ) -> Result<(), Error> {
314 let mut compounds = compounds_left_to_right(selector);
315 let (leftmost, _) = compounds
316 .next()
317 .expect("a selector has at least one compound");
318
319 // :scope is the node the XPath is evaluated from. In the leftmost
320 // compound it anchors the expression on the self:: axis, which
321 // replaces the prefix (`:scope > a` is `self::*/a`, the context
322 // node's `a` children). Anywhere else the context node would have
323 // to be named from inside a predicate, which XPath 1.0 cannot do.
324 //
325 // A written `:scope` is normally caught by the pre-parse scan,
326 // which knows where it is and so reports the same construct with
327 // a caret; this check stays as the backstop for one Servo can
328 // introduce with no source text of its own (`ImplicitScope`).
329 // Match order stores the leftmost compound last.
330 let raw = selector.iter_raw_match_order().as_slice();
331 let right_of_leftmost = &raw[..raw.len() - leftmost.len()];
332 if right_of_leftmost
333 .iter()
334 .any(|c| matches!(c, Component::Scope))
335 {
336 return Err(Error::unsupported(
337 "the `:scope` pseudo-class outside the leftmost compound",
338 ));
339 }
340 let scope_anchored = leftmost.iter().any(|c| matches!(c, Component::Scope));
341
342 // Leftmost compound first, then fold rightwards.
343 let mut xpath = self.compound_to_xpath(
344 leftmost.iter().filter(|c| !matches!(c, Component::Scope)),
345 0,
346 )?;
347 let mut path = std::mem::take(out);
348 path.push_str(if scope_anchored { "self::" } else { prefix });
349 xpath.path = path;
350 for (compound, combinator) in compounds {
351 let combinator =
352 combinator.ok_or_else(|| Error::unsupported("an unexpected selector structure"))?;
353 let right = self.compound_to_xpath(compound, 0)?;
354 xpath = apply_combinator(combinator, xpath, right)?;
355 }
356 xpath.render_into(out);
357 Ok(())
358 }
359
360 /// Translate one compound selector (a sequence of simple selectors).
361 /// Element-ish components (namespace, type) always precede condition
362 /// components in a valid compound; conditions are applied in source
363 /// order.
364 ///
365 /// `of_depth` is how many `An+B of S` argument lists this compound is
366 /// nested inside; see [`nth::MAX_NTH_OF_DEPTH`].
367 fn compound_to_xpath<'c>(
368 &self,
369 components: impl IntoIterator<Item = &'c Component<CssToXpathImpl>>,
370 of_depth: usize,
371 ) -> Result<XPathExpr, Error> {
372 let mut ns = NsConstraint::None;
373 let mut element: Option<&str> = None;
374 let mut xpath: Option<XPathExpr> = None;
375
376 for component in components {
377 match component {
378 Component::Namespace(prefix, _) if xpath.is_none() => {
379 ns = NsConstraint::Prefix(prefix.as_str());
380 }
381 // Plain `e` and the implicit universal of a type-less
382 // compound. Without a configured default namespace this
383 // is the sentinel (see `CssToXpathParser`) and means "no
384 // constraint written"; with one it is that prefix, and
385 // qualifies the name exactly as a written `h|e` would.
386 Component::DefaultNamespace(prefix) if xpath.is_none() => {
387 ns = match prefix.as_str() {
388 "" => NsConstraint::None,
389 prefix => NsConstraint::Prefix(prefix),
390 };
391 }
392 Component::ExplicitAnyNamespace if xpath.is_none() => {
393 ns = NsConstraint::Any;
394 }
395 Component::ExplicitNoNamespace if xpath.is_none() => {
396 ns = NsConstraint::ExplicitNone;
397 }
398 Component::ExplicitUniversalType if xpath.is_none() => {}
399 Component::LocalName(local_name) if xpath.is_none() => {
400 element = Some(local_name.name.as_str());
401 }
402 other => {
403 let xp = match xpath {
404 Some(ref mut xp) => xp,
405 None => {
406 xpath = Some(self.xpath_element(ns, element)?);
407 xpath.as_mut().expect("just set")
408 }
409 };
410 self.apply_simple(xp, other, of_depth)?;
411 }
412 }
413 }
414
415 Ok(match xpath {
416 Some(xp) => xp,
417 None => self.xpath_element(ns, element)?,
418 })
419 }
420
421 /// Build the element part of the expression from the namespace
422 /// constraint and element name.
423 fn xpath_element(&self, ns: NsConstraint, element: Option<&str>) -> Result<XPathExpr, Error> {
424 // Whether the compound wrote a type selector at all. Only the
425 // delimiter `*` is the universal selector; an escape only ever
426 // produces an <ident>, so `\2a` is a type selector naming the
427 // character `*` (css-syntax-3 §4.3.7, Selectors 4 §5.1–5.2).
428 // The two are told apart here by `element`, never by comparing
429 // the name to "*", which would read `*|\2a` as `*|*`.
430 let Some(element) = element else {
431 return Ok(match ns {
432 // '|*': every element with no namespace. A bare '*' is
433 // every element whatever its namespace, so the constraint
434 // has to be written out.
435 NsConstraint::ExplicitNone => {
436 let mut xpath = XPathExpr::new("*");
437 xpath.add_condition("namespace-uri() = ''");
438 xpath
439 }
440 NsConstraint::Prefix(prefix) if !is_ncname(prefix) => {
441 return Err(unsafe_prefix_error(prefix));
442 }
443 NsConstraint::Prefix(prefix) => XPathExpr::new(format!("{prefix}:*")),
444 // '*' and '*|*' translate to an unqualified name test.
445 NsConstraint::None | NsConstraint::Any => XPathExpr::new("*"),
446 });
447 };
448 let safe = is_safe_name(element);
449 let name =
450 if self.lower_case_element_names() && element.bytes().any(|b| b.is_ascii_uppercase()) {
451 Cow::Owned(element.to_ascii_lowercase())
452 } else {
453 Cow::Borrowed(element)
454 };
455 match ns {
456 NsConstraint::Any => {
457 // '*|e': 'e' in any namespace, including none. An unprefixed
458 // XPath name test only matches the null namespace, so test
459 // against local-name() instead. The of-type nodetest counts
460 // by local name too, an approximation: siblings sharing the
461 // name across namespaces are distinct types per the spec,
462 // but XPath 1.0 cannot compare a sibling's namespace
463 // against the matched element's.
464 let cond = format!("local-name() = {}", xpath_expr::xpath_literal(&name));
465 let mut xpath = XPathExpr::new("*");
466 xpath.name_test = Some(format!("*[{cond}]"));
467 xpath.folded_name = Some(name.into_owned());
468 xpath.add_condition(cond);
469 Ok(xpath)
470 }
471 NsConstraint::None | NsConstraint::ExplicitNone if !safe => {
472 // A safe 'e' or '|e' is just an unprefixed XPath name
473 // test, which matches exactly the null namespace. A name
474 // needing quoting cannot be a name test at all, so it
475 // folds into a name() comparison — and name() returns the
476 // *qualified* name, which for an element in a default
477 // namespace is the bare local name. Pin namespace-uri()
478 // alongside it so a quoted name matches exactly what a
479 // safe one does.
480 let cond = format!("name() = {}", xpath_expr::xpath_literal(&name));
481 let mut xpath = XPathExpr::new("*");
482 // The of-type nodetest must carry the namespace pin set
483 // by the condition below.
484 xpath.name_test = Some(format!("*[{cond} and namespace-uri() = '']"));
485 // name() on an element with no namespace is its local
486 // name, which the namespace-uri() pin below makes exact.
487 xpath.folded_name = Some(name.into_owned());
488 xpath.add_condition(cond);
489 xpath.add_condition("namespace-uri() = ''");
490 Ok(xpath)
491 }
492 // A prefix is written into the node test as it stands
493 // (prefixes are case-sensitive:
494 // https://www.w3.org/TR/css-namespaces-3/#prefixes), so it
495 // has to be a name XPath can parse — a looser test than the
496 // local name's, which has the local-name() fallback.
497 NsConstraint::Prefix(prefix) if !is_ncname(prefix) => Err(unsafe_prefix_error(prefix)),
498 NsConstraint::Prefix(prefix) if !safe => {
499 // Only the local name needs quoting: keep the prefix in
500 // the node test so the engine still resolves it through
501 // the caller's namespace map, and compare the local part
502 // alone. Folding the whole 'prefix:name' into a name()
503 // test would instead match only documents that happen to
504 // use that very prefix.
505 let cond = format!("local-name() = {}", xpath_expr::xpath_literal(&name));
506 let mut xpath = XPathExpr::new(format!("{prefix}:*"));
507 // The of-type nodetest must carry the local-name test set
508 // by the condition below.
509 xpath.name_test = Some(format!("{prefix}:*[{cond}]"));
510 xpath.folded_name = Some(name.into_owned());
511 xpath.add_condition(cond);
512 Ok(xpath)
513 }
514 // Every name needing quoting was handled above, so what is
515 // left is a plain node test: 'e' or 'ns:e'.
516 NsConstraint::Prefix(prefix) => Ok(XPathExpr::new(format!("{prefix}:{name}"))),
517 // 'e' and '|e' translate to an unqualified name test.
518 NsConstraint::None | NsConstraint::ExplicitNone => {
519 Ok(XPathExpr::new(name.into_owned()))
520 }
521 }
522 }
523
524 /// Dispatch over the non-element components of a compound — the
525 /// allow-list over `Component` variants. Anything outside the
526 /// supported construct set errors, never approximates.
527 fn apply_simple(
528 &self,
529 xpath: &mut XPathExpr,
530 component: &Component<CssToXpathImpl>,
531 of_depth: usize,
532 ) -> Result<(), Error> {
533 match component {
534 // :root
535 Component::Root => {
536 xpath.add_condition("not(parent::*)");
537 Ok(())
538 }
539 // :empty
540 Component::Empty => {
541 xpath.add_condition("not(*) and not(string-length())");
542 Ok(())
543 }
544 // :first-child, :nth-child(an+b), :only-of-type, ... — Servo
545 // collapses the whole family into NthSelectorData.
546 Component::Nth(data) => self.apply_nth(xpath, data, None, of_depth),
547 // :nth-child(an+b of S) / :nth-last-child(an+b of S)
548 Component::NthOf(data) => {
549 self.apply_nth(xpath, data.nth_data(), Some(data.selectors()), of_depth)
550 }
551 // :not(). Nesting inside other functional pseudo-classes is
552 // allowed (Selectors Level 4).
553 Component::Negation(list) => {
554 let joined = self
555 .arg_conditions(list.slice(), ":not()", of_depth)?
556 .and_then(Condition::join_or);
557 match joined {
558 // not(...) supplies its own grouping, so the
559 // or-join needs no parentheses.
560 Some(joined) => xpath.add_condition(format!("not({})", joined.expr)),
561 // A universal argument makes the negation unmatchable.
562 None => xpath.add_condition("0"),
563 }
564 Ok(())
565 }
566 // :is()/:matches() and :where() — identical translations: the
567 // arguments OR together into a single condition that is AND-ed
568 // onto the outer expression, keeping the compound a conjunction.
569 Component::Is(list) | Component::Where(list) => {
570 // Selectors 4 makes these argument lists forgiving, so an
571 // empty one is valid and matches nothing. The parser
572 // accepts that recovery and no other, so any other list
573 // here is an ordinary one.
574 if parser::is_empty_forgiving_list(list.slice()) {
575 xpath.add_condition("0");
576 return Ok(());
577 }
578 let context = match component {
579 Component::Is(_) => ":is()",
580 _ => ":where()",
581 };
582 // None means an argument matched everything, so the whole
583 // pseudo-class is a no-op constraint.
584 if let Some(conditions) = self.arg_conditions(list.slice(), context, of_depth)?
585 && let Some(joined) = Condition::join_or(conditions)
586 {
587 xpath.push_condition(joined);
588 }
589 Ok(())
590 }
591 // :has(), the one functional pseudo-class that looks forward.
592 Component::Has(relatives) => self.apply_has(xpath, relatives, of_depth),
593 // :hover, :checked, :lang(), ... — translator-dependent.
594 Component::NonTSPseudoClass(pc) => self.apply_pseudo_class(xpath, pc),
595 // e#myid
596 Component::ID(id) => {
597 attrib_equals(xpath, "@id", id.as_str());
598 Ok(())
599 }
600 // .foo is defined as [class~=foo] in the spec
601 Component::Class(class_name) => {
602 attrib_includes(xpath, "@class", class_name.as_str());
603 Ok(())
604 }
605 Component::AttributeInNoNamespaceExists { local_name, .. } => {
606 let attrib = self.attrib_expr(NsConstraint::None, local_name.as_str())?;
607 xpath.add_condition(attrib);
608 Ok(())
609 }
610 Component::AttributeInNoNamespace {
611 local_name,
612 operator,
613 value,
614 case_sensitivity,
615 } => {
616 let attrib = self.attrib_expr(NsConstraint::None, local_name.as_str())?;
617 let (attrib, value) =
618 self.apply_case_flag(attrib, value.as_str(), *case_sensitivity);
619 attrib_operator(xpath, &attrib, *operator, &value)
620 }
621 Component::AttributeOther(attr) => {
622 let ns = match attr.namespace {
623 Some(NamespaceConstraint::Specific((ref prefix, _))) => {
624 NsConstraint::Prefix(prefix.as_str())
625 }
626 Some(NamespaceConstraint::Any) => NsConstraint::Any,
627 // '[|foo]' is equivalent to '[foo]': unprefixed
628 // attribute names have no namespace.
629 None => NsConstraint::None,
630 };
631 let attrib = self.attrib_expr(ns, attr.local_name.as_str())?;
632 match attr.operation {
633 ParsedAttrSelectorOperation::Exists => {
634 xpath.add_condition(attrib);
635 Ok(())
636 }
637 ParsedAttrSelectorOperation::WithValue {
638 operator,
639 case_sensitivity,
640 ref value,
641 } => {
642 let (attrib, value) =
643 self.apply_case_flag(attrib, value.as_str(), case_sensitivity);
644 attrib_operator(xpath, &attrib, operator, &value)
645 }
646 }
647 }
648 unsupported => Err(Error::unsupported(describe_component(unsupported))),
649 }
650 }
651
652 /// `:has()`: each argument is a relative selector whose optional
653 /// leading combinator scopes the match (`>` child, `~` subsequent
654 /// sibling, `+` next sibling; omitted means descendant). Unlike the
655 /// other functional pseudo-classes, `:has()` looks forward, so a
656 /// complex argument extends the existence-test path step by step,
657 /// leftmost compound first.
658 fn apply_has(
659 &self,
660 xpath: &mut XPathExpr,
661 relatives: &[RelativeSelector<CssToXpathImpl>],
662 of_depth: usize,
663 ) -> Result<(), Error> {
664 // Every argument's test, written straight into one union.
665 let mut union = String::new();
666 for (arg, relative) in relatives.iter().enumerate() {
667 let mut compounds = compounds_left_to_right(&relative.selector).peekable();
668 // The leftmost compound is the anchor (the candidate element
669 // itself); the combinator to the right of it is the
670 // argument's leading combinator.
671 let anchor_only = matches!(
672 compounds.next(),
673 Some(([Component::RelativeSelectorAnchor], _))
674 );
675 if !anchor_only || compounds.peek().is_none() {
676 return Err(Error::unsupported(
677 "an unexpected selector structure inside `:has()`",
678 ));
679 }
680 if arg > 0 {
681 union.push_str(" | ");
682 }
683 for (step, (compound, combinator)) in compounds.enumerate() {
684 let first = step == 0;
685 // The first step is an axis from the candidate element;
686 // later steps join onto the path.
687 let axis = match (first, combinator) {
688 (true, Some(Combinator::Descendant)) => ".//",
689 (true, Some(Combinator::Child)) => "child::",
690 (true, Some(Combinator::NextSibling) | Some(Combinator::LaterSibling)) => {
691 "following-sibling::"
692 }
693 (false, Some(Combinator::Descendant)) => "//",
694 (false, Some(Combinator::Child)) => "/",
695 (false, Some(Combinator::NextSibling) | Some(Combinator::LaterSibling)) => {
696 "/following-sibling::"
697 }
698 (_, other) => {
699 return Err(Error::unsupported(format!(
700 "an unexpected combinator ({other:?}) inside `:has()`"
701 )));
702 }
703 };
704 let mut sub = self.compound_to_xpath(compound, of_depth)?;
705 // The name stays in the node test (`.//p`, `.//svg:g`) so
706 // it means exactly what it means at the top level and a
707 // prefix resolves through the namespace map — except under
708 // `+`, where the [1] position predicate has to count every
709 // sibling, so the node test must stay `*`.
710 if matches!(combinator, Some(Combinator::NextSibling)) {
711 sub.take_element_into_self_test();
712 // Only the immediately following sibling: constrain
713 // position before applying the match conditions.
714 sub.add_predicate("1");
715 }
716 union.push_str(axis);
717 sub.render_into(&mut union);
718 }
719 }
720 // A `:has()` list of several arguments renders as a union, which
721 // binds tighter than `and` in XPath 1.0 and so needs no
722 // parentheses — but reads as though it might, so it is marked an
723 // or-group and parenthesized wherever an or-group would be.
724 match relatives.len() {
725 0 => {}
726 1 => xpath.add_condition(union),
727 _ => xpath.add_or_condition(union),
728 }
729 Ok(())
730 }
731
732 /// Whether an attribute-value comparison is case-sensitive, and the
733 /// resulting comparison pair.
734 ///
735 /// Selectors 4 leaves attribute-value case sensitivity to the document
736 /// language unless a flag overrides it, and HTML makes a fixed list of
737 /// attributes (`type`, `rel`, `dir`, `checked`, ... — the presentational
738 /// and enumerated legacy ones) ASCII case-insensitive on HTML elements in
739 /// HTML documents. Servo's parser does that classification for us and
740 /// hands back `AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument`, already
741 /// restricted to unflagged, un-namespaced attributes. The other half of
742 /// that variant's condition — that the element is in the HTML namespace —
743 /// is not checkable from a selector, but it holds wherever the flag does:
744 /// an HTML parser puts every element in one document, without namespaces.
745 ///
746 /// Folding means comparing the ASCII-lowercased attribute (via XPath
747 /// `translate()`) against the ASCII-lowercased value. An empty value needs
748 /// no lowercasing, and skipping it keeps the existence tests exact.
749 fn apply_case_flag<'v>(
750 &self,
751 attrib: String,
752 value: &'v str,
753 case_sensitivity: ParsedCaseSensitivity,
754 ) -> (String, Cow<'v, str>) {
755 let fold = match case_sensitivity {
756 // `[attr="value" i]`.
757 ParsedCaseSensitivity::AsciiCaseInsensitive => true,
758 // No flag on one of HTML's case-insensitive attributes: it
759 // folds only where the document is HTML. `Mode::Xhtml` is XML,
760 // where these attributes are case-sensitive like any other.
761 ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {
762 self.html_document()
763 }
764 // `[attr="value" s]`, and the case-sensitive no-flag default.
765 ParsedCaseSensitivity::ExplicitCaseSensitive | ParsedCaseSensitivity::CaseSensitive => {
766 false
767 }
768 };
769 if fold && !value.is_empty() {
770 (
771 xpath_expr::ascii_lower(&attrib),
772 Cow::Owned(value.to_ascii_lowercase()),
773 )
774 } else {
775 (attrib, Cow::Borrowed(value))
776 }
777 }
778
779 /// Attribute-name handling: ASCII-lowercase (html), safety check, namespace
780 /// qualification. Prefixes are checked too, as in `xpath_element`,
781 /// but against the `NCName` production rather than the local name's
782 /// stricter test: a prefix that cannot be a node test at all errors.
783 fn attrib_expr(&self, ns: NsConstraint, local_name: &str) -> Result<String, Error> {
784 let name = if self.lower_case_attribute_names() {
785 local_name.to_ascii_lowercase()
786 } else {
787 local_name.to_owned()
788 };
789 let safe = is_safe_name(&name);
790 match ns {
791 NsConstraint::Any => {
792 // '[*|attr]': 'attr' in any namespace, including none. An
793 // unprefixed XPath attribute test only matches attributes
794 // with no namespace, so test against local-name() instead.
795 Ok(format!(
796 "@*[local-name() = {}]",
797 xpath_expr::xpath_literal(&name)
798 ))
799 }
800 NsConstraint::Prefix(prefix) if !is_ncname(prefix) => Err(unsafe_prefix_error(prefix)),
801 NsConstraint::Prefix(prefix) if !safe => {
802 // As in `xpath_element`: the prefix stays in the node test
803 // so it resolves through the caller's namespace map, and
804 // only the local part is compared.
805 Ok(format!(
806 "@{prefix}:*[local-name() = {}]",
807 xpath_expr::xpath_literal(&name)
808 ))
809 }
810 NsConstraint::Prefix(prefix) => Ok(format!("@{prefix}:{name}")),
811 NsConstraint::None | NsConstraint::ExplicitNone => Ok(if safe {
812 format!("@{name}")
813 } else {
814 format!(
815 "attribute::*[name() = {}]",
816 xpath_expr::xpath_literal(&name)
817 )
818 }),
819 }
820 }
821
822 /// Harvest the conditions of a pseudo-class argument list, the shared
823 /// pattern of :not()/:is()/:where() and the nth `of S` handling:
824 /// translate each argument into a condition on the candidate element.
825 ///
826 /// Returns `None` when any argument matches everything (e.g. `*`): the
827 /// OR of the list is then trivially true, so callers must not constrain
828 /// on the remaining arguments.
829 fn arg_conditions(
830 &self,
831 selectors: &[Selector<CssToXpathImpl>],
832 context: &str,
833 of_depth: usize,
834 ) -> Result<Option<Vec<Condition>>, Error> {
835 let mut conditions = Vec::new();
836 let mut trivially_true = false;
837 for selector in selectors {
838 match self.argument_condition(selector, context, of_depth)? {
839 None => trivially_true = true,
840 Some(condition) => conditions.push(condition),
841 }
842 }
843 Ok(if trivially_true {
844 None
845 } else {
846 Some(conditions)
847 })
848 }
849
850 /// The condition imposed on the candidate element by the whole
851 /// argument chain. The compound's element becomes a `self::` node
852 /// test, which tests exactly what the name would have tested as the
853 /// node test of a top-level selector: `:is(p)` constrains the same
854 /// elements as `p`, and a prefix still resolves through the caller's
855 /// namespace map. A complex argument applies its rightmost
856 /// compound to the candidate, with everything to its left becoming an
857 /// existence test through reversed axes:
858 /// `:is(a > b ~ c)` matches a `c` with a preceding sibling `b` whose
859 /// parent is an `a`.
860 ///
861 /// The chain is walked twice rather than recursed over, so its length
862 /// costs no stack: once left-to-right to translate each compound and
863 /// pick its reversed axis, then once right-to-left (leftmost compound
864 /// first) to wrap each condition inside the one to its right.
865 ///
866 /// A compound that cannot match ends the chain, the same way a `0`
867 /// absorbs the rest of a conjunction in [`XPathExpr::write_condition`]:
868 /// the existence test it is conjoined with, and every compound
869 /// further left, are dropped, so `:is(a > b:is())` is `0` rather
870 /// than `0 and parent::*[self::a]`. The compounds to its right are
871 /// untouched — `:is(a > b:is() > c)` keeps `self::c` and its
872 /// bracket — and a `0` that is already the innermost condition, with
873 /// nothing conjoined to absorb, stays where it is: `:is(a:is() > b)`
874 /// is `self::b and parent::*[0]`, which still says which compound is
875 /// the impossible one.
876 ///
877 /// `None` means the chain imposes no condition (a bare `*` argument).
878 fn argument_condition(
879 &self,
880 selector: &Selector<CssToXpathImpl>,
881 context: &str,
882 of_depth: usize,
883 ) -> Result<Option<Condition>, Error> {
884 // The compounds, rightmost first.
885 let mut subs: Vec<XPathExpr> = Vec::new();
886 // `axes[i]` points back at where the left-hand side of `subs[i]`'s
887 // combinator must be, relative to the element matched by
888 // `subs[i]`. The leftmost compound has nothing to its left, so
889 // there is one fewer axis than compound.
890 let mut axes: Vec<&str> = Vec::new();
891 for (compound, combinator) in compounds_match_order(selector) {
892 let mut sub = self.compound_to_xpath(compound, of_depth)?;
893 sub.take_element_into_self_test();
894 // A single compound imposes its own conditions and nothing
895 // else.
896 if combinator.is_none() && subs.is_empty() {
897 return Ok(sub.into_condition());
898 }
899 subs.push(sub);
900 if combinator.is_some() {
901 axes.push(match combinator {
902 Some(Combinator::Descendant) => "ancestor::*",
903 Some(Combinator::Child) => "parent::*",
904 Some(Combinator::LaterSibling) => "preceding-sibling::*",
905 Some(Combinator::NextSibling) => "preceding-sibling::*[1]",
906 other => {
907 return Err(Error::unsupported(format!(
908 "an unexpected combinator ({other:?}) inside `{context}`"
909 )));
910 }
911 });
912 }
913 }
914
915 // A compound that cannot match absorbs the existence test it is
916 // conjoined with, and with it every compound further left. The
917 // chain ends there; the compounds to its right keep their own
918 // conditions and their brackets. Every compound is translated
919 // first, so a discarded one still reports its errors.
920 if let Some(dead) = subs.iter().position(XPathExpr::is_never) {
921 subs.truncate(dead + 1);
922 axes.truncate(dead);
923 }
924 let innermost = subs.pop().expect("more than one compound");
925 if subs.is_empty() {
926 return Ok(innermost.into_condition());
927 }
928
929 // The nesting reads outward-in — `c0 and axis0[c1 and axis1[c2]]`
930 // — so write it in that order: each compound emits its own
931 // conditions and opens its axis bracket, and every bracket closes
932 // at the end. Wrapping the other way (nesting the condition built
933 // so far inside the next compound's brackets) would copy the whole
934 // accumulated condition once per compound, making a chain of n
935 // compounds cost O(n^2) bytes.
936 let mut expr = String::new();
937 let mut open = 0usize;
938 for (idx, (sub, axis)) in subs.iter().zip(&axes).enumerate() {
939 // This compound's own conditions come first, conjoined with
940 // the existence test that follows; a lone or-group is
941 // parenthesized here because `and` binds tighter than `or`.
942 if sub
943 .write_condition(&mut expr, /* as_operand = */ true)
944 .is_some()
945 {
946 expr.push_str(" and ");
947 }
948 expr.push_str(axis);
949 // The bracket is only opened when something goes inside it:
950 // the innermost compound may impose no condition at all (a
951 // bare `*`), leaving the axis as a plain existence test.
952 if idx + 1 < subs.len() || innermost.has_condition() {
953 expr.push('[');
954 open += 1;
955 }
956 }
957 // The innermost condition sits inside brackets, so a top-level
958 // `or` needs no parentheses of its own.
959 innermost.write_condition(&mut expr, /* as_operand = */ false);
960 for _ in 0..open {
961 expr.push(']');
962 }
963 Ok(Some(Condition {
964 expr,
965 // Every compound but the innermost contributes an existence
966 // test conjoined at the top level, so the result is an `and`.
967 or_group: false,
968 }))
969 }
970}
971
972/// Join two compound translations with a combinator.
973fn apply_combinator(
974 combinator: Combinator,
975 mut left: XPathExpr,
976 right: XPathExpr,
977) -> Result<XPathExpr, Error> {
978 match combinator {
979 Combinator::Descendant => left.join("//", right),
980 Combinator::Child => left.join("/", right),
981 Combinator::LaterSibling => left.join("/following-sibling::", right),
982 Combinator::NextSibling => {
983 left.join("/following-sibling::", right);
984 // The node test moves into a self:: predicate so the [1]
985 // position test counts every sibling, not only same-name
986 // ones: *[1][self::element][existing conditions]. A `*`
987 // node test already counts every sibling, so it needs no
988 // predicate — `self::*` would test nothing.
989 let target_element = left.take_element();
990 left.add_predicate("1");
991 if target_element != "*" {
992 left.add_predicate(format!("self::{target_element}"));
993 }
994 }
995 // PseudoElement / SlotAssignment / Part combinators can never be
996 // produced: the corresponding parser hooks are disabled.
997 other => {
998 return Err(Error::unsupported(format!("the {other:?} combinator")));
999 }
1000 }
1001 Ok(left)
1002}
1003
1004/// A selector's compounds in match order (rightmost first), each with
1005/// the combinator to its left — `None` for the leftmost only.
1006///
1007/// Servo stores a selector in exactly this order, as one slice with a
1008/// `Component::Combinator` between neighbouring compounds and each
1009/// compound's own components in source order, so the compounds are
1010/// borrowed from it rather than collected.
1011fn compounds_match_order(
1012 selector: &Selector<CssToXpathImpl>,
1013) -> impl Iterator<Item = (&[Component<CssToXpathImpl>], Option<Combinator>)> {
1014 let raw = selector.iter_raw_match_order().as_slice();
1015 let combinators = raw.iter().filter_map(as_combinator).map(Some);
1016 raw.split(Component::is_combinator)
1017 .zip(combinators.chain(std::iter::once(None)))
1018}
1019
1020/// [`compounds_match_order`] reversed: the leftmost compound first,
1021/// each with the combinator to its left.
1022fn compounds_left_to_right(
1023 selector: &Selector<CssToXpathImpl>,
1024) -> impl Iterator<Item = (&[Component<CssToXpathImpl>], Option<Combinator>)> {
1025 let raw = selector.iter_raw_match_order().as_slice();
1026 let combinators = raw.iter().rev().filter_map(as_combinator).map(Some);
1027 raw.rsplit(Component::is_combinator)
1028 .zip(std::iter::once(None).chain(combinators))
1029}
1030
1031fn as_combinator(component: &Component<CssToXpathImpl>) -> Option<Combinator> {
1032 match component {
1033 Component::Combinator(combinator) => Some(*combinator),
1034 _ => None,
1035 }
1036}
1037
1038/// A namespace prefix that is not an XML `NCName` (see [`ncname`]) cannot
1039/// appear in a node test, and XPath 1.0 offers no way to resolve it
1040/// without the namespace URI, which this crate never sees.
1041///
1042/// The obvious symmetry with the local-name path — where a name that
1043/// cannot be a node test folds into a `name()` or `local-name()`
1044/// comparison — does not hold, which is why this is an error and not a
1045/// third fallback. Those fallbacks are exact rewrites: `name() = '123'
1046/// and namespace-uri() = ''` selects what the node test `123` would have
1047/// selected, and `svg:*[local-name() = 'di[v']` still resolves `svg`
1048/// through the evaluator's namespace map. Comparing a whole
1049/// `prefix:name` against `name()` is not a rewrite of anything: it tests
1050/// how the *document* spells its prefix, where every other prefix this
1051/// crate emits is resolved by what the caller bound it to. So a prefix
1052/// that is not a name is refused rather than matched by spelling — and
1053/// the caller could not bind it either way, since an XPath expression
1054/// has no way to name it.
1055fn unsafe_prefix_error(prefix: &str) -> Error {
1056 Error::unsupported(format!(
1057 "a namespace prefix that is not an XPath name (`{prefix}`)"
1058 ))
1059}
1060
1061/// Human-readable construct names for unsupported-error messages.
1062fn describe_component(component: &Component<CssToXpathImpl>) -> String {
1063 match component {
1064 // Top-level :scope is handled (or rejected) in selector_to_xpath,
1065 // so reaching this arm means :scope sits inside a functional
1066 // pseudo-class argument, where the context node is unreachable.
1067 // A written one is rejected by the pre-parse scan first; this is
1068 // the backstop for Servo's own `ImplicitScope`.
1069 Component::Scope | Component::ImplicitScope => {
1070 "the `:scope` pseudo-class inside a functional pseudo-class".into()
1071 }
1072 Component::Slotted(..) => "the `::slotted()` pseudo-element".into(),
1073 Component::Part(..) => "the `::part()` pseudo-element".into(),
1074 // Also reached only as a backstop: `:host(...)`, the one form
1075 // Servo parses without the shadow-DOM hooks this crate leaves
1076 // off, is rejected by the scan with a position.
1077 Component::Host(..) => "the `:host` pseudo-class".into(),
1078 // Unreachable in practice: the pre-parse scan rejects a `&`
1079 // before Servo sees it, and nesting is not enabled anyway.
1080 // Worded as the scan words it, so the two cannot diverge.
1081 Component::ParentSelector => "the `&` nesting selector".into(),
1082 // PseudoElement carries an uninhabited type and the remaining
1083 // variants require parser features this crate never enables; they
1084 // are unreachable, but erroring beats panicking: the caller's
1085 // profile is the caller's to choose, and `panic = abort` there
1086 // would tear down its process.
1087 other => format!("an unexpected construct ({other:?})"),
1088 }
1089}