use sup_xml_tree::dom::{Node, NodeKind};
#[derive(Debug, Clone)]
pub struct Pattern {
branches: Vec<Branch>,
}
#[derive(Debug, Clone)]
struct Branch {
steps: Vec<Step>,
links: Vec<Link>,
absolute: bool,
}
#[derive(Debug, Clone)]
struct Step {
test: Test,
predicates: Vec<Predicate>,
}
#[derive(Debug, Clone)]
enum Test {
Element(String),
AnyElement,
Attribute(String),
AnyAttribute,
}
#[derive(Debug, Clone)]
enum Predicate {
Position(usize),
Last,
AttrExists(String),
AttrEquals(String, String),
}
#[derive(Debug, Clone, Copy)]
enum Link {
Parent,
Ancestor,
}
impl Pattern {
pub fn compile(src: &str) -> Result<Self, String> {
let mut p = Parser::new(src);
let pat = p.parse_pattern()?;
p.skip_ws();
if !p.at_eof() {
return Err(format!("unexpected trailing input near {:?}", &p.src[p.pos..]));
}
Ok(pat)
}
pub fn matches(&self, node: &Node<'_>) -> bool {
self.branches.iter().any(|b| b.matches(node))
}
pub fn matches_attribute(&self, attr_name: &str, parent: Option<&Node<'_>>) -> bool {
self.branches.iter().any(|b| b.matches_attribute(attr_name, parent))
}
}
impl Branch {
fn matches(&self, node: &Node<'_>) -> bool {
self.run_from(0, node)
}
fn matches_attribute(&self, attr_name: &str, parent: Option<&Node<'_>>) -> bool {
let Some(step0) = self.steps.first() else { return false; };
let test_ok = match &step0.test {
Test::Attribute(name) => name.as_str() == attr_name,
Test::AnyAttribute => true,
_ => false,
};
if !test_ok {
return false;
}
if !step0.predicates.is_empty() {
return false;
}
match self.links.first() {
None => !self.absolute,
Some(Link::Parent) => match parent {
Some(p) => self.run_from(1, p),
None => false,
},
Some(Link::Ancestor) => {
let (Some(next_step), Some(p)) = (self.steps.get(1), parent) else {
return false;
};
let mut up = Some(p);
while let Some(anc) = up {
if next_step.test.matches(anc) && predicates_hold(&next_step.predicates, anc) {
return self.run_from(1, anc);
}
up = anc.parent.get();
}
false
}
}
}
fn run_from(&self, start: usize, start_cursor: &Node<'_>) -> bool {
let mut cursor = start_cursor;
for i in start..self.steps.len() {
let step = &self.steps[i];
if !step.test.matches(cursor) {
return false;
}
if !predicates_hold(&step.predicates, cursor) {
return false;
}
let Some(link) = self.links.get(i) else { break; };
cursor = match link {
Link::Parent => match cursor.parent.get() {
Some(p) => p,
None => return false,
},
Link::Ancestor => {
let Some(next_step) = self.steps.get(i + 1) else {
return false;
};
let mut found = None;
let mut up = cursor.parent.get();
while let Some(anc) = up {
if next_step.test.matches(anc) && predicates_hold(&next_step.predicates, anc) {
found = Some(anc);
break;
}
up = anc.parent.get();
}
match found {
Some(a) => {
a
}
None => return false,
}
}
};
}
if self.absolute {
match cursor.parent.get() {
None => true,
Some(p) => matches!(p.kind, NodeKind::Document),
}
} else {
true
}
}
}
impl Test {
fn matches(&self, node: &Node<'_>) -> bool {
match self {
Test::Element(name) => {
matches!(node.kind, NodeKind::Element) && node.name() == name.as_str()
}
Test::AnyElement => matches!(node.kind, NodeKind::Element),
Test::Attribute(name) => {
matches!(node.kind, NodeKind::Attribute) && node.name() == name.as_str()
}
Test::AnyAttribute => matches!(node.kind, NodeKind::Attribute),
}
}
}
fn predicates_hold(preds: &[Predicate], node: &Node<'_>) -> bool {
preds.iter().all(|p| predicate_holds(p, node))
}
fn predicate_holds(pred: &Predicate, node: &Node<'_>) -> bool {
match pred {
Predicate::Position(want) => sibling_position(node) == Some(*want),
Predicate::Last => {
let pos = sibling_position(node);
let cnt = sibling_count(node);
matches!((pos, cnt), (Some(p), Some(c)) if p == c)
}
Predicate::AttrExists(name) => node.attributes().any(|a| a.name() == name.as_str()),
Predicate::AttrEquals(name, val) => node
.attributes()
.any(|a| a.name() == name.as_str() && a.value() == val.as_str()),
}
}
fn sibling_position(node: &Node<'_>) -> Option<usize> {
let parent = node.parent.get()?;
let target_name = node.name();
let mut idx = 0usize;
for sib in parent.children() {
if !matches!(sib.kind, NodeKind::Element) { continue; }
if sib.name() != target_name { continue; }
idx += 1;
if std::ptr::eq(sib as *const _, node as *const _) {
return Some(idx);
}
}
None
}
fn sibling_count(node: &Node<'_>) -> Option<usize> {
let parent = node.parent.get()?;
let target_name = node.name();
let mut n = 0usize;
for sib in parent.children() {
if !matches!(sib.kind, NodeKind::Element) { continue; }
if sib.name() != target_name { continue; }
n += 1;
}
Some(n)
}
struct Parser<'a> {
src: &'a str,
pos: usize,
}
impl<'a> Parser<'a> {
fn new(src: &'a str) -> Self { Self { src, pos: 0 } }
fn at_eof(&self) -> bool { self.pos >= self.src.len() }
fn peek(&self) -> Option<char> { self.src[self.pos..].chars().next() }
fn bump(&mut self) -> Option<char> {
let c = self.peek()?;
self.pos += c.len_utf8();
Some(c)
}
fn skip_ws(&mut self) {
while let Some(c) = self.peek() {
if c.is_ascii_whitespace() { self.bump(); } else { break; }
}
}
fn eat(&mut self, lit: &str) -> bool {
if self.src[self.pos..].starts_with(lit) {
self.pos += lit.len();
true
} else { false }
}
fn parse_pattern(&mut self) -> Result<Pattern, String> {
let mut branches = vec![self.parse_branch()?];
loop {
self.skip_ws();
if !self.eat("|") { break; }
branches.push(self.parse_branch()?);
}
Ok(Pattern { branches })
}
fn parse_branch(&mut self) -> Result<Branch, String> {
self.skip_ws();
let absolute_or_descendant_root = self.peek() == Some('/');
let mut leading_descendant = false;
if absolute_or_descendant_root {
self.bump();
if self.peek() == Some('/') {
self.bump();
leading_descendant = true;
}
}
let mut steps_lr: Vec<Step> = vec![self.parse_step()?];
let mut links_lr: Vec<Link> = Vec::new();
loop {
self.skip_ws();
if !self.eat("/") {
break;
}
let link = if self.peek() == Some('/') { self.bump(); Link::Ancestor } else { Link::Parent };
links_lr.push(link);
steps_lr.push(self.parse_step()?);
}
steps_lr.reverse();
let mut links: Vec<Link> = links_lr.into_iter().rev().collect();
let absolute = if leading_descendant {
false
} else {
absolute_or_descendant_root
};
if steps_lr.len() > 1 {
for s in &steps_lr[1..] {
if matches!(s.test, Test::Attribute(_) | Test::AnyAttribute) {
return Err("attribute step must be the rightmost step".into());
}
}
}
links.truncate(steps_lr.len().saturating_sub(1));
Ok(Branch { steps: steps_lr, links, absolute })
}
fn parse_step(&mut self) -> Result<Step, String> {
self.skip_ws();
let test = if self.peek() == Some('@') {
self.bump();
if self.eat("*") { Test::AnyAttribute }
else {
let n = self.parse_ncname()?;
Test::Attribute(n)
}
} else if self.eat("*") {
Test::AnyElement
} else {
let n = self.parse_ncname()?;
Test::Element(n)
};
let mut predicates = Vec::new();
loop {
self.skip_ws();
if self.peek() != Some('[') { break; }
predicates.push(self.parse_predicate()?);
}
Ok(Step { test, predicates })
}
fn parse_ncname(&mut self) -> Result<String, String> {
let start = self.pos;
let first = match self.peek() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => c,
_ => return Err(format!("expected NCName at offset {}", self.pos)),
};
self.bump();
let _ = first;
while let Some(c) = self.peek() {
if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == ':' {
self.bump();
} else { break; }
}
Ok(self.src[start..self.pos].to_string())
}
fn parse_predicate(&mut self) -> Result<Predicate, String> {
if !self.eat("[") { return Err("expected '['".into()); }
self.skip_ws();
let pred = if self.peek().is_some_and(|c| c.is_ascii_digit()) {
let start = self.pos;
while let Some(c) = self.peek() {
if c.is_ascii_digit() { self.bump(); } else { break; }
}
let n: usize = self.src[start..self.pos].parse().map_err(|e| format!("bad position: {e}"))?;
Predicate::Position(n)
} else if self.eat("last") {
self.skip_ws();
if !self.eat("(") || { self.skip_ws(); !self.eat(")") } {
return Err("expected 'last()'".into());
}
Predicate::Last
} else if self.peek() == Some('@') {
self.bump();
let name = self.parse_ncname()?;
self.skip_ws();
if self.eat("=") {
self.skip_ws();
let val = self.parse_string_literal()?;
Predicate::AttrEquals(name, val)
} else {
Predicate::AttrExists(name)
}
} else {
return Err(format!("unsupported predicate at offset {}", self.pos));
};
self.skip_ws();
if !self.eat("]") { return Err("expected ']'".into()); }
Ok(pred)
}
fn parse_string_literal(&mut self) -> Result<String, String> {
let q = self.bump().ok_or("expected quoted string")?;
if q != '"' && q != '\'' {
return Err(format!("expected quote, got {q:?}"));
}
let start = self.pos;
while let Some(c) = self.peek() {
if c == q { break; }
self.bump();
}
let s = self.src[start..self.pos].to_string();
if !self.eat(&q.to_string()) {
return Err("unterminated string literal".into());
}
Ok(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{parse_str, ParseOptions};
fn parse(s: &str) -> sup_xml_tree::dom::Document {
parse_str(s, &ParseOptions::default()).unwrap()
}
fn root<'a>(d: &'a sup_xml_tree::dom::Document) -> &'a Node<'a> {
d.root()
}
fn nth_child<'a>(parent: &'a Node<'a>, i: usize) -> &'a Node<'a> {
parent.children().nth(i).unwrap()
}
#[test]
fn compile_basic_shapes() {
for src in [
"foo",
"*",
"@id",
"@*",
"foo/bar",
"foo//bar",
"//book",
"/catalog/book",
"foo[1]",
"foo[@id]",
"foo[@id='x']",
"foo[last()]",
"a | b | c",
] {
Pattern::compile(src).unwrap_or_else(|e| panic!("failed on {src:?}: {e}"));
}
}
#[test]
fn rejects_unsupported() {
for src in ["foo[contains(@id,'x')]", "parent::*", "..", "foo[bar=1]"] {
assert!(Pattern::compile(src).is_err(), "should reject {src:?}");
}
}
#[test]
fn match_simple_element() {
let d = parse("<catalog><book/></catalog>");
let book = nth_child(root(&d), 0);
assert!(Pattern::compile("book").unwrap().matches(book));
assert!(!Pattern::compile("catalog").unwrap().matches(book));
}
#[test]
fn match_child_chain() {
let d = parse("<catalog><book/></catalog>");
let book = nth_child(root(&d), 0);
assert!(Pattern::compile("catalog/book").unwrap().matches(book));
assert!(!Pattern::compile("library/book").unwrap().matches(book));
}
#[test]
fn match_descendant() {
let d = parse("<r><a><b><c/></b></a></r>");
let c = nth_child(nth_child(nth_child(root(&d), 0), 0), 0);
assert!(Pattern::compile("//c").unwrap().matches(c));
assert!(Pattern::compile("r//c").unwrap().matches(c));
assert!(Pattern::compile("a//c").unwrap().matches(c));
assert!(!Pattern::compile("a/c").unwrap().matches(c));
}
#[test]
fn match_wildcard() {
let d = parse("<r><a/></r>");
let a = nth_child(root(&d), 0);
assert!(Pattern::compile("*").unwrap().matches(a));
assert!(Pattern::compile("r/*").unwrap().matches(a));
}
#[test]
fn match_absolute() {
let d = parse("<r><a/></r>");
let r = root(&d);
let a = nth_child(r, 0);
assert!(Pattern::compile("/r").unwrap().matches(r));
assert!(Pattern::compile("/r/a").unwrap().matches(a));
assert!(!Pattern::compile("/a").unwrap().matches(a));
}
#[test]
fn match_position_predicate() {
let d = parse("<catalog><book/><book/><book/></catalog>");
let books: Vec<_> = root(&d).children().collect();
assert!(Pattern::compile("book[1]").unwrap().matches(books[0]));
assert!(!Pattern::compile("book[1]").unwrap().matches(books[1]));
assert!(Pattern::compile("book[2]").unwrap().matches(books[1]));
assert!(Pattern::compile("book[last()]").unwrap().matches(books[2]));
assert!(!Pattern::compile("book[last()]").unwrap().matches(books[0]));
}
#[test]
fn match_attr_predicate() {
let d = parse(r#"<catalog><book id="b1"/><book/></catalog>"#);
let books: Vec<_> = root(&d).children().collect();
assert!(Pattern::compile("book[@id]").unwrap().matches(books[0]));
assert!(!Pattern::compile("book[@id]").unwrap().matches(books[1]));
assert!(Pattern::compile(r#"book[@id="b1"]"#).unwrap().matches(books[0]));
assert!(!Pattern::compile(r#"book[@id="b2"]"#).unwrap().matches(books[0]));
}
#[test]
fn match_union() {
let d = parse("<r><a/><b/></r>");
let a = nth_child(root(&d), 0);
let b = nth_child(root(&d), 1);
let p = Pattern::compile("a | b").unwrap();
assert!(p.matches(a));
assert!(p.matches(b));
let p2 = Pattern::compile("a | c").unwrap();
assert!(p2.matches(a));
assert!(!p2.matches(b));
}
}