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 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}