pub(crate) mod error;
mod generic;
mod ncname;
mod nth;
mod pseudo;
mod xpath_expr;
pub use error::{Error, ParseErrorKind};
pub use nth::{MAX_NTH_OF_BYTES, MAX_NTH_OF_DEPTH};
use std::borrow::Cow;
use std::fmt;
use selectors::attr::{NamespaceConstraint, ParsedAttrSelectorOperation, ParsedCaseSensitivity};
use selectors::parser::{Combinator, Component, RelativeSelector, Selector};
use crate::parser::{self, CssToXpathImpl};
use generic::{attrib_equals, attrib_includes, attrib_operator};
use ncname::is_ncname;
use pseudo::LangSource;
use xpath_expr::{Condition, XPathExpr, is_safe_name};
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Kind {
Generic,
Html,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub enum Mode {
#[default]
Generic,
Html,
Xhtml,
}
impl Mode {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Mode::Generic => "generic",
Mode::Html => "html",
Mode::Xhtml => "xhtml",
}
}
}
impl fmt::Display for Mode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for Mode {
type Err = ParseModeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
for mode in [Mode::Generic, Mode::Html, Mode::Xhtml] {
if s.eq_ignore_ascii_case(mode.as_str()) {
return Ok(mode);
}
}
Err(ParseModeError)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ParseModeError;
impl fmt::Display for ParseModeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("expected one of `generic`, `html` or `xhtml`")
}
}
impl std::error::Error for ParseModeError {}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct Translator {
mode: Mode,
default_namespace: Option<Cow<'static, str>>,
}
#[derive(Clone, Copy)]
enum NsConstraint<'a> {
None,
Any,
ExplicitNone,
Prefix(&'a str),
}
impl Translator {
#[must_use]
pub const fn new(mode: Mode) -> Self {
Translator {
mode,
default_namespace: None,
}
}
#[must_use]
pub fn with_default_namespace_prefix(mut self, prefix: impl Into<Cow<'static, str>>) -> Self {
self.default_namespace = Some(prefix.into());
self
}
#[must_use]
pub const fn mode(&self) -> Mode {
self.mode
}
#[must_use]
pub fn default_namespace_prefix(&self) -> Option<&str> {
self.default_namespace.as_deref()
}
pub(crate) const fn kind(&self) -> Kind {
match self.mode {
Mode::Generic => Kind::Generic,
Mode::Html | Mode::Xhtml => Kind::Html,
}
}
pub(crate) const fn lower_case_element_names(&self) -> bool {
matches!(self.mode, Mode::Html)
}
pub(crate) const fn lower_case_attribute_names(&self) -> bool {
matches!(self.mode, Mode::Html)
}
pub(crate) const fn html_document(&self) -> bool {
matches!(self.mode, Mode::Html)
}
pub(crate) const fn lang_source(&self) -> LangSource {
match self.mode {
Mode::Generic => LangSource::XmlLang,
Mode::Html => LangSource::Lang,
Mode::Xhtml => LangSource::Both,
}
}
pub fn css_to_xpath(&self, css: &str, prefix: &str) -> Result<String, Error> {
let list = parser::parse(css, self.default_namespace_prefix())?;
let mut out = String::new();
for (i, sel) in list.slice().iter().enumerate() {
if i > 0 {
out.push_str(" | ");
}
self.selector_to_xpath(sel, prefix, &mut out)?;
}
Ok(out)
}
fn selector_to_xpath(
&self,
selector: &Selector<CssToXpathImpl>,
prefix: &str,
out: &mut String,
) -> Result<(), Error> {
let mut compounds = compounds_left_to_right(selector);
let (leftmost, _) = compounds
.next()
.expect("a selector has at least one compound");
let raw = selector.iter_raw_match_order().as_slice();
let right_of_leftmost = &raw[..raw.len() - leftmost.len()];
if right_of_leftmost
.iter()
.any(|c| matches!(c, Component::Scope))
{
return Err(Error::unsupported(
"the `:scope` pseudo-class outside the leftmost compound",
));
}
let scope_anchored = leftmost.iter().any(|c| matches!(c, Component::Scope));
let mut xpath = self.compound_to_xpath(
leftmost.iter().filter(|c| !matches!(c, Component::Scope)),
0,
)?;
let mut path = std::mem::take(out);
path.push_str(if scope_anchored { "self::" } else { prefix });
xpath.path = path;
for (compound, combinator) in compounds {
let combinator =
combinator.ok_or_else(|| Error::unsupported("an unexpected selector structure"))?;
let right = self.compound_to_xpath(compound, 0)?;
xpath = apply_combinator(combinator, xpath, right)?;
}
xpath.render_into(out);
Ok(())
}
fn compound_to_xpath<'c>(
&self,
components: impl IntoIterator<Item = &'c Component<CssToXpathImpl>>,
of_depth: usize,
) -> Result<XPathExpr, Error> {
let mut ns = NsConstraint::None;
let mut element: Option<&str> = None;
let mut xpath: Option<XPathExpr> = None;
for component in components {
match component {
Component::Namespace(prefix, _) if xpath.is_none() => {
ns = NsConstraint::Prefix(prefix.as_str());
}
Component::DefaultNamespace(prefix) if xpath.is_none() => {
ns = match prefix.as_str() {
"" => NsConstraint::None,
prefix => NsConstraint::Prefix(prefix),
};
}
Component::ExplicitAnyNamespace if xpath.is_none() => {
ns = NsConstraint::Any;
}
Component::ExplicitNoNamespace if xpath.is_none() => {
ns = NsConstraint::ExplicitNone;
}
Component::ExplicitUniversalType if xpath.is_none() => {}
Component::LocalName(local_name) if xpath.is_none() => {
element = Some(local_name.name.as_str());
}
other => {
let xp = match xpath {
Some(ref mut xp) => xp,
None => {
xpath = Some(self.xpath_element(ns, element)?);
xpath.as_mut().expect("just set")
}
};
self.apply_simple(xp, other, of_depth)?;
}
}
}
Ok(match xpath {
Some(xp) => xp,
None => self.xpath_element(ns, element)?,
})
}
fn xpath_element(&self, ns: NsConstraint, element: Option<&str>) -> Result<XPathExpr, Error> {
let Some(element) = element else {
return Ok(match ns {
NsConstraint::ExplicitNone => {
let mut xpath = XPathExpr::new("*");
xpath.add_condition("namespace-uri() = ''");
xpath
}
NsConstraint::Prefix(prefix) if !is_ncname(prefix) => {
return Err(unsafe_prefix_error(prefix));
}
NsConstraint::Prefix(prefix) => XPathExpr::new(format!("{prefix}:*")),
NsConstraint::None | NsConstraint::Any => XPathExpr::new("*"),
});
};
let safe = is_safe_name(element);
let name =
if self.lower_case_element_names() && element.bytes().any(|b| b.is_ascii_uppercase()) {
Cow::Owned(element.to_ascii_lowercase())
} else {
Cow::Borrowed(element)
};
match ns {
NsConstraint::Any => {
let cond = format!("local-name() = {}", xpath_expr::xpath_literal(&name));
let mut xpath = XPathExpr::new("*");
xpath.name_test = Some(format!("*[{cond}]"));
xpath.folded_name = Some(name.into_owned());
xpath.add_condition(cond);
Ok(xpath)
}
NsConstraint::None | NsConstraint::ExplicitNone if !safe => {
let cond = format!("name() = {}", xpath_expr::xpath_literal(&name));
let mut xpath = XPathExpr::new("*");
xpath.name_test = Some(format!("*[{cond} and namespace-uri() = '']"));
xpath.folded_name = Some(name.into_owned());
xpath.add_condition(cond);
xpath.add_condition("namespace-uri() = ''");
Ok(xpath)
}
NsConstraint::Prefix(prefix) if !is_ncname(prefix) => Err(unsafe_prefix_error(prefix)),
NsConstraint::Prefix(prefix) if !safe => {
let cond = format!("local-name() = {}", xpath_expr::xpath_literal(&name));
let mut xpath = XPathExpr::new(format!("{prefix}:*"));
xpath.name_test = Some(format!("{prefix}:*[{cond}]"));
xpath.folded_name = Some(name.into_owned());
xpath.add_condition(cond);
Ok(xpath)
}
NsConstraint::Prefix(prefix) => Ok(XPathExpr::new(format!("{prefix}:{name}"))),
NsConstraint::None | NsConstraint::ExplicitNone => {
Ok(XPathExpr::new(name.into_owned()))
}
}
}
fn apply_simple(
&self,
xpath: &mut XPathExpr,
component: &Component<CssToXpathImpl>,
of_depth: usize,
) -> Result<(), Error> {
match component {
Component::Root => {
xpath.add_condition("not(parent::*)");
Ok(())
}
Component::Empty => {
xpath.add_condition("not(*) and not(string-length())");
Ok(())
}
Component::Nth(data) => self.apply_nth(xpath, data, None, of_depth),
Component::NthOf(data) => {
self.apply_nth(xpath, data.nth_data(), Some(data.selectors()), of_depth)
}
Component::Negation(list) => {
let joined = self
.arg_conditions(list.slice(), ":not()", of_depth)?
.and_then(Condition::join_or);
match joined {
Some(joined) => xpath.add_condition(format!("not({})", joined.expr)),
None => xpath.add_condition("0"),
}
Ok(())
}
Component::Is(list) | Component::Where(list) => {
if parser::is_empty_forgiving_list(list.slice()) {
xpath.add_condition("0");
return Ok(());
}
let context = match component {
Component::Is(_) => ":is()",
_ => ":where()",
};
if let Some(conditions) = self.arg_conditions(list.slice(), context, of_depth)?
&& let Some(joined) = Condition::join_or(conditions)
{
xpath.push_condition(joined);
}
Ok(())
}
Component::Has(relatives) => self.apply_has(xpath, relatives, of_depth),
Component::NonTSPseudoClass(pc) => self.apply_pseudo_class(xpath, pc),
Component::ID(id) => {
attrib_equals(xpath, "@id", id.as_str());
Ok(())
}
Component::Class(class_name) => {
attrib_includes(xpath, "@class", class_name.as_str());
Ok(())
}
Component::AttributeInNoNamespaceExists { local_name, .. } => {
let attrib = self.attrib_expr(NsConstraint::None, local_name.as_str())?;
xpath.add_condition(attrib);
Ok(())
}
Component::AttributeInNoNamespace {
local_name,
operator,
value,
case_sensitivity,
} => {
let attrib = self.attrib_expr(NsConstraint::None, local_name.as_str())?;
let (attrib, value) =
self.apply_case_flag(attrib, value.as_str(), *case_sensitivity);
attrib_operator(xpath, &attrib, *operator, &value)
}
Component::AttributeOther(attr) => {
let ns = match attr.namespace {
Some(NamespaceConstraint::Specific((ref prefix, _))) => {
NsConstraint::Prefix(prefix.as_str())
}
Some(NamespaceConstraint::Any) => NsConstraint::Any,
None => NsConstraint::None,
};
let attrib = self.attrib_expr(ns, attr.local_name.as_str())?;
match attr.operation {
ParsedAttrSelectorOperation::Exists => {
xpath.add_condition(attrib);
Ok(())
}
ParsedAttrSelectorOperation::WithValue {
operator,
case_sensitivity,
ref value,
} => {
let (attrib, value) =
self.apply_case_flag(attrib, value.as_str(), case_sensitivity);
attrib_operator(xpath, &attrib, operator, &value)
}
}
}
unsupported => Err(Error::unsupported(describe_component(unsupported))),
}
}
fn apply_has(
&self,
xpath: &mut XPathExpr,
relatives: &[RelativeSelector<CssToXpathImpl>],
of_depth: usize,
) -> Result<(), Error> {
let mut union = String::new();
for (arg, relative) in relatives.iter().enumerate() {
let mut compounds = compounds_left_to_right(&relative.selector).peekable();
let anchor_only = matches!(
compounds.next(),
Some(([Component::RelativeSelectorAnchor], _))
);
if !anchor_only || compounds.peek().is_none() {
return Err(Error::unsupported(
"an unexpected selector structure inside `:has()`",
));
}
if arg > 0 {
union.push_str(" | ");
}
for (step, (compound, combinator)) in compounds.enumerate() {
let first = step == 0;
let axis = match (first, combinator) {
(true, Some(Combinator::Descendant)) => ".//",
(true, Some(Combinator::Child)) => "child::",
(true, Some(Combinator::NextSibling) | Some(Combinator::LaterSibling)) => {
"following-sibling::"
}
(false, Some(Combinator::Descendant)) => "//",
(false, Some(Combinator::Child)) => "/",
(false, Some(Combinator::NextSibling) | Some(Combinator::LaterSibling)) => {
"/following-sibling::"
}
(_, other) => {
return Err(Error::unsupported(format!(
"an unexpected combinator ({other:?}) inside `:has()`"
)));
}
};
let mut sub = self.compound_to_xpath(compound, of_depth)?;
if matches!(combinator, Some(Combinator::NextSibling)) {
sub.take_element_into_self_test();
sub.add_predicate("1");
}
union.push_str(axis);
sub.render_into(&mut union);
}
}
match relatives.len() {
0 => {}
1 => xpath.add_condition(union),
_ => xpath.add_or_condition(union),
}
Ok(())
}
fn apply_case_flag<'v>(
&self,
attrib: String,
value: &'v str,
case_sensitivity: ParsedCaseSensitivity,
) -> (String, Cow<'v, str>) {
let fold = match case_sensitivity {
ParsedCaseSensitivity::AsciiCaseInsensitive => true,
ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {
self.html_document()
}
ParsedCaseSensitivity::ExplicitCaseSensitive | ParsedCaseSensitivity::CaseSensitive => {
false
}
};
if fold && !value.is_empty() {
(
xpath_expr::ascii_lower(&attrib),
Cow::Owned(value.to_ascii_lowercase()),
)
} else {
(attrib, Cow::Borrowed(value))
}
}
fn attrib_expr(&self, ns: NsConstraint, local_name: &str) -> Result<String, Error> {
let name = if self.lower_case_attribute_names() {
local_name.to_ascii_lowercase()
} else {
local_name.to_owned()
};
let safe = is_safe_name(&name);
match ns {
NsConstraint::Any => {
Ok(format!(
"@*[local-name() = {}]",
xpath_expr::xpath_literal(&name)
))
}
NsConstraint::Prefix(prefix) if !is_ncname(prefix) => Err(unsafe_prefix_error(prefix)),
NsConstraint::Prefix(prefix) if !safe => {
Ok(format!(
"@{prefix}:*[local-name() = {}]",
xpath_expr::xpath_literal(&name)
))
}
NsConstraint::Prefix(prefix) => Ok(format!("@{prefix}:{name}")),
NsConstraint::None | NsConstraint::ExplicitNone => Ok(if safe {
format!("@{name}")
} else {
format!(
"attribute::*[name() = {}]",
xpath_expr::xpath_literal(&name)
)
}),
}
}
fn arg_conditions(
&self,
selectors: &[Selector<CssToXpathImpl>],
context: &str,
of_depth: usize,
) -> Result<Option<Vec<Condition>>, Error> {
let mut conditions = Vec::new();
let mut trivially_true = false;
for selector in selectors {
match self.argument_condition(selector, context, of_depth)? {
None => trivially_true = true,
Some(condition) => conditions.push(condition),
}
}
Ok(if trivially_true {
None
} else {
Some(conditions)
})
}
fn argument_condition(
&self,
selector: &Selector<CssToXpathImpl>,
context: &str,
of_depth: usize,
) -> Result<Option<Condition>, Error> {
let mut subs: Vec<XPathExpr> = Vec::new();
let mut axes: Vec<&str> = Vec::new();
for (compound, combinator) in compounds_match_order(selector) {
let mut sub = self.compound_to_xpath(compound, of_depth)?;
sub.take_element_into_self_test();
if combinator.is_none() && subs.is_empty() {
return Ok(sub.into_condition());
}
subs.push(sub);
if combinator.is_some() {
axes.push(match combinator {
Some(Combinator::Descendant) => "ancestor::*",
Some(Combinator::Child) => "parent::*",
Some(Combinator::LaterSibling) => "preceding-sibling::*",
Some(Combinator::NextSibling) => "preceding-sibling::*[1]",
other => {
return Err(Error::unsupported(format!(
"an unexpected combinator ({other:?}) inside `{context}`"
)));
}
});
}
}
if let Some(dead) = subs.iter().position(XPathExpr::is_never) {
subs.truncate(dead + 1);
axes.truncate(dead);
}
let innermost = subs.pop().expect("more than one compound");
if subs.is_empty() {
return Ok(innermost.into_condition());
}
let mut expr = String::new();
let mut open = 0usize;
for (idx, (sub, axis)) in subs.iter().zip(&axes).enumerate() {
if sub
.write_condition(&mut expr, true)
.is_some()
{
expr.push_str(" and ");
}
expr.push_str(axis);
if idx + 1 < subs.len() || innermost.has_condition() {
expr.push('[');
open += 1;
}
}
innermost.write_condition(&mut expr, false);
for _ in 0..open {
expr.push(']');
}
Ok(Some(Condition {
expr,
or_group: false,
}))
}
}
fn apply_combinator(
combinator: Combinator,
mut left: XPathExpr,
right: XPathExpr,
) -> Result<XPathExpr, Error> {
match combinator {
Combinator::Descendant => left.join("//", right),
Combinator::Child => left.join("/", right),
Combinator::LaterSibling => left.join("/following-sibling::", right),
Combinator::NextSibling => {
left.join("/following-sibling::", right);
let target_element = left.take_element();
left.add_predicate("1");
if target_element != "*" {
left.add_predicate(format!("self::{target_element}"));
}
}
other => {
return Err(Error::unsupported(format!("the {other:?} combinator")));
}
}
Ok(left)
}
fn compounds_match_order(
selector: &Selector<CssToXpathImpl>,
) -> impl Iterator<Item = (&[Component<CssToXpathImpl>], Option<Combinator>)> {
let raw = selector.iter_raw_match_order().as_slice();
let combinators = raw.iter().filter_map(as_combinator).map(Some);
raw.split(Component::is_combinator)
.zip(combinators.chain(std::iter::once(None)))
}
fn compounds_left_to_right(
selector: &Selector<CssToXpathImpl>,
) -> impl Iterator<Item = (&[Component<CssToXpathImpl>], Option<Combinator>)> {
let raw = selector.iter_raw_match_order().as_slice();
let combinators = raw.iter().rev().filter_map(as_combinator).map(Some);
raw.rsplit(Component::is_combinator)
.zip(std::iter::once(None).chain(combinators))
}
fn as_combinator(component: &Component<CssToXpathImpl>) -> Option<Combinator> {
match component {
Component::Combinator(combinator) => Some(*combinator),
_ => None,
}
}
fn unsafe_prefix_error(prefix: &str) -> Error {
Error::unsupported(format!(
"a namespace prefix that is not an XPath name (`{prefix}`)"
))
}
fn describe_component(component: &Component<CssToXpathImpl>) -> String {
match component {
Component::Scope | Component::ImplicitScope => {
"the `:scope` pseudo-class inside a functional pseudo-class".into()
}
Component::Slotted(..) => "the `::slotted()` pseudo-element".into(),
Component::Part(..) => "the `::part()` pseudo-element".into(),
Component::Host(..) => "the `:host` pseudo-class".into(),
Component::ParentSelector => "the `&` nesting selector".into(),
other => format!("an unexpected construct ({other:?})"),
}
}