@tradik/xslt-processor 1.1.1 → 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 (122) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +102 -757
  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 +17 -0
  7. package/bin/lib/output.js +114 -0
  8. package/bin/lib/paths.js +3 -3
  9. package/bin/lib/transform.js +124 -33
  10. package/bin/xslt.js +26 -27
  11. package/dist/xslt-processor.browser.js +8784 -2720
  12. package/dist/xslt-processor.browser.js.map +4 -4
  13. package/dist/xslt-processor.browser.min.js +13 -6
  14. package/dist/xslt-processor.browser.min.js.map +4 -4
  15. package/dist/xslt-processor.cjs +8789 -2723
  16. package/dist/xslt-processor.cjs.map +4 -4
  17. package/dist/xslt-processor.d.cts +380 -21
  18. package/dist/xslt-processor.d.ts +380 -21
  19. package/dist/xslt-processor.js +8770 -2722
  20. package/dist/xslt-processor.js.map +4 -4
  21. package/package.json +51 -11
  22. package/src/XSLTProcessor.js +343 -66
  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 +16 -4
  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 +475 -355
  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 +1 -1
  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 +176 -2020
  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 +22 -9
  87. package/src/xslt/forwardsCompatible.js +75 -0
  88. package/src/xslt/functions.js +94 -15
  89. package/src/xslt/index.js +7 -1
  90. package/src/xslt/keys.js +51 -28
  91. package/src/xslt/literalResult.js +63 -7
  92. package/src/xslt/matchScope.js +116 -0
  93. package/src/xslt/number.js +171 -78
  94. package/src/xslt/numberFormat.js +124 -26
  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 +143 -6
  102. package/src/xslt/serializer/baseWriter.js +173 -66
  103. package/src/xslt/serializer/chunks.js +120 -0
  104. package/src/xslt/serializer/constants.js +14 -0
  105. package/src/xslt/serializer/encoding.js +327 -0
  106. package/src/xslt/serializer/escape.js +49 -12
  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 +123 -25
  111. package/src/xslt/serializer/settings.js +89 -13
  112. package/src/xslt/serializer/textSerializer.js +58 -10
  113. package/src/xslt/serializer/xhtmlDocument.js +103 -0
  114. package/src/xslt/serializer/xmlSerializer.js +113 -13
  115. package/src/xslt/serializer.js +50 -17
  116. package/src/xslt/sort.js +151 -0
  117. package/src/xslt/spaceNameTests.js +115 -0
  118. package/src/xslt/stylesheetChecks.js +206 -0
  119. package/src/xslt/stylesheetNamespaces.js +266 -0
  120. package/src/xslt/variables.js +152 -0
  121. package/src/xslt/whitespace.js +43 -27
  122. package/LICENSE +0 -29
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@tradik/xslt-processor",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "JavaScript implementation of XSLTProcessor for browser environments and CLI",
5
5
  "type": "module",
6
+ "workspaces": [
7
+ "packages/*"
8
+ ],
6
9
  "main": "dist/xslt-processor.cjs",
7
10
  "module": "dist/xslt-processor.js",
8
11
  "browser": "dist/xslt-processor.js",
@@ -38,13 +41,31 @@
38
41
  },
