Skip to main content

sup_xml_xslt/
lib.rs

1//! XSLT 1.0 transformation engine.
2//!
3//! Public API surface:
4//!
5//! ```
6//! use sup_xml_xslt::Stylesheet;
7//! use sup_xml_core::{parse_str, ParseOptions};
8//!
9//! let xslt = Stylesheet::compile_str(r#"<xsl:stylesheet version="1.0"
10//!     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
11//!   <xsl:output method="xml" omit-xml-declaration="yes"/>
12//!   <xsl:template match="/"><out><xsl:value-of select="/r"/></out></xsl:template>
13//! </xsl:stylesheet>"#).unwrap();
14//!
15//! let src = parse_str("<r>hello</r>", &ParseOptions::default()).unwrap();
16//! let result = xslt.apply(&src).unwrap();
17//! assert_eq!(result.to_string().unwrap(), "<out>hello</out>");
18//! ```
19//!
20//! The engine is built *on top of* `sup-xml-core`'s XPath
21//! evaluator — XSLT match patterns and `select=` expressions parse
22//! to the same AST, and template-body XPath calls reach the
23//! engine's binding hook for the XSLT-added functions (`current()`,
24//! `document()`, `key()`, `format-number()` etc.).  EXSLT functions
25//! (math/date/str/set) are already always-on in the XPath engine,
26//! so XSLT stylesheets that use them work without any additional
27//! registration step.
28
29#![forbid(unsafe_code)]
30
31pub mod ast;
32pub mod compiler;
33pub mod error;
34pub mod eval;
35pub mod extensions;
36pub mod format_number;
37pub mod functions;
38pub mod loader;
39pub mod number;
40pub mod output;
41pub mod pattern;
42pub mod result_tree;
43pub mod schematron;
44pub mod sort;
45pub mod walk;
46pub mod whitespace;
47
48pub use loader::{FilesystemLoader, InMemoryLoader, Loader, NullLoader};
49
50pub use ast::StylesheetAst;
51pub use error::XsltError;
52pub use extensions::{ExtensionFunctions, Extensions};
53/// The XPath value type passed to extension functions and returned
54/// by them.  Re-exported from `sup-xml-core` for convenience.
55pub use sup_xml_core::xpath::XPathValue;
56
57/// The XSLT namespace URI — all `xsl:*` instructions are
58/// recognised by belonging to this URI.
59pub const XSLT_NS: &str = "http://www.w3.org/1999/XSL/Transform";
60
61/// A compiled XSLT 1.0 stylesheet, ready to apply to source
62/// documents.  Compilation walks the stylesheet tree once,
63/// pre-parses every XPath expression, and resolves
64/// xsl:import/xsl:include chains.
65///
66/// A compiled XSLT 1.0 stylesheet.
67#[derive(Debug)]
68pub struct Stylesheet {
69    /// The compiled AST — pre-parsed XPath expressions, decomposed
70    /// AVTs, indexed templates.  Public to ease debugging /
71    /// introspection from downstream tools; the apply() machinery
72    /// is the supported public surface.
73    pub ast: StylesheetAst,
74}
75
76impl Stylesheet {
77    /// Compile a parsed stylesheet document into a reusable
78    /// transformer.  Walks the stylesheet once, pre-parses every
79    /// XPath expression in select/match/test/use attributes,
80    /// decomposes Attribute Value Templates, and indexes templates
81    /// for later pattern-matching.
82    ///
83    /// The supplied document MUST have been parsed in
84    /// namespace-aware mode (`ParseOptions { namespace_aware: true,
85    /// .. }`) — XSLT is fundamentally namespace-driven and the
86    /// compiler detects `xsl:*` instructions via their namespace
87    /// URI.  Use [`Stylesheet::compile_str`] for the common case
88    /// where you have the stylesheet source as text and want the
89    /// parse handled for you.
90    pub fn compile(
91        stylesheet_doc: &sup_xml_tree::dom::Document,
92    ) -> Result<Self, XsltError> {
93        let ast = compiler::compile(stylesheet_doc)?;
94        Ok(Stylesheet { ast })
95    }
96
97    /// Parse + compile a stylesheet from source text.  Convenience
98    /// wrapper that sets `namespace_aware: true` for you.
99    ///
100    /// Stylesheets that use `xsl:import` / `xsl:include` will
101    /// successfully compile, but the imports' templates won't be
102    /// loaded — references to imported templates surface as
103    /// runtime errors.  For stylesheets that need import
104    /// resolution, use [`Stylesheet::compile_str_with_loader`].
105    pub fn compile_str(stylesheet_text: &str) -> Result<Self, XsltError> {
106        Self::compile_str_with_loader(stylesheet_text, &loader::NullLoader, None)
107    }
108
109    /// Parse + compile + resolve imports.  Each `xsl:import` /
110    /// `xsl:include` is followed via `loader`, recursively, with
111    /// `base` supplying the URI to resolve relative hrefs against.
112    ///
113    /// Imported templates are merged into the resulting AST with
114    /// lower [`Template::import_precedence`] than the importing
115    /// stylesheet's own templates, so XSLT 1.0 §2.6.2 import
116    /// precedence is honoured at pattern-match time.
117    pub fn compile_str_with_loader(
118        stylesheet_text: &str,
119        loader:          &dyn Loader,
120        base:            Option<&str>,
121    ) -> Result<Self, XsltError> {
122        let ast = compiler::compile_with_imports(
123            stylesheet_text, loader, base,
124            ast::StylesheetAst::default(),
125            &mut 0,
126        )?;
127        Self::finalize(ast)
128    }
129
130    /// Compile with a package library available for `xsl:use-package`
131    /// (XSLT 3.0 §3.5.1) — `packages` maps a package name to its
132    /// (source text, base URI).  Imports/includes still resolve via
133    /// `loader`.
134    pub fn compile_str_with_packages(
135        stylesheet_text: &str,
136        loader:          &dyn Loader,
137        base:            Option<&str>,
138        packages: std::collections::HashMap<String, (String, Option<String>)>,
139    ) -> Result<Self, XsltError> {
140        let ast = compiler::compile_with_packages(stylesheet_text, loader, base, packages)?;
141        Self::finalize(ast)
142    }
143
144    /// Shared post-compilation cleanup + validation for the
145    /// `compile_str_with_*` entry points.
146    fn finalize(mut ast: ast::StylesheetAst) -> Result<Self, XsltError> {
147        // ASTs accumulated during recursive compilation might
148        // double-count includes vs. imports for the same file —
149        // dedup at the top level.
150        ast.includes.sort();
151        ast.includes.dedup();
152        ast.imports.sort();
153        ast.imports.dedup();
154        ast.documents_to_load.sort();
155        ast.documents_to_load.dedup();
156        // XSLT 1.0 §7.1.4 (XTSE0710): every `use-attribute-sets`
157        // reference must name a declared attribute-set.  Validate
158        // at the end so cross-stylesheet (imported) declarations
159        // are in scope.
160        compiler::validate_attribute_set_refs(&ast)?;
161        compiler::validate_named_template_uniqueness(&ast)?;
162        // validate_global_variable_uniqueness is disabled: the
163        // current Variable / Param structs don't carry an
164        // import_precedence field, so the validator can't tell
165        // apart "same precedence" duplicates (XTSE0630) from
166        // legitimate shadowing from imports / includes.  Re-enable
167        // once per-global precedence tracking lands.
168        compiler::validate_output_declarations(&ast)?;
169        compiler::validate_call_template_with_params(&ast)?;
170        compiler::validate_iterate_constraints(&ast)?;
171        compiler::validate_input_type_annotations(&ast)?;
172        Ok(Stylesheet { ast })
173    }
174
175    /// Apply this stylesheet to `source_doc`, returning the
176    /// materialized result tree.  Serialize via
177    /// [`ResultTree::to_string`] (honors `<xsl:output method=…>`)
178    /// or inspect [`ResultTree::children`] directly.
179    pub fn apply(
180        &self,
181        source_doc: &sup_xml_tree::dom::Document,
182    ) -> Result<ResultTree, XsltError> {
183        eval::apply_stylesheet(&self.ast, source_doc)
184    }
185
186    /// Apply this stylesheet using `loader` to resolve any
187    /// `document()` URIs the stylesheet references with string
188    /// literals.  `base` supplies the base URI for relative-href
189    /// resolution (passed through to [`Loader::load`]).
190    ///
191    /// Dynamic forms of `document()` — node-set arguments, the
192    /// empty-string URI, or any expression that isn't a string
193    /// literal — return a clear runtime error explaining the
194    /// limitation.  Stylesheets that don't call `document()` at all
195    /// behave identically to [`Stylesheet::apply`].
196    pub fn apply_with_loader(
197        &self,
198        source_doc: &sup_xml_tree::dom::Document,
199        loader:     &dyn Loader,
200        base:       Option<&str>,
201    ) -> Result<ResultTree, XsltError> {
202        eval::apply_stylesheet_with_loader(&self.ast, source_doc, loader, base)
203    }
204
205    /// Apply this stylesheet with caller-supplied XPath extension
206    /// functions registered via [`ExtensionFunctions`].
207    ///
208    /// Extension calls are dispatched via namespace + local name:
209    /// when an XPath expression invokes `prefix:fname(…)`, the engine
210    /// looks up `(namespace-uri(prefix), "fname")` in the supplied
211    /// trait object.  Returning `Some(Ok(_))` provides the result;
212    /// `Some(Err(_))` surfaces a runtime error; `None` falls through
213    /// to the built-in EXSLT chain and finally to "unknown function".
214    ///
215    /// The most common use case is implemented by [`Extensions`],
216    /// which lets callers register individual closures keyed by
217    /// `(namespace, name)`.  For richer integrations (stateful
218    /// dispatch, integration with an existing function registry,
219    /// etc.) implement [`ExtensionFunctions`] on your own type.
220    pub fn apply_with_extensions(
221        &self,
222        source_doc: &sup_xml_tree::dom::Document,
223        extensions: &dyn ExtensionFunctions,
224    ) -> Result<ResultTree, XsltError> {
225        eval::apply_stylesheet_full(
226            &self.ast, source_doc,
227            &loader::NullLoader, None,
228            Some(extensions),
229        )
230    }
231
232    /// Apply this stylesheet with both a `document()` loader and
233    /// caller-supplied XPath extension functions.  Combines
234    /// [`Stylesheet::apply_with_loader`] and
235    /// [`Stylesheet::apply_with_extensions`].
236    pub fn apply_with_loader_and_extensions(
237        &self,
238        source_doc: &sup_xml_tree::dom::Document,
239        loader:     &dyn Loader,
240        base:       Option<&str>,
241        extensions: &dyn ExtensionFunctions,
242    ) -> Result<ResultTree, XsltError> {
243        eval::apply_stylesheet_full(
244            &self.ast, source_doc,
245            loader, base,
246            Some(extensions),
247        )
248    }
249
250    /// Apply the stylesheet with caller-supplied overrides for
251    /// top-level `xsl:param` declarations.  Each `(name, value)`
252    /// pair replaces the matching param's default at apply time
253    /// — XSLT 1.0 §11.4.  Match is by local-name; unmatched names
254    /// are silently ignored.
255    pub fn apply_with_params(
256        &self,
257        source_doc: &sup_xml_tree::dom::Document,
258        loader:     &dyn Loader,
259        base:       Option<&str>,
260        params:     &[(String, String)],
261    ) -> Result<ResultTree, XsltError> {
262        eval::apply_stylesheet_full_with_params(
263            &self.ast, source_doc,
264            loader, base, None, params,
265        )
266    }
267
268    /// Apply with both top-level params and a named-template entry
269    /// point.  `initial_template` selects an `xsl:template name="…"`
270    /// declaration to call instead of doing the default
271    /// apply-templates dispatch on the document root.  This matches
272    /// the XSLT 3.0 named-entry convention used by W3C test
273    /// harnesses via `<initial-template name="go"/>`.
274    pub fn apply_with_params_and_initial(
275        &self,
276        source_doc:        &sup_xml_tree::dom::Document,
277        loader:            &dyn Loader,
278        base:              Option<&str>,
279        params:            &[(String, String)],
280        initial_template:  Option<&str>,
281    ) -> Result<ResultTree, XsltError> {
282        eval::apply_stylesheet_full_with_params_and_initial(
283            &self.ast, source_doc,
284            loader, base, None, params, initial_template, None,
285        )
286    }
287
288    /// Apply with top-level params, an optional named-template entry
289    /// point, and an optional initial mode for the default
290    /// `apply-templates` dispatch.  XSLT 3.0 / W3C test conventions
291    /// pass `<initial-mode name="X"/>` so the entry-point dispatch
292    /// sees the named mode rather than the unnamed default.
293    pub fn apply_with_params_initial_and_mode(
294        &self,
295        source_doc:        &sup_xml_tree::dom::Document,
296        loader:            &dyn Loader,
297        base:              Option<&str>,
298        params:            &[(String, String)],
299        initial_template:  Option<&str>,
300        initial_mode:      Option<&str>,
301    ) -> Result<ResultTree, XsltError> {
302        eval::apply_stylesheet_full_with_params_and_initial(
303            &self.ast, source_doc,
304            loader, base, None, params, initial_template, initial_mode,
305        )
306    }
307}
308
309pub use result_tree::ResultTree;