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