39
42
  "scripts": {
40
43
  "build": "node scripts/build.js",
41
- "test": "node --test --experimental-test-coverage src/*.test.js src/**/*.test.js",
44
+ "test": "node --test --experimental-test-coverage --test-coverage-lines=100 --test-coverage-functions=100 --test-coverage-branches=96 --test-coverage-exclude=\"**/*.test.js\" --test-coverage-exclude=\"packages/**\" \"src/**/*.test.js\"",
45
+ "test:xslt3": "npm test --workspace @tradik/xslt3",
46
+ "suites:fetch": "node packages/xslt3/test-suites/fetch.mjs all",
47
+ "test:suites:unit": "node --test --experimental-test-coverage --test-coverage-include=\"packages/xslt3/test-suites/**\" --test-coverage-exclude=\"**/*.test.mjs\" --test-coverage-lines=96 \"packages/xslt3/test-suites/**/*.test.mjs\"",
48
+ "test:qt3:parse": "node packages/xslt3/test-suites/run.mjs qt3 --parse-only",
49
+ "test:qt3": "node packages/xslt3/test-suites/run.mjs qt3",
50
+ "test:xslt30": "node packages/xslt3/test-suites/run.mjs xslt30",
42
51
  "test:watch": "node --test --watch src/**/*.test.js",
43
- "test:browser": "node tests/browser-test-runner.js",
44
- "lint": "eslint src/",
45
- "format": "prettier --write \"src/**/*.js\" \"bin/**/*.js\"",
46
- "format:check": "prettier --check \"src/**/*.js\" \"bin/**/*.js\"",
47
- "prepublishOnly": "npm run build && npm run test"
52
+ "test:conformance": "node scripts/conformance.mjs",
53
+ "test:dom": "node scripts/test-dom.mjs",
54
+ "test:conformance:unit": "node --test \"scripts/conformance/*.test.mjs\"",
55
+ "conformance:fetch": "node scripts/fetch-conformance.mjs",
56
+ "lint": "eslint src/ bin/ scripts/ tests/browser/ site/ packages/",
57
+ "format": "prettier --write \"src/**/*.js\" \"bin/**/*.js\" \"scripts/**/*.{js,mjs}\" \"tests/browser/**/*.mjs\" \"site/scripts/*.mjs\" \"site/templates/**/js/*.js\" \"packages/*/src/**/*.js\" \"packages/*/test-suites/**/*.mjs\"",
58
+ "format:check": "prettier --check \"src/**/*.js\" \"bin/**/*.js\" \"scripts/**/*.{js,mjs}\" \"tests/browser/**/*.mjs\" \"site/scripts/*.mjs\" \"site/templates/**/js/*.js\" \"packages/*/src/**/*.js\" \"packages/*/test-suites/**/*.mjs\"",
59
+ "prepublishOnly": "npm run build && npm run test",
60
+ "docs:check": "node scripts/check-links.mjs",
61
+ "bench": "node scripts/benchmark/run.mjs",
62
+ "bench:xpath": "node scripts/benchmark/run.mjs --suite xpath",
63
+ "bench:xslt": "node scripts/benchmark/run.mjs --suite xslt",
64
+ "test:bench": "node --test \"scripts/benchmark/*.test.mjs\"",
65
+ "test:browser": "playwright test -c tests/browser/playwright.config.mjs",
66
+ "test:binaries": "node --test \"scripts/binaries/*.test.mjs\" \"scripts/lib/*.test.mjs\"",
67
+ "test:site": "node --test --experimental-test-coverage --test-coverage-include=\"site/**\" --test-coverage-lines=96 \"site/scripts/*.test.mjs\"",
68
+ "site:content": "node site/scripts/build-content.mjs"
48
69
  },
49
70
  "keywords": [
50
71
  "xslt",
@@ -65,15 +86,34 @@
65
86
  "bugs": {
66
87
  "url": "https://github.com/spagu/XSLT-Processor/issues"
67
88
  },
68
- "homepage": "https://github.com/spagu/XSLT-Processor#readme",
89
+ "homepage": "https://xslt-processor.tradik.com/",
69
90
  "engines": {
70
91
  "node": ">=20.19.0"
71
92
  },
72
93
  "devDependencies": {
73
94
  "@eslint/js": "^10.0.1",
95
+ "@playwright/test": "^1.63.0",
96
+ "@xmldom/xmldom": "^0.9.12",
74
97
  "esbuild": "^0.28.2",
75
- "eslint": "^10.10.0",
76
- "jsdom": "^29.1.1",
77
- "prettier": "^3.9.6"
98
+ "eslint": "^10.11.0",
99
+ "jsdom": "^30.1.1",
100
+ "linkedom": "^0.18.13",
101
+ "prettier": "^3.9.9"
102
+ },
103
+ "peerDependencies": {
104
+ "@tradik/xslt3": "^1.0.0",
105
+ "@xmldom/xmldom": ">=0.9.0",
106
+ "jsdom": ">=25.0.0"
107
+ },
108
+ "peerDependenciesMeta": {
109
+ "@tradik/xslt3": {
110
+ "optional": true
111
+ },
112
+ "@xmldom/xmldom": {
113
+ "optional": true
114
+ },
115
+ "jsdom": {
116
+ "optional": true
117
+ }
78
118
  }
79
119
  }
@@ -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,12 +46,79 @@ 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();
33
90
  this._stylesheetLoader = null;
34
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);
35
122
  }
36
123
 
