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 parts: Vec<String> = Vec::new();
for sel in list.slice() {
parts.push(self.selector_to_xpath(sel, prefix)?);
}
Ok(parts.join(" | "))
}
fn selector_to_xpath(
&self,
selector: &Selector<CssToXpathImpl>,
prefix: &str,
) -> Result<String, Error> {
let seqs = collect_seqs(selector);
let leftmost = seqs.len() - 1;
for (compound, _) in &seqs[..leftmost] {
if compound.iter().any(|c| matches!(c, Component::Scope)) {
return Err(Error::unsupported(
"the `:scope` pseudo-class outside the leftmost compound",
));
}
}
let scope_anchored = seqs[leftmost]
.0
.iter()
.any(|c| matches!(c, Component::Scope));
let mut xpath = if scope_anchored {
let compound: Vec<&Component<CssToXpathImpl>> = seqs[leftmost]
.0
.iter()
.filter(|c| !matches!(c, Component::Scope))
.copied()
.collect();
let mut xp = self.compound_to_xpath(&compound, 0)?;
xp.path = "self::".to_owned();
xp
} else {
self.compound_to_xpath(&seqs[leftmost].0, 0)?
};
for i in (0..leftmost).rev() {
let combinator = seqs[i]
.1
.ok_or_else(|| Error::unsupported("an unexpected selector structure"))?;
let right = self.compound_to_xpath(&seqs[i].0, 0)?;
xpath = apply_combinator(combinator, xpath, &right)?;
}
let prefix = if scope_anchored { "" } else { prefix };
Ok(format!("{prefix}{}", xpath.render()))
}
fn compound_to_xpath(
&self,
components: &[&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 (mut name, safe) = match element {
None => ("*".to_owned(), true),
Some(e) => {
let safe = is_safe_name(e);
let e = if self.lower_case_element_names() {
e.to_ascii_lowercase()
} else {
e.to_owned()
};
(e, safe)
}
};
match ns {
NsConstraint::Any if name != "*" => {
let cond = format!("local-name() = {}", xpath_expr::xpath_literal(&name));
let mut xpath = XPathExpr::new("*");
xpath.name_test = Some(format!("*[{cond}]"));
xpath.local_name = Some(name);
xpath.add_condition(&cond);
return Ok(xpath);
}
NsConstraint::ExplicitNone if name == "*" => {
let mut xpath = XPathExpr::new("*");
xpath.add_condition("namespace-uri() = ''");
return 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.local_name = Some(name);
xpath.add_condition(&cond);
xpath.add_condition("namespace-uri() = ''");
return Ok(xpath);
}
NsConstraint::Prefix(prefix) if !is_ncname(prefix) => {
return 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.local_name = Some(name);
xpath.add_condition(&cond);
return Ok(xpath);
}
NsConstraint::Prefix(prefix) => {
name = format!("{prefix}:{name}");
}
_ => {}
}
Ok(XPathExpr::new(&name))
}
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(|conditions| Condition::join_or(&conditions));
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 conditions: Vec<String> = Vec::new();
for relative in relatives.iter() {
let seqs = collect_seqs(&relative.selector);
let anchor = &seqs[seqs.len() - 1].0;
let anchor_only = seqs.len() >= 2
&& anchor.len() == 1
&& matches!(anchor[0], Component::RelativeSelectorAnchor);
if !anchor_only {
return Err(Error::unsupported(
"an unexpected selector structure inside `:has()`",
));
}
let mut test = String::new();
for i in (0..seqs.len() - 1).rev() {
let first = i == seqs.len() - 2;
let combinator = seqs[i].1;
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(&seqs[i].0, of_depth)?;
if matches!(combinator, Some(Combinator::NextSibling)) {
sub.take_element_into_self_test();
sub.add_predicate("1");
}
test.push_str(axis);
test.push_str(&sub.render());
}
conditions.push(test);
}
match conditions.len() {
0 => {}
1 => xpath.add_condition(&conditions[0]),
_ => xpath.add_or_condition(&conditions.join(" | ")),
}
Ok(())
}
fn apply_case_flag(
&self,
attrib: String,
value: &str,
case_sensitivity: ParsedCaseSensitivity,
) -> (String, String) {
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), value.to_ascii_lowercase())
} else {
(attrib, value.to_owned())
}
}
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 {
let seqs = collect_seqs(selector);
match self.argument_condition(&seqs, context, of_depth)? {
None => trivially_true = true,
Some(condition) => conditions.push(condition),
}
}
Ok(if trivially_true {
None
} else {
Some(conditions)
})
}
fn argument_condition(
&self,
seqs: &[(Vec<&Component<CssToXpathImpl>>, Option<Combinator>)],
context: &str,
of_depth: usize,
) -> Result<Option<Condition>, Error> {
let mut subs: Vec<XPathExpr> = Vec::with_capacity(seqs.len());
let mut axes: Vec<&str> = Vec::with_capacity(seqs.len().saturating_sub(1));
for (idx, (compound, combinator)) in seqs.iter().enumerate() {
let mut sub = self.compound_to_xpath(compound, of_depth)?;
sub.take_element_into_self_test();
subs.push(sub);
if idx + 1 < seqs.len() {
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 subs.len() == 1 {
return Ok(subs.pop().expect("checked").condition());
}
let innermost = subs.last().expect("more than one compound").condition();
let mut expr = String::new();
let mut open = 0usize;
for (idx, (sub, axis)) in subs[..subs.len() - 1].iter().zip(&axes).enumerate() {
if let Some(condition) = sub.condition() {
if condition.or_group {
expr.push('(');
expr.push_str(&condition.expr);
expr.push(')');
} else {
expr.push_str(&condition.expr);
}
expr.push_str(" and ");
}
expr.push_str(axis);
if idx + 2 < subs.len() || innermost.is_some() {
expr.push('[');
open += 1;
}
}
if let Some(condition) = &innermost {
expr.push_str(&condition.expr);
}
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 = std::mem::replace(&mut left.element, "*".to_owned());
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 collect_seqs(
selector: &Selector<CssToXpathImpl>,
) -> Vec<(Vec<&Component<CssToXpathImpl>>, Option<Combinator>)> {
let mut iter = selector.iter();
let mut seqs: Vec<(Vec<&Component<CssToXpathImpl>>, Option<Combinator>)> = Vec::new();
loop {
let compound: Vec<&Component<CssToXpathImpl>> = (&mut iter).collect();
let combinator = iter.next_sequence();
let done = combinator.is_none();
seqs.push((compound, combinator));
if done {
break;
}
}
seqs
}
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:?})"),
}
}