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;
29
30#[derive(Clone, Copy)]
60pub struct EvaluationContext<'ctx, 'hook, N: Node<'ctx>> {
61 pub node: N,
62 pub position: usize,
63 pub size: usize,
64 pub variables: Option<&'hook VariableLookup<'hook, N>>,
65 pub namespaces: Option<&'hook NamespaceLookup<'hook>>,
66 _ctx: std::marker::PhantomData<&'ctx ()>,
67}
68
69impl<'ctx, 'hook, N: Node<'ctx>> EvaluationContext<'ctx, 'hook, N> {
70 pub fn new(node: N) -> Self {
74 EvaluationContext {
75 node,
76 position: 1,
77 size: 1,
78 variables: None,
79 namespaces: None,
80 _ctx: std::marker::PhantomData,
81 }
82 }
83
84 fn with(&self, node: N, position: usize, size: usize) -> Self {
88 EvaluationContext {
89 node,
90 position,
91 size,
92 variables: self.variables,
93 namespaces: self.namespaces,
94 _ctx: std::marker::PhantomData,
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq)]
103pub enum EvalError {
104 UnknownFunction(QName),
109 UnboundVariable(QName),
111 ArgumentCount {
113 function: &'static str,
114 expected: usize,
115 got: usize,
116 },
117 ExpectedNodeSet { context: &'static str },
120}
121
122impl std::fmt::Display for EvalError {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 match self {
125 EvalError::UnknownFunction(name) => {
126 write!(f, "unknown function: {}", format_qname(name))
127 }
128 EvalError::UnboundVariable(name) => {
129 write!(f, "unbound variable: ${}", format_qname(name))
130 }
131 EvalError::ArgumentCount {
132 function,
133 expected,
134 got,
135 } => write!(f, "{function}() expects {expected} argument(s), got {got}"),
136 EvalError::ExpectedNodeSet { context } => {
137 write!(f, "expected a node-set in {context}")
138 }
139 }
140 }
141}
142
143impl std::error::Error for EvalError {}
144
145fn format_qname(q: &QName) -> String {
146 match &q.prefix {
147 Some(p) => format!("{p}:{}", q.local),
148 None => q.local.clone(),
149 }
150}
151
152pub fn evaluate<'ctx, 'hook, N: Node<'ctx>>(
154 expr: &Expr,
155 ctx: &EvaluationContext<'ctx, 'hook, N>,
156) -> Result<Value<N>, EvalError> {
157 match expr {
158 Expr::Or(l, r) => {
164 if evaluate(l, ctx)?.to_boolean() {
165 Ok(Value::Boolean(true))
166 } else {
167 Ok(Value::Boolean(evaluate(r, ctx)?.to_boolean()))
168 }
169 }
170 Expr::And(l, r) => {
171 if !evaluate(l, ctx)?.to_boolean() {
172 Ok(Value::Boolean(false))
173 } else {
174 Ok(Value::Boolean(evaluate(r, ctx)?.to_boolean()))
175 }
176 }
177 Expr::Equality(l, op, r) => {
178 let lv = evaluate(l, ctx)?;
179 let rv = evaluate(r, ctx)?;
180 Ok(Value::Boolean(compare_equality(&lv, *op, &rv)))
181 }
182 Expr::Relational(l, op, r) => {
183 let lv = evaluate(l, ctx)?;
184 let rv = evaluate(r, ctx)?;
185 Ok(Value::Boolean(compare_relational(&lv, *op, &rv)))
186 }
187 Expr::Additive(l, op, r) => {
188 let lv = evaluate(l, ctx)?.to_number();
189 let rv = evaluate(r, ctx)?.to_number();
190 Ok(Value::Number(match op {
191 AdditiveOp::Add => lv + rv,
192 AdditiveOp::Sub => lv - rv,
193 }))
194 }
195 Expr::Multiplicative(l, op, r) => {
196 let lv = evaluate(l, ctx)?.to_number();
197 let rv = evaluate(r, ctx)?.to_number();
198 Ok(Value::Number(match op {
199 MultiplicativeOp::Mul => lv * rv,
200 MultiplicativeOp::Div => lv / rv,
204 MultiplicativeOp::Mod => lv % rv,
205 }))
206 }
207 Expr::Negate(e) => Ok(Value::Number(-evaluate(e, ctx)?.to_number())),
208 Expr::Union(l, r) => {
209 let lv = evaluate(l, ctx)?;
210 let rv = evaluate(r, ctx)?;
211 match (lv, rv) {
212 (Value::NodeSet(mut a), Value::NodeSet(b)) => {
213 a.extend(b);
214 sort_dedup(&mut a);
215 Ok(Value::NodeSet(a))
216 }
217 _ => Err(EvalError::ExpectedNodeSet {
218 context: "union operand",
219 }),
220 }
221 }
222 Expr::Path(path) => evaluate_path(path, ctx),
223 }
224}
225
226fn eq_result<T: PartialEq>(op: EqualityOp, l: T, r: T) -> bool {
229 match op {
230 EqualityOp::Eq => l == r,
231 EqualityOp::Ne => l != r,
232 }
233}
234
235fn rel_result(op: RelationalOp, l: f64, r: f64) -> bool {
236 match op {
237 RelationalOp::Lt => l < r,
238 RelationalOp::Le => l <= r,
239 RelationalOp::Gt => l > r,
240 RelationalOp::Ge => l >= r,
241 }
242}
243
244fn compare_equality<'a, N: Node<'a>>(lhs: &Value<N>, op: EqualityOp, rhs: &Value<N>) -> bool {
248 match (lhs, rhs) {
249 (Value::NodeSet(a), Value::NodeSet(b)) => a.iter().copied().any(|na| {
250 b.iter()
251 .copied()
252 .any(|nb| eq_result(op, na.string_value(), nb.string_value()))
253 }),
254 (Value::NodeSet(a), Value::Number(n)) | (Value::Number(n), Value::NodeSet(a)) => a
255 .iter()
256 .copied()
257 .any(|na| eq_result(op, string_to_number(&na.string_value()), *n)),
258 (Value::NodeSet(a), Value::String(s)) | (Value::String(s), Value::NodeSet(a)) => a
259 .iter()
260 .copied()
261 .any(|na| eq_result(op, na.string_value(), s.clone())),
262 (Value::NodeSet(a), Value::Boolean(b)) | (Value::Boolean(b), Value::NodeSet(a)) => {
263 eq_result(op, !a.is_empty(), *b)
264 }
265 _ => {
266 if matches!(lhs, Value::Boolean(_)) || matches!(rhs, Value::Boolean(_)) {
270 eq_result(op, lhs.to_boolean(), rhs.to_boolean())
271 } else if matches!(lhs, Value::Number(_)) || matches!(rhs, Value::Number(_)) {
272 eq_result(op, lhs.to_number(), rhs.to_number())
273 } else {
274 eq_result(op, lhs.to_xpath_string(), rhs.to_xpath_string())
275 }
276 }
277 }
278}
279
280fn compare_relational<'a, N: Node<'a>>(lhs: &Value<N>, op: RelationalOp, rhs: &Value<N>) -> bool {
291 match (lhs, rhs) {
292 (Value::NodeSet(a), Value::NodeSet(b)) => a.iter().copied().any(|na| {
293 b.iter().copied().any(|nb| {
294 rel_result(
295 op,
296 string_to_number(&na.string_value()),
297 string_to_number(&nb.string_value()),
298 )
299 })
300 }),
301 (Value::NodeSet(a), Value::Number(n)) => a
302 .iter()
303 .copied()
304 .any(|na| rel_result(op, string_to_number(&na.string_value()), *n)),
305 (Value::Number(n), Value::NodeSet(a)) => a
306 .iter()
307 .copied()
308 .any(|na| rel_result(op, *n, string_to_number(&na.string_value()))),
309 (Value::NodeSet(a), Value::String(s)) => {
310 let sn = string_to_number(s);
311 a.iter()
312 .copied()
313 .any(|na| rel_result(op, string_to_number(&na.string_value()), sn))
314 }
315 (Value::String(s), Value::NodeSet(a)) => {
316 let sn = string_to_number(s);
317 a.iter()
318 .copied()
319 .any(|na| rel_result(op, sn, string_to_number(&na.string_value())))
320 }
321 (Value::NodeSet(a), Value::Boolean(b)) => {
322 rel_result(op, bool_to_number(!a.is_empty()), bool_to_number(*b))
323 }
324 (Value::Boolean(b), Value::NodeSet(a)) => {
325 rel_result(op, bool_to_number(*b), bool_to_number(!a.is_empty()))
326 }
327 _ => rel_result(op, lhs.to_number(), rhs.to_number()),
328 }
329}
330
331fn bool_to_number(b: bool) -> f64 {
332 if b { 1.0 } else { 0.0 }
333}
334
335pub(crate) fn root_of<'a, N: Node<'a>>(n: N) -> N {
338 let mut cur = n;
339 while let Some(p) = cur.parent() {
340 cur = p;
341 }
342 cur
343}
344
345pub(crate) fn sort_dedup<'a, N: Node<'a>>(nodes: &mut Vec<N>) {
346 nodes.sort_by(|a, b| a.document_order(*b));
347 nodes.dedup();
348}
349
350fn axis_nodes<'a, N: Node<'a>>(axis: Axis, n: N) -> Vec<N> {
351 match axis {
352 Axis::Ancestor => axes::ancestor(n).collect(),
353 Axis::AncestorOrSelf => axes::ancestor_or_self(n).collect(),
354 Axis::Attribute => axes::attribute(n).collect(),
355 Axis::Child => axes::child(n).collect(),
356 Axis::Descendant => axes::descendant(n).collect(),
357 Axis::DescendantOrSelf => axes::descendant_or_self(n).collect(),
358 Axis::Following => axes::following(n).collect(),
359 Axis::FollowingSibling => axes::following_sibling(n).collect(),
360 Axis::Namespace => axes::namespace(n).collect(),
361 Axis::Parent => axes::parent(n).collect(),
362 Axis::Preceding => axes::preceding(n).collect(),
363 Axis::PrecedingSibling => axes::preceding_sibling(n).collect(),
364 Axis::SelfAxis => axes::self_axis(n).collect(),
365 }
366}
367
368fn principal_node_kind(axis: Axis) -> NodeKind {
374 match axis {
375 Axis::Attribute => NodeKind::Attribute,
376 Axis::Namespace => NodeKind::Namespace,
377 _ => NodeKind::Element,
378 }
379}
380
381fn resolve_prefix<'hook>(
392 prefix: &str,
393 namespaces: Option<&NamespaceLookup<'hook>>,
394) -> Option<String> {
395 namespaces.and_then(|lookup| lookup(prefix))
396}
397
398fn matches_node_test<'ctx, 'hook, N: Node<'ctx>>(
403 test: &NodeTest,
404 axis: Axis,
405 n: N,
406 namespaces: Option<&NamespaceLookup<'hook>>,
407) -> bool {
408 match test {
409 NodeTest::Node => true,
410 NodeTest::Text => n.kind() == NodeKind::Text,
411 NodeTest::Comment => n.kind() == NodeKind::Comment,
412 NodeTest::ProcessingInstruction(target) => {
413 n.kind() == NodeKind::ProcessingInstruction
414 && match target {
415 None => true,
416 Some(t) => n.expanded_name().is_some_and(|e| e.local_name == *t),
417 }
418 }
419 NodeTest::AnyName => n.kind() == principal_node_kind(axis) && n.expanded_name().is_some(),
420 NodeTest::NamespaceWildcard(prefix) => {
421 let Some(uri) = resolve_prefix(prefix, namespaces) else {
422 return false;
423 };
424 n.kind() == principal_node_kind(axis)
425 && n.expanded_name()
426 .is_some_and(|e| e.namespace_uri.as_deref() == Some(uri.as_str()))
427 }
428 NodeTest::QName(qname) => {
429 let uri = match &qname.prefix {
430 None => None,
431 Some(p) => match resolve_prefix(p, namespaces) {
432 Some(uri) => Some(uri),
433 None => return false,
434 },
435 };
436 n.kind() == principal_node_kind(axis)
437 && n.expanded_name().is_some_and(|e| {
438 e.local_name == qname.local && e.namespace_uri.as_deref() == uri.as_deref()
439 })
440 }
441 }
442}
443
444fn predicate_truth<'a, N: Node<'a>>(val: &Value<N>, position: usize) -> bool {
451 match val {
452 Value::Number(n) => *n == position as f64,
453 _ => val.to_boolean(),
454 }
455}
456
457fn apply_predicates<'ctx, 'hook, N: Node<'ctx>>(
464 predicates: &[Expr],
465 nodes: Vec<N>,
466 ctx: &EvaluationContext<'ctx, 'hook, N>,
467) -> Result<Vec<N>, EvalError> {
468 let mut current = nodes;
469 for pred in predicates {
470 let size = current.len();
471 let mut next = Vec::with_capacity(current.len());
472 for (i, &n) in current.iter().enumerate() {
473 let position = i + 1;
474 let pred_ctx = ctx.with(n, position, size);
475 let val = evaluate(pred, &pred_ctx)?;
476 if predicate_truth(&val, position) {
477 next.push(n);
478 }
479 }
480 current = next;
481 }
482 Ok(current)
483}
484
485fn evaluate_step<'ctx, 'hook, N: Node<'ctx>>(
491 step: &Step,
492 context_nodes: &[N],
493 ctx: &EvaluationContext<'ctx, 'hook, N>,
494) -> Result<Vec<N>, EvalError> {
495 let mut result = Vec::new();
496 for &n in context_nodes {
497 let candidates: Vec<N> = axis_nodes(step.axis, n)
498 .into_iter()
499 .filter(|&c| matches_node_test(&step.node_test, step.axis, c, ctx.namespaces))
500 .collect();
501 let filtered = apply_predicates(&step.predicates, candidates, ctx)?;
502 result.extend(filtered);
503 }
504 sort_dedup(&mut result);
505 Ok(result)
506}
507
508fn evaluate_location_steps<'ctx, 'hook, N: Node<'ctx>>(
509 steps: &[Step],
510 mut nodes: Vec<N>,
511 ctx: &EvaluationContext<'ctx, 'hook, N>,
512) -> Result<Vec<N>, EvalError> {
513 for step in steps {
514 nodes = evaluate_step(step, &nodes, ctx)?;
515 }
516 Ok(nodes)
517}
518
519fn evaluate_location_path<'ctx, 'hook, N: Node<'ctx>>(
520 loc: &LocationPath,
521 start: Vec<N>,
522 ctx: &EvaluationContext<'ctx, 'hook, N>,
523) -> Result<Vec<N>, EvalError> {
524 let start = if loc.is_absolute {
525 start.first().map(|&n| vec![root_of(n)]).unwrap_or_default()
526 } else {
527 start
528 };
529 evaluate_location_steps(&loc.steps, start, ctx)
530}
531
532fn evaluate_path<'ctx, 'hook, N: Node<'ctx>>(
533 path: &PathExpr,
534 ctx: &EvaluationContext<'ctx, 'hook, N>,
535) -> Result<Value<N>, EvalError> {
536 match path {
537 PathExpr::Location(loc) => {
538 let result = evaluate_location_path(loc, vec![ctx.node], ctx)?;
539 Ok(Value::NodeSet(result))
540 }
541 PathExpr::Filter(filter) => evaluate_filter_expr(filter, ctx),
542 PathExpr::FilterLocation(filter, loc) => {
543 let filter_val = evaluate_filter_expr(filter, ctx)?;
544 let start = match filter_val {
545 Value::NodeSet(nodes) => nodes,
546 _ => {
547 return Err(EvalError::ExpectedNodeSet {
548 context: "path expression left of '/'",
549 });
550 }
551 };
552 let result = evaluate_location_path(loc, start, ctx)?;
553 Ok(Value::NodeSet(result))
554 }
555 }
556}
557
558fn evaluate_filter_expr<'ctx, 'hook, N: Node<'ctx>>(
559 filter: &FilterExpr,
560 ctx: &EvaluationContext<'ctx, 'hook, N>,
561) -> Result<Value<N>, EvalError> {
562 let primary_val = evaluate_primary(&filter.primary, ctx)?;
563 if filter.predicates.is_empty() {
564 return Ok(primary_val);
565 }
566 match primary_val {
567 Value::NodeSet(mut nodes) => {
568 sort_dedup(&mut nodes);
575 let filtered = apply_predicates(&filter.predicates, nodes, ctx)?;
576 Ok(Value::NodeSet(filtered))
577 }
578 _ => Err(EvalError::ExpectedNodeSet {
579 context: "filter expression predicate",
580 }),
581 }
582}
583
584fn evaluate_primary<'ctx, 'hook, N: Node<'ctx>>(
585 primary: &PrimaryExpr,
586 ctx: &EvaluationContext<'ctx, 'hook, N>,
587) -> Result<Value<N>, EvalError> {
588 match primary {
589 PrimaryExpr::Variable(qname) => {
590 let bound = ctx.variables.and_then(|lookup| lookup(qname));
591 bound.ok_or_else(|| EvalError::UnboundVariable(qname.clone()))
592 }
593 PrimaryExpr::Parenthesized(inner) => evaluate(inner, ctx),
594 PrimaryExpr::Literal(s) => Ok(Value::String(s.clone())),
595 PrimaryExpr::Number(n) => Ok(Value::Number(*n)),
596 PrimaryExpr::Function(call) => evaluate_function(call, ctx),
597 }
598}
599
600fn check_arity(function: &'static str, args: &[Expr], expected: usize) -> Result<(), EvalError> {
601 if args.len() == expected {
602 Ok(())
603 } else {
604 Err(EvalError::ArgumentCount {
605 function,
606 expected,
607 got: args.len(),
608 })
609 }
610}
611
612fn evaluate_function<'ctx, 'hook, N: Node<'ctx>>(
619 call: &FunctionCall,
620 ctx: &EvaluationContext<'ctx, 'hook, N>,
621) -> Result<Value<N>, EvalError> {
622 if call.name.prefix.is_none() {
623 match call.name.local.as_str() {
624 "last" => {
625 check_arity("last", &call.args, 0)?;
626 return Ok(Value::Number(ctx.size as f64));
627 }
628 "position" => {
629 check_arity("position", &call.args, 0)?;
630 return Ok(Value::Number(ctx.position as f64));
631 }
632 "count" => {
633 check_arity("count", &call.args, 1)?;
634 return match evaluate(&call.args[0], ctx)? {
635 Value::NodeSet(nodes) => Ok(Value::Number(nodes.len() as f64)),
636 _ => Err(EvalError::ExpectedNodeSet {
637 context: "count() argument",
638 }),
639 };
640 }
641 _ => {}
642 }
643 }
644 functions::dispatch(call, ctx)
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650 use crate::document::fixture::{Fixture, TestNode, build};
651
652 fn eval_str<'a>(src: &str, node: TestNode<'a>) -> Result<Value<TestNode<'a>>, EvalError> {
653 let expr = crate::parse(src).expect("test expression must parse");
654 let ctx = EvaluationContext::new(node);
655 evaluate(&expr, &ctx)
656 }
657
658 fn node_set<'a>(v: Result<Value<TestNode<'a>>, EvalError>) -> Vec<TestNode<'a>> {
659 match v.expect("expected Ok") {
660 Value::NodeSet(nodes) => nodes,
661 other => panic!("expected a node-set, got {other:?}"),
662 }
663 }
664
665 fn root(f: &Fixture) -> TestNode<'_> {
666 f.doc.node(f.html)
667 }
668
669 #[test]
672 fn or_short_circuits_when_left_is_true() {
673 let f = build();
674 assert_eq!(
675 eval_str("(1 = 1) or unknownfn()", root(&f)),
676 Ok(Value::Boolean(true))
677 );
678 }
679
680 #[test]
681 fn or_evaluates_right_when_left_is_false() {
682 let f = build();
683 assert!(matches!(
684 eval_str("(1 = 0) or unknownfn()", root(&f)),
685 Err(EvalError::UnknownFunction(_))
686 ));
687 }
688
689 #[test]
690 fn and_short_circuits_when_left_is_false() {
691 let f = build();
692 assert_eq!(
693 eval_str("(1 = 0) and unknownfn()", root(&f)),
694 Ok(Value::Boolean(false))
695 );
696 }
697
698 #[test]
699 fn and_evaluates_right_when_left_is_true() {
700 let f = build();
701 assert!(matches!(
702 eval_str("(1 = 1) and unknownfn()", root(&f)),
703 Err(EvalError::UnknownFunction(_))
704 ));
705 }
706
707 #[test]
710 fn equality_node_set_vs_node_set() {
711 let f = build();
712 assert_eq!(
713 eval_str(
714 "/html/body/span/@data-x = /html/body/span/@data-x",
715 root(&f)
716 ),
717 Ok(Value::Boolean(true))
718 );
719 assert_eq!(
720 eval_str("/html/body/span/@data-x = /html/@id", root(&f)),
721 Ok(Value::Boolean(false))
722 );
723 assert_eq!(
724 eval_str("/html/body/span/@data-x != /html/@id", root(&f)),
725 Ok(Value::Boolean(true))
726 );
727 }
728
729 #[test]
730 fn equality_node_set_vs_number() {
731 let f = build();
732 assert_eq!(
733 eval_str("/html/body/span/@data-x = 1", root(&f)),
734 Ok(Value::Boolean(true))
735 );
736 assert_eq!(
737 eval_str("/html/body/span/@data-x = 2", root(&f)),
738 Ok(Value::Boolean(false))
739 );
740 assert_eq!(
741 eval_str("/html/body/span/@data-x != 2", root(&f)),
742 Ok(Value::Boolean(true))
743 );
744 }
745
746 #[test]
747 fn equality_node_set_vs_string() {
748 let f = build();
749 assert_eq!(
750 eval_str("/html/body/span/@data-x = '1'", root(&f)),
751 Ok(Value::Boolean(true))
752 );
753 assert_eq!(
754 eval_str("/html/@id = 'nope'", root(&f)),
755 Ok(Value::Boolean(false))
756 );
757 }
758
759 #[test]
760 fn equality_node_set_vs_boolean() {
761 let f = build();
762 assert_eq!(
764 eval_str("/html/body/span/@data-x = (1 = 1)", root(&f)),
765 Ok(Value::Boolean(true))
766 );
767 assert_eq!(
769 eval_str("/html/body/nonexistent = (1 = 1)", root(&f)),
770 Ok(Value::Boolean(false))
771 );
772 }
773
774 #[test]
777 fn relational_with_node_set_operand() {
778 let f = build();
779 assert_eq!(
780 eval_str("/html/body/span/@data-x > 0", root(&f)),
781 Ok(Value::Boolean(true))
782 );
783 assert_eq!(
784 eval_str("/html/body/span/@data-x < 0", root(&f)),
785 Ok(Value::Boolean(false))
786 );
787 assert_eq!(
789 eval_str("/html/body/span/@data-x > '0'", root(&f)),
790 Ok(Value::Boolean(true))
791 );
792 assert_eq!(
794 eval_str("/html/body/span/@data-x > (1 = 0)", root(&f)),
795 Ok(Value::Boolean(true))
796 );
797 }
798
799 #[test]
802 fn arithmetic_operators() {
803 let f = build();
804 assert_eq!(eval_str("1 + 2", root(&f)), Ok(Value::Number(3.0)));
805 assert_eq!(eval_str("1 - 2", root(&f)), Ok(Value::Number(-1.0)));
806 assert_eq!(eval_str("2 * 3", root(&f)), Ok(Value::Number(6.0)));
807 assert_eq!(eval_str("7 div 2", root(&f)), Ok(Value::Number(3.5)));
808 assert_eq!(eval_str("7 mod 3", root(&f)), Ok(Value::Number(1.0)));
809 assert_eq!(eval_str("-1 + 2", root(&f)), Ok(Value::Number(1.0)));
810 }
811
812 #[test]
815 fn union_deduplicates_overlapping_node_sets() {
816 let f = build();
817 let nodes = node_set(eval_str(
818 "/html/body/child::node() | /html/body/child::*",
819 root(&f),
820 ));
821 assert_eq!(
822 nodes,
823 vec![
824 f.doc.node(f.text0),
825 f.doc.node(f.span),
826 f.doc.node(f.comment),
827 f.doc.node(f.pi),
828 f.doc.node(f.text2),
829 ],
830 "span (present in both operands) must appear exactly once, in document order"
831 );
832 }
833
834 #[test]
837 fn multi_step_path_with_numeric_predicate() {
838 let f = build();
839 let nodes = node_set(eval_str("/html/body/child::node()[3]", root(&f)));
840 assert_eq!(nodes, vec![f.doc.node(f.comment)]);
841 }
842
843 #[test]
844 fn multi_step_mixed_axis_path_with_predicate() {
845 let f = build();
846 let nodes = node_set(eval_str("/html//text()[2]", root(&f)));
859 assert_eq!(nodes, vec![f.doc.node(f.text2)]);
860 }
861
862 #[test]
865 fn predicate_order_changes_the_result() {
866 let f = build();
867 let position_then_test = node_set(eval_str(
870 "/html/body/child::node()[2][self::span]",
871 root(&f),
872 ));
873 assert_eq!(
874 position_then_test,
875 vec![f.doc.node(f.span)],
876 "[2] keeps the 2nd child (span); [self::span] then confirms it against itself"
877 );
878
879 let test_then_position = node_set(eval_str(
880 "/html/body/child::node()[self::span][2]",
881 root(&f),
882 ));
883 assert_eq!(
884 test_then_position,
885 Vec::<TestNode>::new(),
886 "[self::span] first narrows to just [span] (size 1); [2] then has nothing at position 2"
887 );
888 }
889
890 #[test]
893 fn last_position_count_in_predicates() {
894 let f = build();
895 assert_eq!(
896 node_set(eval_str("/html/body/child::node()[last()]", root(&f))),
897 vec![f.doc.node(f.text2)]
898 );
899 assert_eq!(
900 node_set(eval_str(
901 "/html/body/child::node()[position() > 1]",
902 root(&f)
903 )),
904 vec![
905 f.doc.node(f.span),
906 f.doc.node(f.comment),
907 f.doc.node(f.pi),
908 f.doc.node(f.text2),
909 ]
910 );
911 assert_eq!(
912 eval_str("count(/html/body/child::node())", root(&f)),
913 Ok(Value::Number(5.0))
914 );
915 }
916
917 #[test]
920 fn numeric_predicate_selects_by_position() {
921 let f = build();
922 let nodes = node_set(eval_str("/html/body/child::node()[3]", root(&f)));
923 assert_eq!(nodes, vec![f.doc.node(f.comment)]);
924 }
925
926 #[test]
927 fn non_numeric_predicate_uses_boolean_coercion() {
928 let f = build();
929 let nodes = node_set(eval_str("/html/body/child::node()[span]", root(&f)));
933 assert_eq!(nodes, Vec::<TestNode>::new());
934 }
935
936 #[test]
939 fn unknown_function_is_an_error() {
940 let f = build();
941 assert!(matches!(
942 eval_str("foo()", root(&f)),
943 Err(EvalError::UnknownFunction(_))
944 ));
945 }
946
947 #[test]
948 fn unbound_variable_is_an_error() {
949 let f = build();
950 assert!(matches!(
951 eval_str("$missing", root(&f)),
952 Err(EvalError::UnboundVariable(_))
953 ));
954 }
955
956 #[test]
957 fn wrong_arity_is_an_error() {
958 let f = build();
959 assert!(matches!(
960 eval_str("last(1)", root(&f)),
961 Err(EvalError::ArgumentCount {
962 function: "last",
963 expected: 0,
964 got: 1
965 })
966 ));
967 assert!(matches!(
968 eval_str("count()", root(&f)),
969 Err(EvalError::ArgumentCount {
970 function: "count",
971 expected: 1,
972 got: 0
973 })
974 ));
975 }
976
977 #[test]
978 fn count_of_non_node_set_is_an_error() {
979 let f = build();
980 assert!(matches!(
981 eval_str("count(1)", root(&f)),
982 Err(EvalError::ExpectedNodeSet { .. })
983 ));
984 }
985
986 #[test]
989 fn bound_variable_resolves_via_the_lookup_hook() {
990 let f = build();
991 let lookup = |q: &QName| -> Option<Value<TestNode<'_>>> {
992 if q.prefix.is_none() && q.local == "x" {
993 Some(Value::Number(42.0))
994 } else {
995 None
996 }
997 };
998 let ctx = EvaluationContext {
999 node: root(&f),
1000 position: 1,
1001 size: 1,
1002 variables: Some(&lookup),
1003 namespaces: None,
1004 _ctx: std::marker::PhantomData,
1005 };
1006 let expr = crate::parse("$x + 1").unwrap();
1007 assert_eq!(evaluate(&expr, &ctx), Ok(Value::Number(43.0)));
1008 }
1009
1010 #[test]
1011 fn filter_expr_sorts_a_variable_supplied_node_set_into_document_order_before_predicates() {
1012 let f = build();
1013 let span = f.doc.node(f.span);
1014 let pi = f.doc.node(f.pi);
1015 let lookup = |q: &QName| -> Option<Value<TestNode<'_>>> {
1020 if q.prefix.is_none() && q.local == "nodes" {
1021 Some(Value::NodeSet(vec![pi, span]))
1022 } else {
1023 None
1024 }
1025 };
1026 let ctx = EvaluationContext {
1027 node: root(&f),
1028 position: 1,
1029 size: 1,
1030 variables: Some(&lookup),
1031 namespaces: None,
1032 _ctx: std::marker::PhantomData,
1033 };
1034 let expr = crate::parse("$nodes[1]").unwrap();
1035 assert_eq!(evaluate(&expr, &ctx), Ok(Value::NodeSet(vec![span])));
1036 }
1037
1038 #[derive(Clone, Copy, PartialEq, Eq)]
1052 struct NsTestNode {
1053 namespace_uri: Option<&'static str>,
1054 local_name: &'static str,
1055 }
1056
1057 impl<'a> Node<'a> for NsTestNode {
1058 fn kind(self) -> NodeKind {
1059 NodeKind::Element
1060 }
1061
1062 fn parent(self) -> Option<Self> {
1063 None
1064 }
1065
1066 fn children(self) -> impl Iterator<Item = Self> + 'a {
1067 std::iter::empty()
1068 }
1069
1070 fn attributes(self) -> impl Iterator<Item = Self> + 'a {
1071 std::iter::empty()
1072 }
1073
1074 fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
1075 std::iter::empty()
1076 }
1077
1078 fn expanded_name(self) -> Option<crate::document::ExpandedName> {
1079 Some(crate::document::ExpandedName {
1080 namespace_uri: self.namespace_uri.map(str::to_string),
1081 local_name: self.local_name.to_string(),
1082 })
1083 }
1084
1085 fn string_value(self) -> String {
1086 String::new()
1087 }
1088
1089 fn document_order(self, _other: Self) -> std::cmp::Ordering {
1090 std::cmp::Ordering::Equal
1091 }
1092 }
1093
1094 fn ns_node() -> NsTestNode {
1095 NsTestNode {
1096 namespace_uri: Some("http://example/ns"),
1097 local_name: "rect",
1098 }
1099 }
1100
1101 fn qname_test() -> NodeTest {
1102 NodeTest::QName(QName {
1103 prefix: Some("p".to_string()),
1104 local: "rect".to_string(),
1105 })
1106 }
1107
1108 #[test]
1109 fn qname_test_matches_when_hook_resolves_prefix_to_the_real_uri() {
1110 let lookup =
1111 |p: &str| -> Option<String> { (p == "p").then(|| "http://example/ns".to_string()) };
1112 assert!(matches_node_test(
1113 &qname_test(),
1114 Axis::Child,
1115 ns_node(),
1116 Some(&lookup)
1117 ));
1118 }
1119
1120 #[test]
1121 fn qname_test_does_not_match_when_hook_does_not_know_the_prefix() {
1122 let lookup = |_: &str| -> Option<String> { None };
1126 assert!(!matches_node_test(
1127 &qname_test(),
1128 Axis::Child,
1129 ns_node(),
1130 Some(&lookup)
1131 ));
1132 }
1133
1134 #[test]
1135 fn qname_test_does_not_match_when_hook_present_but_unresolvable_even_if_raw_prefix_equals_the_uri()
1136 {
1137 let mut node = ns_node();
1141 node.namespace_uri = Some("p");
1142 let lookup = |_: &str| -> Option<String> { None };
1143 assert!(!matches_node_test(
1144 &qname_test(),
1145 Axis::Child,
1146 node,
1147 Some(&lookup)
1148 ));
1149 }
1150
1151 #[test]
1152 fn qname_test_does_not_match_with_no_hook_at_all() {
1153 assert!(!matches_node_test(
1156 &qname_test(),
1157 Axis::Child,
1158 ns_node(),
1159 None
1160 ));
1161 }
1162
1163 #[test]
1164 fn qname_test_does_not_match_with_no_hook_at_all_even_if_raw_prefix_equals_the_uri() {
1165 let mut node = ns_node();
1171 node.namespace_uri = Some("p");
1172 assert!(!matches_node_test(&qname_test(), Axis::Child, node, None));
1173 }
1174
1175 #[test]
1176 fn namespace_wildcard_matches_when_hook_resolves_prefix_to_the_real_uri() {
1177 let lookup =
1178 |p: &str| -> Option<String> { (p == "p").then(|| "http://example/ns".to_string()) };
1179 let test = NodeTest::NamespaceWildcard("p".to_string());
1180 assert!(matches_node_test(
1181 &test,
1182 Axis::Child,
1183 ns_node(),
1184 Some(&lookup)
1185 ));
1186 }
1187
1188 #[test]
1189 fn namespace_wildcard_does_not_match_when_hook_does_not_know_the_prefix() {
1190 let lookup = |_: &str| -> Option<String> { None };
1191 let test = NodeTest::NamespaceWildcard("p".to_string());
1192 assert!(!matches_node_test(
1193 &test,
1194 Axis::Child,
1195 ns_node(),
1196 Some(&lookup)
1197 ));
1198 }
1199
1200 #[test]
1201 fn namespace_wildcard_does_not_match_when_hook_present_but_unresolvable_even_if_raw_prefix_equals_the_uri()
1202 {
1203 let mut node = ns_node();
1204 node.namespace_uri = Some("p");
1205 let lookup = |_: &str| -> Option<String> { None };
1206 let test = NodeTest::NamespaceWildcard("p".to_string());
1207 assert!(!matches_node_test(&test, Axis::Child, node, Some(&lookup)));
1208 }
1209
1210 #[test]
1211 fn namespace_wildcard_does_not_match_with_no_hook_at_all() {
1212 let test = NodeTest::NamespaceWildcard("p".to_string());
1213 assert!(!matches_node_test(&test, Axis::Child, ns_node(), None));
1214 }
1215
1216 #[derive(Debug)]
1225 enum HookLifetimeEntry {
1226 Root {
1227 children: Vec<usize>,
1228 },
1229 Element {
1230 #[allow(dead_code)]
1231 parent: usize,
1232 namespace_uri: Option<&'static str>,
1233 local_name: &'static str,
1234 },
1235 }
1236
1237 #[derive(Debug)]
1238 struct HookLifetimeArena(Vec<HookLifetimeEntry>);
1239
1240 #[derive(Clone, Copy, Debug)]
1241 struct HookLifetimeNode<'a> {
1242 arena: &'a HookLifetimeArena,
1243 idx: usize,
1244 }
1245
1246 impl<'a> PartialEq for HookLifetimeNode<'a> {
1247 fn eq(&self, other: &Self) -> bool {
1248 std::ptr::eq(self.arena, other.arena) && self.idx == other.idx
1249 }
1250 }
1251 impl<'a> Eq for HookLifetimeNode<'a> {}
1252
1253 impl<'a> Node<'a> for HookLifetimeNode<'a> {
1254 fn kind(self) -> NodeKind {
1255 match &self.arena.0[self.idx] {
1256 HookLifetimeEntry::Root { .. } => NodeKind::Root,
1257 HookLifetimeEntry::Element { .. } => NodeKind::Element,
1258 }
1259 }
1260 fn parent(self) -> Option<Self> {
1261 match &self.arena.0[self.idx] {
1262 HookLifetimeEntry::Root { .. } => None,
1263 HookLifetimeEntry::Element { parent, .. } => Some(HookLifetimeNode {
1264 arena: self.arena,
1265 idx: *parent,
1266 }),
1267 }
1268 }
1269 fn children(self) -> impl Iterator<Item = Self> + 'a {
1270 let arena = self.arena;
1271 let indices = match &arena.0[self.idx] {
1272 HookLifetimeEntry::Root { children } => children.clone(),
1273 HookLifetimeEntry::Element { .. } => Vec::new(),
1274 };
1275 indices
1276 .into_iter()
1277 .map(move |i| HookLifetimeNode { arena, idx: i })
1278 }
1279 fn attributes(self) -> impl Iterator<Item = Self> + 'a {
1280 std::iter::empty()
1281 }
1282 fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
1283 std::iter::empty()
1284 }
1285 fn expanded_name(self) -> Option<crate::document::ExpandedName> {
1286 match &self.arena.0[self.idx] {
1287 HookLifetimeEntry::Root { .. } => None,
1288 HookLifetimeEntry::Element {
1289 namespace_uri,
1290 local_name,
1291 ..
1292 } => Some(crate::document::ExpandedName {
1293 namespace_uri: namespace_uri.map(str::to_string),
1294 local_name: local_name.to_string(),
1295 }),
1296 }
1297 }
1298 fn string_value(self) -> String {
1299 String::new()
1300 }
1301 fn document_order(self, other: Self) -> std::cmp::Ordering {
1302 self.idx.cmp(&other.idx)
1303 }
1304 }
1305
1306 #[test]
1307 fn evaluate_result_with_locally_scoped_namespace_hook_outlives_the_hook() {
1308 let arena = HookLifetimeArena(vec![
1312 HookLifetimeEntry::Root {
1313 children: vec![1, 2],
1314 },
1315 HookLifetimeEntry::Element {
1316 parent: 0,
1317 namespace_uri: Some("http://example/ns"),
1318 local_name: "rect",
1319 },
1320 HookLifetimeEntry::Element {
1321 parent: 0,
1322 namespace_uri: None,
1323 local_name: "other",
1324 },
1325 ]);
1326 let root_node = HookLifetimeNode {
1327 arena: &arena,
1328 idx: 0,
1329 };
1330
1331 let nodes = {
1344 let bound_uri = "http://example/ns".to_string();
1345 let namespace_hook = move |prefix: &str| -> Option<String> {
1346 (prefix == "p").then(|| bound_uri.clone())
1347 };
1348 let ctx = EvaluationContext {
1349 node: root_node,
1350 position: 1,
1351 size: 1,
1352 variables: None,
1353 namespaces: Some(&namespace_hook),
1354 _ctx: std::marker::PhantomData,
1355 };
1356 let expr = crate::parse("p:rect").expect("test expression must parse");
1357 match evaluate(&expr, &ctx).expect("expected Ok") {
1358 Value::NodeSet(nodes) => nodes,
1359 other => panic!("expected a node-set, got {other:?}"),
1360 }
1361 };
1362
1363 assert_eq!(nodes.len(), 1);
1367 assert_eq!(
1368 nodes[0],
1369 HookLifetimeNode {
1370 arena: &arena,
1371 idx: 1
1372 }
1373 );
1374 }
1375}