@tradik/xslt-processor 1.0.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +110 -520
  3. package/bin/lib/decode.js +15 -0
  4. package/bin/lib/dom.js +177 -0
  5. package/bin/lib/loaders.js +127 -0
  6. package/bin/lib/options.js +131 -0
  7. package/bin/lib/output.js +114 -0
  8. package/bin/lib/paths.js +186 -0
  9. package/bin/lib/transform.js +206 -0
  10. package/bin/xslt.js +73 -168
  11. package/dist/xslt-processor.browser.js +9564 -1585
  12. package/dist/xslt-processor.browser.js.map +4 -4
  13. package/dist/xslt-processor.browser.min.js +13 -2
  14. package/dist/xslt-processor.browser.min.js.map +4 -4
  15. package/dist/xslt-processor.cjs +9572 -1586
  16. package/dist/xslt-processor.cjs.map +4 -4
  17. package/dist/xslt-processor.d.cts +658 -0
  18. package/dist/xslt-processor.d.ts +459 -12
  19. package/dist/xslt-processor.js +9546 -1582
  20. package/dist/xslt-processor.js.map +4 -4
  21. package/package.json +71 -20
  22. package/src/XSLTProcessor.js +494 -48
  23. package/src/async/abort.js +63 -0
  24. package/src/async/documentUris.js +128 -0
  25. package/src/async/loaders.js +134 -0
  26. package/src/async/preload.js +159 -0
  27. package/src/async/processor.js +206 -0
  28. package/src/async/stream.js +125 -0
  29. package/src/bridge/engine.js +221 -0
  30. package/src/bridge/loader.js +78 -0
  31. package/src/bridge/results.js +75 -0
  32. package/src/bridge/version.js +63 -0
  33. package/src/index.js +26 -8
  34. package/src/io/decode.js +140 -0
  35. package/src/io/readSource.js +167 -0
  36. package/src/xpath/axes.js +562 -0
  37. package/src/xpath/documentOrder.js +270 -0
  38. package/src/xpath/evaluator.js +518 -357
  39. package/src/xpath/index.js +8 -2
  40. package/src/xpath/namespaceNodes.js +172 -0
  41. package/src/xpath/nodeSetFunctions.js +169 -0
  42. package/src/xpath/parser.js +30 -5
  43. package/src/xpath/strings.js +183 -0
  44. package/src/xpath/tokenizer.js +37 -23
  45. package/src/xslt/attributeSets.js +95 -0
  46. package/src/xslt/avt.js +103 -0
  47. package/src/xslt/computedNames.js +91 -0
  48. package/src/xslt/copying.js +212 -0
  49. package/src/xslt/declarationNames.js +80 -0
  50. package/src/xslt/domParsing.js +95 -0
  51. package/src/xslt/elements.js +57 -0
  52. package/src/xslt/engine/bindings.js +195 -0
  53. package/src/xslt/engine/context.js +105 -0
  54. package/src/xslt/engine/controlFlow.js +145 -0
  55. package/src/xslt/engine/copyInstructions.js +133 -0
  56. package/src/xslt/engine/declarations.js +233 -0
  57. package/src/xslt/engine/functionSupport.js +103 -0
  58. package/src/xslt/engine/methods.js +33 -0
  59. package/src/xslt/engine/nodeConstruction.js +187 -0
  60. package/src/xslt/engine/numbering.js +104 -0
  61. package/src/xslt/engine/outputDeclaration.js +77 -0
  62. package/src/xslt/engine/sequenceConstructor.js +228 -0
  63. package/src/xslt/engine/stylesheetLoading.js +208 -0
  64. package/src/xslt/engine/templateInvocation.js +253 -0
  65. package/src/xslt/engine/templateRules.js +243 -0
  66. package/src/xslt/engine/textInstructions.js +171 -0
  67. package/src/xslt/engine/topLevel.js +130 -0
  68. package/src/xslt/engine/transformation.js +263 -0
  69. package/src/xslt/engine/workStack.js +245 -0
  70. package/src/xslt/engine.js +184 -1736
  71. package/src/xslt/exslt/arguments.js +99 -0
  72. package/src/xslt/exslt/calendar.js +120 -0
  73. package/src/xslt/exslt/common.js +44 -0
  74. package/src/xslt/exslt/dateCalc.js +261 -0
  75. package/src/xslt/exslt/dateFormat.js +150 -0
  76. package/src/xslt/exslt/dateParse.js +265 -0
  77. package/src/xslt/exslt/dates.js +259 -0
  78. package/src/xslt/exslt/duration.js +207 -0
  79. package/src/xslt/exslt/dynamic.js +59 -0
  80. package/src/xslt/exslt/index.js +59 -0
  81. package/src/xslt/exslt/math.js +177 -0
  82. package/src/xslt/exslt/sets.js +96 -0
  83. package/src/xslt/exslt/stringOps.js +163 -0
  84. package/src/xslt/exslt/strings.js +147 -0
  85. package/src/xslt/exslt/uri.js +92 -0
  86. package/src/xslt/formatNumber.js +233 -0
  87. package/src/xslt/forwardsCompatible.js +75 -0
  88. package/src/xslt/functions.js +270 -0
  89. package/src/xslt/index.js +38 -1
  90. package/src/xslt/keys.js +164 -0
  91. package/src/xslt/literalResult.js +223 -0
  92. package/src/xslt/matchScope.js +116 -0
  93. package/src/xslt/number.js +271 -0
  94. package/src/xslt/numberFormat.js +253 -0
  95. package/src/xslt/outputNames.js +58 -0
  96. package/src/xslt/patternCompiler.js +175 -0
  97. package/src/xslt/patterns.js +324 -0
  98. package/src/xslt/qname.js +90 -0
  99. package/src/xslt/resultDocument.js +98 -0
  100. package/src/xslt/resultNamespaces.js +219 -0
  101. package/src/xslt/resultTree.js +211 -0
  102. package/src/xslt/serializer/baseWriter.js +390 -0
  103. package/src/xslt/serializer/chunks.js +120 -0
  104. package/src/xslt/serializer/constants.js +92 -0
  105. package/src/xslt/serializer/encoding.js +327 -0
  106. package/src/xslt/serializer/escape.js +135 -0
  107. package/src/xslt/serializer/frames.js +168 -0
  108. package/src/xslt/serializer/htmlDoctype.js +102 -0
  109. package/src/xslt/serializer/htmlEntities.js +77 -0
  110. package/src/xslt/serializer/htmlSerializer.js +239 -0
  111. package/src/xslt/serializer/indent.js +51 -0
  112. package/src/xslt/serializer/namespaces.js +68 -0
  113. package/src/xslt/serializer/rawText.js +41 -0
  114. package/src/xslt/serializer/settings.js +179 -0
  115. package/src/xslt/serializer/textSerializer.js +77 -0
  116. package/src/xslt/serializer/xhtmlDocument.js +103 -0
  117. package/src/xslt/serializer/xmlSerializer.js +227 -0
  118. package/src/xslt/serializer.js +90 -0
  119. package/src/xslt/sort.js +151 -0
  120. package/src/xslt/spaceNameTests.js +115 -0
  121. package/src/xslt/stylesheetChecks.js +206 -0
  122. package/src/xslt/stylesheetNamespaces.js +266 -0
  123. package/src/xslt/templatePriority.js +45 -0
  124. package/src/xslt/uri.js +68 -0
  125. package/src/xslt/variables.js +152 -0
  126. package/src/xslt/whitespace.js +200 -0
  127. package/LICENSE +0 -29
  128. package/src/XSLTProcessor.test.js +0 -930
  129. package/src/xpath/evaluator.test.js +0 -1852
  130. package/src/xpath/tokenizer.test.js +0 -224
  131. package/src/xslt/engine.test.js +0 -3130
