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) fn xpath_literal(literal: &str) -> String {
if !literal.contains('\'') {
format!("'{literal}'")
} else if !literal.contains('"') {
format!("\"{literal}\"")
} else {
let mut parts: Vec<String> = Vec::new();
let mut rest = literal;
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);
parts.push(format!("{quote}{run}{quote}"));
rest = tail;
}
format!("concat({})", parts.join(","))
}
}
#[derive(Clone, Debug)]
pub(crate) struct Condition {
pub(crate) expr: String,
pub(crate) or_group: bool,
}
impl Condition {
pub(crate) fn join_or(conditions: &[Condition]) -> Option<Condition> {
let mut kept: Vec<&Condition> = Vec::new();
for condition in conditions {
if !kept.iter().any(|k| k.expr == condition.expr) {
kept.push(condition);
}
}
let first = kept.first()?;
let exprs: Vec<&str> = kept.iter().map(|c| c.expr.as_str()).collect();
Some(Condition {
expr: exprs.join(" or "),
or_group: kept.len() > 1 || first.or_group,
})
}
}
#[derive(Clone, Debug)]
pub(crate) struct XPathExpr {
pub(crate) path: String,
pub(crate) element: String,
conditions: Vec<Condition>,
predicates: Vec<String>,
pub(crate) name_test: Option<String>,
pub(crate) local_name: Option<String>,
}
impl XPathExpr {
pub(crate) fn new(element: &str) -> Self {
let local_name = match element {
"*" => None,
_ if element.ends_with(":*") => None,
_ => Some(element.rsplit(':').next().unwrap_or(element).to_owned()),
};
XPathExpr {
path: String::new(),
element: element.to_owned(),
conditions: Vec::new(),
predicates: Vec::new(),
name_test: None,
local_name,
}
}
pub(crate) fn render(&self) -> String {
let mut p = self.path.clone();
self.render_tail(&mut p);
p
}
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 let Some(condition) = self.condition() {
out.push('[');
out.push_str(&condition.expr);
out.push(']');
}
}
pub(crate) fn condition(&self) -> Option<Condition> {
if self.conditions.is_empty() {
return None;
}
if self.conditions.iter().any(|c| c.expr == "0") {
return Some(Condition {
expr: "0".to_owned(),
or_group: false,
});
}
let mut kept: Vec<&Condition> = Vec::new();
for condition in &self.conditions {
if !kept
.iter()
.any(|k| k.expr == condition.expr && k.or_group == condition.or_group)
{
kept.push(condition);
}
}
match kept.len() {
1 => Some(kept[0].clone()),
_ => {
let parts: Vec<String> = kept
.iter()
.map(|c| {
if c.or_group {
format!("({})", c.expr)
} else {
c.expr.clone()
}
})
.collect();
Some(Condition {
expr: parts.join(" and "),
or_group: false,
})
}
}
}
pub(crate) fn add_predicate(&mut self, predicate: &str) {
self.predicates.push(predicate.to_owned());
}
pub(crate) fn add_condition(&mut self, condition: &str) {
self.push_condition(Condition {
expr: condition.to_owned(),
or_group: false,
});
}
pub(crate) fn add_or_condition(&mut self, condition: &str) {
self.push_condition(Condition {
expr: condition.to_owned(),
or_group: true,
});
}
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, "*".to_owned());
self.add_condition(&format!("self::{element}"));
self.name_test.get_or_insert(element);
}
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())
}
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.path = path;
self.element = other.element.clone();
self.conditions = other.conditions.clone();
self.predicates = other.predicates.clone();
self.name_test = other.name_test.clone();
self.local_name = other.local_name.clone();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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(""));
}
#[test]
fn literals() {
assert_eq!(xpath_literal("foo"), "'foo'");
assert_eq!(xpath_literal("f'oo"), "\"f'oo\"");
assert_eq!(xpath_literal("f'o\"o"), "concat('f',\"'\",'o\"o')");
assert_eq!(xpath_literal("it's \"q\""), "concat('it',\"'\",'s \"q\"')");
assert_eq!(xpath_literal("''a\"b"), "concat(\"''\",'a\"b')");
}
#[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]");
}
}