use std::borrow::Cow;
use std::fmt;
pub(crate) fn is_safe_name(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
if !(first.is_ascii_alphabetic() || first == '_') {
return false;
}
chars.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-'))
}
pub(crate) fn ascii_lower(subject: &str) -> String {
format!(
"translate({subject}, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', \
'abcdefghijklmnopqrstuvwxyz')"
)
}
pub(crate) const fn xpath_literal(text: &str) -> Literal<'_> {
Literal::affixed("", text, "")
}
#[derive(Clone, Copy)]
pub(crate) struct Literal<'a> {
before: &'static str,
text: &'a str,
after: &'static str,
}
impl<'a> Literal<'a> {
pub(crate) const fn affixed(before: &'static str, text: &'a str, after: &'static str) -> Self {
Literal {
before,
text,
after,
}
}
}
impl fmt::Display for Literal<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
debug_assert!(
!(self.before.contains(['\'', '"']) || self.after.contains(['\'', '"'])),
"a literal's affixes never decide its quoting"
);
let Literal {
before,
text,
after,
} = *self;
if !text.contains('\'') {
return write!(f, "'{before}{text}{after}'");
}
if !text.contains('"') {
return write!(f, "\"{before}{text}{after}\"");
}
let whole;
let mut rest = if before.is_empty() && after.is_empty() {
text
} else {
whole = format!("{before}{text}{after}");
&whole
};
f.write_str("concat(")?;
let mut first = true;
while !rest.is_empty() {
let (len, quote) = if rest.starts_with('\'') {
(rest.len() - rest.trim_start_matches('\'').len(), '"')
} else {
(rest.find('\'').unwrap_or(rest.len()), '\'')
};
let (run, tail) = rest.split_at(len);
if !first {
f.write_str(",")?;
}
first = false;
write!(f, "{quote}{run}{quote}")?;
rest = tail;
}
f.write_str(")")
}
}
#[derive(Clone, Debug)]
pub(crate) struct Condition {
pub(crate) expr: String,
pub(crate) or_group: bool,
}
impl Condition {
pub(crate) fn plain(expr: impl Into<String>) -> Self {
Condition {
expr: expr.into(),
or_group: false,
}
}
pub(crate) fn or_group(expr: impl Into<String>) -> Self {
Condition {
expr: expr.into(),
or_group: true,
}
}
fn never() -> Self {
Condition::plain("0")
}
fn is_never(&self) -> bool {
self.expr == "0"
}
pub(crate) fn join_or(mut conditions: Vec<Condition>) -> Option<Condition> {
if conditions.len() == 1 {
return conditions.pop();
}
let mut expr = String::new();
let mut kept = 0usize;
let mut first_or_group = false;
for_each_distinct(
&conditions,
|c| c.expr.as_str(),
|c| {
if kept == 0 {
first_or_group = c.or_group;
} else {
expr.push_str(" or ");
}
expr.push_str(&c.expr);
kept += 1;
},
);
(kept > 0).then_some(Condition {
expr,
or_group: kept > 1 || first_or_group,
})
}
}
const LINEAR_DISTINCT_MAX: usize = 16;
fn for_each_distinct<'a, K>(
conditions: &'a [Condition],
key: impl Fn(&'a Condition) -> K,
mut f: impl FnMut(&'a Condition),
) where
K: Eq + std::hash::Hash,
{
if conditions.len() <= LINEAR_DISTINCT_MAX {
for (i, condition) in conditions.iter().enumerate() {
let k = key(condition);
if !conditions[..i].iter().any(|earlier| key(earlier) == k) {
f(condition);
}
}
} else {
let mut seen = std::collections::HashSet::with_capacity(conditions.len());
for condition in conditions {
if seen.insert(key(condition)) {
f(condition);
}
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct XPathExpr {
pub(crate) path: String,
element: Cow<'static, str>,
conditions: Vec<Condition>,
predicates: Vec<Cow<'static, str>>,
pub(crate) name_test: Option<String>,
pub(crate) folded_name: Option<String>,
}
impl XPathExpr {
pub(crate) fn new(element: impl Into<Cow<'static, str>>) -> Self {
XPathExpr {
path: String::new(),
element: element.into(),
conditions: Vec::new(),
predicates: Vec::new(),
name_test: None,
folded_name: None,
}
}
pub(crate) fn local_name(&self) -> Option<&str> {
if let Some(name) = &self.folded_name {
return Some(name);
}
let node_test = self.name_test.as_deref().unwrap_or(&self.element);
match node_test {
"*" => None,
_ if node_test.ends_with(":*") => None,
_ => node_test.rsplit(':').next(),
}
}
pub(crate) fn render_into(mut self, out: &mut String) {
if out.is_empty() {
*out = std::mem::take(&mut self.path);
} else {
out.push_str(&self.path);
}
self.render_tail(out);
}
fn render_tail(&self, out: &mut String) {
out.push_str(&self.element);
for predicate in &self.predicates {
out.push('[');
out.push_str(predicate);
out.push(']');
}
if !self.conditions.is_empty() {
out.push('[');
self.write_condition(out, false);
out.push(']');
}
}
pub(crate) fn is_never(&self) -> bool {
self.conditions.iter().any(Condition::is_never)
}
pub(crate) fn has_condition(&self) -> bool {
!self.conditions.is_empty()
}
pub(crate) fn write_condition(&self, out: &mut String, as_operand: bool) -> Option<bool> {
if self.conditions.is_empty() {
return None;
}
if self.is_never() {
out.push('0');
return Some(false);
}
out.reserve(
self.conditions
.iter()
.map(|c| c.expr.len() + " and ()".len())
.sum(),
);
let start = out.len();
let mut kept = 0usize;
let mut last_or_group = false;
for_each_distinct(
&self.conditions,
|c| (c.expr.as_str(), c.or_group),
|c| {
if kept > 0 {
out.push_str(" and ");
}
if c.or_group {
out.push('(');
out.push_str(&c.expr);
out.push(')');
} else {
out.push_str(&c.expr);
}
last_or_group = c.or_group;
kept += 1;
},
);
if kept == 1 && last_or_group && !as_operand {
out.remove(start);
out.pop();
return Some(true);
}
Some(false)
}
pub(crate) fn into_condition(mut self) -> Option<Condition> {
if self.is_never() {
return Some(Condition::never());
}
if self.conditions.len() == 1 {
return self.conditions.pop();
}
let mut expr = String::new();
let or_group = self.write_condition(&mut expr, false)?;
Some(Condition { expr, or_group })
}
pub(crate) fn add_predicate(&mut self, predicate: impl Into<Cow<'static, str>>) {
self.predicates.push(predicate.into());
}
pub(crate) fn add_condition(&mut self, condition: impl Into<String>) {
self.push_condition(Condition::plain(condition));
}
pub(crate) fn add_or_condition(&mut self, condition: impl Into<String>) {
self.push_condition(Condition::or_group(condition));
}
pub(crate) fn push_condition(&mut self, condition: Condition) {
self.conditions.push(condition);
}
pub(crate) fn take_element_into_self_test(&mut self) {
if self.element == "*" {
return;
}
let element = std::mem::replace(&mut self.element, Cow::Borrowed("*"));
self.add_condition(format!("self::{element}"));
self.name_test.get_or_insert_with(|| element.into_owned());
}
pub(crate) fn same_type_nodetest(&self) -> Option<String> {
match &self.name_test {
Some(name_test) => Some(name_test.clone()),
None if self.element != "*" && !self.element.ends_with(":*") => {
Some(self.element.clone().into_owned())
}
None => None,
}
}
pub(crate) fn join(&mut self, combiner: &str, other: XPathExpr) {
let mut path = std::mem::take(&mut self.path);
self.render_tail(&mut path);
path.push_str(combiner);
path.push_str(&other.path);
*self = XPathExpr { path, ..other };
}
pub(crate) fn take_element(&mut self) -> Cow<'static, str> {
std::mem::replace(&mut self.element, Cow::Borrowed("*"))
}
}
#[cfg(test)]
mod tests {
use super::*;
impl XPathExpr {
fn render(&self) -> String {
let mut out = String::new();
self.clone().render_into(&mut out);
out
}
}
#[test]
fn safe_names() {
assert!(is_safe_name("div"));
assert!(is_safe_name("_x"));
assert!(is_safe_name("a-b.c_1"));
assert!(!is_safe_name("1a"));
assert!(!is_safe_name("di[v"));
assert!(!is_safe_name("di\u{a0}v"));
assert!(!is_safe_name(""));
}
fn xpath_literal_string(text: &str) -> String {
xpath_literal(text).to_string()
}
#[test]
fn literals() {
assert_eq!(xpath_literal_string("foo"), "'foo'");
assert_eq!(xpath_literal_string("f'oo"), "\"f'oo\"");
assert_eq!(xpath_literal_string("f'o\"o"), "concat('f',\"'\",'o\"o')");
assert_eq!(
xpath_literal_string("it's \"q\""),
"concat('it',\"'\",'s \"q\"')"
);
assert_eq!(xpath_literal_string("''a\"b"), "concat(\"''\",'a\"b')");
assert_eq!(Literal::affixed(" ", "a", " ").to_string(), "' a '");
assert_eq!(Literal::affixed("", "a'b", "-").to_string(), "\"a'b-\"");
assert_eq!(
Literal::affixed(" ", "'a\"", " ").to_string(),
"concat(' ',\"'\",'a\" ')"
);
}
#[test]
fn condition_parens() {
let mut xp = XPathExpr::new("e");
xp.add_condition("@foo = 'bar'");
assert_eq!(xp.render(), "e[@foo = 'bar']");
xp.add_condition("@baz");
assert_eq!(xp.render(), "e[@foo = 'bar' and @baz]");
let mut xp = XPathExpr::new("e");
xp.add_or_condition("@a or @b");
assert_eq!(xp.render(), "e[@a or @b]");
xp.add_condition("@c");
assert_eq!(xp.render(), "e[(@a or @b) and @c]");
}
#[test]
fn never_matching_condition_absorbs_the_conjunction() {
let mut xp = XPathExpr::new("a");
xp.add_condition("@x");
xp.add_condition("0");
xp.add_or_condition("@a or @b");
assert_eq!(xp.render(), "a[0]");
let mut xp = XPathExpr::new("*");
xp.add_predicate("1");
xp.add_condition("0");
assert_eq!(xp.render(), "*[1][0]");
}
#[test]
fn duplicate_conditions_are_kept_once() {
let mut xp = XPathExpr::new("a");
xp.add_condition("@href");
xp.add_condition("@href");
assert_eq!(xp.render(), "a[@href]");
xp.add_condition("@x");
xp.add_condition("@href");
assert_eq!(xp.render(), "a[@href and @x]");
let mut xp = XPathExpr::new("a");
xp.add_or_condition("@a or @b");
xp.add_or_condition("@a or @b");
xp.add_condition("@c");
assert_eq!(xp.render(), "a[(@a or @b) and @c]");
}
#[test]
fn predicates_render_separately_before_condition() {
let mut xp = XPathExpr::new("*");
xp.add_predicate("1");
xp.add_predicate("self::f");
assert_eq!(xp.render(), "*[1][self::f]");
xp.add_condition("@bar");
assert_eq!(xp.render(), "*[1][self::f][@bar]");
let other = XPathExpr::new("g");
xp.join("/following-sibling::", other);
assert_eq!(xp.render(), "*[1][self::f][@bar]/following-sibling::g");
xp.add_predicate("1");
assert_eq!(xp.render(), "*[1][self::f][@bar]/following-sibling::g[1]");
}
}