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