1use sup_xml_core::error::XmlError;
32use sup_xml_core::xpath::ast::{Expr, LocationPath, NodeTest, Step};
33use sup_xml_core::xpath::eval::{
34 eval_expr, EvalCtx, NoBindings, StaticContext, Value, XPathBindings,
35};
36use sup_xml_core::xpath::{DocIndexLike, NodeId};
37
38use crate::ast::{QName, StylesheetAst, Template};
39
40type Result<T> = std::result::Result<T, XmlError>;
41
42pub fn matches<I: DocIndexLike>(
48 pattern: &Expr,
49 node: NodeId,
50 idx: &I,
51 bindings: &dyn XPathBindings,
52) -> Result<bool> {
53 sup_xml_core::xpath::eval::reset_eval_budget();
57 if let Expr::BackwardsCompat(inner) = pattern {
61 return matches(inner, node, idx, bindings);
62 }
63 if let Expr::Union(l, r) = pattern {
71 if matches(l, node, idx, bindings)? { return Ok(true); }
72 return matches(r, node, idx, bindings);
73 }
74 if pattern_is_document_node(pattern) {
75 return Ok(matches!(idx.kind(node),
76 sup_xml_core::xpath::XPathNodeKind::Document));
77 }
78 if let Some(Some(inner)) = single_step_document_test(pattern) {
83 if !matches!(idx.kind(node), sup_xml_core::xpath::XPathNodeKind::Document) {
84 return Ok(false);
85 }
86 return Ok(idx.children(node).iter().any(|c|
87 matches!(idx.kind(*c), sup_xml_core::xpath::XPathNodeKind::Element)
88 && sup_xml_core::xpath::eval::node_matches_child(*c, inner, idx, bindings)));
89 }
90 if let Some(rest) = rewrite_document_node_prefix(pattern) {
98 return matches(&rest, node, idx, bindings);
99 }
100 let sc = StaticContext {
102 xpath_2_0: bindings.xpath_version_2_or_later(),
103 xpath_3_0: false,
104 libxml2_compatible: false, current_node: None,
105 };
106 let mut cur = Some(node);
107 while let Some(ctx_node) = cur {
108 let ctx = EvalCtx {
109 context_node: ctx_node, pos: 1, size: 1, bindings, static_ctx: &sc };
110 let v = eval_expr(pattern, &ctx, idx)?;
111 if let Value::NodeSet(ns) = v {
112 if ns.contains(&node) {
113 return Ok(true);
114 }
115 }
116 cur = idx.parent(ctx_node);
117 }
118 Ok(false)
119}
120
121fn rewrite_document_node_prefix(p: &Expr) -> Option<Expr> {
125 if let Expr::Path(LocationPath::Relative(steps)) = p {
126 if steps.len() >= 2
127 && matches!(&steps[0].node_test, NodeTest::Document(None))
128 && steps[0].predicates.is_empty()
129 {
130 return Some(Expr::Path(LocationPath::Absolute(steps[1..].to_vec())));
131 }
132 }
133 None
134}
135
136fn single_step_document_test(p: &Expr) -> Option<&Option<Box<NodeTest>>> {
141 let step = single_step_pattern(p)?;
142 if !step.predicates.is_empty() {
143 return None;
144 }
145 match &step.node_test {
146 NodeTest::Document(inner) => Some(inner),
147 _ => None,
148 }
149}
150
151fn pattern_is_document_node(p: &Expr) -> bool {
154 fn step_is_doc(s: &Step) -> bool {
155 matches!(&s.node_test, NodeTest::Document(None)) && s.predicates.is_empty()
156 }
157 match p {
158 Expr::Path(LocationPath::Relative(s)) if s.len() == 1
159 => step_is_doc(&s[0]),
160 Expr::Path(LocationPath::Absolute(s)) if s.is_empty() => true,
161 Expr::Union(a, b) => pattern_is_document_node(a) || pattern_is_document_node(b),
162 _ => false,
163 }
164}
165
166pub fn default_priority(pattern: &Expr) -> f64 {
178 if let Expr::BackwardsCompat(inner) = pattern {
182 return default_priority(inner);
183 }
184 if let Expr::Union(l, r) = pattern {
186 return default_priority(l).max(default_priority(r));
187 }
188 if pattern_is_document_node(pattern) {
191 return -0.5;
192 }
193 let single_step = single_step_pattern(pattern);
194 match single_step {
195 Some(step) if step.predicates.is_empty() => match &step.node_test {
196 NodeTest::QName(_, _) => 0.0,
197 NodeTest::DefaultNamespaceName { .. } => 0.0,
198 NodeTest::PrefixWildcard(_) => -0.25,
199 NodeTest::LocalNameOnly(_) => -0.25,
203 NodeTest::LocalName(_) => 0.0,
204 NodeTest::AnyNode | NodeTest::Wildcard
207 | NodeTest::Text | NodeTest::Comment
208 | NodeTest::PI(None) => -0.5,
209 NodeTest::Document(inner) => match inner.as_deref() {
214 Some(NodeTest::QName(..))
215 | Some(NodeTest::DefaultNamespaceName { .. })
216 | Some(NodeTest::LocalName(_)) => 0.0,
217 Some(NodeTest::PrefixWildcard(_))
218 | Some(NodeTest::LocalNameOnly(_)) => -0.25,
219 _ => -0.5,
220 },
221 NodeTest::PI(Some(_)) => 0.0,
223 },
224 _ => 0.5,
225 }
226}
227
228fn single_step_pattern(expr: &Expr) -> Option<&Step> {
233 match expr {
234 Expr::Path(lp) => {
235 let steps = match lp {
236 LocationPath::Absolute(s) | LocationPath::Relative(s) => s,
237 };
238 if steps.len() == 1 { Some(&steps[0]) } else { None }
239 }
240 _ => None,
241 }
242}
243
244pub struct Selected<'a> {
258 pub template: &'a Template,
259 pub priority: f64,
260 pub branch_idx: Option<usize>,
261}
262
263pub fn pattern_branches(pat: &Expr) -> Vec<&Expr> {
270 fn walk<'a>(p: &'a Expr, out: &mut Vec<&'a Expr>) {
271 match p {
272 Expr::BackwardsCompat(inner) => walk(inner, out),
273 Expr::Union(l, r) => { walk(l, out); walk(r, out); }
274 _ => out.push(p),
275 }
276 }
277 let mut out = Vec::new();
278 walk(pat, &mut out);
279 out
280}
281
282pub fn select_template<'a, I: DocIndexLike>(
295 style: &'a StylesheetAst,
296 node: NodeId,
297 mode: Option<&QName>,
298 idx: &I,
299 bindings: &dyn XPathBindings,
300) -> Result<Option<Selected<'a>>> {
301 select_template_inner(style, node, mode, idx, bindings, None)
302}
303
304pub fn select_template_max_precedence<'a, I: DocIndexLike>(
309 style: &'a StylesheetAst,
310 node: NodeId,
311 mode: Option<&QName>,
312 idx: &I,
313 bindings: &dyn XPathBindings,
314 max_precedence: i32,
315) -> Result<Option<Selected<'a>>> {
316 select_template_inner(style, node, mode, idx, bindings, Some(max_precedence))
317}
318
319pub fn select_template_next<'a, I: DocIndexLike>(
333 style: &'a StylesheetAst,
334 node: NodeId,
335 mode: Option<&QName>,
336 idx: &I,
337 bindings: &dyn XPathBindings,
338 current: &Selected<'_>,
339 current_index: usize,
340) -> Result<Option<Selected<'a>>> {
341 let cur_prec = current.template.import_precedence;
342 let cur_prio = current.priority;
343 let cur_path = current.template.source_path.as_slice();
344 let cur_branch = current.branch_idx;
345 let mut best: Option<Selected<'a>> = None;
346 let mut best_path: &[u32] = &[];
347 let mut best_branch: Option<usize> = None;
348 for (i, t) in style.templates.iter().enumerate() {
349 let Some(pat) = t.match_pattern.as_ref() else { continue; };
350 if !template_mode_matches(t, mode) { continue; }
351 let branches = pattern_branches(pat);
352 let multi = branches.len() > 1;
353 for (b, branch_pat) in branches.iter().enumerate() {
354 if i == current_index && (!multi || Some(b) == cur_branch) {
357 continue;
358 }
359 if !matches(branch_pat, node, idx, bindings)? { continue; }
360 let priority = match t.priority {
361 Some(p) => p,
362 None => default_priority(branch_pat),
363 };
364 let branch_idx = if multi { Some(b) } else { None };
365 let prec = t.import_precedence;
370 let path = t.source_path.as_slice();
371 let strictly_after_current = if prec != cur_prec {
372 prec < cur_prec
373 } else if (priority - cur_prio).abs() > f64::EPSILON {
374 priority < cur_prio
375 } else if path != cur_path {
376 path < cur_path
377 } else {
378 branch_idx < cur_branch
380 };
381 if !strictly_after_current { continue; }
382 let take = match &best {
383 None => true,
384 Some(bs) => {
385 let bprec = bs.template.import_precedence;
386 if prec != bprec {
387 prec > bprec
388 } else if (priority - bs.priority).abs() > f64::EPSILON {
389 priority > bs.priority
390 } else if path != best_path {
391 path > best_path
392 } else {
393 branch_idx > best_branch
394 }
395 }
396 };
397 if take {
398 best = Some(Selected { template: t, priority, branch_idx });
399 best_path = path;
400 best_branch = branch_idx;
401 }
402 }
403 }
404 Ok(best)
405}
406
407fn select_template_inner<'a, I: DocIndexLike>(
408 style: &'a StylesheetAst,
409 node: NodeId,
410 mode: Option<&QName>,
411 idx: &I,
412 bindings: &dyn XPathBindings,
413 max_precedence: Option<i32>,
414) -> Result<Option<Selected<'a>>> {
415 let mut best: Option<Selected<'a>> = None;
416 let mut best_path: &[u32] = &[];
417 let mut best_branch: Option<usize> = None;
418 let mut multiple = false;
422 for t in style.templates.iter() {
423 if let Some(cap) = max_precedence {
424 if t.import_precedence > cap { continue; }
425 }
426 let Some(pat) = t.match_pattern.as_ref() else { continue; };
429 if !template_mode_matches(t, mode) { continue; }
430 let branches = pattern_branches(pat);
434 let multi = branches.len() > 1;
435 for (b, branch_pat) in branches.iter().enumerate() {
436 if !matches(branch_pat, node, idx, bindings)? { continue; }
437 let priority = match t.priority {
438 Some(p) => p,
439 None => default_priority(branch_pat),
440 };
441 let branch_idx = if multi { Some(b) } else { None };
442 let (take, tie) = match &best {
449 None => (true, false),
450 Some(bs) => {
451 let prec = t.import_precedence;
452 let bprec = bs.template.import_precedence;
453 let path = t.source_path.as_slice();
454 if prec != bprec {
455 (prec > bprec, false)
456 } else if (priority - bs.priority).abs() > f64::EPSILON {
457 (priority > bs.priority, false)
458 } else if path != best_path {
459 (path > best_path, true)
460 } else {
461 (branch_idx > best_branch, true)
465 }
466 }
467 };
468 if take && !tie { multiple = false; }
471 if tie { multiple = true; }
472 if take {
473 best = Some(Selected { template: t, priority, branch_idx });
474 best_path = t.source_path.as_slice();
475 best_branch = branch_idx;
476 }
477 }
478 }
479 if multiple && on_multiple_match_is_error() {
483 return Err(sup_xml_core::xpath::eval::xpath_err(
484 "more than one template rule matches the node with the same \
485 import precedence and priority"
486 ).with_xpath_code("XTRE0540"));
487 }
488 Ok(best)
489}
490
491thread_local! {
492 static ON_MULTIPLE_MATCH_ERROR: std::cell::Cell<bool> =
497 const { std::cell::Cell::new(false) };
498}
499
500fn on_multiple_match_is_error() -> bool {
501 ON_MULTIPLE_MATCH_ERROR.with(|c| c.get())
502}
503
504pub fn set_on_multiple_match_error(v: bool) -> bool {
508 ON_MULTIPLE_MATCH_ERROR.with(|c| c.replace(v))
509}
510
511fn mode_matches(template_mode: Option<&QName>, requested: Option<&QName>) -> bool {
512 match (template_mode, requested) {
513 (None, None) => true,
514 (Some(a), Some(b)) => a.uri == b.uri && a.local == b.local,
515 _ => false,
516 }
517}
518
519fn template_mode_matches(t: &crate::ast::Template, requested: Option<&QName>) -> bool {
526 if t.modes_match_all { return true; }
527 if t.modes.is_empty() {
528 return mode_matches(t.mode.as_ref(), requested);
531 }
532 let is_default = |q: &QName| q.local.is_empty() && q.uri.is_empty();
533 match requested {
534 None => t.modes.iter().any(is_default),
535 Some(r) => t.modes.iter().any(|m|
536 !is_default(m) && m.uri == r.uri && m.local == r.local),
537 }
538}
539
540pub fn select_template_no_bindings<'a, I: DocIndexLike>(
546 style: &'a StylesheetAst,
547 node: NodeId,
548 mode: Option<&QName>,
549 idx: &I,
550) -> Result<Option<Selected<'a>>> {
551 select_template(style, node, mode, idx, &NoBindings)
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use crate::Stylesheet;
558 use sup_xml_core::{parse_str, ParseOptions, XPathContext};
559
560 fn build_stylesheet(body: &str) -> Stylesheet {
561 let text = format!(
562 r#"<xsl:stylesheet version="1.0"
563 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">{body}</xsl:stylesheet>"#,
564 );
565 Stylesheet::compile_str(&text).unwrap()
566 }
567
568 fn doc_ns(xml: &str) -> sup_xml_tree::dom::Document {
569 let opts = ParseOptions { namespace_aware: true, ..ParseOptions::default() };
570 parse_str(xml, &opts).unwrap()
571 }
572
573 #[test]
576 fn priority_star_is_negative_half() {
577 let xslt = build_stylesheet(r#"<xsl:template match="*"/>"#);
578 let p = default_priority(xslt.ast.templates[0].match_pattern.as_ref().unwrap());
579 assert_eq!(p, -0.5);
580 }
581
582 #[test]
583 fn priority_named_element_is_zero() {
584 let xslt = build_stylesheet(r#"<xsl:template match="book"/>"#);
585 let p = default_priority(xslt.ast.templates[0].match_pattern.as_ref().unwrap());
586 assert_eq!(p, 0.0);
587 }
588
589 #[test]
590 fn priority_node_test_is_negative_half() {
591 let xslt = build_stylesheet(r#"<xsl:template match="text()"/>"#);
592 let p = default_priority(xslt.ast.templates[0].match_pattern.as_ref().unwrap());
593 assert_eq!(p, -0.5);
594 }
595
596 #[test]
597 fn priority_path_is_half() {
598 let xslt = build_stylesheet(r#"<xsl:template match="book/chapter"/>"#);
599 let p = default_priority(xslt.ast.templates[0].match_pattern.as_ref().unwrap());
600 assert_eq!(p, 0.5);
601 }
602
603 #[test]
604 fn priority_predicate_is_half() {
605 let xslt = build_stylesheet(r#"<xsl:template match="book[@x]"/>"#);
606 let p = default_priority(xslt.ast.templates[0].match_pattern.as_ref().unwrap());
607 assert_eq!(p, 0.5);
608 }
609
610 #[test]
613 fn root_pattern_matches_document_node() {
614 let xslt = build_stylesheet(r#"<xsl:template match="/"/>"#);
615 let pat = xslt.ast.templates[0].match_pattern.as_ref().unwrap();
616 let doc = doc_ns("<r/>");
617 let ctx = XPathContext::new(&doc);
618 assert!(matches(pat, 0, &ctx.index, &NoBindings).unwrap());
620 }
621
622 #[test]
623 fn star_pattern_matches_elements() {
624 let xslt = build_stylesheet(r#"<xsl:template match="*"/>"#);
625 let pat = xslt.ast.templates[0].match_pattern.as_ref().unwrap();
626 let doc = doc_ns("<r><a/></r>");
627 let ctx = XPathContext::new(&doc);
628 let v = ctx.eval("/r").unwrap();
631 let r_id = match v {
632 Value::NodeSet(ns) => ns[0],
633 _ => panic!(),
634 };
635 assert!(matches(pat, r_id, &ctx.index, &NoBindings).unwrap());
636 }
637
638 #[test]
639 fn named_pattern_only_matches_that_name() {
640 let xslt = build_stylesheet(r#"<xsl:template match="book"/>"#);
641 let pat = xslt.ast.templates[0].match_pattern.as_ref().unwrap();
642 let doc = doc_ns("<r><book/><article/></r>");
643 let ctx = XPathContext::new(&doc);
644 let book_id = match ctx.eval("/r/book").unwrap() {
645 Value::NodeSet(ns) => ns[0], _ => panic!(),
646 };
647 let art_id = match ctx.eval("/r/article").unwrap() {
648 Value::NodeSet(ns) => ns[0], _ => panic!(),
649 };
650 assert!(matches(pat, book_id, &ctx.index, &NoBindings).unwrap());
651 assert!(!matches(pat, art_id, &ctx.index, &NoBindings).unwrap());
652 }
653
654 #[test]
655 fn path_pattern_walks_ancestors() {
656 let xslt = build_stylesheet(r#"<xsl:template match="book/chapter"/>"#);
662 let pat = xslt.ast.templates[0].match_pattern.as_ref().unwrap();
663 let doc = doc_ns("<root><book><chapter/></book></root>");
664 let ctx = XPathContext::new(&doc);
665 let chap_id = match ctx.eval("/root/book/chapter").unwrap() {
666 Value::NodeSet(ns) => ns[0], _ => panic!(),
667 };
668 assert!(matches(pat, chap_id, &ctx.index, &NoBindings).unwrap());
669 }
670
671 #[test]
672 fn path_pattern_does_not_match_outside_path() {
673 let xslt = build_stylesheet(r#"<xsl:template match="book/chapter"/>"#);
674 let pat = xslt.ast.templates[0].match_pattern.as_ref().unwrap();
675 let doc = doc_ns("<root><chapter/></root>");
677 let ctx = XPathContext::new(&doc);
678 let chap_id = match ctx.eval("/root/chapter").unwrap() {
679 Value::NodeSet(ns) => ns[0], _ => panic!(),
680 };
681 assert!(!matches(pat, chap_id, &ctx.index, &NoBindings).unwrap());
682 }
683
684 #[test]
687 fn selects_higher_default_priority() {
688 let xslt = build_stylesheet(r#"
692 <xsl:template match="*"><star/></xsl:template>
693 <xsl:template match="a"><named/></xsl:template>
694 "#);
695 let doc = doc_ns("<r><a/></r>");
696 let ctx = XPathContext::new(&doc);
697 let a_id = match ctx.eval("/r/a").unwrap() {
698 Value::NodeSet(ns) => ns[0], _ => panic!(),
699 };
700 let sel = select_template_no_bindings(&xslt.ast, a_id, None, &ctx.index).unwrap().unwrap();
701 match &sel.template.body[0] {
703 crate::ast::Instr::LiteralElement { name, .. } => assert_eq!(name.local, "named"),
704 other => panic!("expected LiteralElement, got {other:?}"),
705 }
706 }
707
708 #[test]
709 fn explicit_priority_overrides_default() {
710 let xslt = build_stylesheet(r#"
711 <xsl:template match="*" priority="10"><high/></xsl:template>
712 <xsl:template match="a"><low/></xsl:template>
713 "#);
714 let doc = doc_ns("<r><a/></r>");
715 let ctx = XPathContext::new(&doc);
716 let a_id = match ctx.eval("/r/a").unwrap() {
717 Value::NodeSet(ns) => ns[0], _ => panic!(),
718 };
719 let sel = select_template_no_bindings(&xslt.ast, a_id, None, &ctx.index).unwrap().unwrap();
720 match &sel.template.body[0] {
721 crate::ast::Instr::LiteralElement { name, .. } => assert_eq!(name.local, "high"),
722 other => panic!("expected LiteralElement, got {other:?}"),
723 }
724 }
725
726 #[test]
727 fn ties_break_by_document_order_last_wins() {
728 let xslt = build_stylesheet(r#"
732 <xsl:template match="*"><first/></xsl:template>
733 <xsl:template match="*"><second/></xsl:template>
734 "#);
735 let doc = doc_ns("<r/>");
736 let ctx = XPathContext::new(&doc);
737 let r_id = match ctx.eval("/r").unwrap() {
738 Value::NodeSet(ns) => ns[0], _ => panic!(),
739 };
740 let sel = select_template_no_bindings(&xslt.ast, r_id, None, &ctx.index).unwrap().unwrap();
741 match &sel.template.body[0] {
742 crate::ast::Instr::LiteralElement { name, .. } => assert_eq!(name.local, "second"),
743 other => panic!("expected LiteralElement, got {other:?}"),
744 }
745 }
746
747 #[test]
748 fn no_match_returns_none() {
749 let xslt = build_stylesheet(r#"<xsl:template match="book"/>"#);
750 let doc = doc_ns("<r><article/></r>");
751 let ctx = XPathContext::new(&doc);
752 let art_id = match ctx.eval("/r/article").unwrap() {
753 Value::NodeSet(ns) => ns[0], _ => panic!(),
754 };
755 let sel = select_template_no_bindings(&xslt.ast, art_id, None, &ctx.index).unwrap();
756 assert!(sel.is_none());
757 }
758
759 #[test]
760 fn mode_filters_templates() {
761 let xslt = build_stylesheet(r#"
762 <xsl:template match="a"><default/></xsl:template>
763 <xsl:template match="a" mode="big"><big/></xsl:template>
764 "#);
765 let doc = doc_ns("<r><a/></r>");
766 let ctx = XPathContext::new(&doc);
767 let a_id = match ctx.eval("/r/a").unwrap() {
768 Value::NodeSet(ns) => ns[0], _ => panic!(),
769 };
770 let sel = select_template_no_bindings(&xslt.ast, a_id, None, &ctx.index).unwrap().unwrap();
772 match &sel.template.body[0] {
773 crate::ast::Instr::LiteralElement { name, .. } => assert_eq!(name.local, "default"),
774 _ => panic!(),
775 }
776 let big = QName { prefix: None, local: "big".into(), uri: String::new() };
778 let sel = select_template_no_bindings(&xslt.ast, a_id, Some(&big), &ctx.index).unwrap().unwrap();
779 match &sel.template.body[0] {
780 crate::ast::Instr::LiteralElement { name, .. } => assert_eq!(name.local, "big"),
781 _ => panic!(),
782 }
783 }
784
785 fn parse_pat(src: &str) -> Expr {
788 sup_xml_core::xpath::parse_xpath_with(src,
789 &sup_xml_core::xpath::XPathOptions {
790 xpath_2_0: true, libxml2_compatible: false,
791 ..sup_xml_core::xpath::XPathOptions::default()
792 }).unwrap()
793 }
794
795 #[test]
796 fn detects_bare_document_node_pattern() {
797 assert!(super::pattern_is_document_node(&parse_pat("document-node()")));
798 assert!(super::pattern_is_document_node(&parse_pat("/")));
801 }
802
803 #[test]
804 fn detects_document_node_branch_inside_union() {
805 assert!(super::pattern_is_document_node(&parse_pat("* | /")));
807 assert!(super::pattern_is_document_node(&parse_pat("/ | foo")));
808 }
809
810 #[test]
811 fn rejects_non_document_node_patterns() {
812 assert!(!super::pattern_is_document_node(&parse_pat("foo")));
813 assert!(!super::pattern_is_document_node(&parse_pat("element()")));
814 assert!(!super::pattern_is_document_node(&parse_pat("foo/bar")));
815 }
816
817 #[test]
818 fn rewrites_document_node_slash_rest() {
819 let p = parse_pat("document-node()/element()");
822 let rest = super::rewrite_document_node_prefix(&p).expect("should rewrite");
823 match rest {
824 Expr::Path(LocationPath::Absolute(steps)) => {
825 assert_eq!(steps.len(), 1);
826 assert!(matches!(steps[0].node_test, NodeTest::Wildcard | NodeTest::AnyNode));
827 }
828 other => panic!("expected absolute path, got {other:?}"),
829 }
830 }
831
832 #[test]
833 fn does_not_rewrite_bare_or_unrelated() {
834 assert!(super::rewrite_document_node_prefix(&parse_pat("document-node()")).is_none());
836 assert!(super::rewrite_document_node_prefix(&parse_pat("element()/foo")).is_none());
838 }
839
840 #[test]
843 fn default_priority_for_document_node_patterns() {
844 assert_eq!(super::default_priority(&parse_pat("/")), -0.5);
847 assert_eq!(super::default_priority(&parse_pat("document-node()")), -0.5);
848 }
849}