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