@tradik/xslt-processor 1.0.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.
@@ -0,0 +1,368 @@
1
+ /**
2
+ * XSLTProcessor - JavaScript Implementation
3
+ *
4
+ * Native-compatible XSLTProcessor implementation for browser environments.
5
+ * Based on W3C DOM Level 3 XSL Transformations and XSLT 1.0 Specification.
6
+ *
7
+ * Reference: https://developer.mozilla.org/en-US/docs/Web/API/XSLTProcessor
8
+ * XSLT 1.0: http://www.w3.org/TR/1999/REC-xslt-19991116
9
+ * XPath 1.0: http://www.w3.org/TR/1999/REC-xpath-19991116
10
+ *
11
+ * This implementation provides 1:1 API compatibility with the native
12
+ * browser XSLTProcessor. It can be used as a replacement when native
13
+ * XSLT support is deprecated or unavailable.
14
+ */
15
+
16
+ import { XsltEngine } from "./xslt/engine.js";
17
+
18
+ /**
19
+ * XSLTProcessor
20
+ *
21
+ * Applies XSLT stylesheet transformations to XML documents.
22
+ *
23
+ * @example
24
+ * const processor = new XSLTProcessor();
25
+ * processor.importStylesheet(xsltDoc);
26
+ * const fragment = processor.transformToFragment(xmlDoc, document);
27
+ */
28
+ export class XSLTProcessor {
29
+ constructor() {
30
+ this._engine = null;
31
+ this._stylesheet = null;
32
+ this._parameters = new Map();
33
+ }
34
+
35
+ /**
36
+ * Imports the XSLT stylesheet.
37
+ *
38
+ * If the given node is a document node, you can pass in a full XSL Transform
39
+ * or a literal result element transform; otherwise, it must be an
40
+ * <xsl:stylesheet> or <xsl:transform> element.
41
+ *
42
+ * @param {Node} style - The XSLT stylesheet to import (Document or Element)
43
+ * @returns {void}
44
+ *
45
+ * @example
46
+ * const parser = new DOMParser();
47
+ * const xslDoc = parser.parseFromString(xslText, 'application/xml');
48
+ * processor.importStylesheet(xslDoc);
49
+ */
50
+ importStylesheet(style) {
51
+ if (!style) {
52
+ throw new TypeError(
53
+ "Failed to execute 'importStylesheet' on 'XSLTProcessor': 1 argument required, but only 0 present.",
54
+ );
55
+ }
56
+
57
+ // Validate node type
58
+ if (style.nodeType !== 1 && style.nodeType !== 9) {
59
+ throw new TypeError(
60
+ "Failed to execute 'importStylesheet' on 'XSLTProcessor': The node provided is not a Document or Element.",
61
+ );
62
+ }
63
+
64
+ // Check for parser errors
65
+ const errorNode = style.querySelector
66
+ ? style.querySelector("parsererror")
67
+ : null;
68
+ if (errorNode) {
69
+ throw new Error("XSLT stylesheet contains parse errors");
70
+ }
71
+
72
+ this._stylesheet = style;
73
+ this._engine = new XsltEngine();
74
+
75
+ // Apply any previously set parameters
76
+ for (const [key, value] of this._parameters) {
77
+ this._engine.globalParameters[key] = { value };
78
+ }
79
+
80
+ this._engine.importStylesheet(style);
81
+ }
82
+
83
+ /**
84
+ * Transforms the node source by applying the XSLT stylesheet.
85
+ * Returns a document fragment.
86
+ *
87
+ * @param {Node} source - The XML document to transform
88
+ * @param {Document} output - The document that will own the generated fragment
89
+ * @returns {DocumentFragment} The transformed result as a DocumentFragment
90
+ *
91
+ * @example
92
+ * const fragment = processor.transformToFragment(xmlDoc, document);
93
+ * document.getElementById('output').appendChild(fragment);
94
+ */
95
+ transformToFragment(source, output) {
96
+ if (!source) {
97
+ throw new TypeError(
98
+ "Failed to execute 'transformToFragment' on 'XSLTProcessor': 2 arguments required, but only 0 present.",
99
+ );
100
+ }
101
+
102
+ if (!output) {
103
+ throw new TypeError(
104
+ "Failed to execute 'transformToFragment' on 'XSLTProcessor': 2 arguments required, but only 1 present.",
105
+ );
106
+ }
107
+
108
+ if (!this._engine || !this._stylesheet) {
109
+ throw new Error(
110
+ "Failed to execute 'transformToFragment' on 'XSLTProcessor': No stylesheet has been imported.",
111
+ );
112
+ }
113
+
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
+ }
124
+
125
+ // Validate output document
126
+ if (output.nodeType !== 9) {
127
+ throw new TypeError(
128
+ "Failed to execute 'transformToFragment' on 'XSLTProcessor': The output is not a Document.",
129
+ );
130
+ }
131
+
132
+ try {
133
+ return this._engine.transform(source, output);
134
+ } catch (error) {
135
+ // Match native behavior - return null on error
136
+ console.error("XSLT transformation error:", error);
137
+ return null;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Transforms the node source by applying the XSLT stylesheet.
143
+ * Returns a full XML document.
144
+ *
145
+ * @param {Node} source - The XML document to transform
146
+ * @returns {XMLDocument} The transformed result as an XMLDocument
147
+ *
148
+ * @example
149
+ * const resultDoc = processor.transformToDocument(xmlDoc);
150
+ * const serialized = new XMLSerializer().serializeToString(resultDoc);
151
+ */
152
+ transformToDocument(source) {
153
+ if (!source) {
154
+ throw new TypeError(
155
+ "Failed to execute 'transformToDocument' on 'XSLTProcessor': 1 argument required, but only 0 present.",
156
+ );
157
+ }
158
+
159
+ if (!this._engine || !this._stylesheet) {
160
+ throw new Error(
161
+ "Failed to execute 'transformToDocument' on 'XSLTProcessor': No stylesheet has been imported.",
162
+ );
163
+ }
164
+
165
+ // Validate source node
166
+ if (
167
+ source.nodeType !== 1 &&
168
+ source.nodeType !== 9 &&
169
+ source.nodeType !== 11
170
+ ) {
171
+ throw new TypeError(
172
+ "Failed to execute 'transformToDocument' on 'XSLTProcessor': The source is not a valid node type.",
173
+ );
174
+ }
175
+
176
+ try {
177
+ return this._engine.transformToDocument(source);
178
+ } catch (error) {
179
+ // Match native behavior - return null on error
180
+ console.error("XSLT transformation error:", error);
181
+ return null;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Sets a parameter in the XSLT stylesheet.
187
+ *
188
+ * @param {string|null} namespaceURI - The namespace URI of the XSLT parameter (use null for no namespace)
189
+ * @param {string} localName - The local name of the parameter
190
+ * @param {*} value - The value to set (string, number, boolean, or node-set)
191
+ * @returns {void}
192
+ *
193
+ * @example
194
+ * processor.setParameter(null, 'sortOrder', 'ascending');
195
+ * processor.setParameter('http://example.com/ns', 'limit', 10);
196
+ */
197
+ setParameter(namespaceURI, localName, value) {
198
+ if (arguments.length < 3) {
199
+ throw new TypeError(
200
+ `Failed to execute 'setParameter' on 'XSLTProcessor': 3 arguments required, but only ${arguments.length} present.`,
201
+ );
202
+ }
203
+
204
+ if (typeof localName !== "string" || localName === "") {
205
+ throw new TypeError(
206
+ "Failed to execute 'setParameter' on 'XSLTProcessor': The localName argument must be a non-empty string.",
207
+ );
208
+ }
209
+
210
+ const key = namespaceURI ? `{${namespaceURI}}${localName}` : localName;
211
+ this._parameters.set(key, value);
212
+
213
+ // If engine is already initialized, update it
214
+ if (this._engine) {
215
+ this._engine.globalParameters[key] = { value };
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Gets the value of a parameter from the XSLT stylesheet.
221
+ *
222
+ * @param {string|null} namespaceURI - The namespace URI of the parameter
223
+ * @param {string} localName - The local name of the parameter
224
+ * @returns {*} The parameter value, or empty string if not set
225
+ *
226
+ * @example
227
+ * const sortOrder = processor.getParameter(null, 'sortOrder');
228
+ */
229
+ getParameter(namespaceURI, localName) {
230
+ if (arguments.length < 2) {
231
+ throw new TypeError(
232
+ `Failed to execute 'getParameter' on 'XSLTProcessor': 2 arguments required, but only ${arguments.length} present.`,
233
+ );
234
+ }
235
+
236
+ if (typeof localName !== "string") {
237
+ throw new TypeError(
238
+ "Failed to execute 'getParameter' on 'XSLTProcessor': The localName argument must be a string.",
239
+ );
240
+ }
241
+
242
+ const key = namespaceURI ? `{${namespaceURI}}${localName}` : localName;
243
+
244
+ if (this._parameters.has(key)) {
245
+ return this._parameters.get(key);
246
+ }
247
+
248
+ // Return empty string for unset parameters (matches native behavior)
249
+ return "";
250
+ }
251
+
252
+ /**
253
+ * Removes a parameter from the XSLT processor.
254
+ *
255
+ * The XSLTProcessor will use the default value for the parameter
256
+ * as specified in the XSLT stylesheet.
257
+ *
258
+ * @param {string|null} namespaceURI - The namespace URI of the parameter
259
+ * @param {string} localName - The local name of the parameter
260
+ * @returns {void}
261
+ *
262
+ * @example
263
+ * processor.removeParameter(null, 'sortOrder');
264
+ */
265
+ removeParameter(namespaceURI, localName) {
266
+ if (arguments.length < 2) {
267
+ throw new TypeError(
268
+ `Failed to execute 'removeParameter' on 'XSLTProcessor': 2 arguments required, but only ${arguments.length} present.`,
269
+ );
270
+ }
271
+
272
+ if (typeof localName !== "string") {
273
+ throw new TypeError(
274
+ "Failed to execute 'removeParameter' on 'XSLTProcessor': The localName argument must be a string.",
275
+ );
276
+ }
277
+
278
+ const key = namespaceURI ? `{${namespaceURI}}${localName}` : localName;
279
+ this._parameters.delete(key);
280
+
281
+ if (this._engine) {
282
+ delete this._engine.globalParameters[key];
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Removes all set parameters from the XSLTProcessor.
288
+ *
289
+ * The processor will use default values specified in the XSLT stylesheet.
290
+ *
291
+ * @returns {void}
292
+ *
293
+ * @example
294
+ * processor.clearParameters();
295
+ */
296
+ clearParameters() {
297
+ this._parameters.clear();
298
+
299
+ if (this._engine) {
300
+ this._engine.globalParameters = {};
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Removes all parameters and stylesheets from the XSLTProcessor.
306
+ *
307
+ * @returns {void}
308
+ *
309
+ * @example
310
+ * processor.reset();
311
+ * // Now need to call importStylesheet() again before transforming
312
+ */
313
+ reset() {
314
+ this._engine = null;
315
+ this._stylesheet = null;
316
+ this._parameters.clear();
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Check if native XSLTProcessor is available and functional
322
+ *
323
+ * @returns {boolean} True if native XSLTProcessor works correctly
324
+ */
325
+ export function isNativeXSLTSupported() {
326
+ if (typeof globalThis.XSLTProcessor === "undefined") {
327
+ return false;
328
+ }
329
+
330
+ try {
331
+ const processor = new globalThis.XSLTProcessor();
332
+ const parser = new DOMParser();
333
+
334
+ const xslt = parser.parseFromString(
335
+ `<?xml version="1.0"?>
336
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
337
+ <xsl:template match="/"><test/></xsl:template>
338
+ </xsl:stylesheet>`,
339
+ "application/xml",
340
+ );
341
+
342
+ processor.importStylesheet(xslt);
343
+
344
+ const xml = parser.parseFromString("<root/>", "application/xml");
345
+ const result = processor.transformToFragment(xml, document);
346
+
347
+ return result !== null && result.childNodes.length > 0;
348
+ } catch {
349
+ return false;
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Install as global XSLTProcessor replacement if native is not functional
355
+ *
356
+ * @param {boolean} force - Force installation even if native is available
357
+ * @returns {boolean} True if installed as global
358
+ */
359
+ export function installGlobal(force = false) {
360
+ if (!force && isNativeXSLTSupported()) {
361
+ return false;
362
+ }
363
+
364
+ globalThis.XSLTProcessor = XSLTProcessor;
365
+ return true;
366
+ }
367
+
368
+ export default XSLTProcessor;