use crate::ast::{
AdditiveOp, Axis, EqualityOp, Expr, FilterExpr, FunctionCall, LocationPath, MultiplicativeOp,
NodeTest, PathExpr, PrimaryExpr, QName, RelationalOp, Step,
};
use crate::axes;
use crate::document::{Node, NodeKind};
use crate::functions;
use crate::value::{Value, string_to_number};
pub type VariableLookup<'a, N> = dyn Fn(&QName) -> Option<Value<N>> + 'a;
pub type NamespaceLookup<'a> = dyn Fn(&str) -> Option<String> + 'a;
#[derive(Clone, Copy)]
pub struct EvaluationContext<'ctx, 'hook, N: Node<'ctx>> {
pub node: N,
pub position: usize,
pub size: usize,
pub variables: Option<&'hook VariableLookup<'hook, N>>,
pub namespaces: Option<&'hook NamespaceLookup<'hook>>,
_ctx: std::marker::PhantomData<&'ctx ()>,
}
impl<'ctx, 'hook, N: Node<'ctx>> EvaluationContext<'ctx, 'hook, N> {
pub fn new(node: N) -> Self {
EvaluationContext {
node,
position: 1,
size: 1,
variables: None,
namespaces: None,
_ctx: std::marker::PhantomData,
}
}
fn with(&self, node: N, position: usize, size: usize) -> Self {
EvaluationContext {
node,
position,
size,
variables: self.variables,
namespaces: self.namespaces,
_ctx: std::marker::PhantomData,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum EvalError {
UnknownFunction(QName),
UnboundVariable(QName),
ArgumentCount {
function: &'static str,
expected: usize,
got: usize,
},
ExpectedNodeSet { context: &'static str },
}
impl std::fmt::Display for EvalError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EvalError::UnknownFunction(name) => {
write!(f, "unknown function: {}", format_qname(name))
}
EvalError::UnboundVariable(name) => {
write!(f, "unbound variable: ${}", format_qname(name))
}
EvalError::ArgumentCount {
function,
expected,
got,
} => write!(f, "{function}() expects {expected} argument(s), got {got}"),
EvalError::ExpectedNodeSet { context } => {
write!(f, "expected a node-set in {context}")
}
}
}
}
impl std::error::Error for EvalError {}
fn format_qname(q: &QName) -> String {
match &q.prefix {
Some(p) => format!("{p}:{}", q.local),
None => q.local.clone(),
}
}
pub fn evaluate<'ctx, 'hook, N: Node<'ctx>>(
expr: &Expr,
ctx: &EvaluationContext<'ctx, 'hook, N>,
) -> Result<Value<N>, EvalError> {
match expr {
Expr::Or(l, r) => {
if evaluate(l, ctx)?.to_boolean() {
Ok(Value::Boolean(true))
} else {
Ok(Value::Boolean(evaluate(r, ctx)?.to_boolean()))
}
}
Expr::And(l, r) => {
if !evaluate(l, ctx)?.to_boolean() {
Ok(Value::Boolean(false))
} else {
Ok(Value::Boolean(evaluate(r, ctx)?.to_boolean()))
}
}
Expr::Equality(l, op, r) => {
let lv = evaluate(l, ctx)?;
let rv = evaluate(r, ctx)?;
Ok(Value::Boolean(compare_equality(&lv, *op, &rv)))
}
Expr::Relational(l, op, r) => {
let lv = evaluate(l, ctx)?;
let rv = evaluate(r, ctx)?;
Ok(Value::Boolean(compare_relational(&lv, *op, &rv)))
}
Expr::Additive(l, op, r) => {
let lv = evaluate(l, ctx)?.to_number();
let rv = evaluate(r, ctx)?.to_number();
Ok(Value::Number(match op {
AdditiveOp::Add => lv + rv,
AdditiveOp::Sub => lv - rv,
}))
}
Expr::Multiplicative(l, op, r) => {
let lv = evaluate(l, ctx)?.to_number();
let rv = evaluate(r, ctx)?.to_number();
Ok(Value::Number(match op {
MultiplicativeOp::Mul => lv * rv,
MultiplicativeOp::Div => lv / rv,
MultiplicativeOp::Mod => lv % rv,
}))
}
Expr::Negate(e) => Ok(Value::Number(-evaluate(e, ctx)?.to_number())),
Expr::Union(l, r) => {
let lv = evaluate(l, ctx)?;
let rv = evaluate(r, ctx)?;
match (lv, rv) {
(Value::NodeSet(mut a), Value::NodeSet(b)) => {
a.extend(b);
sort_dedup(&mut a);
Ok(Value::NodeSet(a))
}
_ => Err(EvalError::ExpectedNodeSet {
context: "union operand",
}),
}
}
Expr::Path(path) => evaluate_path(path, ctx),
}
}
fn eq_result<T: PartialEq>(op: EqualityOp, l: T, r: T) -> bool {
match op {
EqualityOp::Eq => l == r,
EqualityOp::Ne => l != r,
}
}
fn rel_result(op: RelationalOp, l: f64, r: f64) -> bool {
match op {
RelationalOp::Lt => l < r,
RelationalOp::Le => l <= r,
RelationalOp::Gt => l > r,
RelationalOp::Ge => l >= r,
}
}
fn compare_equality<'a, N: Node<'a>>(lhs: &Value<N>, op: EqualityOp, rhs: &Value<N>) -> bool {
match (lhs, rhs) {
(Value::NodeSet(a), Value::NodeSet(b)) => a.iter().copied().any(|na| {
b.iter()
.copied()
.any(|nb| eq_result(op, na.string_value(), nb.string_value()))
}),
(Value::NodeSet(a), Value::Number(n)) | (Value::Number(n), Value::NodeSet(a)) => a
.iter()
.copied()
.any(|na| eq_result(op, string_to_number(&na.string_value()), *n)),
(Value::NodeSet(a), Value::String(s)) | (Value::String(s), Value::NodeSet(a)) => a
.iter()
.copied()
.any(|na| eq_result(op, na.string_value(), s.clone())),
(Value::NodeSet(a), Value::Boolean(b)) | (Value::Boolean(b), Value::NodeSet(a)) => {
eq_result(op, !a.is_empty(), *b)
}
_ => {
if matches!(lhs, Value::Boolean(_)) || matches!(rhs, Value::Boolean(_)) {
eq_result(op, lhs.to_boolean(), rhs.to_boolean())
} else if matches!(lhs, Value::Number(_)) || matches!(rhs, Value::Number(_)) {
eq_result(op, lhs.to_number(), rhs.to_number())
} else {
eq_result(op, lhs.to_xpath_string(), rhs.to_xpath_string())
}
}
}
}
fn compare_relational<'a, N: Node<'a>>(lhs: &Value<N>, op: RelationalOp, rhs: &Value<N>) -> bool {
match (lhs, rhs) {
(Value::NodeSet(a), Value::NodeSet(b)) => a.iter().copied().any(|na| {
b.iter().copied().any(|nb| {
rel_result(
op,
string_to_number(&na.string_value()),
string_to_number(&nb.string_value()),
)
})
}),
(Value::NodeSet(a), Value::Number(n)) => a
.iter()
.copied()
.any(|na| rel_result(op, string_to_number(&na.string_value()), *n)),
(Value::Number(n), Value::NodeSet(a)) => a
.iter()
.copied()
.any(|na| rel_result(op, *n, string_to_number(&na.string_value()))),
(Value::NodeSet(a), Value::String(s)) => {
let sn = string_to_number(s);
a.iter()
.copied()
.any(|na| rel_result(op, string_to_number(&na.string_value()), sn))
}
(Value::String(s), Value::NodeSet(a)) => {
let sn = string_to_number(s);
a.iter()
.copied()
.any(|na| rel_result(op, sn, string_to_number(&na.string_value())))
}
(Value::NodeSet(a), Value::Boolean(b)) => {
rel_result(op, bool_to_number(!a.is_empty()), bool_to_number(*b))
}
(Value::Boolean(b), Value::NodeSet(a)) => {
rel_result(op, bool_to_number(*b), bool_to_number(!a.is_empty()))
}
_ => rel_result(op, lhs.to_number(), rhs.to_number()),
}
}
fn bool_to_number(b: bool) -> f64 {
if b { 1.0 } else { 0.0 }
}
pub(crate) fn root_of<'a, N: Node<'a>>(n: N) -> N {
let mut cur = n;
while let Some(p) = cur.parent() {
cur = p;
}
cur
}
pub(crate) fn sort_dedup<'a, N: Node<'a>>(nodes: &mut Vec<N>) {
nodes.sort_by(|a, b| a.document_order(*b));
nodes.dedup();
}
fn axis_nodes<'a, N: Node<'a>>(axis: Axis, n: N) -> Vec<N> {
match axis {
Axis::Ancestor => axes::ancestor(n).collect(),
Axis::AncestorOrSelf => axes::ancestor_or_self(n).collect(),
Axis::Attribute => axes::attribute(n).collect(),
Axis::Child => axes::child(n).collect(),
Axis::Descendant => axes::descendant(n).collect(),
Axis::DescendantOrSelf => axes::descendant_or_self(n).collect(),
Axis::Following => axes::following(n).collect(),
Axis::FollowingSibling => axes::following_sibling(n).collect(),
Axis::Namespace => axes::namespace(n).collect(),
Axis::Parent => axes::parent(n).collect(),
Axis::Preceding => axes::preceding(n).collect(),
Axis::PrecedingSibling => axes::preceding_sibling(n).collect(),
Axis::SelfAxis => axes::self_axis(n).collect(),
}
}
fn principal_node_kind(axis: Axis) -> NodeKind {
match axis {
Axis::Attribute => NodeKind::Attribute,
Axis::Namespace => NodeKind::Namespace,
_ => NodeKind::Element,
}
}
fn resolve_prefix<'hook>(
prefix: &str,
namespaces: Option<&NamespaceLookup<'hook>>,
) -> Option<String> {
namespaces.and_then(|lookup| lookup(prefix))
}
fn matches_node_test<'ctx, 'hook, N: Node<'ctx>>(
test: &NodeTest,
axis: Axis,
n: N,
namespaces: Option<&NamespaceLookup<'hook>>,
) -> bool {
match test {
NodeTest::Node => true,
NodeTest::Text => n.kind() == NodeKind::Text,
NodeTest::Comment => n.kind() == NodeKind::Comment,
NodeTest::ProcessingInstruction(target) => {
n.kind() == NodeKind::ProcessingInstruction
&& match target {
None => true,
Some(t) => n.expanded_name().is_some_and(|e| e.local_name == *t),
}
}
NodeTest::AnyName => n.kind() == principal_node_kind(axis) && n.expanded_name().is_some(),
NodeTest::NamespaceWildcard(prefix) => {
let Some(uri) = resolve_prefix(prefix, namespaces) else {
return false;
};
n.kind() == principal_node_kind(axis)
&& n.expanded_name()
.is_some_and(|e| e.namespace_uri.as_deref() == Some(uri.as_str()))
}
NodeTest::QName(qname) => {
let uri = match &qname.prefix {
None => None,
Some(p) => match resolve_prefix(p, namespaces) {
Some(uri) => Some(uri),
None => return false,
},
};
n.kind() == principal_node_kind(axis)
&& n.expanded_name().is_some_and(|e| {
e.local_name == qname.local && e.namespace_uri.as_deref() == uri.as_deref()
})
}
}
}
fn predicate_truth<'a, N: Node<'a>>(val: &Value<N>, position: usize) -> bool {
match val {
Value::Number(n) => *n == position as f64,
_ => val.to_boolean(),
}
}
fn apply_predicates<'ctx, 'hook, N: Node<'ctx>>(
predicates: &[Expr],
nodes: Vec<N>,
ctx: &EvaluationContext<'ctx, 'hook, N>,
) -> Result<Vec<N>, EvalError> {
let mut current = nodes;
for pred in predicates {
let size = current.len();
let mut next = Vec::with_capacity(current.len());
for (i, &n) in current.iter().enumerate() {
let position = i + 1;
let pred_ctx = ctx.with(n, position, size);
let val = evaluate(pred, &pred_ctx)?;
if predicate_truth(&val, position) {
next.push(n);
}
}
current = next;
}
Ok(current)
}
fn evaluate_step<'ctx, 'hook, N: Node<'ctx>>(
step: &Step,
context_nodes: &[N],
ctx: &EvaluationContext<'ctx, 'hook, N>,
) -> Result<Vec<N>, EvalError> {
let mut result = Vec::new();
for &n in context_nodes {
let candidates: Vec<N> = axis_nodes(step.axis, n)
.into_iter()
.filter(|&c| matches_node_test(&step.node_test, step.axis, c, ctx.namespaces))
.collect();
let filtered = apply_predicates(&step.predicates, candidates, ctx)?;
result.extend(filtered);
}
sort_dedup(&mut result);
Ok(result)
}
fn evaluate_location_steps<'ctx, 'hook, N: Node<'ctx>>(
steps: &[Step],
mut nodes: Vec<N>,
ctx: &EvaluationContext<'ctx, 'hook, N>,
) -> Result<Vec<N>, EvalError> {
for step in steps {
nodes = evaluate_step(step, &nodes, ctx)?;
}
Ok(nodes)
}
fn evaluate_location_path<'ctx, 'hook, N: Node<'ctx>>(
loc: &LocationPath,
start: Vec<N>,
ctx: &EvaluationContext<'ctx, 'hook, N>,
) -> Result<Vec<N>, EvalError> {
let start = if loc.is_absolute {
start.first().map(|&n| vec![root_of(n)]).unwrap_or_default()
} else {
start
};
evaluate_location_steps(&loc.steps, start, ctx)
}
fn evaluate_path<'ctx, 'hook, N: Node<'ctx>>(
path: &PathExpr,
ctx: &EvaluationContext<'ctx, 'hook, N>,
) -> Result<Value<N>, EvalError> {
match path {
PathExpr::Location(loc) => {
let result = evaluate_location_path(loc, vec![ctx.node], ctx)?;
Ok(Value::NodeSet(result))
}
PathExpr::Filter(filter) => evaluate_filter_expr(filter, ctx),
PathExpr::FilterLocation(filter, loc) => {
let filter_val = evaluate_filter_expr(filter, ctx)?;
let start = match filter_val {
Value::NodeSet(nodes) => nodes,
_ => {
return Err(EvalError::ExpectedNodeSet {
context: "path expression left of '/'",
});
}
};
let result = evaluate_location_path(loc, start, ctx)?;
Ok(Value::NodeSet(result))
}
}
}
fn evaluate_filter_expr<'ctx, 'hook, N: Node<'ctx>>(
filter: &FilterExpr,
ctx: &EvaluationContext<'ctx, 'hook, N>,
) -> Result<Value<N>, EvalError> {
let primary_val = evaluate_primary(&filter.primary, ctx)?;
if filter.predicates.is_empty() {
return Ok(primary_val);
}
match primary_val {
Value::NodeSet(mut nodes) => {
sort_dedup(&mut nodes);
let filtered = apply_predicates(&filter.predicates, nodes, ctx)?;
Ok(Value::NodeSet(filtered))
}
_ => Err(EvalError::ExpectedNodeSet {
context: "filter expression predicate",
}),
}
}
fn evaluate_primary<'ctx, 'hook, N: Node<'ctx>>(
primary: &PrimaryExpr,
ctx: &EvaluationContext<'ctx, 'hook, N>,
) -> Result<Value<N>, EvalError> {
match primary {
PrimaryExpr::Variable(qname) => {
let bound = ctx.variables.and_then(|lookup| lookup(qname));
bound.ok_or_else(|| EvalError::UnboundVariable(qname.clone()))
}
PrimaryExpr::Parenthesized(inner) => evaluate(inner, ctx),
PrimaryExpr::Literal(s) => Ok(Value::String(s.clone())),
PrimaryExpr::Number(n) => Ok(Value::Number(*n)),
PrimaryExpr::Function(call) => evaluate_function(call, ctx),
}
}
fn check_arity(function: &'static str, args: &[Expr], expected: usize) -> Result<(), EvalError> {
if args.len() == expected {
Ok(())
} else {
Err(EvalError::ArgumentCount {
function,
expected,
got: args.len(),
})
}
}
fn evaluate_function<'ctx, 'hook, N: Node<'ctx>>(
call: &FunctionCall,
ctx: &EvaluationContext<'ctx, 'hook, N>,
) -> Result<Value<N>, EvalError> {
if call.name.prefix.is_none() {
match call.name.local.as_str() {
"last" => {
check_arity("last", &call.args, 0)?;
return Ok(Value::Number(ctx.size as f64));
}
"position" => {
check_arity("position", &call.args, 0)?;
return Ok(Value::Number(ctx.position as f64));
}
"count" => {
check_arity("count", &call.args, 1)?;
return match evaluate(&call.args[0], ctx)? {
Value::NodeSet(nodes) => Ok(Value::Number(nodes.len() as f64)),
_ => Err(EvalError::ExpectedNodeSet {
context: "count() argument",
}),
};
}
_ => {}
}
}
functions::dispatch(call, ctx)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::fixture::{Fixture, TestNode, build};
fn eval_str<'a>(src: &str, node: TestNode<'a>) -> Result<Value<TestNode<'a>>, EvalError> {
let expr = crate::parse(src).expect("test expression must parse");
let ctx = EvaluationContext::new(node);
evaluate(&expr, &ctx)
}
fn node_set<'a>(v: Result<Value<TestNode<'a>>, EvalError>) -> Vec<TestNode<'a>> {
match v.expect("expected Ok") {
Value::NodeSet(nodes) => nodes,
other => panic!("expected a node-set, got {other:?}"),
}
}
fn root(f: &Fixture) -> TestNode<'_> {
f.doc.node(f.html)
}
#[test]
fn or_short_circuits_when_left_is_true() {
let f = build();
assert_eq!(
eval_str("(1 = 1) or unknownfn()", root(&f)),
Ok(Value::Boolean(true))
);
}
#[test]
fn or_evaluates_right_when_left_is_false() {
let f = build();
assert!(matches!(
eval_str("(1 = 0) or unknownfn()", root(&f)),
Err(EvalError::UnknownFunction(_))
));
}
#[test]
fn and_short_circuits_when_left_is_false() {
let f = build();
assert_eq!(
eval_str("(1 = 0) and unknownfn()", root(&f)),
Ok(Value::Boolean(false))
);
}
#[test]
fn and_evaluates_right_when_left_is_true() {
let f = build();
assert!(matches!(
eval_str("(1 = 1) and unknownfn()", root(&f)),
Err(EvalError::UnknownFunction(_))
));
}
#[test]
fn equality_node_set_vs_node_set() {
let f = build();
assert_eq!(
eval_str(
"/html/body/span/@data-x = /html/body/span/@data-x",
root(&f)
),
Ok(Value::Boolean(true))
);
assert_eq!(
eval_str("/html/body/span/@data-x = /html/@id", root(&f)),
Ok(Value::Boolean(false))
);
assert_eq!(
eval_str("/html/body/span/@data-x != /html/@id", root(&f)),
Ok(Value::Boolean(true))
);
}
#[test]
fn equality_node_set_vs_number() {
let f = build();
assert_eq!(
eval_str("/html/body/span/@data-x = 1", root(&f)),
Ok(Value::Boolean(true))
);
assert_eq!(
eval_str("/html/body/span/@data-x = 2", root(&f)),
Ok(Value::Boolean(false))
);
assert_eq!(
eval_str("/html/body/span/@data-x != 2", root(&f)),
Ok(Value::Boolean(true))
);
}
#[test]
fn equality_node_set_vs_string() {
let f = build();
assert_eq!(
eval_str("/html/body/span/@data-x = '1'", root(&f)),
Ok(Value::Boolean(true))
);
assert_eq!(
eval_str("/html/@id = 'nope'", root(&f)),
Ok(Value::Boolean(false))
);
}
#[test]
fn equality_node_set_vs_boolean() {
let f = build();
assert_eq!(
eval_str("/html/body/span/@data-x = (1 = 1)", root(&f)),
Ok(Value::Boolean(true))
);
assert_eq!(
eval_str("/html/body/nonexistent = (1 = 1)", root(&f)),
Ok(Value::Boolean(false))
);
}
#[test]
fn relational_with_node_set_operand() {
let f = build();
assert_eq!(
eval_str("/html/body/span/@data-x > 0", root(&f)),
Ok(Value::Boolean(true))
);
assert_eq!(
eval_str("/html/body/span/@data-x < 0", root(&f)),
Ok(Value::Boolean(false))
);
assert_eq!(
eval_str("/html/body/span/@data-x > '0'", root(&f)),
Ok(Value::Boolean(true))
);
assert_eq!(
eval_str("/html/body/span/@data-x > (1 = 0)", root(&f)),
Ok(Value::Boolean(true))
);
}
#[test]
fn arithmetic_operators() {
let f = build();
assert_eq!(eval_str("1 + 2", root(&f)), Ok(Value::Number(3.0)));
assert_eq!(eval_str("1 - 2", root(&f)), Ok(Value::Number(-1.0)));
assert_eq!(eval_str("2 * 3", root(&f)), Ok(Value::Number(6.0)));
assert_eq!(eval_str("7 div 2", root(&f)), Ok(Value::Number(3.5)));
assert_eq!(eval_str("7 mod 3", root(&f)), Ok(Value::Number(1.0)));
assert_eq!(eval_str("-1 + 2", root(&f)), Ok(Value::Number(1.0)));
}
#[test]
fn union_deduplicates_overlapping_node_sets() {
let f = build();
let nodes = node_set(eval_str(
"/html/body/child::node() | /html/body/child::*",
root(&f),
));
assert_eq!(
nodes,
vec![
f.doc.node(f.text0),
f.doc.node(f.span),
f.doc.node(f.comment),
f.doc.node(f.pi),
f.doc.node(f.text2),
],
"span (present in both operands) must appear exactly once, in document order"
);
}
#[test]
fn multi_step_path_with_numeric_predicate() {
let f = build();
let nodes = node_set(eval_str("/html/body/child::node()[3]", root(&f)));
assert_eq!(nodes, vec![f.doc.node(f.comment)]);
}
#[test]
fn multi_step_mixed_axis_path_with_predicate() {
let f = build();
let nodes = node_set(eval_str("/html//text()[2]", root(&f)));
assert_eq!(nodes, vec![f.doc.node(f.text2)]);
}
#[test]
fn predicate_order_changes_the_result() {
let f = build();
let position_then_test = node_set(eval_str(
"/html/body/child::node()[2][self::span]",
root(&f),
));
assert_eq!(
position_then_test,
vec![f.doc.node(f.span)],
"[2] keeps the 2nd child (span); [self::span] then confirms it against itself"
);
let test_then_position = node_set(eval_str(
"/html/body/child::node()[self::span][2]",
root(&f),
));
assert_eq!(
test_then_position,
Vec::<TestNode>::new(),
"[self::span] first narrows to just [span] (size 1); [2] then has nothing at position 2"
);
}
#[test]
fn last_position_count_in_predicates() {
let f = build();
assert_eq!(
node_set(eval_str("/html/body/child::node()[last()]", root(&f))),
vec![f.doc.node(f.text2)]
);
assert_eq!(
node_set(eval_str(
"/html/body/child::node()[position() > 1]",
root(&f)
)),
vec![
f.doc.node(f.span),
f.doc.node(f.comment),
f.doc.node(f.pi),
f.doc.node(f.text2),
]
);
assert_eq!(
eval_str("count(/html/body/child::node())", root(&f)),
Ok(Value::Number(5.0))
);
}
#[test]
fn numeric_predicate_selects_by_position() {
let f = build();
let nodes = node_set(eval_str("/html/body/child::node()[3]", root(&f)));
assert_eq!(nodes, vec![f.doc.node(f.comment)]);
}
#[test]
fn non_numeric_predicate_uses_boolean_coercion() {
let f = build();
let nodes = node_set(eval_str("/html/body/child::node()[span]", root(&f)));
assert_eq!(nodes, Vec::<TestNode>::new());
}
#[test]
fn unknown_function_is_an_error() {
let f = build();
assert!(matches!(
eval_str("foo()", root(&f)),
Err(EvalError::UnknownFunction(_))
));
}
#[test]
fn unbound_variable_is_an_error() {
let f = build();
assert!(matches!(
eval_str("$missing", root(&f)),
Err(EvalError::UnboundVariable(_))
));
}
#[test]
fn wrong_arity_is_an_error() {
let f = build();
assert!(matches!(
eval_str("last(1)", root(&f)),
Err(EvalError::ArgumentCount {
function: "last",
expected: 0,
got: 1
})
));
assert!(matches!(
eval_str("count()", root(&f)),
Err(EvalError::ArgumentCount {
function: "count",
expected: 1,
got: 0
})
));
}
#[test]
fn count_of_non_node_set_is_an_error() {
let f = build();
assert!(matches!(
eval_str("count(1)", root(&f)),
Err(EvalError::ExpectedNodeSet { .. })
));
}
#[test]
fn bound_variable_resolves_via_the_lookup_hook() {
let f = build();
let lookup = |q: &QName| -> Option<Value<TestNode<'_>>> {
if q.prefix.is_none() && q.local == "x" {
Some(Value::Number(42.0))
} else {
None
}
};
let ctx = EvaluationContext {
node: root(&f),
position: 1,
size: 1,
variables: Some(&lookup),
namespaces: None,
_ctx: std::marker::PhantomData,
};
let expr = crate::parse("$x + 1").unwrap();
assert_eq!(evaluate(&expr, &ctx), Ok(Value::Number(43.0)));
}
#[test]
fn filter_expr_sorts_a_variable_supplied_node_set_into_document_order_before_predicates() {
let f = build();
let span = f.doc.node(f.span);
let pi = f.doc.node(f.pi);
let lookup = |q: &QName| -> Option<Value<TestNode<'_>>> {
if q.prefix.is_none() && q.local == "nodes" {
Some(Value::NodeSet(vec![pi, span]))
} else {
None
}
};
let ctx = EvaluationContext {
node: root(&f),
position: 1,
size: 1,
variables: Some(&lookup),
namespaces: None,
_ctx: std::marker::PhantomData,
};
let expr = crate::parse("$nodes[1]").unwrap();
assert_eq!(evaluate(&expr, &ctx), Ok(Value::NodeSet(vec![span])));
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct NsTestNode {
namespace_uri: Option<&'static str>,
local_name: &'static str,
}
impl<'a> Node<'a> for NsTestNode {
fn kind(self) -> NodeKind {
NodeKind::Element
}
fn parent(self) -> Option<Self> {
None
}
fn children(self) -> impl Iterator<Item = Self> + 'a {
std::iter::empty()
}
fn attributes(self) -> impl Iterator<Item = Self> + 'a {
std::iter::empty()
}
fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
std::iter::empty()
}
fn expanded_name(self) -> Option<crate::document::ExpandedName> {
Some(crate::document::ExpandedName {
namespace_uri: self.namespace_uri.map(str::to_string),
local_name: self.local_name.to_string(),
})
}
fn string_value(self) -> String {
String::new()
}
fn document_order(self, _other: Self) -> std::cmp::Ordering {
std::cmp::Ordering::Equal
}
}
fn ns_node() -> NsTestNode {
NsTestNode {
namespace_uri: Some("http://example/ns"),
local_name: "rect",
}
}
fn qname_test() -> NodeTest {
NodeTest::QName(QName {
prefix: Some("p".to_string()),
local: "rect".to_string(),
})
}
#[test]
fn qname_test_matches_when_hook_resolves_prefix_to_the_real_uri() {
let lookup =
|p: &str| -> Option<String> { (p == "p").then(|| "http://example/ns".to_string()) };
assert!(matches_node_test(
&qname_test(),
Axis::Child,
ns_node(),
Some(&lookup)
));
}
#[test]
fn qname_test_does_not_match_when_hook_does_not_know_the_prefix() {
let lookup = |_: &str| -> Option<String> { None };
assert!(!matches_node_test(
&qname_test(),
Axis::Child,
ns_node(),
Some(&lookup)
));
}
#[test]
fn qname_test_does_not_match_when_hook_present_but_unresolvable_even_if_raw_prefix_equals_the_uri()
{
let mut node = ns_node();
node.namespace_uri = Some("p");
let lookup = |_: &str| -> Option<String> { None };
assert!(!matches_node_test(
&qname_test(),
Axis::Child,
node,
Some(&lookup)
));
}
#[test]
fn qname_test_does_not_match_with_no_hook_at_all() {
assert!(!matches_node_test(
&qname_test(),
Axis::Child,
ns_node(),
None
));
}
#[test]
fn qname_test_does_not_match_with_no_hook_at_all_even_if_raw_prefix_equals_the_uri() {
let mut node = ns_node();
node.namespace_uri = Some("p");
assert!(!matches_node_test(&qname_test(), Axis::Child, node, None));
}
#[test]
fn namespace_wildcard_matches_when_hook_resolves_prefix_to_the_real_uri() {
let lookup =
|p: &str| -> Option<String> { (p == "p").then(|| "http://example/ns".to_string()) };
let test = NodeTest::NamespaceWildcard("p".to_string());
assert!(matches_node_test(
&test,
Axis::Child,
ns_node(),
Some(&lookup)
));
}
#[test]
fn namespace_wildcard_does_not_match_when_hook_does_not_know_the_prefix() {
let lookup = |_: &str| -> Option<String> { None };
let test = NodeTest::NamespaceWildcard("p".to_string());
assert!(!matches_node_test(
&test,
Axis::Child,
ns_node(),
Some(&lookup)
));
}
#[test]
fn namespace_wildcard_does_not_match_when_hook_present_but_unresolvable_even_if_raw_prefix_equals_the_uri()
{
let mut node = ns_node();
node.namespace_uri = Some("p");
let lookup = |_: &str| -> Option<String> { None };
let test = NodeTest::NamespaceWildcard("p".to_string());
assert!(!matches_node_test(&test, Axis::Child, node, Some(&lookup)));
}
#[test]
fn namespace_wildcard_does_not_match_with_no_hook_at_all() {
let test = NodeTest::NamespaceWildcard("p".to_string());
assert!(!matches_node_test(&test, Axis::Child, ns_node(), None));
}
#[derive(Debug)]
enum HookLifetimeEntry {
Root {
children: Vec<usize>,
},
Element {
#[allow(dead_code)]
parent: usize,
namespace_uri: Option<&'static str>,
local_name: &'static str,
},
}
#[derive(Debug)]
struct HookLifetimeArena(Vec<HookLifetimeEntry>);
#[derive(Clone, Copy, Debug)]
struct HookLifetimeNode<'a> {
arena: &'a HookLifetimeArena,
idx: usize,
}
impl<'a> PartialEq for HookLifetimeNode<'a> {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self.arena, other.arena) && self.idx == other.idx
}
}
impl<'a> Eq for HookLifetimeNode<'a> {}
impl<'a> Node<'a> for HookLifetimeNode<'a> {
fn kind(self) -> NodeKind {
match &self.arena.0[self.idx] {
HookLifetimeEntry::Root { .. } => NodeKind::Root,
HookLifetimeEntry::Element { .. } => NodeKind::Element,
}
}
fn parent(self) -> Option<Self> {
match &self.arena.0[self.idx] {
HookLifetimeEntry::Root { .. } => None,
HookLifetimeEntry::Element { parent, .. } => Some(HookLifetimeNode {
arena: self.arena,
idx: *parent,
}),
}
}
fn children(self) -> impl Iterator<Item = Self> + 'a {
let arena = self.arena;
let indices = match &arena.0[self.idx] {
HookLifetimeEntry::Root { children } => children.clone(),
HookLifetimeEntry::Element { .. } => Vec::new(),
};
indices
.into_iter()
.map(move |i| HookLifetimeNode { arena, idx: i })
}
fn attributes(self) -> impl Iterator<Item = Self> + 'a {
std::iter::empty()
}
fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
std::iter::empty()
}
fn expanded_name(self) -> Option<crate::document::ExpandedName> {
match &self.arena.0[self.idx] {
HookLifetimeEntry::Root { .. } => None,
HookLifetimeEntry::Element {
namespace_uri,
local_name,
..
} => Some(crate::document::ExpandedName {
namespace_uri: namespace_uri.map(str::to_string),
local_name: local_name.to_string(),
}),
}
}
fn string_value(self) -> String {
String::new()
}
fn document_order(self, other: Self) -> std::cmp::Ordering {
self.idx.cmp(&other.idx)
}
}
#[test]
fn evaluate_result_with_locally_scoped_namespace_hook_outlives_the_hook() {
let arena = HookLifetimeArena(vec![
HookLifetimeEntry::Root {
children: vec![1, 2],
},
HookLifetimeEntry::Element {
parent: 0,
namespace_uri: Some("http://example/ns"),
local_name: "rect",
},
HookLifetimeEntry::Element {
parent: 0,
namespace_uri: None,
local_name: "other",
},
]);
let root_node = HookLifetimeNode {
arena: &arena,
idx: 0,
};
let nodes = {
let bound_uri = "http://example/ns".to_string();
let namespace_hook = move |prefix: &str| -> Option<String> {
(prefix == "p").then(|| bound_uri.clone())
};
let ctx = EvaluationContext {
node: root_node,
position: 1,
size: 1,
variables: None,
namespaces: Some(&namespace_hook),
_ctx: std::marker::PhantomData,
};
let expr = crate::parse("p:rect").expect("test expression must parse");
match evaluate(&expr, &ctx).expect("expected Ok") {
Value::NodeSet(nodes) => nodes,
other => panic!("expected a node-set, got {other:?}"),
}
};
assert_eq!(nodes.len(), 1);
assert_eq!(
nodes[0],
HookLifetimeNode {
arena: &arena,
idx: 1
}
);
}
}