Skip to main content

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}