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