@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.
- package/LICENSE +29 -0
- package/LICENSE.md +28 -0
- package/README.md +634 -0
- package/bin/xslt.js +208 -0
- package/dist/xslt-processor.browser.js +3302 -0
- package/dist/xslt-processor.browser.js.map +7 -0
- package/dist/xslt-processor.browser.min.js +6 -0
- package/dist/xslt-processor.browser.min.js.map +7 -0
- package/dist/xslt-processor.cjs +3311 -0
- package/dist/xslt-processor.cjs.map +7 -0
- package/dist/xslt-processor.d.ts +211 -0
- package/dist/xslt-processor.js +3271 -0
- package/dist/xslt-processor.js.map +7 -0
- package/package.json +68 -0
- package/src/XSLTProcessor.js +368 -0
- package/src/XSLTProcessor.test.js +930 -0
- package/src/index.js +66 -0
- package/src/xpath/evaluator.js +1012 -0
- package/src/xpath/evaluator.test.js +1852 -0
- package/src/xpath/index.js +67 -0
- package/src/xpath/parser.js +595 -0
- package/src/xpath/tokenizer.js +383 -0
- package/src/xpath/tokenizer.test.js +224 -0
- package/src/xslt/engine.js +1812 -0
- package/src/xslt/engine.test.js +3130 -0
- package/src/xslt/index.js +6 -0
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XSLTProcessor Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for JavaScript XSLTProcessor implementation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, beforeEach } from "node:test";
|
|
8
|
+
import assert from "node:assert";
|
|
9
|
+
import { JSDOM } from "jsdom";
|
|
10
|
+
import {
|
|
11
|
+
XSLTProcessor,
|
|
12
|
+
isNativeXSLTSupported,
|
|
13
|
+
installGlobal,
|
|
14
|
+
} from "./XSLTProcessor.js";
|
|
15
|
+
|
|
16
|
+
// Setup JSDOM environment
|
|
17
|
+
function setupDOM() {
|
|
18
|
+
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
|
|
19
|
+
contentType: "text/html",
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
global.document = dom.window.document;
|
|
23
|
+
global.DOMParser = dom.window.DOMParser;
|
|
24
|
+
global.XMLSerializer = dom.window.XMLSerializer;
|
|
25
|
+
|
|
26
|
+
return dom;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseXML(xmlString) {
|
|
30
|
+
const parser = new DOMParser();
|
|
31
|
+
return parser.parseFromString(xmlString, "application/xml");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("XSLTProcessor", () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
setupDOM();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("Constructor", () => {
|
|
40
|
+
it("should create a new XSLTProcessor instance", () => {
|
|
41
|
+
const processor = new XSLTProcessor();
|
|
42
|
+
assert.ok(processor instanceof XSLTProcessor);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("importStylesheet", () => {
|
|
47
|
+
it("should import a valid XSLT stylesheet", () => {
|
|
48
|
+
const processor = new XSLTProcessor();
|
|
49
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
50
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
51
|
+
<xsl:template match="/">
|
|
52
|
+
<html><body>Hello</body></html>
|
|
53
|
+
</xsl:template>
|
|
54
|
+
</xsl:stylesheet>
|
|
55
|
+
`);
|
|
56
|
+
|
|
57
|
+
assert.doesNotThrow(() => {
|
|
58
|
+
processor.importStylesheet(xslt);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("should throw when no argument provided", () => {
|
|
63
|
+
const processor = new XSLTProcessor();
|
|
64
|
+
assert.throws(() => {
|
|
65
|
+
processor.importStylesheet();
|
|
66
|
+
}, /1 argument required/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("should throw for invalid node type", () => {
|
|
70
|
+
const processor = new XSLTProcessor();
|
|
71
|
+
assert.throws(() => {
|
|
72
|
+
processor.importStylesheet(document.createTextNode("text"));
|
|
73
|
+
}, /not a Document or Element/);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("transformToFragment", () => {
|
|
78
|
+
it("should transform XML to fragment", () => {
|
|
79
|
+
const processor = new XSLTProcessor();
|
|
80
|
+
|
|
81
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
82
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
83
|
+
<xsl:template match="/">
|
|
84
|
+
<div class="result">
|
|
85
|
+
<xsl:value-of select="/root/item"/>
|
|
86
|
+
</div>
|
|
87
|
+
</xsl:template>
|
|
88
|
+
</xsl:stylesheet>
|
|
89
|
+
`);
|
|
90
|
+
|
|
91
|
+
const xml = parseXML(`<?xml version="1.0"?>
|
|
92
|
+
<root>
|
|
93
|
+
<item>Hello World</item>
|
|
94
|
+
</root>
|
|
95
|
+
`);
|
|
96
|
+
|
|
97
|
+
processor.importStylesheet(xslt);
|
|
98
|
+
const fragment = processor.transformToFragment(xml, document);
|
|
99
|
+
|
|
100
|
+
assert.ok(fragment);
|
|
101
|
+
assert.ok(fragment.nodeType === 11); // DocumentFragment
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should throw when no stylesheet imported", () => {
|
|
105
|
+
const processor = new XSLTProcessor();
|
|
106
|
+
const xml = parseXML("<root/>");
|
|
107
|
+
|
|
108
|
+
assert.throws(() => {
|
|
109
|
+
processor.transformToFragment(xml, document);
|
|
110
|
+
}, /No stylesheet/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should throw when missing arguments", () => {
|
|
114
|
+
const processor = new XSLTProcessor();
|
|
115
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
116
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
117
|
+
<xsl:template match="/"><out/></xsl:template>
|
|
118
|
+
</xsl:stylesheet>
|
|
119
|
+
`);
|
|
120
|
+
|
|
121
|
+
processor.importStylesheet(xslt);
|
|
122
|
+
|
|
123
|
+
assert.throws(() => {
|
|
124
|
+
processor.transformToFragment();
|
|
125
|
+
}, /2 arguments required/);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("transformToDocument", () => {
|
|
130
|
+
it("should transform XML to document", () => {
|
|
131
|
+
const processor = new XSLTProcessor();
|
|
132
|
+
|
|
133
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
134
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
135
|
+
<xsl:template match="/">
|
|
136
|
+
<result>
|
|
137
|
+
<xsl:value-of select="/root/item"/>
|
|
138
|
+
</result>
|
|
139
|
+
</xsl:template>
|
|
140
|
+
</xsl:stylesheet>
|
|
141
|
+
`);
|
|
142
|
+
|
|
143
|
+
const xml = parseXML(`<?xml version="1.0"?>
|
|
144
|
+
<root>
|
|
145
|
+
<item>Test</item>
|
|
146
|
+
</root>
|
|
147
|
+
`);
|
|
148
|
+
|
|
149
|
+
processor.importStylesheet(xslt);
|
|
150
|
+
const result = processor.transformToDocument(xml);
|
|
151
|
+
|
|
152
|
+
assert.ok(result);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should throw when no stylesheet imported", () => {
|
|
156
|
+
const processor = new XSLTProcessor();
|
|
157
|
+
const xml = parseXML("<root/>");
|
|
158
|
+
|
|
159
|
+
assert.throws(() => {
|
|
160
|
+
processor.transformToDocument(xml);
|
|
161
|
+
}, /No stylesheet/);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("Parameters", () => {
|
|
166
|
+
it("should set and get parameter", () => {
|
|
167
|
+
const processor = new XSLTProcessor();
|
|
168
|
+
|
|
169
|
+
processor.setParameter(null, "myParam", "myValue");
|
|
170
|
+
const value = processor.getParameter(null, "myParam");
|
|
171
|
+
|
|
172
|
+
assert.strictEqual(value, "myValue");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should set parameter with namespace", () => {
|
|
176
|
+
const processor = new XSLTProcessor();
|
|
177
|
+
|
|
178
|
+
processor.setParameter("http://example.com", "param", "value");
|
|
179
|
+
const value = processor.getParameter("http://example.com", "param");
|
|
180
|
+
|
|
181
|
+
assert.strictEqual(value, "value");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should return empty string for unset parameter", () => {
|
|
185
|
+
const processor = new XSLTProcessor();
|
|
186
|
+
const value = processor.getParameter(null, "nonexistent");
|
|
187
|
+
assert.strictEqual(value, "");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should remove parameter", () => {
|
|
191
|
+
const processor = new XSLTProcessor();
|
|
192
|
+
|
|
193
|
+
processor.setParameter(null, "param", "value");
|
|
194
|
+
processor.removeParameter(null, "param");
|
|
195
|
+
const value = processor.getParameter(null, "param");
|
|
196
|
+
|
|
197
|
+
assert.strictEqual(value, "");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should clear all parameters", () => {
|
|
201
|
+
const processor = new XSLTProcessor();
|
|
202
|
+
|
|
203
|
+
processor.setParameter(null, "param1", "value1");
|
|
204
|
+
processor.setParameter(null, "param2", "value2");
|
|
205
|
+
processor.clearParameters();
|
|
206
|
+
|
|
207
|
+
assert.strictEqual(processor.getParameter(null, "param1"), "");
|
|
208
|
+
assert.strictEqual(processor.getParameter(null, "param2"), "");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should throw when setParameter has insufficient arguments", () => {
|
|
212
|
+
const processor = new XSLTProcessor();
|
|
213
|
+
assert.throws(() => {
|
|
214
|
+
processor.setParameter(null, "param");
|
|
215
|
+
}, /3 arguments required/);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("should throw when getParameter has insufficient arguments", () => {
|
|
219
|
+
const processor = new XSLTProcessor();
|
|
220
|
+
assert.throws(() => {
|
|
221
|
+
processor.getParameter(null);
|
|
222
|
+
}, /2 arguments required/);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("reset", () => {
|
|
227
|
+
it("should reset processor state", () => {
|
|
228
|
+
const processor = new XSLTProcessor();
|
|
229
|
+
|
|
230
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
231
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
232
|
+
<xsl:template match="/"><out/></xsl:template>
|
|
233
|
+
</xsl:stylesheet>
|
|
234
|
+
`);
|
|
235
|
+
|
|
236
|
+
processor.importStylesheet(xslt);
|
|
237
|
+
processor.setParameter(null, "param", "value");
|
|
238
|
+
processor.reset();
|
|
239
|
+
|
|
240
|
+
assert.strictEqual(processor.getParameter(null, "param"), "");
|
|
241
|
+
|
|
242
|
+
const xml = parseXML("<root/>");
|
|
243
|
+
assert.throws(() => {
|
|
244
|
+
processor.transformToFragment(xml, document);
|
|
245
|
+
}, /No stylesheet/);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("XSLT Features", () => {
|
|
250
|
+
it("should handle xsl:value-of", () => {
|
|
251
|
+
const processor = new XSLTProcessor();
|
|
252
|
+
|
|
253
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
254
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
255
|
+
<xsl:template match="/">
|
|
256
|
+
<p><xsl:value-of select="/root/text"/></p>
|
|
257
|
+
</xsl:template>
|
|
258
|
+
</xsl:stylesheet>
|
|
259
|
+
`);
|
|
260
|
+
|
|
261
|
+
const xml = parseXML(`<root><text>Hello</text></root>`);
|
|
262
|
+
|
|
263
|
+
processor.importStylesheet(xslt);
|
|
264
|
+
const fragment = processor.transformToFragment(xml, document);
|
|
265
|
+
|
|
266
|
+
assert.ok(fragment);
|
|
267
|
+
const p = fragment.querySelector("p");
|
|
268
|
+
assert.ok(p);
|
|
269
|
+
assert.strictEqual(p.textContent, "Hello");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should handle xsl:for-each", () => {
|
|
273
|
+
const processor = new XSLTProcessor();
|
|
274
|
+
|
|
275
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
276
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
277
|
+
<xsl:template match="/">
|
|
278
|
+
<ul>
|
|
279
|
+
<xsl:for-each select="/root/item">
|
|
280
|
+
<li><xsl:value-of select="."/></li>
|
|
281
|
+
</xsl:for-each>
|
|
282
|
+
</ul>
|
|
283
|
+
</xsl:template>
|
|
284
|
+
</xsl:stylesheet>
|
|
285
|
+
`);
|
|
286
|
+
|
|
287
|
+
const xml = parseXML(`
|
|
288
|
+
<root>
|
|
289
|
+
<item>One</item>
|
|
290
|
+
<item>Two</item>
|
|
291
|
+
<item>Three</item>
|
|
292
|
+
</root>
|
|
293
|
+
`);
|
|
294
|
+
|
|
295
|
+
processor.importStylesheet(xslt);
|
|
296
|
+
const fragment = processor.transformToFragment(xml, document);
|
|
297
|
+
|
|
298
|
+
const items = fragment.querySelectorAll("li");
|
|
299
|
+
assert.strictEqual(items.length, 3);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("should handle xsl:if", () => {
|
|
303
|
+
const processor = new XSLTProcessor();
|
|
304
|
+
|
|
305
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
306
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
307
|
+
<xsl:template match="/">
|
|
308
|
+
<xsl:if test="/root/show">
|
|
309
|
+
<p>Visible</p>
|
|
310
|
+
</xsl:if>
|
|
311
|
+
</xsl:template>
|
|
312
|
+
</xsl:stylesheet>
|
|
313
|
+
`);
|
|
314
|
+
|
|
315
|
+
const xmlWithShow = parseXML(`<root><show/></root>`);
|
|
316
|
+
const xmlWithoutShow = parseXML(`<root></root>`);
|
|
317
|
+
|
|
318
|
+
processor.importStylesheet(xslt);
|
|
319
|
+
|
|
320
|
+
const fragment1 = processor.transformToFragment(xmlWithShow, document);
|
|
321
|
+
assert.ok(fragment1.querySelector("p"));
|
|
322
|
+
|
|
323
|
+
const fragment2 = processor.transformToFragment(xmlWithoutShow, document);
|
|
324
|
+
assert.ok(!fragment2.querySelector("p"));
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("should handle xsl:choose", () => {
|
|
328
|
+
const processor = new XSLTProcessor();
|
|
329
|
+
|
|
330
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
331
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
332
|
+
<xsl:template match="/">
|
|
333
|
+
<xsl:choose>
|
|
334
|
+
<xsl:when test="/root/type = 'a'">
|
|
335
|
+
<p>Type A</p>
|
|
336
|
+
</xsl:when>
|
|
337
|
+
<xsl:when test="/root/type = 'b'">
|
|
338
|
+
<p>Type B</p>
|
|
339
|
+
</xsl:when>
|
|
340
|
+
<xsl:otherwise>
|
|
341
|
+
<p>Unknown</p>
|
|
342
|
+
</xsl:otherwise>
|
|
343
|
+
</xsl:choose>
|
|
344
|
+
</xsl:template>
|
|
345
|
+
</xsl:stylesheet>
|
|
346
|
+
`);
|
|
347
|
+
|
|
348
|
+
const xml = parseXML(`<root><type>b</type></root>`);
|
|
349
|
+
|
|
350
|
+
processor.importStylesheet(xslt);
|
|
351
|
+
const fragment = processor.transformToFragment(xml, document);
|
|
352
|
+
|
|
353
|
+
const p = fragment.querySelector("p");
|
|
354
|
+
assert.strictEqual(p.textContent, "Type B");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should handle xsl:apply-templates", () => {
|
|
358
|
+
const processor = new XSLTProcessor();
|
|
359
|
+
|
|
360
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
361
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
362
|
+
<xsl:template match="/">
|
|
363
|
+
<div><xsl:apply-templates select="/root/item"/></div>
|
|
364
|
+
</xsl:template>
|
|
365
|
+
<xsl:template match="item">
|
|
366
|
+
<span><xsl:value-of select="."/></span>
|
|
367
|
+
</xsl:template>
|
|
368
|
+
</xsl:stylesheet>
|
|
369
|
+
`);
|
|
370
|
+
|
|
371
|
+
const xml = parseXML(`
|
|
372
|
+
<root>
|
|
373
|
+
<item>A</item>
|
|
374
|
+
<item>B</item>
|
|
375
|
+
</root>
|
|
376
|
+
`);
|
|
377
|
+
|
|
378
|
+
processor.importStylesheet(xslt);
|
|
379
|
+
const fragment = processor.transformToFragment(xml, document);
|
|
380
|
+
|
|
381
|
+
const spans = fragment.querySelectorAll("span");
|
|
382
|
+
assert.strictEqual(spans.length, 2);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("should handle xsl:call-template", () => {
|
|
386
|
+
const processor = new XSLTProcessor();
|
|
387
|
+
|
|
388
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
389
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
390
|
+
<xsl:template match="/">
|
|
391
|
+
<xsl:call-template name="greeting">
|
|
392
|
+
<xsl:with-param name="name" select="/root/name"/>
|
|
393
|
+
</xsl:call-template>
|
|
394
|
+
</xsl:template>
|
|
395
|
+
<xsl:template name="greeting">
|
|
396
|
+
<xsl:param name="name"/>
|
|
397
|
+
<p>Hello, <xsl:value-of select="$name"/>!</p>
|
|
398
|
+
</xsl:template>
|
|
399
|
+
</xsl:stylesheet>
|
|
400
|
+
`);
|
|
401
|
+
|
|
402
|
+
const xml = parseXML(`<root><name>World</name></root>`);
|
|
403
|
+
|
|
404
|
+
processor.importStylesheet(xslt);
|
|
405
|
+
const fragment = processor.transformToFragment(xml, document);
|
|
406
|
+
|
|
407
|
+
const p = fragment.querySelector("p");
|
|
408
|
+
assert.ok(p.textContent.includes("Hello"));
|
|
409
|
+
assert.ok(p.textContent.includes("World"));
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("should handle xsl:copy-of", () => {
|
|
413
|
+
const processor = new XSLTProcessor();
|
|
414
|
+
|
|
415
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
416
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
417
|
+
<xsl:template match="/">
|
|
418
|
+
<div>
|
|
419
|
+
<xsl:copy-of select="/root/content/*"/>
|
|
420
|
+
</div>
|
|
421
|
+
</xsl:template>
|
|
422
|
+
</xsl:stylesheet>
|
|
423
|
+
`);
|
|
424
|
+
|
|
425
|
+
const xml = parseXML(`
|
|
426
|
+
<root>
|
|
427
|
+
<content>
|
|
428
|
+
<p>Paragraph</p>
|
|
429
|
+
<span>Span</span>
|
|
430
|
+
</content>
|
|
431
|
+
</root>
|
|
432
|
+
`);
|
|
433
|
+
|
|
434
|
+
processor.importStylesheet(xslt);
|
|
435
|
+
const fragment = processor.transformToFragment(xml, document);
|
|
436
|
+
|
|
437
|
+
assert.ok(fragment.querySelector("p"));
|
|
438
|
+
assert.ok(fragment.querySelector("span"));
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("should handle xsl:attribute", () => {
|
|
442
|
+
const processor = new XSLTProcessor();
|
|
443
|
+
|
|
444
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
445
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
446
|
+
<xsl:template match="/">
|
|
447
|
+
<div>
|
|
448
|
+
<xsl:attribute name="class">
|
|
449
|
+
<xsl:value-of select="/root/className"/>
|
|
450
|
+
</xsl:attribute>
|
|
451
|
+
Content
|
|
452
|
+
</div>
|
|
453
|
+
</xsl:template>
|
|
454
|
+
</xsl:stylesheet>
|
|
455
|
+
`);
|
|
456
|
+
|
|
457
|
+
const xml = parseXML(`<root><className>my-class</className></root>`);
|
|
458
|
+
|
|
459
|
+
processor.importStylesheet(xslt);
|
|
460
|
+
const fragment = processor.transformToFragment(xml, document);
|
|
461
|
+
|
|
462
|
+
const div = fragment.querySelector("div");
|
|
463
|
+
assert.strictEqual(div.getAttribute("class"), "my-class");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("should handle attribute value templates", () => {
|
|
467
|
+
const processor = new XSLTProcessor();
|
|
468
|
+
|
|
469
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
470
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
471
|
+
<xsl:template match="/">
|
|
472
|
+
<a href="/item/{/root/id}">Link</a>
|
|
473
|
+
</xsl:template>
|
|
474
|
+
</xsl:stylesheet>
|
|
475
|
+
`);
|
|
476
|
+
|
|
477
|
+
const xml = parseXML(`<root><id>123</id></root>`);
|
|
478
|
+
|
|
479
|
+
processor.importStylesheet(xslt);
|
|
480
|
+
const fragment = processor.transformToFragment(xml, document);
|
|
481
|
+
|
|
482
|
+
const a = fragment.querySelector("a");
|
|
483
|
+
assert.strictEqual(a.getAttribute("href"), "/item/123");
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("should handle xsl:sort", () => {
|
|
487
|
+
const processor = new XSLTProcessor();
|
|
488
|
+
|
|
489
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
490
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
491
|
+
<xsl:template match="/">
|
|
492
|
+
<ul>
|
|
493
|
+
<xsl:for-each select="/root/item">
|
|
494
|
+
<xsl:sort select="@name"/>
|
|
495
|
+
<li><xsl:value-of select="@name"/></li>
|
|
496
|
+
</xsl:for-each>
|
|
497
|
+
</ul>
|
|
498
|
+
</xsl:template>
|
|
499
|
+
</xsl:stylesheet>
|
|
500
|
+
`);
|
|
501
|
+
|
|
502
|
+
const xml = parseXML(`
|
|
503
|
+
<root>
|
|
504
|
+
<item name="Charlie"/>
|
|
505
|
+
<item name="Alice"/>
|
|
506
|
+
<item name="Bob"/>
|
|
507
|
+
</root>
|
|
508
|
+
`);
|
|
509
|
+
|
|
510
|
+
processor.importStylesheet(xslt);
|
|
511
|
+
const fragment = processor.transformToFragment(xml, document);
|
|
512
|
+
|
|
513
|
+
const items = fragment.querySelectorAll("li");
|
|
514
|
+
assert.strictEqual(items[0].textContent, "Alice");
|
|
515
|
+
assert.strictEqual(items[1].textContent, "Bob");
|
|
516
|
+
assert.strictEqual(items[2].textContent, "Charlie");
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
describe("Utility Functions", () => {
|
|
521
|
+
it("isNativeXSLTSupported should return boolean", () => {
|
|
522
|
+
const result = isNativeXSLTSupported();
|
|
523
|
+
assert.strictEqual(typeof result, "boolean");
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("installGlobal with force should work", () => {
|
|
527
|
+
const result = installGlobal(true);
|
|
528
|
+
assert.strictEqual(typeof result, "boolean");
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it("installGlobal without force when native not supported", () => {
|
|
532
|
+
// Save original
|
|
533
|
+
const original = globalThis.XSLTProcessor;
|
|
534
|
+
delete globalThis.XSLTProcessor;
|
|
535
|
+
|
|
536
|
+
const result = installGlobal(false);
|
|
537
|
+
|
|
538
|
+
// Restore
|
|
539
|
+
if (original) {
|
|
540
|
+
globalThis.XSLTProcessor = original;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
assert.strictEqual(typeof result, "boolean");
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("isNativeXSLTSupported should return true with working mock native", () => {
|
|
547
|
+
// Save original
|
|
548
|
+
const original = globalThis.XSLTProcessor;
|
|
549
|
+
|
|
550
|
+
// Create a mock native XSLTProcessor that works
|
|
551
|
+
class MockNativeXSLTProcessor {
|
|
552
|
+
importStylesheet() {}
|
|
553
|
+
transformToFragment() {
|
|
554
|
+
// Return a mock fragment with childNodes
|
|
555
|
+
const fragment = document.createDocumentFragment();
|
|
556
|
+
fragment.appendChild(document.createElement("test"));
|
|
557
|
+
return fragment;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
globalThis.XSLTProcessor = MockNativeXSLTProcessor;
|
|
562
|
+
|
|
563
|
+
const result = isNativeXSLTSupported();
|
|
564
|
+
|
|
565
|
+
// Restore
|
|
566
|
+
globalThis.XSLTProcessor = original;
|
|
567
|
+
|
|
568
|
+
assert.strictEqual(result, true);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it("isNativeXSLTSupported should return false when native throws", () => {
|
|
572
|
+
// Save original
|
|
573
|
+
const original = globalThis.XSLTProcessor;
|
|
574
|
+
|
|
575
|
+
// Create a mock that throws
|
|
576
|
+
class MockThrowingXSLTProcessor {
|
|
577
|
+
importStylesheet() {
|
|
578
|
+
throw new Error("Mock error");
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
globalThis.XSLTProcessor = MockThrowingXSLTProcessor;
|
|
583
|
+
|
|
584
|
+
const result = isNativeXSLTSupported();
|
|
585
|
+
|
|
586
|
+
// Restore
|
|
587
|
+
globalThis.XSLTProcessor = original;
|
|
588
|
+
|
|
589
|
+
assert.strictEqual(result, false);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("isNativeXSLTSupported should return false when transformToFragment returns null", () => {
|
|
593
|
+
// Save original
|
|
594
|
+
const original = globalThis.XSLTProcessor;
|
|
595
|
+
|
|
596
|
+
// Create a mock that returns null
|
|
597
|
+
class MockNullResultXSLTProcessor {
|
|
598
|
+
importStylesheet() {}
|
|
599
|
+
transformToFragment() {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
globalThis.XSLTProcessor = MockNullResultXSLTProcessor;
|
|
605
|
+
|
|
606
|
+
const result = isNativeXSLTSupported();
|
|
607
|
+
|
|
608
|
+
// Restore
|
|
609
|
+
globalThis.XSLTProcessor = original;
|
|
610
|
+
|
|
611
|
+
assert.strictEqual(result, false);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it("isNativeXSLTSupported should return false when transformToFragment returns empty fragment", () => {
|
|
615
|
+
// Save original
|
|
616
|
+
const original = globalThis.XSLTProcessor;
|
|
617
|
+
|
|
618
|
+
// Create a mock that returns empty fragment
|
|
619
|
+
class MockEmptyResultXSLTProcessor {
|
|
620
|
+
importStylesheet() {}
|
|
621
|
+
transformToFragment() {
|
|
622
|
+
return document.createDocumentFragment();
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
globalThis.XSLTProcessor = MockEmptyResultXSLTProcessor;
|
|
627
|
+
|
|
628
|
+
const result = isNativeXSLTSupported();
|
|
629
|
+
|
|
630
|
+
// Restore
|
|
631
|
+
globalThis.XSLTProcessor = original;
|
|
632
|
+
|
|
633
|
+
assert.strictEqual(result, false);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
it("installGlobal should return false when native is supported and force is false", () => {
|
|
637
|
+
// Save original
|
|
638
|
+
const original = globalThis.XSLTProcessor;
|
|
639
|
+
|
|
640
|
+
// Create a working mock native
|
|
641
|
+
class MockNativeXSLTProcessor {
|
|
642
|
+
importStylesheet() {}
|
|
643
|
+
transformToFragment() {
|
|
644
|
+
const fragment = document.createDocumentFragment();
|
|
645
|
+
fragment.appendChild(document.createElement("test"));
|
|
646
|
+
return fragment;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
globalThis.XSLTProcessor = MockNativeXSLTProcessor;
|
|
651
|
+
|
|
652
|
+
const result = installGlobal(false);
|
|
653
|
+
|
|
654
|
+
// Restore
|
|
655
|
+
globalThis.XSLTProcessor = original;
|
|
656
|
+
|
|
657
|
+
assert.strictEqual(result, false);
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
describe("importStylesheet error cases", () => {
|
|
662
|
+
it("should throw for stylesheet with parse errors", () => {
|
|
663
|
+
const processor = new XSLTProcessor();
|
|
664
|
+
|
|
665
|
+
// Create document with parsererror element
|
|
666
|
+
const doc = document.implementation.createDocument(null, null, null);
|
|
667
|
+
const root = doc.createElement("root");
|
|
668
|
+
const parseError = doc.createElement("parsererror");
|
|
669
|
+
parseError.textContent = "XML parsing error";
|
|
670
|
+
root.appendChild(parseError);
|
|
671
|
+
doc.appendChild(root);
|
|
672
|
+
|
|
673
|
+
assert.throws(() => {
|
|
674
|
+
processor.importStylesheet(doc);
|
|
675
|
+
}, /parse errors/);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
describe("setParameter with existing engine", () => {
|
|
680
|
+
it("should update engine parameters after stylesheet import", () => {
|
|
681
|
+
const processor = new XSLTProcessor();
|
|
682
|
+
|
|
683
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
684
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
685
|
+
<xsl:param name="myParam" select="'default'"/>
|
|
686
|
+
<xsl:template match="/">
|
|
687
|
+
<result><xsl:value-of select="$myParam"/></result>
|
|
688
|
+
</xsl:template>
|
|
689
|
+
</xsl:stylesheet>
|
|
690
|
+
`);
|
|
691
|
+
|
|
692
|
+
processor.importStylesheet(xslt);
|
|
693
|
+
processor.setParameter(null, "myParam", "updated");
|
|
694
|
+
|
|
695
|
+
// Verify the parameter was set
|
|
696
|
+
const value = processor.getParameter(null, "myParam");
|
|
697
|
+
assert.strictEqual(value, "updated");
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("should apply pre-set parameters when importing stylesheet", () => {
|
|
701
|
+
const processor = new XSLTProcessor();
|
|
702
|
+
|
|
703
|
+
// Set parameter before importing stylesheet
|
|
704
|
+
processor.setParameter(null, "presetParam", "presetValue");
|
|
705
|
+
|
|
706
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
707
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
708
|
+
<xsl:param name="presetParam"/>
|
|
709
|
+
<xsl:template match="/"><out/></xsl:template>
|
|
710
|
+
</xsl:stylesheet>
|
|
711
|
+
`);
|
|
712
|
+
|
|
713
|
+
processor.importStylesheet(xslt);
|
|
714
|
+
|
|
715
|
+
const value = processor.getParameter(null, "presetParam");
|
|
716
|
+
assert.strictEqual(value, "presetValue");
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
describe("transformToFragment error handling", () => {
|
|
721
|
+
it("should throw when output is missing", () => {
|
|
722
|
+
const processor = new XSLTProcessor();
|
|
723
|
+
|
|
724
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
725
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
726
|
+
<xsl:template match="/"><out/></xsl:template>
|
|
727
|
+
</xsl:stylesheet>
|
|
728
|
+
`);
|
|
729
|
+
|
|
730
|
+
processor.importStylesheet(xslt);
|
|
731
|
+
const xml = parseXML("<root/>");
|
|
732
|
+
|
|
733
|
+
assert.throws(() => {
|
|
734
|
+
processor.transformToFragment(xml);
|
|
735
|
+
}, /2 arguments required/);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it("should throw for invalid source node type", () => {
|
|
739
|
+
const processor = new XSLTProcessor();
|
|
740
|
+
|
|
741
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
742
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
743
|
+
<xsl:template match="/"><out/></xsl:template>
|
|
744
|
+
</xsl:stylesheet>
|
|
745
|
+
`);
|
|
746
|
+
|
|
747
|
+
processor.importStylesheet(xslt);
|
|
748
|
+
|
|
749
|
+
// Text node is not valid source
|
|
750
|
+
const textNode = document.createTextNode("text");
|
|
751
|
+
|
|
752
|
+
assert.throws(() => {
|
|
753
|
+
processor.transformToFragment(textNode, document);
|
|
754
|
+
}, /not a valid node type/);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it("should throw for invalid output document type", () => {
|
|
758
|
+
const processor = new XSLTProcessor();
|
|
759
|
+
|
|
760
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
761
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
762
|
+
<xsl:template match="/"><out/></xsl:template>
|
|
763
|
+
</xsl:stylesheet>
|
|
764
|
+
`);
|
|
765
|
+
|
|
766
|
+
processor.importStylesheet(xslt);
|
|
767
|
+
const xml = parseXML("<root/>");
|
|
768
|
+
|
|
769
|
+
// Element is not a document
|
|
770
|
+
const element = document.createElement("div");
|
|
771
|
+
|
|
772
|
+
assert.throws(() => {
|
|
773
|
+
processor.transformToFragment(xml, element);
|
|
774
|
+
}, /not a Document/);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it("should return null on transformation error", () => {
|
|
778
|
+
const processor = new XSLTProcessor();
|
|
779
|
+
|
|
780
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
781
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
782
|
+
<xsl:template match="/">
|
|
783
|
+
<xsl:message terminate="yes">Force error</xsl:message>
|
|
784
|
+
</xsl:template>
|
|
785
|
+
</xsl:stylesheet>
|
|
786
|
+
`);
|
|
787
|
+
|
|
788
|
+
processor.importStylesheet(xslt);
|
|
789
|
+
const xml = parseXML("<root/>");
|
|
790
|
+
|
|
791
|
+
const result = processor.transformToFragment(xml, document);
|
|
792
|
+
assert.strictEqual(result, null);
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
describe("transformToDocument error handling", () => {
|
|
797
|
+
it("should throw when source is missing", () => {
|
|
798
|
+
const processor = new XSLTProcessor();
|
|
799
|
+
|
|
800
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
801
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
802
|
+
<xsl:template match="/"><out/></xsl:template>
|
|
803
|
+
</xsl:stylesheet>
|
|
804
|
+
`);
|
|
805
|
+
|
|
806
|
+
processor.importStylesheet(xslt);
|
|
807
|
+
|
|
808
|
+
assert.throws(() => {
|
|
809
|
+
processor.transformToDocument();
|
|
810
|
+
}, /1 argument required/);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it("should throw for invalid source node type", () => {
|
|
814
|
+
const processor = new XSLTProcessor();
|
|
815
|
+
|
|
816
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
817
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
818
|
+
<xsl:template match="/"><out/></xsl:template>
|
|
819
|
+
</xsl:stylesheet>
|
|
820
|
+
`);
|
|
821
|
+
|
|
822
|
+
processor.importStylesheet(xslt);
|
|
823
|
+
const textNode = document.createTextNode("text");
|
|
824
|
+
|
|
825
|
+
assert.throws(() => {
|
|
826
|
+
processor.transformToDocument(textNode);
|
|
827
|
+
}, /not a valid node type/);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it("should return null on transformation error", () => {
|
|
831
|
+
const processor = new XSLTProcessor();
|
|
832
|
+
|
|
833
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
834
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
835
|
+
<xsl:template match="/">
|
|
836
|
+
<xsl:message terminate="yes">Force error</xsl:message>
|
|
837
|
+
</xsl:template>
|
|
838
|
+
</xsl:stylesheet>
|
|
839
|
+
`);
|
|
840
|
+
|
|
841
|
+
processor.importStylesheet(xslt);
|
|
842
|
+
const xml = parseXML("<root/>");
|
|
843
|
+
|
|
844
|
+
const result = processor.transformToDocument(xml);
|
|
845
|
+
assert.strictEqual(result, null);
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
describe("setParameter validation", () => {
|
|
850
|
+
it("should throw for non-string localName", () => {
|
|
851
|
+
const processor = new XSLTProcessor();
|
|
852
|
+
|
|
853
|
+
assert.throws(() => {
|
|
854
|
+
processor.setParameter(null, 123, "value");
|
|
855
|
+
}, /non-empty string/);
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it("should throw for empty localName", () => {
|
|
859
|
+
const processor = new XSLTProcessor();
|
|
860
|
+
|
|
861
|
+
assert.throws(() => {
|
|
862
|
+
processor.setParameter(null, "", "value");
|
|
863
|
+
}, /non-empty string/);
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
describe("getParameter validation", () => {
|
|
868
|
+
it("should throw for non-string localName", () => {
|
|
869
|
+
const processor = new XSLTProcessor();
|
|
870
|
+
|
|
871
|
+
assert.throws(() => {
|
|
872
|
+
processor.getParameter(null, 123);
|
|
873
|
+
}, /must be a string/);
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
describe("removeParameter validation", () => {
|
|
878
|
+
it("should throw when arguments missing", () => {
|
|
879
|
+
const processor = new XSLTProcessor();
|
|
880
|
+
|
|
881
|
+
assert.throws(() => {
|
|
882
|
+
processor.removeParameter();
|
|
883
|
+
}, /2 arguments required/);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it("should throw for non-string localName", () => {
|
|
887
|
+
const processor = new XSLTProcessor();
|
|
888
|
+
|
|
889
|
+
assert.throws(() => {
|
|
890
|
+
processor.removeParameter(null, 123);
|
|
891
|
+
}, /must be a string/);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it("should remove parameter from engine", () => {
|
|
895
|
+
const processor = new XSLTProcessor();
|
|
896
|
+
|
|
897
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
898
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
899
|
+
<xsl:template match="/"><out/></xsl:template>
|
|
900
|
+
</xsl:stylesheet>
|
|
901
|
+
`);
|
|
902
|
+
|
|
903
|
+
processor.importStylesheet(xslt);
|
|
904
|
+
processor.setParameter(null, "testParam", "value");
|
|
905
|
+
processor.removeParameter(null, "testParam");
|
|
906
|
+
|
|
907
|
+
assert.strictEqual(processor.getParameter(null, "testParam"), "");
|
|
908
|
+
});
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
describe("clearParameters with engine", () => {
|
|
912
|
+
it("should clear parameters from engine", () => {
|
|
913
|
+
const processor = new XSLTProcessor();
|
|
914
|
+
|
|
915
|
+
const xslt = parseXML(`<?xml version="1.0"?>
|
|
916
|
+
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
|
|
917
|
+
<xsl:template match="/"><out/></xsl:template>
|
|
918
|
+
</xsl:stylesheet>
|
|
919
|
+
`);
|
|
920
|
+
|
|
921
|
+
processor.importStylesheet(xslt);
|
|
922
|
+
processor.setParameter(null, "param1", "value1");
|
|
923
|
+
processor.setParameter(null, "param2", "value2");
|
|
924
|
+
processor.clearParameters();
|
|
925
|
+
|
|
926
|
+
assert.strictEqual(processor.getParameter(null, "param1"), "");
|
|
927
|
+
assert.strictEqual(processor.getParameter(null, "param2"), "");
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
});
|