use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use super::ast::{Axis, BinaryOp, Expr, NodeTest, Step};
use super::float;
use crate::tree::{Document, NodeId, NodeKind};
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
NodeSet(Vec<NodeId>),
String(String),
Number(f64),
Boolean(bool),
}
impl Value {
#[must_use]
pub fn to_boolean(&self) -> bool {
match self {
Self::Boolean(b) => *b,
Self::Number(n) => *n != 0.0 && !n.is_nan(),
Self::String(s) => !s.is_empty(),
Self::NodeSet(n) => !n.is_empty(),
}
}
#[must_use]
pub fn to_number(&self, doc: &Document) -> f64 {
match self {
Self::Number(n) => *n,
Self::Boolean(b) => f64::from(u8::from(*b)),
Self::String(s) => s.trim().parse().unwrap_or(f64::NAN),
Self::NodeSet(_) => {
self.to_str(doc).trim().parse().unwrap_or(f64::NAN)
}
}
}
#[must_use]
pub fn to_str(&self, doc: &Document) -> String {
match self {
Self::String(s) => s.clone(),
Self::Boolean(b) => {
if *b {
"true".to_owned()
} else {
"false".to_owned()
}
}
Self::Number(n) => format_number(*n),
Self::NodeSet(nodes) => nodes
.first()
.map(|id| string_value(doc, *id))
.unwrap_or_default(),
}
}
#[must_use]
pub fn nodes(&self) -> Option<&[NodeId]> {
match self {
Self::NodeSet(n) => Some(n),
_ => None,
}
}
}
fn format_number(n: f64) -> String {
if n.is_nan() {
return "NaN".to_owned();
}
if n.is_infinite() {
return if n > 0.0 { "Infinity" } else { "-Infinity" }.to_owned();
}
#[allow(clippy::float_cmp)]
let is_integral = n == float::trunc(n);
if is_integral && n.abs() < 1e21 {
return format!("{}", n as i64);
}
let rounded: f64 = format!("{n:.14e}").parse().unwrap_or(n);
let mut s = rounded.to_string();
if s.contains('.') {
s = s.trim_end_matches('0').trim_end_matches('.').to_owned();
}
s
}
fn string_value(doc: &Document, id: NodeId) -> String {
match doc.kind(id) {
Some(NodeKind::Attr(a)) => a.value.clone(),
Some(NodeKind::Comment(t)) => t.clone(),
Some(NodeKind::ProcessingInstruction { data, .. }) => data.clone(),
_ => doc.text(id),
}
}
#[must_use]
pub fn evaluate(doc: &Document, expr: &Expr, context: NodeId) -> Value {
eval(doc, expr, context, 1, 1)
}
fn eval(
doc: &Document,
expr: &Expr,
ctx: NodeId,
position: usize,
size: usize,
) -> Value {
match expr {
Expr::Literal(s) => Value::String(s.clone()),
Expr::Number(n) => Value::Number(*n),
Expr::Negate(inner) => {
Value::Number(-eval(doc, inner, ctx, position, size).to_number(doc))
}
Expr::Path { absolute, steps } => {
let start = if *absolute { doc.root() } else { ctx };
Value::NodeSet(eval_path(doc, steps, start))
}
Expr::Binary { op, lhs, rhs } => {
eval_binary(doc, *op, lhs, rhs, ctx, position, size)
}
Expr::Function { name, args } => {
eval_function(doc, name, args, ctx, position, size)
}
}
}
fn eval_path(doc: &Document, steps: &[Step], start: NodeId) -> Vec<NodeId> {
let mut current = alloc::vec![start];
for step in steps {
let mut next: Vec<NodeId> = Vec::new();
for &node in ¤t {
next.extend(
axis_nodes(doc, node, step.axis)
.into_iter()
.filter(|&c| test_matches(doc, c, &step.test, step.axis)),
);
}
next.sort_unstable();
next.dedup();
for pred in &step.predicates {
let size = next.len();
let mut kept = Vec::with_capacity(next.len());
for (idx, &node) in next.iter().enumerate() {
let v = eval(doc, pred, node, idx + 1, size);
let keep = match v {
Value::Number(n) => {
(n - (idx + 1) as f64).abs() < f64::EPSILON
}
other => other.to_boolean(),
};
if keep {
kept.push(node);
}
}
next = kept;
}
current = next;
}
current.sort_unstable();
current.dedup();
current
}
fn axis_nodes(doc: &Document, node: NodeId, axis: Axis) -> Vec<NodeId> {
match axis {
Axis::Child => doc.children(node).to_vec(),
Axis::SelfAxis => alloc::vec![node],
Axis::Parent => doc.parent(node).into_iter().collect(),
Axis::Attribute => doc.attribute_nodes(node).to_vec(),
Axis::Descendant => {
let mut out = Vec::new();
collect_descendants(doc, node, &mut out);
out
}
Axis::DescendantOrSelf => {
let mut out = alloc::vec![node];
collect_descendants(doc, node, &mut out);
out
}
Axis::Ancestor => {
let mut out = Vec::new();
let mut cur = doc.parent(node);
while let Some(p) = cur {
out.push(p);
cur = doc.parent(p);
}
out
}
Axis::AncestorOrSelf => {
let mut out = alloc::vec![node];
let mut cur = doc.parent(node);
while let Some(p) = cur {
out.push(p);
cur = doc.parent(p);
}
out
}
Axis::FollowingSibling | Axis::PrecedingSibling => {
let Some(parent) = doc.parent(node) else {
return Vec::new();
};
let sibs = doc.children(parent);
let Some(idx) = sibs.iter().position(|&s| s == node) else {
return Vec::new();
};
if axis == Axis::FollowingSibling {
sibs[idx + 1..].to_vec()
} else {
sibs[..idx].to_vec()
}
}
}
}
fn collect_descendants(doc: &Document, node: NodeId, out: &mut Vec<NodeId>) {
for &child in doc.children(node) {
out.push(child);
collect_descendants(doc, child, out);
}
}
fn test_matches(
doc: &Document,
node: NodeId,
test: &NodeTest,
axis: Axis,
) -> bool {
if axis == Axis::Attribute {
return match (test, doc.kind(node)) {
(NodeTest::Wildcard | NodeTest::Any, Some(NodeKind::Attr(_))) => {
true
}
(NodeTest::Name { namespace, local }, Some(NodeKind::Attr(a))) => {
doc.name(a.name).is_some_and(|name| {
&name.local == local
&& name.namespace.as_deref() == namespace.as_deref()
})
}
_ => false,
};
}
match test {
NodeTest::Any => true,
NodeTest::Wildcard => doc.is_element(node),
NodeTest::Name { namespace, local } => {
doc.element_name(node).is_some_and(|e| {
&e.local == local
&& e.namespace.as_deref() == namespace.as_deref()
})
}
NodeTest::Text => {
matches!(doc.kind(node), Some(NodeKind::Text(_)))
}
NodeTest::Comment => {
matches!(doc.kind(node), Some(NodeKind::Comment(_)))
}
NodeTest::ProcessingInstruction(want) => matches!(
doc.kind(node),
Some(NodeKind::ProcessingInstruction { target, .. })
if want.as_ref().is_none_or(|w| w == target)
),
}
}
fn eval_binary(
doc: &Document,
op: BinaryOp,
lhs: &Expr,
rhs: &Expr,
ctx: NodeId,
position: usize,
size: usize,
) -> Value {
match op {
BinaryOp::And => {
let l = eval(doc, lhs, ctx, position, size);
if !l.to_boolean() {
return Value::Boolean(false);
}
return Value::Boolean(
eval(doc, rhs, ctx, position, size).to_boolean(),
);
}
BinaryOp::Or => {
let l = eval(doc, lhs, ctx, position, size);
if l.to_boolean() {
return Value::Boolean(true);
}
return Value::Boolean(
eval(doc, rhs, ctx, position, size).to_boolean(),
);
}
_ => {}
}
let l = eval(doc, lhs, ctx, position, size);
let r = eval(doc, rhs, ctx, position, size);
match op {
BinaryOp::Union => {
let mut out = l.nodes().unwrap_or(&[]).to_vec();
out.extend_from_slice(r.nodes().unwrap_or(&[]));
out.sort_unstable();
out.dedup();
Value::NodeSet(out)
}
BinaryOp::Eq | BinaryOp::Ne => {
let eq = compare_equality(doc, &l, &r);
Value::Boolean(if op == BinaryOp::Eq { eq } else { !eq })
}
BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge => {
let a = l.to_number(doc);
let b = r.to_number(doc);
Value::Boolean(match op {
BinaryOp::Lt => a < b,
BinaryOp::Le => a <= b,
BinaryOp::Gt => a > b,
_ => a >= b,
})
}
BinaryOp::Add
| BinaryOp::Sub
| BinaryOp::Mul
| BinaryOp::Div
| BinaryOp::Mod => {
let a = l.to_number(doc);
let b = r.to_number(doc);
Value::Number(match op {
BinaryOp::Add => a + b,
BinaryOp::Sub => a - b,
BinaryOp::Mul => a * b,
BinaryOp::Div => a / b,
_ => a % b,
})
}
BinaryOp::And | BinaryOp::Or => unreachable!("handled above"),
}
}
fn compare_equality(doc: &Document, l: &Value, r: &Value) -> bool {
match (l, r) {
(Value::NodeSet(a), Value::NodeSet(b)) => a.iter().any(|x| {
b.iter()
.any(|y| string_value(doc, *x) == string_value(doc, *y))
}),
(Value::NodeSet(a), other) | (other, Value::NodeSet(a)) => {
match other {
Value::Number(n) => a.iter().any(|x| {
string_value(doc, *x)
.trim()
.parse::<f64>()
.is_ok_and(|v| (v - n).abs() < f64::EPSILON)
}),
Value::Boolean(b) => a.is_empty() != *b,
_ => {
let s = other.to_str(doc);
a.iter().any(|x| string_value(doc, *x) == s)
}
}
}
(Value::Boolean(_), _) | (_, Value::Boolean(_)) => {
l.to_boolean() == r.to_boolean()
}
(Value::Number(_), _) | (_, Value::Number(_)) => {
let a = l.to_number(doc);
let b = r.to_number(doc);
(a - b).abs() < f64::EPSILON
}
_ => l.to_str(doc) == r.to_str(doc),
}
}
fn eval_function(
doc: &Document,
name: &str,
args: &[Expr],
ctx: NodeId,
position: usize,
size: usize,
) -> Value {
let arg = |i: usize| -> Option<Value> {
args.get(i).map(|a| eval(doc, a, ctx, position, size))
};
match name {
"true" => Value::Boolean(true),
"false" => Value::Boolean(false),
"not" => Value::Boolean(!arg(0).is_some_and(|v| v.to_boolean())),
"position" => Value::Number(position as f64),
"last" => Value::Number(size as f64),
"count" => Value::Number(
arg(0)
.and_then(|v| v.nodes().map(<[NodeId]>::len))
.unwrap_or(0) as f64,
),
"string" => Value::String(
arg(0).map_or_else(|| string_value(doc, ctx), |v| v.to_str(doc)),
),
"number" => {
Value::Number(arg(0).map_or(f64::NAN, |v| v.to_number(doc)))
}
"boolean" => Value::Boolean(arg(0).is_some_and(|v| v.to_boolean())),
"concat" => {
let mut s = String::new();
for a in args {
s.push_str(&eval(doc, a, ctx, position, size).to_str(doc));
}
Value::String(s)
}
"string-length" => Value::Number(
arg(0)
.map_or_else(|| string_value(doc, ctx), |v| v.to_str(doc))
.chars()
.count() as f64,
),
"starts-with" => {
let a = arg(0).map(|v| v.to_str(doc)).unwrap_or_default();
let b = arg(1).map(|v| v.to_str(doc)).unwrap_or_default();
Value::Boolean(a.starts_with(&b))
}
"contains" => {
let a = arg(0).map(|v| v.to_str(doc)).unwrap_or_default();
let b = arg(1).map(|v| v.to_str(doc)).unwrap_or_default();
Value::Boolean(a.contains(&b))
}
"normalize-space" => {
let s = arg(0)
.map_or_else(|| string_value(doc, ctx), |v| v.to_str(doc));
Value::String(s.split_whitespace().collect::<Vec<_>>().join(" "))
}
"substring" => {
let s = arg(0).map(|v| v.to_str(doc)).unwrap_or_default();
let chars: Vec<char> = s.chars().collect();
let start = xpath_round(arg(1).map_or(1.0, |v| v.to_number(doc)));
let end = match arg(2) {
Some(v) => {
let len = xpath_round(v.to_number(doc));
if len.is_nan() || start.is_nan() {
f64::NAN
} else {
start + len
}
}
None => f64::INFINITY,
};
let out: String = chars
.into_iter()
.enumerate()
.filter(|(i, _)| {
let p = *i as f64 + 1.0;
p >= start && p < end
})
.map(|(_, c)| c)
.collect();
Value::String(out)
}
_ => eval_node_function(doc, name, args, ctx, position, size),
}
}
fn name_parts(doc: &Document, id: NodeId) -> Option<(&str, Option<&str>)> {
match doc.kind(id)? {
NodeKind::Element { .. } => doc
.element_name(id)
.map(|n| (n.local.as_str(), n.namespace.as_deref())),
NodeKind::Attr(attribute) => doc
.name(attribute.name)
.map(|n| (n.local.as_str(), n.namespace.as_deref())),
NodeKind::ProcessingInstruction { target, .. } => {
Some((target.as_str(), None))
}
NodeKind::Root | NodeKind::Text(_) | NodeKind::Comment(_) => None,
}
}
fn node_argument(
doc: &Document,
args: &[Expr],
ctx: NodeId,
position: usize,
size: usize,
) -> Option<NodeId> {
match args.first() {
None => Some(ctx),
Some(a) => {
match eval(doc, a, ctx, position, size) {
Value::NodeSet(nodes) => nodes.first().copied(),
_ => None,
}
}
}
}
fn xpath_round(n: f64) -> f64 {
if n.is_nan() || n.is_infinite() {
return n;
}
float::floor(n + 0.5)
}
fn eval_node_function(
doc: &Document,
name: &str,
args: &[Expr],
ctx: NodeId,
position: usize,
size: usize,
) -> Value {
let arg = |i: usize| -> Option<Value> {
args.get(i).map(|a| eval(doc, a, ctx, position, size))
};
match name {
"local-name" => Value::String(
node_argument(doc, args, ctx, position, size)
.and_then(|n| name_parts(doc, n))
.map(|(local, _)| local.to_owned())
.unwrap_or_default(),
),
"namespace-uri" => Value::String(
node_argument(doc, args, ctx, position, size)
.and_then(|n| name_parts(doc, n))
.and_then(|(_, namespace)| namespace)
.map(str::to_owned)
.unwrap_or_default(),
),
"sum" => {
let total = arg(0)
.and_then(|v| v.nodes().map(<[NodeId]>::to_vec))
.unwrap_or_default()
.iter()
.filter_map(|id| {
string_value(doc, *id).trim().parse::<f64>().ok()
})
.sum();
Value::Number(total)
}
"floor" => Value::Number(float::floor(
arg(0).map_or(f64::NAN, |v| v.to_number(doc)),
)),
"ceiling" => Value::Number(float::ceil(
arg(0).map_or(f64::NAN, |v| v.to_number(doc)),
)),
"round" => Value::Number(xpath_round(
arg(0).map_or(f64::NAN, |v| v.to_number(doc)),
)),
_ => Value::NodeSet(Vec::new()),
}
}