@tradik/xslt-processor 1.0.3 → 1.1.1

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 (47) hide show
  1. package/README.md +290 -45
  2. package/bin/lib/options.js +114 -0
  3. package/bin/lib/paths.js +186 -0
  4. package/bin/lib/transform.js +115 -0
  5. package/bin/xslt.js +68 -162
  6. package/dist/xslt-processor.browser.js +2074 -159
  7. package/dist/xslt-processor.browser.js.map +4 -4
  8. package/dist/xslt-processor.browser.min.js +6 -2
  9. package/dist/xslt-processor.browser.min.js.map +4 -4
  10. package/dist/xslt-processor.cjs +2078 -158
  11. package/dist/xslt-processor.cjs.map +4 -4
  12. package/dist/xslt-processor.d.cts +299 -0
  13. package/dist/xslt-processor.d.ts +92 -4
  14. package/dist/xslt-processor.js +2073 -157
  15. package/dist/xslt-processor.js.map +4 -4
  16. package/package.json +26 -15
  17. package/src/XSLTProcessor.js +177 -8
  18. package/src/index.js +11 -5
  19. package/src/xpath/evaluator.js +48 -7
  20. package/src/xslt/elements.js +57 -0
  21. package/src/xslt/engine.js +471 -179
  22. package/src/xslt/formatNumber.js +220 -0
  23. package/src/xslt/functions.js +191 -0
  24. package/src/xslt/index.js +31 -0
  25. package/src/xslt/keys.js +141 -0
  26. package/src/xslt/literalResult.js +167 -0
  27. package/src/xslt/number.js +178 -0
  28. package/src/xslt/numberFormat.js +155 -0
  29. package/src/xslt/resultTree.js +74 -0
  30. package/src/xslt/serializer/baseWriter.js +283 -0
  31. package/src/xslt/serializer/constants.js +78 -0
  32. package/src/xslt/serializer/escape.js +98 -0
  33. package/src/xslt/serializer/htmlSerializer.js +141 -0
  34. package/src/xslt/serializer/indent.js +51 -0
  35. package/src/xslt/serializer/namespaces.js +68 -0
  36. package/src/xslt/serializer/rawText.js +41 -0
  37. package/src/xslt/serializer/settings.js +103 -0
  38. package/src/xslt/serializer/textSerializer.js +29 -0
  39. package/src/xslt/serializer/xmlSerializer.js +127 -0
  40. package/src/xslt/serializer.js +57 -0
  41. package/src/xslt/templatePriority.js +45 -0
  42. package/src/xslt/uri.js +68 -0
  43. package/src/xslt/whitespace.js +184 -0
  44. package/src/XSLTProcessor.test.js +0 -930
  45. package/src/xpath/evaluator.test.js +0 -1852
  46. package/src/xpath/tokenizer.test.js +0 -224
  47. package/src/xslt/engine.test.js +0 -3130
