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 any_node_pair<'a, N: Node<'a>>(a: &[N], b: &[N], f: impl Fn(N, N) -> bool) -> bool {
248 a.iter()
249 .copied()
250 .any(|na| b.iter().copied().any(|nb| f(na, nb)))
251}
252
253fn compare_equality<'a, N: Node<'a>>(lhs: &Value<N>, op: EqualityOp, rhs: &Value<N>) -> bool {
257 match (lhs, rhs) {
258 (Value::NodeSet(a), Value::NodeSet(b)) => any_node_pair(a, b, |na, nb| {
259 eq_result(op, na.string_value(), nb.string_value())
260 }),
261 (Value::NodeSet(a), Value::Number(n)) | (Value::Number(n), Value::NodeSet(a)) => a
262 .iter()
263 .copied()
264 .any(|na| eq_result(op, string_to_number(&na.string_value()), *n)),
265 (Value::NodeSet(a), Value::String(s)) | (Value::String(s), Value::NodeSet(a)) => a
266 .iter()
267 .copied()
268 .any(|na| eq_result(op, na.string_value(), s.clone())),
269 (Value::NodeSet(a), Value::Boolean(b)) | (Value::Boolean(b), Value::NodeSet(a)) => {
270 eq_result(op, !a.is_empty(), *b)
271 }
272 _ => {
273 if matches!(lhs, Value::Boolean(_)) || matches!(rhs, Value::Boolean(_)) {
277 eq_result(op, lhs.to_boolean(), rhs.to_boolean())
278 } else if matches!(lhs, Value::Number(_)) || matches!(rhs, Value::Number(_)) {
279 eq_result(op, lhs.to_number(), rhs.to_number())
280 } else {
281 eq_result(op, lhs.to_xpath_string(), rhs.to_xpath_string())
282 }
283 }
284 }
285}
286
287fn compare_relational<'a, N: Node<'a>>(lhs: &Value<N>, op: RelationalOp, rhs: &Value<N>) -> bool {
298 match (lhs, rhs) {
299 (Value::NodeSet(a), Value::NodeSet(b)) => any_node_pair(a, b, |na, nb| {
300 rel_result(
301 op,
302 string_to_number(&na.string_value()),
303 string_to_number(&nb.string_value()),
304 )
305 }),
306 (Value::NodeSet(a), Value::Number(n)) => a
307 .iter()
308 .copied()
309 .any(|na| rel_result(op, string_to_number(&na.string_value()), *n)),
310 (Value::Number(n), Value::NodeSet(a)) => a
311 .iter()
312 .copied()
313 .any(|na| rel_result(op, *n, string_to_number(&na.string_value()))),
314 (Value::NodeSet(a), Value::String(s)) => {
315 let sn = string_to_number(s);
316 a.iter()
317 .copied()
318 .any(|na| rel_result(op, string_to_number(&na.string_value()), sn))
319 }
320 (Value::String(s), Value::NodeSet(a)) => {
321 let sn = string_to_number(s);
322 a.iter()
323 .copied()
324 .any(|na| rel_result(op, sn, string_to_number(&na.string_value())))
325 }
326 (Value::NodeSet(a), Value::Boolean(b)) => {
327 rel_result(op, bool_to_number(!a.is_empty()), bool_to_number(*b))
328 }
329 (Value::Boolean(b), Value::NodeSet(a)) => {
330 rel_result(op, bool_to_number(*b), bool_to_number(!a.is_empty()))
331 }
332 _ => rel_result(op, lhs.to_number(), rhs.to_number()),
333 }
334}
335
336fn bool_to_number(b: bool) -> f64 {
337 if b { 1.0 } else { 0.0 }
338}
339
340pub(crate) fn sort_dedup<'a, N: Node<'a>>(nodes: &mut Vec<N>) {
343 nodes.sort_by(|a, b| a.document_order(*b));
344 nodes.dedup();
345}
346
347fn axis_nodes<'a, N: Node<'a>>(axis: Axis, n: N) -> Vec<N> {
348 match axis {
349 Axis::Ancestor => axes::ancestor(n).collect(),
350 Axis::AncestorOrSelf => axes::ancestor_or_self(n).collect(),
351 Axis::Attribute => axes::attribute(n).collect(),
352 Axis::Child => axes::child(n).collect(),
353 Axis::Descendant => axes::descendant(n).collect(),
354 Axis::DescendantOrSelf => axes::descendant_or_self(n).collect(),
355 Axis::Following => axes::following(n).collect(),
356 Axis::FollowingSibling => axes::following_sibling(n).collect(),
357 Axis::Namespace => axes::namespace(n).collect(),
358 Axis::Parent => axes::parent(n).collect(),
359 Axis::Preceding => axes::preceding(n).collect(),
360 Axis::PrecedingSibling => axes::preceding_sibling(n).collect(),
361 Axis::SelfAxis => axes::self_axis(n).collect(),
362 }
363}
364
365fn principal_node_kind(axis: Axis) -> NodeKind {
371 match axis {
372 Axis::Attribute => NodeKind::Attribute,
373 Axis::Namespace => NodeKind::Namespace,
374 _ => NodeKind::Element,
375 }
376}
377
378fn resolve_prefix<'hook>(
389 prefix: &str,
390 namespaces: Option<&NamespaceLookup<'hook>>,
391) -> Option<String> {
392 namespaces.and_then(|lookup| lookup(prefix))
393}
394
395fn matches_node_test<'ctx, 'hook, N: Node<'ctx>>(
400 test: &NodeTest,
401 axis: Axis,
402 n: N,
403 namespaces: Option<&NamespaceLookup<'hook>>,
404) -> bool {
405 match test {
406 NodeTest::Node => true,
407 NodeTest::Text => n.kind() == NodeKind::Text,
408 NodeTest::Comment => n.kind() == NodeKind::Comment,
409 NodeTest::ProcessingInstruction(target) => {
410 n.kind() == NodeKind::ProcessingInstruction
411 && match target {
412 None => true,
413 Some(t) => n.expanded_name().is_some_and(|e| e.local_name == *t),
414 }
415 }
416 NodeTest::AnyName => n.kind() == principal_node_kind(axis) && n.expanded_name().is_some(),
417 NodeTest::NamespaceWildcard(prefix) => {
418 let Some(uri) = resolve_prefix(prefix, namespaces) else {
419 return false;
420 };
421 n.kind() == principal_node_kind(axis)
422 && n.expanded_name()
423 .is_some_and(|e| e.namespace_uri.as_deref() == Some(uri.as_str()))
424 }
425 NodeTest::QName(qname) => {
426 let uri = match &qname.prefix {
427 None => None,
428 Some(p) => match resolve_prefix(p, namespaces) {
429 Some(uri) => Some(uri),
430 None => return false,
431 },
432 };
433 n.kind() == principal_node_kind(axis)
434 && n.expanded_name().is_some_and(|e| {
435 e.local_name == qname.local && e.namespace_uri.as_deref() == uri.as_deref()
436 })
437 }
438 }
439}
440
441fn predicate_truth<'a, N: Node<'a>>(val: &Value<N>, position: usize) -> bool {
448 match val {
449 Value::Number(n) => *n == position as f64,
450 _ => val.to_boolean(),
451 }
452}
453
454fn apply_predicates<'ctx, 'hook, N: Node<'ctx>>(
461 predicates: &[Expr],
462 nodes: Vec<N>,
463 ctx: &EvaluationContext<'ctx, 'hook, N>,
464) -> Result<Vec<N>, EvalError> {
465 let mut current = nodes;
466 for pred in predicates {
467 let size = current.len();
468 let mut next = Vec::with_capacity(current.len());
469 for (i, &n) in current.iter().enumerate() {
470 let position = i + 1;
471 let pred_ctx = ctx.with(n, position, size);
472 let val = evaluate(pred, &pred_ctx)?;
473 if predicate_truth(&val, position) {
474 next.push(n);
475 }
476 }
477 current = next;
478 }
479 Ok(current)
480}
481
482fn evaluate_step<'ctx, 'hook, N: Node<'ctx>>(
488 step: &Step,
489 context_nodes: &[N],
490 ctx: &EvaluationContext<'ctx, 'hook, N>,
491) -> Result<Vec<N>, EvalError> {
492 let mut result = Vec::new();
493 for &n in context_nodes {
494 let candidates: Vec<N> = axis_nodes(step.axis, n)
495 .into_iter()
496 .filter(|&c| matches_node_test(&step.node_test, step.axis, c, ctx.namespaces))
497 .collect();
498 let filtered = apply_predicates(&step.predicates, candidates, ctx)?;
499 result.extend(filtered);
500 }
501 sort_dedup(&mut result);
502 Ok(result)
503}
504
505fn evaluate_location_steps<'ctx, 'hook, N: Node<'ctx>>(
506 steps: &[Step],
507 mut nodes: Vec<N>,
508 ctx: &EvaluationContext<'ctx, 'hook, N>,
509) -> Result<Vec<N>, EvalError> {
510 for step in steps {
511 nodes = evaluate_step(step, &nodes, ctx)?;
512 }
513 Ok(nodes)
514}
515
516fn evaluate_location_path<'ctx, 'hook, N: Node<'ctx>>(
517 loc: &LocationPath,
518 start: Vec<N>,
519 ctx: &EvaluationContext<'ctx, 'hook, N>,
520) -> Result<Vec<N>, EvalError> {
521 let start = if loc.is_absolute {
522 start
523 .first()
524 .map(|&n| vec![axes::root_of(n)])
525 .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
600pub(crate) fn check_arity(
601 function: &'static str,
602 args: &[Expr],
603 expected: usize,
604) -> Result<(), EvalError> {
605 if args.len() == expected {
606 Ok(())
607 } else {
608 Err(EvalError::ArgumentCount {
609 function,
610 expected,
611 got: args.len(),
612 })
613 }
614}
615
616fn evaluate_function<'ctx, 'hook, N: Node<'ctx>>(
623 call: &FunctionCall,
624 ctx: &EvaluationContext<'ctx, 'hook, N>,
625) -> Result<Value<N>, EvalError> {
626 if call.name.prefix.is_none() {
627 match call.name.local.as_str() {
628 "last" => {
629 check_arity("last", &call.args, 0)?;
630 return Ok(Value::Number(ctx.size as f64));
631 }
632 "position" => {
633 check_arity("position", &call.args, 0)?;
634 return Ok(Value::Number(ctx.position as f64));
635 }
636 "count" => {
637 check_arity("count", &call.args, 1)?;
638 return match evaluate(&call.args[0], ctx)? {
639 Value::NodeSet(nodes) => Ok(Value::Number(nodes.len() as f64)),
640 _ => Err(EvalError::ExpectedNodeSet {
641 context: "count() argument",
642 }),
643 };
644 }
645 _ => {}
646 }
647 }
648 functions::dispatch(call, ctx)
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654 use crate::document::fixture::{Fixture, TestNode, build};
655
656 fn eval_str<'a>(src: &str, node: TestNode<'a>) -> Result<Value<TestNode<'a>>, EvalError> {
657 let expr = crate::parse(src).expect("test expression must parse");
658 let ctx = EvaluationContext::new(node);
659 evaluate(&expr, &ctx)
660 }
661
662 fn node_set<'a>(v: Result<Value<TestNode<'a>>, EvalError>) -> Vec<TestNode<'a>> {
663 match v.expect("expected Ok") {
664 Value::NodeSet(nodes) => nodes,
665 other => panic!("expected a node-set, got {other:?}"),
666 }
667 }
668
669 fn root(f: &Fixture) -> TestNode<'_> {
670 f.doc.node(f.html)
671 }
672
673 #[test]
676 fn or_short_circuits_when_left_is_true() {
677 let f = build();
678 assert_eq!(
679 eval_str("(1 = 1) or unknownfn()", root(&f)),
680 Ok(Value::Boolean(true))
681 );
682 }
683
684 #[test]
685 fn or_evaluates_right_when_left_is_false() {
686 let f = build();
687 assert!(matches!(
688 eval_str("(1 = 0) or unknownfn()", root(&f)),
689 Err(EvalError::UnknownFunction(_))
690 ));
691 }
692
693 #[test]
694 fn and_short_circuits_when_left_is_false() {
695 let f = build();
696 assert_eq!(
697 eval_str("(1 = 0) and unknownfn()", root(&f)),
698 Ok(Value::Boolean(false))
699 );
700 }
701
702 #[test]
703 fn and_evaluates_right_when_left_is_true() {
704 let f = build();
705 assert!(matches!(
706 eval_str("(1 = 1) and unknownfn()", root(&f)),
707 Err(EvalError::UnknownFunction(_))
708 ));
709 }
710
711 #[test]
714 fn equality_node_set_vs_node_set() {
715 let f = build();
716 assert_eq!(
717 eval_str(
718 "/html/body/span/@data-x = /html/body/span/@data-x",
719 root(&f)
720 ),
721 Ok(Value::Boolean(true))
722 );
723 assert_eq!(
724 eval_str("/html/body/span/@data-x = /html/@id", root(&f)),
725 Ok(Value::Boolean(false))
726 );
727 assert_eq!(
728 eval_str("/html/body/span/@data-x != /html/@id", root(&f)),
729 Ok(Value::Boolean(true))
730 );
731 }
732
733 #[test]
734 fn equality_node_set_vs_number() {
735 let f = build();
736 assert_eq!(
737 eval_str("/html/body/span/@data-x = 1", root(&f)),
738 Ok(Value::Boolean(true))
739 );
740 assert_eq!(
741 eval_str("/html/body/span/@data-x = 2", root(&f)),
742 Ok(Value::Boolean(false))
743 );
744 assert_eq!(
745 eval_str("/html/body/span/@data-x != 2", root(&f)),
746 Ok(Value::Boolean(true))
747 );
748 }
749
750 #[test]
751 fn equality_node_set_vs_string() {
752 let f = build();
753 assert_eq!(
754 eval_str("/html/body/span/@data-x = '1'", root(&f)),
755 Ok(Value::Boolean(true))
756 );
757 assert_eq!(
758 eval_str("/html/@id = 'nope'", root(&f)),
759 Ok(Value::Boolean(false))
760 );
761 }
762
763 #[test]
764 fn equality_node_set_vs_boolean() {
765 let f = build();
766 assert_eq!(
768 eval_str("/html/body/span/@data-x = (1 = 1)", root(&f)),
769 Ok(Value::Boolean(true))
770 );
771 assert_eq!(
773 eval_str("/html/body/nonexistent = (1 = 1)", root(&f)),
774 Ok(Value::Boolean(false))
775 );
776 }
777
778 #[test]
781 fn relational_with_node_set_operand() {
782 let f = build();
783 assert_eq!(
784 eval_str("/html/body/span/@data-x > 0", root(&f)),
785 Ok(Value::Boolean(true))
786 );
787 assert_eq!(
788 eval_str("/html/body/span/@data-x < 0", root(&f)),
789 Ok(Value::Boolean(false))
790 );
791 assert_eq!(
793 eval_str("/html/body/span/@data-x > '0'", root(&f)),
794 Ok(Value::Boolean(true))
795 );
796 assert_eq!(
798 eval_str("/html/body/span/@data-x > (1 = 0)", root(&f)),
799 Ok(Value::Boolean(true))
800 );
801 }
802
803 #[test]
806 fn arithmetic_operators() {
807 let f = build();
808 assert_eq!(eval_str("1 + 2", root(&f)), Ok(Value::Number(3.0)));
809 assert_eq!(eval_str("1 - 2", root(&f)), Ok(Value::Number(-1.0)));
810 assert_eq!(eval_str("2 * 3", root(&f)), Ok(Value::Number(6.0)));
811 assert_eq!(eval_str("7 div 2", root(&f)), Ok(Value::Number(3.5)));
812 assert_eq!(eval_str("7 mod 3", root(&f)), Ok(Value::Number(1.0)));
813 assert_eq!(eval_str("-1 + 2", root(&f)), Ok(Value::Number(1.0)));
814 }
815
816 #[test]
819 fn union_deduplicates_overlapping_node_sets() {
820 let f = build();
821 let nodes = node_set(eval_str(
822 "/html/body/child::node() | /html/body/child::*",
823 root(&f),
824 ));
825 assert_eq!(
826 nodes,
827 vec![
828 f.doc.node(f.text0),
829 f.doc.node(f.span),
830 f.doc.node(f.comment),
831 f.doc.node(f.pi),
832 f.doc.node(f.text2),
833 ],
834 "span (present in both operands) must appear exactly once, in document order"
835 );
836 }
837
838 #[test]
841 fn multi_step_path_with_numeric_predicate() {
842 let f = build();
843 let nodes = node_set(eval_str("/html/body/child::node()[3]", root(&f)));
844 assert_eq!(nodes, vec![f.doc.node(f.comment)]);
845 }
846
847 #[test]
848 fn multi_step_mixed_axis_path_with_predicate() {
849 let f = build();
850 let nodes = node_set(eval_str("/html//text()[2]", root(&f)));
863 assert_eq!(nodes, vec![f.doc.node(f.text2)]);
864 }
865
866 #[test]
869 fn predicate_order_changes_the_result() {
870 let f = build();
871 let position_then_test = node_set(eval_str(
874 "/html/body/child::node()[2][self::span]",
875 root(&f),
876 ));
877 assert_eq!(
878 position_then_test,
879 vec![f.doc.node(f.span)],
880 "[2] keeps the 2nd child (span); [self::span] then confirms it against itself"
881 );
882
883 let test_then_position = node_set(eval_str(
884 "/html/body/child::node()[self::span][2]",
885 root(&f),
886 ));
887 assert_eq!(
888 test_then_position,
889 Vec::<TestNode>::new(),
890 "[self::span] first narrows to just [span] (size 1); [2] then has nothing at position 2"
891 );
892 }
893
894 #[test]
897 fn last_position_count_in_predicates() {
898 let f = build();
899 assert_eq!(
900 node_set(eval_str("/html/body/child::node()[last()]", root(&f))),
901 vec![f.doc.node(f.text2)]
902 );
903 assert_eq!(
904 node_set(eval_str(
905 "/html/body/child::node()[position() > 1]",
906 root(&f)
907 )),
908 vec![
909 f.doc.node(f.span),
910 f.doc.node(f.comment),
911 f.doc.node(f.pi),
912 f.doc.node(f.text2),
913 ]
914 );
915 assert_eq!(
916 eval_str("count(/html/body/child::node())", root(&f)),
917 Ok(Value::Number(5.0))
918 );
919 }
920
921 #[test]
924 fn numeric_predicate_selects_by_position() {
925 let f = build();
926 let nodes = node_set(eval_str("/html/body/child::node()[3]", root(&f)));
927 assert_eq!(nodes, vec![f.doc.node(f.comment)]);
928 }
929
930 #[test]
931 fn non_numeric_predicate_uses_boolean_coercion() {
932 let f = build();
933 let nodes = node_set(eval_str("/html/body/child::node()[span]", root(&f)));
937 assert_eq!(nodes, Vec::<TestNode>::new());
938 }
939
940 #[test]
943 fn unknown_function_is_an_error() {
944 let f = build();
945 assert!(matches!(
946 eval_str("foo()", root(&f)),
947 Err(EvalError::UnknownFunction(_))
948 ));
949 }
950
951 #[test]
952 fn unbound_variable_is_an_error() {
953 let f = build();
954 assert!(matches!(
955 eval_str("$missing", root(&f)),
956 Err(EvalError::UnboundVariable(_))
957 ));
958 }
959
960 #[test]
961 fn wrong_arity_is_an_error() {
962 let f = build();
963 assert!(matches!(
964 eval_str("last(1)", root(&f)),
965 Err(EvalError::ArgumentCount {
966 function: "last",
967 expected: 0,
968 got: 1
969 })
970 ));
971 assert!(matches!(
972 eval_str("count()", root(&f)),
973 Err(EvalError::ArgumentCount {
974 function: "count",
975 expected: 1,
976 got: 0
977 })
978 ));
979 }
980
981 #[test]
982 fn count_of_non_node_set_is_an_error() {
983 let f = build();
984 assert!(matches!(
985 eval_str("count(1)", root(&f)),
986 Err(EvalError::ExpectedNodeSet { .. })
987 ));
988 }
989
990 #[test]
993 fn bound_variable_resolves_via_the_lookup_hook() {
994 let f = build();
995 let lookup = |q: &QName| -> Option<Value<TestNode<'_>>> {
996 if q.prefix.is_none() && q.local == "x" {
997 Some(Value::Number(42.0))
998 } else {
999 None
1000 }
1001 };
1002 let ctx = EvaluationContext {
1003 node: root(&f),
1004 position: 1,
1005 size: 1,
1006 variables: Some(&lookup),
1007 namespaces: None,
1008 _ctx: std::marker::PhantomData,
1009 };
1010 let expr = crate::parse("$x + 1").unwrap();
1011 assert_eq!(evaluate(&expr, &ctx), Ok(Value::Number(43.0)));
1012 }
1013
1014 #[test]
1015 fn filter_expr_sorts_a_variable_supplied_node_set_into_document_order_before_predicates() {
1016 let f = build();
1017 let span = f.doc.node(f.span);
1018 let pi = f.doc.node(f.pi);
1019 let lookup = |q: &QName| -> Option<Value<TestNode<'_>>> {
1024 if q.prefix.is_none() && q.local == "nodes" {
1025 Some(Value::NodeSet(vec![pi, span]))
1026 } else {
1027 None
1028 }
1029 };
1030 let ctx = EvaluationContext {
1031 node: root(&f),
1032 position: 1,
1033 size: 1,
1034 variables: Some(&lookup),
1035 namespaces: None,
1036 _ctx: std::marker::PhantomData,
1037 };
1038 let expr = crate::parse("$nodes[1]").unwrap();
1039 assert_eq!(evaluate(&expr, &ctx), Ok(Value::NodeSet(vec![span])));
1040 }
1041
1042 #[derive(Clone, Copy, PartialEq, Eq)]
1056 struct NsTestNode {
1057 namespace_uri: Option<&'static str>,
1058 local_name: &'static str,
1059 }
1060
1061 impl<'a> Node<'a> for NsTestNode {
1062 fn kind(self) -> NodeKind {
1063 NodeKind::Element
1064 }
1065
1066 fn parent(self) -> Option<Self> {
1067 None
1068 }
1069
1070 fn children(self) -> impl Iterator<Item = Self> + 'a {
1071 std::iter::empty()
1072 }
1073
1074 fn attributes(self) -> impl Iterator<Item = Self> + 'a {
1075 std::iter::empty()
1076 }
1077
1078 fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
1079 std::iter::empty()
1080 }
1081
1082 fn expanded_name(self) -> Option<crate::document::ExpandedName> {
1083 Some(crate::document::ExpandedName {
1084 namespace_uri: self.namespace_uri.map(str::to_string),
1085 local_name: self.local_name.to_string(),
1086 })
1087 }
1088
1089 fn string_value(self) -> String {
1090 String::new()
1091 }
1092
1093 fn document_order(self, _other: Self) -> std::cmp::Ordering {
1094 std::cmp::Ordering::Equal
1095 }
1096 }
1097
1098 fn ns_node() -> NsTestNode {
1099 NsTestNode {
1100 namespace_uri: Some("http://example/ns"),
1101 local_name: "rect",
1102 }
1103 }
1104
1105 fn qname_test() -> NodeTest {
1106 NodeTest::QName(QName {
1107 prefix: Some("p".to_string()),
1108 local: "rect".to_string(),
1109 })
1110 }
1111
1112 #[test]
1113 fn qname_test_matches_when_hook_resolves_prefix_to_the_real_uri() {
1114 let lookup =
1115 |p: &str| -> Option<String> { (p == "p").then(|| "http://example/ns".to_string()) };
1116 assert!(matches_node_test(
1117 &qname_test(),
1118 Axis::Child,
1119 ns_node(),
1120 Some(&lookup)
1121 ));
1122 }
1123
1124 #[test]
1125 fn qname_test_does_not_match_when_hook_does_not_know_the_prefix() {
1126 let lookup = |_: &str| -> Option<String> { None };
1130 assert!(!matches_node_test(
1131 &qname_test(),
1132 Axis::Child,
1133 ns_node(),
1134 Some(&lookup)
1135 ));
1136 }
1137
1138 #[test]
1139 fn qname_test_does_not_match_when_hook_present_but_unresolvable_even_if_raw_prefix_equals_the_uri()
1140 {
1141 let mut node = ns_node();
1145 node.namespace_uri = Some("p");
1146 let lookup = |_: &str| -> Option<String> { None };
1147 assert!(!matches_node_test(
1148 &qname_test(),
1149 Axis::Child,
1150 node,
1151 Some(&lookup)
1152 ));
1153 }
1154
1155 #[test]
1156 fn qname_test_does_not_match_with_no_hook_at_all() {
1157 assert!(!matches_node_test(
1160 &qname_test(),
1161 Axis::Child,
1162 ns_node(),
1163 None
1164 ));
1165 }
1166
1167 #[test]
1168 fn qname_test_does_not_match_with_no_hook_at_all_even_if_raw_prefix_equals_the_uri() {
1169 let mut node = ns_node();
1175 node.namespace_uri = Some("p");
1176 assert!(!matches_node_test(&qname_test(), Axis::Child, node, None));
1177 }
1178
1179 #[test]
1180 fn namespace_wildcard_matches_when_hook_resolves_prefix_to_the_real_uri() {
1181 let lookup =
1182 |p: &str| -> Option<String> { (p == "p").then(|| "http://example/ns".to_string()) };
1183 let test = NodeTest::NamespaceWildcard("p".to_string());
1184 assert!(matches_node_test(
1185 &test,
1186 Axis::Child,
1187 ns_node(),
1188 Some(&lookup)
1189 ));
1190 }
1191
1192 #[test]
1193 fn namespace_wildcard_does_not_match_when_hook_does_not_know_the_prefix() {
1194 let lookup = |_: &str| -> Option<String> { None };
1195 let test = NodeTest::NamespaceWildcard("p".to_string());
1196 assert!(!matches_node_test(
1197 &test,
1198 Axis::Child,
1199 ns_node(),
1200 Some(&lookup)
1201 ));
1202 }
1203
1204 #[test]
1205 fn namespace_wildcard_does_not_match_when_hook_present_but_unresolvable_even_if_raw_prefix_equals_the_uri()
1206 {
1207 let mut node = ns_node();
1208 node.namespace_uri = Some("p");
1209 let lookup = |_: &str| -> Option<String> { None };
1210 let test = NodeTest::NamespaceWildcard("p".to_string());
1211 assert!(!matches_node_test(&test, Axis::Child, node, Some(&lookup)));
1212 }
1213
1214 #[test]
1215 fn namespace_wildcard_does_not_match_with_no_hook_at_all() {
1216 let test = NodeTest::NamespaceWildcard("p".to_string());
1217 assert!(!matches_node_test(&test, Axis::Child, ns_node(), None));
1218 }
1219
1220 #[derive(Debug)]
1229 enum HookLifetimeEntry {
1230 Root {
1231 children: Vec<usize>,
1232 },
1233 Element {
1234 #[allow(dead_code)]
1235 parent: usize,
1236 namespace_uri: Option<&'static str>,
1237 local_name: &'static str,
1238 },
1239 }
1240
1241 #[derive(Debug)]
1242 struct HookLifetimeArena(Vec<HookLifetimeEntry>);
1243
1244 #[derive(Clone, Copy, Debug)]
1245 struct HookLifetimeNode<'a> {
1246 arena: &'a HookLifetimeArena,
1247 idx: usize,
1248 }
1249
1250 impl<'a> PartialEq for HookLifetimeNode<'a> {
1251 fn eq(&self, other: &Self) -> bool {
1252 std::ptr::eq(self.arena, other.arena) && self.idx == other.idx
1253 }
1254 }
1255 impl<'a> Eq for HookLifetimeNode<'a> {}
1256
1257 impl<'a> Node<'a> for HookLifetimeNode<'a> {
1258 fn kind(self) -> NodeKind {
1259 match &self.arena.0[self.idx] {
1260 HookLifetimeEntry::Root { .. } => NodeKind::Root,
1261 HookLifetimeEntry::Element { .. } => NodeKind::Element,
1262 }
1263 }
1264 fn parent(self) -> Option<Self> {
1265 match &self.arena.0[self.idx] {
1266 HookLifetimeEntry::Root { .. } => None,
1267 HookLifetimeEntry::Element { parent, .. } => Some(HookLifetimeNode {
1268 arena: self.arena,
1269 idx: *parent,
1270 }),
1271 }
1272 }
1273 fn children(self) -> impl Iterator<Item = Self> + 'a {
1274 let arena = self.arena;
1275 let indices = match &arena.0[self.idx] {
1276 HookLifetimeEntry::Root { children } => children.clone(),
1277 HookLifetimeEntry::Element { .. } => Vec::new(),
1278 };
1279 indices
1280 .into_iter()
1281 .map(move |i| HookLifetimeNode { arena, idx: i })
1282 }
1283 fn attributes(self) -> impl Iterator<Item = Self> + 'a {
1284 std::iter::empty()
1285 }
1286 fn namespaces(self) -> impl Iterator<Item = Self> + 'a {
1287 std::iter::empty()
1288 }
1289 fn expanded_name(self) -> Option<crate::document::ExpandedName> {
1290 match &self.arena.0[self.idx] {
1291 HookLifetimeEntry::Root { .. } => None,
1292 HookLifetimeEntry::Element {
1293 namespace_uri,
1294 local_name,
1295 ..
1296 } => Some(crate::document::ExpandedName {
1297 namespace_uri: namespace_uri.map(str::to_string),
1298 local_name: local_name.to_string(),
1299 }),
1300 }
1301 }
1302 fn string_value(self) -> String {
1303 String::new()
1304 }
1305 fn document_order(self, other: Self) -> std::cmp::Ordering {
1306 self.idx.cmp(&other.idx)
1307 }
1308 }
1309
1310 #[test]
1311 fn evaluate_result_with_locally_scoped_namespace_hook_outlives_the_hook() {
1312 let arena = HookLifetimeArena(vec![
1316 HookLifetimeEntry::Root {
1317 children: vec![1, 2],
1318 },
1319 HookLifetimeEntry::Element {
1320 parent: 0,
1321 namespace_uri: Some("http://example/ns"),
1322 local_name: "rect",
1323 },
1324 HookLifetimeEntry::Element {
1325 parent: 0,
1326 namespace_uri: None,
1327 local_name: "other",
1328 },
1329 ]);
1330 let root_node = HookLifetimeNode {
1331 arena: &arena,
1332 idx: 0,
1333 };
1334
1335 let nodes = {
1348 let bound_uri = "http://example/ns".to_string();
1349 let namespace_hook = move |prefix: &str| -> Option<String> {
1350 (prefix == "p").then(|| bound_uri.clone())
1351 };
1352 let ctx = EvaluationContext {
1353 node: root_node,
1354 position: 1,
1355 size: 1,
1356 variables: None,
1357 namespaces: Some(&namespace_hook),
1358 _ctx: std::marker::PhantomData,
1359 };
1360 let expr = crate::parse("p:rect").expect("test expression must parse");
1361 match evaluate(&expr, &ctx).expect("expected Ok") {
1362 Value::NodeSet(nodes) => nodes,
1363 other => panic!("expected a node-set, got {other:?}"),
1364 }
1365 };
1366
1367 assert_eq!(nodes.len(), 1);
1371 assert_eq!(
1372 nodes[0],
1373 HookLifetimeNode {
1374 arena: &arena,
1375 idx: 1
1376 }
1377 );
1378 }
1379}