37
124
  /**
@@ -40,9 +127,11 @@ export class XSLTProcessor {
40
127
  * The engine is created lazily by {@link XSLTProcessor#importStylesheet},
41
128
  * so this getter returns `null` until a stylesheet has been imported.
42
129
  * Prefer the public {@link XSLTProcessor#setStylesheetLoader} over reaching
43
- * into the engine directly.
130
+ * into the engine directly. A stylesheet run by @tradik/xslt3
131
+ * (`xsltVersion: "auto"`) has an `Xslt3Engine` (bridge/engine.js).
44
132
  *
45
- * @returns {import('./xslt/engine.js').XsltEngine|null} The engine, or null before import
133
+ * @returns {import('./xslt/engine.js').XsltEngine|import('./bridge/engine.js').Xslt3Engine|null}
134
+ * The engine, or null before import
46
135
  *
47
136
  * @example
48
137
  * processor.importStylesheet(xslDoc, '/styles/main.xsl');
@@ -133,7 +222,7 @@ export class XSLTProcessor {
133
222
 
134
223
  // Keep an already created engine in sync
135
224
  if (this._engine) {
136
- this._engine.setDocumentLoader(this._documentLoader);
225
+ this._engine.setDocumentLoader(this._engineDocumentLoader());
137
226
  }
138
227
 
139
228
  return this;
@@ -151,6 +240,10 @@ export class XSLTProcessor {
151
240
  * base URI when resolving relative `xsl:import`/`xsl:include` hrefs. When
152
241
  * omitted, hrefs are passed to the loader unresolved.
153
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})
154
247
  *
155
248
  * @example
156
249
  * const parser = new DOMParser();
@@ -172,31 +265,193 @@ export class XSLTProcessor {
172
265
  }
173
266
 
174
267
  // Check for parser errors
175
- const errorNode = style.querySelector
176
- ? style.querySelector("parsererror")
177
- : null;
178
- if (errorNode) {
268
+ if (findParseError(style)) {
179
269
  throw new Error("XSLT stylesheet contains parse errors");
180
270
  }
181
271
 
182
- this._stylesheet = style;
183
- this._engine = new XsltEngine({
184
- stylesheetLoader: this._stylesheetLoader,
185
- documentLoader: this._documentLoader,
186
- });
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);
187
336
 
188
337
  // Apply any previously set parameters
189
338
  for (const [key, value] of this._parameters) {
190
- this._engine.setParameterValue(key, value);
339
+ engine.setParameterValue(key, value);
191
340
  }
192
341
 
193
- this._engine.importStylesheet(style, stylesheetUri);
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
+ );
419
+ }
420
+ }
421
+
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
+ }
194
442
  }
195
443
 
196
444
  /**
197
445
  * Transforms the node source by applying the XSLT stylesheet.
198
446
  * Returns a document fragment.
199
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
+ *
200
455
  * @param {Node} source - The XML document to transform
201
456
  * @param {Document} output - The document that will own the generated fragment
202
457
  * @returns {DocumentFragment} The transformed result as a DocumentFragment
@@ -218,22 +473,9 @@ export class XSLTProcessor {
218
473
  );
219
474
  }
220
475
 
221
- if (!this._engine || !this._stylesheet) {
222
- throw new Error(
223
- "Failed to execute 'transformToFragment' on 'XSLTProcessor': No stylesheet has been imported.",
224
- );
225
- }
476
+ this._requireStylesheet("transformToFragment");
226
477
 
227
- // Validate source node
228
- if (
229
- source.nodeType !== 1 &&
230
- source.nodeType !== 9 &&
231
- source.nodeType !== 11
232
- ) {
233
- throw new TypeError(
234
- "Failed to execute 'transformToFragment' on 'XSLTProcessor': The source is not a valid node type.",
235
- );
236
- }
478
+ this._checkSource("transformToFragment", source);
237
479
 
238
480
  // Validate output document
239
481
  if (output.nodeType !== 9) {
@@ -243,7 +485,7 @@ export class XSLTProcessor {
243
485
  }
244
486
 
245
487
  try {
246
- return this._engine.transform(source, output);
488
+ return this._engine.transformToFragment(source, output);
247
489
  } catch (error) {
248
490
  // Match native behavior - return null on error
249
491
  console.error("XSLT transformation error:", error);
@@ -269,22 +511,9 @@ export class XSLTProcessor {
269
511
  );
270
512
  }
271
513
 
272
- if (!this._engine || !this._stylesheet) {
273
- throw new Error(
274
- "Failed to execute 'transformToDocument' on 'XSLTProcessor': No stylesheet has been imported.",
275
- );
276
- }
514
+ this._requireStylesheet("transformToDocument");
277
515
 
278
- // Validate source node
279
- if (
280
- source.nodeType !== 1 &&
281
- source.nodeType !== 9 &&
282
- source.nodeType !== 11
283
- ) {
284
- throw new TypeError(
285
- "Failed to execute 'transformToDocument' on 'XSLTProcessor': The source is not a valid node type.",
286
- );
287
- }
516
+ this._checkSource("transformToDocument", source);
288
517
 
289
518
  try {
290
519
  return this._engine.transformToDocument(source);
@@ -318,22 +547,9 @@ export class XSLTProcessor {
318
547
  );
319
548
  }
320
549
 
321
- if (!this._engine || !this._stylesheet) {
322
- throw new Error(
323
- "Failed to execute 'transformToString' on 'XSLTProcessor': No stylesheet has been imported.",
324
- );
325
- }
550
+ this._requireStylesheet("transformToString");
326
551
 
327
- // Validate source node
328
- if (
329
- source.nodeType !== 1 &&
330
- source.nodeType !== 9 &&
331
- source.nodeType !== 11
332
- ) {
333
- throw new TypeError(
334
- "Failed to execute 'transformToString' on 'XSLTProcessor': The source is not a valid node type.",
335
- );
336
- }
552
+ this._checkSource("transformToString", source);
337
553
 
338
554
  try {
339
555
  return this._engine.transformToString(source);
@@ -344,6 +560,57 @@ export class XSLTProcessor {
344
560
  }
345
561
  }
346
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
+
347
614
  /**
348
615
  * Sets a parameter in the XSLT stylesheet.
349
616
  *
@@ -482,22 +749,28 @@ export class XSLTProcessor {
482
749
  reset() {
483
750
  this._engine = null;
484
751
  this._stylesheet = null;
752
+ this._stylesheetUri = undefined;
753
+ this._modules = [];
754
+ this._preloadedDocuments = null;
485
755
  this._parameters.clear();
486
756
  }
487
757
  }
488
758
 
489
759
  /**
490
- * 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.
491
764
  *
492
765
  * @returns {boolean} True if native XSLTProcessor works correctly
493
766
  */
