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