@@ -4,6 +4,9 @@
4
4
  * Native-compatible XSLTProcessor implementation for browser environments.
5
5
  * Based on W3C DOM Level 3 XSL Transformations and XSLT 1.0 Specification.
6
6
  *
7
+ * With the `xsltVersion: "auto"` option, XSLT 2.0/3.0 stylesheets are run by
8
+ * the optional peer dependency @tradik/xslt3 (see bridge/).
9
+ *
7
10
  * Reference: https://developer.mozilla.org/en-US/docs/Web/API/XSLTProcessor
8
11
  * XSLT 1.0: http://www.w3.org/TR/1999/REC-xslt-19991116
9
12
  * XPath 1.0: http://www.w3.org/TR/1999/REC-xpath-19991116
@@ -14,6 +17,23 @@
14
17
  */
15
18
 
16
19
  import { XsltEngine } from "./xslt/engine.js";
20
+ import { findParseError } from "./xslt/domParsing.js";
21
+ import { preloadedDocumentLoader } from "./async/preload.js";
22
+ import {
23
+ importStylesheetAsync,
24
+ transformAsync,
25
+ transformToStream,
26
+ } from "./async/processor.js";
27
+ import { createXslt3Engine } from "./bridge/engine.js";
28
+ import { loadXslt3 } from "./bridge/loader.js";
29
+ import { usesXslt3, xsltVersionMode } from "./bridge/version.js";
30
+
31
+ /**
32
+ * The native XSLTProcessor constructor that installGlobal() replaced, kept so
33
+ * that isNativeXSLTSupported() keeps probing the browser's implementation
34
+ * rather than this one.
35
+ */
36
+ let nativeProcessor = null;
17
37
 
18
38
  /**
19
39
  * XSLTProcessor
@@ -26,10 +46,186 @@ import { XsltEngine } from "./xslt/engine.js";
26
46
  * const fragment = processor.transformToFragment(xmlDoc, document);
27
47
  */
