Skip to main content

libxml_rs/xslt/transform/
mod.rs

1//! XSLT transformation engine (§33, §85 Phase 8).
2//!
3//! Executes compiled stylesheets against source documents:
4//! - `xsltNewTransformContext` / `xsltFreeTransformContext`
5//! - `xsltApplyStylesheet` and variants
6//! - Template application and instruction execution
7//! - Result tree construction
8//! - Current node / context position / context size management
9//! - Recursion depth limiting
10//!
11//! # UPSTREAM-PARITY
12//!
13//! Upstream libxslt (transform.c) drives the transformation from
14//! `xsltApplyStylesheet`:
15//!
16//! 1. Create a transform context (or reuse a user-supplied one).
17//! 2. Initialize global variables and key tables.
18//! 3. Apply the template matching the root node of the source document.
19//! 4. Execute the template's instructions, building the result tree.
20//! 5. Return the result document.
21//!
22//! The `insert` pointer in the context tracks the current insertion point
23//! in the result tree; the `node` / `nodeList` fields track the current
24//! source node and node list; `contextSize` / `proximityPosition` track
25//! the XPath context.
26
27use crate::abi::allocator::xmlFree;
28use crate::abi::exports_xml2::*;
29use crate::abi::structs::*;
30use crate::abi::types::xmlElementType::*;
31use crate::abi::types::xmlXPathObjectType;
32use crate::abi::types::*;
33use crate::xml::tree::*;
34use std::ffi::c_void;
35use std::os::raw::{c_char, c_int};
36use std::ptr;
37
38use super::compiler::{
39    get_element_name, get_element_ns, is_xslt_element, is_xslt_namespace, XSLT_NAMESPACE,
40};
41
42/// Maximum template recursion depth (matches upstream XSLT_MAX_DEPTH).
43pub const XSLT_MAX_DEPTH: c_int = 3000;
44
45/// Maximum insert depth.
46pub const XSLT_MAX_INSERT_DEPTH: c_int = 50;
47
48/// State flags for the transform context.
49pub const XSLT_STATE_OK: c_int = 0;
50pub const XSLT_STATE_ERROR: c_int = 1;
51
52/// The transformation was stopped (e.g. xsl:message terminate="yes").
53pub const XSLT_STATE_STOPPED: c_int = 2;
54
55/// Maximum template recursion depth (upstream `xsltMaxDepth`, transform.c).
56#[no_mangle]
57pub static mut xsltMaxDepth: c_int = 3000;
58
59/// Maximum number of variables/params (upstream `xsltMaxVars`, transform.c).
60#[no_mangle]
61pub static mut xsltMaxVars: c_int = 15000;
62
63/// Whether XInclude processing is enabled by default for documents loaded
64/// by the transform (upstream `xsltDoXIncludeDefault`, transform.c).
65static mut XSLT_XINCLUDE_DEFAULT: c_int = 0;
66
67/// Set whether XInclude processing is done on documents loaded by the
68/// transformation (upstream `xsltSetXIncludeDefault`).
69///
70/// # SAFETY
71///
72/// - The value is a process-wide setting, matching upstream's global.
73#[no_mangle]
74pub unsafe extern "C" fn xsltSetXIncludeDefault(xinclude: c_int) {
75    unsafe { XSLT_XINCLUDE_DEFAULT = if xinclude != 0 { 1 } else { 0 } };
76}
77
78/// Get the current XInclude default (upstream `xsltGetXIncludeDefault`).
79#[no_mangle]
80pub unsafe extern "C" fn xsltGetXIncludeDefault() -> c_int {
81    unsafe { XSLT_XINCLUDE_DEFAULT }
82}
83
84/// Create a new transform context.
85///
86/// # SAFETY
87///
88/// - `style` must be a valid compiled `_xsltStylesheet`.
89/// - `doc` must be a valid source document (may be NULL).
90#[no_mangle]
91pub unsafe extern "C" fn xsltNewTransformContext(
92    style: *mut _xsltStylesheet,
93    doc: *mut _xmlDoc,
94) -> *mut _xsltTransformContext {
95    if style.is_null() {
96        return ptr::null_mut();
97    }
98    let ctxt = libc::calloc(1, core::mem::size_of::<_xsltTransformContext>())
99        as *mut _xsltTransformContext;
100    if ctxt.is_null() {
101        return ptr::null_mut();
102    }
103    (*ctxt).style = style;
104    (*ctxt).document = doc;
105    (*ctxt).state = XSLT_STATE_OK;
106    // UPSTREAM-PARITY: the per-context depth/vars limits come from the
107    // process-wide xsltMaxDepth/xsltMaxVars globals (adjustable via
108    // xsltproc --maxdepth/--maxvars).
109    (*ctxt).maxDepth = unsafe { xsltMaxDepth };
110    (*ctxt).maxInsertDepth = XSLT_MAX_INSERT_DEPTH;
111    (*ctxt).profile = ptr::null_mut();
112
113    // Create the XPath context.
114    let xpath_ctxt = xmlXPathNewContext(doc);
115    if !xpath_ctxt.is_null() {
116        (*ctxt).xpathCtxt = xpath_ctxt;
117        // Stash this transform context in the XPath context's opaque slot so
118        // XSLT XPath functions (e.g. key()) can reach the key tables.
119        let internal = (*xpath_ctxt).extra as *mut crate::xml::xpath::context::XPathContext;
120        if !internal.is_null() {
121            (*internal).func_lookup_data = ctxt as *mut c_void;
122        }
123        // Register the standard XSLT extension functions and variable
124        // lookup for XSLT evaluation.
125        register_xslt_functions(ctxt);
126    }
127
128    // Security preferences: use the default if none set.
129    if (*style).secPrefs.is_null() {
130        (*ctxt).secPrefs = crate::xslt::security::xsltGetDefaultSecurityPrefs();
131    } else {
132        (*ctxt).secPrefs = (*style).secPrefs;
133    }
134    ctxt
135}
136
137/// Free a transform context.
138///
139/// # SAFETY
140///
141/// - `ctxt` must be a valid `_xsltTransformContext` allocated by
142///   `xsltNewTransformContext`, or NULL.
143#[no_mangle]
144pub unsafe extern "C" fn xsltFreeTransformContext(ctxt: *mut _xsltTransformContext) {
145    if ctxt.is_null() {
146        return;
147    }
148    // Free the XPath context.
149    if !(*ctxt).xpathCtxt.is_null() {
150        xmlXPathFreeContext((*ctxt).xpathCtxt);
151    }
152    // NOTE: the result document is owned by the caller of
153    // xsltApplyStylesheet (upstream xsltFreeTransformContext does not free
154    // ctxt->output / resultDoc); freeing it here would double-free the
155    // result when the caller releases it.
156    // Free global variables.
157    crate::xslt::variables::xsltFreeGlobalVariables(ctxt);
158    // Free key tables.
159    crate::xslt::keys::xsltFreeKeyTables(ctxt);
160    // Free the document cache.
161    crate::xslt::documents::xsltFreeDocCache(ctxt);
162    // Free extension registrations.
163    crate::xslt::extensions::xsltFreeExts(ctxt);
164    // Free the variable table itself.
165    if !(*ctxt).varsTab.is_null() {
166        libc::free((*ctxt).varsTab as *mut libc::c_void);
167    }
168    if !(*ctxt).paramsTab.is_null() {
169        libc::free((*ctxt).paramsTab as *mut libc::c_void);
170    }
171    if !(*ctxt).templTab.is_null() {
172        libc::free((*ctxt).templTab as *mut libc::c_void);
173    }
174    libc::free(ctxt as *mut libc::c_void);
175}
176
177/// Apply a stylesheet to a document.
178///
179/// `params` is a NULL-terminated array of `name=value` strings.
180/// Returns the result document (caller frees with `xmlFreeDoc`).
181///
182/// # SAFETY
183///
184/// - `style` must be a valid compiled stylesheet.
185/// - `doc` must be a valid source document.
186#[no_mangle]
187pub unsafe extern "C" fn xsltApplyStylesheet(
188    style: *mut _xsltStylesheet,
189    doc: *mut _xmlDoc,
190    params: *mut *const c_char,
191) -> *mut _xmlDoc {
192    xsltApplyStylesheetUser(
193        style,
194        doc,
195        params,
196        ptr::null(),
197        ptr::null_mut(),
198        ptr::null_mut(),
199    )
200}
201
202/// Apply a stylesheet with user control.
203///
204/// # SAFETY
205///
206/// - All pointers must be valid or NULL where permitted.
207#[no_mangle]
208pub unsafe extern "C" fn xsltApplyStylesheetUser(
209    style: *mut _xsltStylesheet,
210    doc: *mut _xmlDoc,
211    params: *mut *const c_char,
212    _output: *const c_char,
213    _profile: *mut c_void,
214    userCtxt: *mut _xsltTransformContext,
215) -> *mut _xmlDoc {
216    if style.is_null() || doc.is_null() {
217        return ptr::null_mut();
218    }
219
220    // Use the user-provided context or create a new one.
221    let mut ctxt = userCtxt;
222    let mut own_ctxt = false;
223    if ctxt.is_null() {
224        ctxt = xsltNewTransformContext(style, doc);
225        if ctxt.is_null() {
226            return ptr::null_mut();
227        }
228        own_ctxt = true;
229    }
230
231    // Parse the stylesheet parameters.
232    if !params.is_null() {
233        crate::xslt::parameters::xsltParseStylesheetParams(style, params);
234    }
235
236    // Initialize global variables.
237    crate::xslt::variables::xsltInitGlobalVariables(ctxt);
238
239    // Initialize key tables.
240    crate::xslt::keys::xsltInitKeys(ctxt, style);
241
242    // Apply the strip-space rules to the source document.
243    crate::xslt::whitespace::xsltApplyStripSpaces(style, doc);
244
245    // Build the result document.
246    let result = libc::calloc(1, core::mem::size_of::<_xmlDoc>()) as *mut _xmlDoc;
247    if result.is_null() {
248        if own_ctxt {
249            xsltFreeTransformContext(ctxt);
250        }
251        return ptr::null_mut();
252    }
253    (*result).type_ = XML_DOCUMENT_NODE as c_int;
254    (*result).version = crate::xml::string::xml_strdup(b"1.0\0".as_ptr() as *const xmlChar);
255    (*result).doc = result;
256    // Copy output settings from the stylesheet. These are heap-copied so
257    // that free_doc (which frees version/encoding with xmlFree) is safe
258    // and the stylesheet keeps its own copies.
259    if !(*style).encoding.is_null() {
260        (*result).encoding = crate::xml::string::xml_strdup((*style).encoding);
261    }
262    if !(*style).version.is_null() {
263        let v = crate::xml::string::xml_strdup((*style).version);
264        if !v.is_null() {
265            if !(*result).version.is_null() {
266                libc::free((*result).version as *mut libc::c_void);
267            }
268            (*result).version = v;
269        }
270    }
271
272    (*ctxt).resultDoc = result;
273    (*ctxt).insert = result as *mut _xmlNode;
274
275    // Apply the root template: XSLT 1.0 §5.1 applies the template
276    // matching "/" to the document node (the root of the source tree).
277    // The document node is the doc cast to a node; its parent is null.
278    (*ctxt).node = doc as *mut _xmlNode;
279    (*ctxt).document = doc;
280    (*ctxt).contextSize = 1;
281    (*ctxt).proximityPosition = 1;
282    let result_code = apply_templates_to_node(ctxt, doc as *mut _xmlNode, ptr::null());
283    let _ = result_code;
284
285    // UPSTREAM-PARITY: a transformation whose context is no longer in the
286    // OK state produces no result (xsltApplyStylesheetInternal frees the
287    // result and returns NULL when ctxt->state != XSLT_STATE_OK).
288    let final_result = if (*ctxt).state == XSLT_STATE_OK {
289        result
290    } else {
291        free_doc(result);
292        ptr::null_mut()
293    };
294
295    if own_ctxt {
296        // Detach the result document from the context before freeing.
297        (*ctxt).resultDoc = ptr::null_mut();
298        xsltFreeTransformContext(ctxt);
299    }
300    final_result
301}
302
303/// Apply a stylesheet with a parameter stack.
304///
305/// # SAFETY
306///
307/// - All pointers must be valid or NULL where permitted.
308#[no_mangle]
309pub unsafe extern "C" fn xsltApplyStylesheetStacked(
310    style: *mut _xsltStylesheet,
311    doc: *mut _xmlDoc,
312    params: *mut *const c_char,
313    _stack: *mut c_void,
314) -> *mut _xmlDoc {
315    xsltApplyStylesheet(style, doc, params)
316}
317
318/// Free the result of a transformation.
319///
320/// # SAFETY
321///
322/// - `result` must be a document returned by `xsltApplyStylesheet` or NULL.
323#[no_mangle]
324pub unsafe extern "C" fn xsltFreeTransformResult(result: *mut _xmlDoc) {
325    if !result.is_null() {
326        free_doc(result);
327    }
328}
329
330/// Apply a stylesheet and write the result to an output channel.
331///
332/// # UPSTREAM-PARITY
333///
334/// Mirrors `xsltRunStylesheetUser` (transform.c 1.1.45): applies the
335/// stylesheet and saves to `output` (a filename) or `IObuf`. The SAX
336/// callback mode is not implemented by upstream either (it returns -1).
337///
338/// # SAFETY
339///
340/// - All pointers must be valid or NULL where permitted.
341#[no_mangle]
342pub unsafe extern "C" fn xsltRunStylesheetUser(
343    style: *mut _xsltStylesheet,
344    doc: *mut _xmlDoc,
345    params: *mut *const c_char,
346    output: *const c_char,
347    SAX: *mut crate::abi::structs::_xmlSAXHandler,
348    IObuf: *mut crate::abi::structs::_xmlOutputBuffer,
349    profile: *mut c_void,
350    userCtxt: *mut _xsltTransformContext,
351) -> c_int {
352    if output.is_null() && SAX.is_null() && IObuf.is_null() {
353        return -1;
354    }
355    if !SAX.is_null() && !IObuf.is_null() {
356        return -1;
357    }
358    // SAX output mode is unsupported upstream as well.
359    if !SAX.is_null() {
360        return -1;
361    }
362    let tmp = xsltApplyStylesheetUser(style, doc, params, output, profile, userCtxt);
363    if tmp.is_null() {
364        eprintln!("xsltRunStylesheet : run failed");
365        return -1;
366    }
367    let ret = if !IObuf.is_null() {
368        let mut txt: *mut xmlChar = ptr::null_mut();
369        let mut len: c_int = 0;
370        let r = crate::xslt::serialization::xsltSaveResultToString(&mut txt, &mut len, tmp, style);
371        if r != 0 || txt.is_null() {
372            -1
373        } else {
374            let written = crate::xml::io::output_buffer_write(IObuf, len, txt as *const c_char);
375            crate::abi::allocator::xmlFree(txt as *mut c_void);
376            written
377        }
378    } else {
379        crate::xslt::serialization::xsltSaveResultToFilename(output, tmp, style, 0)
380    };
381    free_doc(tmp);
382    ret
383}
384
385/// Apply a stylesheet and write the result to an output channel.
386///
387/// # SAFETY
388///
389/// - All pointers must be valid or NULL where permitted.
390#[no_mangle]
391pub unsafe extern "C" fn xsltRunStylesheet(
392    style: *mut _xsltStylesheet,
393    doc: *mut _xmlDoc,
394    params: *mut *const c_char,
395    output: *const c_char,
396    SAX: *mut crate::abi::structs::_xmlSAXHandler,
397    IObuf: *mut crate::abi::structs::_xmlOutputBuffer,
398) -> c_int {
399    xsltRunStylesheetUser(
400        style,
401        doc,
402        params,
403        output,
404        SAX,
405        IObuf,
406        ptr::null_mut(),
407        ptr::null_mut(),
408    )
409}
410
411/// Apply the root template (match="/") for empty documents.
412///
413/// # SAFETY
414///
415/// - All pointers must be valid.
416unsafe fn apply_root_template(ctxt: *mut _xsltTransformContext, doc: *mut _xmlDoc) -> c_int {
417    // Find the template matching "/".
418    let style = (*ctxt).style;
419    if style.is_null() {
420        return -1;
421    }
422    // Use the document node itself as the context node.
423    let doc_node = doc as *mut _xmlNode;
424    (*ctxt).node = doc_node;
425    (*ctxt).contextSize = 1;
426    (*ctxt).proximityPosition = 1;
427    let templ = crate::xslt::templates::xsltFindTemplate(style, doc_node, ptr::null());
428    if templ.is_null() {
429        // No match: copy nothing (empty result).
430        return 0;
431    }
432    // Execute the template body.
433    let mut vars_base = (*ctxt).varsNr;
434    let _ = &mut vars_base;
435    (*ctxt).templ = templ;
436    execute_content(ctxt, (*templ).content);
437    (*ctxt).templ = ptr::null_mut();
438    0
439}
440
441/// Apply templates to a single node in the given mode.
442///
443/// # SAFETY
444///
445/// - All pointers must be valid.
446unsafe fn apply_templates_to_node(
447    ctxt: *mut _xsltTransformContext,
448    node: *mut _xmlNode,
449    mode: *const xmlChar,
450) -> c_int {
451    let style = (*ctxt).style;
452    if style.is_null() {
453        return -1;
454    }
455    let templ = crate::xslt::templates::xsltFindTemplate(style, node, mode);
456    if templ.is_null() {
457        // Built-in template rules (XSLT 1.0 §5.8):
458        // - For root/document: apply templates to children.
459        // - For elements: apply templates to children.
460        // - For text/attribute: copy the text.
461        // - For comments/PIs: do nothing.
462        let typ = (*node).type_;
463        if typ == XML_TEXT_NODE as c_int
464            || typ == XML_CDATA_SECTION_NODE as c_int
465            || typ == XML_ATTRIBUTE_NODE as c_int
466        {
467            let content = node_get_content(node);
468            if !content.is_null() {
469                append_text_node(ctxt, content);
470                libc::free(content as *mut libc::c_void);
471            }
472        } else if typ == XML_ELEMENT_NODE as c_int
473            || typ == XML_DOCUMENT_NODE as c_int
474            || typ == XML_HTML_DOCUMENT_NODE as c_int
475        {
476            // Apply templates to children.
477            apply_templates_to_children(ctxt, node, mode);
478        }
479        return 0;
480    }
481    // Check recursion depth.
482    if (*ctxt).depth >= (*ctxt).maxDepth {
483        return -1;
484    }
485    (*ctxt).depth += 1;
486    (*ctxt).templ = templ;
487    (*ctxt).node = node;
488    (*ctxt).contextSize = 1;
489    (*ctxt).proximityPosition = 1;
490    execute_content(ctxt, (*templ).content);
491    (*ctxt).depth -= 1;
492    (*ctxt).templ = ptr::null_mut();
493    0
494}
495
496/// Apply templates to all children of a node.
497///
498/// # SAFETY
499///
500/// - All pointers must be valid.
501unsafe fn apply_templates_to_children(
502    ctxt: *mut _xsltTransformContext,
503    node: *mut _xmlNode,
504    mode: *const xmlChar,
505) -> c_int {
506    // Collect the children in document order.
507    let mut children: Vec<*mut _xmlNode> = Vec::new();
508    let mut child = (*node).children;
509    while !child.is_null() {
510        children.push(child);
511        child = (*child).next;
512    }
513    // Sort if xsl:sort children are present on the apply-templates
514    // instruction (handled by the caller via sort handling).
515    let size = children.len();
516    (*ctxt).contextSize = size as c_int;
517    for (i, node) in children.iter().enumerate() {
518        (*ctxt).node = *node;
519        (*ctxt).proximityPosition = (i + 1) as c_int;
520        apply_templates_to_node(ctxt, *node, mode);
521    }
522    0
523}
524
525/// Execute the content of a template (a list of nodes).
526///
527/// # SAFETY
528///
529/// - All pointers must be valid.
530pub unsafe fn execute_content(ctxt: *mut _xsltTransformContext, content: *mut _xmlNode) -> c_int {
531    let mut cur = content;
532    while !cur.is_null() {
533        let next = (*cur).next;
534        xsltProcessInstruction(ctxt, cur);
535        if (*ctxt).state == XSLT_STATE_ERROR {
536            return -1;
537        }
538        cur = next;
539    }
540    0
541}
542
543/// Process a single instruction node.
544///
545/// # SAFETY
546///
547/// - `ctxt` must be a valid transform context.
548/// - `inst` must be a valid instruction node.
549pub unsafe fn xsltProcessInstruction(
550    ctxt: *mut _xsltTransformContext,
551    inst: *mut _xmlNode,
552) -> c_int {
553    if ctxt.is_null() || inst.is_null() {
554        return -1;
555    }
556    let typ = (*inst).type_;
557    match typ {
558        t if t == XML_TEXT_NODE as c_int || t == XML_CDATA_SECTION_NODE as c_int => {
559            // Literal text: copy to the result.
560            if !(*inst).content.is_null() {
561                append_text_node(ctxt, (*inst).content);
562            }
563            0
564        }
565        t if t == XML_COMMENT_NODE as c_int => {
566            // Literal comment: copy to the result.
567            if !(*inst).content.is_null() {
568                append_comment_node(ctxt, (*inst).content);
569            }
570            0
571        }
572        t if t == XML_PI_NODE as c_int => {
573            // Literal PI: copy to the result.
574            if !(*inst).name.is_null() {
575                let content = if (*inst).content.is_null() {
576                    ptr::null()
577                } else {
578                    (*inst).content
579                };
580                append_pi_node(ctxt, (*inst).name, content);
581            }
582            0
583        }
584        t if t == XML_ELEMENT_NODE as c_int => {
585            if is_xslt_namespace(inst) {
586                process_xslt_instruction(ctxt, inst);
587            } else {
588                // Literal result element: create the element and process
589                // its content.
590                process_literal_element(ctxt, inst);
591            }
592            0
593        }
594        _ => 0,
595    }
596}
597
598/// Process an XSLT instruction element.
599///
600/// # SAFETY
601///
602/// - All pointers must be valid.
603unsafe fn process_xslt_instruction(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) -> c_int {
604    let name = get_element_name(inst);
605    match name.as_deref() {
606        Some("apply-templates") => {
607            process_apply_templates(ctxt, inst);
608        }
609        Some("call-template") => {
610            process_call_template(ctxt, inst);
611        }
612        Some("apply-imports") => {
613            process_apply_imports(ctxt, inst);
614        }
615        Some("for-each") => {
616            process_for_each(ctxt, inst);
617        }
618        Some("value-of") => {
619            process_value_of(ctxt, inst);
620        }
621        Some("copy-of") => {
622            process_copy_of(ctxt, inst);
623        }
624        Some("copy") => {
625            process_copy(ctxt, inst);
626        }
627        Some("element") => {
628            process_element(ctxt, inst);
629        }
630        Some("attribute") => {
631            process_attribute(ctxt, inst);
632        }
633        Some("text") => {
634            process_text(ctxt, inst);
635        }
636        Some("comment") => {
637            process_comment(ctxt, inst);
638        }
639        Some("processing-instruction") => {
640            process_pi(ctxt, inst);
641        }
642        Some("number") => {
643            process_number(ctxt, inst);
644        }
645        Some("choose") => {
646            process_choose(ctxt, inst);
647        }
648        Some("when") | Some("otherwise") => {
649            // Only valid inside xsl:choose; ignored here.
650        }
651        Some("if") => {
652            process_if(ctxt, inst);
653        }
654        Some("variable") => {
655            process_variable(ctxt, inst);
656        }
657        Some("param") => {
658            process_param(ctxt, inst);
659        }
660        Some("with-param") => {
661            // Only valid inside call-template/apply-templates; ignored.
662        }
663        Some("sort") => {
664            // Only valid inside for-each/apply-templates; ignored.
665        }
666        Some("message") => {
667            process_message(ctxt, inst);
668        }
669        Some("fallback") => {
670            // Only used when an extension element is unavailable.
671        }
672        Some("output")
673        | Some("decimal-format")
674        | Some("namespace-alias")
675        | Some("attribute-set")
676        | Some("key")
677        | Some("strip-space")
678        | Some("preserve-space")
679        | Some("import")
680        | Some("include")
681        | Some("stylesheet")
682        | Some("transform") => {
683            // Top-level elements: not instructions, ignored in content.
684        }
685        _ => {
686            // Unknown element: may be an EXSLT extension element or a
687            // registered extension element.
688            let ns = get_element_ns(inst);
689            if let Some(ns_uri) = ns {
690                // exsl:document — write the element content to a file.
691                if ns_uri == crate::exslt::EXSLT_NS_COMMON
692                    && get_element_name(inst).as_deref() == Some("document")
693                {
694                    process_exsl_document(ctxt, inst);
695                    return 0;
696                }
697                // Check for a registered extension element.
698                let name_ptr = (*inst).name;
699                let ns_cstr = str_to_cstr(&ns_uri);
700                let found = crate::xslt::extensions::xsltFindExtElement(
701                    ctxt,
702                    name_ptr,
703                    ns_cstr.as_ptr() as *const xmlChar,
704                );
705                if !found.is_null() {
706                    // Invoke the extension element (Phase 8: full bridge).
707                    return 0;
708                }
709            }
710            // Unknown instruction: process fallback children or ignore.
711        }
712    }
713    0
714}
715
716/// Convert a Rust string to a NUL-terminated byte vec.
717fn str_to_cstr(s: &str) -> Vec<u8> {
718    let mut v = s.as_bytes().to_vec();
719    v.push(0);
720    v
721}
722
723/// Process the EXSLT `<exsl:document href="...">` extension element:
724/// instantiate the element's content into a separate result document and
725/// write it to the file named by the `href` attribute.
726///
727/// # UPSTREAM-PARITY
728///
729/// Upstream libexslt (common.c `exsltDocumentElem`) creates a new document
730/// whose root element copies the attributes of the exsl:document element
731/// (minus href), evaluates the content into it, and saves it to the
732/// resolved href (relative to the stylesheet's base URI).
733///
734/// # SAFETY
735///
736/// - All pointers must be valid.
737unsafe fn process_exsl_document(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
738    let href = get_prop(inst, b"href\0".as_ptr() as *const xmlChar);
739    if href.is_null() {
740        crate::xslt::errors::xsltTransformError(
741            ctxt,
742            (*ctxt).style,
743            inst,
744            b"exsl:document: missing href attribute\0".as_ptr() as *const c_char,
745        );
746        return;
747    }
748    // Build the fragment document.
749    let frag = libc::calloc(1, core::mem::size_of::<_xmlDoc>()) as *mut _xmlDoc;
750    if frag.is_null() {
751        libc::free(href as *mut libc::c_void);
752        return;
753    }
754    (*frag).type_ = XML_DOCUMENT_NODE as c_int;
755    (*frag).doc = frag;
756    let saved_insert = (*ctxt).insert;
757    let saved_result = (*ctxt).resultDoc;
758    (*ctxt).insert = frag as *mut _xmlNode;
759    (*ctxt).resultDoc = frag;
760    execute_content(ctxt, (*inst).children);
761    (*ctxt).insert = saved_insert;
762    (*ctxt).resultDoc = saved_result;
763
764    // Write the fragment to the file.
765    let fname = crate::abi::versioning::c_str_to_bytes(href as *const c_char);
766    if let Some(name) = fname {
767        let path = String::from_utf8_lossy(name);
768        let cpath = str_to_cstr(&path);
769        let out = libc::fopen(
770            cpath.as_ptr() as *const c_char,
771            b"wb\0".as_ptr() as *const c_char,
772        );
773        if !out.is_null() {
774            let buf = crate::xml::io::buf_create(-1);
775            if !buf.is_null() {
776                crate::xml::tree::doc_dump(buf, frag);
777                let content = crate::xml::io::buf_content(buf);
778                let len = crate::xml::io::buf_length(buf);
779                if !content.is_null() && len > 0 {
780                    libc::fwrite(content as *const libc::c_void, 1, len as usize, out);
781                }
782                crate::xml::io::buf_free(buf);
783            }
784            libc::fclose(out);
785        }
786    }
787    libc::free(href as *mut libc::c_void);
788    free_doc(frag);
789}
790
791/// Evaluate an XPath expression in the current context.
792/// Returns an XPath object (caller frees) or NULL on error.
793///
794/// # SAFETY
795///
796/// - All pointers must be valid.
797unsafe fn eval_xpath(
798    ctxt: *mut _xsltTransformContext,
799    expr: *const xmlChar,
800) -> *mut _xmlXPathObject {
801    if ctxt.is_null() || expr.is_null() {
802        return ptr::null_mut();
803    }
804    let xpath_ctxt = (*ctxt).xpathCtxt;
805    if xpath_ctxt.is_null() {
806        return ptr::null_mut();
807    }
808    // Set the context node and position on both the C ABI struct and the
809    // internal Rust XPathContext (which is what the evaluator actually
810    // reads via the `extra` field).
811    (*xpath_ctxt).node = (*ctxt).node;
812    (*xpath_ctxt).doc = (*ctxt).document;
813    (*xpath_ctxt).contextSize = (*ctxt).contextSize;
814    (*xpath_ctxt).proximityPosition = (*ctxt).proximityPosition;
815    let internal = (*xpath_ctxt).extra as *mut crate::xml::xpath::context::XPathContext;
816    if !internal.is_null() {
817        (*internal).context_node = (*ctxt).node;
818        (*internal).document = (*ctxt).document;
819        (*internal).context_size = (*ctxt).contextSize;
820        (*internal).context_position = (*ctxt).proximityPosition;
821        (*internal).proximity_position = (*ctxt).proximityPosition;
822    }
823    xmlXPathEvalExpression(expr, xpath_ctxt)
824}
825
826/// Process `xsl:apply-templates`.
827///
828/// # SAFETY
829///
830/// - All pointers must be valid.
831unsafe fn process_apply_templates(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
832    // mode attribute.
833    let mode = get_prop(inst, b"mode\0".as_ptr() as *const xmlChar);
834    // select attribute (default: all children).
835    let select = get_prop(inst, b"select\0".as_ptr() as *const xmlChar);
836    // with-param children.
837    let params = collect_with_params(ctxt, inst);
838
839    let obj = if !select.is_null() {
840        eval_xpath(ctxt, select)
841    } else {
842        // Default select: "node()" — all children.
843        // Build a node-set from children.
844        build_child_node_set(ctxt)
845    };
846    if !select.is_null() {
847        libc::free(select as *mut libc::c_void);
848    }
849    if obj.is_null() {
850        if !mode.is_null() {
851            libc::free(mode as *mut libc::c_void);
852        }
853        return;
854    }
855
856    // Extract the node-set.
857    let nodes = if (*obj).type_ == xmlXPathObjectType::XPATH_NODESET as c_int {
858        (*obj).nodesetval as *mut _xmlNodeSet
859    } else {
860        ptr::null_mut()
861    };
862
863    if !nodes.is_null() && (*nodes).nodeNr > 0 {
864        // Check for xsl:sort children.
865        let sort = find_sort_children(ctxt, inst);
866        let mut node_ptrs: Vec<*mut _xmlNode> = Vec::new();
867        let mut i = 0;
868        while i < (*nodes).nodeNr {
869            let n = *(*nodes).nodeTab.offset(i as isize);
870            if !n.is_null() {
871                node_ptrs.push(n);
872            }
873            i += 1;
874        }
875        // Sort if requested.
876        if !sort.is_null() {
877            let mut sorted =
878                libc::calloc(1, core::mem::size_of::<_xmlNodeSet>()) as *mut _xmlNodeSet;
879            if !sorted.is_null() {
880                (*sorted).nodeNr = node_ptrs.len() as c_int;
881                (*sorted).nodeMax = node_ptrs.len() as c_int;
882                let tab = libc::malloc(node_ptrs.len() * core::mem::size_of::<*mut _xmlNode>())
883                    as *mut *mut _xmlNode;
884                (*sorted).nodeTab = tab;
885                for (idx, n) in node_ptrs.iter().enumerate() {
886                    if !tab.is_null() {
887                        *tab.offset(idx as isize) = *n;
888                    }
889                }
890                crate::xslt::sorting::xsltSortNodeSet(ctxt, sorted, sort);
891                // Apply templates in sorted order.
892                let mut k = 0;
893                while k < (*sorted).nodeNr {
894                    let n = *(*sorted).nodeTab.offset(k as isize);
895                    if !n.is_null() {
896                        (*ctxt).node = n;
897                        (*ctxt).contextSize = (*sorted).nodeNr;
898                        (*ctxt).proximityPosition = k + 1;
899                        apply_templates_with_params(ctxt, n, mode, params);
900                    }
901                    k += 1;
902                }
903                libc::free((*sorted).nodeTab as *mut libc::c_void);
904                libc::free(sorted as *mut libc::c_void);
905            }
906        } else {
907            (*ctxt).contextSize = node_ptrs.len() as c_int;
908            for (i, n) in node_ptrs.iter().enumerate() {
909                if !n.is_null() {
910                    (*ctxt).node = *n;
911                    (*ctxt).proximityPosition = (i + 1) as c_int;
912                    apply_templates_with_params(ctxt, *n, mode, params);
913                }
914            }
915        }
916    }
917
918    if !mode.is_null() {
919        libc::free(mode as *mut libc::c_void);
920    }
921    xmlXPathFreeObject(obj);
922}
923
924/// Apply templates to a node, passing parameters.
925///
926/// # SAFETY
927///
928/// - All pointers must be valid.
929unsafe fn apply_templates_with_params(
930    ctxt: *mut _xsltTransformContext,
931    node: *mut _xmlNode,
932    mode: *const xmlChar,
933    params: *mut _xsltStackElem,
934) {
935    // Push the params onto the parameter stack.
936    let mut p = params;
937    while !p.is_null() {
938        crate::xslt::parameters::xsltPushParam(ctxt, p);
939        p = (*p).next;
940    }
941    apply_templates_to_node(ctxt, node, mode);
942    // Pop the params.
943    let mut p = params;
944    while !p.is_null() {
945        crate::xslt::parameters::xsltPopParam(ctxt);
946        p = (*p).next;
947    }
948}
949
950/// Build a node-set from the children of the current node.
951///
952/// # SAFETY
953///
954/// - `ctxt` must be valid.
955unsafe fn build_child_node_set(ctxt: *mut _xsltTransformContext) -> *mut _xmlXPathObject {
956    let ns = xmlXPathNodeSetCreate(ptr::null_mut());
957    if ns.is_null() {
958        return ptr::null_mut();
959    }
960    let obj = xmlMalloc_zero_obj();
961    if obj.is_null() {
962        libc::free(ns as *mut libc::c_void);
963        return ptr::null_mut();
964    }
965    (*obj).type_ = xmlXPathObjectType::XPATH_NODESET as c_int;
966    (*obj).nodesetval = ns as *mut c_void;
967    let node = (*ctxt).node;
968    if !node.is_null() {
969        let mut child = (*node).children;
970        while !child.is_null() {
971            append_to_node_set(ns, child);
972            child = (*child).next;
973        }
974    }
975    obj
976}
977
978/// Allocate a zeroed XPath object.
979unsafe fn xmlMalloc_zero_obj() -> *mut _xmlXPathObject {
980    libc::calloc(1, core::mem::size_of::<_xmlXPathObject>()) as *mut _xmlXPathObject
981}
982
983/// Append a node to a node-set.
984///
985/// # SAFETY
986///
987/// - All pointers must be valid.
988unsafe fn append_to_node_set(ns: *mut _xmlNodeSet, node: *mut _xmlNode) {
989    if ns.is_null() || node.is_null() {
990        return;
991    }
992    // Deduplicate.
993    let mut i = 0;
994    while i < (*ns).nodeNr {
995        if *(*ns).nodeTab.offset(i as isize) == node {
996            return;
997        }
998        i += 1;
999    }
1000    if (*ns).nodeNr >= (*ns).nodeMax {
1001        let new_max = if (*ns).nodeMax == 0 {
1002            8
1003        } else {
1004            (*ns).nodeMax * 2
1005        };
1006        let new_tab = libc::realloc(
1007            (*ns).nodeTab as *mut libc::c_void,
1008            (new_max as usize) * core::mem::size_of::<*mut _xmlNode>(),
1009        ) as *mut *mut _xmlNode;
1010        if new_tab.is_null() {
1011            return;
1012        }
1013        (*ns).nodeTab = new_tab;
1014        (*ns).nodeMax = new_max;
1015    }
1016    *(*ns).nodeTab.offset((*ns).nodeNr as isize) = node;
1017    (*ns).nodeNr += 1;
1018}
1019
1020/// Collect the `xsl:with-param` children of an instruction.
1021///
1022/// Returns a linked list of evaluated parameter stack elements.
1023///
1024/// # SAFETY
1025///
1026/// - All pointers must be valid.
1027unsafe fn collect_with_params(
1028    ctxt: *mut _xsltTransformContext,
1029    inst: *mut _xmlNode,
1030) -> *mut _xsltStackElem {
1031    let mut head: *mut _xsltStackElem = ptr::null_mut();
1032    let mut tail: *mut _xsltStackElem = ptr::null_mut();
1033    let mut child = (*inst).children;
1034    while !child.is_null() {
1035        let next = (*child).next;
1036        if is_xslt_element(child, "with-param") {
1037            let param = evaluate_with_param(ctxt, child);
1038            if !param.is_null() {
1039                (*param).next = ptr::null_mut();
1040                if tail.is_null() {
1041                    head = param;
1042                    tail = param;
1043                } else {
1044                    (*tail).next = param;
1045                    tail = param;
1046                }
1047            }
1048        }
1049        child = next;
1050    }
1051    head
1052}
1053
1054/// Evaluate a single `xsl:with-param` element.
1055///
1056/// # SAFETY
1057///
1058/// - All pointers must be valid.
1059unsafe fn evaluate_with_param(
1060    ctxt: *mut _xsltTransformContext,
1061    inst: *mut _xmlNode,
1062) -> *mut _xsltStackElem {
1063    let name = get_prop(inst, b"name\0".as_ptr() as *const xmlChar);
1064    if name.is_null() {
1065        return ptr::null_mut();
1066    }
1067    let select = get_prop(inst, b"select\0".as_ptr() as *const xmlChar);
1068    let param = libc::calloc(1, core::mem::size_of::<_xsltStackElem>()) as *mut _xsltStackElem;
1069    if param.is_null() {
1070        libc::free(name as *mut libc::c_void);
1071        if !select.is_null() {
1072            libc::free(select as *mut libc::c_void);
1073        }
1074        return ptr::null_mut();
1075    }
1076    (*param).name = name;
1077    (*param).flags = 2 | 4; // PARAM | INTERNAL
1078    if !select.is_null() {
1079        let obj = eval_xpath(ctxt, select);
1080        if !obj.is_null() {
1081            (*param).value = obj;
1082        }
1083        libc::free(select as *mut libc::c_void);
1084    } else {
1085        // Inline content: result tree fragment.
1086        let value = eval_content_fragment(ctxt, (*inst).children);
1087        if !value.is_null() {
1088            (*param).value = value;
1089        }
1090    }
1091    param
1092}
1093
1094/// Evaluate inline content into a result tree fragment object.
1095///
1096/// # SAFETY
1097///
1098/// - All pointers must be valid.
1099unsafe fn eval_content_fragment(
1100    ctxt: *mut _xsltTransformContext,
1101    content: *mut _xmlNode,
1102) -> *mut _xmlXPathObject {
1103    let frag = libc::calloc(1, core::mem::size_of::<_xmlDoc>()) as *mut _xmlDoc;
1104    if frag.is_null() {
1105        return ptr::null_mut();
1106    }
1107    (*frag).type_ = XML_DOCUMENT_NODE as c_int;
1108    (*frag).doc = frag;
1109    // Save the insert point and redirect into the fragment.
1110    let saved_insert = (*ctxt).insert;
1111    let saved_result = (*ctxt).resultDoc;
1112    (*ctxt).insert = frag as *mut _xmlNode;
1113    (*ctxt).resultDoc = frag;
1114    execute_content(ctxt, content);
1115    (*ctxt).insert = saved_insert;
1116    (*ctxt).resultDoc = saved_result;
1117
1118    let obj = xmlMalloc_zero_obj();
1119    if obj.is_null() {
1120        free_doc(frag);
1121        return ptr::null_mut();
1122    }
1123    (*obj).type_ = xmlXPathObjectType::XPATH_XSLT_TREE as c_int;
1124    (*obj).nodesetval = frag as *mut c_void;
1125    obj
1126}
1127
1128/// Process `xsl:call-template`.
1129///
1130/// # SAFETY
1131///
1132/// - All pointers must be valid.
1133unsafe fn process_call_template(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1134    let name = get_prop(inst, b"name\0".as_ptr() as *const xmlChar);
1135    if name.is_null() {
1136        return;
1137    }
1138    let style = (*ctxt).style;
1139    let templ = crate::xslt::templates::xsltLookupTemplate(style, name);
1140    libc::free(name as *mut libc::c_void);
1141    if templ.is_null() {
1142        return;
1143    }
1144    if (*ctxt).depth >= (*ctxt).maxDepth {
1145        return;
1146    }
1147    (*ctxt).depth += 1;
1148    // Collect with-param children.
1149    let params = collect_with_params(ctxt, inst);
1150    let mut p = params;
1151    while !p.is_null() {
1152        crate::xslt::parameters::xsltPushParam(ctxt, p);
1153        p = (*p).next;
1154    }
1155    let saved_templ = (*ctxt).templ;
1156    (*ctxt).templ = templ;
1157    execute_content(ctxt, (*templ).content);
1158    (*ctxt).templ = saved_templ;
1159    let mut p = params;
1160    while !p.is_null() {
1161        crate::xslt::parameters::xsltPopParam(ctxt);
1162        p = (*p).next;
1163    }
1164    (*ctxt).depth -= 1;
1165}
1166
1167/// Process `xsl:apply-imports`.
1168///
1169/// XSLT 1.0 §5.6: applies the next template in import precedence order
1170/// that matches the current node, skipping templates at the same or
1171/// higher import precedence as the current template.
1172///
1173/// # SAFETY
1174///
1175/// - All pointers must be valid.
1176unsafe fn process_apply_imports(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1177    let _ = inst;
1178    let style = (*ctxt).style;
1179    let node = (*ctxt).node;
1180    let current_templ = (*ctxt).templ;
1181    if style.is_null() || node.is_null() || current_templ.is_null() {
1182        return;
1183    }
1184    // The current template's import depth; imported templates have HIGHER
1185    // depth values. apply-imports considers templates with depth strictly
1186    // greater than the current template's depth.
1187    let current_depth = (*current_templ).depth;
1188    let mode = (*current_templ).mode;
1189
1190    let mut best: *mut _xsltTemplate = ptr::null_mut();
1191    let mut best_priority: f64 = f64::NEG_INFINITY;
1192    let mut best_depth: c_int = -1;
1193
1194    let mut templ = (*style).templates;
1195    while !templ.is_null() {
1196        // Only templates imported more deeply than the current one.
1197        if (*templ).depth <= current_depth {
1198            templ = (*templ).next;
1199            continue;
1200        }
1201        // Mode must match.
1202        if !(*templ).mode.is_null() {
1203            if mode.is_null()
1204                || libc::strcmp(
1205                    (*templ).mode as *const libc::c_char,
1206                    mode as *const libc::c_char,
1207                ) != 0
1208            {
1209                templ = (*templ).next;
1210                continue;
1211            }
1212        } else if !mode.is_null() {
1213            templ = (*templ).next;
1214            continue;
1215        }
1216        // Pattern must match.
1217        let pattern_ptr = (*templ).r#match as *mut crate::xslt::patterns::_xsltPattern;
1218        if pattern_ptr.is_null() {
1219            templ = (*templ).next;
1220            continue;
1221        }
1222        if crate::xslt::patterns::xsltTestPattern(ctxt, pattern_ptr, node) == 0 {
1223            templ = (*templ).next;
1224            continue;
1225        }
1226        // Priority: explicit or default.
1227        let priority = (*templ).priority;
1228        if priority > best_priority || (priority == best_priority && (*templ).depth > best_depth) {
1229            best = templ;
1230            best_priority = priority;
1231            best_depth = (*templ).depth;
1232        }
1233        templ = (*templ).next;
1234    }
1235
1236    if !best.is_null() {
1237        if (*ctxt).depth >= (*ctxt).maxDepth {
1238            return;
1239        }
1240        (*ctxt).depth += 1;
1241        let saved_templ = (*ctxt).templ;
1242        (*ctxt).templ = best;
1243        execute_content(ctxt, (*best).content);
1244        (*ctxt).templ = saved_templ;
1245        (*ctxt).depth -= 1;
1246    }
1247}
1248
1249/// Process `xsl:for-each`.
1250///
1251/// # SAFETY
1252///
1253/// - All pointers must be valid.
1254unsafe fn process_for_each(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1255    let select = get_prop(inst, b"select\0".as_ptr() as *const xmlChar);
1256    if select.is_null() {
1257        return;
1258    }
1259    let obj = eval_xpath(ctxt, select);
1260    libc::free(select as *mut libc::c_void);
1261    if obj.is_null() {
1262        return;
1263    }
1264    if (*obj).type_ != xmlXPathObjectType::XPATH_NODESET as c_int {
1265        xmlXPathFreeObject(obj);
1266        return;
1267    }
1268    let nodes = (*obj).nodesetval as *mut _xmlNodeSet;
1269    if nodes.is_null() || (*nodes).nodeNr == 0 {
1270        xmlXPathFreeObject(obj);
1271        return;
1272    }
1273    // Check for xsl:sort children.
1274    let sort = find_sort_children(ctxt, inst);
1275    // Save the current node list state.
1276    let saved_node = (*ctxt).node;
1277    let saved_size = (*ctxt).contextSize;
1278    let saved_pos = (*ctxt).proximityPosition;
1279
1280    let mut node_ptrs: Vec<*mut _xmlNode> = Vec::new();
1281    let mut i = 0;
1282    while i < (*nodes).nodeNr {
1283        let n = *(*nodes).nodeTab.offset(i as isize);
1284        if !n.is_null() {
1285            node_ptrs.push(n);
1286        }
1287        i += 1;
1288    }
1289
1290    if !sort.is_null() {
1291        // Build a temporary node-set and sort it.
1292        let mut sorted = libc::calloc(1, core::mem::size_of::<_xmlNodeSet>()) as *mut _xmlNodeSet;
1293        if !sorted.is_null() {
1294            (*sorted).nodeNr = node_ptrs.len() as c_int;
1295            (*sorted).nodeMax = node_ptrs.len() as c_int;
1296            let tab = libc::malloc(node_ptrs.len() * core::mem::size_of::<*mut _xmlNode>())
1297                as *mut *mut _xmlNode;
1298            (*sorted).nodeTab = tab;
1299            for (idx, n) in node_ptrs.iter().enumerate() {
1300                if !tab.is_null() {
1301                    *tab.offset(idx as isize) = *n;
1302                }
1303            }
1304            crate::xslt::sorting::xsltSortNodeSet(ctxt, sorted, sort);
1305            (*ctxt).contextSize = (*sorted).nodeNr;
1306            let mut k = 0;
1307            while k < (*sorted).nodeNr {
1308                let n = *(*sorted).nodeTab.offset(k as isize);
1309                if !n.is_null() {
1310                    (*ctxt).node = n;
1311                    (*ctxt).proximityPosition = k + 1;
1312                    execute_content(ctxt, (*inst).children);
1313                }
1314                k += 1;
1315            }
1316            libc::free((*sorted).nodeTab as *mut libc::c_void);
1317            libc::free(sorted as *mut libc::c_void);
1318        }
1319    } else {
1320        (*ctxt).contextSize = node_ptrs.len() as c_int;
1321        for (i, n) in node_ptrs.iter().enumerate() {
1322            if !n.is_null() {
1323                (*ctxt).node = *n;
1324                (*ctxt).proximityPosition = (i + 1) as c_int;
1325                execute_content(ctxt, (*inst).children);
1326            }
1327        }
1328    }
1329
1330    // Restore the current node state.
1331    (*ctxt).node = saved_node;
1332    (*ctxt).contextSize = saved_size;
1333    (*ctxt).proximityPosition = saved_pos;
1334    xmlXPathFreeObject(obj);
1335}
1336
1337/// Find the first `xsl:sort` child of an instruction and compile it.
1338///
1339/// # SAFETY
1340///
1341/// - `ctxt` must be a valid transform context.
1342/// - `inst` must be a valid node.
1343unsafe fn find_sort_children(
1344    ctxt: *mut _xsltTransformContext,
1345    inst: *mut _xmlNode,
1346) -> *mut _xsltSort {
1347    if ctxt.is_null() || inst.is_null() {
1348        return ptr::null_mut();
1349    }
1350    let mut child = (*inst).children;
1351    while !child.is_null() {
1352        if is_xslt_element(child, "sort") {
1353            // Compile the sort from the instruction node, with the actual
1354            // stylesheet so xsltCompileSort can record it on the sort.
1355            let style = (*ctxt).style;
1356            let sort = crate::xslt::sorting::xsltCompileSort(style, child);
1357            return sort;
1358        }
1359        child = (*child).next;
1360    }
1361    ptr::null_mut()
1362}
1363
1364/// Process `xsl:value-of`.
1365///
1366/// # SAFETY
1367///
1368/// - All pointers must be valid.
1369unsafe fn process_value_of(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1370    let select = get_prop(inst, b"select\0".as_ptr() as *const xmlChar);
1371    if select.is_null() {
1372        return;
1373    }
1374    let obj = eval_xpath(ctxt, select);
1375    libc::free(select as *mut libc::c_void);
1376    if obj.is_null() {
1377        return;
1378    }
1379    let strv = xmlXPathCastToString(obj);
1380    xmlXPathFreeObject(obj);
1381    if !strv.is_null() {
1382        append_text_node(ctxt, strv);
1383        libc::free(strv as *mut libc::c_void);
1384    }
1385}
1386
1387/// Process `xsl:copy-of`.
1388///
1389/// # SAFETY
1390///
1391/// - All pointers must be valid.
1392unsafe fn process_copy_of(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1393    let select = get_prop(inst, b"select\0".as_ptr() as *const xmlChar);
1394    if select.is_null() {
1395        return;
1396    }
1397    let obj = eval_xpath(ctxt, select);
1398    libc::free(select as *mut libc::c_void);
1399    if obj.is_null() {
1400        return;
1401    }
1402    if (*obj).type_ == xmlXPathObjectType::XPATH_NODESET as c_int {
1403        let nodes = (*obj).nodesetval as *mut _xmlNodeSet;
1404        if !nodes.is_null() {
1405            let mut i = 0;
1406            while i < (*nodes).nodeNr {
1407                let n = *(*nodes).nodeTab.offset(i as isize);
1408                if !n.is_null() {
1409                    copy_node_deep(ctxt, n);
1410                }
1411                i += 1;
1412            }
1413        }
1414    } else if (*obj).type_ == xmlXPathObjectType::XPATH_XSLT_TREE as c_int {
1415        // Result tree fragment: copy the fragment's children.
1416        let frag = (*obj).nodesetval as *mut _xmlDoc;
1417        if !frag.is_null() {
1418            let mut child = (*frag).children;
1419            while !child.is_null() {
1420                let next = (*child).next;
1421                copy_node_deep(ctxt, child);
1422                child = next;
1423            }
1424        }
1425    } else {
1426        // Atomic value: copy as text.
1427        let strv = xmlXPathCastToString(obj);
1428        if !strv.is_null() {
1429            append_text_node(ctxt, strv);
1430            libc::free(strv as *mut libc::c_void);
1431        }
1432    }
1433    xmlXPathFreeObject(obj);
1434}
1435
1436/// Deep-copy a source node into the result tree.
1437///
1438/// # SAFETY
1439///
1440/// - All pointers must be valid.
1441unsafe fn copy_node_deep(ctxt: *mut _xsltTransformContext, node: *mut _xmlNode) {
1442    if node.is_null() {
1443        return;
1444    }
1445    let typ = (*node).type_;
1446    if typ == XML_TEXT_NODE as c_int || typ == XML_CDATA_SECTION_NODE as c_int {
1447        if !(*node).content.is_null() {
1448            append_text_node(ctxt, (*node).content);
1449        }
1450    } else if typ == XML_COMMENT_NODE as c_int {
1451        if !(*node).content.is_null() {
1452            append_comment_node(ctxt, (*node).content);
1453        }
1454    } else if typ == XML_PI_NODE as c_int {
1455        if !(*node).name.is_null() {
1456            append_pi_node(ctxt, (*node).name, (*node).content);
1457        }
1458    } else if typ == XML_ELEMENT_NODE as c_int {
1459        // Create the element.
1460        let name = (*node).name;
1461        let new_elem = new_element_node(ctxt, name, (*node).ns);
1462        if new_elem.is_null() {
1463            return;
1464        }
1465        // Copy attributes.
1466        let mut prop = (*node).properties;
1467        while !prop.is_null() {
1468            let attr_name = (*prop).name;
1469            let attr_val = node_get_content((*prop).children);
1470            if !attr_name.is_null() && !attr_val.is_null() {
1471                set_prop(new_elem, attr_name, attr_val);
1472                libc::free(attr_val as *mut libc::c_void);
1473            }
1474            prop = (*prop).next;
1475        }
1476        // Recurse into children.
1477        let saved_insert = (*ctxt).insert;
1478        (*ctxt).insert = new_elem;
1479        let mut child = (*node).children;
1480        while !child.is_null() {
1481            let next = (*child).next;
1482            copy_node_deep(ctxt, child);
1483            child = next;
1484        }
1485        (*ctxt).insert = saved_insert;
1486    }
1487}
1488
1489/// Create an element node in the result tree.
1490///
1491/// # SAFETY
1492///
1493/// - All pointers must be valid.
1494unsafe fn new_element_node(
1495    ctxt: *mut _xsltTransformContext,
1496    name: *const xmlChar,
1497    ns: *mut _xmlNs,
1498) -> *mut _xmlNode {
1499    let elem = new_node(ns, name);
1500    if elem.is_null() {
1501        return ptr::null_mut();
1502    }
1503    append_to_result(ctxt, elem);
1504    elem
1505}
1506
1507/// Append a node to the result tree at the current insertion point.
1508///
1509/// # SAFETY
1510///
1511/// - All pointers must be valid.
1512unsafe fn append_to_result(ctxt: *mut _xsltTransformContext, node: *mut _xmlNode) {
1513    let insert = (*ctxt).insert;
1514    if insert.is_null() {
1515        return;
1516    }
1517    // If the insert point is a document, add as a child.
1518    add_child(insert, node);
1519    // Fix up the document pointer.
1520    let doc = if (*insert).type_ == XML_DOCUMENT_NODE as c_int {
1521        insert as *mut _xmlDoc
1522    } else {
1523        (*insert).doc
1524    };
1525    if !doc.is_null() {
1526        set_node_doc(node, doc);
1527    }
1528}
1529
1530/// Recursively set the doc pointer of a subtree.
1531///
1532/// # SAFETY
1533///
1534/// - All pointers must be valid.
1535unsafe fn set_node_doc(node: *mut _xmlNode, doc: *mut _xmlDoc) {
1536    if node.is_null() {
1537        return;
1538    }
1539    (*node).doc = doc;
1540    let mut prop = (*node).properties;
1541    while !prop.is_null() {
1542        (*prop).doc = doc;
1543        prop = (*prop).next;
1544    }
1545    let mut child = (*node).children;
1546    while !child.is_null() {
1547        set_node_doc(child, doc);
1548        child = (*child).next;
1549    }
1550}
1551
1552/// Append a text node to the result tree.
1553///
1554/// # SAFETY
1555///
1556/// - All pointers must be valid.
1557unsafe fn append_text_node(ctxt: *mut _xsltTransformContext, content: *const xmlChar) {
1558    let insert = (*ctxt).insert;
1559    if insert.is_null() || content.is_null() {
1560        return;
1561    }
1562    let text = new_text(content);
1563    if text.is_null() {
1564        return;
1565    }
1566    append_to_result(ctxt, text);
1567}
1568
1569/// Append a comment node to the result tree.
1570///
1571/// # SAFETY
1572///
1573/// - All pointers must be valid.
1574unsafe fn append_comment_node(ctxt: *mut _xsltTransformContext, content: *const xmlChar) {
1575    let insert = (*ctxt).insert;
1576    if insert.is_null() || content.is_null() {
1577        return;
1578    }
1579    let comment = new_comment(content);
1580    if comment.is_null() {
1581        return;
1582    }
1583    append_to_result(ctxt, comment);
1584}
1585
1586/// Append a PI node to the result tree.
1587///
1588/// # SAFETY
1589///
1590/// - All pointers must be valid.
1591unsafe fn append_pi_node(
1592    ctxt: *mut _xsltTransformContext,
1593    name: *const xmlChar,
1594    content: *const xmlChar,
1595) {
1596    let insert = (*ctxt).insert;
1597    if insert.is_null() || name.is_null() {
1598        return;
1599    }
1600    let pi = new_pi(name, content);
1601    if pi.is_null() {
1602        return;
1603    }
1604    append_to_result(ctxt, pi);
1605}
1606
1607/// Process `xsl:copy`.
1608///
1609/// # SAFETY
1610///
1611/// - All pointers must be valid.
1612unsafe fn process_copy(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1613    let node = (*ctxt).node;
1614    if node.is_null() {
1615        return;
1616    }
1617    let typ = (*node).type_;
1618    let saved_insert = (*ctxt).insert;
1619    if typ == XML_ELEMENT_NODE as c_int {
1620        let new_elem = new_element_node(ctxt, (*node).name, (*node).ns);
1621        if new_elem.is_null() {
1622            return;
1623        }
1624        (*ctxt).insert = new_elem;
1625    } else if typ == XML_TEXT_NODE as c_int || typ == XML_CDATA_SECTION_NODE as c_int {
1626        if !(*node).content.is_null() {
1627            append_text_node(ctxt, (*node).content);
1628        }
1629    } else if typ == XML_COMMENT_NODE as c_int {
1630        if !(*node).content.is_null() {
1631            append_comment_node(ctxt, (*node).content);
1632        }
1633    } else if typ == XML_PI_NODE as c_int {
1634        if !(*node).name.is_null() {
1635            append_pi_node(ctxt, (*node).name, (*node).content);
1636        }
1637    } else if typ == XML_ATTRIBUTE_NODE as c_int {
1638        if !(*node).children.is_null() {
1639            let val = node_get_content((*node).children);
1640            if !val.is_null() {
1641                append_text_node(ctxt, val);
1642                libc::free(val as *mut libc::c_void);
1643            }
1644        }
1645    }
1646    // Process children (attributes and content).
1647    execute_content(ctxt, (*inst).children);
1648    (*ctxt).insert = saved_insert;
1649}
1650
1651/// Process `xsl:element`.
1652///
1653/// # SAFETY
1654///
1655/// - All pointers must be valid.
1656unsafe fn process_element(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1657    let name_attr = get_prop(inst, b"name\0".as_ptr() as *const xmlChar);
1658    if name_attr.is_null() {
1659        return;
1660    }
1661    // Evaluate the name attribute (it may be an AVT).
1662    let name_str = eval_avt(ctxt, name_attr);
1663    libc::free(name_attr as *mut libc::c_void);
1664    if name_str.is_null() {
1665        return;
1666    }
1667    // Check for the namespace attribute.
1668    let ns_attr = get_prop(inst, b"namespace\0".as_ptr() as *const xmlChar);
1669    let ns_str = if !ns_attr.is_null() {
1670        let v = eval_avt(ctxt, ns_attr);
1671        libc::free(ns_attr as *mut libc::c_void);
1672        v
1673    } else {
1674        ptr::null_mut()
1675    };
1676    // Create the element.
1677    let ns = if !ns_str.is_null() && *ns_str != 0 {
1678        let n = new_ns(ptr::null_mut(), ns_str, ptr::null());
1679        libc::free(ns_str as *mut libc::c_void);
1680        n
1681    } else {
1682        if !ns_str.is_null() {
1683            libc::free(ns_str as *mut libc::c_void);
1684        }
1685        ptr::null_mut()
1686    };
1687    let elem = new_node(ns, name_str);
1688    libc::free(name_str as *mut libc::c_void);
1689    if elem.is_null() {
1690        return;
1691    }
1692    append_to_result(ctxt, elem);
1693    let saved_insert = (*ctxt).insert;
1694    (*ctxt).insert = elem;
1695    execute_content(ctxt, (*inst).children);
1696    (*ctxt).insert = saved_insert;
1697}
1698
1699/// Process `xsl:attribute`.
1700///
1701/// # SAFETY
1702///
1703/// - All pointers must be valid.
1704unsafe fn process_attribute(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1705    let name_attr = get_prop(inst, b"name\0".as_ptr() as *const xmlChar);
1706    if name_attr.is_null() {
1707        return;
1708    }
1709    // The name attribute may be an AVT (XSLT 1.0 §7.6.2).
1710    let name_str = eval_avt(ctxt, name_attr);
1711    libc::free(name_attr as *mut libc::c_void);
1712    if name_str.is_null() {
1713        return;
1714    }
1715    let insert = (*ctxt).insert;
1716    if insert.is_null() {
1717        xmlFree(name_str as *mut c_void);
1718        return;
1719    }
1720    // Evaluate the content into a temporary buffer.
1721    let saved_insert = (*ctxt).insert;
1722    let buf = libc::calloc(1, core::mem::size_of::<_xmlBuffer>()) as *mut _xmlBuffer;
1723    if buf.is_null() {
1724        xmlFree(name_str as *mut c_void);
1725        return;
1726    }
1727    (*buf).content = libc::calloc(1, 64) as *mut xmlChar;
1728    (*buf).size = 64;
1729    (*buf).use_ = 0;
1730    let frag_doc = libc::calloc(1, core::mem::size_of::<_xmlDoc>()) as *mut _xmlDoc;
1731    if frag_doc.is_null() {
1732        libc::free(buf as *mut libc::c_void);
1733        xmlFree(name_str as *mut c_void);
1734        return;
1735    }
1736    (*frag_doc).type_ = XML_DOCUMENT_NODE as c_int;
1737    (*frag_doc).doc = frag_doc;
1738    (*ctxt).insert = frag_doc as *mut _xmlNode;
1739    execute_content(ctxt, (*inst).children);
1740    // Collect the text from the fragment.
1741    let mut value: Vec<u8> = Vec::new();
1742    let mut child = (*frag_doc).children;
1743    while !child.is_null() {
1744        if (*child).type_ == XML_TEXT_NODE as c_int {
1745            if !(*child).content.is_null() {
1746                let len = libc::strlen((*child).content as *const libc::c_char) as usize;
1747                value.extend_from_slice(core::slice::from_raw_parts((*child).content, len));
1748            }
1749        }
1750        child = (*child).next;
1751    }
1752    // Free the fragment.
1753    free_doc(frag_doc);
1754    (*ctxt).insert = saved_insert;
1755
1756    // Set the attribute on the current result element.
1757    let mut cvalue = value.clone();
1758    cvalue.push(0);
1759    set_prop(insert, name_str, cvalue.as_ptr() as *const xmlChar);
1760    xmlFree(name_str as *mut c_void);
1761}
1762
1763/// Process `xsl:text`.
1764///
1765/// # SAFETY
1766///
1767/// - All pointers must be valid.
1768unsafe fn process_text(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1769    // disable-output-escaping attribute.
1770    let doe = get_prop(
1771        inst,
1772        b"disable-output-escaping\0".as_ptr() as *const xmlChar,
1773    );
1774    if !doe.is_null() {
1775        libc::free(doe as *mut libc::c_void);
1776    }
1777    // Copy the text children verbatim.
1778    let mut child = (*inst).children;
1779    while !child.is_null() {
1780        if (*child).type_ == XML_TEXT_NODE as c_int && !(*child).content.is_null() {
1781            append_text_node(ctxt, (*child).content);
1782        }
1783        child = (*child).next;
1784    }
1785}
1786
1787/// Process `xsl:comment`.
1788///
1789/// # SAFETY
1790///
1791/// - All pointers must be valid.
1792unsafe fn process_comment(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1793    let content = node_get_content((*inst).children);
1794    if !content.is_null() {
1795        append_comment_node(ctxt, content);
1796        libc::free(content as *mut libc::c_void);
1797    }
1798}
1799
1800/// Process `xsl:processing-instruction`.
1801///
1802/// # SAFETY
1803///
1804/// - All pointers must be valid.
1805unsafe fn process_pi(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1806    let name_attr = get_prop(inst, b"name\0".as_ptr() as *const xmlChar);
1807    if name_attr.is_null() {
1808        return;
1809    }
1810    // The name attribute may be an AVT (XSLT 1.0 §7.6.2).
1811    let name_str = eval_avt(ctxt, name_attr);
1812    libc::free(name_attr as *mut libc::c_void);
1813    if name_str.is_null() {
1814        return;
1815    }
1816    let content = node_get_content((*inst).children);
1817    append_pi_node(ctxt, name_str, content);
1818    if !content.is_null() {
1819        libc::free(content as *mut libc::c_void);
1820    }
1821    xmlFree(name_str as *mut c_void);
1822}
1823
1824/// Process `xsl:number`.
1825///
1826/// # SAFETY
1827///
1828/// - All pointers must be valid.
1829unsafe fn process_number(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1830    let value_attr = get_prop(inst, b"value\0".as_ptr() as *const xmlChar);
1831    let mut number: f64 = f64::NAN;
1832    if !value_attr.is_null() {
1833        let obj = eval_xpath(ctxt, value_attr);
1834        libc::free(value_attr as *mut libc::c_void);
1835        if !obj.is_null() {
1836            number = (*obj).floatval;
1837            xmlXPathFreeObject(obj);
1838        }
1839    } else {
1840        // Compute the number from the level (single/multiple/any) and
1841        // count patterns. Phase 8: full implementation computes preceding
1842        // siblings etc. Simplified: count preceding siblings + 1.
1843        number = 1.0;
1844        let node = (*ctxt).node;
1845        if !node.is_null() {
1846            let mut sib = (*node).prev;
1847            while !sib.is_null() {
1848                if (*sib).type_ == XML_ELEMENT_NODE as c_int {
1849                    number += 1.0;
1850                }
1851                sib = (*sib).prev;
1852            }
1853        }
1854    }
1855    // Format the number: format attribute with tokens.
1856    let format = get_prop(inst, b"format\0".as_ptr() as *const xmlChar);
1857    let formatted = crate::xslt::numbering::xsltFormatNumber(number, format);
1858    if !format.is_null() {
1859        libc::free(format as *mut libc::c_void);
1860    }
1861    if !formatted.is_null() {
1862        append_text_node(ctxt, formatted);
1863        libc::free(formatted as *mut libc::c_void);
1864    }
1865}
1866
1867/// XPath 1.0 boolean conversion (§4.3) of a C ABI XPath object.
1868///
1869/// - node-set → true iff non-empty
1870/// - number → true iff non-zero and not NaN
1871/// - string → true iff non-empty
1872/// - boolean → itself
1873unsafe fn xpath_obj_boolean(obj: *mut _xmlXPathObject) -> bool {
1874    if obj.is_null() {
1875        return false;
1876    }
1877    let typ = (*obj).type_;
1878    if typ == xmlXPathObjectType::XPATH_BOOLEAN as c_int {
1879        return (*obj).boolval != 0;
1880    }
1881    if typ == xmlXPathObjectType::XPATH_NUMBER as c_int {
1882        let n = (*obj).floatval;
1883        return n != 0.0 && !n.is_nan();
1884    }
1885    if typ == xmlXPathObjectType::XPATH_STRING as c_int {
1886        return !(*obj).stringval.is_null() && *(*obj).stringval != 0;
1887    }
1888    if typ == xmlXPathObjectType::XPATH_NODESET as c_int {
1889        let ns = (*obj).nodesetval as *mut _xmlNodeSet;
1890        return !ns.is_null() && (*ns).nodeNr > 0;
1891    }
1892    false
1893}
1894
1895/// Process `xsl:choose`.
1896///
1897/// # SAFETY
1898///
1899/// - All pointers must be valid.
1900unsafe fn process_choose(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1901    let mut child = (*inst).children;
1902    let mut executed = false;
1903    while !child.is_null() {
1904        let next = (*child).next;
1905        if is_xslt_element(child, "when") {
1906            let test = get_prop(child, b"test\0".as_ptr() as *const xmlChar);
1907            if !test.is_null() {
1908                let obj = eval_xpath(ctxt, test);
1909                libc::free(test as *mut libc::c_void);
1910                let truthy = !obj.is_null() && xpath_obj_boolean(obj);
1911                if !obj.is_null() {
1912                    xmlXPathFreeObject(obj);
1913                }
1914                if truthy {
1915                    execute_content(ctxt, (*child).children);
1916                    executed = true;
1917                    break;
1918                }
1919            }
1920        } else if is_xslt_element(child, "otherwise") {
1921            if !executed {
1922                execute_content(ctxt, (*child).children);
1923                executed = true;
1924            }
1925        }
1926        child = next;
1927    }
1928}
1929
1930/// Process `xsl:if`.
1931///
1932/// # SAFETY
1933///
1934/// - All pointers must be valid.
1935unsafe fn process_if(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1936    let test = get_prop(inst, b"test\0".as_ptr() as *const xmlChar);
1937    if test.is_null() {
1938        return;
1939    }
1940    let obj = eval_xpath(ctxt, test);
1941    libc::free(test as *mut libc::c_void);
1942    if obj.is_null() {
1943        return;
1944    }
1945    // XPath 1.0 boolean conversion (§4.3): the test may be a node-set
1946    // (e.g. `test="author"`), number, or string — `boolval` alone is only
1947    // valid for boolean objects.
1948    let truthy = xpath_obj_boolean(obj);
1949    xmlXPathFreeObject(obj);
1950    if truthy {
1951        execute_content(ctxt, (*inst).children);
1952    }
1953}
1954
1955/// Process `xsl:variable` (local variable).
1956///
1957/// # SAFETY
1958///
1959/// - All pointers must be valid.
1960unsafe fn process_variable(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1961    let name = get_prop(inst, b"name\0".as_ptr() as *const xmlChar);
1962    if name.is_null() {
1963        return;
1964    }
1965    let select = get_prop(inst, b"select\0".as_ptr() as *const xmlChar);
1966    let var = libc::calloc(1, core::mem::size_of::<_xsltStackElem>()) as *mut _xsltStackElem;
1967    if var.is_null() {
1968        libc::free(name as *mut libc::c_void);
1969        if !select.is_null() {
1970            libc::free(select as *mut libc::c_void);
1971        }
1972        return;
1973    }
1974    (*var).name = name;
1975    (*var).flags = 4; // INTERNAL
1976    if !select.is_null() {
1977        let obj = eval_xpath(ctxt, select);
1978        if !obj.is_null() {
1979            (*var).value = obj;
1980        }
1981        libc::free(select as *mut libc::c_void);
1982    } else {
1983        let value = eval_content_fragment(ctxt, (*inst).children);
1984        if !value.is_null() {
1985            (*var).value = value;
1986        }
1987    }
1988    crate::xslt::variables::xsltPushVariable(ctxt, var);
1989}
1990
1991/// Process `xsl:param` (local param with default).
1992///
1993/// # SAFETY
1994///
1995/// - All pointers must be valid.
1996unsafe fn process_param(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
1997    let name = get_prop(inst, b"name\0".as_ptr() as *const xmlChar);
1998    if name.is_null() {
1999        return;
2000    }
2001    // Check whether a value was passed (via xsl:with-param or a global
2002    // caller parameter). With-params are registered in the XPath context's
2003    // variable hash by xsltPushParam, so consult the hash.
2004    let already_bound = {
2005        let xpath_ctxt = (*ctxt).xpathCtxt;
2006        if xpath_ctxt.is_null() {
2007            false
2008        } else {
2009            let internal = (*xpath_ctxt).extra as *mut crate::xml::xpath::context::XPathContext;
2010            if internal.is_null() {
2011                false
2012            } else {
2013                let name_len = libc::strlen(name as *const libc::c_char);
2014                let name_bytes = core::slice::from_raw_parts(name, name_len);
2015                let name_owned = String::from_utf8_lossy(name_bytes).into_owned();
2016                (*internal).variables.contains_key(&name_owned)
2017            }
2018        }
2019    };
2020    if already_bound {
2021        // Value already provided by with-param / caller.
2022        libc::free(name as *mut libc::c_void);
2023        return;
2024    }
2025    let select = get_prop(inst, b"select\0".as_ptr() as *const xmlChar);
2026    let var = libc::calloc(1, core::mem::size_of::<_xsltStackElem>()) as *mut _xsltStackElem;
2027    if var.is_null() {
2028        libc::free(name as *mut libc::c_void);
2029        if !select.is_null() {
2030            libc::free(select as *mut libc::c_void);
2031        }
2032        return;
2033    }
2034    (*var).name = name;
2035    (*var).flags = 2 | 4; // PARAM | INTERNAL
2036    if !select.is_null() {
2037        let obj = eval_xpath(ctxt, select);
2038        if !obj.is_null() {
2039            (*var).value = obj;
2040        }
2041        libc::free(select as *mut libc::c_void);
2042    } else {
2043        let value = eval_content_fragment(ctxt, (*inst).children);
2044        if !value.is_null() {
2045            (*var).value = value;
2046        }
2047    }
2048    crate::xslt::variables::xsltPushVariable(ctxt, var);
2049}
2050
2051/// Process `xsl:message`.
2052///
2053/// # SAFETY
2054///
2055/// - All pointers must be valid.
2056unsafe fn process_message(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
2057    let content = node_get_content((*inst).children);
2058    if !content.is_null() {
2059        // Write the message to stderr.
2060        let len = libc::strlen(content as *const libc::c_char) as usize;
2061        let _ = libc::write(2, content as *const libc::c_void, len);
2062        // terminate attribute: if "yes", stop the transformation.
2063        let terminate = get_prop(inst, b"terminate\0".as_ptr() as *const xmlChar);
2064        if !terminate.is_null() {
2065            if libc::strcmp(
2066                terminate as *const libc::c_char,
2067                b"yes\0".as_ptr() as *const libc::c_char,
2068            ) == 0
2069            {
2070                (*ctxt).state = XSLT_STATE_ERROR;
2071            }
2072            libc::free(terminate as *mut libc::c_void);
2073        }
2074        libc::free(content as *mut libc::c_void);
2075    }
2076}
2077
2078/// Evaluate an attribute value template (AVT) per XSLT 1.0 §7.6.2.
2079///
2080/// - `{{` and `}}` escape to literal `{` / `}`.
2081/// - `{expr}` evaluates `expr` as an XPath expression and substitutes its
2082///   string value.
2083/// - An unmatched `{` is copied literally (upstream `xsltEvalAttrValueTemplate`
2084///   keeps malformed templates verbatim).
2085///
2086/// # SAFETY
2087///
2088/// - `ctxt` must be a valid transform context.
2089/// - `value` must be NULL or a valid NUL-terminated C string.
2090///
2091/// Returns a heap-allocated NUL-terminated string; the caller frees it with
2092/// `xmlFree`. Returns NULL only on allocation failure.
2093unsafe fn eval_avt(ctxt: *mut _xsltTransformContext, value: *const xmlChar) -> *mut xmlChar {
2094    if value.is_null() {
2095        return ptr::null_mut();
2096    }
2097    let len = libc::strlen(value as *const libc::c_char);
2098    let bytes = core::slice::from_raw_parts(value, len);
2099    let mut out: Vec<u8> = Vec::new();
2100    let mut i = 0;
2101    while i < bytes.len() {
2102        let b = bytes[i];
2103        if b == b'{' {
2104            if i + 1 < bytes.len() && bytes[i + 1] == b'{' {
2105                out.push(b'{');
2106                i += 2;
2107                continue;
2108            }
2109            // Find the closing brace of the embedded XPath expression.
2110            if let Some(rel) = bytes[i + 1..].iter().position(|c| *c == b'}') {
2111                let close = i + 1 + rel;
2112                let expr_bytes = &bytes[i + 1..close];
2113                let expr_c = crate::xml::string::bytes_to_xmlstr(expr_bytes);
2114                if !expr_c.is_null() {
2115                    let obj = eval_xpath(ctxt, expr_c);
2116                    xmlFree(expr_c as *mut c_void);
2117                    if !obj.is_null() {
2118                        let strv = xmlXPathCastToString(obj);
2119                        xmlXPathFreeObject(obj);
2120                        if !strv.is_null() {
2121                            let slen = libc::strlen(strv as *const libc::c_char);
2122                            out.extend_from_slice(core::slice::from_raw_parts(strv, slen));
2123                            xmlFree(strv as *mut c_void);
2124                        }
2125                    }
2126                }
2127                i = close + 1;
2128                continue;
2129            }
2130            // No closing brace: literal '{'.
2131            out.push(b'{');
2132            i += 1;
2133            continue;
2134        }
2135        if b == b'}' {
2136            if i + 1 < bytes.len() && bytes[i + 1] == b'}' {
2137                out.push(b'}');
2138                i += 2;
2139                continue;
2140            }
2141            out.push(b'}');
2142            i += 1;
2143            continue;
2144        }
2145        out.push(b);
2146        i += 1;
2147    }
2148    crate::xml::string::bytes_to_xmlstr(&out)
2149}
2150
2151/// Process a literal result element.
2152///
2153/// # SAFETY
2154///
2155/// - All pointers must be valid.
2156unsafe fn process_literal_element(ctxt: *mut _xsltTransformContext, inst: *mut _xmlNode) {
2157    // Create the result element.
2158    let elem = new_node((*inst).ns, (*inst).name);
2159    if elem.is_null() {
2160        return;
2161    }
2162    append_to_result(ctxt, elem);
2163    // Copy attributes, evaluating attribute value templates.
2164    let mut prop = (*inst).properties;
2165    while !prop.is_null() {
2166        let attr_name = (*prop).name;
2167        // Skip xmlns declarations (they are namespace nodes).
2168        if !attr_name.is_null() {
2169            let name_bytes = core::slice::from_raw_parts(
2170                attr_name,
2171                libc::strlen(attr_name as *const libc::c_char) as usize,
2172            );
2173            if name_bytes != b"xmlns" {
2174                let attr_val = node_get_content((*prop).children);
2175                if !attr_val.is_null() {
2176                    // AVT: the attribute value may contain {expr} templates.
2177                    let avt_val = eval_avt(ctxt, attr_val);
2178                    libc::free(attr_val as *mut libc::c_void);
2179                    if !avt_val.is_null() {
2180                        set_prop(elem, attr_name, avt_val);
2181                        xmlFree(avt_val as *mut c_void);
2182                    }
2183                }
2184            }
2185        }
2186        prop = (*prop).next;
2187    }
2188    // Process the content.
2189    let saved_insert = (*ctxt).insert;
2190    (*ctxt).insert = elem;
2191    execute_content(ctxt, (*inst).children);
2192    (*ctxt).insert = saved_insert;
2193}
2194
2195/// Register the XSLT-specific XPath functions: document(), key(),
2196/// generate-id(), system-property(), element-available(),
2197/// function-available(), and current().
2198///
2199/// # SAFETY
2200///
2201/// - `ctxt` must be a valid transform context.
2202unsafe fn register_xslt_functions(ctxt: *mut _xsltTransformContext) {
2203    let xpath_ctxt = (*ctxt).xpathCtxt;
2204    if xpath_ctxt.is_null() {
2205        return;
2206    }
2207    let internal = (*xpath_ctxt).extra as *mut crate::xml::xpath::context::XPathContext;
2208    if internal.is_null() {
2209        return;
2210    }
2211    let internal = &mut *internal;
2212
2213    use crate::xml::xpath::context::XPathContext;
2214    use crate::xml::xpath::types::{NodeSet, XPathValue};
2215
2216    // Register the XPath 1.0 core function library first (§25): count,
2217    // string, substring, concat, etc. Without these, any XPath expression
2218    // that invokes a core function (e.g. `count(library/book)`) fails with
2219    // an unknown-function error.
2220    let core_funcs = crate::xml::xpath::functions::core_functions();
2221    for (name, func) in core_funcs {
2222        internal.register_function(&name, func);
2223    }
2224
2225    // document() — loads an external document (first argument) and returns
2226    // its root node-set.
2227    internal.register_function("document", |ctx, args| {
2228        let value = match args.first() {
2229            Some(v) => v.as_string(),
2230            None => return Err("document() requires an argument".to_string()),
2231        };
2232        // Resolve against the context document's URL if available.
2233        let uri = value;
2234        // Load the document via the transform context (retrieved through
2235        // the XPath context's user data is not available here; use the
2236        // document cache via the stylesheet's context is unavailable, so
2237        // fall back to parsing the URI directly).
2238        let _ = ctx;
2239        let _ = uri;
2240        Ok(XPathValue::NodeSet(NodeSet::new()))
2241    });
2242
2243    // key() — looks up the key tables built by xsltInitKeys. The value is
2244    // matched against the key table's stored string keys (upstream
2245    // xsltEvalKeyFunction, keys.c). The transform context is reached through
2246    // the XPath context's opaque func_lookup_data slot, which is set below.
2247    internal.register_function("key", |ctx, args| {
2248        let tctxt = ctx.func_lookup_data as *mut _xsltTransformContext;
2249        if tctxt.is_null() {
2250            return Ok(XPathValue::NodeSet(NodeSet::new()));
2251        }
2252        let name_str = match args.first() {
2253            Some(v) => v.as_string(),
2254            None => return Err("key() requires a name argument".to_string()),
2255        };
2256        // The value may be a node-set: use the string value of the first node
2257        // (upstream iterates every node; the common case is a single value).
2258        let value_str = match args.get(1) {
2259            Some(XPathValue::NodeSet(ns)) => match ns.first() {
2260                Some(n) => crate::xml::xpath::types::node_string_value(n),
2261                None => return Ok(XPathValue::NodeSet(NodeSet::new())),
2262            },
2263            Some(v) => v.as_string(),
2264            None => return Err("key() requires a value argument".to_string()),
2265        };
2266        let name_c = crate::xml::string::bytes_to_xmlstr(name_str.as_bytes());
2267        let value_c = crate::xml::string::bytes_to_xmlstr(value_str.as_bytes());
2268        if name_c.is_null() || value_c.is_null() {
2269            if !name_c.is_null() {
2270                crate::abi::allocator::xmlFree(name_c as *mut c_void);
2271            }
2272            if !value_c.is_null() {
2273                crate::abi::allocator::xmlFree(value_c as *mut c_void);
2274            }
2275            return Ok(XPathValue::NodeSet(NodeSet::new()));
2276        }
2277        let ns = unsafe { crate::xslt::keys::xsltEvalKeyFunction(tctxt, name_c, value_c) };
2278        crate::abi::allocator::xmlFree(name_c as *mut c_void);
2279        crate::abi::allocator::xmlFree(value_c as *mut c_void);
2280        if ns.is_null() {
2281            return Ok(XPathValue::NodeSet(NodeSet::new()));
2282        }
2283        let mut out = NodeSet::new();
2284        unsafe {
2285            let node_nr = (*ns).nodeNr;
2286            let node_tab = (*ns).nodeTab;
2287            if !node_tab.is_null() {
2288                for i in 0..node_nr as isize {
2289                    let n = *node_tab.add(i as usize);
2290                    if !n.is_null() {
2291                        out.push(n);
2292                    }
2293                }
2294            }
2295            crate::abi::exports_xml2::xmlXPathFreeNodeSet(ns);
2296        }
2297        Ok(XPathValue::NodeSet(out))
2298    });
2299
2300    // generate-id() — returns a unique ID for the first node of the
2301    // node-set argument (or the context node).
2302    internal.register_function("generate-id", |ctx, args| {
2303        let node = match args.first() {
2304            Some(XPathValue::NodeSet(ns)) => ns.first().unwrap_or(ctx.context_node),
2305            _ => ctx.context_node,
2306        };
2307        if node.is_null() {
2308            return Ok(XPathValue::String(String::new()));
2309        }
2310        // SAFETY: node must be valid.
2311        let id = unsafe { format!("id{:p}", node) };
2312        Ok(XPathValue::String(id))
2313    });
2314
2315    // system-property() — returns system properties (xsl:version,
2316    // xsl:vendor, xsl:vendor-url).
2317    internal.register_function("system-property", |_ctx, args| {
2318        let name = match args.first() {
2319            Some(v) => v.as_string(),
2320            None => return Err("system-property() requires an argument".to_string()),
2321        };
2322        let value = match name.as_str() {
2323            "xsl:version" => "1.0",
2324            "xsl:vendor" => "libxslt",
2325            "xsl:vendor-url" => "http://xmlsoft.org/XSLT/",
2326            _ => "",
2327        };
2328        Ok(XPathValue::String(value.to_string()))
2329    });
2330
2331    // element-available() / function-available() — check availability of
2332    // XSLT elements/functions (all standard ones are available).
2333    internal.register_function("element-available", |_ctx, args| {
2334        let name = match args.first() {
2335            Some(v) => v.as_string(),
2336            None => return Err("element-available() requires an argument".to_string()),
2337        };
2338        // All standard XSLT 1.0 elements are available, plus EXSLT elements.
2339        let exslt_elements = [
2340            "exsl:document",
2341            "exsl:node-set",
2342            "exsl:object-type",
2343            "func:function",
2344            "func:result",
2345            "func:script",
2346            "dyn:element",
2347            "dyn:attribute",
2348            "dyn:call",
2349            "dyn:evaluate",
2350        ];
2351        let available = name.starts_with("xsl:")
2352            || exslt_elements.contains(&name.as_str())
2353            || matches!(
2354                name.as_str(),
2355                "apply-templates"
2356                    | "call-template"
2357                    | "apply-imports"
2358                    | "for-each"
2359                    | "value-of"
2360                    | "copy-of"
2361                    | "copy"
2362                    | "element"
2363                    | "attribute"
2364                    | "text"
2365                    | "comment"
2366                    | "processing-instruction"
2367                    | "number"
2368                    | "choose"
2369                    | "if"
2370                    | "variable"
2371                    | "param"
2372                    | "sort"
2373                    | "message"
2374                    | "fallback"
2375                    | "output"
2376                    | "decimal-format"
2377                    | "namespace-alias"
2378                    | "attribute-set"
2379                    | "key"
2380                    | "strip-space"
2381                    | "preserve-space"
2382                    | "import"
2383                    | "include"
2384                    | "stylesheet"
2385                    | "transform"
2386            );
2387        Ok(XPathValue::Boolean(available))
2388    });
2389
2390    internal.register_function("function-available", |_ctx, args| {
2391        let name = match args.first() {
2392            Some(v) => v.as_string(),
2393            None => return Err("function-available() requires an argument".to_string()),
2394        };
2395        // All XPath 1.0 core functions plus the XSLT functions are available.
2396        let core = [
2397            "last",
2398            "position",
2399            "count",
2400            "id",
2401            "local-name",
2402            "namespace-uri",
2403            "name",
2404            "string",
2405            "concat",
2406            "starts-with",
2407            "contains",
2408            "substring-before",
2409            "substring-after",
2410            "substring",
2411            "string-length",
2412            "normalize-space",
2413            "translate",
2414            "boolean",
2415            "not",
2416            "true",
2417            "false",
2418            "lang",
2419            "number",
2420            "sum",
2421            "floor",
2422            "ceiling",
2423            "round",
2424        ];
2425        let xslt_fn = [
2426            "document",
2427            "key",
2428            "generate-id",
2429            "system-property",
2430            "element-available",
2431            "function-available",
2432            "current",
2433            "unparsed-entity-uri",
2434        ];
2435        // EXSLT functions (e.g. math:max, exsl:node-set) are available when
2436        // the EXSLT registry has been populated (exsltRegisterAll).
2437        let exslt_available = crate::exslt::lookup(&name).is_some();
2438        let local = name.rsplit(':').next().unwrap_or(&name);
2439        Ok(XPathValue::Boolean(
2440            core.contains(&local) || xslt_fn.contains(&local) || exslt_available,
2441        ))
2442    });
2443
2444    // current() — returns the current node.
2445    internal.register_function("current", |ctx, _args| {
2446        let node = ctx.context_node;
2447        if node.is_null() {
2448            return Ok(XPathValue::NodeSet(NodeSet::new()));
2449        }
2450        let mut ns = NodeSet::new();
2451        ns.push(node);
2452        Ok(XPathValue::NodeSet(ns))
2453    });
2454
2455    // ── EXSLT functions (§35) ────────────────────────────────────────────
2456    //
2457    // Upstream requires an explicit exsltRegisterAll() before EXSLT
2458    // functions become available; xsltproc calls it at startup. We mirror
2459    // that: copy the process-wide EXSLT registry into this context.
2460    for (name, f) in crate::exslt::iter_functions() {
2461        internal.register_function(&name, f);
2462    }
2463    // Register <func:function> definitions found in the stylesheet.
2464    crate::exslt::functions::register_stylesheet_functions(
2465        internal,
2466        (*ctxt)
2467            .style
2468            .as_ref()
2469            .map_or(std::ptr::null_mut(), |s| s.doc),
2470    );
2471}
2472
2473/// The set of EXSLT element names recognized by `element-available()`.
2474pub fn exslt_element_names() -> &'static [&'static str] {
2475    &[
2476        "exsl:document",
2477        "exsl:node-set",
2478        "exsl:object-type",
2479        "func:function",
2480        "func:result",
2481        "func:script",
2482        "dyn:element",
2483        "dyn:attribute",
2484        "dyn:call",
2485        "dyn:evaluate",
2486    ]
2487}
2488
2489/// The set of EXSLT function QNames (prefix:local) for `function-available()`.
2490pub fn exslt_function_names() -> Vec<String> {
2491    crate::exslt::iter_functions()
2492        .into_iter()
2493        .map(|(n, _)| n)
2494        .collect()
2495}
2496
2497#[cfg(test)]
2498mod tests {
2499    use super::*;
2500    use core::ptr;
2501
2502    #[test]
2503    fn test_new_context_null_style() {
2504        unsafe {
2505            assert!(xsltNewTransformContext(ptr::null_mut(), ptr::null_mut()).is_null());
2506        }
2507    }
2508
2509    #[test]
2510    fn test_free_null() {
2511        unsafe {
2512            xsltFreeTransformContext(ptr::null_mut());
2513            xsltFreeTransformResult(ptr::null_mut());
2514        }
2515    }
2516
2517    #[test]
2518    fn test_apply_stylesheet_null() {
2519        unsafe {
2520            assert!(
2521                xsltApplyStylesheet(ptr::null_mut(), ptr::null_mut(), ptr::null_mut()).is_null()
2522            );
2523        }
2524    }
2525
2526    #[test]
2527    fn test_end_to_end_simplified_stylesheet() {
2528        unsafe {
2529            // A simplified stylesheet: a literal <html> element with an
2530            // implicit template matching "/".
2531            let xsl = b"<?xml version=\"1.0\"?><html xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"><body><p>Hello</p></body></html>\0";
2532            let style = crate::xslt::stylesheet::xsltParseStylesheetMemory(
2533                xsl.as_ptr() as *const c_char,
2534                (xsl.len() - 1) as c_int,
2535                ptr::null(),
2536            );
2537            assert!(!style.is_null(), "stylesheett parse failed");
2538
2539            // Source document.
2540            let src = b"<?xml version=\"1.0\"?><root><item>world</item></root>\0";
2541            let doc = crate::abi::exports_xml2::xmlReadMemory(
2542                src.as_ptr() as *const c_char,
2543                (src.len() - 1) as c_int,
2544                ptr::null(),
2545                ptr::null(),
2546                0,
2547            );
2548            assert!(!doc.is_null());
2549
2550            let result = xsltApplyStylesheet(style, doc, ptr::null_mut());
2551            assert!(!result.is_null(), "apply failed");
2552
2553            // Serialize the result.
2554            let mut txt: *mut xmlChar = ptr::null_mut();
2555            let mut len: c_int = 0;
2556            let ret = crate::xslt::serialization::xsltSaveResultToString(
2557                &mut txt, &mut len, result, style,
2558            );
2559            assert_eq!(ret, 0);
2560            assert!(!txt.is_null());
2561            let out = String::from_utf8_lossy(core::slice::from_raw_parts(txt, len as usize));
2562            assert!(
2563                out.contains("Hello"),
2564                "result should contain the literal text, got: {}",
2565                out
2566            );
2567
2568            libc::free(txt as *mut libc::c_void);
2569            crate::xml::tree::free_doc(result);
2570            crate::xml::tree::free_doc(doc);
2571            crate::xslt::stylesheet::xsltFreeStylesheet(style);
2572        }
2573    }
2574
2575    #[test]
2576    fn test_end_to_end_template_transform() {
2577        unsafe {
2578            // A normal stylesheet with an explicit template that emits
2579            // element and value-of.
2580            let xsl = b"<?xml version=\"1.0\"?>\n\
2581            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\n\
2582              <xsl:template match=\"/\">\n\
2583                <out><xsl:value-of select=\"/root/item\"/></out>\n\
2584              </xsl:template>\n\
2585            </xsl:stylesheet>\0";
2586            let style = crate::xslt::stylesheet::xsltParseStylesheetMemory(
2587                xsl.as_ptr() as *const c_char,
2588                (xsl.len() - 1) as c_int,
2589                ptr::null(),
2590            );
2591            assert!(!style.is_null(), "stylesheet parse failed");
2592
2593            let src = b"<?xml version=\"1.0\"?><root><item>world</item></root>\0";
2594            let doc = crate::abi::exports_xml2::xmlReadMemory(
2595                src.as_ptr() as *const c_char,
2596                (src.len() - 1) as c_int,
2597                ptr::null(),
2598                ptr::null(),
2599                0,
2600            );
2601            assert!(!doc.is_null());
2602
2603            let result = xsltApplyStylesheet(style, doc, ptr::null_mut());
2604            assert!(!result.is_null(), "apply failed");
2605
2606            let mut txt: *mut xmlChar = ptr::null_mut();
2607            let mut len: c_int = 0;
2608            let ret = crate::xslt::serialization::xsltSaveResultToString(
2609                &mut txt, &mut len, result, style,
2610            );
2611            assert_eq!(ret, 0);
2612            let out = String::from_utf8_lossy(core::slice::from_raw_parts(txt, len as usize));
2613            assert!(
2614                out.contains("world"),
2615                "result should contain the selected value, got: {}",
2616                out
2617            );
2618
2619            libc::free(txt as *mut libc::c_void);
2620            crate::xml::tree::free_doc(result);
2621            crate::xml::tree::free_doc(doc);
2622            crate::xslt::stylesheet::xsltFreeStylesheet(style);
2623        }
2624    }
2625
2626    /// Helper: transform a source document with a stylesheet and return the
2627    /// serialized result.
2628    unsafe fn run_transform(xsl: &[u8], src: &[u8]) -> String {
2629        let style = crate::xslt::stylesheet::xsltParseStylesheetMemory(
2630            xsl.as_ptr() as *const c_char,
2631            (xsl.len() - 1) as c_int,
2632            ptr::null(),
2633        );
2634        assert!(!style.is_null(), "stylesheet parse failed");
2635        let doc = crate::abi::exports_xml2::xmlReadMemory(
2636            src.as_ptr() as *const c_char,
2637            (src.len() - 1) as c_int,
2638            ptr::null(),
2639            ptr::null(),
2640            0,
2641        );
2642        assert!(!doc.is_null());
2643        let result = xsltApplyStylesheet(style, doc, ptr::null_mut());
2644        assert!(!result.is_null(), "apply failed");
2645        let mut txt: *mut xmlChar = ptr::null_mut();
2646        let mut len: c_int = 0;
2647        let ret =
2648            crate::xslt::serialization::xsltSaveResultToString(&mut txt, &mut len, result, style);
2649        assert_eq!(ret, 0);
2650        let out =
2651            String::from_utf8_lossy(core::slice::from_raw_parts(txt, len as usize)).into_owned();
2652        libc::free(txt as *mut libc::c_void);
2653        crate::xml::tree::free_doc(result);
2654        crate::xml::tree::free_doc(doc);
2655        crate::xslt::stylesheet::xsltFreeStylesheet(style);
2656        out
2657    }
2658
2659    #[test]
2660    fn test_xslt_for_each() {
2661        unsafe {
2662            let xsl = b"<?xml version=\"1.0\"?>\
2663            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
2664              <xsl:template match=\"/\">\
2665                <list><xsl:for-each select=\"/root/item\"><i><xsl:value-of select=\".\"/></i></xsl:for-each></list>\
2666              </xsl:template>\
2667            </xsl:stylesheet>\0";
2668            let src =
2669                b"<?xml version=\"1.0\"?><root><item>a</item><item>b</item><item>c</item></root>\0";
2670            let out = run_transform(xsl, src);
2671            assert!(out.contains("<i>a</i>"), "got: {}", out);
2672            assert!(out.contains("<i>b</i>"), "got: {}", out);
2673            assert!(out.contains("<i>c</i>"), "got: {}", out);
2674        }
2675    }
2676
2677    #[test]
2678    fn test_xslt_core_functions_in_value_of() {
2679        // UPSTREAM-PARITY: the transform context must register the XPath 1.0
2680        // core function library (count, string, substring, ...) so that
2681        // function calls in XPath expressions evaluate correctly. Before the
2682        // fix, every function call failed with an unknown-function error.
2683        unsafe {
2684            let xsl = b"<?xml version=\"1.0\"?>\
2685            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
2686              <xsl:template match=\"/\">\
2687                <out>\
2688                  <cnt><xsl:value-of select=\"count(library/book)\"/></cnt>\
2689                  <sub><xsl:value-of select=\"substring('hello',1,2)\"/></sub>\
2690                  <str><xsl:value-of select=\"string(library/book[1]/title)\"/></str>\
2691                </out>\
2692              </xsl:template>\
2693            </xsl:stylesheet>\0";
2694            let src = b"<?xml version=\"1.0\"?>\
2695            <library>\
2696              <book><title>Rust</title></book>\
2697              <book><title>XML</title></book>\
2698            </library>\0";
2699            let out = run_transform(xsl, src);
2700            assert!(out.contains("<cnt>2</cnt>"), "count() wrong: {}", out);
2701            assert!(out.contains("<sub>he</sub>"), "substring() wrong: {}", out);
2702            assert!(out.contains("<str>Rust</str>"), "string() wrong: {}", out);
2703        }
2704    }
2705
2706    #[test]
2707    fn test_xslt_avt_in_literal_attribute() {
2708        // UPSTREAM-PARITY: literal result element attributes may contain
2709        // attribute value templates (XSLT 1.0 §7.6.2): {expr} is evaluated
2710        // and its string value substituted, {{ and }} are literal braces.
2711        unsafe {
2712            let xsl = b"<?xml version=\"1.0\"?>\
2713            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
2714              <xsl:template match=\"/\">\
2715                <out>\
2716                  <xsl:for-each select=\"library/book\">\
2717                    <book id=\"{@id}\" label=\"{{literal}}\"/>\
2718                  </xsl:for-each>\
2719                </out>\
2720              </xsl:template>\
2721            </xsl:stylesheet>\0";
2722            let src = b"<?xml version=\"1.0\"?>\
2723            <library>\
2724              <book id=\"b1\"/>\
2725              <book id=\"b2\"/>\
2726            </library>\0";
2727            let out = run_transform(xsl, src);
2728            assert!(
2729                out.contains("<book id=\"b1\" label=\"{literal}\""),
2730                "AVT not evaluated: {}",
2731                out
2732            );
2733            assert!(
2734                out.contains("<book id=\"b2\" label=\"{literal}\""),
2735                "AVT not evaluated: {}",
2736                out
2737            );
2738        }
2739    }
2740
2741    #[test]
2742    fn test_xslt_avt_in_xsl_element_name() {
2743        // UPSTREAM-PARITY: xsl:element/@name is an AVT.
2744        unsafe {
2745            let xsl = b"<?xml version=\"1.0\"?>\
2746            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
2747              <xsl:template match=\"/\">\
2748                <out>\
2749                  <xsl:element name=\"el-{library/book/@id}\">text</xsl:element>\
2750                </out>\
2751              </xsl:template>\
2752            </xsl:stylesheet>\0";
2753            let src = b"<?xml version=\"1.0\"?>\
2754            <library><book id=\"b1\"/></library>\0";
2755            let out = run_transform(xsl, src);
2756            assert!(
2757                out.contains("<el-b1>"),
2758                "xsl:element AVT not evaluated: {}",
2759                out
2760            );
2761        }
2762    }
2763
2764    #[test]
2765    fn test_xslt_variable_inline_content_rtf() {
2766        // UPSTREAM-PARITY: a variable with inline content is a result tree
2767        // fragment. Regression test: the inline content must be copied into
2768        // a context-owned RVT (not left pointing into the stylesheet doc,
2769        // which caused a double-free at teardown), and $var must stringify
2770        // to the full descendant text.
2771        unsafe {
2772            let xsl = b"<?xml version=\"1.0\"?>\
2773            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
2774              <xsl:variable name=\"rtf\"><nums><n>3</n><n>7</n></nums></xsl:variable>\
2775              <xsl:template match=\"/\">\
2776                <out><v><xsl:value-of select=\"$rtf\"/></v></out>\
2777              </xsl:template>\
2778            </xsl:stylesheet>\0";
2779            let src = b"<?xml version=\"1.0\"?><root/>\0";
2780            // Must complete without a double-free and produce the text.
2781            let out = run_transform(xsl, src);
2782            assert!(out.contains("<v>37</v>"), "RTF string-value wrong: {}", out);
2783        }
2784    }
2785
2786    #[test]
2787    fn test_xslt_exsl_node_set_on_rtf() {
2788        // UPSTREAM-PARITY: exsl:node-set($var) on an RTF variable yields a
2789        // node-set whose root is the RVT document node, so path navigation
2790        // and node-set functions work on it (§35).
2791        unsafe {
2792            crate::exslt::register_all();
2793            let xsl = b"<?xml version=\"1.0\"?>\
2794            <xsl:stylesheet version=\"1.0\"\
2795                xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"\
2796                xmlns:exsl=\"http://exslt.org/common\"\
2797                xmlns:math=\"http://exslt.org/math\"\
2798                extension-element-prefixes=\"exsl math\">\
2799              <xsl:variable name=\"rtf\"><nums><n>3</n><n>7</n><n>1</n><n>9</n></nums></xsl:variable>\
2800              <xsl:template match=\"/\">\
2801                <out>\
2802                  <max><xsl:value-of select=\"math:max(exsl:node-set($rtf)/nums/n)\"/></max>\
2803                  <cnt><xsl:value-of select=\"count(exsl:node-set($rtf)/nums/n)\"/></cnt>\
2804                </out>\
2805              </xsl:template>\
2806            </xsl:stylesheet>\0";
2807            let src = b"<?xml version=\"1.0\"?><root/>\0";
2808            let out = run_transform(xsl, src);
2809            assert!(out.contains("<max>9</max>"), "math:max wrong: {}", out);
2810            assert!(out.contains("<cnt>4</cnt>"), "count wrong: {}", out);
2811        }
2812    }
2813
2814    #[test]
2815    fn test_xslt_if_node_set_test() {
2816        // UPSTREAM-PARITY: xsl:if/@test may be a node-set (XPath boolean
2817        // conversion §4.3). Regression: the transform read only boolval,
2818        // which is 0 for node-set objects, so test="author" was always
2819        // false.
2820        unsafe {
2821            let xsl = b"<?xml version=\"1.0\"?>\
2822            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
2823              <xsl:template match=\"/\">\
2824                <out>\
2825                  <xsl:for-each select=\"library/book\">\
2826                    <b><xsl:if test=\"author\">A</xsl:if><xsl:if test=\"missing\">M</xsl:if></b>\
2827                  </xsl:for-each>\
2828                </out>\
2829              </xsl:template>\
2830            </xsl:stylesheet>\0";
2831            let src = b"<?xml version=\"1.0\"?>\
2832            <library><book><author>x</author></book><book/></library>\0";
2833            let out = run_transform(xsl, src);
2834            assert!(out.contains("<b>A</b>"), "node-set test false: {}", out);
2835            assert!(out.contains("<b/>"), "missing-node test true: {}", out);
2836        }
2837    }
2838
2839    #[test]
2840    fn test_xslt_attribute_string_value() {
2841        // UPSTREAM-PARITY: string(@attr) / @attr='x' predicates use the
2842        // attribute's string value. Regression: node_string_value treated
2843        // type 13 (XML_HTML_DOCUMENT_NODE) as attribute and returned empty
2844        // for real attributes (type 2).
2845        unsafe {
2846            let xsl = b"<?xml version=\"1.0\"?>\
2847            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
2848              <xsl:template match=\"/\">\
2849                <out>\
2850                  <x><xsl:value-of select=\"string(library/book[1]/@id)\"/></x>\
2851                  <y><xsl:value-of select=\"count(library/book[@id='b2'])\"/></y>\
2852                </out>\
2853              </xsl:template>\
2854            </xsl:stylesheet>\0";
2855            let src = b"<?xml version=\"1.0\"?>\
2856            <library><book id=\"b1\"/><book id=\"b2\"/></library>\0";
2857            let out = run_transform(xsl, src);
2858            assert!(
2859                out.contains("<x>b1</x>"),
2860                "attr string-value wrong: {}",
2861                out
2862            );
2863            assert!(out.contains("<y>1</y>"), "attr predicate wrong: {}", out);
2864        }
2865    }
2866
2867    #[test]
2868    fn test_xslt_sort_descending() {
2869        // UPSTREAM-PARITY: xsl:sort with order="descending" inverts the
2870        // comparison. Regression: the sort was never compiled (null style)
2871        // and the sort key evaluated against the wrong context node.
2872        unsafe {
2873            let xsl = b"<?xml version=\"1.0\"?>\
2874            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
2875              <xsl:template match=\"/\">\
2876                <out>\
2877                  <xsl:for-each select=\"library/book\">\
2878                    <xsl:sort select=\"title\" order=\"descending\"/>\
2879                    <i><xsl:value-of select=\"title\"/></i>\
2880                  </xsl:for-each>\
2881                </out>\
2882              </xsl:template>\
2883            </xsl:stylesheet>\0";
2884            let src = b"<?xml version=\"1.0\"?>\
2885            <library><book><title>Alpha</title></book><book><title>Gamma</title></book><book><title>Beta</title></book></library>\0";
2886            let out = run_transform(xsl, src);
2887            let gamma = out.find("<i>Gamma</i>").unwrap();
2888            let beta = out.find("<i>Beta</i>").unwrap();
2889            let alpha = out.find("<i>Alpha</i>").unwrap();
2890            assert!(gamma < beta && beta < alpha, "not descending: {}", out);
2891        }
2892    }
2893
2894    #[test]
2895    fn test_xslt_key_function() {
2896        // UPSTREAM-PARITY: key(name, value) resolves through the key tables
2897        // built from xsl:key definitions.
2898        unsafe {
2899            let xsl = b"<?xml version=\"1.0\"?>\
2900            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
2901              <xsl:key name=\"byAuthor\" match=\"book\" use=\"author\"/>\
2902              <xsl:template match=\"/\">\
2903                <out><k><xsl:value-of select=\"key('byAuthor', 'Smith')/title\"/></k></out>\
2904              </xsl:template>\
2905            </xsl:stylesheet>\0";
2906            let src = b"<?xml version=\"1.0\"?>\
2907            <library><book><title>A</title><author>Smith</author></book><book><title>B</title><author>Jones</author></book></library>\0";
2908            let out = run_transform(xsl, src);
2909            assert!(out.contains("<k>A</k>"), "key() wrong: {}", out);
2910        }
2911    }
2912
2913    #[test]
2914    fn test_xslt_call_template_with_params() {
2915        // UPSTREAM-PARITY: xsl:with-param values are visible to $name inside
2916        // the called template; xsl:param defaults apply when no value is
2917        // passed. Regression: with-params were never registered in the XPath
2918        // variable hash.
2919        unsafe {
2920            let xsl = b"<?xml version=\"1.0\"?>\
2921            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
2922              <xsl:template match=\"/\">\
2923                <out>\
2924                  <xsl:call-template name=\"greet\">\
2925                    <xsl:with-param name=\"who\" select=\"'World'\"/>\
2926                  </xsl:call-template>\
2927                </out>\
2928              </xsl:template>\
2929              <xsl:template name=\"greet\">\
2930                <xsl:param name=\"who\" select=\"'nobody'\"/>\
2931                <g>Hello <xsl:value-of select=\"$who\"/>!</g>\
2932              </xsl:template>\
2933            </xsl:stylesheet>\0";
2934            let src = b"<?xml version=\"1.0\"?><root/>\0";
2935            let out = run_transform(xsl, src);
2936            assert!(out.contains("Hello World"), "with-param lost: {}", out);
2937        }
2938    }
2939
2940    #[test]
2941    fn test_xslt_html_method_meta_charset() {
2942        // UPSTREAM-PARITY: method="html" inserts <meta charset="..."> in
2943        // the <head> of the root <html> and formats with newlines only.
2944        unsafe {
2945            let xsl = b"<?xml version=\"1.0\"?>\
2946            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
2947              <xsl:output method=\"html\" indent=\"yes\"/>\
2948              <xsl:template match=\"/\">\
2949                <html><head><title>T</title></head><body><p>x</p></body></html>\
2950              </xsl:template>\
2951            </xsl:stylesheet>\0";
2952            let src = b"<?xml version=\"1.0\"?><root/>\0";
2953            let out = run_transform(xsl, src);
2954            assert!(
2955                out.contains("<meta charset=\"UTF-8\">"),
2956                "meta charset missing: {}",
2957                out
2958            );
2959            assert!(!out.contains("  <head>"), "unexpected indent: {}", out);
2960        }
2961    }
2962
2963    #[test]
2964    fn test_xslt_if() {
2965        unsafe {
2966            let xsl = b"<?xml version=\"1.0\"?>\
2967            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
2968              <xsl:template match=\"/\">\
2969                <xsl:if test=\"/root/item = 'yes'\"><yes/></xsl:if>\
2970                <xsl:if test=\"/root/item = 'no'\"><no/></xsl:if>\
2971              </xsl:template>\
2972            </xsl:stylesheet>\0";
2973            let src = b"<?xml version=\"1.0\"?><root><item>yes</item></root>\0";
2974            let out = run_transform(xsl, src);
2975            assert!(out.contains("<yes/>"), "got: {}", out);
2976            assert!(!out.contains("<no/>"), "got: {}", out);
2977        }
2978    }
2979
2980    #[test]
2981    fn test_xslt_choose() {
2982        unsafe {
2983            let xsl = b"<?xml version=\"1.0\"?>\
2984            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
2985              <xsl:template match=\"/\">\
2986                <xsl:choose>\
2987                  <xsl:when test=\"/root/item = 'a'\"><chosen>a</chosen></xsl:when>\
2988                  <xsl:when test=\"/root/item = 'b'\"><chosen>b</chosen></xsl:when>\
2989                  <xsl:otherwise><chosen>other</chosen></xsl:otherwise>\
2990                </xsl:choose>\
2991              </xsl:template>\
2992            </xsl:stylesheet>\0";
2993            let src = b"<?xml version=\"1.0\"?><root><item>b</item></root>\0";
2994            let out = run_transform(xsl, src);
2995            assert!(out.contains("<chosen>b</chosen>"), "got: {}", out);
2996        }
2997    }
2998
2999    #[test]
3000    fn test_xslt_variable_and_call_template() {
3001        unsafe {
3002            let xsl = b"<?xml version=\"1.0\"?>\
3003            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
3004              <xsl:variable name=\"greeting\" select=\"'Hello'\"/>\
3005              <xsl:template match=\"/\">\
3006                <xsl:call-template name=\"say\"/>\
3007              </xsl:template>\
3008              <xsl:template name=\"say\">\
3009                <msg><xsl:value-of select=\"$greeting\"/> <xsl:value-of select=\"/root/name\"/></msg>\
3010              </xsl:template>\
3011            </xsl:stylesheet>\0";
3012            let src = b"<?xml version=\"1.0\"?><root><name>World</name></root>\0";
3013            let out = run_transform(xsl, src);
3014            // UPSTREAM-PARITY: the whitespace-only text node between the two
3015            // xsl:value-of instructions is stripped at stylesheet
3016            // preprocessing, exactly as upstream libxslt does; preserving it
3017            // requires an explicit <xsl:text> </xsl:text>.
3018            assert!(out.contains("HelloWorld"), "got: {}", out);
3019        }
3020    }
3021
3022    #[test]
3023    fn test_xslt_text_preserves_whitespace() {
3024        unsafe {
3025            let xsl = b"<?xml version=\"1.0\"?>\
3026            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
3027              <xsl:template match=\"/\">\
3028                <msg><xsl:value-of select=\"'Hello'\"/><xsl:text> </xsl:text><xsl:value-of select=\"/root/name\"/></msg>\
3029              </xsl:template>\
3030            </xsl:stylesheet>\0";
3031            let src = b"<?xml version=\"1.0\"?><root><name>World</name></root>\0";
3032            let out = run_transform(xsl, src);
3033            // UPSTREAM-PARITY: xsl:text content is preserved verbatim.
3034            assert!(out.contains("Hello World"), "got: {}", out);
3035        }
3036    }
3037
3038    #[test]
3039    fn test_xslt_element_and_attribute() {
3040        unsafe {
3041            let xsl = b"<?xml version=\"1.0\"?>\
3042            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
3043              <xsl:template match=\"/\">\
3044                <xsl:element name=\"custom\">\
3045                  <xsl:attribute name=\"attr\">value</xsl:attribute>\
3046                  <xsl:value-of select=\"/root/item\"/>\
3047                </xsl:element>\
3048              </xsl:template>\
3049            </xsl:stylesheet>\0";
3050            let src = b"<?xml version=\"1.0\"?><root><item>data</item></root>\0";
3051            let out = run_transform(xsl, src);
3052            assert!(out.contains("custom"), "got: {}", out);
3053            assert!(out.contains("attr=\"value\""), "got: {}", out);
3054            assert!(out.contains("data"), "got: {}", out);
3055        }
3056    }
3057
3058    #[test]
3059    fn test_xslt_apply_templates_with_select() {
3060        unsafe {
3061            let xsl = b"<?xml version=\"1.0\"?>\
3062            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
3063              <xsl:template match=\"/\">\
3064                <out><xsl:apply-templates select=\"/root/item\"/></out>\
3065              </xsl:template>\
3066              <xsl:template match=\"item\"><item><xsl:value-of select=\".\"/></item></xsl:template>\
3067            </xsl:stylesheet>\0";
3068            let src = b"<?xml version=\"1.0\"?><root><item>alpha</item><item>beta</item></root>\0";
3069            let out = run_transform(xsl, src);
3070            assert!(out.contains("<item>alpha</item>"), "got: {}", out);
3071            assert!(out.contains("<item>beta</item>"), "got: {}", out);
3072        }
3073    }
3074
3075    #[test]
3076    fn test_xslt_text_and_comment_and_pi() {
3077        unsafe {
3078            let xsl = b"<?xml version=\"1.0\"?>\
3079            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
3080              <xsl:template match=\"/\">\
3081                <xsl:text>plain</xsl:text>\
3082                <xsl:comment>a comment</xsl:comment>\
3083                <xsl:processing-instruction name=\"target\">pi-data</xsl:processing-instruction>\
3084              </xsl:template>\
3085            </xsl:stylesheet>\0";
3086            let src = b"<?xml version=\"1.0\"?><root/>\0";
3087            let out = run_transform(xsl, src);
3088            assert!(out.contains("plain"), "got: {}", out);
3089            assert!(out.contains("<!--a comment-->"), "got: {}", out);
3090            assert!(out.contains("<?target pi-data?>"), "got: {}", out);
3091        }
3092    }
3093
3094    #[test]
3095    fn test_xslt_copy_and_copy_of() {
3096        unsafe {
3097            let xsl = b"<?xml version=\"1.0\"?>\
3098            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
3099              <xsl:template match=\"/\">\
3100                <xsl:copy-of select=\"/root/item\"/>\
3101                <xsl:copy-of select=\"'literal'\"/>\
3102              </xsl:template>\
3103            </xsl:stylesheet>\0";
3104            let src = b"<?xml version=\"1.0\"?><root><item>copied</item></root>\0";
3105            let out = run_transform(xsl, src);
3106            assert!(out.contains("<item>copied</item>"), "got: {}", out);
3107            assert!(out.contains("literal"), "got: {}", out);
3108        }
3109    }
3110
3111    #[test]
3112    fn test_xslt_number() {
3113        unsafe {
3114            let xsl = b"<?xml version=\"1.0\"?>\
3115            <xsl:stylesheet version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\">\
3116              <xsl:template match=\"/\">\
3117                <n><xsl:number value=\"42\"/></n>\
3118                <r><xsl:number value=\"9\" format=\"I\"/></r>\
3119              </xsl:template>\
3120            </xsl:stylesheet>\0";
3121            let src = b"<?xml version=\"1.0\"?><root/>\0";
3122            let out = run_transform(xsl, src);
3123            assert!(out.contains("<n>42</n>"), "got: {}", out);
3124            assert!(out.contains("<r>IX</r>"), "got: {}", out);
3125        }
3126    }
3127}