use sup_xml_core::error::XmlError;
use sup_xml_core::xpath::ast::{Expr, LocationPath, NodeTest, Step};
use sup_xml_core::xpath::eval::{
eval_expr, EvalCtx, NoBindings, StaticContext, Value, XPathBindings,
};
use sup_xml_core::xpath::{DocIndexLike, NodeId};
use crate::ast::{QName, StylesheetAst, Template};
type Result<T> = std::result::Result<T, XmlError>;
pub fn matches<I: DocIndexLike>(
pattern: &Expr,
node: NodeId,
idx: &I,
bindings: &dyn XPathBindings,
) -> Result<bool> {
sup_xml_core::xpath::eval::reset_eval_budget();
if let Expr::BackwardsCompat(inner) = pattern {
return matches(inner, node, idx, bindings);
}
if let Expr::Union(l, r) = pattern {
if matches(l, node, idx, bindings)? { return Ok(true); }
return matches(r, node, idx, bindings);
}
if pattern_is_document_node(pattern) {
return Ok(matches!(idx.kind(node),
sup_xml_core::xpath::XPathNodeKind::Document));
}
if let Some(Some(inner)) = single_step_document_test(pattern) {
if !matches!(idx.kind(node), sup_xml_core::xpath::XPathNodeKind::Document) {
return Ok(false);
}
return Ok(idx.children(node).iter().any(|c|
matches!(idx.kind(*c), sup_xml_core::xpath::XPathNodeKind::Element)
&& sup_xml_core::xpath::eval::node_matches_child(*c, inner, idx, bindings)));
}
if let Some(rest) = rewrite_document_node_prefix(pattern) {
return matches(&rest, node, idx, bindings);
}
let sc = StaticContext {
xpath_2_0: bindings.xpath_version_2_or_later(),
xpath_3_0: false,
libxml2_compatible: false, current_node: None,
};
let mut cur = Some(node);
while let Some(ctx_node) = cur {
let ctx = EvalCtx {
context_node: ctx_node, pos: 1, size: 1, bindings, static_ctx: &sc };
let v = eval_expr(pattern, &ctx, idx)?;
if let Value::NodeSet(ns) = v {
if ns.contains(&node) {
return Ok(true);
}
}
cur = idx.parent(ctx_node);
}
Ok(false)
}
fn rewrite_document_node_prefix(p: &Expr) -> Option<Expr> {
if let Expr::Path(LocationPath::Relative(steps)) = p {
if steps.len() >= 2
&& matches!(&steps[0].node_test, NodeTest::Document(None))
&& steps[0].predicates.is_empty()
{
return Some(Expr::Path(LocationPath::Absolute(steps[1..].to_vec())));
}
}
None
}
fn single_step_document_test(p: &Expr) -> Option<&Option<Box<NodeTest>>> {
let step = single_step_pattern(p)?;
if !step.predicates.is_empty() {
return None;
}
match &step.node_test {
NodeTest::Document(inner) => Some(inner),
_ => None,
}
}
fn pattern_is_document_node(p: &Expr) -> bool {
fn step_is_doc(s: &Step) -> bool {
matches!(&s.node_test, NodeTest::Document(None)) && s.predicates.is_empty()
}
match p {
Expr::Path(LocationPath::Relative(s)) if s.len() == 1
=> step_is_doc(&s[0]),
Expr::Path(LocationPath::Absolute(s)) if s.is_empty() => true,
Expr::Union(a, b) => pattern_is_document_node(a) || pattern_is_document_node(b),
_ => false,
}
}
pub fn default_priority(pattern: &Expr) -> f64 {
if let Expr::BackwardsCompat(inner) = pattern {
return default_priority(inner);
}
if let Expr::Union(l, r) = pattern {
return default_priority(l).max(default_priority(r));
}
if pattern_is_document_node(pattern) {
return -0.5;
}
let single_step = single_step_pattern(pattern);
match single_step {
Some(step) if step.predicates.is_empty() => match &step.node_test {
NodeTest::QName(_, _) => 0.0,
NodeTest::DefaultNamespaceName { .. } => 0.0,
NodeTest::PrefixWildcard(_) => -0.25,
NodeTest::LocalNameOnly(_) => -0.25,
NodeTest::LocalName(_) => 0.0,
NodeTest::AnyNode | NodeTest::Wildcard
| NodeTest::Text | NodeTest::Comment
| NodeTest::PI(None) => -0.5,
NodeTest::Document(inner) => match inner.as_deref() {
Some(NodeTest::QName(..))
| Some(NodeTest::DefaultNamespaceName { .. })
| Some(NodeTest::LocalName(_)) => 0.0,
Some(NodeTest::PrefixWildcard(_))
| Some(NodeTest::LocalNameOnly(_)) => -0.25,
_ => -0.5,
},
NodeTest::PI(Some(_)) => 0.0,
},
_ => 0.5,
}
}
fn single_step_pattern(expr: &Expr) -> Option<&Step> {
match expr {
Expr::Path(lp) => {
let steps = match lp {
LocationPath::Absolute(s) | LocationPath::Relative(s) => s,
};
if steps.len() == 1 { Some(&steps[0]) } else { None }
}
_ => None,
}
}
pub struct Selected<'a> {
pub template: &'a Template,
pub priority: f64,
pub branch_idx: Option<usize>,
}
pub fn pattern_branches(pat: &Expr) -> Vec<&Expr> {
fn walk<'a>(p: &'a Expr, out: &mut Vec<&'a Expr>) {
match p {
Expr::BackwardsCompat(inner) => walk(inner, out),
Expr::Union(l, r) => { walk(l, out); walk(r, out); }
_ => out.push(p),
}
}
let mut out = Vec::new();
walk(pat, &mut out);
out
}
pub fn select_template<'a, I: DocIndexLike>(
style: &'a StylesheetAst,
node: NodeId,
mode: Option<&QName>,
idx: &I,
bindings: &dyn XPathBindings,
) -> Result<Option<Selected<'a>>> {
select_template_inner(style, node, mode, idx, bindings, None)
}
pub fn select_template_max_precedence<'a, I: DocIndexLike>(
style: &'a StylesheetAst,
node: NodeId,
mode: Option<&QName>,
idx: &I,
bindings: &dyn XPathBindings,
max_precedence: i32,
) -> Result<Option<Selected<'a>>> {
select_template_inner(style, node, mode, idx, bindings, Some(max_precedence))
}
pub fn select_template_next<'a, I: DocIndexLike>(
style: &'a StylesheetAst,
node: NodeId,
mode: Option<&QName>,
idx: &I,
bindings: &dyn XPathBindings,
current: &Selected<'_>,
current_index: usize,
) -> Result<Option<Selected<'a>>> {
let cur_prec = current.template.import_precedence;
let cur_prio = current.priority;
let cur_path = current.template.source_path.as_slice();
let cur_branch = current.branch_idx;
let mut best: Option<Selected<'a>> = None;
let mut best_path: &[u32] = &[];
let mut best_branch: Option<usize> = None;
for (i, t) in style.templates.iter().enumerate() {
let Some(pat) = t.match_pattern.as_ref() else { continue; };
if !template_mode_matches(t, mode) { continue; }
let branches = pattern_branches(pat);
let multi = branches.len() > 1;
for (b, branch_pat) in branches.iter().enumerate() {
if i == current_index && (!multi || Some(b) == cur_branch) {
continue;
}
if !matches(branch_pat, node, idx, bindings)? { continue; }
let priority = match t.priority {
Some(p) => p,
None => default_priority(branch_pat),
};
let branch_idx = if multi { Some(b) } else { None };
let prec = t.import_precedence;
let path = t.source_path.as_slice();
let strictly_after_current = if prec != cur_prec {
prec < cur_prec
} else if (priority - cur_prio).abs() > f64::EPSILON {
priority < cur_prio
} else if path != cur_path {
path < cur_path
} else {
branch_idx < cur_branch
};
if !strictly_after_current { continue; }
let take = match &best {
None => true,
Some(bs) => {
let bprec = bs.template.import_precedence;
if prec != bprec {
prec > bprec
} else if (priority - bs.priority).abs() > f64::EPSILON {
priority > bs.priority
} else if path != best_path {
path > best_path
} else {
branch_idx > best_branch
}
}
};
if take {
best = Some(Selected { template: t, priority, branch_idx });
best_path = path;
best_branch = branch_idx;
}
}
}
Ok(best)
}
fn select_template_inner<'a, I: DocIndexLike>(
style: &'a StylesheetAst,
node: NodeId,
mode: Option<&QName>,
idx: &I,
bindings: &dyn XPathBindings,
max_precedence: Option<i32>,
) -> Result<Option<Selected<'a>>> {
let mut best: Option<Selected<'a>> = None;
let mut best_path: &[u32] = &[];
let mut best_branch: Option<usize> = None;
let mut multiple = false;
for t in style.templates.iter() {
if let Some(cap) = max_precedence {
if t.import_precedence > cap { continue; }
}
let Some(pat) = t.match_pattern.as_ref() else { continue; };
if !template_mode_matches(t, mode) { continue; }
let branches = pattern_branches(pat);
let multi = branches.len() > 1;
for (b, branch_pat) in branches.iter().enumerate() {
if !matches(branch_pat, node, idx, bindings)? { continue; }
let priority = match t.priority {
Some(p) => p,
None => default_priority(branch_pat),
};
let branch_idx = if multi { Some(b) } else { None };
let (take, tie) = match &best {
None => (true, false),
Some(bs) => {
let prec = t.import_precedence;
let bprec = bs.template.import_precedence;
let path = t.source_path.as_slice();
if prec != bprec {
(prec > bprec, false)
} else if (priority - bs.priority).abs() > f64::EPSILON {
(priority > bs.priority, false)
} else if path != best_path {
(path > best_path, true)
} else {
(branch_idx > best_branch, true)
}
}
};
if take && !tie { multiple = false; }
if tie { multiple = true; }
if take {
best = Some(Selected { template: t, priority, branch_idx });
best_path = t.source_path.as_slice();
best_branch = branch_idx;
}
}
}
if multiple && on_multiple_match_is_error() {
return Err(sup_xml_core::xpath::eval::xpath_err(
"more than one template rule matches the node with the same \
import precedence and priority"
).with_xpath_code("XTRE0540"));
}
Ok(best)
}
thread_local! {
static ON_MULTIPLE_MATCH_ERROR: std::cell::Cell<bool> =
const { std::cell::Cell::new(false) };
}
fn on_multiple_match_is_error() -> bool {
ON_MULTIPLE_MATCH_ERROR.with(|c| c.get())
}
pub fn set_on_multiple_match_error(v: bool) -> bool {
ON_MULTIPLE_MATCH_ERROR.with(|c| c.replace(v))
}
fn mode_matches(template_mode: Option<&QName>, requested: Option<&QName>) -> bool {
match (template_mode, requested) {
(None, None) => true,
(Some(a), Some(b)) => a.uri == b.uri && a.local == b.local,
_ => false,
}
}
fn template_mode_matches(t: &crate::ast::Template, requested: Option<&QName>) -> bool {
if t.modes_match_all { return true; }
if t.modes.is_empty() {
return mode_matches(t.mode.as_ref(), requested);
}
let is_default = |q: &QName| q.local.is_empty() && q.uri.is_empty();
match requested {
None => t.modes.iter().any(is_default),
Some(r) => t.modes.iter().any(|m|
!is_default(m) && m.uri == r.uri && m.local == r.local),
}
}
pub fn select_template_no_bindings<'a, I: DocIndexLike>(
style: &'a StylesheetAst,
node: NodeId,
mode: Option<&QName>,
idx: &I,
) -> Result<Option<Selected<'a>>> {
select_template(style, node, mode, idx, &NoBindings)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Stylesheet;
use sup_xml_core::{parse_str, ParseOptions, XPathContext};
fn build_stylesheet(body: &str) -> Stylesheet {
let text = format!(
r#"<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">{body}</xsl:stylesheet>"#,
);
Stylesheet::compile_str(&text).unwrap()
}
fn doc_ns(xml: &str) -> sup_xml_tree::dom::Document {
let opts = ParseOptions { namespace_aware: true, ..ParseOptions::default() };
parse_str(xml, &opts).unwrap()
}
#[test]
fn priority_star_is_negative_half() {
let xslt = build_stylesheet(r#"<xsl:template match="*"/>"#);
let p = default_priority(xslt.ast.templates[0].match_pattern.as_ref().unwrap());
assert_eq!(p, -0.5);
}
#[test]
fn priority_named_element_is_zero() {
let xslt = build_stylesheet(r#"<xsl:template match="book"/>"#);
let p = default_priority(xslt.ast.templates[0].match_pattern.as_ref().unwrap());
assert_eq!(p, 0.0);
}
#[test]
fn priority_node_test_is_negative_half() {
let xslt = build_stylesheet(r#"<xsl:template match="text()"/>"#);
let p = default_priority(xslt.ast.templates[0].match_pattern.as_ref().unwrap());
assert_eq!(p, -0.5);
}
#[test]
fn priority_path_is_half() {
let xslt = build_stylesheet(r#"<xsl:template match="book/chapter"/>"#);
let p = default_priority(xslt.ast.templates[0].match_pattern.as_ref().unwrap());
assert_eq!(p, 0.5);
}
#[test]
fn priority_predicate_is_half() {
let xslt = build_stylesheet(r#"<xsl:template match="book[@x]"/>"#);
let p = default_priority(xslt.ast.templates[0].match_pattern.as_ref().unwrap());
assert_eq!(p, 0.5);
}
#[test]
fn root_pattern_matches_document_node() {
let xslt = build_stylesheet(r#"<xsl:template match="/"/>"#);
let pat = xslt.ast.templates[0].match_pattern.as_ref().unwrap();
let doc = doc_ns("<r/>");
let ctx = XPathContext::new(&doc);
assert!(matches(pat, 0, &ctx.index, &NoBindings).unwrap());
}
#[test]
fn star_pattern_matches_elements() {
let xslt = build_stylesheet(r#"<xsl:template match="*"/>"#);
let pat = xslt.ast.templates[0].match_pattern.as_ref().unwrap();
let doc = doc_ns("<r><a/></r>");
let ctx = XPathContext::new(&doc);
let v = ctx.eval("/r").unwrap();
let r_id = match v {
Value::NodeSet(ns) => ns[0],
_ => panic!(),
};
assert!(matches(pat, r_id, &ctx.index, &NoBindings).unwrap());
}
#[test]
fn named_pattern_only_matches_that_name() {
let xslt = build_stylesheet(r#"<xsl:template match="book"/>"#);
let pat = xslt.ast.templates[0].match_pattern.as_ref().unwrap();
let doc = doc_ns("<r><book/><article/></r>");
let ctx = XPathContext::new(&doc);
let book_id = match ctx.eval("/r/book").unwrap() {
Value::NodeSet(ns) => ns[0], _ => panic!(),
};
let art_id = match ctx.eval("/r/article").unwrap() {
Value::NodeSet(ns) => ns[0], _ => panic!(),
};
assert!(matches(pat, book_id, &ctx.index, &NoBindings).unwrap());
assert!(!matches(pat, art_id, &ctx.index, &NoBindings).unwrap());
}
#[test]
fn path_pattern_walks_ancestors() {
let xslt = build_stylesheet(r#"<xsl:template match="book/chapter"/>"#);
let pat = xslt.ast.templates[0].match_pattern.as_ref().unwrap();
let doc = doc_ns("<root><book><chapter/></book></root>");
let ctx = XPathContext::new(&doc);
let chap_id = match ctx.eval("/root/book/chapter").unwrap() {
Value::NodeSet(ns) => ns[0], _ => panic!(),
};
assert!(matches(pat, chap_id, &ctx.index, &NoBindings).unwrap());
}
#[test]
fn path_pattern_does_not_match_outside_path() {
let xslt = build_stylesheet(r#"<xsl:template match="book/chapter"/>"#);
let pat = xslt.ast.templates[0].match_pattern.as_ref().unwrap();
let doc = doc_ns("<root><chapter/></root>");
let ctx = XPathContext::new(&doc);
let chap_id = match ctx.eval("/root/chapter").unwrap() {
Value::NodeSet(ns) => ns[0], _ => panic!(),
};
assert!(!matches(pat, chap_id, &ctx.index, &NoBindings).unwrap());
}
#[test]
fn selects_higher_default_priority() {
let xslt = build_stylesheet(r#"
<xsl:template match="*"><star/></xsl:template>
<xsl:template match="a"><named/></xsl:template>
"#);
let doc = doc_ns("<r><a/></r>");
let ctx = XPathContext::new(&doc);
let a_id = match ctx.eval("/r/a").unwrap() {
Value::NodeSet(ns) => ns[0], _ => panic!(),
};
let sel = select_template_no_bindings(&xslt.ast, a_id, None, &ctx.index).unwrap().unwrap();
match &sel.template.body[0] {
crate::ast::Instr::LiteralElement { name, .. } => assert_eq!(name.local, "named"),
other => panic!("expected LiteralElement, got {other:?}"),
}
}
#[test]
fn explicit_priority_overrides_default() {
let xslt = build_stylesheet(r#"
<xsl:template match="*" priority="10"><high/></xsl:template>
<xsl:template match="a"><low/></xsl:template>
"#);
let doc = doc_ns("<r><a/></r>");
let ctx = XPathContext::new(&doc);
let a_id = match ctx.eval("/r/a").unwrap() {
Value::NodeSet(ns) => ns[0], _ => panic!(),
};
let sel = select_template_no_bindings(&xslt.ast, a_id, None, &ctx.index).unwrap().unwrap();
match &sel.template.body[0] {
crate::ast::Instr::LiteralElement { name, .. } => assert_eq!(name.local, "high"),
other => panic!("expected LiteralElement, got {other:?}"),
}
}
#[test]
fn ties_break_by_document_order_last_wins() {
let xslt = build_stylesheet(r#"
<xsl:template match="*"><first/></xsl:template>
<xsl:template match="*"><second/></xsl:template>
"#);
let doc = doc_ns("<r/>");
let ctx = XPathContext::new(&doc);
let r_id = match ctx.eval("/r").unwrap() {
Value::NodeSet(ns) => ns[0], _ => panic!(),
};
let sel = select_template_no_bindings(&xslt.ast, r_id, None, &ctx.index).unwrap().unwrap();
match &sel.template.body[0] {
crate::ast::Instr::LiteralElement { name, .. } => assert_eq!(name.local, "second"),
other => panic!("expected LiteralElement, got {other:?}"),
}
}
#[test]
fn no_match_returns_none() {
let xslt = build_stylesheet(r#"<xsl:template match="book"/>"#);
let doc = doc_ns("<r><article/></r>");
let ctx = XPathContext::new(&doc);
let art_id = match ctx.eval("/r/article").unwrap() {
Value::NodeSet(ns) => ns[0], _ => panic!(),
};
let sel = select_template_no_bindings(&xslt.ast, art_id, None, &ctx.index).unwrap();
assert!(sel.is_none());
}
#[test]
fn mode_filters_templates() {
let xslt = build_stylesheet(r#"
<xsl:template match="a"><default/></xsl:template>
<xsl:template match="a" mode="big"><big/></xsl:template>
"#);
let doc = doc_ns("<r><a/></r>");
let ctx = XPathContext::new(&doc);
let a_id = match ctx.eval("/r/a").unwrap() {
Value::NodeSet(ns) => ns[0], _ => panic!(),
};
let sel = select_template_no_bindings(&xslt.ast, a_id, None, &ctx.index).unwrap().unwrap();
match &sel.template.body[0] {
crate::ast::Instr::LiteralElement { name, .. } => assert_eq!(name.local, "default"),
_ => panic!(),
}
let big = QName { prefix: None, local: "big".into(), uri: String::new() };
let sel = select_template_no_bindings(&xslt.ast, a_id, Some(&big), &ctx.index).unwrap().unwrap();
match &sel.template.body[0] {
crate::ast::Instr::LiteralElement { name, .. } => assert_eq!(name.local, "big"),
_ => panic!(),
}
}
fn parse_pat(src: &str) -> Expr {
sup_xml_core::xpath::parse_xpath_with(src,
&sup_xml_core::xpath::XPathOptions {
xpath_2_0: true, libxml2_compatible: false,
..sup_xml_core::xpath::XPathOptions::default()
}).unwrap()
}
#[test]
fn detects_bare_document_node_pattern() {
assert!(super::pattern_is_document_node(&parse_pat("document-node()")));
assert!(super::pattern_is_document_node(&parse_pat("/")));
}
#[test]
fn detects_document_node_branch_inside_union() {
assert!(super::pattern_is_document_node(&parse_pat("* | /")));
assert!(super::pattern_is_document_node(&parse_pat("/ | foo")));
}
#[test]
fn rejects_non_document_node_patterns() {
assert!(!super::pattern_is_document_node(&parse_pat("foo")));
assert!(!super::pattern_is_document_node(&parse_pat("element()")));
assert!(!super::pattern_is_document_node(&parse_pat("foo/bar")));
}
#[test]
fn rewrites_document_node_slash_rest() {
let p = parse_pat("document-node()/element()");
let rest = super::rewrite_document_node_prefix(&p).expect("should rewrite");
match rest {
Expr::Path(LocationPath::Absolute(steps)) => {
assert_eq!(steps.len(), 1);
assert!(matches!(steps[0].node_test, NodeTest::Wildcard | NodeTest::AnyNode));
}
other => panic!("expected absolute path, got {other:?}"),
}
}
#[test]
fn does_not_rewrite_bare_or_unrelated() {
assert!(super::rewrite_document_node_prefix(&parse_pat("document-node()")).is_none());
assert!(super::rewrite_document_node_prefix(&parse_pat("element()/foo")).is_none());
}
#[test]
fn default_priority_for_document_node_patterns() {
assert_eq!(super::default_priority(&parse_pat("/")), -0.5);
assert_eq!(super::default_priority(&parse_pat("document-node()")), -0.5);
}
}