1use crate::ast::{
12 AdditiveOp, Axis, EqualityOp, Expr, FilterExpr, FunctionCall, LocationPath, MultiplicativeOp,
13 NodeTest, PathExpr, PrimaryExpr, QName, RelationalOp, Step,
14};
15use crate::axes;
16use crate::document::{Node, NodeKind};
17use crate::functions;
18use crate::value::{Value, string_to_number};
19
20pub type VariableLookup<'a, N> = dyn Fn(&QName) -> Option<Value<N>> + 'a;
23
24pub type NamespaceLookup<'a> = dyn Fn(&str) -> Option<String> + 'a;
30
31#[derive(Clone, Copy)]
62pub struct EvaluationContext<'ctx, 'hook, N: Node<'ctx>> {
63 pub node: N,
64 pub position: usize,
65 pub size: usize,
66 pub variables: Option<&'hook VariableLookup<'hook, N>>,
67 pub namespaces: Option<&'hook NamespaceLookup<'hook>>,
68 _ctx: std::marker::PhantomData<&'ctx ()>,
69}
70
71impl<'ctx, 'hook, N: Node<'ctx>> EvaluationContext<'ctx, 'hook, N> {
72 pub fn new(node: N) -> Self {
76 EvaluationContext {
77 node,
78 position: 1,
79 size: 1,
80 variables: None,
81 namespaces: None,
82 _ctx: std::marker::PhantomData,
83 }
84 }
85
86 fn with(&self, node: N, position: usize, size: usize) -> Self {
90 EvaluationContext {
91 node,
92 position,
93 size,
94 variables: self.variables,
95 namespaces: self.namespaces,
96 _ctx: std::marker::PhantomData,
97 }
98 }
99}
100
101#[derive(Debug, Clone, PartialEq)]
105pub enum EvalError {
106 UnknownFunction(QName),
112 UnboundVariable(QName),
114 ArgumentCount {
116 function: &'static str,
117 expected: usize,
118 got: usize,
119 },
120 ExpectedNodeSet { context: &'static str },
123}
124
125impl std::fmt::Display for EvalError {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 match self {
128 EvalError::UnknownFunction(name) => {
129 write!(f, "unknown function: {}", format_qname(name))
130 }
131 EvalError::UnboundVariable(name) => {
132 write!(f, "unbound variable: ${}", format_qname(name))
133 }
134 EvalError::ArgumentCount {
135 function,
136 expected,
137 got,
138 } => write!(f, "{function}() expects {expected} argument(s), got {got}"),
139 EvalError::ExpectedNodeSet { context } => {
140 write!(f, "expected a node-set in {context}")
141 }
142 }
143 }
144}
145
146impl std::error::Error for EvalError {}
147
148fn format_qname(q: &QName) -> String {
149 match &q.prefix {
150 Some(p) => format!("{p}:{}", q.local),
151 None => q.local.clone(),
152 }
153}
154
155pub fn evaluate<'ctx, 'hook, N: Node<'ctx>>(
157 expr: &Expr,
158 ctx: &EvaluationContext<'ctx, 'hook, N>,
159) -> Result<Value<N>, EvalError> {
160 match expr {
161 Expr::Or(l, r) => {
167 if evaluate(l, ctx)?.to_boolean() {
168 Ok(Value::Boolean(true))
169 } else {
170 Ok(Value::Boolean(evaluate(r, ctx)?.to_boolean()))
171 }
172 }
173 Expr::And(l, r) => {
174 if !evaluate(l, ctx)?.to_boolean() {
175 Ok(Value::Boolean(false))
176 } else {
177 Ok(Value::Boolean(evaluate(r, ctx)?.to_boolean()))
178 }
179 }
180 Expr::Equality(l, op, r) => {
181 let lv = evaluate(l, ctx)?;
182 let rv = evaluate(r, ctx)?;
183 Ok(Value::Boolean(compare_equality(&lv, *op, &rv)))
184 }
185 Expr::Relational(l, op, r) => {
186 let lv = evaluate(l, ctx)?;
187 let rv = evaluate(r, ctx)?;
188 Ok(Value::Boolean(compare_relational(&lv, *op, &rv)))
189 }
190 Expr::Additive(l, op, r) => {
191 let lv = evaluate(l, ctx)?.to_number();
192 let rv = evaluate(r, ctx)?.to_number();
193 Ok(Value::Number(match op {
194 AdditiveOp::Add => lv + rv,
195 AdditiveOp::Sub => lv - rv,
196 }))
197 }
198 Expr::Multiplicative(l, op, r) => {
199 let lv = evaluate(l, ctx)?.to_number();
200 let rv = evaluate(r, ctx)?.to_number();
201 Ok(Value::Number(match op {
202 MultiplicativeOp::Mul => lv * rv,
203 MultiplicativeOp::Div => lv / rv,
207 MultiplicativeOp::Mod => lv % rv,
208 }))
209 }
210 Expr::Negate(e) => Ok(Value::Number(-evaluate(e, ctx)?.to_number())),
211 Expr::Union(l, r) => {
212 let lv = evaluate(l, ctx)?;
213 let rv = evaluate(r, ctx)?;
214 match (lv, rv) {
215 (Value::NodeSet(mut a), Value::NodeSet(b)) => {
216 a.extend(b);
217 sort_dedup(&mut a);
218 Ok(Value::NodeSet(a))
219 }
220 _ => Err(EvalError::ExpectedNodeSet {
221 context: "union operand",
222 }),
223 }
224 }
225 Expr::Path(path) => evaluate_path(path, ctx),
226 }
227}
228
229fn eq_result<T: PartialEq>(op: EqualityOp, l: T, r: T) -> bool {
232 match op {
233 EqualityOp::Eq => l == r,
234 EqualityOp::Ne => l != r,
235 }
236}
237
238fn rel_result(op: RelationalOp, l: f64, r: f64) -> bool {
239 match op {
240 RelationalOp::Lt => l < r,
241 RelationalOp::Le => l <= r,
242 RelationalOp::Gt => l > r,
243 RelationalOp::Ge => l >= r,
244 }
245}
246
247fn compare_equality<'a, N: Node<'a>>(lhs: &Value<N>, op: EqualityOp, rhs: &Value<N>) -> bool {
251 match (lhs, rhs) {
252 (Value::NodeSet(a), Value::NodeSet(b)) => a.iter().copied().any(|na| {
253 b.iter()
254 .copied()
255 .any(|nb| eq_result(op, na.string_value(), nb.string_value()))
256 }),
257 (Value::NodeSet(a), Value::Number(n)) | (Value::Number(n), Value::NodeSet(a)) => a
258 .iter()
259 .copied()
260 .any(|na| eq_result(op, string_to_number(&na.string_value()), *n)),
261 (Value::NodeSet(a), Value::String(s)) | (Value::String(s), Value::NodeSet(a)) => a
262 .iter()
263 .copied()
264 .any(|na| eq_result(op, na.string_value(), s.clone())),
265 (Value::NodeSet(a), Value::Boolean(b)) | (Value::Boolean(b), Value::NodeSet(a)) => {
266 eq_result(op, !a.is_empty(), *b)
267 }
268 _ => {
269 if matches!(lhs, Value::Boolean(_)) || matches!(rhs, Value::Boolean(_)) {
273 eq_result(op, lhs.to_boolean(), rhs.to_boolean())
274 } else if matches!(lhs, Value::Number(_)) || matches!(rhs, Value::Number(_)) {
275 eq_result(op, lhs.to_number(), rhs.to_number())
276 } else {
277 eq_result(op, lhs.to_xpath_string(), rhs.to_xpath_string())
278 }
279 }
280 }
281}
282
283fn compare_relational<'a, N: Node<'a>>(lhs: &Value<N>, op: RelationalOp, rhs: &Value<N>) -> bool {
294 match (lhs, rhs) {
295 (Value::NodeSet(a), Value::NodeSet(b)) => a.iter().copied().any(|na| {
296 b.iter().copied().any(|nb| {
297 rel_result(
298 op,
299 string_to_number(&na.string_value()),
300 string_to_number(&nb.string_value()),
301 )
302 })
303 }),
304 (Value::NodeSet(a), Value::Number(n)) => a
305 .iter()
306 .copied()
307 .any(|na| rel_result(op, string_to_number(&na.string_value()), *n)),
308 (Value::Number(n), Value::NodeSet(a)) => a
309 .iter()
310 .copied()
311 .any(|na| rel_result(op, *n, string_to_number(&na.string_value()))),
312 (Value::NodeSet(a), Value::String(s)) => {
313 let sn = string_to_number(s);
314 a.iter()
315 .copied()
316 .any(|na| rel_result(op, string_to_number(&na.string_value()), sn))
317 }
318 (Value::String(s), Value::NodeSet(a)) => {
319 let sn = string_to_number(s);
320 a.iter()
321 .copied()
322 .any(|na| rel_result(op, sn, string_to_number(&na.string_value())))
323 }
324 (Value::NodeSet(a), Value::Boolean(b)) => {
325 rel_result(op, bool_to_number(!a.is_empty()), bool_to_number(*b))
326 }
327 (Value::Boolean(b), Value::NodeSet(a)) => {
328 rel_result(op, bool_to_number(*b), bool_to_number(!a.is_empty()))
329 }
330 _ => rel_result(op, lhs.to_number(), rhs.to_number()),
331 }
332}
333
334fn bool_to_number(b: bool) -> f64 {
335 if b { 1.0 } else { 0.0 }
336}
337
338fn root_of<'a, N: Node<'a>>(n: N) -> N {
341 let mut cur = n;
342 while let Some(p) = cur.parent() {
343 cur = p;
344 }
345 cur
346}
347
348fn sort_dedup<'a, N: Node<'a>>(nodes: &mut Vec<N>) {
349 nodes.sort_by(|a, b| a.document_order(*b));
350 nodes.dedup();
351}
352
353fn axis_nodes<'a, N: Node<'a>>(axis: Axis, n: N) -> Vec<N> {
354 match axis {
355 Axis::Ancestor => axes::ancestor(n).collect(),
356 Axis::AncestorOrSelf => axes::ancestor_or_self(n).collect(),
357 Axis::Attribute => axes::attribute(n).collect(),
358 Axis::Child => axes::child(n).collect(),
359 Axis::Descendant => axes::descendant(n).collect(),
360 Axis::DescendantOrSelf => axes::descendant_or_self(n).collect(),
361 Axis::Following => axes::following(n).collect(),
362 Axis::FollowingSibling => axes::following_sibling(n).collect(),
363 Axis::Namespace => axes::namespace(n).collect(),
364 Axis::Parent => axes::parent(n).collect(),
365 Axis::Preceding => axes::preceding(n).collect(),
366 Axis::PrecedingSibling => axes::preceding_sibling(n).collect(),
367 Axis::SelfAxis => axes::self_axis(n).collect(),
368 }
369}
370
371fn principal_node_kind(axis: Axis) -> NodeKind {
377 match axis {
378 Axis::Attribute => NodeKind::Attribute,
379 Axis::Namespace => NodeKind::Namespace,
380 _ => NodeKind::Element,
381 }
382}
383
384enum PrefixResolution {
386 Value(String),
390 Unbound,
396}
397
398fn resolve_prefix<'hook>(
402 prefix: &str,
403 namespaces: Option<&NamespaceLookup<'hook>>,
404) -> PrefixResolution {
405 match namespaces {
406 None => PrefixResolution::Value(prefix.to_string()),
407 Some(lookup) => match lookup(prefix) {
408 Some(uri) => PrefixResolution::Value(uri),
409 None => PrefixResolution::Unbound,
410 },
411 }
412}
413
414fn matches_node_test<'ctx, 'hook, N: Node<'ctx>>(
425 test: &NodeTest,
426 axis: Axis,
427 n: N,
428 namespaces: Option<&NamespaceLookup<'hook>>,
429) -> bool {
430 match test {
431 NodeTest::Node => true,
432 NodeTest::Text => n.kind() == NodeKind::Text,
433 NodeTest::Comment => n.kind() == NodeKind::Comment,
434 NodeTest::ProcessingInstruction(target) => {
435 n.kind() == NodeKind::ProcessingInstruction
436 && match target {
437 None => true,
438 Some(t) => n.expanded_name().is_some_and(|e| e.local_name == *t),
439 }
440 }
441 NodeTest::AnyName => n.kind() == principal_node_kind(axis) && n.expanded_name().is_some(),
442 NodeTest::NamespaceWildcard(prefix) => {
443 let uri = match resolve_prefix(prefix, namespaces) {
444 PrefixResolution::Unbound => return false,
445 PrefixResolution::Value(uri) => uri,
446 };
447 n.kind() == principal_node_kind(axis)
448 && n.expanded_name()
449 .is_some_and(|e| e.namespace_uri.as_deref() == Some(uri.as_str()))
450 }
451 NodeTest::QName(qname) => {
452 let uri = match &qname.prefix {
453 None => None,
454 Some(p) => match resolve_prefix(p, namespaces) {
455 PrefixResolution::Unbound => return false,
456 PrefixResolution::Value(uri) => Some(uri),
457 },
458 };
459 n.kind() == principal_node_kind(axis)
460 && n.expanded_name().is_some_and(|e| {
461 e.local_name == qname.local && e.namespace_uri.as_deref() == uri.as_deref()
462 })
463 }
464 }
465}
466
467fn predicate_truth<'a, N: Node<'a>>(val: &Value<N>, position: usize) -> bool {
474 match val {
475 Value::Number(n) => *n == position as f64,
476 _ => val.to_boolean(),
477 }
478}
479
480fn apply_predicates<'ctx, 'hook, N: Node<'ctx>>(
487 predicates: &[Expr],
488 nodes: Vec<N>,
489 ctx: &EvaluationContext<'ctx, 'hook, N>,
490) -> Result<Vec<N>, EvalError> {
491 let mut current = nodes;
492 for pred in predicates {
493 let size = current.len();
494 let mut next = Vec::with_capacity(current.len());
495 for (i, &n) in current.iter().enumerate() {
496 let position = i + 1;
497 let pred_ctx = ctx.with(n, position, size);
498 let val = evaluate(pred, &pred_ctx)?;
499 if predicate_truth(&val, position) {
500 next.push(n);
501 }
502 }
503 current = next;
504 }
505 Ok(current)
506}
507
508fn evaluate_step<'ctx, 'hook, N: Node<'ctx>>(
514 step: &Step,
515 context_nodes: &[N],
516 ctx: &EvaluationContext<'ctx, 'hook, N>,
517) -> Result<Vec<N>, EvalError> {
518 let mut result = Vec::new();
519 for &n in context_nodes {
520 let candidates: Vec<N> = axis_nodes(step.axis, n)
521 .into_iter()
522 .filter(|&c| matches_node_test(&step.node_test, step.axis, c, ctx.namespaces))
523 .collect();
524 let filtered = apply_predicates(&step.predicates, candidates, ctx)?;
525 result.extend(filtered);
526 }
527 sort_dedup(&mut result);
528 Ok(result)
529}
530
531fn evaluate_location_steps<'ctx, 'hook, N: Node<'ctx>>(
532 steps: &[Step],
533 mut nodes: Vec<N>,
534 ctx: &EvaluationContext<'ctx, 'hook, N>,
535) -> Result<Vec<N>, EvalError> {
536 for step in steps {
537 nodes = evaluate_step(step, &nodes, ctx)?;
538 }
539 Ok(nodes)
540}
541
542fn evaluate_location_path<'ctx, 'hook, N: Node<'ctx>>(
543 loc: &LocationPath,
544 start: Vec<N>,
545 ctx: &EvaluationContext<'ctx, 'hook, N>,
546) -> Result<Vec<N>, EvalError> {
547 let start = if loc.is_absolute {
548 start.first().map(|&n| vec![root_of(n)]).unwrap_or_default()
549 } else {
550 start
551 };
552 evaluate_location_steps(&loc.steps, start, ctx)
553}
554
555fn evaluate_path<'ctx, 'hook, N: Node<'ctx>>(
556 path: &PathExpr,
557 ctx: &EvaluationContext<'ctx, 'hook, N>,
558) -> Result<Value<N>, EvalError> {
559 match path {
560 PathExpr::Location(loc) => {
561 let result = evaluate_location_path(loc, vec![ctx.node], ctx)?;
562 Ok(Value::NodeSet(result))
563 }
564 PathExpr::Filter(filter) => evaluate_filter_expr(filter, ctx),
565 PathExpr::FilterLocation(filter, loc) => {
566 let filter_val = evaluate_filter_expr(filter, ctx)?;
567 let start = match filter_val {
568 Value::NodeSet(nodes) => nodes,
569 _ => {
570 return Err(EvalError::ExpectedNodeSet {
571 context: "path expression left of '/'",
572 });
573 }
574 };
575 let result = evaluate_location_path(loc, start, ctx)?;
576 Ok(Value::NodeSet(result))
577 }
578 }
579}
580
581fn evaluate_filter_expr<'ctx, 'hook, N: Node<'ctx>>(
582 filter: &FilterExpr,
583 ctx: &EvaluationContext<'ctx, 'hook, N>,
584) -> Result<Value<N>, EvalError> {
585 let primary_val = evaluate_primary(&filter.primary, ctx)?;
586 if filter.predicates.is_empty() {
587 return Ok(primary_val);
588 }
589 match primary_val {
590 Value::NodeSet(mut nodes) => {
591 sort_dedup(&mut nodes);
598 let filtered = apply_predicates(&filter.predicates, nodes, ctx)?;
599 Ok(Value::NodeSet(filtered))
600 }
601 _ => Err(EvalError::ExpectedNodeSet {
602 context: "filter expression predicate",
603 }),
604 }
605}
606
607fn evaluate_primary<'ctx, 'hook, N: Node<'ctx>>(
608 primary: &PrimaryExpr,
609 ctx: &EvaluationContext<'ctx, 'hook, N>,
610) -> Result<Value<N>, EvalError> {
611 match primary {
612 PrimaryExpr::Variable(qname) => {
613 let bound = ctx.variables.and_then(|lookup| lookup(qname));
614 bound.ok_or_else(|| EvalError::UnboundVariable(qname.clone()))
615 }
616 PrimaryExpr::Parenthesized(inner) => evaluate(inner, ctx),
617 PrimaryExpr::Literal(s) => Ok(Value::String(s.clone())),
618 PrimaryExpr::Number(n) => Ok(Value::Number(*n)),
619 PrimaryExpr::Function(call) => evaluate_function(call, ctx),
620 }
621}
622
623fn check_arity(function: &'static str, args: &[Expr], expected: usize) -> Result<(), EvalError> {
624 if args.len() == expected {
625 Ok(())
626 } else {
627 Err(EvalError::ArgumentCount {
628 function,
629 expected,
630 got: args.len(),
631 })
632 }
633}
634
635fn evaluate_function<'ctx, 'hook, N: Node<'ctx>>(
642 call: &FunctionCall,
643 ctx: &EvaluationContext<'ctx, 'hook, N>,
644) -> Result<Value<N>, EvalError> {
645 if call.name.prefix.is_none() {
646 match call.name.local.as_str() {
647 "last" => {
648 check_arity("last", &call.args, 0)?;
649 return Ok(Value::Number(ctx.size as f64));
650 }
651 "position" => {
652 check_arity("position", &call.args, 0)?;
653 return Ok(Value::Number(ctx.position as f64));
654 }
655 "count" => {
656 check_arity("count", &call.args, 1)?;
657 return match evaluate(&call.args[0], ctx)? {
658 Value::NodeSet(nodes) => Ok(Value::Number(nodes.len() as f64)),
659 _ => Err(EvalError::ExpectedNodeSet {
660 context: "count() argument",
661 }),
662 };
663 }
664 _ => {}
665 }
666 }
667 functions::dispatch(call, ctx)
668}
669
670#[cfg(test)]
671mod tests {
672 use super::*;
673 use crate::document::fixture::{Fixture, TestNode, build};
674
675 fn eval_str<'a>(src: &str, node: TestNode<'a>) -> Result<Value<TestNode<'a>>, EvalError> {
676 let expr = crate::parse(src).expect("test expression must parse");
677 let ctx = EvaluationContext::new(node);
678 evaluate(&expr, &ctx)
679 }
680
681 fn node_set<'a>(v: Result<Value<TestNode<'a>>, EvalError>) -> Vec<TestNode<'a>> {
682 match v.expect("expected Ok") {
683 Value::NodeSet(nodes) => nodes,
684 other => panic!("expected a node-set, got {other:?}"),
685 }
686 }
687
688 fn root(f: &Fixture) -> TestNode<'_> {
689 f.doc.node(f.html)
690 }
691
692 #[test]
695 fn or_short_circuits_when_left_is_true() {
696 let f = build();
697 assert_eq!(
698 eval_str("(1 = 1) or unknownfn()", root(&f)),
699 Ok(Value::Boolean(true))
700 );
701 }
702
703 #[test]
704 fn or_evaluates_right_when_left_is_false() {
705 let f = build();
706 assert!(matches!(
707 eval_str("(1 = 0) or unknownfn()", root(&f)),
708 Err(EvalError::UnknownFunction(_))
709 ));
710 }
711
712 #[test]
713 fn and_short_circuits_when_left_is_false() {
714 let f = build();
715 assert_eq!(
716 eval_str("(1 = 0) and unknownfn()", root(&f)),
717 Ok(Value::Boolean(false))
718 );
719 }
720
721 #[test]
722 fn and_evaluates_right_when_left_is_true() {
723 let f = build();
724 assert!(matches!(
725 eval_str("(1 = 1) and unknownfn()", root(&f)),
726 Err(EvalError::UnknownFunction(_))
727 ));
728 }
729
730 #[test]
733 fn equality_node_set_vs_node_set() {
734 let f = build();
735 assert_eq!(
736 eval_str(
737 "/html/body/span/@data-x = /html/body/span/@data-x",
738 root(&f)
739 ),
740 Ok(Value::Boolean(true))
741 );
742 assert_eq!(
743 eval_str("/html/body/span/@data-x = /html/@id", root(&f)),
744 Ok(Value::Boolean(false))
745 );
746 assert_eq!(
747 eval_str("/html/body/span/@data-x != /html/@id", root(&f)),
748 Ok(Value::Boolean(true))
749 );
750 }
751
752 #[test]
753 fn equality_node_set_vs_number() {
754 let f = build();
755 assert_eq!(
756 eval_str("/html/body/span/@data-x = 1", root(&f)),
757 Ok(Value::Boolean(true))
758 );
759 assert_eq!(
760 eval_str("/html/body/span/@data-x = 2", root(&f)),
761 Ok(Value::Boolean(false))
762 );
763 assert_eq!(
764 eval_str("/html/body/span/@data-x != 2", root(&f)),
765 Ok(Value::Boolean(true))
766 );
767 }
768
769 #[test]
770 fn equality_node_set_vs_string() {
771 let f = build();
772 assert_eq!(
773 eval_str("/html/body/span/@data-x = '1'", root(&f)),
774 Ok(Value::Boolean(true))
775 );
776 assert_eq!(
777 eval_str("/html/@id = 'nope'", root(&f)),
778 Ok(Value::Boolean(false))
779 );
780 }
781
782 #[test]
783 fn equality_node_set_vs_boolean() {
784 let f = build();
785 assert_eq!(
787 eval_str("/html/body/span/@data-x = (1 = 1)", root(&f)),
788 Ok(Value::Boolean(true))
789 );
790 assert_eq!(
792 eval_str("/html/body/nonexistent = (1 = 1)", root(&f)),
793 Ok(Value::Boolean(false))
794 );
795 }
796
797 #[test]
800 fn relational_with_node_set_operand() {
801 let f = build();
802 assert_eq!(
803 eval_str("/html/body/span/@data-x > 0", root(&f)),
804 Ok(Value::Boolean(true))
805 );
806 assert_eq!(
807 eval_str("/html/body/span/@data-x < 0", root(&f)),
808 Ok(Value::Boolean(false))
809 );
810 assert_eq!(
812 eval_str("/html/body/span/@data-x > '0'", root(&f)),
813 Ok(Value::Boolean(true))
814 );
815 assert_eq!(
817 eval_str("/html/body/span/@data-x > (1 = 0)", root(&f)),
818 Ok(Value::Boolean(true))
819 );
820 }
821
822 #[test]
825 fn arithmetic_operators() {
826 let f = build();
827 assert_eq!(eval_str("1 + 2", root(&f)), Ok(Value::Number(3.0)));
828 assert_eq!(eval_str("1 - 2", root(&f)), Ok(Value::Number(-1.0)));
829 assert_eq!(eval_str("2 * 3", root(&f)), Ok(Value::Number(6.0)));
830 assert_eq!(eval_str("7 div 2", root(&f)), Ok(Value::Number(3.5)));
831 assert_eq!(eval_str("7 mod 3", root(&f)), Ok(Value::Number(1.0)));
832 assert_eq!(eval_str("-1 + 2", root(&f)), Ok(Value::Number(1.0)));
833 }
834
835 #[test]
838 fn union_deduplicates_overlapping_node_sets() {
839 let f = build();
840 let nodes = node_set(eval_str(
841 "/html/body/child::node() | /html/body/child::*",
842 root(&f),
843 ));
844 assert_eq!(
845 nodes,
846 vec![
847 f.doc.node(f.text0),
848 f.doc.node(f.span),
849 f.doc.node(f.comment),
850 f.doc.node(f.pi),
851 f.doc.node(f.text2),
852 ],
853 "span (present in both operands) must appear exactly once, in document order"
854 );
855 }
856
857 #[test]
860 fn multi_step_path_with_numeric_predicate() {
861 let f = build();
862 let nodes = node_set(eval_str("/html/body/child::node()[3]", root(&f)));
863 assert_eq!(nodes, vec![f.doc.node(f.comment)]);
864 }
865
866 #[test]
867 fn multi_step_mixed_axis_path_with_predicate() {
868 let f = build();
869 let nodes = node_set(eval_str("/html//text()[2]", root(&f)));
882 assert_eq!(nodes, vec![f.doc.node(f.text2)]);
883 }
884
885 #[test]
888 fn predicate_order_changes_the_result() {
889 let f = build();
890 let position_then_test = node_set(eval_str(
893 "/html/body/child::node()[2][self::span]",
894 root(&f),
895 ));
896 assert_eq!(
897 position_then_test,
898 vec![f.doc.node(f.span)],
899 "[2] keeps the 2nd child (span); [self::span] then confirms it against itself"
900 );
901
902 let test_then_position = node_set(eval_str(
903 "/html/body/child::node()[self::span][2]",
904 root(&f),
905 ));
906 assert_eq!(
907 test_then_position,
908 Vec::<TestNode>::new(),
909 "[self::span] first narrows to just [span] (size 1); [2] then has nothing at position 2"
910 );
911 }
912
913 #[test]
916 fn last_position_count_in_predicates() {
917 let f = build();
918 assert_eq!(
919 node_set(eval_str("/html/body/child::node()[last()]", root(&f))),
920 vec![f.doc.node(f.text2)]
921 );
922 assert_eq!(
923 node_set(eval_str(
924 "/html/body/child::node()[position() > 1]",
925 root(&f)
926 )),
927 vec![
928 f.doc.node(f.span),
929 f.doc.node(f.comment),
930 f.doc.node(f.pi),
931 f.doc.node(f.text2),
932 ]
933 );
934 assert_eq!(
935 eval_str("count(/html/body/child::node())", root(&f)),
936 Ok(Value::Number(5.0))
937 );
938 }
939
940 #[test]
943 fn numeric_predicate_selects_by_position() {
944 let f = build();
945 let nodes = node_set(eval_str("/html/body/child::node()[3]", root(&f)));
946 assert_eq!(nodes, vec![f.doc.node(f.comment)]);
947 }
948
949 #[test]
950 fn non_numeric_predicate_uses_boolean_coercion() {
951 let f = build();
952 let nodes = node_set(eval_str("/html/body/child::node()[span]", root(&f)));
956 assert_eq!(nodes, Vec::<TestNode>::new());
957 }
958
959 #[test]
962 fn unknown_function_is_an_error() {
963 let f = build();
964 assert!(matches!(
965 eval_str("foo()", root(&f)),
966 Err(EvalError::UnknownFunction(_))
967 ));
968 }
969
970 #[test]
971 fn unbound_variable_is_an_error() {
972 let f = build();
973 assert!(matches!(
974 eval_str("$missing", root(&f)),
975 Err(EvalError::UnboundVariable(_))
976 ));
977 }
978
979 #[test]
980 fn wrong_arity_is_an_error() {
981 let f = build();
982 assert!(matches!(
983 eval_str("last(1)", root(&f)),
984 Err(EvalError::ArgumentCount {
985 function: "last",
986 expected: 0,
987 got: 1
988 })
989 ));
990 assert!(matches!(
991 eval_str("count()", root(&f)),
992 Err(EvalError::ArgumentCount {
993 function: "count",
994 expected: 1,
995 got: 0
996 })
997 ));
998 }
999
1000 #[test]
1001 fn count_of_non_node_set_is_an_error() {
1002 let f = build();
1003 assert!(matches!(
1004 eval_str("count(1)", root(&f)),
1005 Err(EvalError::ExpectedNodeSet { .. })
1006 ));
1007 }
1008
1009 #[test]
1012 fn bound_variable_resolves_via_the_lookup_hook() {
1013 let f = build();
1014 let lookup = |q: &QName| -> Option<Value<TestNode<'_>>> {
1015 if q.prefix.is_none() && q.local == "x" {
1016 Some(Value::Number(42.0))
1017 } else {
1018 None
1019 }
1020 };
1021 let ctx = EvaluationContext {
1022 node: root(&f),
1023 position: 1,
1024 size: 1,
1025 variables: Some(&lookup),
1026 namespaces: None,
1027 _ctx: std::marker::PhantomData,
1028 };
1029 let expr = crate::parse("$x + 1").unwrap();
1030 assert_eq!(evaluate(&expr, &ctx), Ok(Value::Number(43.0)));
1031 }
1032
1033 #[test]
1034 fn filter_expr_sorts_a_variable_supplied_node_set_into_document_order_before_predicates() {
1035 let f = build();
1036 let span = f.doc.node(f.span);
1037 let pi = f.doc.node(f.pi);
1038 let lookup = |q: &QName| -> Option<Value<TestNode<'_>>> {
1043 if q.prefix.is_none() && q.local == "nodes" {
1044 Some(Value::NodeSet(vec![pi, span]))
1045 } else {
1046 None
1047 }
1048 };
1049 let ctx = EvaluationContext {
1050 node: root(&f),
1051 position: 1,
1052 size: 1,
1053 variables: Some(&lookup),
1054 namespaces: None,
1055 _ctx: std::marker::PhantomData,
1056 };
1057 let expr = crate::parse("$nodes[1]").unwrap();
1058 assert_eq!(evaluate(&expr, &ctx), Ok(Value::NodeSet(vec![span])));
1059 }
1060
1061 #[derive(Clone, Copy, PartialEq, Eq)]
1075 struct NsTestNode {
1076 namespace_uri: Option<&'static str>,
1077 local_name: &'static str,
1078 }
1079
1080 impl<'a> Node<'a> for NsTestNode {
1081 fn kind(self) -> NodeKind {
1082 NodeKind::Element
1083 }
1084
1085 fn parent(self) -> Option<Self> {
1086 None
1087 }
1088
1089 fn children(self) -> impl Iterator<Item = Self> + 'a {
1090 std::iter::empty()
1091 }
1092
1093 fn attributes(self) -> impl Iterator<Item = Self> + 'a {
1094 std::iter::empty()
1095 }
1096
1097 fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
1098 std::iter::empty()
1099 }
1100
1101 fn expanded_name(self) -> Option<crate::document::ExpandedName> {
1102 Some(crate::document::ExpandedName {
1103 namespace_uri: self.namespace_uri.map(str::to_string),
1104 local_name: self.local_name.to_string(),
1105 })
1106 }
1107
1108 fn string_value(self) -> String {
1109 String::new()
1110 }
1111
1112 fn document_order(self, _other: Self) -> std::cmp::Ordering {
1113 std::cmp::Ordering::Equal
1114 }
1115 }
1116
1117 fn ns_node() -> NsTestNode {
1118 NsTestNode {
1119 namespace_uri: Some("http://example/ns"),
1120 local_name: "rect",
1121 }
1122 }
1123
1124 fn qname_test() -> NodeTest {
1125 NodeTest::QName(QName {
1126 prefix: Some("p".to_string()),
1127 local: "rect".to_string(),
1128 })
1129 }
1130
1131 #[test]
1132 fn qname_test_matches_when_hook_resolves_prefix_to_the_real_uri() {
1133 let lookup =
1134 |p: &str| -> Option<String> { (p == "p").then(|| "http://example/ns".to_string()) };
1135 assert!(matches_node_test(
1136 &qname_test(),
1137 Axis::Child,
1138 ns_node(),
1139 Some(&lookup)
1140 ));
1141 }
1142
1143 #[test]
1144 fn qname_test_does_not_match_when_hook_does_not_know_the_prefix() {
1145 let lookup = |_: &str| -> Option<String> { None };
1149 assert!(!matches_node_test(
1150 &qname_test(),
1151 Axis::Child,
1152 ns_node(),
1153 Some(&lookup)
1154 ));
1155 }
1156
1157 #[test]
1158 fn qname_test_does_not_match_when_hook_present_but_unresolvable_even_if_raw_prefix_equals_the_uri()
1159 {
1160 let mut node = ns_node();
1165 node.namespace_uri = Some("p");
1166 let lookup = |_: &str| -> Option<String> { None };
1167 assert!(!matches_node_test(
1168 &qname_test(),
1169 Axis::Child,
1170 node,
1171 Some(&lookup)
1172 ));
1173 }
1174
1175 #[test]
1176 fn qname_test_does_not_match_with_no_hook_at_all() {
1177 assert!(!matches_node_test(
1181 &qname_test(),
1182 Axis::Child,
1183 ns_node(),
1184 None
1185 ));
1186 }
1187
1188 #[test]
1189 fn qname_test_matches_with_no_hook_at_all_when_raw_prefix_equals_the_uri() {
1190 let mut node = ns_node();
1196 node.namespace_uri = Some("p");
1197 assert!(matches_node_test(&qname_test(), Axis::Child, node, None));
1198 }
1199
1200 #[test]
1201 fn namespace_wildcard_matches_when_hook_resolves_prefix_to_the_real_uri() {
1202 let lookup =
1203 |p: &str| -> Option<String> { (p == "p").then(|| "http://example/ns".to_string()) };
1204 let test = NodeTest::NamespaceWildcard("p".to_string());
1205 assert!(matches_node_test(
1206 &test,
1207 Axis::Child,
1208 ns_node(),
1209 Some(&lookup)
1210 ));
1211 }
1212
1213 #[test]
1214 fn namespace_wildcard_does_not_match_when_hook_does_not_know_the_prefix() {
1215 let lookup = |_: &str| -> Option<String> { None };
1216 let test = NodeTest::NamespaceWildcard("p".to_string());
1217 assert!(!matches_node_test(
1218 &test,
1219 Axis::Child,
1220 ns_node(),
1221 Some(&lookup)
1222 ));
1223 }
1224
1225 #[test]
1226 fn namespace_wildcard_does_not_match_when_hook_present_but_unresolvable_even_if_raw_prefix_equals_the_uri()
1227 {
1228 let mut node = ns_node();
1229 node.namespace_uri = Some("p");
1230 let lookup = |_: &str| -> Option<String> { None };
1231 let test = NodeTest::NamespaceWildcard("p".to_string());
1232 assert!(!matches_node_test(&test, Axis::Child, node, Some(&lookup)));
1233 }
1234
1235 #[test]
1236 fn namespace_wildcard_does_not_match_with_no_hook_at_all() {
1237 let test = NodeTest::NamespaceWildcard("p".to_string());
1238 assert!(!matches_node_test(&test, Axis::Child, ns_node(), None));
1239 }
1240
1241 #[derive(Debug)]
1250 enum HookLifetimeEntry {
1251 Root {
1252 children: Vec<usize>,
1253 },
1254 Element {
1255 #[allow(dead_code)]
1256 parent: usize,
1257 namespace_uri: Option<&'static str>,
1258 local_name: &'static str,
1259 },
1260 }
1261
1262 #[derive(Debug)]
1263 struct HookLifetimeArena(Vec<HookLifetimeEntry>);
1264
1265 #[derive(Clone, Copy, Debug)]
1266 struct HookLifetimeNode<'a> {
1267 arena: &'a HookLifetimeArena,
1268 idx: usize,
1269 }
1270
1271 impl<'a> PartialEq for HookLifetimeNode<'a> {
1272 fn eq(&self, other: &Self) -> bool {
1273 std::ptr::eq(self.arena, other.arena) && self.idx == other.idx
1274 }
1275 }
1276 impl<'a> Eq for HookLifetimeNode<'a> {}
1277
1278 impl<'a> Node<'a> for HookLifetimeNode<'a> {
1279 fn kind(self) -> NodeKind {
1280 match &self.arena.0[self.idx] {
1281 HookLifetimeEntry::Root { .. } => NodeKind::Root,
1282 HookLifetimeEntry::Element { .. } => NodeKind::Element,
1283 }
1284 }
1285 fn parent(self) -> Option<Self> {
1286 match &self.arena.0[self.idx] {
1287 HookLifetimeEntry::Root { .. } => None,
1288 HookLifetimeEntry::Element { parent, .. } => Some(HookLifetimeNode {
1289 arena: self.arena,
1290 idx: *parent,
1291 }),
1292 }
1293 }
1294 fn children(self) -> impl Iterator<Item = Self> + 'a {
1295 let arena = self.arena;
1296 let indices = match &arena.0[self.idx] {
1297 HookLifetimeEntry::Root { children } => children.clone(),
1298 HookLifetimeEntry::Element { .. } => Vec::new(),
1299 };
1300 indices
1301 .into_iter()
1302 .map(move |i| HookLifetimeNode { arena, idx: i })
1303 }
1304 fn attributes(self) -> impl Iterator<Item = Self> + 'a {
1305 std::iter::empty()
1306 }
1307 fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
1308 std::iter::empty()
1309 }
1310 fn expanded_name(self) -> Option<crate::document::ExpandedName> {
1311 match &self.arena.0[self.idx] {
1312 HookLifetimeEntry::Root { .. } => None,
1313 HookLifetimeEntry::Element {
1314 namespace_uri,
1315 local_name,
1316 ..
1317 } => Some(crate::document::ExpandedName {
1318 namespace_uri: namespace_uri.map(str::to_string),
1319 local_name: local_name.to_string(),
1320 }),
1321 }
1322 }
1323 fn string_value(self) -> String {
1324 String::new()
1325 }
1326 fn document_order(self, other: Self) -> std::cmp::Ordering {
1327 self.idx.cmp(&other.idx)
1328 }
1329 }
1330
1331 #[test]
1332 fn evaluate_result_with_locally_scoped_namespace_hook_outlives_the_hook() {
1333 let arena = HookLifetimeArena(vec![
1337 HookLifetimeEntry::Root {
1338 children: vec![1, 2],
1339 },
1340 HookLifetimeEntry::Element {
1341 parent: 0,
1342 namespace_uri: Some("http://example/ns"),
1343 local_name: "rect",
1344 },
1345 HookLifetimeEntry::Element {
1346 parent: 0,
1347 namespace_uri: None,
1348 local_name: "other",
1349 },
1350 ]);
1351 let root_node = HookLifetimeNode {
1352 arena: &arena,
1353 idx: 0,
1354 };
1355
1356 let nodes = {
1369 let bound_uri = "http://example/ns".to_string();
1370 let namespace_hook = move |prefix: &str| -> Option<String> {
1371 (prefix == "p").then(|| bound_uri.clone())
1372 };
1373 let ctx = EvaluationContext {
1374 node: root_node,
1375 position: 1,
1376 size: 1,
1377 variables: None,
1378 namespaces: Some(&namespace_hook),
1379 _ctx: std::marker::PhantomData,
1380 };
1381 let expr = crate::parse("p:rect").expect("test expression must parse");
1382 match evaluate(&expr, &ctx).expect("expected Ok") {
1383 Value::NodeSet(nodes) => nodes,
1384 other => panic!("expected a node-set, got {other:?}"),
1385 }
1386 };
1387
1388 assert_eq!(nodes.len(), 1);
1392 assert_eq!(
1393 nodes[0],
1394 HookLifetimeNode {
1395 arena: &arena,
1396 idx: 1
1397 }
1398 );
1399 }
1400}