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.to_owned(),
Some(NodeKind::Comment(t)) => t.to_owned(),
Some(NodeKind::ProcessingInstruction { data, .. }) => data.to_owned(),
_ => 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()
}
}
Axis::Namespace => {
let mut out: Vec<NodeId> = Vec::new();
let mut seen: Vec<&str> = Vec::new();
let mut at = Some(node);
while let Some(current) = at {
for &ns in doc.namespace_nodes(current) {
let Some(NodeKind::Namespace { prefix, uri }) =
doc.kind(ns)
else {
continue;
};
if seen.contains(&prefix) {
continue;
}
seen.push(prefix);
if !uri.is_empty() {
out.push(ns);
}
}
at = doc.parent(current);
}
out
}
Axis::Following | Axis::Preceding => {
let here = node.index();
doc.descendants()
.filter(|&cand| cand != node)
.filter(|&cand| {
!matches!(
doc.kind(cand),
Some(NodeKind::Attr(_) | NodeKind::Namespace { .. })
)
})
.filter(|&cand| {
if axis == Axis::Following {
cand.index() > here && !is_ancestor(doc, node, cand)
} else {
cand.index() < here && !is_ancestor(doc, cand, node)
}
})
.collect()
}
}
}
fn is_ancestor(doc: &Document, maybe: NodeId, of: NodeId) -> bool {
let mut cur = doc.parent(of);
while let Some(p) = cur {
if p == maybe {
return true;
}
cur = doc.parent(p);
}
false
}
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,
};
}
if axis == Axis::Namespace {
return match (test, doc.kind(node)) {
(
NodeTest::Wildcard | NodeTest::Any,
Some(NodeKind::Namespace { .. }),
) => true,
(
NodeTest::Name { namespace, local },
Some(NodeKind::Namespace { prefix, .. }),
) => namespace.is_none() && local == prefix,
_ => 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 substring(s: &str, start: f64, length: Option<f64>) -> Value {
let start = xpath_round(start);
let end = match length {
Some(length) => {
let length = xpath_round(length);
if length.is_nan() || start.is_nan() {
f64::NAN
} else {
start + length
}
}
None => f64::INFINITY,
};
Value::String(
s.chars()
.enumerate()
.filter(|(i, _)| {
let p = *i as f64 + 1.0;
p >= start && p < end
})
.map(|(_, c)| c)
.collect(),
)
}
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())),
_ => eval_string_function(doc, name, args, ctx, position, size),
}
}
fn eval_string_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 {
"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" => substring(
&arg(0).map(|v| v.to_str(doc)).unwrap_or_default(),
arg(1).map_or(1.0, |v| v.to_number(doc)),
arg(2).map(|v| v.to_number(doc)),
),
"substring-before" => {
let haystack = arg(0).map(|v| v.to_str(doc)).unwrap_or_default();
let needle = arg(1).map(|v| v.to_str(doc)).unwrap_or_default();
Value::String(
haystack
.find(&needle)
.map(|at| haystack[..at].to_owned())
.unwrap_or_default(),
)
}
"substring-after" => {
let haystack = arg(0).map(|v| v.to_str(doc)).unwrap_or_default();
let needle = arg(1).map(|v| v.to_str(doc)).unwrap_or_default();
Value::String(
haystack
.find(&needle)
.map(|at| haystack[at + needle.len()..].to_owned())
.unwrap_or_default(),
)
}
"translate" => {
let s = arg(0).map(|v| v.to_str(doc)).unwrap_or_default();
let from: Vec<char> = arg(1)
.map(|v| v.to_str(doc))
.unwrap_or_default()
.chars()
.collect();
let to: Vec<char> = arg(2)
.map(|v| v.to_str(doc))
.unwrap_or_default()
.chars()
.collect();
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match from.iter().position(|&f| f == c) {
Some(i) => {
if let Some(&r) = to.get(i) {
out.push(r);
}
}
None => out.push(c),
}
}
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, None)),
NodeKind::Namespace { prefix, .. } => Some((prefix, None)),
NodeKind::Root | NodeKind::Text(_) | NodeKind::Comment(_) => None,
}
}
const XML_NAMESPACE: &str = "http://www.w3.org/XML/1998/namespace";
fn attribute_by_name<'d>(
doc: &'d Document,
id: NodeId,
namespace: Option<&str>,
local: &str,
) -> Option<&'d str> {
doc.attribute_nodes(id).iter().find_map(|&a| {
let NodeKind::Attr(attribute) = doc.kind(a)? else {
return None;
};
let name = doc.name(attribute.name)?;
(name.local == local && name.namespace.as_deref() == namespace)
.then_some(attribute.value)
})
}
fn in_scope_lang(doc: &Document, id: NodeId) -> Option<&str> {
let mut at = id;
loop {
if let Some(value) =
attribute_by_name(doc, at, Some(XML_NAMESPACE), "lang")
{
return Some(value);
}
at = doc.parent(at)?;
}
}
fn lang_matches(doc: &Document, ctx: NodeId, want: &str) -> bool {
let Some(have) = in_scope_lang(doc, ctx) else {
return false;
};
if have.eq_ignore_ascii_case(want) {
return true;
}
have.as_bytes().get(want.len()) == Some(&b'-')
&& have
.get(..want.len())
.is_some_and(|head| head.eq_ignore_ascii_case(want))
}
fn qualified_name(doc: &Document, id: NodeId) -> Option<String> {
let name_id = match doc.kind(id)? {
NodeKind::Element { name, .. } => name,
NodeKind::Attr(attribute) => attribute.name,
NodeKind::ProcessingInstruction { target, .. } => {
return Some(target.to_owned());
}
NodeKind::Namespace { prefix, .. } => return Some(prefix.to_owned()),
NodeKind::Root | NodeKind::Text(_) | NodeKind::Comment(_) => {
return None;
}
};
let local = &doc.name(name_id)?.local;
Some(match doc.prefix(name_id) {
Some(prefix) => {
let mut out = String::with_capacity(prefix.len() + 1 + local.len());
out.push_str(prefix);
out.push(':');
out.push_str(local);
out
}
None => local.clone(),
})
}
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(),
),
"lang" => Value::Boolean(lang_matches(
doc,
ctx,
&arg(0).map(|v| v.to_str(doc)).unwrap_or_default(),
)),
"name" => Value::String(
node_argument(doc, args, ctx, position, size)
.and_then(|n| qualified_name(doc, n))
.unwrap_or_default(),
),
"id" => {
let mut out = Vec::new();
let collect = |list: &str, out: &mut Vec<NodeId>| {
for token in list.split_whitespace() {
if let Some(found) = doc.element_by_id(token) {
out.push(found);
}
}
};
match arg(0) {
Some(Value::NodeSet(nodes)) => {
for node in nodes {
collect(&string_value(doc, node), &mut out);
}
}
Some(other) => collect(&other.to_str(doc), &mut out),
None => {}
}
out.sort_unstable();
out.dedup();
Value::NodeSet(out)
}
"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()),
}
}