28
48
  export class XSLTProcessor {
29
- constructor() {
49
+ /**
50
+ * @param {object} [options] - Non-standard options (the native constructor
51
+ * takes none)
52
+ * @param {boolean} [options.legacyNameTests] - Deprecated: let unprefixed
53
+ * name tests (`item`, `@a`) also match nodes in a namespace, as before
54
+ * 1.2.0. XPath 1.0 and Chrome only match nodes in no namespace.
55
+ * @param {boolean} [options.enableDynamicEvaluate] - Allow EXSLT
56
+ * `dyn:evaluate()`, which evaluates XPath built from strings; enable it
57
+ * only for trusted input.
58
+ * @param {() => Date} [options.clock] - Clock for EXSLT current-time
59
+ * functions (reproducible output).
60
+ * @param {number} [options.maxTemplateDepth] - Deepest nesting of template
61
+ * instantiations; deeper recursion throws "Template recursion too deep"
62
+ * (default 3000, libxslt's limit).
63
+ * @param {"1.0"|"auto"} [options.xsltVersion] - "1.0" (default): every
64
+ * stylesheet runs with the XSLT 1.0 engine, a `version="2.0"` one in
65
+ * forwards-compatible mode, as in Chrome. "auto": a stylesheet whose
66
+ * version is 2.0 or more runs with @tradik/xslt3 (an optional peer
67
+ * dependency, loaded on demand; see {@link XSLTProcessor.preload}).
68
+ * @throws {RangeError} For an invalid `xsltVersion`
69
+ *
70
+ * @example
71
+ * // Temporary migration aid for stylesheets written against 1.1.x
72
+ * const processor = new XSLTProcessor({ legacyNameTests: true });
73
+ *
74
+ * @example
75
+ * // XSLT 2.0/3.0 stylesheets through the same API
76
+ * const processor = new XSLTProcessor({ xsltVersion: "auto" });
77
+ * await processor.importStylesheetAsync(xsl20Doc);
78
+ */
79
+ constructor(options = {}) {
80
+ this._options = {
81
+ legacyNameTests: options?.legacyNameTests === true,
82
+ enableDynamicEvaluate: options?.enableDynamicEvaluate === true,
83
+ clock: options?.clock ?? null,
84
+ maxTemplateDepth: options?.maxTemplateDepth,
85
+ xsltVersion: xsltVersionMode(options?.xsltVersion),
86
+ };
30
87
  this._engine = null;
31
88
  this._stylesheet = null;
32
89
  this._parameters = new Map();
90
+ this._stylesheetLoader = null;
91
+ this._documentLoader = null;
92
+ // Set by importStylesheet/importStylesheetAsync: the URI and modules of
93
+ // the stylesheet, and the document() documents preloaded asynchronously
94
+ this._stylesheetUri = undefined;
95
+ this._modules = [];
96
+ this._preloadedDocuments = null;
97
+ }
98
+
99
+ /**
100
+ * Load @tradik/xslt3, so that the synchronous API of processors created
101
+ * with `xsltVersion: "auto"` can run XSLT 2.0/3.0 stylesheets (the
102
+ * asynchronous API loads it by itself). Non-W3C.
103
+ *
104
+ * @param {"2.0"|"3.0"} [version] - The XSLT version to prepare for
105
+ * @returns {Promise<void>} Resolves once the engine is loaded
106
+ * @throws {RangeError} For another version (synchronously)
107
+ * @throws {Error} Rejects with "Cannot load @tradik/xslt3: install
108
+ * @tradik/xslt3 to run XSLT 2.0/3.0 stylesheets" when it is missing
109
+ *
110
+ * @example
111
+ * await XSLTProcessor.preload("3.0");
112
+ * const processor = new XSLTProcessor({ xsltVersion: "auto" });
113
+ * processor.importStylesheet(xsl30Doc);
114
+ */
115
+ static preload(version = "3.0") {
116
+ if (version !== "2.0" && version !== "3.0") {
117
+ throw new RangeError(
118
+ `Invalid XSLT version "${version}": expected "2.0" or "3.0"`,
119
+ );
120
+ }
121
+ return loadXslt3().then(() => undefined);
122
+ }
123
+
124
+ /**
125
+ * The underlying XSLT engine (advanced usage).
126
+ *
127
+ * The engine is created lazily by {@link XSLTProcessor#importStylesheet},
128
+ * so this getter returns `null` until a stylesheet has been imported.
129
+ * Prefer the public {@link XSLTProcessor#setStylesheetLoader} over reaching
130
+ * into the engine directly. A stylesheet run by @tradik/xslt3
131
+ * (`xsltVersion: "auto"`) has an `Xslt3Engine` (bridge/engine.js).
132
+ *
133
+ * @returns {import('./xslt/engine.js').XsltEngine|import('./bridge/engine.js').Xslt3Engine|null}
134
+ * The engine, or null before import
135
+ *
136
+ * @example
137
+ * processor.importStylesheet(xslDoc, '/styles/main.xsl');
138
+ * console.log(processor.engine.outputSettings.method);
139
+ */
140
+ get engine() {
141
+ return this._engine;
142
+ }
143
+
144
+ /**
145
+ * Sets the loader used to resolve `xsl:import` and `xsl:include` references.
146
+ *
147
+ * The loader is synchronous: it MUST return the external stylesheet as a
148
+ * `Document` or as an XML string (which is parsed automatically). Promises
149
+ * are not awaited by the engine, so pre-load remote stylesheets before
150
+ * calling `importStylesheet`.
151
+ *
152
+ * The loader may be set before or after `importStylesheet`. When set before,
153
+ * it is passed to the engine on creation, which is required for the loader to
154
+ * be used while the stylesheet is being compiled. When set after, the live
155
+ * engine is updated as well.
156
+ *
157
+ * @param {((href: string, baseUri?: string) => (Document|string))|null} loader
158
+ * The loader function, or null to remove a previously configured loader
159
+ * @returns {XSLTProcessor} This processor, to allow chaining
160
+ * @throws {TypeError} If the loader is neither a function nor null
161
+ *
162
+ * @example
163
+ * processor.setStylesheetLoader((href) => readFileSync(href, 'utf8'));
164
+ * processor.importStylesheet(mainStylesheet, '/styles/main.xsl');
165
+ */
166
+ setStylesheetLoader(loader) {
167
+ if (
168
+ loader !== null &&
169
+ loader !== undefined &&
170
+ typeof loader !== "function"
171
+ ) {
172
+ throw new TypeError(
173
+ "Failed to execute 'setStylesheetLoader' on 'XSLTProcessor': The loader argument must be a function or null.",
174
+ );
175
+ }
176
+
177
+ this._stylesheetLoader = loader ?? null;
178
+
179
+ // Keep an already created engine in sync
180
+ if (this._engine) {
181
+ this._engine.setStylesheetLoader(this._stylesheetLoader);
182
+ }
183
+
184
+ return this;
185
+ }
186
+
187
+ /**
188
+ * Sets the loader used to resolve the XSLT `document()` function.
189
+ *
190
+ * The loader is synchronous: it MUST return the referenced document as a
191
+ * `Document`, as an XML string (which is parsed automatically) or as `null`
192
+ * when the document cannot be provided. Returning `null`, like configuring no
193
+ * loader at all, makes `document()` evaluate to an empty node-set rather than
194
+ * failing the transformation.
195
+ *
196
+ * The loader may be set before or after `importStylesheet`; a live engine is
197
+ * kept in sync.
198
+ *
199
+ * @param {((uri: string, baseUri?: string) => (Document|string|null))|null} loader
200
+ * The loader function, or null to remove a previously configured loader
201
+ * @returns {XSLTProcessor} This processor, to allow chaining
202
+ * @throws {TypeError} If the loader is neither a function nor null
203
+ *
204
+ * @example
205
+ * // Node.js: resolve document() against the file system
206
+ * import { readFileSync } from 'node:fs';
207
+ * processor.setDocumentLoader((uri) => readFileSync(uri, 'utf8'));
208
+ * processor.importStylesheet(xslDoc, '/styles/main.xsl');
209
+ */
210
+ setDocumentLoader(loader) {
211
+ if (
212
+ loader !== null &&
213
+ loader !== undefined &&
214
+ typeof loader !== "function"
215
+ ) {
216
+ throw new TypeError(
217
+ "Failed to execute 'setDocumentLoader' on 'XSLTProcessor': The loader argument must be a function or null.",
218
+ );
219
+ }
220
+
221
+ this._documentLoader = loader ?? null;
222
+
223
+ // Keep an already created engine in sync
224
+ if (this._engine) {
225
+ this._engine.setDocumentLoader(this._engineDocumentLoader());
226
+ }
227
+
228
+ return this;
33
229
  }
34
230
 
35
231
  /**
@@ -40,14 +236,21 @@ export class XSLTProcessor {
40
236
  * <xsl:stylesheet> or <xsl:transform> element.
41
237
  *
42
238
  * @param {Node} style - The XSLT stylesheet to import (Document or Element)
239
+ * @param {string} [stylesheetUri] - Optional URI of the stylesheet, used as the
240
+ * base URI when resolving relative `xsl:import`/`xsl:include` hrefs. When
241
+ * omitted, hrefs are passed to the loader unresolved.
43
242
  * @returns {void}
243
+ * @throws {Error} When the stylesheet is malformed or invalid, e.g. has an
244
+ * invalid pattern (XSLT 1.0 section 5.2), or, with `xsltVersion: "auto"`,
245
+ * is an XSLT 2.0/3.0 stylesheet and @tradik/xslt3 has not been loaded
246
+ * ({@link XSLTProcessor.preload})
44
247
  *
45
248
  * @example
46
249
  * const parser = new DOMParser();
47
250
  * const xslDoc = parser.parseFromString(xslText, 'application/xml');
48
- * processor.importStylesheet(xslDoc);
251
+ * processor.importStylesheet(xslDoc, '/styles/main.xsl');
49
252
  */
50
- importStylesheet(style) {
253
+ importStylesheet(style, stylesheetUri) {
51
254
  if (!style) {
52
255
  throw new TypeError(
53
256
  "Failed to execute 'importStylesheet' on 'XSLTProcessor': 1 argument required, but only 0 present.",
@@ -62,28 +265,193 @@ export class XSLTProcessor {
62
265
  }
63
266
 
64
267
  // Check for parser errors
65
- const errorNode = style.querySelector
66
- ? style.querySelector("parsererror")
67
- : null;
68
- if (errorNode) {
268
+ if (findParseError(style)) {
69
269
  throw new Error("XSLT stylesheet contains parse errors");
70
270
  }
71
271
 
72
- this._stylesheet = style;
73
- this._engine = new XsltEngine();
272
+ this._compile(style, stylesheetUri);
273
+ }
274
+
275
+ /**
276
+ * Imports a stylesheet whose `xsl:import`/`xsl:include` modules and
277
+ * literal `document('...')` documents are loaded asynchronously first
278
+ * (non-W3C). The modules are loaded in parallel, each URI once; a cycle
279
+ * rejects with "Circular stylesheet reference detected". A document that
280
+ * fails to load is reported only if the transformation evaluates that
281
+ * `document()` call. Computed `document()` URIs still go through the
282
+ * synchronous {@link XSLTProcessor#setDocumentLoader} loader, and modules
283
+ * that were not preloaded through {@link XSLTProcessor#setStylesheetLoader}.
284
+ *
285
+ * @param {Node|string|Uint8Array|ArrayBuffer|ReadableStream|AsyncIterable} style -
286
+ * The stylesheet: a node, or markup / a stream of markup to parse
287
+ * @param {string} [stylesheetUri] - Base URI of relative hrefs and
288
+ * `document()` URIs
289
+ * @param {object} [options] - Loading options
290
+ * @param {Function} [options.loader] - `(uri, baseUri, { signal }) =>
291
+ * Promise<Document|string|Uint8Array|ArrayBuffer|Response|null>`; the
292
+ * global `fetch` by default
293
+ * @param {Function} [options.documentLoader] - Loader of `document()`
294
+ * documents, `options.loader` by default
295
+ * @param {AbortSignal} [options.signal] - Cancels loading
296
+ * @returns {Promise<void>} Resolves once the stylesheet is imported; on
297
+ * failure the processor keeps its previous stylesheet
298
+ *
299
+ * @example
300
+ * await processor.importStylesheetAsync(xslDoc, "https://example.com/xsl/main.xsl");
301
+ */
302
+ importStylesheetAsync(style, stylesheetUri, options = {}) {
303
+ return importStylesheetAsync(this, style, stylesheetUri, options);
304
+ }
305
+
306
+ /**
307
+ * Compile a stylesheet into a new engine; the processor is only updated
308
+ * when compiling succeeds.
309
+ *
310
+ * @param {Node} style - The stylesheet
311
+ * @param {string} [stylesheetUri] - Its URI
312
+ * @param {object} [preloaded] - What importStylesheetAsync loaded
313
+ * @param {Function|null} [preloaded.stylesheetLoader] - Module loader
314
+ * @param {Node[]} [preloaded.modules] - Every stylesheet module
315
+ * @param {Map<string, object>|null} [preloaded.documents] - document() documents
316
+ * @returns {void}
317
+ * @private
318
+ */
319
+ _compile(style, stylesheetUri, preloaded = {}) {
320
+ const {
321
+ stylesheetLoader = this._stylesheetLoader,
322
+ modules = [style],
323
+ documents = null,
324
+ } = preloaded;
325
+ const engineOptions = {
326
+ legacyNameTests: this._options.legacyNameTests,
327
+ enableDynamicEvaluate: this._options.enableDynamicEvaluate,
328
+ clock: this._options.clock,
329
+ maxTemplateDepth: this._options.maxTemplateDepth,
330
+ stylesheetLoader,
331
+ documentLoader: this._engineDocumentLoader(documents),
332
+ };
333
+ const engine = this._usesXslt3(style)
334
+ ? createXslt3Engine(style, engineOptions)
335
+ : new XsltEngine(engineOptions);
74
336
 
75
337
  // Apply any previously set parameters
76
338
  for (const [key, value] of this._parameters) {
77
- this._engine.globalParameters[key] = { value };
339
+ engine.setParameterValue(key, value);
340
+ }
341
+
342
+ // An invalid stylesheet (e.g. an invalid pattern) throws here and leaves
343
+ // the processor as it was
344
+ engine.importStylesheet(style, stylesheetUri);
345
+ // Modules not preloaded keep going through the configured loader
346
+ engine.setStylesheetLoader(this._stylesheetLoader);
347
+ this._engine = engine;
348
+ this._stylesheet = style;
349
+ this._stylesheetUri = stylesheetUri;
350
+ this._modules = modules;
351
+ this._preloadedDocuments = documents;
352
+ }
353
+
354
+ /**
355
+ * Whether a stylesheet is run by @tradik/xslt3 (`xsltVersion: "auto"` and
356
+ * version 2.0 or more).
357
+ *
358
+ * @param {Node} style - The stylesheet
359
+ * @returns {boolean} True for @tradik/xslt3
360
+ * @private
361
+ */
362
+ _usesXslt3(style) {
363
+ return usesXslt3(style, this._options.xsltVersion);
364
+ }
365
+
366
+ /**
367
+ * Load the engine a stylesheet needs before compiling it (async API).
368
+ *
369
+ * @param {Node} style - The stylesheet
370
+ * @returns {Promise<void>} Resolves once the engine is available
371
+ * @private
372
+ */
373
+ async _prepareEngine(style) {
374
+ if (this._usesXslt3(style)) await loadXslt3();
375
+ }
376
+
377
+ /**
378
+ * The document() loader given to the engine: preloaded documents first,
379
+ * then the configured synchronous loader.
380
+ *
381
+ * @param {Map<string, object>|null} [documents] - Preloaded documents
382
+ * @returns {Function|null} The loader
383
+ * @private
384
+ */
385
+ _engineDocumentLoader(documents = this._preloadedDocuments) {
386
+ return documents
387
+ ? preloadedDocumentLoader(documents, this._documentLoader)
388
+ : this._documentLoader;
389
+ }
390
+
391
+ /**
392
+ * Add asynchronously preloaded document() documents.
393
+ *
394
+ * @param {Map<string, object>} documents - Preloaded outcomes by URI
395
+ * @returns {void}
396
+ * @private
397
+ */
398
+ _usePreloadedDocuments(documents) {
399
+ this._preloadedDocuments = new Map([
400
+ ...(this._preloadedDocuments ?? []),
401
+ ...documents,
402
+ ]);
403
+ this._engine.setDocumentLoader(this._engineDocumentLoader());
404
+ }
405
+
406
+ /**
407
+ * Throw the native error of a transformation without stylesheet.
408
+ *
409
+ * @param {string} method - The method called
410
+ * @returns {void}
411
+ * @throws {Error} When no stylesheet has been imported
412
+ * @private
413
+ */
414
+ _requireStylesheet(method) {
415
+ if (!this._engine || !this._stylesheet) {
416
+ throw new Error(
417
+ `Failed to execute '${method}' on 'XSLTProcessor': No stylesheet has been imported.`,
418
+ );
78
419
  }
420
+ }
79
421
 
80
- this._engine.importStylesheet(style);
422
+ /**
423
+ * Throw the native error of a source that is not a document, element or
424
+ * fragment.
425
+ *
426
+ * @param {string} method - The method called
427
+ * @param {Node} source - The source node
428
+ * @returns {void}
429
+ * @throws {TypeError} For other node types
430
+ * @private
431
+ */
432
+ _checkSource(method, source) {
433
+ if (
434
+ source.nodeType !== 1 &&
435
+ source.nodeType !== 9 &&
436
+ source.nodeType !== 11
437
+ ) {
438
+ throw new TypeError(
439
+ `Failed to execute '${method}' on 'XSLTProcessor': The source is not a valid node type.`,
440
+ );
441
+ }
81
442
  }
82
443
 
83
444
  /**
84
445
  * Transforms the node source by applying the XSLT stylesheet.
85
446
  * Returns a document fragment.
86
447
  *
448
+ * As in Chrome, when `output` is an HTML document and the output method is
449
+ * `html` (declared, or detected from an `<html>` result root), the result
450
+ * is serialized and parsed by the HTML parser of `output`, so it holds
451
+ * real `HTMLElement`s (`<a>` is an `HTMLAnchorElement`, `<script>` runs
452
+ * when inserted). Other results keep the element names and namespaces of
453
+ * the result tree.
454
+ *
87
455
  * @param {Node} source - The XML document to transform
88
456
  * @param {Document} output - The document that will own the generated fragment
89
457
  * @returns {DocumentFragment} The transformed result as a DocumentFragment
@@ -105,22 +473,9 @@ export class XSLTProcessor {
105
473
  );
106
474
  }
107
475
 
108
- if (!this._engine || !this._stylesheet) {
109
- throw new Error(
110
- "Failed to execute 'transformToFragment' on 'XSLTProcessor': No stylesheet has been imported.",
111
- );
112
- }
476
+ this._requireStylesheet("transformToFragment");
113
477
 
114
- // Validate source node
115
- if (
116
- source.nodeType !== 1 &&
117
- source.nodeType !== 9 &&
118
- source.nodeType !== 11
119
- ) {
120
- throw new TypeError(
121
- "Failed to execute 'transformToFragment' on 'XSLTProcessor': The source is not a valid node type.",
122
- );
123
- }
478
+ this._checkSource("transformToFragment", source);
124
479
 
125
480
  // Validate output document
126
481
  if (output.nodeType !== 9) {
@@ -130,7 +485,7 @@ export class XSLTProcessor {
130
485
  }
131
486
 
132
487
  try {
133
- return this._engine.transform(source, output);
488
+ return this._engine.transformToFragment(source, output);
134
489
  } catch (error) {
135
490
  // Match native behavior - return null on error
136
491
  console.error("XSLT transformation error:", error);
@@ -156,32 +511,106 @@ export class XSLTProcessor {
156
511
  );
157
512
  }
158
513
 
159
- if (!this._engine || !this._stylesheet) {
160
- throw new Error(
161
- "Failed to execute 'transformToDocument' on 'XSLTProcessor': No stylesheet has been imported.",
162
- );
514
+ this._requireStylesheet("transformToDocument");
515
+
516
+ this._checkSource("transformToDocument", source);
517
+
518
+ try {
519
+ return this._engine.transformToDocument(source);
520
+ } catch (error) {
521
+ // Match native behavior - return null on error
522
+ console.error("XSLT transformation error:", error);
523
+ return null;
163
524
  }
525
+ }
164
526
 
165
- // Validate source node
166
- if (
167
- source.nodeType !== 1 &&
168
- source.nodeType !== 9 &&
169
- source.nodeType !== 11
170
- ) {
527
+ /**
528
+ * Transforms the node source by applying the XSLT stylesheet and serializes
529
+ * the result to a string honoring the stylesheet `xsl:output` settings.
530
+ *
531
+ * Non-W3C convenience method: the native XSLTProcessor has no equivalent.
532
+ * Output method, indentation, XML declaration, document type declaration,
533
+ * CDATA sections and `disable-output-escaping` are all honored
534
+ * (XSLT 1.0 section 16).
535
+ *
536
+ * @param {Node} source - The XML document to transform
537
+ * @returns {string|null} The serialized result, or null on a transformation error
538
+ *
539
+ * @example
540
+ * const xml = processor.transformToString(xmlDoc);
541
+ * // '<?xml version="1.0" encoding="UTF-8"?>\n<BAR>\n <QUX/>\n</BAR>'
542
+ */
543
+ transformToString(source) {
544
+ if (!source) {
171
545
  throw new TypeError(
172
- "Failed to execute 'transformToDocument' on 'XSLTProcessor': The source is not a valid node type.",
546
+ "Failed to execute 'transformToString' on 'XSLTProcessor': 1 argument required, but only 0 present.",
173
547
  );
174
548
  }
175
549
 
550
+ this._requireStylesheet("transformToString");
551
+
552
+ this._checkSource("transformToString", source);
553
+
176
554
  try {
177
- return this._engine.transformToDocument(source);
555
+ return this._engine.transformToString(source);
178
556
  } catch (error) {
179
- // Match native behavior - return null on error
557
+ // Match transformToDocument behavior - return null on error
180
558
  console.error("XSLT transformation error:", error);
181
559
  return null;
182
560
  }
183
561
  }
184
562
 
563
+ /**
564
+ * Transforms asynchronously and resolves with the serialized result
565
+ * (non-W3C). The source may be a node, markup, bytes, a `ReadableStream`
566
+ * or an async iterable of strings or bytes; streams are read to their end
567
+ * before parsing, because XSLT 1.0 needs the whole source tree. Unlike
568
+ * {@link XSLTProcessor#transformToString}, failures reject the promise.
569
+ *
570
+ * @param {Node|string|Uint8Array|ArrayBuffer|ReadableStream|AsyncIterable} source - Input
571
+ * @param {object} [options] - Options
572
+ * @param {AbortSignal} [options.signal] - Cancels loading and reading
573
+ * @param {Node|string|ReadableStream|AsyncIterable} [options.stylesheet] -
574
+ * A stylesheet to import first with importStylesheetAsync
575
+ * @param {string} [options.stylesheetUri] - The URI of that stylesheet
576
+ * @param {Function} [options.fetchStylesheet] - Asynchronous loader of its
577
+ * xsl:import/xsl:include modules (`fetch` by default)
578
+ * @param {Function} [options.fetchDocument] - Asynchronous loader of the
579
+ * literal document() documents (`fetchStylesheet` by default when a
580
+ * stylesheet is given)
581
+ * @returns {Promise<string>} The serialized result
582
+ *
583
+ * @example
584
+ * const html = await processor.transformAsync((await fetch("data.xml")).body);
585
+ */
586
+ transformAsync(source, options = {}) {
587
+ return transformAsync(this, source, options);
588
+ }
589
+
590
+ /**
591
+ * Transforms and returns the serialized result as a `ReadableStream` of
592
+ * strings (non-W3C). The result tree is built in memory on the first read;
593
+ * serialization then produces chunks of about `chunkSize` code units on
594
+ * demand, so the output is never one string and the first bytes are
595
+ * available before serialization ends. Failures error the stream.
596
+ *
597
+ * @param {Node|string|Uint8Array|ArrayBuffer|ReadableStream|AsyncIterable} source - Input
598
+ * @param {{signal?: AbortSignal, chunkSize?: number}} [options] - `signal`
599
+ * cancels (errors the stream with its reason); `chunkSize` defaults to
600
+ * 16384
601
+ * @returns {ReadableStream<string>} The serialized result
602
+ * @throws {Error} When no stylesheet has been imported
603
+ * @throws {TypeError} For a source node of the wrong type
604
+ * @throws {RangeError} For an invalid chunk size
605
+ *
606
+ * @example
607
+ * // Node.js
608
+ * Readable.fromWeb(processor.transformToStream(xmlDoc)).pipe(process.stdout);
609
+ */
610
+ transformToStream(source, options = {}) {
611
+ return transformToStream(this, source, options);
612
+ }
613
+
185
614
  /**
186
615
  * Sets a parameter in the XSLT stylesheet.
187
616
  *
@@ -212,7 +641,7 @@ export class XSLTProcessor {
212
641
 
213
642
  // If engine is already initialized, update it
214
643
  if (this._engine) {
215
- this._engine.globalParameters[key] = { value };
644
+ this._engine.setParameterValue(key, value);
216
645
  }
217
646
  }
218
647
 
@@ -279,7 +708,7 @@ export class XSLTProcessor {
279
708
  this._parameters.delete(key);
280
709
 
281
710
  if (this._engine) {
282
- delete this._engine.globalParameters[key];
711
+ this._engine.clearParameterValue(key);
283
712
  }
284
713
  }
285
714
 
@@ -297,13 +726,20 @@ export class XSLTProcessor {
297
726
  this._parameters.clear();
298
727
 
299
728
  if (this._engine) {
300
- this._engine.globalParameters = {};
729
+ this._engine.clearParameterValues();
301
730
  }
302
731
  }
303
732
 
304
733
  /**
305
734
  * Removes all parameters and stylesheets from the XSLTProcessor.
306
735
  *
736
+ * Per the W3C `XSLTProcessor` semantics, `reset()` clears stylesheet state and
737
+ * parameters only. The stylesheet and document loaders are processor
738
+ * configuration rather than stylesheet state, so they are deliberately
739
+ * preserved and stay effective for the next `importStylesheet()` call. Pass
740
+ * `null` to {@link XSLTProcessor#setStylesheetLoader} or
741
+ * {@link XSLTProcessor#setDocumentLoader} to remove them explicitly.
742
+ *
307
743
  * @returns {void}
308
744
  *
309
745
  * @example
@@ -313,22 +749,28 @@ export class XSLTProcessor {
313
749
  reset() {
314
750
  this._engine = null;
315
751
  this._stylesheet = null;
752
+ this._stylesheetUri = undefined;
753
+ this._modules = [];
754
+ this._preloadedDocuments = null;
316
755
  this._parameters.clear();
317
756
  }
318
757
  }
319
758
 
320
759
  /**
321
- * Check if native XSLTProcessor is available and functional
760
+ * Check if native XSLTProcessor is available and functional.
761
+ *
762
+ * After installGlobal() replaced the global, the original native constructor
763
+ * is probed; this implementation never counts as native.
322
764
  *
323
765
  * @returns {boolean} True if native XSLTProcessor works correctly
324
766
  */
325
767
  export function isNativeXSLTSupported() {
326
- if (typeof globalThis.XSLTProcessor === "undefined") {
327
- return false;
328
- }
768
+ const current = globalThis.XSLTProcessor;
769
+ const Native = current === XSLTProcessor ? nativeProcessor : current;
770
+ if (typeof Native !== "function") return false;
329
771
 
330
772
  try {
331
- const processor = new globalThis.XSLTProcessor();
773
+ const processor = new Native();
332
774
  const parser = new DOMParser();
333
775
 
334
776
  const xslt = parser.parseFromString(
@@ -361,6 +803,10 @@ export function installGlobal(force = false) {
361
803
  return false;
362
804
  }
363
805
 
806
+ const current = globalThis.XSLTProcessor;
807
+ if (typeof current === "function" && current !== XSLTProcessor) {
808
+ nativeProcessor = current;
809
+ }
364
810
  globalThis.XSLTProcessor = XSLTProcessor;
365
811
  return true;
366
812
  }