494
767
  export function isNativeXSLTSupported() {
495
- if (typeof globalThis.XSLTProcessor === "undefined") {
496
- return false;
497
- }
768
+ const current = globalThis.XSLTProcessor;
769
+ const Native = current === XSLTProcessor ? nativeProcessor : current;
770
+ if (typeof Native !== "function") return false;
498
771
 
499
772
  try {
500
- const processor = new globalThis.XSLTProcessor();
773
+ const processor = new Native();
501
774
  const parser = new DOMParser();
502
775
 
503
776
  const xslt = parser.parseFromString(
@@ -530,6 +803,10 @@ export function installGlobal(force = false) {
530
803
  return false;
531
804
  }
532
805
 
806
+ const current = globalThis.XSLTProcessor;
807
+ if (typeof current === "function" && current !== XSLTProcessor) {
808
+ nativeProcessor = current;
809
+ }
533
810
  globalThis.XSLTProcessor = XSLTProcessor;
534
811
  return true;
535
812
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * AbortSignal Helpers
3
+ *
4
+ * Every asynchronous entry point takes an optional `signal`. It is checked
5
+ * between steps (and between output chunks), passed to loaders, and raced
6
+ * against loader promises so that a loader ignoring the signal cannot keep an
7
+ * aborted call pending. The synchronous parts (compiling the stylesheet,
8
+ * building the result tree) cannot be interrupted once started.
9
+ *
10
+ * @module async/abort
11
+ */
12
+
13
+ /**
14
+ * The reason of an aborted signal, with a DOMException fallback for
15
+ * implementations without `reason`.
16
+ *
17
+ * @param {AbortSignal} signal - An aborted signal
18
+ * @returns {*} The abort reason
19
+ */
20
+ function abortReason(signal) {
21
+ return (
22
+ signal.reason ??
23
+ new globalThis.DOMException("This operation was aborted", "AbortError")
24
+ );
25
+ }
26
+
27
+ /**
28
+ * Throw the abort reason when a signal is aborted.
29
+ *
30
+ * @param {AbortSignal} [signal] - Optional signal
31
+ * @returns {void}
32
+ * @throws {*} The abort reason
33
+ *
34
+ * @example
35
+ * throwIfAborted(options.signal);
36
+ */
37
+ export function throwIfAborted(signal) {
38
+ if (signal?.aborted) throw abortReason(signal);
39
+ }
40
+
41
+ /**
42
+ * Settle with a promise, or reject with the abort reason as soon as the
43
+ * signal aborts, whichever comes first.
44
+ *
45
+ * @template T
46
+ * @param {Promise<T>|T} work - The pending work
47
+ * @param {AbortSignal} [signal] - Optional signal
48
+ * @returns {Promise<T>} The work's outcome, or the abort rejection
49
+ *
50
+ * @example
51
+ * const text = await abortable(loader(uri), signal);
52
+ */
53
+ export function abortable(work, signal) {
54
+ if (!signal) return Promise.resolve(work);
55
+ if (signal.aborted) return Promise.reject(abortReason(signal));
56
+ return new Promise((resolve, reject) => {
57
+ const onAbort = () => reject(abortReason(signal));
58
+ signal.addEventListener("abort", onAbort, { once: true });
59
+ Promise.resolve(work)
60
+ .then(resolve, reject)
61
+ .finally(() => signal.removeEventListener("abort", onAbort));
62
+ });
63
+ }