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        compiler::validate_package_exposes(&ast)?;
173        Ok(Stylesheet { ast })
174    }
175
176    /// Apply this stylesheet to `source_doc`, returning the
177    /// materialized result tree.  Serialize via
178    /// [`ResultTree::to_string`] (honors `<xsl:output method=…>`)
179    /// or inspect [`ResultTree::children`] directly.
180    pub fn apply(
181        &self,
182        source_doc: &sup_xml_tree::dom::Document,
183    ) -> Result<ResultTree, XsltError> {
184        eval::apply_stylesheet(&self.ast, source_doc)
185    }
186
187    /// Apply this stylesheet using `loader` to resolve any
188    /// `document()` URIs the stylesheet references with string
189    /// literals.  `base` supplies the base URI for relative-href
190    /// resolution (passed through to [`Loader::load`]).
191    ///
192    /// Dynamic forms of `document()` — node-set arguments, the
193    /// empty-string URI, or any expression that isn't a string
194    /// literal — return a clear runtime error explaining the
195    /// limitation.  Stylesheets that don't call `document()` at all
196    /// behave identically to [`Stylesheet::apply`].
197    pub fn apply_with_loader(
198        &self,
199        source_doc: &sup_xml_tree::dom::Document,
200        loader:     &dyn Loader,
201        base:       Option<&str>,
202    ) -> Result<ResultTree, XsltError> {
203        eval::apply_stylesheet_with_loader(&self.ast, source_doc, loader, base)
204    }
205
206    /// Apply this stylesheet with caller-supplied XPath extension
207    /// functions registered via [`ExtensionFunctions`].
208    ///
209    /// Extension calls are dispatched via namespace + local name:
210    /// when an XPath expression invokes `prefix:fname(…)`, the engine
211    /// looks up `(namespace-uri(prefix), "fname")` in the supplied
212    /// trait object.  Returning `Some(Ok(_))` provides the result;
213    /// `Some(Err(_))` surfaces a runtime error; `None` falls through
214    /// to the built-in EXSLT chain and finally to "unknown function".
215    ///
216    /// The most common use case is implemented by [`Extensions`],
217    /// which lets callers register individual closures keyed by
218    /// `(namespace, name)`.  For richer integrations (stateful
219    /// dispatch, integration with an existing function registry,
220    /// etc.) implement [`ExtensionFunctions`] on your own type.
221    pub fn apply_with_extensions(
222        &self,
223        source_doc: &sup_xml_tree::dom::Document,
224        extensions: &dyn ExtensionFunctions,
225    ) -> Result<ResultTree, XsltError> {
226        eval::apply_stylesheet_full(
227            &self.ast, source_doc,
228            &loader::NullLoader, None,
229            Some(extensions),
230        )
231    }
232
233    /// Apply this stylesheet with both a `document()` loader and
234    /// caller-supplied XPath extension functions.  Combines
235    /// [`Stylesheet::apply_with_loader`] and
236    /// [`Stylesheet::apply_with_extensions`].
237    pub fn apply_with_loader_and_extensions(
238        &self,
239        source_doc: &sup_xml_tree::dom::Document,
240        loader:     &dyn Loader,
241        base:       Option<&str>,
242        extensions: &dyn ExtensionFunctions,
243    ) -> Result<ResultTree, XsltError> {
244        eval::apply_stylesheet_full(
245            &self.ast, source_doc,
246            loader, base,
247            Some(extensions),
248        )
249    }
250
251    /// Apply the stylesheet with caller-supplied overrides for
252    /// top-level `xsl:param` declarations.  Each `(name, value)`
253    /// pair replaces the matching param's default at apply time
254    /// — XSLT 1.0 §11.4.  Match is by local-name; unmatched names
255    /// are silently ignored.
256    pub fn apply_with_params(
257        &self,
258        source_doc: &sup_xml_tree::dom::Document,
259        loader:     &dyn Loader,
260        base:       Option<&str>,
261        params:     &[(String, String)],
262    ) -> Result<ResultTree, XsltError> {
263        eval::apply_stylesheet_full_with_params(
264            &self.ast, source_doc,
265            loader, base, None, params,
266        )
267    }
268
269    /// Apply with both top-level params and a named-template entry
270    /// point.  `initial_template` selects an `xsl:template name="…"`
271    /// declaration to call instead of doing the default
272    /// apply-templates dispatch on the document root.  This matches
273    /// the XSLT 3.0 named-entry convention used by W3C test
274    /// harnesses via `<initial-template name="go"/>`.
275    pub fn apply_with_params_and_initial(
276        &self,
277        source_doc:        &sup_xml_tree::dom::Document,
278        loader:            &dyn Loader,
279        base:              Option<&str>,
280        params:            &[(String, String)],
281        initial_template:  Option<&str>,
282    ) -> Result<ResultTree, XsltError> {
283        eval::apply_stylesheet_full_with_params_and_initial(
284            &self.ast, source_doc,
285            loader, base, None, params, initial_template, None,
286        )
287    }
288
289    /// Apply with top-level params, an optional named-template entry
290    /// point, and an optional initial mode for the default
291    /// `apply-templates` dispatch.  XSLT 3.0 / W3C test conventions
292    /// pass `<initial-mode name="X"/>` so the entry-point dispatch
293    /// sees the named mode rather than the unnamed default.
294    pub fn apply_with_params_initial_and_mode(
295        &self,
296        source_doc:        &sup_xml_tree::dom::Document,
297        loader:            &dyn Loader,
298        base:              Option<&str>,
299        params:            &[(String, String)],
300        initial_template:  Option<&str>,
301        initial_mode:      Option<&str>,
302    ) -> Result<ResultTree, XsltError> {
303        eval::apply_stylesheet_full_with_params_and_initial(
304            &self.ast, source_doc,
305            loader, base, None, params, initial_template, initial_mode,
306        )
307    }
308}
309
310pub use result_tree::ResultTree;