@@ -1,3130 +0,0 @@
1
- /**
2
- * XSLT Engine Tests
3
- *
4
- * Tests for XSLT 1.0 Processing Engine.
5
- */
6
-
7
- import { describe, it, beforeEach } from "node:test";
8
- import assert from "node:assert";
9
- import { JSDOM } from "jsdom";
10
- import { XsltContext, XsltEngine } from "./engine.js";
11
-
12
- function setupDOM() {
13
- const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
14
- contentType: "text/html",
15
- });
16
-
17
- global.document = dom.window.document;
18
- global.DOMParser = dom.window.DOMParser;
19
- global.XMLSerializer = dom.window.XMLSerializer;
20
-
21
- return dom;
22
- }
23
-
24
- function parseXML(xmlString) {
25
- const parser = new DOMParser();
26
- return parser.parseFromString(xmlString, "application/xml");
27
- }
28
-
29
- describe("XsltContext", () => {
30
- beforeEach(() => {
31
- setupDOM();
32
- });
33
-
34
- describe("constructor", () => {
35
- it("should create context with default values", () => {
36
- const context = new XsltContext();
37
- assert.strictEqual(context.position, 1);
38
- assert.deepStrictEqual(context.variables, {});
39
- assert.deepStrictEqual(context.parameters, {});
40
- assert.strictEqual(context.outputMethod, "xml");
41
- });
42
-
43
- it("should create context with provided options", () => {
44
- const node = parseXML("<root/>").documentElement;
45
- const context = new XsltContext({
46
- currentNode: node,
47
- currentNodeList: [node],
48
- position: 3,
49
- variables: { foo: "bar" },
50
- parameters: { param1: "value1" },
51
- outputMethod: "html",
52
- });
53
-
54
- assert.strictEqual(context.currentNode, node);
55
- assert.strictEqual(context.position, 3);
56
- assert.strictEqual(context.variables.foo, "bar");
57
- assert.strictEqual(context.parameters.param1, "value1");
58
- assert.strictEqual(context.outputMethod, "html");
59
- });
60
- });
61
-
62
- describe("clone", () => {
63
- it("should clone context with overrides", () => {
64
- const context = new XsltContext({
65
- position: 1,
66
- variables: { a: 1 },
67
- parameters: { b: 2 },
68
- });
69
-
70
- const cloned = context.clone({
71
- position: 5,
72
- variables: { c: 3 },
73
- });
74
-
75
- assert.strictEqual(cloned.position, 5);
76
- assert.strictEqual(cloned.variables.a, 1);
77
- assert.strictEqual(cloned.variables.c, 3);
78
- assert.strictEqual(cloned.parameters.b, 2);
79
- });
80
-
81
- it("should preserve original context", () => {
82
- const context = new XsltContext({
83
- position: 1,
84
- variables: { a: 1 },
85
- });
86
-
87
- context.clone({ variables: { b: 2 } });
88
-
89
- assert.strictEqual(context.variables.a, 1);
90
- assert.strictEqual(context.variables.b, undefined);
91
- });
92
- });
93
-
94
- describe("getVariable", () => {
95
- it("should return variable value", () => {
96
- const context = new XsltContext({
97
- variables: { myVar: "value" },
98
- });
99
-
100
- assert.strictEqual(context.getVariable("myVar"), "value");
101
- });
102
-
103
- it("should return parameter if variable not found", () => {
104
- const context = new XsltContext({
105
- parameters: { myParam: "paramValue" },
106
- });
107
-
108
- assert.strictEqual(context.getVariable("myParam"), "paramValue");
109
- });
110
-
111
- it("should throw for undefined variable", () => {
112
- const context = new XsltContext();
113
-
114
- assert.throws(() => {
115
- context.getVariable("nonexistent");
116
- }, /Undefined variable/);
117
- });
118
- });
119
-
120
- describe("setVariable", () => {
121
- it("should set variable value", () => {
122
- const context = new XsltContext();
123
- context.setVariable("test", "value");
124
-
125
- assert.strictEqual(context.variables.test, "value");
126
- });
127
- });
128
- });
129
-
130
- describe("XsltEngine", () => {
131
- let engine;
132
-
133
- beforeEach(() => {
134
- setupDOM();
135
- engine = new XsltEngine();
136
- });
137
-
138
- describe("constructor", () => {
139
- it("should initialize with default settings", () => {
140
- assert.strictEqual(engine.outputSettings.method, "xml");
141
- assert.strictEqual(engine.outputSettings.encoding, "UTF-8");
142
- assert.strictEqual(engine.outputSettings.indent, "no");
143
- assert.deepStrictEqual(engine.templates, []);
144
- assert.deepStrictEqual(engine.keys, {});
145
- });
146
- });
147
-
148
- describe("importStylesheet", () => {
149
- it("should import valid stylesheet", () => {
150
- const xslt = parseXML(`<?xml version="1.0"?>
151
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
152
- <xsl:template match="/">
153
- <output/>
154
- </xsl:template>
155
- </xsl:stylesheet>
156
- `);
157
-
158
- assert.doesNotThrow(() => {
159
- engine.importStylesheet(xslt);
160
- });
161
- assert.strictEqual(engine.templates.length, 1);
162
- });
163
-
164
- it("should throw for invalid stylesheet", () => {
165
- const invalid = parseXML("<invalid/>");
166
-
167
- assert.throws(() => {
168
- engine.importStylesheet(invalid);
169
- }, /Invalid XSLT stylesheet/);
170
- });
171
-
172
- it("should handle literal result element stylesheet", () => {
173
- const xslt = parseXML(`<?xml version="1.0"?>
174
- <html xsl:version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
175
- <body>Content</body>
176
- </html>
177
- `);
178
-
179
- assert.doesNotThrow(() => {
180
- engine.importStylesheet(xslt);
181
- });
182
- assert.strictEqual(engine.templates.length, 1);
183
- assert.strictEqual(engine.templates[0].match, "/");
184
- });
185
-
186
- it("should process xsl:output element", () => {
187
- const xslt = parseXML(`<?xml version="1.0"?>
188
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
189
- <xsl:output method="html" encoding="ISO-8859-1" indent="yes"/>
190
- <xsl:template match="/"><out/></xsl:template>
191
- </xsl:stylesheet>
192
- `);
193
-
194
- engine.importStylesheet(xslt);
195
-
196
- assert.strictEqual(engine.outputSettings.method, "html");
197
- assert.strictEqual(engine.outputSettings.encoding, "ISO-8859-1");
198
- assert.strictEqual(engine.outputSettings.indent, "yes");
199
- });
200
-
201
- it("should process global variables", () => {
202
- const xslt = parseXML(`<?xml version="1.0"?>
203
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
204
- <xsl:variable name="globalVar" select="'test'"/>
205
- <xsl:template match="/"><out/></xsl:template>
206
- </xsl:stylesheet>
207
- `);
208
-
209
- engine.importStylesheet(xslt);
210
-
211
- assert.ok("globalVar" in engine.globalVariables);
212
- });
213
-
214
- it("should process global parameters", () => {
215
- const xslt = parseXML(`<?xml version="1.0"?>
216
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
217
- <xsl:param name="globalParam" select="'default'"/>
218
- <xsl:template match="/"><out/></xsl:template>
219
- </xsl:stylesheet>
220
- `);
221
-
222
- engine.importStylesheet(xslt);
223
-
224
- assert.ok("globalParam" in engine.globalParameters);
225
- });
226
-
227
- it("should process xsl:key", () => {
228
- const xslt = parseXML(`<?xml version="1.0"?>
229
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
230
- <xsl:key name="itemById" match="item" use="@id"/>
231
- <xsl:template match="/"><out/></xsl:template>
232
- </xsl:stylesheet>
233
- `);
234
-
235
- engine.importStylesheet(xslt);
236
-
237
- assert.ok("itemById" in engine.keys);
238
- assert.strictEqual(engine.keys.itemById.match, "item");
239
- assert.strictEqual(engine.keys.itemById.use, "@id");
240
- });
241
-
242
- it("should process xsl:decimal-format", () => {
243
- const xslt = parseXML(`<?xml version="1.0"?>
244
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
245
- <xsl:decimal-format name="euro" decimal-separator="," grouping-separator="."/>
246
- <xsl:template match="/"><out/></xsl:template>
247
- </xsl:stylesheet>
248
- `);
249
-
250
- engine.importStylesheet(xslt);
251
-
252
- assert.ok("euro" in engine.decimalFormats);
253
- assert.strictEqual(engine.decimalFormats.euro.decimalSeparator, ",");
254
- assert.strictEqual(engine.decimalFormats.euro.groupingSeparator, ".");
255
- });
256
-
257
- it("should process xsl:namespace-alias", () => {
258
- const xslt = parseXML(`<?xml version="1.0"?>
259
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
260
- <xsl:namespace-alias stylesheet-prefix="myns" result-prefix="output"/>
261
- <xsl:template match="/"><out/></xsl:template>
262
- </xsl:stylesheet>
263
- `);
264
-
265
- engine.importStylesheet(xslt);
266
-
267
- assert.strictEqual(engine.namespaceAliases.myns, "output");
268
- });
269
-
270
- it("should process xsl:attribute-set", () => {
271
- const xslt = parseXML(`<?xml version="1.0"?>
272
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
273
- <xsl:attribute-set name="common">
274
- <xsl:attribute name="class">default</xsl:attribute>
275
- </xsl:attribute-set>
276
- <xsl:template match="/"><out/></xsl:template>
277
- </xsl:stylesheet>
278
- `);
279
-
280
- engine.importStylesheet(xslt);
281
-
282
- assert.ok("common" in engine.attributeSets);
283
- });
284
-
285
- it("should process xsl:strip-space", () => {
286
- const xslt = parseXML(`<?xml version="1.0"?>
287
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
288
- <xsl:strip-space elements="p div span"/>
289
- <xsl:template match="/"><out/></xsl:template>
290
- </xsl:stylesheet>
291
- `);
292
-
293
- engine.importStylesheet(xslt);
294
-
295
- assert.deepStrictEqual(engine.stripSpace, ["p", "div", "span"]);
296
- });
297
-
298
- it("should process xsl:preserve-space", () => {
299
- const xslt = parseXML(`<?xml version="1.0"?>
300
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
301
- <xsl:preserve-space elements="pre code"/>
302
- <xsl:template match="/"><out/></xsl:template>
303
- </xsl:stylesheet>
304
- `);
305
-
306
- engine.importStylesheet(xslt);
307
-
308
- assert.deepStrictEqual(engine.preserveSpace, ["pre", "code"]);
309
- });
310
- });
311
-
312
- describe("calculatePriority", () => {
313
- it("should return -0.5 for wildcard patterns", () => {
314
- assert.strictEqual(engine.calculatePriority("*"), -0.5);
315
- assert.strictEqual(engine.calculatePriority("node()"), -0.5);
316
- assert.strictEqual(engine.calculatePriority("text()"), -0.5);
317
- assert.strictEqual(engine.calculatePriority("comment()"), -0.5);
318
- assert.strictEqual(
319
- engine.calculatePriority("processing-instruction()"),
320
- -0.5,
321
- );
322
- });
323
-
324
- it("should return -0.25 for NCName:* patterns", () => {
325
- assert.strictEqual(engine.calculatePriority("foo:*"), -0.25);
326
- });
327
-
328
- it("should return 0 for simple QName patterns", () => {
329
- assert.strictEqual(engine.calculatePriority("item"), 0);
330
- assert.strictEqual(engine.calculatePriority("my-element"), 0);
331
- });
332
-
333
- it("should return 0.5 for complex patterns", () => {
334
- assert.strictEqual(engine.calculatePriority("/root/item"), 0.5);
335
- assert.strictEqual(engine.calculatePriority("item[@id]"), 0.5);
336
- });
337
-
338
- it("should return 0.5 for null pattern", () => {
339
- assert.strictEqual(engine.calculatePriority(null), 0.5);
340
- });
341
- });
342
-
343
- describe("transform", () => {
344
- it("should transform simple XML", () => {
345
- const xslt = parseXML(`<?xml version="1.0"?>
346
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
347
- <xsl:template match="/">
348
- <result><xsl:value-of select="/root/item"/></result>
349
- </xsl:template>
350
- </xsl:stylesheet>
351
- `);
352
-
353
- const xml = parseXML("<root><item>Hello</item></root>");
354
-
355
- engine.importStylesheet(xslt);
356
- const result = engine.transform(xml, document);
357
-
358
- assert.ok(result);
359
- assert.ok(result.nodeType === 11);
360
- });
361
-
362
- it("should throw when no document available", () => {
363
- const xslt = parseXML(`<?xml version="1.0"?>
364
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
365
- <xsl:template match="/"><out/></xsl:template>
366
- </xsl:stylesheet>
367
- `);
368
-
369
- const xml = parseXML("<root/>");
370
-
371
- engine.importStylesheet(xslt);
372
-
373
- const originalDoc = global.document;
374
- global.document = undefined;
375
-
376
- assert.throws(() => {
377
- engine.transform(xml);
378
- }, /No output document/);
379
-
380
- global.document = originalDoc;
381
- });
382
-
383
- it("should evaluate global variables during transform", () => {
384
- const xslt = parseXML(`<?xml version="1.0"?>
385
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
386
- <xsl:variable name="prefix" select="'Item: '"/>
387
- <xsl:template match="/">
388
- <result><xsl:value-of select="concat($prefix, /root/item)"/></result>
389
- </xsl:template>
390
- </xsl:stylesheet>
391
- `);
392
-
393
- const xml = parseXML("<root><item>Test</item></root>");
394
-
395
- engine.importStylesheet(xslt);
396
- const result = engine.transform(xml, document);
397
-
398
- const resultEl = result.querySelector("result");
399
- assert.ok(resultEl.textContent.includes("Item:"));
400
- assert.ok(resultEl.textContent.includes("Test"));
401
- });
402
- });
403
-
404
- describe("xsl:apply-templates", () => {
405
- it("should apply templates with mode", () => {
406
- const xslt = parseXML(`<?xml version="1.0"?>
407
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
408
- <xsl:template match="/">
409
- <div><xsl:apply-templates select="/root/item" mode="special"/></div>
410
- </xsl:template>
411
- <xsl:template match="item" mode="special">
412
- <span class="special"><xsl:value-of select="."/></span>
413
- </xsl:template>
414
- <xsl:template match="item">
415
- <span><xsl:value-of select="."/></span>
416
- </xsl:template>
417
- </xsl:stylesheet>
418
- `);
419
-
420
- const xml = parseXML("<root><item>Test</item></root>");
421
-
422
- engine.importStylesheet(xslt);
423
- const result = engine.transform(xml, document);
424
-
425
- const span = result.querySelector("span.special");
426
- assert.ok(span);
427
- assert.strictEqual(span.textContent, "Test");
428
- });
429
-
430
- it("should apply templates with sorting", () => {
431
- const xslt = parseXML(`<?xml version="1.0"?>
432
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
433
- <xsl:template match="/">
434
- <ul>
435
- <xsl:apply-templates select="/root/item">
436
- <xsl:sort select="." order="descending"/>
437
- </xsl:apply-templates>
438
- </ul>
439
- </xsl:template>
440
- <xsl:template match="item">
441
- <li><xsl:value-of select="."/></li>
442
- </xsl:template>
443
- </xsl:stylesheet>
444
- `);
445
-
446
- const xml = parseXML(
447
- "<root><item>A</item><item>C</item><item>B</item></root>",
448
- );
449
-
450
- engine.importStylesheet(xslt);
451
- const result = engine.transform(xml, document);
452
-
453
- const items = result.querySelectorAll("li");
454
- assert.strictEqual(items[0].textContent, "C");
455
- assert.strictEqual(items[1].textContent, "B");
456
- assert.strictEqual(items[2].textContent, "A");
457
- });
458
-
459
- it("should apply templates with with-param", () => {
460
- const xslt = parseXML(`<?xml version="1.0"?>
461
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
462
- <xsl:template match="/">
463
- <xsl:apply-templates select="/root/item">
464
- <xsl:with-param name="prefix" select="'>> '"/>
465
- </xsl:apply-templates>
466
- </xsl:template>
467
- <xsl:template match="item">
468
- <xsl:param name="prefix"/>
469
- <p><xsl:value-of select="concat($prefix, .)"/></p>
470
- </xsl:template>
471
- </xsl:stylesheet>
472
- `);
473
-
474
- const xml = parseXML("<root><item>Test</item></root>");
475
-
476
- engine.importStylesheet(xslt);
477
- const result = engine.transform(xml, document);
478
-
479
- const p = result.querySelector("p");
480
- assert.ok(p.textContent.includes(">>"));
481
- assert.ok(p.textContent.includes("Test"));
482
- });
483
- });
484
-
485
- describe("xsl:call-template", () => {
486
- it("should throw for non-existent template", () => {
487
- const xslt = parseXML(`<?xml version="1.0"?>
488
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
489
- <xsl:template match="/">
490
- <xsl:call-template name="nonexistent"/>
491
- </xsl:template>
492
- </xsl:stylesheet>
493
- `);
494
-
495
- const xml = parseXML("<root/>");
496
-
497
- engine.importStylesheet(xslt);
498
-
499
- assert.throws(() => {
500
- engine.transform(xml, document);
501
- }, /Template not found/);
502
- });
503
-
504
- it("should call template with with-param content", () => {
505
- const xslt = parseXML(`<?xml version="1.0"?>
506
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
507
- <xsl:template match="/">
508
- <xsl:call-template name="wrapper">
509
- <xsl:with-param name="content"><strong>Bold</strong></xsl:with-param>
510
- </xsl:call-template>
511
- </xsl:template>
512
- <xsl:template name="wrapper">
513
- <xsl:param name="content"/>
514
- <div><xsl:copy-of select="$content"/></div>
515
- </xsl:template>
516
- </xsl:stylesheet>
517
- `);
518
-
519
- const xml = parseXML("<root/>");
520
-
521
- engine.importStylesheet(xslt);
522
- const result = engine.transform(xml, document);
523
-
524
- const div = result.querySelector("div");
525
- assert.ok(div);
526
- });
527
- });
528
-
529
- describe("xsl:value-of", () => {
530
- it("should output value with disable-output-escaping", () => {
531
- const xslt = parseXML(`<?xml version="1.0"?>
532
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
533
- <xsl:template match="/">
534
- <div><xsl:value-of select="/root/content" disable-output-escaping="yes"/></div>
535
- </xsl:template>
536
- </xsl:stylesheet>
537
- `);
538
-
539
- const xml = parseXML(
540
- "<root><content>&lt;p&gt;text&lt;/p&gt;</content></root>",
541
- );
542
-
543
- engine.importStylesheet(xslt);
544
- const result = engine.transform(xml, document);
545
-
546
- assert.ok(result.querySelector("div"));
547
- });
548
- });
549
-
550
- describe("xsl:text", () => {
551
- it("should output text content", () => {
552
- const xslt = parseXML(`<?xml version="1.0"?>
553
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
554
- <xsl:template match="/">
555
- <p><xsl:text>Hello, World!</xsl:text></p>
556
- </xsl:template>
557
- </xsl:stylesheet>
558
- `);
559
-
560
- const xml = parseXML("<root/>");
561
-
562
- engine.importStylesheet(xslt);
563
- const result = engine.transform(xml, document);
564
-
565
- const p = result.querySelector("p");
566
- assert.strictEqual(p.textContent, "Hello, World!");
567
- });
568
-
569
- it("should preserve whitespace in xsl:text", () => {
570
- const xslt = parseXML(`<?xml version="1.0"?>
571
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
572
- <xsl:template match="/">
573
- <pre><xsl:text> indented </xsl:text></pre>
574
- </xsl:template>
575
- </xsl:stylesheet>
576
- `);
577
-
578
- const xml = parseXML("<root/>");
579
-
580
- engine.importStylesheet(xslt);
581
- const result = engine.transform(xml, document);
582
-
583
- const pre = result.querySelector("pre");
584
- assert.strictEqual(pre.textContent, " indented ");
585
- });
586
- });
587
-
588
- describe("xsl:element", () => {
589
- it("should create element with dynamic name", () => {
590
- const xslt = parseXML(`<?xml version="1.0"?>
591
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
592
- <xsl:template match="/">
593
- <xsl:element name="{/root/tagName}">
594
- <xsl:value-of select="/root/content"/>
595
- </xsl:element>
596
- </xsl:template>
597
- </xsl:stylesheet>
598
- `);
599
-
600
- const xml = parseXML(
601
- "<root><tagName>custom</tagName><content>value</content></root>",
602
- );
603
-
604
- engine.importStylesheet(xslt);
605
- const result = engine.transform(xml, document);
606
-
607
- const custom = result.querySelector("custom");
608
- assert.ok(custom);
609
- assert.strictEqual(custom.textContent, "value");
610
- });
611
-
612
- it("should create element with namespace", () => {
613
- const xslt = parseXML(`<?xml version="1.0"?>
614
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
615
- <xsl:template match="/">
616
- <xsl:element name="item" namespace="http://example.com/ns">content</xsl:element>
617
- </xsl:template>
618
- </xsl:stylesheet>
619
- `);
620
-
621
- const xml = parseXML("<root/>");
622
-
623
- engine.importStylesheet(xslt);
624
- const result = engine.transform(xml, document);
625
-
626
- const item = result.querySelector("item");
627
- assert.ok(item);
628
- });
629
-
630
- it("should apply use-attribute-sets", () => {
631
- const xslt = parseXML(`<?xml version="1.0"?>
632
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
633
- <xsl:attribute-set name="common">
634
- <xsl:attribute name="class">styled</xsl:attribute>
635
- </xsl:attribute-set>
636
- <xsl:template match="/">
637
- <xsl:element name="div" use-attribute-sets="common">content</xsl:element>
638
- </xsl:template>
639
- </xsl:stylesheet>
640
- `);
641
-
642
- const xml = parseXML("<root/>");
643
-
644
- engine.importStylesheet(xslt);
645
- const result = engine.transform(xml, document);
646
-
647
- const div = result.querySelector("div");
648
- assert.strictEqual(div.getAttribute("class"), "styled");
649
- });
650
- });
651
-
652
- describe("xsl:attribute", () => {
653
- it("should create attribute with dynamic name", () => {
654
- const xslt = parseXML(`<?xml version="1.0"?>
655
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
656
- <xsl:template match="/">
657
- <div>
658
- <xsl:attribute name="{/root/attrName}">
659
- <xsl:value-of select="/root/attrValue"/>
660
- </xsl:attribute>
661
- </div>
662
- </xsl:template>
663
- </xsl:stylesheet>
664
- `);
665
-
666
- const xml = parseXML(
667
- "<root><attrName>data-id</attrName><attrValue>123</attrValue></root>",
668
- );
669
-
670
- engine.importStylesheet(xslt);
671
- const result = engine.transform(xml, document);
672
-
673
- const div = result.querySelector("div");
674
- assert.strictEqual(div.getAttribute("data-id"), "123");
675
- });
676
-
677
- it("should create attribute with namespace", () => {
678
- const xslt = parseXML(`<?xml version="1.0"?>
679
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
680
- <xsl:template match="/">
681
- <div>
682
- <xsl:attribute name="xml:lang" namespace="http://www.w3.org/XML/1998/namespace">en</xsl:attribute>
683
- </div>
684
- </xsl:template>
685
- </xsl:stylesheet>
686
- `);
687
-
688
- const xml = parseXML("<root/>");
689
-
690
- engine.importStylesheet(xslt);
691
- const result = engine.transform(xml, document);
692
-
693
- assert.ok(result.querySelector("div"));
694
- });
695
- });
696
-
697
- describe("xsl:copy", () => {
698
- it("should copy element with children", () => {
699
- const xslt = parseXML(`<?xml version="1.0"?>
700
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
701
- <xsl:template match="item">
702
- <xsl:copy>
703
- <xsl:apply-templates select="@*|node()"/>
704
- </xsl:copy>
705
- </xsl:template>
706
- <xsl:template match="@*|node()">
707
- <xsl:copy>
708
- <xsl:apply-templates select="@*|node()"/>
709
- </xsl:copy>
710
- </xsl:template>
711
- </xsl:stylesheet>
712
- `);
713
-
714
- const xml = parseXML('<root><item id="1">content</item></root>');
715
-
716
- engine.importStylesheet(xslt);
717
- const result = engine.transform(xml, document);
718
-
719
- const item = result.querySelector("item");
720
- assert.ok(item);
721
- });
722
-
723
- it("should copy text node", () => {
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="text()">
727
- <xsl:copy/>
728
- </xsl:template>
729
- </xsl:stylesheet>
730
- `);
731
-
732
- const xml = parseXML("<root>Hello</root>");
733
-
734
- engine.importStylesheet(xslt);
735
- const result = engine.transform(xml, document);
736
-
737
- assert.ok(result.textContent.includes("Hello"));
738
- });
739
-
740
- it("should copy with use-attribute-sets", () => {
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:attribute-set name="extra">
744
- <xsl:attribute name="data-copied">true</xsl:attribute>
745
- </xsl:attribute-set>
746
- <xsl:template match="item">
747
- <xsl:copy use-attribute-sets="extra">
748
- <xsl:apply-templates/>
749
- </xsl:copy>
750
- </xsl:template>
751
- <xsl:template match="/">
752
- <xsl:apply-templates select="/root/item"/>
753
- </xsl:template>
754
- </xsl:stylesheet>
755
- `);
756
-
757
- const xml = parseXML("<root><item>test</item></root>");
758
-
759
- engine.importStylesheet(xslt);
760
- const result = engine.transform(xml, document);
761
-
762
- const item = result.querySelector("item");
763
- assert.ok(item);
764
- assert.strictEqual(item.getAttribute("data-copied"), "true");
765
- });
766
- });
767
-
768
- describe("xsl:copy-of", () => {
769
- it("should deep copy nodes", () => {
770
- const xslt = parseXML(`<?xml version="1.0"?>
771
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
772
- <xsl:template match="/">
773
- <result>
774
- <xsl:copy-of select="/root/nested"/>
775
- </result>
776
- </xsl:template>
777
- </xsl:stylesheet>
778
- `);
779
-
780
- const xml = parseXML(
781
- '<root><nested><child attr="val">text</child></nested></root>',
782
- );
783
-
784
- engine.importStylesheet(xslt);
785
- const result = engine.transform(xml, document);
786
-
787
- const child = result.querySelector("child");
788
- assert.ok(child);
789
- assert.strictEqual(child.getAttribute("attr"), "val");
790
- assert.strictEqual(child.textContent, "text");
791
- });
792
-
793
- it("should copy primitive values as text", () => {
794
- const xslt = parseXML(`<?xml version="1.0"?>
795
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
796
- <xsl:template match="/">
797
- <result><xsl:copy-of select="1 + 2"/></result>
798
- </xsl:template>
799
- </xsl:stylesheet>
800
- `);
801
-
802
- const xml = parseXML("<root/>");
803
-
804
- engine.importStylesheet(xslt);
805
- const result = engine.transform(xml, document);
806
-
807
- assert.ok(result.textContent.includes("3"));
808
- });
809
- });
810
-
811
- describe("xsl:comment", () => {
812
- it("should create comment node", () => {
813
- const xslt = parseXML(`<?xml version="1.0"?>
814
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
815
- <xsl:template match="/">
816
- <div><xsl:comment>This is a comment</xsl:comment></div>
817
- </xsl:template>
818
- </xsl:stylesheet>
819
- `);
820
-
821
- const xml = parseXML("<root/>");
822
-
823
- engine.importStylesheet(xslt);
824
- const result = engine.transform(xml, document);
825
-
826
- let hasComment = false;
827
- for (const node of result.querySelector("div").childNodes) {
828
- if (node.nodeType === 8) {
829
- hasComment = true;
830
- assert.strictEqual(node.nodeValue, "This is a comment");
831
- }
832
- }
833
- assert.ok(hasComment);
834
- });
835
- });
836
-
837
- describe("xsl:processing-instruction", () => {
838
- it("should create processing instruction", () => {
839
- const xslt = parseXML(`<?xml version="1.0"?>
840
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
841
- <xsl:template match="/">
842
- <result>
843
- <xsl:processing-instruction name="xml-stylesheet">type="text/css" href="style.css"</xsl:processing-instruction>
844
- </result>
845
- </xsl:template>
846
- </xsl:stylesheet>
847
- `);
848
-
849
- const xml = parseXML("<root/>");
850
-
851
- engine.importStylesheet(xslt);
852
- const result = engine.transform(xml, document);
853
-
854
- let hasPI = false;
855
- for (const node of result.querySelector("result").childNodes) {
856
- if (node.nodeType === 7) {
857
- hasPI = true;
858
- assert.strictEqual(node.target, "xml-stylesheet");
859
- }
860
- }
861
- assert.ok(hasPI);
862
- });
863
- });
864
-
865
- describe("xsl:number", () => {
866
- it("should format number with padding", () => {
867
- const xslt = parseXML(`<?xml version="1.0"?>
868
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
869
- <xsl:template match="/">
870
- <p><xsl:number value="5" format="001"/></p>
871
- </xsl:template>
872
- </xsl:stylesheet>
873
- `);
874
-
875
- const xml = parseXML("<root/>");
876
-
877
- engine.importStylesheet(xslt);
878
- const result = engine.transform(xml, document);
879
-
880
- assert.strictEqual(result.querySelector("p").textContent, "005");
881
- });
882
-
883
- it("should format number as lowercase letter", () => {
884
- const xslt = parseXML(`<?xml version="1.0"?>
885
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
886
- <xsl:template match="/">
887
- <p><xsl:number value="3" format="a"/></p>
888
- </xsl:template>
889
- </xsl:stylesheet>
890
- `);
891
-
892
- const xml = parseXML("<root/>");
893
-
894
- engine.importStylesheet(xslt);
895
- const result = engine.transform(xml, document);
896
-
897
- assert.strictEqual(result.querySelector("p").textContent, "c");
898
- });
899
-
900
- it("should format number as uppercase letter", () => {
901
- const xslt = parseXML(`<?xml version="1.0"?>
902
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
903
- <xsl:template match="/">
904
- <p><xsl:number value="1" format="A"/></p>
905
- </xsl:template>
906
- </xsl:stylesheet>
907
- `);
908
-
909
- const xml = parseXML("<root/>");
910
-
911
- engine.importStylesheet(xslt);
912
- const result = engine.transform(xml, document);
913
-
914
- assert.strictEqual(result.querySelector("p").textContent, "A");
915
- });
916
-
917
- it("should format number as lowercase roman numeral", () => {
918
- const xslt = parseXML(`<?xml version="1.0"?>
919
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
920
- <xsl:template match="/">
921
- <p><xsl:number value="4" format="i"/></p>
922
- </xsl:template>
923
- </xsl:stylesheet>
924
- `);
925
-
926
- const xml = parseXML("<root/>");
927
-
928
- engine.importStylesheet(xslt);
929
- const result = engine.transform(xml, document);
930
-
931
- assert.strictEqual(result.querySelector("p").textContent, "iv");
932
- });
933
-
934
- it("should format number as uppercase roman numeral", () => {
935
- const xslt = parseXML(`<?xml version="1.0"?>
936
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
937
- <xsl:template match="/">
938
- <p><xsl:number value="9" format="I"/></p>
939
- </xsl:template>
940
- </xsl:stylesheet>
941
- `);
942
-
943
- const xml = parseXML("<root/>");
944
-
945
- engine.importStylesheet(xslt);
946
- const result = engine.transform(xml, document);
947
-
948
- assert.strictEqual(result.querySelector("p").textContent, "IX");
949
- });
950
-
951
- it("should count position without value attribute", () => {
952
- const xslt = parseXML(`<?xml version="1.0"?>
953
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
954
- <xsl:template match="/">
955
- <ul><xsl:apply-templates select="/root/item"/></ul>
956
- </xsl:template>
957
- <xsl:template match="item">
958
- <li><xsl:number/>. <xsl:value-of select="."/></li>
959
- </xsl:template>
960
- </xsl:stylesheet>
961
- `);
962
-
963
- const xml = parseXML(
964
- "<root><item>A</item><item>B</item><item>C</item></root>",
965
- );
966
-
967
- engine.importStylesheet(xslt);
968
- const result = engine.transform(xml, document);
969
-
970
- const items = result.querySelectorAll("li");
971
- assert.ok(items[0].textContent.includes("1"));
972
- assert.ok(items[1].textContent.includes("2"));
973
- assert.ok(items[2].textContent.includes("3"));
974
- });
975
- });
976
-
977
- describe("xsl:message", () => {
978
- it("should output message without terminating", () => {
979
- const xslt = parseXML(`<?xml version="1.0"?>
980
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
981
- <xsl:template match="/">
982
- <xsl:message>Debug message</xsl:message>
983
- <result>success</result>
984
- </xsl:template>
985
- </xsl:stylesheet>
986
- `);
987
-
988
- const xml = parseXML("<root/>");
989
-
990
- engine.importStylesheet(xslt);
991
- const result = engine.transform(xml, document);
992
-
993
- assert.ok(result.querySelector("result"));
994
- });
995
-
996
- it("should terminate when terminate=yes", () => {
997
- const xslt = parseXML(`<?xml version="1.0"?>
998
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
999
- <xsl:template match="/">
1000
- <xsl:message terminate="yes">Fatal error</xsl:message>
1001
- <result>should not reach</result>
1002
- </xsl:template>
1003
- </xsl:stylesheet>
1004
- `);
1005
-
1006
- const xml = parseXML("<root/>");
1007
-
1008
- engine.importStylesheet(xslt);
1009
-
1010
- assert.throws(() => {
1011
- engine.transform(xml, document);
1012
- }, /XSLT terminated/);
1013
- });
1014
- });
1015
-
1016
- describe("xsl:variable", () => {
1017
- it("should create variable with select", () => {
1018
- const xslt = parseXML(`<?xml version="1.0"?>
1019
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1020
- <xsl:template match="/">
1021
- <xsl:variable name="count" select="count(/root/item)"/>
1022
- <p>Count: <xsl:value-of select="$count"/></p>
1023
- </xsl:template>
1024
- </xsl:stylesheet>
1025
- `);
1026
-
1027
- const xml = parseXML("<root><item/><item/><item/></root>");
1028
-
1029
- engine.importStylesheet(xslt);
1030
- const result = engine.transform(xml, document);
1031
-
1032
- assert.ok(result.querySelector("p").textContent.includes("3"));
1033
- });
1034
-
1035
- it("should create variable with content", () => {
1036
- const xslt = parseXML(`<?xml version="1.0"?>
1037
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1038
- <xsl:template match="/">
1039
- <xsl:variable name="text">emphasized text</xsl:variable>
1040
- <div><xsl:value-of select="$text"/></div>
1041
- </xsl:template>
1042
- </xsl:stylesheet>
1043
- `);
1044
-
1045
- const xml = parseXML("<root/>");
1046
-
1047
- engine.importStylesheet(xslt);
1048
- const result = engine.transform(xml, document);
1049
-
1050
- const div = result.querySelector("div");
1051
- assert.ok(div);
1052
- assert.ok(div.textContent.includes("emphasized"));
1053
- });
1054
- });
1055
-
1056
- describe("sortNodes", () => {
1057
- it("should sort nodes by number", () => {
1058
- const xslt = parseXML(`<?xml version="1.0"?>
1059
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1060
- <xsl:template match="/">
1061
- <ul>
1062
- <xsl:for-each select="/root/item">
1063
- <xsl:sort select="@value" data-type="number"/>
1064
- <li><xsl:value-of select="@value"/></li>
1065
- </xsl:for-each>
1066
- </ul>
1067
- </xsl:template>
1068
- </xsl:stylesheet>
1069
- `);
1070
-
1071
- const xml = parseXML(
1072
- '<root><item value="10"/><item value="2"/><item value="5"/></root>',
1073
- );
1074
-
1075
- engine.importStylesheet(xslt);
1076
- const result = engine.transform(xml, document);
1077
-
1078
- const items = result.querySelectorAll("li");
1079
- assert.strictEqual(items[0].textContent, "2");
1080
- assert.strictEqual(items[1].textContent, "5");
1081
- assert.strictEqual(items[2].textContent, "10");
1082
- });
1083
-
1084
- it("should sort with case-order lower-first", () => {
1085
- const xslt = parseXML(`<?xml version="1.0"?>
1086
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1087
- <xsl:template match="/">
1088
- <ul>
1089
- <xsl:for-each select="/root/item">
1090
- <xsl:sort select="." case-order="lower-first"/>
1091
- <li><xsl:value-of select="."/></li>
1092
- </xsl:for-each>
1093
- </ul>
1094
- </xsl:template>
1095
- </xsl:stylesheet>
1096
- `);
1097
-
1098
- const xml = parseXML(
1099
- "<root><item>B</item><item>a</item><item>A</item></root>",
1100
- );
1101
-
1102
- engine.importStylesheet(xslt);
1103
- const result = engine.transform(xml, document);
1104
-
1105
- const items = result.querySelectorAll("li");
1106
- assert.strictEqual(items.length, 3);
1107
- });
1108
- });
1109
-
1110
- describe("splitUnionPattern", () => {
1111
- it("should split union patterns", () => {
1112
- const parts = engine.splitUnionPattern("a|b|c");
1113
- assert.deepStrictEqual(parts, ["a", "b", "c"]);
1114
- });
1115
-
1116
- it("should handle patterns with predicates", () => {
1117
- const parts = engine.splitUnionPattern('a[@x]|b[contains(., "|")]');
1118
- assert.strictEqual(parts.length, 2);
1119
- assert.strictEqual(parts[0], "a[@x]");
1120
- assert.strictEqual(parts[1], 'b[contains(., "|")]');
1121
- });
1122
-
1123
- it("should handle patterns with strings", () => {
1124
- const parts = engine.splitUnionPattern("a|'string|with|pipes'|b");
1125
- assert.strictEqual(parts.length, 3);
1126
- });
1127
- });
1128
-
1129
- describe("processAttributeValueTemplate", () => {
1130
- it("should process simple template", () => {
1131
- const context = new XsltContext({
1132
- currentNode: parseXML("<root><id>123</id></root>").documentElement,
1133
- namespaces: {},
1134
- });
1135
-
1136
- const result = engine.processAttributeValueTemplate(
1137
- "/item/{/root/id}",
1138
- context,
1139
- );
1140
- assert.strictEqual(result, "/item/123");
1141
- });
1142
-
1143
- it("should handle escaped braces", () => {
1144
- const context = new XsltContext({
1145
- currentNode: parseXML("<root/>").documentElement,
1146
- namespaces: {},
1147
- });
1148
-
1149
- const result = engine.processAttributeValueTemplate(
1150
- "{{literal}}",
1151
- context,
1152
- );
1153
- assert.strictEqual(result, "{literal}");
1154
- });
1155
-
1156
- it("should return unchanged string without braces", () => {
1157
- const context = new XsltContext({
1158
- currentNode: parseXML("<root/>").documentElement,
1159
- namespaces: {},
1160
- });
1161
-
1162
- const result = engine.processAttributeValueTemplate(
1163
- "plain text",
1164
- context,
1165
- );
1166
- assert.strictEqual(result, "plain text");
1167
- });
1168
-
1169
- it("should handle unmatched closing brace gracefully", () => {
1170
- const context = new XsltContext({
1171
- currentNode: parseXML("<root/>").documentElement,
1172
- namespaces: {},
1173
- });
1174
-
1175
- // The engine handles single } by either throwing or passing through
1176
- // depending on implementation - test the actual behavior
1177
- try {
1178
- const result = engine.processAttributeValueTemplate(
1179
- "text}more",
1180
- context,
1181
- );
1182
- // If it doesn't throw, it should preserve the text somehow
1183
- assert.ok(typeof result === "string");
1184
- } catch (e) {
1185
- // If it throws, it should be about unmatched brace
1186
- assert.ok(e.message.includes("}") || e.message.includes("brace"));
1187
- }
1188
- });
1189
- });
1190
-
1191
- describe("built-in templates", () => {
1192
- it("should apply built-in template for elements", () => {
1193
- const xslt = parseXML(`<?xml version="1.0"?>
1194
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1195
- <xsl:template match="target">
1196
- <found><xsl:value-of select="."/></found>
1197
- </xsl:template>
1198
- </xsl:stylesheet>
1199
- `);
1200
-
1201
- const xml = parseXML(
1202
- "<root><wrapper><target>value</target></wrapper></root>",
1203
- );
1204
-
1205
- engine.importStylesheet(xslt);
1206
- const result = engine.transform(xml, document);
1207
-
1208
- const found = result.querySelector("found");
1209
- assert.ok(found);
1210
- assert.strictEqual(found.textContent, "value");
1211
- });
1212
-
1213
- it("should apply built-in template for text nodes", () => {
1214
- const xslt = parseXML(`<?xml version="1.0"?>
1215
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1216
- </xsl:stylesheet>
1217
- `);
1218
-
1219
- const xml = parseXML("<root>plain text</root>");
1220
-
1221
- engine.importStylesheet(xslt);
1222
- const result = engine.transform(xml, document);
1223
-
1224
- assert.ok(result.textContent.includes("plain text"));
1225
- });
1226
- });
1227
-
1228
- describe("toRoman", () => {
1229
- it("should convert numbers to roman numerals", () => {
1230
- assert.strictEqual(engine.toRoman(1), "I");
1231
- assert.strictEqual(engine.toRoman(4), "IV");
1232
- assert.strictEqual(engine.toRoman(9), "IX");
1233
- assert.strictEqual(engine.toRoman(10), "X");
1234
- assert.strictEqual(engine.toRoman(40), "XL");
1235
- assert.strictEqual(engine.toRoman(50), "L");
1236
- assert.strictEqual(engine.toRoman(90), "XC");
1237
- assert.strictEqual(engine.toRoman(100), "C");
1238
- assert.strictEqual(engine.toRoman(400), "CD");
1239
- assert.strictEqual(engine.toRoman(500), "D");
1240
- assert.strictEqual(engine.toRoman(900), "CM");
1241
- assert.strictEqual(engine.toRoman(1000), "M");
1242
- assert.strictEqual(engine.toRoman(1999), "MCMXCIX");
1243
- assert.strictEqual(engine.toRoman(2024), "MMXXIV");
1244
- });
1245
- });
1246
-
1247
- describe("deepCloneNode", () => {
1248
- it("should clone element with attributes and children", () => {
1249
- const source = parseXML(
1250
- '<root attr="value"><child>text</child></root>',
1251
- ).documentElement;
1252
- const clone = engine.deepCloneNode(source, document);
1253
-
1254
- assert.strictEqual(clone.nodeName.toLowerCase(), "root");
1255
- assert.strictEqual(clone.getAttribute("attr"), "value");
1256
- assert.ok(clone.querySelector("child"));
1257
- });
1258
-
1259
- it("should clone text node", () => {
1260
- const textNode = document.createTextNode("hello");
1261
- const clone = engine.deepCloneNode(textNode, document);
1262
-
1263
- assert.strictEqual(clone.nodeType, 3);
1264
- assert.strictEqual(clone.nodeValue, "hello");
1265
- });
1266
-
1267
- it("should clone comment node", () => {
1268
- const comment = document.createComment("comment text");
1269
- const clone = engine.deepCloneNode(comment, document);
1270
-
1271
- assert.strictEqual(clone.nodeType, 8);
1272
- assert.strictEqual(clone.nodeValue, "comment text");
1273
- });
1274
-
1275
- it("should clone document fragment", () => {
1276
- const frag = document.createDocumentFragment();
1277
- frag.appendChild(document.createElement("div"));
1278
- frag.appendChild(document.createElement("span"));
1279
-
1280
- const clone = engine.deepCloneNode(frag, document);
1281
-
1282
- assert.strictEqual(clone.nodeType, 11);
1283
- assert.strictEqual(clone.childNodes.length, 2);
1284
- });
1285
- });
1286
-
1287
- describe("shouldPreserveSpace", () => {
1288
- it("should return true when xml:space is preserve", () => {
1289
- const xml = parseXML(
1290
- '<root xml:space="preserve"><child> text </child></root>',
1291
- );
1292
- const textNode = xml.querySelector("child").firstChild;
1293
-
1294
- assert.strictEqual(engine.shouldPreserveSpace(textNode), true);
1295
- });
1296
-
1297
- it("should return false when xml:space is default", () => {
1298
- const xml = parseXML(
1299
- '<root xml:space="default"><child> text </child></root>',
1300
- );
1301
- const textNode = xml.querySelector("child").firstChild;
1302
-
1303
- assert.strictEqual(engine.shouldPreserveSpace(textNode), false);
1304
- });
1305
-
1306
- it("should return false when no xml:space attribute", () => {
1307
- const xml = parseXML("<root><child> text </child></root>");
1308
- const textNode = xml.querySelector("child").firstChild;
1309
-
1310
- assert.strictEqual(engine.shouldPreserveSpace(textNode), false);
1311
- });
1312
- });
1313
-
1314
- describe("collectNamespaces", () => {
1315
- it("should collect namespace declarations", () => {
1316
- const xml = parseXML(
1317
- '<root xmlns:foo="http://foo.com" xmlns:bar="http://bar.com"/>',
1318
- );
1319
-
1320
- engine.collectNamespaces(xml.documentElement);
1321
-
1322
- assert.strictEqual(engine.namespaces.foo, "http://foo.com");
1323
- assert.strictEqual(engine.namespaces.bar, "http://bar.com");
1324
- });
1325
-
1326
- it("should ignore XSLT namespace", () => {
1327
- const xml = parseXML(
1328
- '<root xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/>',
1329
- );
1330
-
1331
- engine.collectNamespaces(xml.documentElement);
1332
-
1333
- assert.strictEqual(engine.namespaces.xsl, undefined);
1334
- });
1335
- });
1336
-
1337
- describe("applyAttributeSets", () => {
1338
- it("should apply nested attribute sets via xsl:element", () => {
1339
- const xslt = parseXML(`<?xml version="1.0"?>
1340
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1341
- <xsl:attribute-set name="base">
1342
- <xsl:attribute name="class">base-class</xsl:attribute>
1343
- </xsl:attribute-set>
1344
- <xsl:attribute-set name="extended" use-attribute-sets="base">
1345
- <xsl:attribute name="id">extended-id</xsl:attribute>
1346
- </xsl:attribute-set>
1347
- <xsl:template match="/">
1348
- <xsl:element name="div" use-attribute-sets="extended">content</xsl:element>
1349
- </xsl:template>
1350
- </xsl:stylesheet>
1351
- `);
1352
-
1353
- const xml = parseXML("<root/>");
1354
-
1355
- engine.importStylesheet(xslt);
1356
- const result = engine.transform(xml, document);
1357
-
1358
- const div = result.querySelector("div");
1359
- assert.strictEqual(div.getAttribute("class"), "base-class");
1360
- assert.strictEqual(div.getAttribute("id"), "extended-id");
1361
- });
1362
-
1363
- it("should apply attribute sets directly", () => {
1364
- const xslt = parseXML(`<?xml version="1.0"?>
1365
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1366
- <xsl:attribute-set name="common">
1367
- <xsl:attribute name="data-test">value</xsl:attribute>
1368
- </xsl:attribute-set>
1369
- <xsl:template match="/"><out/></xsl:template>
1370
- </xsl:stylesheet>
1371
- `);
1372
-
1373
- engine.importStylesheet(xslt);
1374
-
1375
- const context = new XsltContext({
1376
- currentNode: parseXML("<root/>").documentElement,
1377
- outputDocument: document,
1378
- });
1379
-
1380
- const element = document.createElement("div");
1381
- engine.applyAttributeSets("common", context, element);
1382
-
1383
- assert.strictEqual(element.getAttribute("data-test"), "value");
1384
- });
1385
- });
1386
-
1387
- describe("collectNamespaces - default namespace", () => {
1388
- it("should collect default xmlns namespace", () => {
1389
- const xml = parseXML('<root xmlns="http://example.com/default"/>');
1390
-
1391
- engine.collectNamespaces(xml.documentElement);
1392
-
1393
- assert.strictEqual(engine.namespaces[""], "http://example.com/default");
1394
- });
1395
- });
1396
-
1397
- describe("xsl:output cdata-section-elements", () => {
1398
- it("should process cdata-section-elements attribute", () => {
1399
- const xslt = parseXML(`<?xml version="1.0"?>
1400
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1401
- <xsl:output method="xml" cdata-section-elements="script style"/>
1402
- <xsl:template match="/"><out/></xsl:template>
1403
- </xsl:stylesheet>
1404
- `);
1405
-
1406
- engine.importStylesheet(xslt);
1407
-
1408
- assert.deepStrictEqual(engine.outputSettings.cdataSectionElements, [
1409
- "script",
1410
- "style",
1411
- ]);
1412
- });
1413
- });
1414
-
1415
- describe("global parameters evaluation", () => {
1416
- it("should evaluate global parameters without preset values", () => {
1417
- const xslt = parseXML(`<?xml version="1.0"?>
1418
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1419
- <xsl:param name="testParam" select="'defaultValue'"/>
1420
- <xsl:template match="/">
1421
- <result><xsl:value-of select="$testParam"/></result>
1422
- </xsl:template>
1423
- </xsl:stylesheet>
1424
- `);
1425
-
1426
- const xml = parseXML("<root/>");
1427
-
1428
- engine.importStylesheet(xslt);
1429
- const result = engine.transform(xml, document);
1430
-
1431
- assert.ok(
1432
- result.querySelector("result").textContent.includes("defaultValue"),
1433
- );
1434
- });
1435
- });
1436
-
1437
- describe("evaluateVariable with content", () => {
1438
- it("should evaluate variable with tree fragment content", () => {
1439
- const xslt = parseXML(`<?xml version="1.0"?>
1440
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1441
- <xsl:variable name="frag"><item>content</item></xsl:variable>
1442
- <xsl:template match="/">
1443
- <result><xsl:copy-of select="$frag"/></result>
1444
- </xsl:template>
1445
- </xsl:stylesheet>
1446
- `);
1447
-
1448
- const xml = parseXML("<root/>");
1449
-
1450
- engine.importStylesheet(xslt);
1451
- const result = engine.transform(xml, document);
1452
-
1453
- assert.ok(result.querySelector("result"));
1454
- });
1455
- });
1456
-
1457
- describe("matchesSinglePattern with absolute path", () => {
1458
- it("should match pattern starting with /", () => {
1459
- const xslt = parseXML(`<?xml version="1.0"?>
1460
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1461
- <xsl:template match="/root/item">
1462
- <matched><xsl:value-of select="."/></matched>
1463
- </xsl:template>
1464
- </xsl:stylesheet>
1465
- `);
1466
-
1467
- const xml = parseXML("<root><item>test</item></root>");
1468
-
1469
- engine.importStylesheet(xslt);
1470
- const result = engine.transform(xml, document);
1471
-
1472
- assert.ok(result.querySelector("matched"));
1473
- });
1474
- });
1475
-
1476
- describe("matchesSinglePattern catch block", () => {
1477
- it("should return false on pattern matching error", () => {
1478
- const context = new XsltContext({
1479
- currentNode: parseXML("<root/>").documentElement,
1480
- variables: {},
1481
- parameters: {},
1482
- namespaces: {},
1483
- });
1484
-
1485
- // Invalid pattern should not throw, just return false
1486
- const result = engine.matchesPattern(
1487
- parseXML("<root/>").documentElement,
1488
- "[[[invalid",
1489
- context,
1490
- );
1491
-
1492
- assert.strictEqual(result, false);
1493
- });
1494
- });
1495
-
1496
- describe("template param processing", () => {
1497
- it("should process template params with default values", () => {
1498
- const xslt = parseXML(`<?xml version="1.0"?>
1499
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1500
- <xsl:template match="/">
1501
- <xsl:call-template name="test"/>
1502
- </xsl:template>
1503
- <xsl:template name="test">
1504
- <xsl:param name="p1" select="'default'"/>
1505
- <result><xsl:value-of select="$p1"/></result>
1506
- </xsl:template>
1507
- </xsl:stylesheet>
1508
- `);
1509
-
1510
- const xml = parseXML("<root/>");
1511
-
1512
- engine.importStylesheet(xslt);
1513
- const result = engine.transform(xml, document);
1514
-
1515
- assert.ok(result.querySelector("result").textContent.includes("default"));
1516
- });
1517
- });
1518
-
1519
- describe("unknown XSLT element", () => {
1520
- it("should warn on unknown XSLT element", () => {
1521
- const xslt = parseXML(`<?xml version="1.0"?>
1522
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1523
- <xsl:template match="/">
1524
- <xsl:unknown-element/>
1525
- <result>done</result>
1526
- </xsl:template>
1527
- </xsl:stylesheet>
1528
- `);
1529
-
1530
- const xml = parseXML("<root/>");
1531
-
1532
- engine.importStylesheet(xslt);
1533
- const result = engine.transform(xml, document);
1534
-
1535
- assert.ok(result.querySelector("result"));
1536
- });
1537
- });
1538
-
1539
- describe("xsl:text with disable-output-escaping", () => {
1540
- it("should handle disable-output-escaping in xsl:text", () => {
1541
- const xslt = parseXML(`<?xml version="1.0"?>
1542
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1543
- <xsl:template match="/">
1544
- <div><xsl:text disable-output-escaping="yes">&lt;span&gt;</xsl:text></div>
1545
- </xsl:template>
1546
- </xsl:stylesheet>
1547
- `);
1548
-
1549
- const xml = parseXML("<root/>");
1550
-
1551
- engine.importStylesheet(xslt);
1552
- const result = engine.transform(xml, document);
1553
-
1554
- assert.ok(result.querySelector("div"));
1555
- });
1556
- });
1557
-
1558
- describe("xsl:choose with otherwise", () => {
1559
- it("should execute otherwise when no when matches", () => {
1560
- const xslt = parseXML(`<?xml version="1.0"?>
1561
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1562
- <xsl:template match="/">
1563
- <xsl:choose>
1564
- <xsl:when test="false()">
1565
- <wrong/>
1566
- </xsl:when>
1567
- <xsl:otherwise>
1568
- <correct/>
1569
- </xsl:otherwise>
1570
- </xsl:choose>
1571
- </xsl:template>
1572
- </xsl:stylesheet>
1573
- `);
1574
-
1575
- const xml = parseXML("<root/>");
1576
-
1577
- engine.importStylesheet(xslt);
1578
- const result = engine.transform(xml, document);
1579
-
1580
- assert.ok(result.querySelector("correct"));
1581
- assert.ok(!result.querySelector("wrong"));
1582
- });
1583
- });
1584
-
1585
- describe("xsl:for-each with single node result", () => {
1586
- it("should handle single node as select result", () => {
1587
- const xslt = parseXML(`<?xml version="1.0"?>
1588
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1589
- <xsl:template match="/">
1590
- <xsl:for-each select="/root">
1591
- <item><xsl:value-of select="local-name()"/></item>
1592
- </xsl:for-each>
1593
- </xsl:template>
1594
- </xsl:stylesheet>
1595
- `);
1596
-
1597
- const xml = parseXML("<root/>");
1598
-
1599
- engine.importStylesheet(xslt);
1600
- const result = engine.transform(xml, document);
1601
-
1602
- const item = result.querySelector("item");
1603
- assert.ok(item);
1604
- assert.strictEqual(item.textContent, "root");
1605
- });
1606
- });
1607
-
1608
- describe("xsl:copy for various node types", () => {
1609
- it("should copy element with namespace", () => {
1610
- const xslt = parseXML(`<?xml version="1.0"?>
1611
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ns="http://example.com">
1612
- <xsl:template match="ns:item">
1613
- <xsl:copy>copied</xsl:copy>
1614
- </xsl:template>
1615
- <xsl:template match="/">
1616
- <xsl:apply-templates select="//*[local-name()='item']"/>
1617
- </xsl:template>
1618
- </xsl:stylesheet>
1619
- `);
1620
-
1621
- const xml = parseXML(
1622
- '<root xmlns:ns="http://example.com"><ns:item/></root>',
1623
- );
1624
-
1625
- engine.importStylesheet(xslt);
1626
- const result = engine.transform(xml, document);
1627
-
1628
- assert.ok(result.textContent.includes("copied"));
1629
- });
1630
-
1631
- it("should copy processing instruction node", () => {
1632
- const xslt = parseXML(`<?xml version="1.0"?>
1633
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1634
- <xsl:template match="processing-instruction()">
1635
- <xsl:copy/>
1636
- </xsl:template>
1637
- <xsl:template match="/">
1638
- <result><xsl:apply-templates select="/root/processing-instruction()"/></result>
1639
- </xsl:template>
1640
- </xsl:stylesheet>
1641
- `);
1642
-
1643
- const xml = parseXML("<root><?target data?></root>");
1644
-
1645
- engine.importStylesheet(xslt);
1646
- const result = engine.transform(xml, document);
1647
-
1648
- let _hasPI = false;
1649
- const walker = document.createTreeWalker(result, 128); // NodeFilter.SHOW_PROCESSING_INSTRUCTION
1650
- if (walker.nextNode()) {
1651
- _hasPI = true;
1652
- }
1653
- // PI may or may not be created depending on DOM impl
1654
- assert.ok(result.querySelector("result"));
1655
- });
1656
-
1657
- it("should copy comment node", () => {
1658
- const xslt = parseXML(`<?xml version="1.0"?>
1659
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1660
- <xsl:template match="comment()">
1661
- <xsl:copy/>
1662
- </xsl:template>
1663
- <xsl:template match="/">
1664
- <result><xsl:apply-templates select="/root/comment()"/></result>
1665
- </xsl:template>
1666
- </xsl:stylesheet>
1667
- `);
1668
-
1669
- const xml = parseXML("<root><!-- test comment --></root>");
1670
-
1671
- engine.importStylesheet(xslt);
1672
- const result = engine.transform(xml, document);
1673
-
1674
- assert.ok(result.querySelector("result"));
1675
- });
1676
-
1677
- it("should copy document fragment children", () => {
1678
- const xslt = parseXML(`<?xml version="1.0"?>
1679
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1680
- <xsl:template match="/">
1681
- <xsl:copy>
1682
- <item>inside</item>
1683
- </xsl:copy>
1684
- </xsl:template>
1685
- </xsl:stylesheet>
1686
- `);
1687
-
1688
- const xml = parseXML("<root/>");
1689
-
1690
- engine.importStylesheet(xslt);
1691
- const result = engine.transform(xml, document);
1692
-
1693
- assert.ok(result.querySelector("item"));
1694
- });
1695
- });
1696
-
1697
- describe("deepCloneNode for processing instruction", () => {
1698
- it("should clone processing instruction node", () => {
1699
- const pi = document.createProcessingInstruction("target", "data");
1700
- const clone = engine.deepCloneNode(pi, document);
1701
-
1702
- assert.strictEqual(clone.nodeType, 7);
1703
- assert.strictEqual(clone.target, "target");
1704
- assert.strictEqual(clone.data, "data");
1705
- });
1706
- });
1707
-
1708
- describe("deepCloneNode default case", () => {
1709
- it("should return empty text for unknown node types", () => {
1710
- // Create a mock node with unusual nodeType
1711
- const mockNode = { nodeType: 99 };
1712
- const clone = engine.deepCloneNode(mockNode, document);
1713
-
1714
- assert.strictEqual(clone.nodeType, 3);
1715
- assert.strictEqual(clone.nodeValue, "");
1716
- });
1717
- });
1718
-
1719
- describe("countNumber without count pattern", () => {
1720
- it("should count siblings without count attribute", () => {
1721
- const xslt = parseXML(`<?xml version="1.0"?>
1722
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1723
- <xsl:template match="item">
1724
- <li><xsl:number level="single"/>. <xsl:value-of select="."/></li>
1725
- </xsl:template>
1726
- <xsl:template match="/">
1727
- <ul><xsl:apply-templates select="/root/item"/></ul>
1728
- </xsl:template>
1729
- </xsl:stylesheet>
1730
- `);
1731
-
1732
- const xml = parseXML("<root><item>A</item><item>B</item></root>");
1733
-
1734
- engine.importStylesheet(xslt);
1735
- const result = engine.transform(xml, document);
1736
-
1737
- const items = result.querySelectorAll("li");
1738
- assert.ok(items.length >= 2);
1739
- });
1740
-
1741
- it("should return 1 for non-single level", () => {
1742
- const xslt = parseXML(`<?xml version="1.0"?>
1743
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1744
- <xsl:template match="item">
1745
- <li><xsl:number level="multiple"/>. <xsl:value-of select="."/></li>
1746
- </xsl:template>
1747
- <xsl:template match="/">
1748
- <ul><xsl:apply-templates select="/root/item"/></ul>
1749
- </xsl:template>
1750
- </xsl:stylesheet>
1751
- `);
1752
-
1753
- const xml = parseXML("<root><item>A</item></root>");
1754
-
1755
- engine.importStylesheet(xslt);
1756
- const result = engine.transform(xml, document);
1757
-
1758
- assert.ok(result.querySelector("li").textContent.includes("1"));
1759
- });
1760
- });
1761
-
1762
- describe("formatNumber default format", () => {
1763
- it("should use default format for unrecognized pattern", () => {
1764
- const xslt = parseXML(`<?xml version="1.0"?>
1765
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1766
- <xsl:template match="/">
1767
- <p><xsl:number value="42" format="x"/></p>
1768
- </xsl:template>
1769
- </xsl:stylesheet>
1770
- `);
1771
-
1772
- const xml = parseXML("<root/>");
1773
-
1774
- engine.importStylesheet(xslt);
1775
- const result = engine.transform(xml, document);
1776
-
1777
- assert.strictEqual(result.querySelector("p").textContent, "42");
1778
- });
1779
- });
1780
-
1781
- describe("xsl:apply-templates with single node result", () => {
1782
- it("should handle single node select result", () => {
1783
- const xslt = parseXML(`<?xml version="1.0"?>
1784
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1785
- <xsl:template match="item">
1786
- <found><xsl:value-of select="."/></found>
1787
- </xsl:template>
1788
- <xsl:template match="/">
1789
- <xsl:apply-templates select="/root/item[1]"/>
1790
- </xsl:template>
1791
- </xsl:stylesheet>
1792
- `);
1793
-
1794
- const xml = parseXML(
1795
- "<root><item>first</item><item>second</item></root>",
1796
- );
1797
-
1798
- engine.importStylesheet(xslt);
1799
- const result = engine.transform(xml, document);
1800
-
1801
- const found = result.querySelector("found");
1802
- assert.ok(found);
1803
- assert.strictEqual(found.textContent, "first");
1804
- });
1805
-
1806
- it("should handle empty node result", () => {
1807
- const xslt = parseXML(`<?xml version="1.0"?>
1808
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1809
- <xsl:template match="/">
1810
- <result><xsl:apply-templates select="/root/nonexistent"/></result>
1811
- </xsl:template>
1812
- </xsl:stylesheet>
1813
- `);
1814
-
1815
- const xml = parseXML("<root/>");
1816
-
1817
- engine.importStylesheet(xslt);
1818
- const result = engine.transform(xml, document);
1819
-
1820
- assert.ok(result.querySelector("result"));
1821
- });
1822
- });
1823
-
1824
- describe("createDocument without global document", () => {
1825
- it("should throw when document is not available", () => {
1826
- const xslt = parseXML(`<?xml version="1.0"?>
1827
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1828
- <xsl:template match="/"><out/></xsl:template>
1829
- </xsl:stylesheet>
1830
- `);
1831
-
1832
- const xml = parseXML("<root/>");
1833
-
1834
- engine.importStylesheet(xslt);
1835
-
1836
- const originalDoc = global.document;
1837
- global.document = undefined;
1838
-
1839
- assert.throws(() => {
1840
- engine.transformToDocument(xml);
1841
- }, /Document creation not available/);
1842
-
1843
- global.document = originalDoc;
1844
- });
1845
- });
1846
-
1847
- describe("namespace aliases in literal result elements", () => {
1848
- it("should apply namespace aliases", () => {
1849
- const xslt = parseXML(`<?xml version="1.0"?>
1850
- <xsl:stylesheet version="1.0"
1851
- xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
1852
- xmlns:myns="http://example.com/myns"
1853
- xmlns:out="http://example.com/output">
1854
- <xsl:namespace-alias stylesheet-prefix="myns" result-prefix="out"/>
1855
- <xsl:template match="/">
1856
- <myns:element>content</myns:element>
1857
- </xsl:template>
1858
- </xsl:stylesheet>
1859
- `);
1860
-
1861
- const xml = parseXML("<root/>");
1862
-
1863
- engine.importStylesheet(xslt);
1864
- const result = engine.transform(xml, document);
1865
-
1866
- assert.ok(result.childNodes.length > 0);
1867
- });
1868
- });
1869
-
1870
- describe("xsl:for-each with empty result", () => {
1871
- it("should handle empty node set", () => {
1872
- const xslt = parseXML(`<?xml version="1.0"?>
1873
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1874
- <xsl:template match="/">
1875
- <result>
1876
- <xsl:for-each select="/root/nonexistent">
1877
- <item/>
1878
- </xsl:for-each>
1879
- done
1880
- </result>
1881
- </xsl:template>
1882
- </xsl:stylesheet>
1883
- `);
1884
-
1885
- const xml = parseXML("<root/>");
1886
-
1887
- engine.importStylesheet(xslt);
1888
- const result = engine.transform(xml, document);
1889
-
1890
- assert.ok(result.querySelector("result"));
1891
- assert.ok(!result.querySelector("item"));
1892
- });
1893
- });
1894
-
1895
- describe("xsl:copy for attribute nodes", () => {
1896
- it("should copy attribute to element output", () => {
1897
- const xslt = parseXML(`<?xml version="1.0"?>
1898
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1899
- <xsl:template match="@*">
1900
- <xsl:copy/>
1901
- </xsl:template>
1902
- <xsl:template match="/">
1903
- <result>
1904
- <xsl:apply-templates select="/root/@*"/>
1905
- </result>
1906
- </xsl:template>
1907
- </xsl:stylesheet>
1908
- `);
1909
-
1910
- const xml = parseXML('<root id="123" class="test"/>');
1911
-
1912
- engine.importStylesheet(xslt);
1913
- const result = engine.transform(xml, document);
1914
-
1915
- const resultEl = result.querySelector("result");
1916
- assert.ok(resultEl);
1917
- });
1918
-
1919
- it("should copy attribute directly to output element", () => {
1920
- const xslt = parseXML(`<?xml version="1.0"?>
1921
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1922
- <xsl:template match="@id">
1923
- <container id="{.}"/>
1924
- </xsl:template>
1925
- <xsl:template match="/">
1926
- <xsl:apply-templates select="/root/@id"/>
1927
- </xsl:template>
1928
- </xsl:stylesheet>
1929
- `);
1930
-
1931
- const xml = parseXML('<root id="test-id"/>');
1932
-
1933
- engine.importStylesheet(xslt);
1934
- const result = engine.transform(xml, document);
1935
-
1936
- // Test that attribute template works
1937
- assert.ok(result.childNodes.length > 0);
1938
- });
1939
- });
1940
-
1941
- describe("literal result element with namespace", () => {
1942
- it("should create namespaced element", () => {
1943
- const xslt = parseXML(`<?xml version="1.0"?>
1944
- <xsl:stylesheet version="1.0"
1945
- xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
1946
- xmlns:svg="http://www.w3.org/2000/svg">
1947
- <xsl:template match="/">
1948
- <svg:svg width="100" height="100">
1949
- <svg:circle cx="50" cy="50" r="40"/>
1950
- </svg:svg>
1951
- </xsl:template>
1952
- </xsl:stylesheet>
1953
- `);
1954
-
1955
- const xml = parseXML("<root/>");
1956
-
1957
- engine.importStylesheet(xslt);
1958
- const result = engine.transform(xml, document);
1959
-
1960
- assert.ok(result.childNodes.length > 0);
1961
- });
1962
- });
1963
-
1964
- describe("xsl:copy with document node", () => {
1965
- it("should copy document/fragment by processing children", () => {
1966
- const xslt = parseXML(`<?xml version="1.0"?>
1967
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1968
- <xsl:template match="/">
1969
- <xsl:copy>
1970
- <wrapper>content</wrapper>
1971
- </xsl:copy>
1972
- </xsl:template>
1973
- </xsl:stylesheet>
1974
- `);
1975
-
1976
- const xml = parseXML("<root>content</root>");
1977
-
1978
- engine.importStylesheet(xslt);
1979
- const result = engine.transform(xml, document);
1980
-
1981
- assert.ok(result.querySelector("wrapper"));
1982
- });
1983
- });
1984
-
1985
- describe("xslApplyTemplates with non-array result", () => {
1986
- it("should handle string result from select", () => {
1987
- const xslt = parseXML(`<?xml version="1.0"?>
1988
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
1989
- <xsl:template match="/">
1990
- <result>
1991
- <xsl:apply-templates select="string(/root/item)"/>
1992
- </result>
1993
- </xsl:template>
1994
- </xsl:stylesheet>
1995
- `);
1996
-
1997
- const xml = parseXML("<root><item>text</item></root>");
1998
-
1999
- engine.importStylesheet(xslt);
2000
- const result = engine.transform(xml, document);
2001
-
2002
- assert.ok(result.querySelector("result"));
2003
- });
2004
- });
2005
-
2006
- describe("xslForEach with non-array result", () => {
2007
- it("should handle string result from select", () => {
2008
- const xslt = parseXML(`<?xml version="1.0"?>
2009
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2010
- <xsl:template match="/">
2011
- <result>
2012
- <xsl:for-each select="string(/root/item)">
2013
- <item><xsl:value-of select="."/></item>
2014
- </xsl:for-each>
2015
- </result>
2016
- </xsl:template>
2017
- </xsl:stylesheet>
2018
- `);
2019
-
2020
- const xml = parseXML("<root><item>text</item></root>");
2021
-
2022
- engine.importStylesheet(xslt);
2023
- const result = engine.transform(xml, document);
2024
-
2025
- assert.ok(result.querySelector("result"));
2026
- });
2027
- });
2028
-
2029
- describe("xsl:fallback element", () => {
2030
- it("should ignore xsl:fallback in supported elements", () => {
2031
- const xslt = parseXML(`<?xml version="1.0"?>
2032
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2033
- <xsl:template match="/">
2034
- <result>
2035
- <xsl:value-of select="/root/item"/>
2036
- <xsl:fallback>Fallback content</xsl:fallback>
2037
- </result>
2038
- </xsl:template>
2039
- </xsl:stylesheet>
2040
- `);
2041
-
2042
- const xml = parseXML("<root><item>value</item></root>");
2043
-
2044
- engine.importStylesheet(xslt);
2045
- const result = engine.transform(xml, document);
2046
-
2047
- assert.ok(result.querySelector("result"));
2048
- });
2049
- });
2050
-
2051
- describe("xsl:with-param standalone", () => {
2052
- it("should ignore xsl:with-param outside call-template/apply-templates", () => {
2053
- const xslt = parseXML(`<?xml version="1.0"?>
2054
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2055
- <xsl:template match="/">
2056
- <result>
2057
- <xsl:with-param name="ignored">value</xsl:with-param>
2058
- <done/>
2059
- </result>
2060
- </xsl:template>
2061
- </xsl:stylesheet>
2062
- `);
2063
-
2064
- const xml = parseXML("<root/>");
2065
-
2066
- engine.importStylesheet(xslt);
2067
- const result = engine.transform(xml, document);
2068
-
2069
- assert.ok(result.querySelector("done"));
2070
- });
2071
- });
2072
-
2073
- describe("xsl:copy direct method call", () => {
2074
- it("should copy attribute node directly", () => {
2075
- const xslt = parseXML(`<?xml version="1.0"?>
2076
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2077
- <xsl:template match="/"><out/></xsl:template>
2078
- </xsl:stylesheet>
2079
- `);
2080
-
2081
- engine.importStylesheet(xslt);
2082
-
2083
- // Create attribute node context
2084
- const sourceDoc = parseXML('<root id="test-value"/>');
2085
- const attrNode = sourceDoc.documentElement.getAttributeNode("id");
2086
-
2087
- const context = new XsltContext({
2088
- currentNode: attrNode,
2089
- currentNodeList: [attrNode],
2090
- position: 1,
2091
- outputDocument: document,
2092
- variables: {},
2093
- parameters: {},
2094
- namespaces: {},
2095
- });
2096
-
2097
- // Create output element
2098
- const outputElement = document.createElement("target");
2099
-
2100
- // Create xsl:copy node
2101
- const copyNode = parseXML(
2102
- '<xsl:copy xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/>',
2103
- ).documentElement;
2104
-
2105
- // Call xslCopy directly
2106
- engine.xslCopy(copyNode, context, outputElement);
2107
-
2108
- assert.strictEqual(outputElement.getAttribute("id"), "test-value");
2109
- });
2110
-
2111
- it("should copy document node directly", () => {
2112
- const xslt = parseXML(`<?xml version="1.0"?>
2113
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2114
- <xsl:template match="/"><out/></xsl:template>
2115
- </xsl:stylesheet>
2116
- `);
2117
-
2118
- engine.importStylesheet(xslt);
2119
-
2120
- // Create document context
2121
- const sourceDoc = parseXML("<root/>");
2122
-
2123
- const context = new XsltContext({
2124
- currentNode: sourceDoc,
2125
- currentNodeList: [sourceDoc],
2126
- position: 1,
2127
- outputDocument: document,
2128
- variables: {},
2129
- parameters: {},
2130
- namespaces: {},
2131
- });
2132
-
2133
- // Create output fragment
2134
- const outputFragment = document.createDocumentFragment();
2135
-
2136
- // Create xsl:copy node with children
2137
- const copyNode = parseXML(
2138
- '<xsl:copy xmlns:xsl="http://www.w3.org/1999/XSL/Transform"><child>content</child></xsl:copy>',
2139
- ).documentElement;
2140
-
2141
- // Call xslCopy directly
2142
- engine.xslCopy(copyNode, context, outputFragment);
2143
-
2144
- assert.ok(outputFragment.childNodes.length > 0);
2145
- });
2146
- });
2147
-
2148
- describe("W3C XSLT 1.0 Specification Compliance", () => {
2149
- describe("Section 5: Template Rules", () => {
2150
- it("should match templates by priority (5.5)", () => {
2151
- const xslt = parseXML(`<?xml version="1.0"?>
2152
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2153
- <xsl:template match="item" priority="1">
2154
- <high><xsl:value-of select="."/></high>
2155
- </xsl:template>
2156
- <xsl:template match="item" priority="0">
2157
- <low><xsl:value-of select="."/></low>
2158
- </xsl:template>
2159
- </xsl:stylesheet>
2160
- `);
2161
-
2162
- const xml = parseXML("<root><item>test</item></root>");
2163
-
2164
- engine.importStylesheet(xslt);
2165
- const result = engine.transform(xml, document);
2166
-
2167
- assert.ok(result.querySelector("high"));
2168
- assert.ok(!result.querySelector("low"));
2169
- });
2170
- });
2171
-
2172
- describe("Section 7: Repetition", () => {
2173
- it("should process xsl:for-each with position() (7.7)", () => {
2174
- const xslt = parseXML(`<?xml version="1.0"?>
2175
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2176
- <xsl:template match="/">
2177
- <ul>
2178
- <xsl:for-each select="/root/item">
2179
- <li><xsl:value-of select="position()"/>: <xsl:value-of select="."/></li>
2180
- </xsl:for-each>
2181
- </ul>
2182
- </xsl:template>
2183
- </xsl:stylesheet>
2184
- `);
2185
-
2186
- const xml = parseXML("<root><item>A</item><item>B</item></root>");
2187
-
2188
- engine.importStylesheet(xslt);
2189
- const result = engine.transform(xml, document);
2190
-
2191
- const items = result.querySelectorAll("li");
2192
- assert.ok(items[0].textContent.includes("1"));
2193
- assert.ok(items[1].textContent.includes("2"));
2194
- });
2195
- });
2196
-
2197
- describe("Section 9: Conditional Processing", () => {
2198
- it("should handle nested xsl:choose (9.2)", () => {
2199
- const xslt = parseXML(`<?xml version="1.0"?>
2200
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2201
- <xsl:template match="/">
2202
- <xsl:choose>
2203
- <xsl:when test="/root/level = 'high'">
2204
- <xsl:choose>
2205
- <xsl:when test="/root/type = 'A'"><result>high-A</result></xsl:when>
2206
- <xsl:otherwise><result>high-other</result></xsl:otherwise>
2207
- </xsl:choose>
2208
- </xsl:when>
2209
- <xsl:otherwise><result>low</result></xsl:otherwise>
2210
- </xsl:choose>
2211
- </xsl:template>
2212
- </xsl:stylesheet>
2213
- `);
2214
-
2215
- const xml = parseXML("<root><level>high</level><type>A</type></root>");
2216
-
2217
- engine.importStylesheet(xslt);
2218
- const result = engine.transform(xml, document);
2219
-
2220
- assert.strictEqual(
2221
- result.querySelector("result").textContent,
2222
- "high-A",
2223
- );
2224
- });
2225
- });
2226
-
2227
- describe("Section 10: Sorting", () => {
2228
- it("should support multiple sort keys (10)", () => {
2229
- const xslt = parseXML(`<?xml version="1.0"?>
2230
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2231
- <xsl:template match="/">
2232
- <ul>
2233
- <xsl:for-each select="/root/item">
2234
- <xsl:sort select="@group"/>
2235
- <xsl:sort select="@name"/>
2236
- <li><xsl:value-of select="@group"/>-<xsl:value-of select="@name"/></li>
2237
- </xsl:for-each>
2238
- </ul>
2239
- </xsl:template>
2240
- </xsl:stylesheet>
2241
- `);
2242
-
2243
- const xml = parseXML(
2244
- '<root><item group="B" name="Z"/><item group="A" name="Y"/><item group="A" name="X"/></root>',
2245
- );
2246
-
2247
- engine.importStylesheet(xslt);
2248
- const result = engine.transform(xml, document);
2249
-
2250
- const items = result.querySelectorAll("li");
2251
- assert.strictEqual(items[0].textContent, "A-X");
2252
- assert.strictEqual(items[1].textContent, "A-Y");
2253
- assert.strictEqual(items[2].textContent, "B-Z");
2254
- });
2255
- });
2256
-
2257
- describe("Section 11: Variables and Parameters", () => {
2258
- it("should support variable scoping (11.2)", () => {
2259
- const xslt = parseXML(`<?xml version="1.0"?>
2260
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2261
- <xsl:variable name="global" select="'outer'"/>
2262
- <xsl:template match="/">
2263
- <xsl:variable name="local" select="'inner'"/>
2264
- <result>
2265
- <global><xsl:value-of select="$global"/></global>
2266
- <local><xsl:value-of select="$local"/></local>
2267
- </result>
2268
- </xsl:template>
2269
- </xsl:stylesheet>
2270
- `);
2271
-
2272
- const xml = parseXML("<root/>");
2273
-
2274
- engine.importStylesheet(xslt);
2275
- const result = engine.transform(xml, document);
2276
-
2277
- assert.strictEqual(result.querySelector("global").textContent, "outer");
2278
- assert.strictEqual(result.querySelector("local").textContent, "inner");
2279
- });
2280
- });
2281
-
2282
- describe("Section 12.4: Number Formatting", () => {
2283
- it("should format numbers correctly", () => {
2284
- const xslt = parseXML(`<?xml version="1.0"?>
2285
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2286
- <xsl:template match="/">
2287
- <result>
2288
- <decimal><xsl:number value="7" format="01"/></decimal>
2289
- <alpha><xsl:number value="3" format="a"/></alpha>
2290
- <roman><xsl:number value="14" format="i"/></roman>
2291
- </result>
2292
- </xsl:template>
2293
- </xsl:stylesheet>
2294
- `);
2295
-
2296
- const xml = parseXML("<root/>");
2297
-
2298
- engine.importStylesheet(xslt);
2299
- const result = engine.transform(xml, document);
2300
-
2301
- assert.strictEqual(result.querySelector("decimal").textContent, "07");
2302
- assert.strictEqual(result.querySelector("alpha").textContent, "c");
2303
- assert.strictEqual(result.querySelector("roman").textContent, "xiv");
2304
- });
2305
- });
2306
-
2307
- describe("Section 16: Output", () => {
2308
- it("should respect xsl:output settings", () => {
2309
- const xslt = parseXML(`<?xml version="1.0"?>
2310
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2311
- <xsl:output method="html" indent="yes" encoding="UTF-8"
2312
- doctype-public="-//W3C//DTD HTML 4.01//EN"
2313
- doctype-system="http://www.w3.org/TR/html4/strict.dtd"
2314
- media-type="text/html"/>
2315
- <xsl:template match="/"><html><body>test</body></html></xsl:template>
2316
- </xsl:stylesheet>
2317
- `);
2318
-
2319
- engine.importStylesheet(xslt);
2320
-
2321
- assert.strictEqual(engine.outputSettings.method, "html");
2322
- assert.strictEqual(engine.outputSettings.indent, "yes");
2323
- assert.strictEqual(engine.outputSettings.encoding, "UTF-8");
2324
- assert.strictEqual(
2325
- engine.outputSettings.doctypePublic,
2326
- "-//W3C//DTD HTML 4.01//EN",
2327
- );
2328
- assert.strictEqual(
2329
- engine.outputSettings.doctypeSystem,
2330
- "http://www.w3.org/TR/html4/strict.dtd",
2331
- );
2332
- assert.strictEqual(engine.outputSettings.mediaType, "text/html");
2333
- });
2334
- });
2335
- });
2336
-
2337
- describe("W3C XPath 1.0 Specification Compliance", () => {
2338
- describe("Section 2: Location Paths", () => {
2339
- it("should support abbreviated syntax (2.5)", () => {
2340
- const xslt = parseXML(`<?xml version="1.0"?>
2341
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2342
- <xsl:template match="/">
2343
- <result>
2344
- <child><xsl:value-of select="/root/item"/></child>
2345
- <attr><xsl:value-of select="/root/item/@id"/></attr>
2346
- <descendant><xsl:value-of select="//nested"/></descendant>
2347
- <parent><xsl:value-of select="/root/item/.."/></parent>
2348
- </result>
2349
- </xsl:template>
2350
- </xsl:stylesheet>
2351
- `);
2352
-
2353
- const xml = parseXML(
2354
- '<root><item id="123"><nested>deep</nested></item></root>',
2355
- );
2356
-
2357
- engine.importStylesheet(xslt);
2358
- const result = engine.transform(xml, document);
2359
-
2360
- assert.ok(result.querySelector("child").textContent.includes("deep"));
2361
- assert.strictEqual(result.querySelector("attr").textContent, "123");
2362
- assert.strictEqual(
2363
- result.querySelector("descendant").textContent,
2364
- "deep",
2365
- );
2366
- });
2367
- });
2368
-
2369
- describe("Section 3: Expressions", () => {
2370
- it("should evaluate union expressions (3.3)", () => {
2371
- const xslt = parseXML(`<?xml version="1.0"?>
2372
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2373
- <xsl:template match="/">
2374
- <result>
2375
- <xsl:for-each select="/root/a | /root/b">
2376
- <item><xsl:value-of select="."/></item>
2377
- </xsl:for-each>
2378
- </result>
2379
- </xsl:template>
2380
- </xsl:stylesheet>
2381
- `);
2382
-
2383
- const xml = parseXML("<root><a>A</a><b>B</b><c>C</c></root>");
2384
-
2385
- engine.importStylesheet(xslt);
2386
- const result = engine.transform(xml, document);
2387
-
2388
- const items = result.querySelectorAll("item");
2389
- assert.strictEqual(items.length, 2);
2390
- });
2391
- });
2392
-
2393
- describe("Section 4: Core Functions", () => {
2394
- it("should implement string functions (4.2)", () => {
2395
- const xslt = parseXML(`<?xml version="1.0"?>
2396
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2397
- <xsl:template match="/">
2398
- <result>
2399
- <concat><xsl:value-of select="concat('a', 'b', 'c')"/></concat>
2400
- <starts><xsl:value-of select="starts-with('hello', 'he')"/></starts>
2401
- <contains><xsl:value-of select="contains('hello', 'ell')"/></contains>
2402
- <substr><xsl:value-of select="substring('hello', 2, 3)"/></substr>
2403
- <length><xsl:value-of select="string-length('hello')"/></length>
2404
- <translate><xsl:value-of select="translate('abc', 'abc', 'ABC')"/></translate>
2405
- </result>
2406
- </xsl:template>
2407
- </xsl:stylesheet>
2408
- `);
2409
-
2410
- const xml = parseXML("<root/>");
2411
-
2412
- engine.importStylesheet(xslt);
2413
- const result = engine.transform(xml, document);
2414
-
2415
- assert.strictEqual(result.querySelector("concat").textContent, "abc");
2416
- assert.strictEqual(result.querySelector("starts").textContent, "true");
2417
- assert.strictEqual(
2418
- result.querySelector("contains").textContent,
2419
- "true",
2420
- );
2421
- assert.strictEqual(result.querySelector("substr").textContent, "ell");
2422
- assert.strictEqual(result.querySelector("length").textContent, "5");
2423
- assert.strictEqual(
2424
- result.querySelector("translate").textContent,
2425
- "ABC",
2426
- );
2427
- });
2428
-
2429
- it("should implement number functions (4.4)", () => {
2430
- const xslt = parseXML(`<?xml version="1.0"?>
2431
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2432
- <xsl:template match="/">
2433
- <result>
2434
- <sum><xsl:value-of select="sum(/root/item)"/></sum>
2435
- <floor><xsl:value-of select="floor(3.7)"/></floor>
2436
- <ceiling><xsl:value-of select="ceiling(3.2)"/></ceiling>
2437
- <round><xsl:value-of select="round(3.5)"/></round>
2438
- </result>
2439
- </xsl:template>
2440
- </xsl:stylesheet>
2441
- `);
2442
-
2443
- const xml = parseXML(
2444
- "<root><item>10</item><item>20</item><item>30</item></root>",
2445
- );
2446
-
2447
- engine.importStylesheet(xslt);
2448
- const result = engine.transform(xml, document);
2449
-
2450
- assert.strictEqual(result.querySelector("sum").textContent, "60");
2451
- assert.strictEqual(result.querySelector("floor").textContent, "3");
2452
- assert.strictEqual(result.querySelector("ceiling").textContent, "4");
2453
- assert.strictEqual(result.querySelector("round").textContent, "4");
2454
- });
2455
- });
2456
- });
2457
-
2458
- describe("W3C DOM Level 3 Core Compliance", () => {
2459
- it("should handle namespaced elements correctly", () => {
2460
- const xslt = parseXML(`<?xml version="1.0"?>
2461
- <xsl:stylesheet version="1.0"
2462
- xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
2463
- xmlns:html="http://www.w3.org/1999/xhtml">
2464
- <xsl:template match="/">
2465
- <html:html>
2466
- <html:body>
2467
- <html:p><xsl:value-of select="/root/content"/></html:p>
2468
- </html:body>
2469
- </html:html>
2470
- </xsl:template>
2471
- </xsl:stylesheet>
2472
- `);
2473
-
2474
- const xml = parseXML("<root><content>Hello World</content></root>");
2475
-
2476
- engine.importStylesheet(xslt);
2477
- const result = engine.transform(xml, document);
2478
-
2479
- assert.ok(result.childNodes.length > 0);
2480
- });
2481
-
2482
- it("should preserve node types during transformation", () => {
2483
- const xslt = parseXML(`<?xml version="1.0"?>
2484
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2485
- <xsl:template match="/">
2486
- <result>
2487
- <xsl:comment>A comment</xsl:comment>
2488
- <xsl:processing-instruction name="target">data</xsl:processing-instruction>
2489
- <xsl:text>Text content</xsl:text>
2490
- </result>
2491
- </xsl:template>
2492
- </xsl:stylesheet>
2493
- `);
2494
-
2495
- const xml = parseXML("<root/>");
2496
-
2497
- engine.importStylesheet(xslt);
2498
- const result = engine.transform(xml, document);
2499
-
2500
- const resultEl = result.querySelector("result");
2501
- let hasComment = false;
2502
- let _hasPI = false;
2503
- let hasText = false;
2504
-
2505
- for (const node of resultEl.childNodes) {
2506
- if (node.nodeType === 8) {
2507
- hasComment = true;
2508
- }
2509
- if (node.nodeType === 7) {
2510
- _hasPI = true;
2511
- }
2512
- if (node.nodeType === 3 && node.textContent.includes("Text")) {
2513
- hasText = true;
2514
- }
2515
- }
2516
-
2517
- assert.ok(hasComment);
2518
- assert.ok(hasText);
2519
- });
2520
- });
2521
-
2522
- describe("xsl:include and xsl:import", () => {
2523
- it("should throw error when no stylesheet loader is configured", () => {
2524
- const xslt = parseXML(`<?xml version="1.0"?>
2525
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2526
- <xsl:include href="other.xsl"/>
2527
- <xsl:template match="/">
2528
- <result/>
2529
- </xsl:template>
2530
- </xsl:stylesheet>
2531
- `);
2532
-
2533
- assert.throws(() => {
2534
- engine.importStylesheet(xslt);
2535
- }, /no stylesheetLoader configured/);
2536
- });
2537
-
2538
- it("should throw error for xsl:include without href", () => {
2539
- const stylesheets = {};
2540
- engine.setStylesheetLoader((href) => stylesheets[href]);
2541
-
2542
- const xslt = parseXML(`<?xml version="1.0"?>
2543
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2544
- <xsl:include/>
2545
- <xsl:template match="/">
2546
- <result/>
2547
- </xsl:template>
2548
- </xsl:stylesheet>
2549
- `);
2550
-
2551
- assert.throws(() => {
2552
- engine.importStylesheet(xslt);
2553
- }, /xsl:include requires an href attribute/);
2554
- });
2555
-
2556
- it("should throw error for xsl:import without href", () => {
2557
- const stylesheets = {};
2558
- engine.setStylesheetLoader((href) => stylesheets[href]);
2559
-
2560
- const xslt = parseXML(`<?xml version="1.0"?>
2561
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2562
- <xsl:import/>
2563
- <xsl:template match="/">
2564
- <result/>
2565
- </xsl:template>
2566
- </xsl:stylesheet>
2567
- `);
2568
-
2569
- assert.throws(() => {
2570
- engine.importStylesheet(xslt);
2571
- }, /xsl:import requires an href attribute/);
2572
- });
2573
-
2574
- it("should include templates from external stylesheet", () => {
2575
- const includedXslt = parseXML(`<?xml version="1.0"?>
2576
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2577
- <xsl:template name="greeting">
2578
- <hello>World</hello>
2579
- </xsl:template>
2580
- </xsl:stylesheet>
2581
- `);
2582
-
2583
- engine.setStylesheetLoader((href) => {
2584
- if (href === "greeting.xsl") return includedXslt;
2585
- throw new Error(`Unknown stylesheet: ${href}`);
2586
- });
2587
-
2588
- const xslt = parseXML(`<?xml version="1.0"?>
2589
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2590
- <xsl:include href="greeting.xsl"/>
2591
- <xsl:template match="/">
2592
- <result>
2593
- <xsl:call-template name="greeting"/>
2594
- </result>
2595
- </xsl:template>
2596
- </xsl:stylesheet>
2597
- `);
2598
-
2599
- const xml = parseXML("<root/>");
2600
-
2601
- engine.importStylesheet(xslt);
2602
- const result = engine.transform(xml, document);
2603
-
2604
- const hello = result.querySelector("hello");
2605
- assert.ok(hello);
2606
- assert.strictEqual(hello.textContent, "World");
2607
- });
2608
-
2609
- it("should import templates with lower precedence", () => {
2610
- const importedXslt = parseXML(`<?xml version="1.0"?>
2611
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2612
- <xsl:template match="item">
2613
- <imported-item><xsl:value-of select="."/></imported-item>
2614
- </xsl:template>
2615
- </xsl:stylesheet>
2616
- `);
2617
-
2618
- engine.setStylesheetLoader((href) => {
2619
- if (href === "base.xsl") return importedXslt;
2620
- throw new Error(`Unknown stylesheet: ${href}`);
2621
- });
2622
-
2623
- // Main stylesheet overrides the imported template
2624
- const xslt = parseXML(`<?xml version="1.0"?>
2625
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2626
- <xsl:import href="base.xsl"/>
2627
- <xsl:template match="item">
2628
- <main-item><xsl:value-of select="."/></main-item>
2629
- </xsl:template>
2630
- <xsl:template match="/">
2631
- <result>
2632
- <xsl:apply-templates select="//item"/>
2633
- </result>
2634
- </xsl:template>
2635
- </xsl:stylesheet>
2636
- `);
2637
-
2638
- const xml = parseXML("<root><item>Test</item></root>");
2639
-
2640
- engine.importStylesheet(xslt);
2641
- const result = engine.transform(xml, document);
2642
-
2643
- // Main template should win due to higher import precedence
2644
- const mainItem = result.querySelector("main-item");
2645
- assert.ok(mainItem, "Main template should override imported template");
2646
- assert.strictEqual(mainItem.textContent, "Test");
2647
-
2648
- const importedItem = result.querySelector("imported-item");
2649
- assert.strictEqual(
2650
- importedItem,
2651
- null,
2652
- "Imported template should not be used",
2653
- );
2654
- });
2655
-
2656
- it("should use imported template when main stylesheet has no matching template", () => {
2657
- const importedXslt = parseXML(`<?xml version="1.0"?>
2658
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2659
- <xsl:template match="special">
2660
- <special-item><xsl:value-of select="."/></special-item>
2661
- </xsl:template>
2662
- </xsl:stylesheet>
2663
- `);
2664
-
2665
- engine.setStylesheetLoader((href) => {
2666
- if (href === "base.xsl") return importedXslt;
2667
- throw new Error(`Unknown stylesheet: ${href}`);
2668
- });
2669
-
2670
- const xslt = parseXML(`<?xml version="1.0"?>
2671
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2672
- <xsl:import href="base.xsl"/>
2673
- <xsl:template match="/">
2674
- <result>
2675
- <xsl:apply-templates select="//special"/>
2676
- </result>
2677
- </xsl:template>
2678
- </xsl:stylesheet>
2679
- `);
2680
-
2681
- const xml = parseXML("<root><special>Content</special></root>");
2682
-
2683
- engine.importStylesheet(xslt);
2684
- const result = engine.transform(xml, document);
2685
-
2686
- const specialItem = result.querySelector("special-item");
2687
- assert.ok(
2688
- specialItem,
2689
- "Imported template should be used when no override exists",
2690
- );
2691
- assert.strictEqual(specialItem.textContent, "Content");
2692
- });
2693
-
2694
- it("should detect circular includes", () => {
2695
- engine.setStylesheetLoader((href) => {
2696
- // Both stylesheets include each other
2697
- if (href === "a.xsl") {
2698
- return parseXML(`<?xml version="1.0"?>
2699
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2700
- <xsl:include href="b.xsl"/>
2701
- </xsl:stylesheet>
2702
- `);
2703
- }
2704
- if (href === "b.xsl") {
2705
- return parseXML(`<?xml version="1.0"?>
2706
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2707
- <xsl:include href="a.xsl"/>
2708
- </xsl:stylesheet>
2709
- `);
2710
- }
2711
- throw new Error(`Unknown stylesheet: ${href}`);
2712
- });
2713
-
2714
- const xslt = parseXML(`<?xml version="1.0"?>
2715
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2716
- <xsl:include href="a.xsl"/>
2717
- <xsl:template match="/">
2718
- <result/>
2719
- </xsl:template>
2720
- </xsl:stylesheet>
2721
- `);
2722
-
2723
- assert.throws(() => {
2724
- engine.importStylesheet(xslt);
2725
- }, /Circular stylesheet reference/);
2726
- });
2727
-
2728
- it("should resolve relative URIs correctly", () => {
2729
- engine.setStylesheetLoader((href) => {
2730
- if (href === "/styles/common/utils.xsl") {
2731
- return parseXML(`<?xml version="1.0"?>
2732
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2733
- <xsl:template name="util">
2734
- <util>OK</util>
2735
- </xsl:template>
2736
- </xsl:stylesheet>
2737
- `);
2738
- }
2739
- throw new Error(`Unknown stylesheet: ${href}`);
2740
- });
2741
-
2742
- const xslt = parseXML(`<?xml version="1.0"?>
2743
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2744
- <xsl:include href="common/utils.xsl"/>
2745
- <xsl:template match="/">
2746
- <result>
2747
- <xsl:call-template name="util"/>
2748
- </result>
2749
- </xsl:template>
2750
- </xsl:stylesheet>
2751
- `);
2752
-
2753
- const xml = parseXML("<root/>");
2754
-
2755
- engine.importStylesheet(xslt, "/styles/main.xsl");
2756
- const result = engine.transform(xml, document);
2757
-
2758
- const util = result.querySelector("util");
2759
- assert.ok(util);
2760
- assert.strictEqual(util.textContent, "OK");
2761
- });
2762
-
2763
- it("should support stylesheet loader returning XML string", () => {
2764
- engine.setStylesheetLoader((href) => {
2765
- if (href === "string.xsl") {
2766
- return `<?xml version="1.0"?>
2767
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2768
- <xsl:template name="fromString">
2769
- <string-template>Success</string-template>
2770
- </xsl:template>
2771
- </xsl:stylesheet>
2772
- `;
2773
- }
2774
- throw new Error(`Unknown stylesheet: ${href}`);
2775
- });
2776
-
2777
- const xslt = parseXML(`<?xml version="1.0"?>
2778
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2779
- <xsl:include href="string.xsl"/>
2780
- <xsl:template match="/">
2781
- <result>
2782
- <xsl:call-template name="fromString"/>
2783
- </result>
2784
- </xsl:template>
2785
- </xsl:stylesheet>
2786
- `);
2787
-
2788
- const xml = parseXML("<root/>");
2789
-
2790
- engine.importStylesheet(xslt);
2791
- const result = engine.transform(xml, document);
2792
-
2793
- const stringTemplate = result.querySelector("string-template");
2794
- assert.ok(stringTemplate);
2795
- assert.strictEqual(stringTemplate.textContent, "Success");
2796
- });
2797
-
2798
- it("should merge global variables from imported stylesheets", () => {
2799
- const importedXslt = parseXML(`<?xml version="1.0"?>
2800
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2801
- <xsl:variable name="importedVar" select="'imported-value'"/>
2802
- </xsl:stylesheet>
2803
- `);
2804
-
2805
- engine.setStylesheetLoader((href) => {
2806
- if (href === "vars.xsl") return importedXslt;
2807
- throw new Error(`Unknown stylesheet: ${href}`);
2808
- });
2809
-
2810
- const xslt = parseXML(`<?xml version="1.0"?>
2811
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2812
- <xsl:import href="vars.xsl"/>
2813
- <xsl:template match="/">
2814
- <result>
2815
- <xsl:value-of select="$importedVar"/>
2816
- </result>
2817
- </xsl:template>
2818
- </xsl:stylesheet>
2819
- `);
2820
-
2821
- const xml = parseXML("<root/>");
2822
-
2823
- engine.importStylesheet(xslt);
2824
- const result = engine.transform(xml, document);
2825
-
2826
- assert.strictEqual(
2827
- result.querySelector("result").textContent,
2828
- "imported-value",
2829
- );
2830
- });
2831
-
2832
- it("should handle nested imports with correct precedence", () => {
2833
- // base.xsl is imported by middle.xsl, which is imported by main.xsl
2834
- // Precedence: base < middle < main
2835
- engine.setStylesheetLoader((href) => {
2836
- if (href === "base.xsl") {
2837
- return parseXML(`<?xml version="1.0"?>
2838
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2839
- <xsl:template match="item">
2840
- <base-item><xsl:value-of select="."/></base-item>
2841
- </xsl:template>
2842
- </xsl:stylesheet>
2843
- `);
2844
- }
2845
- if (href === "middle.xsl") {
2846
- return parseXML(`<?xml version="1.0"?>
2847
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2848
- <xsl:import href="base.xsl"/>
2849
- <xsl:template match="item">
2850
- <middle-item><xsl:value-of select="."/></middle-item>
2851
- </xsl:template>
2852
- </xsl:stylesheet>
2853
- `);
2854
- }
2855
- throw new Error(`Unknown stylesheet: ${href}`);
2856
- });
2857
-
2858
- const xslt = parseXML(`<?xml version="1.0"?>
2859
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2860
- <xsl:import href="middle.xsl"/>
2861
- <xsl:template match="/">
2862
- <result>
2863
- <xsl:apply-templates select="//item"/>
2864
- </result>
2865
- </xsl:template>
2866
- </xsl:stylesheet>
2867
- `);
2868
-
2869
- const xml = parseXML("<root><item>Test</item></root>");
2870
-
2871
- engine.importStylesheet(xslt);
2872
- const result = engine.transform(xml, document);
2873
-
2874
- // middle.xsl template should win over base.xsl
2875
- const middleItem = result.querySelector("middle-item");
2876
- assert.ok(middleItem, "Middle template should override base template");
2877
- assert.strictEqual(middleItem.textContent, "Test");
2878
- });
2879
-
2880
- it("should include templates at same precedence level", () => {
2881
- const includedXslt = parseXML(`<?xml version="1.0"?>
2882
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2883
- <xsl:template match="item" priority="1">
2884
- <included-item><xsl:value-of select="."/></included-item>
2885
- </xsl:template>
2886
- </xsl:stylesheet>
2887
- `);
2888
-
2889
- engine.setStylesheetLoader((href) => {
2890
- if (href === "included.xsl") return includedXslt;
2891
- throw new Error(`Unknown stylesheet: ${href}`);
2892
- });
2893
-
2894
- // With include, templates are at the same precedence, so priority decides
2895
- const xslt = parseXML(`<?xml version="1.0"?>
2896
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2897
- <xsl:include href="included.xsl"/>
2898
- <xsl:template match="item" priority="0.5">
2899
- <main-item><xsl:value-of select="."/></main-item>
2900
- </xsl:template>
2901
- <xsl:template match="/">
2902
- <result>
2903
- <xsl:apply-templates select="//item"/>
2904
- </result>
2905
- </xsl:template>
2906
- </xsl:stylesheet>
2907
- `);
2908
-
2909
- const xml = parseXML("<root><item>Test</item></root>");
2910
-
2911
- engine.importStylesheet(xslt);
2912
- const result = engine.transform(xml, document);
2913
-
2914
- // Included template has higher priority (1 > 0.5), so it should win
2915
- const includedItem = result.querySelector("included-item");
2916
- assert.ok(
2917
- includedItem,
2918
- "Included template with higher priority should be used",
2919
- );
2920
- assert.strictEqual(includedItem.textContent, "Test");
2921
- });
2922
-
2923
- it("should detect circular imports", () => {
2924
- engine.setStylesheetLoader((href) => {
2925
- if (href === "circular.xsl") {
2926
- return parseXML(`<?xml version="1.0"?>
2927
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2928
- <xsl:import href="circular.xsl"/>
2929
- <xsl:template match="/"><out/></xsl:template>
2930
- </xsl:stylesheet>
2931
- `);
2932
- }
2933
- throw new Error(`Unknown stylesheet: ${href}`);
2934
- });
2935
-
2936
- const xslt = parseXML(`<?xml version="1.0"?>
2937
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2938
- <xsl:import href="circular.xsl"/>
2939
- <xsl:template match="/"><out/></xsl:template>
2940
- </xsl:stylesheet>
2941
- `);
2942
-
2943
- assert.throws(() => {
2944
- engine.importStylesheet(xslt);
2945
- }, /Circular stylesheet reference/);
2946
- });
2947
-
2948
- it("should import stylesheet returned as XML string", () => {
2949
- engine.setStylesheetLoader((href) => {
2950
- if (href === "string-import.xsl") {
2951
- return `<?xml version="1.0"?>
2952
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2953
- <xsl:template match="item">
2954
- <imported><xsl:value-of select="."/></imported>
2955
- </xsl:template>
2956
- </xsl:stylesheet>
2957
- `;
2958
- }
2959
- throw new Error(`Unknown stylesheet: ${href}`);
2960
- });
2961
-
2962
- const xslt = parseXML(`<?xml version="1.0"?>
2963
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2964
- <xsl:import href="string-import.xsl"/>
2965
- <xsl:template match="/">
2966
- <result><xsl:apply-templates select="//item"/></result>
2967
- </xsl:template>
2968
- </xsl:stylesheet>
2969
- `);
2970
-
2971
- const xml = parseXML("<root><item>Test</item></root>");
2972
-
2973
- engine.importStylesheet(xslt);
2974
- const result = engine.transform(xml, document);
2975
-
2976
- assert.ok(result.querySelector("imported"));
2977
- });
2978
-
2979
- it("should throw error when import fails", () => {
2980
- engine.setStylesheetLoader(() => {
2981
- throw new Error("Network error");
2982
- });
2983
-
2984
- const xslt = parseXML(`<?xml version="1.0"?>
2985
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
2986
- <xsl:import href="failing.xsl"/>
2987
- <xsl:template match="/"><out/></xsl:template>
2988
- </xsl:stylesheet>
2989
- `);
2990
-
2991
- assert.throws(() => {
2992
- engine.importStylesheet(xslt);
2993
- }, /Failed to import stylesheet/);
2994
- });
2995
-
2996
- it("should throw error for invalid included stylesheet", () => {
2997
- engine.setStylesheetLoader((href) => {
2998
- if (href === "invalid.xsl") {
2999
- return parseXML(`<?xml version="1.0"?><not-a-stylesheet/>`);
3000
- }
3001
- throw new Error(`Unknown stylesheet: ${href}`);
3002
- });
3003
-
3004
- const xslt = parseXML(`<?xml version="1.0"?>
3005
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3006
- <xsl:import href="invalid.xsl"/>
3007
- <xsl:template match="/"><out/></xsl:template>
3008
- </xsl:stylesheet>
3009
- `);
3010
-
3011
- assert.throws(() => {
3012
- engine.importStylesheet(xslt);
3013
- }, /not a valid XSLT stylesheet/);
3014
- });
3015
-
3016
- it("should process various elements in imported stylesheet", () => {
3017
- engine.setStylesheetLoader((href) => {
3018
- if (href === "full-features.xsl") {
3019
- return parseXML(`<?xml version="1.0"?>
3020
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3021
- <xsl:output method="html" indent="yes"/>
3022
- <xsl:param name="importedParam" select="'param-value'"/>
3023
- <xsl:key name="importedKey" match="item" use="@id"/>
3024
- <xsl:decimal-format name="importedFormat" decimal-separator=","/>
3025
- <xsl:namespace-alias stylesheet-prefix="ns1" result-prefix="ns2"/>
3026
- <xsl:attribute-set name="importedAttrs">
3027
- <xsl:attribute name="class">imported</xsl:attribute>
3028
- </xsl:attribute-set>
3029
- <xsl:strip-space elements="pre"/>
3030
- <xsl:preserve-space elements="code"/>
3031
- <xsl:template match="item">
3032
- <imported-item><xsl:value-of select="."/></imported-item>
3033
- </xsl:template>
3034
- </xsl:stylesheet>
3035
- `);
3036
- }
3037
- throw new Error(`Unknown stylesheet: ${href}`);
3038
- });
3039
-
3040
- const xslt = parseXML(`<?xml version="1.0"?>
3041
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3042
- <xsl:import href="full-features.xsl"/>
3043
- <xsl:template match="/">
3044
- <result><xsl:apply-templates select="//item"/></result>
3045
- </xsl:template>
3046
- </xsl:stylesheet>
3047
- `);
3048
-
3049
- const xml = parseXML("<root><item>Test</item></root>");
3050
-
3051
- engine.importStylesheet(xslt);
3052
-
3053
- assert.ok("importedParam" in engine.globalParameters);
3054
- assert.ok("importedKey" in engine.keys);
3055
- assert.ok("importedFormat" in engine.decimalFormats);
3056
- assert.strictEqual(engine.namespaceAliases.ns1, "ns2");
3057
- assert.ok("importedAttrs" in engine.attributeSets);
3058
- assert.ok(engine.stripSpace.includes("pre"));
3059
- assert.ok(engine.preserveSpace.includes("code"));
3060
-
3061
- const result = engine.transform(xml, document);
3062
- assert.ok(result.querySelector("imported-item"));
3063
- });
3064
-
3065
- it("should process include in imported stylesheet", () => {
3066
- engine.setStylesheetLoader((href) => {
3067
- if (href === "parent.xsl") {
3068
- return parseXML(`<?xml version="1.0"?>
3069
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3070
- <xsl:include href="child.xsl"/>
3071
- </xsl:stylesheet>
3072
- `);
3073
- }
3074
- if (href === "child.xsl") {
3075
- return parseXML(`<?xml version="1.0"?>
3076
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3077
- <xsl:template match="item">
3078
- <child-item><xsl:value-of select="."/></child-item>
3079
- </xsl:template>
3080
- </xsl:stylesheet>
3081
- `);
3082
- }
3083
- throw new Error(`Unknown stylesheet: ${href}`);
3084
- });
3085
-
3086
- const xslt = parseXML(`<?xml version="1.0"?>
3087
- <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
3088
- <xsl:import href="parent.xsl"/>
3089
- <xsl:template match="/">
3090
- <result><xsl:apply-templates select="//item"/></result>
3091
- </xsl:template>
3092
- </xsl:stylesheet>
3093
- `);
3094
-
3095
- const xml = parseXML("<root><item>Test</item></root>");
3096
-
3097
- engine.importStylesheet(xslt);
3098
- const result = engine.transform(xml, document);
3099
-
3100
- assert.ok(result.querySelector("child-item"));
3101
- });
3102
- });
3103
-
3104
- describe("parseXmlString", () => {
3105
- it("should throw error for invalid XML with parsererror", () => {
3106
- assert.throws(() => {
3107
- engine.parseXmlString("<invalid><unclosed>");
3108
- }, /XML parse error/);
3109
- });
3110
-
3111
- it("should throw error when DOMParser is not available", () => {
3112
- const originalDOMParser = global.DOMParser;
3113
- global.DOMParser = undefined;
3114
-
3115
- const testEngine = new XsltEngine();
3116
-
3117
- assert.throws(() => {
3118
- testEngine.parseXmlString("<valid/>");
3119
- }, /XML parsing not available/);
3120
-
3121
- global.DOMParser = originalDOMParser;
3122
- });
3123
-
3124
- it("should parse valid XML string", () => {
3125
- const result = engine.parseXmlString("<root><item>test</item></root>");
3126
- assert.ok(result.documentElement);
3127
- assert.strictEqual(result.documentElement.tagName, "root");
3128
- });
3129
- });
3130
- });