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 parts: Vec<String> = Vec::new();
294 for sel in list.slice() {
295 parts.push(self.selector_to_xpath(sel, prefix)?);
296 }
297 Ok(parts.join(" | "))
298 }
299
300 /// Iteration bridge: Servo iterates compound selectors right-to-left
301 /// (match order), but the XPath is built left-to-right. Collect
302 /// Servo's sequences + combinators, then fold from the leftmost
303 /// compound.
304 fn selector_to_xpath(
305 &self,
306 selector: &Selector<CssToXpathImpl>,
307 prefix: &str,
308 ) -> Result<String, Error> {
309 let seqs = collect_seqs(selector);
310
311 // :scope is the node the XPath is evaluated from. In the leftmost
312 // compound it anchors the expression on the self:: axis, which
313 // replaces the prefix (`:scope > a` is `self::*/a`, the context
314 // node's `a` children). Anywhere else the context node would have
315 // to be named from inside a predicate, which XPath 1.0 cannot do.
316 //
317 // A written `:scope` is normally caught by the pre-parse scan,
318 // which knows where it is and so reports the same construct with
319 // a caret; this check stays as the backstop for one Servo can
320 // introduce with no source text of its own (`ImplicitScope`).
321 let leftmost = seqs.len() - 1;
322 for (compound, _) in &seqs[..leftmost] {
323 if compound.iter().any(|c| matches!(c, Component::Scope)) {
324 return Err(Error::unsupported(
325 "the `:scope` pseudo-class outside the leftmost compound",
326 ));
327 }
328 }
329 let scope_anchored = seqs[leftmost]
330 .0
331 .iter()
332 .any(|c| matches!(c, Component::Scope));
333
334 // Leftmost compound first, then fold rightwards.
335 let mut xpath = if scope_anchored {
336 let compound: Vec<&Component<CssToXpathImpl>> = seqs[leftmost]
337 .0
338 .iter()
339 .filter(|c| !matches!(c, Component::Scope))
340 .copied()
341 .collect();
342 let mut xp = self.compound_to_xpath(&compound, 0)?;
343 xp.path = "self::".to_owned();
344 xp
345 } else {
346 self.compound_to_xpath(&seqs[leftmost].0, 0)?
347 };
348 for i in (0..leftmost).rev() {
349 let combinator = seqs[i]
350 .1
351 .ok_or_else(|| Error::unsupported("an unexpected selector structure"))?;
352 let right = self.compound_to_xpath(&seqs[i].0, 0)?;
353 xpath = apply_combinator(combinator, xpath, &right)?;
354 }
355
356 let prefix = if scope_anchored { "" } else { prefix };
357 Ok(format!("{prefix}{}", xpath.render()))
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(
368 &self,
369 components: &[&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 let (mut name, safe) = match element {
425 None => ("*".to_owned(), true),
426 Some(e) => {
427 let safe = is_safe_name(e);
428 let e = if self.lower_case_element_names() {
429 e.to_ascii_lowercase()
430 } else {
431 e.to_owned()
432 };
433 (e, safe)
434 }
435 };
436 match ns {
437 NsConstraint::Any if name != "*" => {
438 // '*|e': 'e' in any namespace, including none. An unprefixed
439 // XPath name test only matches the null namespace, so test
440 // against local-name() instead. The of-type nodetest counts
441 // by local name too, an approximation: siblings sharing the
442 // name across namespaces are distinct types per the spec,
443 // but XPath 1.0 cannot compare a sibling's namespace
444 // against the matched element's.
445 let cond = format!("local-name() = {}", xpath_expr::xpath_literal(&name));
446 let mut xpath = XPathExpr::new("*");
447 xpath.name_test = Some(format!("*[{cond}]"));
448 xpath.local_name = Some(name);
449 xpath.add_condition(&cond);
450 return Ok(xpath);
451 }
452 NsConstraint::ExplicitNone if name == "*" => {
453 // '|*': every element with no namespace. A bare '*' is
454 // every element whatever its namespace, so the constraint
455 // has to be written out.
456 let mut xpath = XPathExpr::new("*");
457 xpath.add_condition("namespace-uri() = ''");
458 return Ok(xpath);
459 }
460 NsConstraint::None | NsConstraint::ExplicitNone if !safe => {
461 // A safe 'e' or '|e' is just an unprefixed XPath name
462 // test, which matches exactly the null namespace. A name
463 // needing quoting cannot be a name test at all, so it
464 // folds into a name() comparison — and name() returns the
465 // *qualified* name, which for an element in a default
466 // namespace is the bare local name. Pin namespace-uri()
467 // alongside it so a quoted name matches exactly what a
468 // safe one does.
469 let cond = format!("name() = {}", xpath_expr::xpath_literal(&name));
470 let mut xpath = XPathExpr::new("*");
471 // The of-type nodetest must carry the namespace pin set
472 // by the condition below.
473 xpath.name_test = Some(format!("*[{cond} and namespace-uri() = '']"));
474 // name() on an element with no namespace is its local
475 // name, which the namespace-uri() pin below makes exact.
476 xpath.local_name = Some(name);
477 xpath.add_condition(&cond);
478 xpath.add_condition("namespace-uri() = ''");
479 return Ok(xpath);
480 }
481 // A prefix is written into the node test as it stands
482 // (prefixes are case-sensitive:
483 // https://www.w3.org/TR/css-namespaces-3/#prefixes), so it
484 // has to be a name XPath can parse — a looser test than the
485 // local name's, which has the local-name() fallback.
486 NsConstraint::Prefix(prefix) if !is_ncname(prefix) => {
487 return Err(unsafe_prefix_error(prefix));
488 }
489 NsConstraint::Prefix(prefix) if !safe => {
490 // Only the local name needs quoting: keep the prefix in
491 // the node test so the engine still resolves it through
492 // the caller's namespace map, and compare the local part
493 // alone. Folding the whole 'prefix:name' into a name()
494 // test would instead match only documents that happen to
495 // use that very prefix.
496 let cond = format!("local-name() = {}", xpath_expr::xpath_literal(&name));
497 let mut xpath = XPathExpr::new(&format!("{prefix}:*"));
498 // The of-type nodetest must carry the local-name test set
499 // by the condition below.
500 xpath.name_test = Some(format!("{prefix}:*[{cond}]"));
501 xpath.local_name = Some(name);
502 xpath.add_condition(&cond);
503 return Ok(xpath);
504 }
505 NsConstraint::Prefix(prefix) => {
506 name = format!("{prefix}:{name}");
507 }
508 // 'e', '|e' and '*|*' translate to an unqualified name test.
509 _ => {}
510 }
511 // Every name needing quoting was handled above, so what is left
512 // is a plain node test: '*', 'e', 'ns:e' or 'ns:*'.
513 Ok(XPathExpr::new(&name))
514 }
515
516 /// Dispatch over the non-element components of a compound — the
517 /// allow-list over `Component` variants. Anything outside the
518 /// supported construct set errors, never approximates.
519 fn apply_simple(
520 &self,
521 xpath: &mut XPathExpr,
522 component: &Component<CssToXpathImpl>,
523 of_depth: usize,
524 ) -> Result<(), Error> {
525 match component {
526 // :root
527 Component::Root => {
528 xpath.add_condition("not(parent::*)");
529 Ok(())
530 }
531 // :empty
532 Component::Empty => {
533 xpath.add_condition("not(*) and not(string-length())");
534 Ok(())
535 }
536 // :first-child, :nth-child(an+b), :only-of-type, ... — Servo
537 // collapses the whole family into NthSelectorData.
538 Component::Nth(data) => self.apply_nth(xpath, data, None, of_depth),
539 // :nth-child(an+b of S) / :nth-last-child(an+b of S)
540 Component::NthOf(data) => {
541 self.apply_nth(xpath, data.nth_data(), Some(data.selectors()), of_depth)
542 }
543 // :not(). Nesting inside other functional pseudo-classes is
544 // allowed (Selectors Level 4).
545 Component::Negation(list) => {
546 let joined = self
547 .arg_conditions(list.slice(), ":not()", of_depth)?
548 .and_then(|conditions| Condition::join_or(&conditions));
549 match joined {
550 // not(...) supplies its own grouping, so the
551 // or-join needs no parentheses.
552 Some(joined) => xpath.add_condition(&format!("not({})", joined.expr)),
553 // A universal argument makes the negation unmatchable.
554 None => xpath.add_condition("0"),
555 }
556 Ok(())
557 }
558 // :is()/:matches() and :where() — identical translations: the
559 // arguments OR together into a single condition that is AND-ed
560 // onto the outer expression, keeping the compound a conjunction.
561 Component::Is(list) | Component::Where(list) => {
562 // Selectors 4 makes these argument lists forgiving, so an
563 // empty one is valid and matches nothing. The parser
564 // accepts that recovery and no other, so any other list
565 // here is an ordinary one.
566 if parser::is_empty_forgiving_list(list.slice()) {
567 xpath.add_condition("0");
568 return Ok(());
569 }
570 let context = match component {
571 Component::Is(_) => ":is()",
572 _ => ":where()",
573 };
574 // None means an argument matched everything, so the whole
575 // pseudo-class is a no-op constraint.
576 if let Some(conditions) = self.arg_conditions(list.slice(), context, of_depth)?
577 && let Some(joined) = Condition::join_or(&conditions)
578 {
579 xpath.push_condition(joined);
580 }
581 Ok(())
582 }
583 // :has(), the one functional pseudo-class that looks forward.
584 Component::Has(relatives) => self.apply_has(xpath, relatives, of_depth),
585 // :hover, :checked, :lang(), ... — translator-dependent.
586 Component::NonTSPseudoClass(pc) => self.apply_pseudo_class(xpath, pc),
587 // e#myid
588 Component::ID(id) => {
589 attrib_equals(xpath, "@id", id.as_str());
590 Ok(())
591 }
592 // .foo is defined as [class~=foo] in the spec
593 Component::Class(class_name) => {
594 attrib_includes(xpath, "@class", class_name.as_str());
595 Ok(())
596 }
597 Component::AttributeInNoNamespaceExists { local_name, .. } => {
598 let attrib = self.attrib_expr(NsConstraint::None, local_name.as_str())?;
599 xpath.add_condition(&attrib);
600 Ok(())
601 }
602 Component::AttributeInNoNamespace {
603 local_name,
604 operator,
605 value,
606 case_sensitivity,
607 } => {
608 let attrib = self.attrib_expr(NsConstraint::None, local_name.as_str())?;
609 let (attrib, value) =
610 self.apply_case_flag(attrib, value.as_str(), *case_sensitivity);
611 attrib_operator(xpath, &attrib, *operator, &value)
612 }
613 Component::AttributeOther(attr) => {
614 let ns = match attr.namespace {
615 Some(NamespaceConstraint::Specific((ref prefix, _))) => {
616 NsConstraint::Prefix(prefix.as_str())
617 }
618 Some(NamespaceConstraint::Any) => NsConstraint::Any,
619 // '[|foo]' is equivalent to '[foo]': unprefixed
620 // attribute names have no namespace.
621 None => NsConstraint::None,
622 };
623 let attrib = self.attrib_expr(ns, attr.local_name.as_str())?;
624 match attr.operation {
625 ParsedAttrSelectorOperation::Exists => {
626 xpath.add_condition(&attrib);
627 Ok(())
628 }
629 ParsedAttrSelectorOperation::WithValue {
630 operator,
631 case_sensitivity,
632 ref value,
633 } => {
634 let (attrib, value) =
635 self.apply_case_flag(attrib, value.as_str(), case_sensitivity);
636 attrib_operator(xpath, &attrib, operator, &value)
637 }
638 }
639 }
640 unsupported => Err(Error::unsupported(describe_component(unsupported))),
641 }
642 }
643
644 /// `:has()`: each argument is a relative selector whose optional
645 /// leading combinator scopes the match (`>` child, `~` subsequent
646 /// sibling, `+` next sibling; omitted means descendant). Unlike the
647 /// other functional pseudo-classes, `:has()` looks forward, so a
648 /// complex argument extends the existence-test path step by step,
649 /// leftmost compound first.
650 fn apply_has(
651 &self,
652 xpath: &mut XPathExpr,
653 relatives: &[RelativeSelector<CssToXpathImpl>],
654 of_depth: usize,
655 ) -> Result<(), Error> {
656 let mut conditions: Vec<String> = Vec::new();
657 for relative in relatives.iter() {
658 let seqs = collect_seqs(&relative.selector);
659 // The leftmost sequence is the anchor (the candidate element
660 // itself); its combinator slot carries the argument's leading
661 // combinator.
662 let anchor = &seqs[seqs.len() - 1].0;
663 let anchor_only = seqs.len() >= 2
664 && anchor.len() == 1
665 && matches!(anchor[0], Component::RelativeSelectorAnchor);
666 if !anchor_only {
667 return Err(Error::unsupported(
668 "an unexpected selector structure inside `:has()`",
669 ));
670 }
671 let mut test = String::new();
672 for i in (0..seqs.len() - 1).rev() {
673 let first = i == seqs.len() - 2;
674 let combinator = seqs[i].1;
675 // The first step is an axis from the candidate element;
676 // later steps join onto the path.
677 let axis = match (first, combinator) {
678 (true, Some(Combinator::Descendant)) => ".//",
679 (true, Some(Combinator::Child)) => "child::",
680 (true, Some(Combinator::NextSibling) | Some(Combinator::LaterSibling)) => {
681 "following-sibling::"
682 }
683 (false, Some(Combinator::Descendant)) => "//",
684 (false, Some(Combinator::Child)) => "/",
685 (false, Some(Combinator::NextSibling) | Some(Combinator::LaterSibling)) => {
686 "/following-sibling::"
687 }
688 (_, other) => {
689 return Err(Error::unsupported(format!(
690 "an unexpected combinator ({other:?}) inside `:has()`"
691 )));
692 }
693 };
694 let mut sub = self.compound_to_xpath(&seqs[i].0, of_depth)?;
695 // The name stays in the node test (`.//p`, `.//svg:g`) so
696 // it means exactly what it means at the top level and a
697 // prefix resolves through the namespace map — except under
698 // `+`, where the [1] position predicate has to count every
699 // sibling, so the node test must stay `*`.
700 if matches!(combinator, Some(Combinator::NextSibling)) {
701 sub.take_element_into_self_test();
702 // Only the immediately following sibling: constrain
703 // position before applying the match conditions.
704 sub.add_predicate("1");
705 }
706 test.push_str(axis);
707 test.push_str(&sub.render());
708 }
709 conditions.push(test);
710 }
711 // A `:has()` list of several arguments renders as a union, which
712 // binds tighter than `and` in XPath 1.0 and so needs no
713 // parentheses — but reads as though it might, so it is marked an
714 // or-group and parenthesized wherever an or-group would be.
715 match conditions.len() {
716 0 => {}
717 1 => xpath.add_condition(&conditions[0]),
718 _ => xpath.add_or_condition(&conditions.join(" | ")),
719 }
720 Ok(())
721 }
722
723 /// Whether an attribute-value comparison is case-sensitive, and the
724 /// resulting comparison pair.
725 ///
726 /// Selectors 4 leaves attribute-value case sensitivity to the document
727 /// language unless a flag overrides it, and HTML makes a fixed list of
728 /// attributes (`type`, `rel`, `dir`, `checked`, ... — the presentational
729 /// and enumerated legacy ones) ASCII case-insensitive on HTML elements in
730 /// HTML documents. Servo's parser does that classification for us and
731 /// hands back `AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument`, already
732 /// restricted to unflagged, un-namespaced attributes. The other half of
733 /// that variant's condition — that the element is in the HTML namespace —
734 /// is not checkable from a selector, but it holds wherever the flag does:
735 /// an HTML parser puts every element in one document, without namespaces.
736 ///
737 /// Folding means comparing the ASCII-lowercased attribute (via XPath
738 /// `translate()`) against the ASCII-lowercased value. An empty value needs
739 /// no lowercasing, and skipping it keeps the existence tests exact.
740 fn apply_case_flag(
741 &self,
742 attrib: String,
743 value: &str,
744 case_sensitivity: ParsedCaseSensitivity,
745 ) -> (String, String) {
746 let fold = match case_sensitivity {
747 // `[attr="value" i]`.
748 ParsedCaseSensitivity::AsciiCaseInsensitive => true,
749 // No flag on one of HTML's case-insensitive attributes: it
750 // folds only where the document is HTML. `Mode::Xhtml` is XML,
751 // where these attributes are case-sensitive like any other.
752 ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {
753 self.html_document()
754 }
755 // `[attr="value" s]`, and the case-sensitive no-flag default.
756 ParsedCaseSensitivity::ExplicitCaseSensitive | ParsedCaseSensitivity::CaseSensitive => {
757 false
758 }
759 };
760 if fold && !value.is_empty() {
761 (xpath_expr::ascii_lower(&attrib), value.to_ascii_lowercase())
762 } else {
763 (attrib, value.to_owned())
764 }
765 }
766
767 /// Attribute-name handling: ASCII-lowercase (html), safety check, namespace
768 /// qualification. Prefixes are checked too, as in `xpath_element`,
769 /// but against the `NCName` production rather than the local name's
770 /// stricter test: a prefix that cannot be a node test at all errors.
771 fn attrib_expr(&self, ns: NsConstraint, local_name: &str) -> Result<String, Error> {
772 let name = if self.lower_case_attribute_names() {
773 local_name.to_ascii_lowercase()
774 } else {
775 local_name.to_owned()
776 };
777 let safe = is_safe_name(&name);
778 match ns {
779 NsConstraint::Any => {
780 // '[*|attr]': 'attr' in any namespace, including none. An
781 // unprefixed XPath attribute test only matches attributes
782 // with no namespace, so test against local-name() instead.
783 Ok(format!(
784 "@*[local-name() = {}]",
785 xpath_expr::xpath_literal(&name)
786 ))
787 }
788 NsConstraint::Prefix(prefix) if !is_ncname(prefix) => Err(unsafe_prefix_error(prefix)),
789 NsConstraint::Prefix(prefix) if !safe => {
790 // As in `xpath_element`: the prefix stays in the node test
791 // so it resolves through the caller's namespace map, and
792 // only the local part is compared.
793 Ok(format!(
794 "@{prefix}:*[local-name() = {}]",
795 xpath_expr::xpath_literal(&name)
796 ))
797 }
798 NsConstraint::Prefix(prefix) => Ok(format!("@{prefix}:{name}")),
799 NsConstraint::None | NsConstraint::ExplicitNone => Ok(if safe {
800 format!("@{name}")
801 } else {
802 format!(
803 "attribute::*[name() = {}]",
804 xpath_expr::xpath_literal(&name)
805 )
806 }),
807 }
808 }
809
810 /// Harvest the conditions of a pseudo-class argument list, the shared
811 /// pattern of :not()/:is()/:where() and the nth `of S` handling:
812 /// translate each argument into a condition on the candidate element.
813 ///
814 /// Returns `None` when any argument matches everything (e.g. `*`): the
815 /// OR of the list is then trivially true, so callers must not constrain
816 /// on the remaining arguments.
817 fn arg_conditions(
818 &self,
819 selectors: &[Selector<CssToXpathImpl>],
820 context: &str,
821 of_depth: usize,
822 ) -> Result<Option<Vec<Condition>>, Error> {
823 let mut conditions = Vec::new();
824 let mut trivially_true = false;
825 for selector in selectors {
826 let seqs = collect_seqs(selector);
827 match self.argument_condition(&seqs, context, of_depth)? {
828 None => trivially_true = true,
829 Some(condition) => conditions.push(condition),
830 }
831 }
832 Ok(if trivially_true {
833 None
834 } else {
835 Some(conditions)
836 })
837 }
838
839 /// The condition imposed on the candidate element by the whole
840 /// argument chain. The compound's element becomes a `self::` node
841 /// test, which tests exactly what the name would have tested as the
842 /// node test of a top-level selector: `:is(p)` constrains the same
843 /// elements as `p`, and a prefix still resolves through the caller's
844 /// namespace map. A complex argument applies its rightmost
845 /// compound to the candidate, with everything to its left becoming an
846 /// existence test through reversed axes:
847 /// `:is(a > b ~ c)` matches a `c` with a preceding sibling `b` whose
848 /// parent is an `a`.
849 ///
850 /// The chain is walked twice rather than recursed over, so its length
851 /// costs no stack: once left-to-right to translate each compound and
852 /// pick its reversed axis, then once right-to-left (leftmost compound
853 /// first) to wrap each condition inside the one to its right.
854 ///
855 /// `None` means the chain imposes no condition (a bare `*` argument).
856 fn argument_condition(
857 &self,
858 seqs: &[(Vec<&Component<CssToXpathImpl>>, Option<Combinator>)],
859 context: &str,
860 of_depth: usize,
861 ) -> Result<Option<Condition>, Error> {
862 let mut subs: Vec<XPathExpr> = Vec::with_capacity(seqs.len());
863 // `axes[i]` points back at where the left-hand side of `seqs[i]`'s
864 // combinator must be, relative to the element matched by
865 // `seqs[i]`. The leftmost compound has nothing to its left, so
866 // there is one fewer axis than compound.
867 let mut axes: Vec<&str> = Vec::with_capacity(seqs.len().saturating_sub(1));
868 for (idx, (compound, combinator)) in seqs.iter().enumerate() {
869 let mut sub = self.compound_to_xpath(compound, of_depth)?;
870 sub.take_element_into_self_test();
871 subs.push(sub);
872 if idx + 1 < seqs.len() {
873 axes.push(match combinator {
874 Some(Combinator::Descendant) => "ancestor::*",
875 Some(Combinator::Child) => "parent::*",
876 Some(Combinator::LaterSibling) => "preceding-sibling::*",
877 Some(Combinator::NextSibling) => "preceding-sibling::*[1]",
878 other => {
879 return Err(Error::unsupported(format!(
880 "an unexpected combinator ({other:?}) inside `{context}`"
881 )));
882 }
883 });
884 }
885 }
886
887 // A single compound imposes its own conditions and nothing else.
888 if subs.len() == 1 {
889 return Ok(subs.pop().expect("checked").condition());
890 }
891
892 // The nesting reads outward-in — `c0 and axis0[c1 and axis1[c2]]`
893 // — so write it in that order: each compound emits its own
894 // conditions and opens its axis bracket, and every bracket closes
895 // at the end. Wrapping the other way (nesting the condition built
896 // so far inside the next compound's brackets) would copy the whole
897 // accumulated condition once per compound, making a chain of n
898 // compounds cost O(n^2) bytes.
899 let innermost = subs.last().expect("more than one compound").condition();
900 let mut expr = String::new();
901 let mut open = 0usize;
902 for (idx, (sub, axis)) in subs[..subs.len() - 1].iter().zip(&axes).enumerate() {
903 // This compound's own conditions come first, conjoined with
904 // the existence test that follows; a lone or-group is
905 // parenthesized here because `and` binds tighter than `or`.
906 if let Some(condition) = sub.condition() {
907 if condition.or_group {
908 expr.push('(');
909 expr.push_str(&condition.expr);
910 expr.push(')');
911 } else {
912 expr.push_str(&condition.expr);
913 }
914 expr.push_str(" and ");
915 }
916 expr.push_str(axis);
917 // The bracket is only opened when something goes inside it:
918 // the innermost compound may impose no condition at all (a
919 // bare `*`), leaving the axis as a plain existence test.
920 if idx + 2 < subs.len() || innermost.is_some() {
921 expr.push('[');
922 open += 1;
923 }
924 }
925 // The innermost condition sits inside brackets, so a top-level
926 // `or` needs no parentheses of its own.
927 if let Some(condition) = &innermost {
928 expr.push_str(&condition.expr);
929 }
930 for _ in 0..open {
931 expr.push(']');
932 }
933 Ok(Some(Condition {
934 expr,
935 // Every compound but the innermost contributes an existence
936 // test conjoined at the top level, so the result is an `and`.
937 or_group: false,
938 }))
939 }
940}
941
942/// Join two compound translations with a combinator.
943fn apply_combinator(
944 combinator: Combinator,
945 mut left: XPathExpr,
946 right: &XPathExpr,
947) -> Result<XPathExpr, Error> {
948 match combinator {
949 Combinator::Descendant => left.join("//", right),
950 Combinator::Child => left.join("/", right),
951 Combinator::LaterSibling => left.join("/following-sibling::", right),
952 Combinator::NextSibling => {
953 left.join("/following-sibling::", right);
954 // The node test moves into a self:: predicate so the [1]
955 // position test counts every sibling, not only same-name
956 // ones: *[1][self::element][existing conditions]. A `*`
957 // node test already counts every sibling, so it needs no
958 // predicate — `self::*` would test nothing.
959 let target_element = std::mem::replace(&mut left.element, "*".to_owned());
960 left.add_predicate("1");
961 if target_element != "*" {
962 left.add_predicate(&format!("self::{target_element}"));
963 }
964 }
965 // PseudoElement / SlotAssignment / Part combinators can never be
966 // produced: the corresponding parser hooks are disabled.
967 other => {
968 return Err(Error::unsupported(format!("the {other:?} combinator")));
969 }
970 }
971 Ok(left)
972}
973
974/// Collect a selector's compound sequences in match order: `seqs[i]` is
975/// (compound, combinator between this compound and the one to its left),
976/// so `seqs[0]` is the rightmost compound and only the last entry's
977/// combinator is `None`.
978fn collect_seqs(
979 selector: &Selector<CssToXpathImpl>,
980) -> Vec<(Vec<&Component<CssToXpathImpl>>, Option<Combinator>)> {
981 let mut iter = selector.iter();
982 let mut seqs: Vec<(Vec<&Component<CssToXpathImpl>>, Option<Combinator>)> = Vec::new();
983 loop {
984 let compound: Vec<&Component<CssToXpathImpl>> = (&mut iter).collect();
985 let combinator = iter.next_sequence();
986 let done = combinator.is_none();
987 seqs.push((compound, combinator));
988 if done {
989 break;
990 }
991 }
992 seqs
993}
994
995/// A namespace prefix that is not an XML `NCName` (see [`ncname`]) cannot
996/// appear in a node test, and XPath 1.0 offers no way to resolve it
997/// without the namespace URI, which this crate never sees. Comparing the
998/// whole `prefix:name` against `name()` instead would match only
999/// documents that happen to use that very prefix, so such a prefix errors
1000/// rather than approximating.
1001fn unsafe_prefix_error(prefix: &str) -> Error {
1002 Error::unsupported(format!(
1003 "a namespace prefix that needs quoting (`{prefix}`)"
1004 ))
1005}
1006
1007/// Human-readable construct names for unsupported-error messages.
1008fn describe_component(component: &Component<CssToXpathImpl>) -> String {
1009 match component {
1010 // Top-level :scope is handled (or rejected) in selector_to_xpath,
1011 // so reaching this arm means :scope sits inside a functional
1012 // pseudo-class argument, where the context node is unreachable.
1013 // A written one is rejected by the pre-parse scan first; this is
1014 // the backstop for Servo's own `ImplicitScope`.
1015 Component::Scope | Component::ImplicitScope => {
1016 "the `:scope` pseudo-class inside a functional pseudo-class".into()
1017 }
1018 Component::Slotted(..) => "the `::slotted()` pseudo-element".into(),
1019 Component::Part(..) => "the `::part()` pseudo-element".into(),
1020 // Also reached only as a backstop: `:host(...)`, the one form
1021 // Servo parses without the shadow-DOM hooks this crate leaves
1022 // off, is rejected by the scan with a position.
1023 Component::Host(..) => "the `:host` pseudo-class".into(),
1024 // Unreachable in practice: the pre-parse scan rejects a `&`
1025 // before Servo sees it, and nesting is not enabled anyway.
1026 // Worded as the scan words it, so the two cannot diverge.
1027 Component::ParentSelector => "the `&` nesting selector".into(),
1028 // PseudoElement carries an uninhabited type and the remaining
1029 // variants require parser features this crate never enables; they
1030 // are unreachable, but erroring beats panicking: the caller's
1031 // profile is the caller's to choose, and `panic = abort` there
1032 // would tear down its process.
1033 other => format!("an unexpected construct ({other:?})"),
1034 }
1035}