Skip to main content

sup_xml_xslt/
pattern.rs

1//! XSLT pattern matching — XSLT 1.0 §5.2.
2//!
3//! XSLT patterns (`match=`, `xsl:key match=`, `xsl:number
4//! count=/from=`) are a restricted XPath grammar.  Rather than
5//! implement a separate matcher, we compile patterns as full XPath
6//! expressions and use the trick from the spec:
7//!
8//! > A node matches a pattern if there is some possible context
9//! > such that evaluating the pattern as an expression with that
10//! > context yields a node-set containing the node being matched.
11//!
12//! Concretely: walk the ancestor-or-self chain of the candidate
13//! node, evaluating the pattern as an XPath at each one.  If any
14//! evaluation produces a node-set containing the candidate, the
15//! pattern matches.
16//!
17//! For default priority we look at the pattern's shape — a name
18//! test `prefix:local` defaults to 0, `*` to -0.5, etc., per
19//! §5.5.  A `|` union behaves as several template rules, one per
20//! alternative (XSLT 2.0 §6.4): template selection flattens the
21//! union via [`pattern_branches`] and evaluates each branch as its
22//! own logical rule, with its own default priority.  `xsl:next-match`
23//! can therefore step from one branch to another within the same
24//! template before falling through to a different template.
25//!
26//! Template selection picks the best template per XSLT's
27//! priority + import-precedence rules.  Currently a linear scan
28//! across all templates; an index by innermost-step-name is a
29//! known optimisation tracked for later.
30
31use 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
42// ── match-ness check ──────────────────────────────────────────────
43
44/// Does `pattern` match `node`?  Walks the ancestor-or-self chain
45/// and evaluates the pattern at each context until either a match
46/// is found or the chain is exhausted.
47pub fn matches<I: DocIndexLike>(
48    pattern: &Expr,
49    node:    NodeId,
50    idx:     &I,
51    bindings: &dyn XPathBindings,
52) -> Result<bool> {
53    // Fresh step budget per pattern match — XSLT runs this once per
54    // (template, candidate-node) pair and the cap is meant to bound a
55    // single evaluation, not the cross-cutting sum.
56    sup_xml_core::xpath::eval::reset_eval_budget();
57    // See through the synthetic backwards-compat wrapper added in a
58    // `version="1.0"` scope so the structural pattern handling below
59    // (union split, document-node, single-step) sees the real shape.
60    if let Expr::BackwardsCompat(inner) = pattern {
61        return matches(inner, node, idx, bindings);
62    }
63    // XSLT 2.0 §5.5.3 — patterns may anchor on `document-node()`.
64    // The "find some context that places `node` in the pattern's
65    // result set" trick fails for these shapes (no XPath
66    // `child::document-node()` step can ever reach a document node).
67    // Handle them structurally here.  Union patterns get split into
68    // their branches and each is matched independently so other
69    // branches keep their normal eval-walk semantics.
70    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    // `document-node(element(N))` / `document-node(element(*))` — like
79    // the bare form, no `child::` step reaches a document node, so
80    // match structurally: the node must be a document node whose
81    // document element satisfies the inner test.
82    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    // `document-node()/REST` reads as "REST anchored to the document
91    // node".  Since every node lives under a document anyway, that
92    // anchor is a tautology in our trees — evaluate REST as an
93    // ordinary absolute path.  This rewrite handles patterns like
94    // `document-node()/child::element()` that the ancestor walk
95    // otherwise can't satisfy (because no XPath child:: step ever
96    // produces a document node).
97    if let Some(rest) = rewrite_document_node_prefix(pattern) {
98        return matches(&rest, node, idx, bindings);
99    }
100    // Walk node, parent(node), parent(parent(node))…
101    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
121/// If `p` is `document-node()/REST` (a relative path whose first
122/// step is `document-node()`), return REST as an absolute path
123/// (rooted at the document).  Returns `None` for other shapes.
124fn 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
136/// If `p` is a single-step `document-node(...)` test with no
137/// predicates, return its inner element test — `&None` for the bare
138/// `document-node()`, `&Some(t)` for `document-node(element(t))`.
139/// Returns `None` for any other pattern shape.
140fn 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
151/// Detect a pattern whose only effect is `document-node()` — either
152/// a bare `document-node()`, or a union one of whose branches is.
153fn 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
166// ── default priority (XSLT 1.0 §5.5) ──────────────────────────────
167
168/// Compute the default priority for a pattern AST node.  The XSLT
169/// spec defines four buckets:
170///
171/// | Pattern shape                                       | Priority |
172/// |-----------------------------------------------------|----------|
173/// | `prefix:local`, `@prefix:local`                     |   0      |
174/// | `NCName:*`, `@NCName:*`                             |  -0.25   |
175/// | `*`, `@*`, `node()`, `text()`, `comment()`, `pi()`  |  -0.5    |
176/// | anything more specific (paths, predicates, etc.)    |   0.5    |
177pub fn default_priority(pattern: &Expr) -> f64 {
178    // See through the synthetic backwards-compat wrapper the compiler
179    // adds in a `version="1.0"` scope — it carries no structural
180    // information relevant to the pattern's default priority.
181    if let Expr::BackwardsCompat(inner) = pattern {
182        return default_priority(inner);
183    }
184    // Unions: each branch independently; take the max.
185    if let Expr::Union(l, r) = pattern {
186        return default_priority(l).max(default_priority(r));
187    }
188    // XSLT 2.0 §6.4 — `match="/"` (and `document-node()`) default
189    // to priority -0.5, the same bucket as `*` and `node()`.
190    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            // XPath 2.0 `*:NCName` — half-bound: a specific local
200            // name in any namespace.  Less specific than a full
201            // QName, more specific than `prefix:*`.
202            NodeTest::LocalNameOnly(_)       => -0.25,
203            NodeTest::LocalName(_)           => 0.0,
204            // node() / text() / comment() / pi() / *  — the
205            // least-specific patterns.
206            NodeTest::AnyNode | NodeTest::Wildcard
207                | NodeTest::Text | NodeTest::Comment
208                | NodeTest::PI(None) => -0.5,
209            // `document-node()` and `document-node(element(*))` are
210            // the least-specific (-0.5); `document-node(element(N))`
211            // carries the name's specificity, so it gets the priority
212            // of the inner element name test (XSLT 2.0 §6.4).
213            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            // pi('target') is more specific than pi() — gets 0.
222            NodeTest::PI(Some(_)) => 0.0,
223        },
224        _ => 0.5,
225    }
226}
227
228/// If `expr` is a one-step location path (with or without an
229/// absolute root), return that step.  XSLT patterns with predicates
230/// or multiple steps don't qualify as "simple" so they get the
231/// more-specific default priority.
232fn 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
244// ── template selection ────────────────────────────────────────────
245
246/// Result of looking up a template: a borrow into the stylesheet's
247/// template list plus the effective priority used to break ties.
248///
249/// XSLT 2.0 §6.4: when a template has a union pattern `A|B`, the
250/// rule behaves as if each operand were a separate template rule
251/// with the same body and source position.  `branch_idx` records
252/// which operand actually matched the node — `Some(i)` indexes the
253/// flattened operand list (see [`pattern_branches`]); `None`
254/// indicates a non-union pattern.  `xsl:next-match` uses this to
255/// pick up other branches of the same template before falling
256/// through to templates with lower precedence/priority.
257pub struct Selected<'a> {
258    pub template:   &'a Template,
259    pub priority:   f64,
260    pub branch_idx: Option<usize>,
261}
262
263/// Flatten a pattern's outermost union into its operands.  Returns
264/// `[pat]` for non-union patterns, `[A, B, …]` (in source order) for
265/// `A|B|…`.  Sees through the synthetic `BackwardsCompat` wrapper
266/// the compiler adds in a `version="1.0"` scope, but does not look
267/// inside nested expressions (only the union shape at the top
268/// matters for §6.4 splitting).
269pub 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
282/// Find the best-matching template for `node` under `mode`.  XSLT
283/// 1.0 conflict resolution (§5.5):
284///
285/// 1. Filter to templates with `match=` that match the node and
286///    whose mode matches.
287/// 2. Within that set, pick the highest effective priority
288///    (explicit `priority=` if present, else default).
289/// 3. Ties break by document order — last wins.
290///
291/// (Import precedence — the additional dimension from
292/// `xsl:import` — lands when the include/import resolver does;
293/// for now every template is at the same precedence.)
294pub 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
304/// Variant of [`select_template`] used by `xsl:apply-imports`:
305/// limits candidates to templates whose `import_precedence` is
306/// at most `max_precedence`.  Conflict resolution within that
307/// pool follows the same rules.
308pub 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
319/// XSLT 2.0 §6.7 `xsl:next-match` — pick the next template in the
320/// conflict-resolution order after `current`.  Conflict order is
321/// (precedence descending, priority descending, source position
322/// descending — last in source wins ties); "next" means strictly
323/// less along that order than `current`.  Returns `None` when no
324/// such template matches the node + mode.
325///
326/// Union patterns participate per §6.4: each operand acts as a
327/// separate logical rule, so `xsl:next-match` may select another
328/// branch of the *same* template (sharing its body) before falling
329/// through to a different template.  Within a single template, the
330/// branch index acts as a secondary source position — later branches
331/// are treated as later in source order for tie-breaking.
332pub 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            // The current (template, branch) is itself excluded
355            // from next-match — the body has already run once for it.
356            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            // Strict "less than current" in conflict-resolution order:
366            // either lower precedence, or same precedence + lower
367            // priority, or same precedence + same priority + earlier
368            // (template source path, branch index).
369            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                // Same template (different branch).
379                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    // Whether ≥2 distinct rules share the winning (precedence, priority)
419    // tier — an unresolved conflict (XSLT 1.0 §5.5).  Reset whenever the
420    // winner moves to a strictly higher tier.
421    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        // Only templates with match= participate in pattern-based
427        // selection (`name=`-only templates are call-targets).
428        let Some(pat) = t.match_pattern.as_ref() else { continue; };
429        if !template_mode_matches(t, mode) { continue; }
430        // XSLT 2.0 §6.4 — each union operand is a logical template
431        // rule with its own default priority.  For non-union patterns
432        // `pattern_branches` yields a single entry.
433        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            // Conflict resolution per XSLT 1.0 §5.5 / 2.0 §6.4:
443            // 1. Highest import precedence wins.
444            // 2. Highest priority wins (within the same precedence).
445            // 3. Last in (include-aware) source order wins — with
446            //    branch index as a secondary source position so later
447            //    union operands beat earlier ones on the same template.
448            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                        // Same template, different branch — strictly
462                        // later branch wins; identical (i, b) can't
463                        // occur because we iterate distinct positions.
464                        (branch_idx > best_branch, true)
465                    }
466                }
467            };
468            // Moving to a strictly higher tier clears any earlier tie;
469            // a same-tier match (whoever wins source-order) records one.
470            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    // XSLT 1.0 §5.5 / XTRE0540 — when configured to report (rather than
480    // recover from) an unresolved conflict, a tie at the winning tier
481    // is a dynamic error.
482    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    /// Per-thread switch: when set, an unresolved template conflict is
493    /// REPORTED as XTRE0540 rather than recovered from (use-last).  The
494    /// host sets it around an apply (e.g. the W3C harness honouring an
495    /// `on-multiple-match="error"` dependency); default is to recover.
496    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
504/// Set whether unresolved template conflicts are reported (XTRE0540)
505/// instead of recovered.  Returns the previous value so the caller can
506/// restore it after the apply.
507pub 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
519/// XSLT 2.0 §6 — a template participates in mode `requested` when:
520/// * it declared `mode="#all"`, OR
521/// * the requested mode is in its `modes` list (an empty-name
522///   QName represents `#default`).
523/// XSLT 1.0 templates with no `mode=` attribute keep matching only
524/// the default mode (the `modes` vec is empty and `mode` is None).
525fn 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        // Legacy / single-mode shape: empty list + None mode means
529        // "default mode only".
530        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
540// ── public helpers ────────────────────────────────────────────────
541
542/// Convenience entry point for callers that don't need to thread
543/// custom XPath bindings (e.g. simple stylesheets with no `xsl:key`
544/// references in the matched patterns).  Uses `NoBindings`.
545pub 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    // ── default-priority table (XSLT 1.0 §5.5) ───────────────
574
575    #[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    // ── match-ness ──────────────────────────────────────────
611
612    #[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        // Node 0 in the index is the synthetic document node.
619        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        // Element <r> exists somewhere in the index; query via XPath
629        // to find its NodeId.
630        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        // match="book/chapter" must match a <chapter> whose parent is
657        // <book>.  Evaluating "book/chapter" against the chapter node
658        // directly returns nothing; the matcher's ancestor walk reaches
659        // the book element, evaluates from there, and finds the
660        // chapter in the result set.
661        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        // chapter directly inside root, not inside book — shouldn't match.
676        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    // ── template selection ──────────────────────────────────
685
686    #[test]
687    fn selects_higher_default_priority() {
688        // Two templates, both match an <a> element.  match="a" has
689        // priority 0, match="*" has priority -0.5.  The named one
690        // wins.
691        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        // Named template body emits <named/>.
702        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        // Two identical-priority templates both matching `*`.
729        // Document order: first one, then second one.  Spec says
730        // last-in-document-order wins.
731        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        // No mode → matches the unmoded template.
771        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        // Mode "big" → matches the moded template.
777        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    // ── pattern_is_document_node / rewrite_document_node_prefix ──
786
787    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        // Absolute `/` parses to an empty-step absolute path — also
799        // a document-node anchor in XSLT pattern semantics.
800        assert!(super::pattern_is_document_node(&parse_pat("/")));
801    }
802
803    #[test]
804    fn detects_document_node_branch_inside_union() {
805        // Either branch matching is enough — used by `* | /`.
806        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        // `document-node()/element()` should rewrite to an absolute
820        // path containing only the element() step.
821        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        // Bare `document-node()` — no rest to rewrite.
835        assert!(super::rewrite_document_node_prefix(&parse_pat("document-node()")).is_none());
836        // Path that doesn't start with document-node().
837        assert!(super::rewrite_document_node_prefix(&parse_pat("element()/foo")).is_none());
838    }
839
840    // ── default_priority for `/` and `document-node()` ───────────
841
842    #[test]
843    fn default_priority_for_document_node_patterns() {
844        // XSLT 2.0 §6.4 — `/` and `document-node()` are the lowest
845        // bucket alongside `*` / `node()`.
846        assert_eq!(super::default_priority(&parse_pat("/")),               -0.5);
847        assert_eq!(super::default_priority(&parse_pat("document-node()")), -0.5);
848    }
849}