pub 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 fn xpath_literal(literal: &str) -> String {
if !literal.contains('\'') {
format!("'{literal}'")
} else if !literal.contains('"') {
format!("\"{literal}\"")
} else {
let parts: Vec<String> = literal
.chars()
.map(|c| {
if c == '\'' {
format!("\"{c}\"")
} else {
format!("'{c}'")
}
})
.collect();
format!("concat({})", parts.join(","))
}
}
#[derive(Clone, Debug)]
pub struct Condition {
pub expr: String,
pub or_group: bool,
}
impl Condition {
pub fn join_or(conditions: &[Condition]) -> Condition {
let exprs: Vec<&str> = conditions.iter().map(|c| c.expr.as_str()).collect();
Condition {
expr: exprs.join(" or "),
or_group: conditions.len() > 1 || conditions[0].or_group,
}
}
}
#[derive(Clone, Debug)]
pub struct XPathExpr {
pub path: String,
pub element: String,
conditions: Vec<Condition>,
predicates: Vec<String>,
pub name_test: Option<String>,
}
impl XPathExpr {
pub fn new(element: &str) -> Self {
XPathExpr {
path: String::new(),
element: element.to_owned(),
conditions: Vec::new(),
predicates: Vec::new(),
name_test: None,
}
}
pub fn str(&self) -> String {
let mut p = format!("{}{}", self.path, self.element);
for predicate in &self.predicates {
p.push('[');
p.push_str(predicate);
p.push(']');
}
if let Some(condition) = self.condition() {
p.push('[');
p.push_str(&condition.expr);
p.push(']');
}
p
}
pub fn condition(&self) -> Option<Condition> {
match self.conditions.len() {
0 => None,
1 => Some(self.conditions[0].clone()),
_ => {
let parts: Vec<String> = self
.conditions
.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 fn add_predicate(&mut self, predicate: &str) {
self.predicates.push(predicate.to_owned());
}
pub fn add_condition(&mut self, condition: &str) {
self.push_condition(Condition {
expr: condition.to_owned(),
or_group: false,
});
}
pub fn add_or_condition(&mut self, condition: &str) {
self.push_condition(Condition {
expr: condition.to_owned(),
or_group: true,
});
}
pub fn push_condition(&mut self, condition: Condition) {
self.conditions.push(condition);
}
pub fn add_name_test(&mut self) {
if self.element == "*" {
return;
}
let cond = format!("name() = {}", xpath_literal(&self.element));
self.name_test = Some(format!("*[{cond}]"));
self.add_condition(&cond);
self.element = "*".to_owned();
}
pub fn same_type_nodetest(&self) -> Option<String> {
if self.element != "*" {
Some(self.element.clone())
} else {
self.name_test.clone()
}
}
pub fn join(&mut self, combiner: &str, other: &XPathExpr) {
let mut p = format!("{}{}", self.str(), combiner);
if other.path != "*/" {
p.push_str(&other.path);
}
self.path = p;
self.element = other.element.clone();
self.conditions = other.conditions.clone();
self.predicates = other.predicates.clone();
self.name_test = other.name_test.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')");
}
#[test]
fn condition_parens() {
let mut xp = XPathExpr::new("e");
xp.add_condition("@foo = 'bar'");
assert_eq!(xp.str(), "e[@foo = 'bar']");
xp.add_condition("@baz");
assert_eq!(xp.str(), "e[@foo = 'bar' and @baz]");
let mut xp = XPathExpr::new("e");
xp.add_or_condition("@a or @b");
assert_eq!(xp.str(), "e[@a or @b]");
xp.add_condition("@c");
assert_eq!(xp.str(), "e[(@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.str(), "*[1][self::f]");
xp.add_condition("@bar");
assert_eq!(xp.str(), "*[1][self::f][@bar]");
let other = XPathExpr::new("g");
xp.join("/following-sibling::", &other);
assert_eq!(xp.str(), "*[1][self::f][@bar]/following-sibling::g");
xp.add_predicate("1");
assert_eq!(xp.str(), "*[1][self::f][@bar]/following-sibling::g[1]");
}
}