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;