Skip to main content

libxml_rs/xslt/patterns/
mod.rs

1//! XSLT pattern matching (§33, §85 Phase 8).
2//!
3//! XSLT patterns are a subset of XPath 1.0 used in `match` attributes of
4//! `<xsl:template>`, `<xsl:key>`, `<xsl:strip-space>`, `<xsl:preserve-space>`.
5//!
6//! Patterns can include:
7//! - Location paths (relative and absolute)
8//! - Union patterns (pattern1 | pattern2)
9//! - Node tests: *, node(), text(), comment(), processing-instruction()
10//! - Attribute axis (@attr)
11//! - Namespace prefix tests (ns:*, ns:name)
12//! - Predicates (limited)
13//! - The id() and key() functions
14//!
15//! # UPSTREAM-PARITY
16//!
17//! Implements XSLT 1.0 §5 "Patterns" with match semantics matching libxslt 1.1.45.
18//! Pattern matching uses the XPath 1.0 AST from `src/xml/xpath` for parsing, then
19//! applies XSLT-specific matching rules.
20//!
21//! # Courts
22//!
23//! XSLT-PATTERNS-*
24
25use crate::abi::structs::*;
26use crate::abi::types::*;
27use crate::xml::string::xmlstr_to_string;
28use crate::xml::xpath::ast::{Axis, Expr, NameTest, NodeTest, Step};
29use crate::xml::xpath::parser::parse_xpath;
30use crate::xml::xpath::types::{NodeSet, XPathValue};
31use std::os::raw::c_int;
32use std::ptr;
33
34/// Sentinel for "no explicit priority" (upstream XSLT_PAT_NO_PRIORITY).
35pub const XSLT_PAT_NO_PRIORITY: f64 = -1.0e9;
36
37// ═══════════════════════════════════════════════════════════════════════════════
38// Internal Pattern Representation
39// ═══════════════════════════════════════════════════════════════════════════════
40
41/// A compiled pattern step — a single axis::node-test[predicates] in a pattern.
42///
43/// This is the internal representation backing `_xsltPatternStep`.
44/// The C ABI type `_xsltPatternStep` is a zero-sized opaque marker;
45/// the real data lives here.
46#[derive(Debug, Clone)]
47pub(crate) struct XsltPatternStep {
48    /// The axis (defaults to Child for element tests, Attribute for @attr).
49    pub axis: Axis,
50    /// The node test.
51    pub node_test: NodeTest,
52    /// Compiled predicate expressions.
53    pub predicates: Vec<Expr>,
54}
55
56/// A single pattern within a union pattern (one side of `|`).
57#[derive(Debug, Clone)]
58pub(crate) struct XsltPattern {
59    /// Steps in this pattern, in reverse order for efficient matching.
60    /// For a pattern like `foo/bar/baz`, steps are [baz, bar, foo].
61    /// For a pattern like `foo//bar`, steps contain a DescendantOrSelf sentinel.
62    pub steps: Vec<PatternStepEntry>,
63    /// Whether this pattern is absolute (starts with `/`).
64    pub is_absolute: bool,
65    /// The original pattern string for this branch.
66    pub original: String,
67    /// The parsed XPath expression (kept for predicate evaluation).
68    pub expr: Expr,
69}
70
71/// One entry in the step chain of a compiled pattern.
72#[derive(Debug, Clone)]
73pub(crate) enum PatternStepEntry {
74    /// A normal step with axis, node test, and predicates.
75    Step(XsltPatternStep),
76    /// A `//` separator — matches descendant-or-self::node().
77    DescendantOrSelf,
78}
79
80/// Full compiled pattern (union of multiple sub-patterns).
81#[derive(Debug, Clone)]
82pub(crate) struct CompiledPattern {
83    /// The branches of a union pattern (`pattern1 | pattern2`).
84    pub patterns: Vec<XsltPattern>,
85}
86
87// ═══════════════════════════════════════════════════════════════════════════════
88// C ABI Opaque Types
89// ═══════════════════════════════════════════════════════════════════════════════
90
91/// Opaque pattern structure.
92///
93/// In the C ABI this is a `typedef struct _xsltPattern xsltPattern`.
94/// The actual data is stored in the internal `CompiledPattern` and accessed
95/// via pointer casts in the implementation functions.
96#[repr(C)]
97pub struct _xsltPattern {
98    _unused: [u8; 0],
99}
100
101/// Opaque pattern step structure.
102#[repr(C)]
103pub struct _xsltPatternStep {
104    _unused: [u8; 0],
105}
106
107// ═══════════════════════════════════════════════════════════════════════════════
108// Pattern Compilation
109// ═══════════════════════════════════════════════════════════════════════════════
110
111/// Compile an XSLT pattern string into a compiled pattern.
112///
113/// Parses the pattern string using the XPath 1.0 parser, then decomposes
114/// it into an internal representation suitable for fast node matching.
115///
116/// # Parameters
117///
118/// * `pattern` — The pattern string (UTF-8, null-terminated `xmlChar*`).
119/// * `doc`     — The document (used for namespace resolution; may be null).
120///
121/// # Returns
122///
123/// A pointer to a compiled `_xsltPattern`, or null on failure.
124///
125/// # Safety
126///
127/// `pattern` must be a valid null-terminated `xmlChar*` or null.
128/// `doc` must be a valid `_xmlDoc*` or null.
129pub unsafe fn xsltCompilePattern(pattern: *const xmlChar, _doc: *mut _xmlDoc) -> *mut _xsltPattern {
130    if pattern.is_null() {
131        return ptr::null_mut();
132    }
133
134    let pattern_str = xmlstr_to_string(pattern);
135    if pattern_str.is_empty() {
136        return ptr::null_mut();
137    }
138
139    let compiled = match compile_pattern_string(&pattern_str) {
140        Some(cp) => cp,
141        None => return ptr::null_mut(),
142    };
143
144    // Allocate and store the compiled pattern
145    let layout = std::alloc::Layout::new::<CompiledPattern>();
146    let ptr = std::alloc::alloc(layout) as *mut CompiledPattern;
147    if ptr.is_null() {
148        return ptr::null_mut();
149    }
150    ptr::write(ptr, compiled);
151    ptr as *mut _xsltPattern
152}
153
154/// Internal: compile a pattern string into a `CompiledPattern`.
155fn compile_pattern_string(pattern_str: &str) -> Option<CompiledPattern> {
156    // Parse the pattern as an XPath expression
157    let expr = parse_xpath(pattern_str).ok()?;
158
159    // Decompose the expression into pattern branches
160    let patterns = decompose_pattern(&expr, pattern_str)?;
161
162    Some(CompiledPattern { patterns })
163}
164
165/// Decompose an XPath expression into a list of pattern branches.
166///
167/// A union pattern `a | b` becomes two branches. Each branch is converted
168/// into a sequence of steps for matching.
169fn decompose_pattern(expr: &Expr, original: &str) -> Option<Vec<XsltPattern>> {
170    match expr {
171        // Union: pattern1 | pattern2 — recurse on both sides
172        Expr::Union(left, right) => {
173            let mut patterns = decompose_pattern(left, original)?;
174            let right_patterns = decompose_pattern(right, original)?;
175            patterns.extend(right_patterns);
176            Some(patterns)
177        }
178        // Single expression — convert to a pattern
179        _ => {
180            let pattern = expr_to_pattern(expr, original)?;
181            Some(vec![pattern])
182        }
183    }
184}
185
186/// Convert a single (non-union) XPath expression into a `XsltPattern`.
187fn expr_to_pattern(expr: &Expr, original: &str) -> Option<XsltPattern> {
188    let (steps, is_absolute) = collect_steps(expr)?;
189
190    Some(XsltPattern {
191        steps,
192        is_absolute,
193        original: original.to_string(),
194        expr: expr.clone(),
195    })
196}
197
198/// Collect the steps from an XPath expression in reverse order.
199///
200/// Returns `(steps, is_absolute)` where steps are ordered from innermost
201/// (node being matched) to outermost (root), making matching efficient.
202fn collect_steps(expr: &Expr) -> Option<(Vec<PatternStepEntry>, bool)> {
203    match expr {
204        // "/" — a bare Self_/node() step represents the document root
205        // node pattern. It matches only the document node itself.
206        Expr::Step(step)
207            if step.axis == Axis::Self_
208                && step.node_test == NodeTest::Node
209                && step.predicates.is_empty() =>
210        {
211            Some((vec![], true))
212        }
213        Expr::Step(step) => {
214            let entry = PatternStepEntry::Step(XsltPatternStep {
215                axis: step.axis,
216                node_test: step.node_test.clone(),
217                predicates: step.predicates.clone(),
218            });
219            Some((vec![entry], false))
220        }
221        Expr::AbsolutePath(inner) => {
222            let (steps, _) = collect_steps(inner)?;
223            Some((steps, true))
224        }
225        Expr::RelativePath(left, right) => {
226            // Steps are collected right-to-left: the rightmost step is the
227            // node being tested, left steps are ancestry constraints.
228            let (mut right_steps, _) = collect_steps(right)?;
229            let (left_steps, left_absolute) = collect_steps(left)?;
230            right_steps.extend(left_steps);
231            Some((right_steps, left_absolute))
232        }
233        // Handle `//` — the parser represents it as RelativePath with
234        // a DescendantOrSelf step in the middle
235        Expr::Filter(_expr, _predicates) => {
236            // Filter expressions like `id('foo')/bar` — we match the filter
237            // as a step
238            // For now, treat as a single step with a wildcard node test
239            let entry = PatternStepEntry::Step(XsltPatternStep {
240                axis: Axis::Self_,
241                node_test: NodeTest::Node,
242                predicates: vec![],
243            });
244            Some((vec![entry], false))
245        }
246        // Bare node-test function calls: node(), text(), comment(),
247        // processing-instruction(). In XPath 1.0 these are NOT functions;
248        // they are node tests forming a step on the child axis. The XPath
249        // parser represents top-level `node()` as a FunctionCall, so we
250        // translate it back into a step here.
251        Expr::FunctionCall { name, args } => {
252            let node_test = match (name.as_str(), args.len()) {
253                ("node", 0) => Some(NodeTest::Node),
254                ("text", 0) => Some(NodeTest::Text),
255                ("comment", 0) => Some(NodeTest::Comment),
256                ("processing-instruction", 0) => Some(NodeTest::ProcessingInstruction(None)),
257                ("processing-instruction", 1) => match &args[0] {
258                    Expr::StringLiteral(s) => {
259                        Some(NodeTest::ProcessingInstruction(Some(s.clone())))
260                    }
261                    _ => None,
262                },
263                _ => None,
264            };
265            match node_test {
266                Some(nt) => {
267                    let entry = PatternStepEntry::Step(XsltPatternStep {
268                        axis: Axis::Child,
269                        node_test: nt,
270                        predicates: vec![],
271                    });
272                    Some((vec![entry], false))
273                }
274                // id() and key() are handled as filter-like patterns.
275                None if name == "id" || name == "key" => {
276                    let entry = PatternStepEntry::Step(XsltPatternStep {
277                        axis: Axis::Self_,
278                        node_test: NodeTest::Node,
279                        predicates: vec![],
280                    });
281                    Some((vec![entry], false))
282                }
283                None => None,
284            }
285        }
286        _ => {
287            // Literals, function calls without path — not valid as patterns
288            // unless they're id() or key() which are handled specially
289            None
290        }
291    }
292}
293
294// ═══════════════════════════════════════════════════════════════════════════════
295// Pattern Deallocation
296// ═══════════════════════════════════════════════════════════════════════════════
297
298/// Free a compiled pattern.
299///
300/// # Safety
301///
302/// `pattern` must have been returned by `xsltCompilePattern` and not already freed.
303pub unsafe fn xsltFreePattern(pattern: *mut _xsltPattern) {
304    if pattern.is_null() {
305        return;
306    }
307    let ptr = pattern as *mut CompiledPattern;
308    // Drop the compiled pattern
309    ptr::drop_in_place(ptr);
310    let layout = std::alloc::Layout::new::<CompiledPattern>();
311    std::alloc::dealloc(ptr as *mut u8, layout);
312}
313
314// ═══════════════════════════════════════════════════════════════════════════════
315// Pattern Matching
316// ═══════════════════════════════════════════════════════════════════════════════
317
318/// Test whether a node matches a compiled pattern.
319///
320/// # Parameters
321///
322/// * `ctxt`    — The transform context (provides XPath context for predicates).
323/// * `pattern` — The compiled pattern.
324/// * `node`    — The node to test.
325///
326/// # Returns
327///
328/// 1 if the node matches, 0 otherwise.
329///
330/// # Safety
331///
332/// All pointers must be valid (or null — null pointers return 0).
333pub unsafe fn xsltTestPattern(
334    ctxt: *mut _xsltTransformContext,
335    pattern: *mut _xsltPattern,
336    node: *mut _xmlNode,
337) -> c_int {
338    if pattern.is_null() || node.is_null() {
339        return 0;
340    }
341
342    let compiled = &*(pattern as *const CompiledPattern);
343    let xpath_ctxt = if !ctxt.is_null() {
344        (*ctxt).xpathCtxt
345    } else {
346        ptr::null_mut()
347    };
348
349    for sub_pattern in &compiled.patterns {
350        if match_sub_pattern(sub_pattern, node, xpath_ctxt) {
351            return 1;
352        }
353    }
354
355    0
356}
357
358/// Test whether a node matches a compiled pattern tree.
359///
360/// In libxslt, the `match` attribute is compiled into a tree of `_xmlNode`
361/// elements during stylesheet compilation. This function walks that tree
362/// to determine whether `node` matches the pattern.
363///
364/// The pattern tree structure uses element nodes where:
365/// - The node `name` encodes the step type (element name, "*", "node()", etc.)
366/// - Children represent path steps (inner to outer)
367/// - Siblings at the root level represent union alternatives
368///
369/// # Parameters
370///
371/// * `node`         — The document node to test.
372/// * `pattern_node` — The compiled pattern tree root (from `_xsltTemplate.r#match`).
373///
374/// # Returns
375///
376/// `true` if the node matches, `false` otherwise.
377///
378/// # Safety
379///
380/// Both pointers must be valid (or null — null pointers return false).
381pub unsafe fn xsltTestMatchPattern(node: *mut _xmlNode, pattern_node: *mut _xmlNode) -> bool {
382    if node.is_null() || pattern_node.is_null() {
383        return false;
384    }
385
386    // Walk the pattern tree. The pattern tree has the following structure:
387    // - Root node: represents the outermost step (or union)
388    // - For union patterns: the root has sibling children
389    // - For path patterns: children represent nested steps
390    //
391    // The node's `name` encodes what kind of test this step performs:
392    // - A QName: match element with that name
393    // - "*": match any element
394    // - "node()": match any node
395    // - "text()": match text nodes
396    // - "comment()": match comment nodes
397    // - "processing-instruction()": match PI nodes
398    // - "@name": match attribute
399    // - "ns:*": match namespace wildcard
400    // - "|": union operator
401    // - "/": path separator
402
403    match_pattern_tree(pattern_node, node)
404}
405
406/// Walk the pattern tree and test if the node matches.
407unsafe fn match_pattern_tree(pattern_node: *mut _xmlNode, node: *mut _xmlNode) -> bool {
408    if pattern_node.is_null() || node.is_null() {
409        return false;
410    }
411
412    let node_ref = &*pattern_node;
413    let name = xmlstr_to_string(node_ref.name);
414
415    match name.as_str() {
416        // Union: any child matches
417        "|" => {
418            let mut child = node_ref.children;
419            while !child.is_null() {
420                if match_pattern_tree(child, node) {
421                    return true;
422                }
423                child = (*child).next;
424            }
425            false
426        }
427        // Path separator: match child step, then parent step
428        "/" => {
429            // For a path like foo/bar:
430            // The root node is "/" with children [bar, foo] (inner first)
431            // We match the first child against node, then the second against node's parent
432            let steps = collect_children(pattern_node);
433            if steps.is_empty() {
434                return false;
435            }
436            match_pattern_path(&steps, node)
437        }
438        _ => {
439            // Leaf node: check if this step matches the node
440            match_pattern_step(pattern_node, node)
441        }
442    }
443}
444
445/// Collect all children of a pattern node into a vector.
446unsafe fn collect_children(pattern_node: *mut _xmlNode) -> Vec<*mut _xmlNode> {
447    let mut children = Vec::new();
448    if pattern_node.is_null() {
449        return children;
450    }
451    let mut child = (*pattern_node).children;
452    while !child.is_null() {
453        children.push(child);
454        child = (*child).next;
455    }
456    children
457}
458
459/// Match a path (sequence of pattern steps) against a node.
460///
461/// Steps are ordered inner-first (the last step is the outermost).
462unsafe fn match_pattern_path(steps: &[*mut _xmlNode], node: *mut _xmlNode) -> bool {
463    if steps.is_empty() {
464        return false;
465    }
466
467    let mut current = node;
468
469    for (i, &step) in steps.iter().enumerate() {
470        if current.is_null() {
471            return false;
472        }
473
474        if !match_pattern_step(step, current) {
475            return false;
476        }
477
478        // Move to parent for the next step (if any)
479        if i < steps.len() - 1 {
480            current = (*current).parent;
481        }
482    }
483
484    true
485}
486
487/// Match a single pattern step node against a document node.
488unsafe fn match_pattern_step(pattern_node: *mut _xmlNode, node: *mut _xmlNode) -> bool {
489    if pattern_node.is_null() || node.is_null() {
490        return false;
491    }
492
493    let pn = &*pattern_node;
494    let nn = &*node;
495    let step_name = xmlstr_to_string(pn.name);
496    let node_name = xmlstr_to_string(nn.name);
497    let node_type = nn.type_;
498
499    match step_name.as_str() {
500        // Wildcard: match any element (or attribute if pattern is attribute-type)
501        "*" => {
502            if pn.type_ == 2 {
503                // Attribute wildcard
504                node_type == 2
505            } else {
506                // Element wildcard
507                node_type == 1
508            }
509        }
510        // node() — match any node type
511        "node()" => true,
512        // text() — match text nodes
513        "text()" => node_type == 3 || node_type == 4,
514        // comment() — match comment nodes
515        "comment()" => node_type == 8,
516        // processing-instruction() — match PI nodes
517        "processing-instruction()" => node_type == 7,
518        // Attribute test (@attr) — the step name starts with "@"
519        s if s.starts_with('@') => {
520            let attr_name = &s[1..];
521            node_type == 2 && node_name == attr_name
522        }
523        // Namespace wildcard (prefix:*)
524        s if s.ends_with(":*") => {
525            if node_type != 1 {
526                return false;
527            }
528            let prefix = &s[..s.len() - 2];
529            if let Some(ns) = nn.ns.as_ref() {
530                let ns_prefix = xmlstr_to_string(ns.prefix);
531                ns_prefix == prefix
532            } else {
533                prefix.is_empty()
534            }
535        }
536        // Namespace-qualified name (ns:local)
537        s if s.contains(':') && !s.starts_with('@') && !s.ends_with(":*") => {
538            if node_type != 1 {
539                return false;
540            }
541            let parts: Vec<&str> = s.splitn(2, ':').collect();
542            if parts.len() != 2 {
543                return false;
544            }
545            let prefix = parts[0];
546            let local = parts[1];
547            if node_name != local {
548                return false;
549            }
550            if let Some(ns) = nn.ns.as_ref() {
551                let ns_prefix = xmlstr_to_string(ns.prefix);
552                ns_prefix == prefix
553            } else {
554                prefix.is_empty()
555            }
556        }
557        // Simple name test: match element/attribute by name
558        _ => {
559            if node_type == 1 || node_type == 2 {
560                node_name == step_name
561            } else {
562                false
563            }
564        }
565    }
566}
567
568/// Check if a node matches a single (non-union) sub-pattern.
569unsafe fn match_sub_pattern(
570    pattern: &XsltPattern,
571    node: *mut _xmlNode,
572    xpath_ctxt: *mut _xmlXPathContext,
573) -> bool {
574    // The steps are stored in reverse order (innermost first).
575    // Walk them from the node upward through the tree.
576    if pattern.steps.is_empty() {
577        // Empty steps with is_absolute means the pattern is "/", which
578        // matches only the document root node.
579        return pattern.is_absolute && is_document_node(node);
580    }
581
582    // Start with the first step (the one closest to the matched node)
583    let mut current_node = node;
584
585    for (i, entry) in pattern.steps.iter().enumerate() {
586        match entry {
587            PatternStepEntry::Step(step) => {
588                if !match_step(step, current_node, xpath_ctxt, i == 0) {
589                    return false;
590                }
591                // For the first step, we stay on the current node (Self_ axis)
592                // or traverse to children/parent depending on the axis.
593                // For subsequent steps, we need to move upward.
594                if i > 0 {
595                    // Move to parent for the next step
596                    current_node = (*current_node).parent;
597                    if current_node.is_null() {
598                        return false;
599                    }
600                }
601            }
602            PatternStepEntry::DescendantOrSelf => {
603                // `//` matches descendant-or-self::node()
604                // This means any ancestor path is valid.
605                // Since steps are in reverse order, this separates
606                // the inner steps (already matched) from outer steps.
607                // We need to find an ancestor that matches the remaining steps.
608                let remaining: Vec<_> = pattern.steps[i + 1..]
609                    .iter()
610                    .filter_map(|e| {
611                        if let PatternStepEntry::Step(s) = e {
612                            Some(s.clone())
613                        } else {
614                            None
615                        }
616                    })
617                    .collect();
618
619                if remaining.is_empty() {
620                    return true;
621                }
622
623                // Try each ancestor
624                let mut ancestor = current_node;
625                loop {
626                    ancestor = (*ancestor).parent;
627                    if ancestor.is_null() {
628                        return false;
629                    }
630                    if match_steps_sequence(&remaining, ancestor, xpath_ctxt) {
631                        return true;
632                    }
633                }
634            }
635        }
636    }
637
638    // If the pattern is absolute, the final ancestor must be the document root
639    if pattern.is_absolute {
640        // After walking up all steps, current_node should be the document node
641        if current_node.is_null() {
642            return true;
643        }
644        // Walk up to root
645        let mut n = node;
646        loop {
647            let parent = (*n).parent;
648            if parent.is_null() {
649                break;
650            }
651            n = parent;
652        }
653        // n is now the root — check if it's the document node
654        return (*n).type_ == 9 || (*n).type_ == 13; // XML_DOCUMENT_NODE or XML_HTML_DOCUMENT_NODE
655    }
656
657    true
658}
659
660/// Match a sequence of steps against a node, starting from the innermost step.
661unsafe fn match_steps_sequence(
662    steps: &[XsltPatternStep],
663    node: *mut _xmlNode,
664    xpath_ctxt: *mut _xmlXPathContext,
665) -> bool {
666    let mut current = node;
667    for (i, step) in steps.iter().enumerate() {
668        if !match_step(step, current, xpath_ctxt, i == 0) {
669            return false;
670        }
671        if i < steps.len() - 1 {
672            current = (*current).parent;
673            if current.is_null() {
674                return false;
675            }
676        }
677    }
678    true
679}
680
681/// Check if a node is a document node (XML_DOCUMENT_NODE or XML_HTML_DOCUMENT_NODE).
682unsafe fn is_document_node(node: *mut _xmlNode) -> bool {
683    if node.is_null() {
684        return false;
685    }
686    (*node).type_ == 9 || (*node).type_ == 13
687}
688
689/// Check if a single step matches a node.
690///
691/// A step `axis::node-test[predicates]` matches if:
692/// 1. The axis relationship holds between the context and the node.
693/// 2. The node test matches the node.
694/// 3. All predicates evaluate to true.
695unsafe fn match_step(
696    step: &XsltPatternStep,
697    node: *mut _xmlNode,
698    xpath_ctxt: *mut _xmlXPathContext,
699    _is_first: bool,
700) -> bool {
701    if node.is_null() {
702        return false;
703    }
704
705    let node_ref = &*node;
706    let node_type = node_ref.type_;
707
708    // Step 1: Check the axis
709    match step.axis {
710        Axis::Attribute => {
711            // Must be an attribute node
712            if node_type != 2 {
713                // XML_ATTRIBUTE_NODE
714                return false;
715            }
716        }
717        Axis::Child | Axis::Self_ => {
718            // Child and Self axes match elements, text, comments, PIs
719            // (all non-document, non-attribute, non-namespace nodes)
720            if node_type == 2 || node_type == 9 || node_type == 13 {
721                // Skip attributes and document nodes for child:: tests
722                if step.axis == Axis::Child
723                    && node_type != 1
724                    && node_type != 3
725                    && node_type != 4
726                    && node_type != 7
727                    && node_type != 8
728                {
729                    return false;
730                }
731            }
732        }
733        _ => {
734            // Other axes are not typically used in patterns
735            // For completeness, accept the node
736        }
737    }
738
739    // Step 2: Check the node test
740    if !match_node_test(node, &step.node_test) {
741        return false;
742    }
743
744    // Step 3: Evaluate predicates (if any)
745    if !step.predicates.is_empty() {
746        if xpath_ctxt.is_null() {
747            // Without an XPath context, we can't evaluate predicates
748            // For now, skip predicates (libxslt would also need a context)
749            return true;
750        }
751
752        if !evaluate_predicates(node, &step.predicates, xpath_ctxt) {
753            return false;
754        }
755    }
756
757    true
758}
759
760/// Check if a node matches a node test.
761unsafe fn match_node_test(node: *mut _xmlNode, node_test: &NodeTest) -> bool {
762    if node.is_null() {
763        return false;
764    }
765
766    let node_ref = &*node;
767    let node_type = node_ref.type_;
768
769    match node_test {
770        NodeTest::Node => {
771            // Matches any node
772            true
773        }
774        NodeTest::Text => {
775            // text() — text nodes (type 3) or CDATA sections (type 4)
776            node_type == 3 || node_type == 4
777        }
778        NodeTest::Comment => {
779            // comment() — comment nodes (type 8)
780            node_type == 8
781        }
782        NodeTest::ProcessingInstruction(target) => {
783            // processing-instruction() or processing-instruction("target")
784            if node_type != 7 {
785                // XML_PI_NODE
786                return false;
787            }
788            if let Some(target) = target {
789                let name = xmlstr_to_string(node_ref.name);
790                name == *target
791            } else {
792                true
793            }
794        }
795        NodeTest::NameTest(name_test) => match_name_test(node, name_test),
796        NodeTest::Wildcard => {
797            // * — matches any element node
798            node_type == 1
799        }
800        NodeTest::NsWildcard(prefix) => {
801            // prefix:* — matches any element in that namespace
802            if node_type != 1 {
803                return false;
804            }
805            if let Some(ns) = node_ref.ns.as_ref() {
806                let ns_prefix = xmlstr_to_string(ns.prefix);
807                ns_prefix == *prefix
808            } else {
809                prefix.is_empty()
810            }
811        }
812    }
813}
814
815/// Check if a node matches a name test.
816unsafe fn match_name_test(node: *mut _xmlNode, name_test: &NameTest) -> bool {
817    if node.is_null() {
818        return false;
819    }
820
821    let node_ref = &*node;
822
823    match name_test {
824        NameTest::Any => {
825            // * — matches any element or attribute
826            node_ref.type_ == 1 || node_ref.type_ == 2
827        }
828        NameTest::LocalName(local) => {
829            let name = xmlstr_to_string(node_ref.name);
830            name == *local
831        }
832        NameTest::QName { prefix, local } => {
833            let name = xmlstr_to_string(node_ref.name);
834            if name != *local {
835                return false;
836            }
837            // Check namespace prefix
838            if let Some(ns) = node_ref.ns.as_ref() {
839                let ns_prefix = xmlstr_to_string(ns.prefix);
840                ns_prefix == *prefix
841            } else {
842                prefix.is_empty()
843            }
844        }
845    }
846}
847
848/// Evaluate predicates for a node match.
849///
850/// Uses the XPath evaluation engine to check if all predicates hold.
851unsafe fn evaluate_predicates(
852    node: *mut _xmlNode,
853    predicates: &[Expr],
854    xpath_ctxt: *mut _xmlXPathContext,
855) -> bool {
856    if xpath_ctxt.is_null() {
857        return true; // Can't evaluate, assume match
858    }
859
860    // Set up a temporary XPath context for predicate evaluation
861    let ctxt = &mut *xpath_ctxt;
862
863    // Save context state
864    let saved_node = ctxt.node;
865
866    // Set the current node as context
867    ctxt.node = node;
868
869    let mut result = true;
870
871    for predicate in predicates {
872        // Evaluate the predicate expression
873        // Get the document from the context or the node
874        let doc = if !ctxt.doc.is_null() {
875            ctxt.doc
876        } else if !node.is_null() {
877            (*node).doc
878        } else {
879            ptr::null_mut()
880        };
881        let mut xpath_ctx = crate::xml::xpath::context::XPathContext::new(doc);
882
883        // Copy relevant state from the C ABI context
884        if !saved_node.is_null() {
885            xpath_ctx.set_context_node(saved_node);
886        }
887
888        // Copy namespace mappings
889        if !ctxt.namespaces.is_null() && ctxt.nsNr > 0 {
890            let ns_slice = std::slice::from_raw_parts(ctxt.namespaces, ctxt.nsNr as usize);
891            for ns_ptr in ns_slice {
892                if !ns_ptr.is_null() {
893                    let ns = &**ns_ptr;
894                    let prefix = xmlstr_to_string(ns.prefix);
895                    let href = xmlstr_to_string(ns.href);
896                    xpath_ctx.register_namespace(&prefix, &href);
897                }
898            }
899        }
900
901        // Register the id() and key() extension functions
902        register_pattern_functions(&mut xpath_ctx);
903
904        let pred_result = crate::xml::xpath::eval::eval(&mut xpath_ctx, predicate);
905
906        match pred_result {
907            Ok(val) => {
908                // Predicate semantics: number n matches if n == context position,
909                // otherwise boolean conversion
910                let matches = match val {
911                    XPathValue::Number(n) => {
912                        // Number predicate: match if n == 1 (first node)
913                        // For pattern predicates, position is always 1
914                        (n - 1.0).abs() < f64::EPSILON
915                    }
916                    _ => val.as_boolean(),
917                };
918                if !matches {
919                    result = false;
920                    break;
921                }
922            }
923            Err(_) => {
924                result = false;
925                break;
926            }
927        }
928    }
929
930    // Restore context
931    ctxt.node = saved_node;
932
933    result
934}
935
936/// Register XSLT-specific functions needed for pattern evaluation (id(), key()).
937fn register_pattern_functions(ctx: &mut crate::xml::xpath::context::XPathContext) {
938    // Register id() function
939    ctx.register_function("id", |_ctx, _args| {
940        // Simple id() implementation: returns empty node-set for now
941        // A full implementation would look up IDs in the document's DTD
942        Ok(XPathValue::NodeSet(NodeSet::new()))
943    });
944
945    // Register key() function
946    ctx.register_function("key", |_ctx, _args| {
947        // Simple key() implementation: returns empty node-set for now
948        // A full implementation would look up keys in the stylesheet's key tables
949        Ok(XPathValue::NodeSet(NodeSet::new()))
950    });
951}
952
953// ═══════════════════════════════════════════════════════════════════════════════
954// Default Priority Computation
955// ═══════════════════════════════════════════════════════════════════════════════
956
957/// Compute default priority for a match pattern.
958///
959/// XSLT 1.0 §5.5:
960/// - 0.0 for simple name tests (child::para, para)
961/// - -0.25 for node() test
962/// - -0.5 for any name test (*) or namespace test (ns:*)
963/// - +0.5 for attribute axis (@attr)
964/// - +0.0 for other cases (compound patterns, id(), key())
965///
966/// # Parameters
967///
968/// * `pattern` — The pattern string (UTF-8, null-terminated `xmlChar*`).
969///
970/// # Returns
971///
972/// The default priority as a f64.
973///
974/// # Safety
975///
976/// `pattern` must be a valid null-terminated `xmlChar*` or null.
977pub unsafe fn xsltDefaultPriority(pattern: *const xmlChar) -> f64 {
978    if pattern.is_null() {
979        return 0.5;
980    }
981
982    let pattern_str = xmlstr_to_string(pattern);
983    if pattern_str.is_empty() {
984        return 0.5;
985    }
986
987    compute_default_priority(&pattern_str)
988}
989
990/// Internal: compute default priority from a pattern string.
991fn compute_default_priority(pattern_str: &str) -> f64 {
992    // Parse the pattern
993    let expr = match parse_xpath(pattern_str) {
994        Ok(e) => e,
995        Err(_) => return 0.5, // Default for unparseable patterns
996    };
997
998    compute_expr_priority(&expr)
999}
1000
1001/// Compute the default priority of an expression.
1002fn compute_expr_priority(expr: &Expr) -> f64 {
1003    match expr {
1004        // Union patterns: use the highest priority of any branch
1005        Expr::Union(left, right) => {
1006            let left_p = compute_expr_priority(left);
1007            let right_p = compute_expr_priority(right);
1008            left_p.max(right_p)
1009        }
1010
1011        // Absolute path: analyze the inner expression
1012        Expr::AbsolutePath(inner) => compute_expr_priority(inner),
1013
1014        // Relative path: priority is based on the final step (rightmost)
1015        Expr::RelativePath(_, right) => compute_expr_priority(right),
1016
1017        // Single step: determine priority from the node test and axis
1018        Expr::Step(step) => compute_step_priority(step),
1019
1020        // Filter expression: priority based on the primary expression
1021        Expr::Filter(primary, _) => compute_expr_priority(primary),
1022
1023        // id() and key() functions: priority 0.0
1024        Expr::FunctionCall { name, .. } => {
1025            if name == "id" || name == "key" {
1026                0.0
1027            } else {
1028                // Bare node tests (node(), text(), comment(),
1029                // processing-instruction()) parse as function calls at the
1030                // top level; translate them to their step priorities.
1031                match name.as_str() {
1032                    "node" => -0.25,
1033                    "text" | "comment" | "processing-instruction" => 0.0,
1034                    _ => 0.5,
1035                }
1036            }
1037        }
1038
1039        // Other expressions (literals, variables, etc.) — not typical patterns
1040        _ => 0.5,
1041    }
1042}
1043
1044/// Compute the default priority of a single step.
1045fn compute_step_priority(step: &Step) -> f64 {
1046    match &step.node_test {
1047        // node() test: -0.25
1048        NodeTest::Node => -0.25,
1049
1050        // text(), comment(), processing-instruction(): 0.0
1051        NodeTest::Text | NodeTest::Comment | NodeTest::ProcessingInstruction(_) => 0.0,
1052
1053        // Name test: 0.0 for a specific name on the child axis;
1054        // 0.5 on the attribute axis (@QName per XSLT 1.0 §5.5).
1055        NodeTest::NameTest(name_test) => match name_test {
1056            NameTest::LocalName(_) | NameTest::QName { .. } => {
1057                if step.axis == Axis::Attribute {
1058                    0.5
1059                } else {
1060                    0.0
1061                }
1062            }
1063            NameTest::Any => {
1064                // * in element context: -0.5
1065                // * in attribute context: +0.5
1066                if step.axis == Axis::Attribute {
1067                    0.5
1068                } else {
1069                    -0.5
1070                }
1071            }
1072        },
1073
1074        // * (Wildcard): -0.5 in element context, +0.5 in attribute context
1075        NodeTest::Wildcard => {
1076            if step.axis == Axis::Attribute {
1077                0.5
1078            } else {
1079                -0.5
1080            }
1081        }
1082
1083        // prefix:* namespace wildcard: -0.5
1084        NodeTest::NsWildcard(_) => {
1085            if step.axis == Axis::Attribute {
1086                0.5
1087            } else {
1088                -0.5
1089            }
1090        }
1091    }
1092}
1093
1094// ═══════════════════════════════════════════════════════════════════════════════
1095// Convenience Functions
1096// ═══════════════════════════════════════════════════════════════════════════════
1097
1098/// Check if a pattern string matches a single step (simple name test).
1099///
1100/// Returns true if the pattern is a simple name test like `para` or `foo:bar`,
1101/// without path separators, predicates, or union operators.
1102pub fn is_simple_name_pattern(pattern: &str) -> bool {
1103    let expr = match parse_xpath(pattern) {
1104        Ok(e) => e,
1105        Err(_) => return false,
1106    };
1107
1108    matches!(&expr, Expr::Step(Step {
1109        axis: Axis::Child,
1110        node_test: NodeTest::NameTest(name_test),
1111        predicates,
1112    }) if predicates.is_empty() && !matches!(name_test, NameTest::Any))
1113}
1114
1115/// Check if a pattern is a union pattern (contains `|`).
1116pub fn is_union_pattern(pattern: &str) -> bool {
1117    let expr = match parse_xpath(pattern) {
1118        Ok(e) => e,
1119        Err(_) => return false,
1120    };
1121
1122    matches!(&expr, Expr::Union(_, _))
1123}
1124
1125/// Get the names matched by a simple name-test pattern.
1126///
1127/// For a simple pattern like `para` or `foo | bar`, returns the list of
1128/// matched element names. Returns an empty vec for complex patterns.
1129pub fn get_pattern_matched_names(pattern: &str) -> Vec<String> {
1130    let expr = match parse_xpath(pattern) {
1131        Ok(e) => e,
1132        Err(_) => return vec![],
1133    };
1134
1135    let mut names = Vec::new();
1136    collect_matched_names(&expr, &mut names);
1137    names
1138}
1139
1140fn collect_matched_names(expr: &Expr, names: &mut Vec<String>) {
1141    match expr {
1142        Expr::Union(left, right) => {
1143            collect_matched_names(left, names);
1144            collect_matched_names(right, names);
1145        }
1146        Expr::Step(Step {
1147            node_test: NodeTest::NameTest(name_test),
1148            ..
1149        }) => match name_test {
1150            NameTest::LocalName(local) => names.push(local.clone()),
1151            NameTest::QName { prefix, local } => names.push(format!("{}:{}", prefix, local)),
1152            NameTest::Any => names.push("*".to_string()),
1153        },
1154        Expr::Step(Step {
1155            node_test: NodeTest::Wildcard,
1156            ..
1157        }) => {
1158            names.push("*".to_string());
1159        }
1160        Expr::Step(Step {
1161            node_test: NodeTest::NsWildcard(prefix),
1162            ..
1163        }) => {
1164            names.push(format!("{}:*", prefix));
1165        }
1166        _ => {}
1167    }
1168}
1169
1170// ═══════════════════════════════════════════════════════════════════════════════
1171// Tests
1172// ═══════════════════════════════════════════════════════════════════════════════
1173
1174#[cfg(test)]
1175mod tests {
1176    use super::*;
1177
1178    // ── Priority Tests ────────────────────────────────────────────────────
1179
1180    #[test]
1181    fn test_default_priority_name_test() {
1182        // Simple name test "para" → 0.0
1183        let priority = compute_default_priority("para");
1184        assert!(
1185            (priority - 0.0).abs() < f64::EPSILON,
1186            "Expected 0.0 for name test, got {}",
1187            priority
1188        );
1189    }
1190
1191    #[test]
1192    fn test_default_priority_qname() {
1193        // Qualified name "xslt:template" → 0.0
1194        let priority = compute_default_priority("xslt:template");
1195        assert!(
1196            (priority - 0.0).abs() < f64::EPSILON,
1197            "Expected 0.0 for QName, got {}",
1198            priority
1199        );
1200    }
1201
1202    #[test]
1203    fn test_default_priority_node_test() {
1204        // node() test → -0.25
1205        let priority = compute_default_priority("node()");
1206        assert!(
1207            (priority - (-0.25)).abs() < f64::EPSILON,
1208            "Expected -0.25 for node(), got {}",
1209            priority
1210        );
1211    }
1212
1213    #[test]
1214    fn test_default_priority_text_test() {
1215        // text() test → 0.0
1216        let priority = compute_default_priority("text()");
1217        assert!(
1218            (priority - 0.0).abs() < f64::EPSILON,
1219            "Expected 0.0 for text(), got {}",
1220            priority
1221        );
1222    }
1223
1224    #[test]
1225    fn test_default_priority_comment_test() {
1226        // comment() test → 0.0
1227        let priority = compute_default_priority("comment()");
1228        assert!(
1229            (priority - 0.0).abs() < f64::EPSILON,
1230            "Expected 0.0 for comment(), got {}",
1231            priority
1232        );
1233    }
1234
1235    #[test]
1236    fn test_default_priority_processing_instruction() {
1237        // processing-instruction() test → 0.0
1238        let priority = compute_default_priority("processing-instruction()");
1239        assert!(
1240            (priority - 0.0).abs() < f64::EPSILON,
1241            "Expected 0.0 for processing-instruction(), got {}",
1242            priority
1243        );
1244    }
1245
1246    #[test]
1247    fn test_default_priority_wildcard() {
1248        // * wildcard → -0.5
1249        let priority = compute_default_priority("*");
1250        assert!(
1251            (priority - (-0.5)).abs() < f64::EPSILON,
1252            "Expected -0.5 for *, got {}",
1253            priority
1254        );
1255    }
1256
1257    #[test]
1258    fn test_default_priority_ns_wildcard() {
1259        // ns:* wildcard → -0.5
1260        let priority = compute_default_priority("ns:*");
1261        assert!(
1262            (priority - (-0.5)).abs() < f64::EPSILON,
1263            "Expected -0.5 for ns:*, got {}",
1264            priority
1265        );
1266    }
1267
1268    #[test]
1269    fn test_default_priority_attribute() {
1270        // @attr → +0.5
1271        let priority = compute_default_priority("@attr");
1272        assert!(
1273            (priority - 0.5).abs() < f64::EPSILON,
1274            "Expected 0.5 for @attr, got {}",
1275            priority
1276        );
1277    }
1278
1279    #[test]
1280    fn test_default_priority_attribute_wildcard() {
1281        // @* → +0.5
1282        let priority = compute_default_priority("@*");
1283        assert!(
1284            (priority - 0.5).abs() < f64::EPSILON,
1285            "Expected 0.5 for @*, got {}",
1286            priority
1287        );
1288    }
1289
1290    #[test]
1291    fn test_default_priority_union() {
1292        // Union "para | *" → max(0.0, -0.5) = 0.0
1293        let priority = compute_default_priority("para | *");
1294        assert!(
1295            (priority - 0.0).abs() < f64::EPSILON,
1296            "Expected 0.0 for union, got {}",
1297            priority
1298        );
1299    }
1300
1301    #[test]
1302    fn test_default_priority_compound_path() {
1303        // Path "foo/bar" → priority of last step "bar" = 0.0
1304        let priority = compute_default_priority("foo/bar");
1305        assert!(
1306            (priority - 0.0).abs() < f64::EPSILON,
1307            "Expected 0.0 for foo/bar, got {}",
1308            priority
1309        );
1310    }
1311
1312    #[test]
1313    fn test_default_priority_empty() {
1314        // Empty pattern → 0.5
1315        let priority = compute_default_priority("");
1316        assert!(
1317            (priority - 0.5).abs() < f64::EPSILON,
1318            "Expected 0.5 for empty pattern, got {}",
1319            priority
1320        );
1321    }
1322
1323    // ── Pattern Matching Tests ────────────────────────────────────────────
1324
1325    /// Create a minimal element node for testing.
1326    unsafe fn create_test_node(name: &str, type_: c_int) -> *mut _xmlNode {
1327        let layout = std::alloc::Layout::new::<_xmlNode>();
1328        let ptr = std::alloc::alloc_zeroed(layout) as *mut _xmlNode;
1329        if ptr.is_null() {
1330            return ptr::null_mut();
1331        }
1332        let node = &mut *ptr;
1333        node.type_ = type_;
1334        // Allocate and copy the name
1335        let name_bytes = name.as_bytes();
1336        let name_buf = std::alloc::alloc_zeroed(
1337            std::alloc::Layout::array::<u8>(name_bytes.len() + 1).unwrap(),
1338        );
1339        if !name_buf.is_null() {
1340            std::ptr::copy_nonoverlapping(name_bytes.as_ptr(), name_buf, name_bytes.len());
1341        }
1342        node.name = name_buf as *mut xmlChar;
1343        ptr
1344    }
1345
1346    /// Free a test node.
1347    unsafe fn free_test_node(node: *mut _xmlNode) {
1348        if node.is_null() {
1349            return;
1350        }
1351        if !(*node).name.is_null() {
1352            let name = (*node).name;
1353            // Find length
1354            let len = crate::abi::exports_xml2::xmlStrlen(name) as usize;
1355            std::alloc::dealloc(
1356                name as *mut u8,
1357                std::alloc::Layout::array::<u8>(len + 1).unwrap(),
1358            );
1359        }
1360        let layout = std::alloc::Layout::new::<_xmlNode>();
1361        std::alloc::dealloc(node as *mut u8, layout);
1362    }
1363
1364    #[test]
1365    fn test_node_test_matching_element() {
1366        unsafe {
1367            let node = create_test_node("para", 1); // XML_ELEMENT_NODE
1368            assert!(!node.is_null());
1369
1370            // Name test
1371            let name_test = NodeTest::NameTest(NameTest::LocalName("para".to_string()));
1372            assert!(match_node_test(node, &name_test));
1373
1374            // Wrong name
1375            let wrong_test = NodeTest::NameTest(NameTest::LocalName("foo".to_string()));
1376            assert!(!match_node_test(node, &wrong_test));
1377
1378            // Wildcard
1379            let wildcard = NodeTest::Wildcard;
1380            assert!(match_node_test(node, &wildcard));
1381
1382            // Node test
1383            let node_test = NodeTest::Node;
1384            assert!(match_node_test(node, &node_test));
1385
1386            // Text test should not match element
1387            let text_test = NodeTest::Text;
1388            assert!(!match_node_test(node, &text_test));
1389
1390            free_test_node(node);
1391        }
1392    }
1393
1394    #[test]
1395    fn test_node_test_matching_text() {
1396        unsafe {
1397            let node = create_test_node("", 3); // XML_TEXT_NODE
1398            assert!(!node.is_null());
1399
1400            let text_test = NodeTest::Text;
1401            assert!(match_node_test(node, &text_test));
1402
1403            let node_test = NodeTest::Node;
1404            assert!(match_node_test(node, &node_test));
1405
1406            let comment_test = NodeTest::Comment;
1407            assert!(!match_node_test(node, &comment_test));
1408
1409            let element_test = NodeTest::NameTest(NameTest::LocalName("para".to_string()));
1410            assert!(!match_node_test(node, &element_test));
1411
1412            free_test_node(node);
1413        }
1414    }
1415
1416    #[test]
1417    fn test_compile_and_free_pattern() {
1418        unsafe {
1419            let pattern_str = "para\0".as_ptr() as *const xmlChar;
1420            let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1421            assert!(!compiled.is_null());
1422            xsltFreePattern(compiled);
1423        }
1424    }
1425
1426    #[test]
1427    fn test_compile_null_pattern() {
1428        unsafe {
1429            let compiled = xsltCompilePattern(ptr::null(), ptr::null_mut());
1430            assert!(compiled.is_null());
1431        }
1432    }
1433
1434    #[test]
1435    fn test_compile_empty_pattern() {
1436        unsafe {
1437            let pattern_str = "\0".as_ptr() as *const xmlChar;
1438            let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1439            assert!(compiled.is_null());
1440        }
1441    }
1442
1443    #[test]
1444    fn test_free_null_pattern() {
1445        unsafe {
1446            xsltFreePattern(ptr::null_mut());
1447            // Should not crash
1448        }
1449    }
1450
1451    #[test]
1452    fn test_is_simple_name_pattern() {
1453        assert!(is_simple_name_pattern("para"));
1454        assert!(is_simple_name_pattern("foo:bar"));
1455        assert!(!is_simple_name_pattern("foo/bar"));
1456        assert!(!is_simple_name_pattern("para | foo"));
1457        assert!(!is_simple_name_pattern("*"));
1458    }
1459
1460    #[test]
1461    fn test_is_union_pattern() {
1462        assert!(is_union_pattern("para | foo"));
1463        assert!(is_union_pattern("para | foo | bar"));
1464        assert!(!is_union_pattern("para"));
1465        assert!(!is_union_pattern("foo/bar"));
1466    }
1467
1468    #[test]
1469    fn test_get_pattern_matched_names() {
1470        let names = get_pattern_matched_names("para");
1471        assert_eq!(names, vec!["para"]);
1472
1473        let names = get_pattern_matched_names("foo | bar");
1474        assert_eq!(names.len(), 2);
1475        assert!(names.contains(&"foo".to_string()));
1476        assert!(names.contains(&"bar".to_string()));
1477
1478        let names = get_pattern_matched_names("foo/bar");
1479        assert!(names.is_empty());
1480    }
1481
1482    #[test]
1483    fn test_compile_union_pattern() {
1484        unsafe {
1485            let pattern_str = "para | foo\0".as_ptr() as *const xmlChar;
1486            let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1487            assert!(!compiled.is_null());
1488            xsltFreePattern(compiled);
1489        }
1490    }
1491
1492    #[test]
1493    fn test_compile_compound_pattern() {
1494        unsafe {
1495            let pattern_str = "foo/bar\0".as_ptr() as *const xmlChar;
1496            let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1497            assert!(!compiled.is_null());
1498            xsltFreePattern(compiled);
1499        }
1500    }
1501
1502    #[test]
1503    fn test_compile_absolute_pattern() {
1504        unsafe {
1505            let pattern_str = "/foo/bar\0".as_ptr() as *const xmlChar;
1506            let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1507            assert!(!compiled.is_null());
1508            xsltFreePattern(compiled);
1509        }
1510    }
1511
1512    #[test]
1513    fn test_compile_attribute_pattern() {
1514        unsafe {
1515            let pattern_str = "@attr\0".as_ptr() as *const xmlChar;
1516            let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1517            assert!(!compiled.is_null());
1518            xsltFreePattern(compiled);
1519        }
1520    }
1521
1522    #[test]
1523    fn test_compile_wildcard_pattern() {
1524        unsafe {
1525            let pattern_str = "*\0".as_ptr() as *const xmlChar;
1526            let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1527            assert!(!compiled.is_null());
1528            xsltFreePattern(compiled);
1529        }
1530    }
1531
1532    #[test]
1533    fn test_compile_ns_wildcard_pattern() {
1534        unsafe {
1535            let pattern_str = "ns:*\0".as_ptr() as *const xmlChar;
1536            let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1537            assert!(!compiled.is_null());
1538            xsltFreePattern(compiled);
1539        }
1540    }
1541
1542    #[test]
1543    fn test_compile_node_test_pattern() {
1544        unsafe {
1545            let pattern_str = "node()\0".as_ptr() as *const xmlChar;
1546            let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1547            assert!(!compiled.is_null());
1548            xsltFreePattern(compiled);
1549        }
1550    }
1551
1552    #[test]
1553    fn test_compile_text_pattern() {
1554        unsafe {
1555            let pattern_str = "text()\0".as_ptr() as *const xmlChar;
1556            let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1557            assert!(!compiled.is_null());
1558            xsltFreePattern(compiled);
1559        }
1560    }
1561
1562    #[test]
1563    fn test_compile_comment_pattern() {
1564        unsafe {
1565            let pattern_str = "comment()\0".as_ptr() as *const xmlChar;
1566            let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1567            assert!(!compiled.is_null());
1568            xsltFreePattern(compiled);
1569        }
1570    }
1571
1572    #[test]
1573    fn test_compile_pi_pattern() {
1574        unsafe {
1575            let pattern_str = "processing-instruction()\0".as_ptr() as *const xmlChar;
1576            let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1577            assert!(!compiled.is_null());
1578            xsltFreePattern(compiled);
1579        }
1580    }
1581
1582    #[test]
1583    fn test_compile_predicate_pattern() {
1584        unsafe {
1585            let pattern_str = "para[1]\0".as_ptr() as *const xmlChar;
1586            let compiled = xsltCompilePattern(pattern_str, ptr::null_mut());
1587            assert!(!compiled.is_null());
1588            xsltFreePattern(compiled);
1589        }
1590    }
1591
1592    #[test]
1593    fn test_decompose_union() {
1594        let expr = parse_xpath("a | b").unwrap();
1595        let patterns = decompose_pattern(&expr, "a | b");
1596        assert!(patterns.is_some());
1597        let patterns = patterns.unwrap();
1598        assert_eq!(patterns.len(), 2);
1599        assert_eq!(patterns[0].original, "a | b");
1600        assert_eq!(patterns[1].original, "a | b");
1601    }
1602
1603    #[test]
1604    fn test_decompose_single() {
1605        let expr = parse_xpath("para").unwrap();
1606        let patterns = decompose_pattern(&expr, "para");
1607        assert!(patterns.is_some());
1608        let patterns = patterns.unwrap();
1609        assert_eq!(patterns.len(), 1);
1610    }
1611
1612    #[test]
1613    fn test_collect_steps_simple() {
1614        let expr = parse_xpath("para").unwrap();
1615        let (steps, is_absolute) = collect_steps(&expr).unwrap();
1616        assert!(!is_absolute);
1617        assert_eq!(steps.len(), 1);
1618        if let PatternStepEntry::Step(step) = &steps[0] {
1619            assert_eq!(step.axis, Axis::Child);
1620            assert!(
1621                matches!(&step.node_test, NodeTest::NameTest(NameTest::LocalName(n)) if n == "para")
1622            );
1623        } else {
1624            panic!("Expected Step entry");
1625        }
1626    }
1627
1628    #[test]
1629    fn test_collect_steps_absolute() {
1630        let expr = parse_xpath("/foo/bar").unwrap();
1631        let (steps, is_absolute) = collect_steps(&expr).unwrap();
1632        assert!(is_absolute);
1633        assert_eq!(steps.len(), 2);
1634    }
1635
1636    #[test]
1637    fn test_collect_steps_attribute() {
1638        let expr = parse_xpath("@attr").unwrap();
1639        let (steps, is_absolute) = collect_steps(&expr).unwrap();
1640        assert!(!is_absolute);
1641        assert_eq!(steps.len(), 1);
1642        if let PatternStepEntry::Step(step) = &steps[0] {
1643            assert_eq!(step.axis, Axis::Attribute);
1644        } else {
1645            panic!("Expected Step entry");
1646        }
1647    }
1648
1649    #[test]
1650    fn test_collect_steps_compound() {
1651        let expr = parse_xpath("foo/bar").unwrap();
1652        let (steps, is_absolute) = collect_steps(&expr).unwrap();
1653        assert!(!is_absolute);
1654        assert_eq!(steps.len(), 2);
1655        // First step should be "bar" (rightmost)
1656        if let PatternStepEntry::Step(step) = &steps[0] {
1657            assert!(
1658                matches!(&step.node_test, NodeTest::NameTest(NameTest::LocalName(n)) if n == "bar")
1659            );
1660        } else {
1661            panic!("Expected Step entry for bar");
1662        }
1663        // Second step should be "foo"
1664        if let PatternStepEntry::Step(step) = &steps[1] {
1665            assert!(
1666                matches!(&step.node_test, NodeTest::NameTest(NameTest::LocalName(n)) if n == "foo")
1667            );
1668        } else {
1669            panic!("Expected Step entry for foo");
1670        }
1671    }
1672
1673    #[test]
1674    fn test_match_name_test_local() {
1675        unsafe {
1676            let node = create_test_node("para", 1);
1677            assert!(!node.is_null());
1678
1679            assert!(match_name_test(
1680                node,
1681                &NameTest::LocalName("para".to_string())
1682            ));
1683            assert!(!match_name_test(
1684                node,
1685                &NameTest::LocalName("foo".to_string())
1686            ));
1687            assert!(match_name_test(node, &NameTest::Any));
1688
1689            free_test_node(node);
1690        }
1691    }
1692
1693    #[test]
1694    fn test_match_node_test_wildcard() {
1695        unsafe {
1696            let element = create_test_node("para", 1);
1697            let text = create_test_node("", 3);
1698            let comment = create_test_node("", 8);
1699
1700            let wildcard = NodeTest::Wildcard;
1701            assert!(match_node_test(element, &wildcard));
1702            assert!(!match_node_test(text, &wildcard));
1703            assert!(!match_node_test(comment, &wildcard));
1704
1705            free_test_node(element);
1706            free_test_node(text);
1707            free_test_node(comment);
1708        }
1709    }
1710
1711    #[test]
1712    fn test_match_node_test_ns_wildcard() {
1713        unsafe {
1714            let node = create_test_node("para", 1);
1715            // No namespace set — only empty prefix matches
1716            let ns_wildcard = NodeTest::NsWildcard("".to_string());
1717            assert!(match_node_test(node, &ns_wildcard));
1718
1719            let ns_wildcard = NodeTest::NsWildcard("foo".to_string());
1720            assert!(!match_node_test(node, &ns_wildcard));
1721
1722            free_test_node(node);
1723        }
1724    }
1725
1726    #[test]
1727    fn test_compute_priority_on_compiled_pattern() {
1728        unsafe {
1729            // Test through the C ABI function
1730            let pattern_str = "para\0".as_ptr() as *const xmlChar;
1731            let priority = xsltDefaultPriority(pattern_str);
1732            assert!(
1733                (priority - 0.0).abs() < f64::EPSILON,
1734                "Expected 0.0 for 'para', got {}",
1735                priority
1736            );
1737
1738            let pattern_str = "*\0".as_ptr() as *const xmlChar;
1739            let priority = xsltDefaultPriority(pattern_str);
1740            assert!(
1741                (priority - (-0.5)).abs() < f64::EPSILON,
1742                "Expected -0.5 for '*', got {}",
1743                priority
1744            );
1745
1746            let pattern_str = "node()\0".as_ptr() as *const xmlChar;
1747            let priority = xsltDefaultPriority(pattern_str);
1748            assert!(
1749                (priority - (-0.25)).abs() < f64::EPSILON,
1750                "Expected -0.25 for 'node()', got {}",
1751                priority
1752            );
1753
1754            let pattern_str = "@attr\0".as_ptr() as *const xmlChar;
1755            let priority = xsltDefaultPriority(pattern_str);
1756            assert!(
1757                (priority - 0.5).abs() < f64::EPSILON,
1758                "Expected 0.5 for '@attr', got {}",
1759                priority
1760            );
1761        }
1762    }
1763
1764    #[test]
1765    fn test_compute_priority_null() {
1766        unsafe {
1767            let priority = xsltDefaultPriority(ptr::null());
1768            assert!(
1769                (priority - 0.5).abs() < f64::EPSILON,
1770                "Expected 0.5 for null pattern, got {}",
1771                priority
1772            );
1773        }
1774    }
1775
1776    #[test]
1777    fn test_compute_priority_empty() {
1778        unsafe {
1779            let pattern_str = "\0".as_ptr() as *const xmlChar;
1780            let priority = xsltDefaultPriority(pattern_str);
1781            assert!(
1782                (priority - 0.5).abs() < f64::EPSILON,
1783                "Expected 0.5 for empty pattern, got {}",
1784                priority
1785            );
1786        }
1787    }
1788
1789    #[test]
1790    fn test_xslt_test_pattern_null_args() {
1791        unsafe {
1792            let result = xsltTestPattern(ptr::null_mut(), ptr::null_mut(), ptr::null_mut());
1793            assert_eq!(result, 0);
1794        }
1795    }
1796}