@tradik/xslt-processor 1.0.2 → 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 +292 -47
  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 +2073 -163
  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 +2077 -162
  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 +2072 -161
  15. package/dist/xslt-processor.js.map +4 -4
  16. package/package.json +27 -16
  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 +474 -185
  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,1852 +0,0 @@
1
- /**
2
- * XPath Evaluator Tests
3
- */
4
-
5
- import { describe, it, beforeEach } from "node:test";
6
- import assert from "node:assert";
7
- import { JSDOM } from "jsdom";
8
- import {
9
- evaluate,
10
- select,
11
- selectFirst,
12
- XPathEvaluator,
13
- XPathContext,
14
- parse,
15
- XPathLimits,
16
- } from "./index.js";
17
- import { Token, TokenType, tokenize } from "./tokenizer.js";
18
-
19
- // Create a DOM environment for testing
20
- function createDOM(html) {
21
- const dom = new JSDOM(html, { contentType: "application/xml" });
22
- return dom.window.document;
23
- }
24
-
25
- describe("XPath Evaluator", () => {
26
- let doc;
27
-
28
- beforeEach(() => {
29
- doc = createDOM(`<?xml version="1.0"?>
30
- <root>
31
- <item id="1">First</item>
32
- <item id="2">Second</item>
33
- <item id="3">Third</item>
34
- <nested>
35
- <child name="a">Alpha</child>
36
- <child name="b">Beta</child>
37
- </nested>
38
- <numbers>
39
- <num>10</num>
40
- <num>20</num>
41
- <num>30</num>
42
- </numbers>
43
- </root>
44
- `);
45
- });
46
-
47
- describe("Location paths", () => {
48
- it("should select root element", () => {
49
- const result = select("/root", doc);
50
- assert.strictEqual(result.length, 1);
51
- assert.strictEqual(result[0].nodeName, "root");
52
- });
53
-
54
- it("should select child elements", () => {
55
- const result = select("/root/item", doc);
56
- assert.strictEqual(result.length, 3);
57
- });
58
-
59
- it("should select descendants with //", () => {
60
- const result = select("//child", doc);
61
- assert.strictEqual(result.length, 2);
62
- });
63
-
64
- it("should select with wildcard", () => {
65
- const result = select("/root/*", doc);
66
- assert.strictEqual(result.length, 5); // item, item, item, nested, numbers
67
- });
68
-
69
- it("should select parent with ..", () => {
70
- const item = selectFirst('//item[@id="1"]', doc);
71
- const parent = selectFirst("..", item);
72
- assert.strictEqual(parent.nodeName, "root");
73
- });
74
-
75
- it("should select self with .", () => {
76
- const item = selectFirst('//item[@id="1"]', doc);
77
- const self = selectFirst(".", item);
78
- assert.strictEqual(self, item);
79
- });
80
- });
81
-
82
- describe("Predicates", () => {
83
- it("should filter by position", () => {
84
- const result = select("/root/item[1]", doc);
85
- assert.strictEqual(result.length, 1);
86
- assert.strictEqual(result[0].getAttribute("id"), "1");
87
- });
88
-
89
- it("should filter by last()", () => {
90
- const result = select("/root/item[last()]", doc);
91
- assert.strictEqual(result.length, 1);
92
- assert.strictEqual(result[0].getAttribute("id"), "3");
93
- });
94
-
95
- it("should filter by attribute", () => {
96
- const result = select('/root/item[@id="2"]', doc);
97
- assert.strictEqual(result.length, 1);
98
- assert.strictEqual(result[0].textContent, "Second");
99
- });
100
-
101
- it("should filter by text content", () => {
102
- const result = select('//child[text()="Alpha"]', doc);
103
- assert.strictEqual(result.length, 1);
104
- });
105
- });
106
-
107
- describe("Attributes", () => {
108
- it("should select attribute", () => {
109
- const result = select("/root/item/@id", doc);
110
- assert.strictEqual(result.length, 3);
111
- });
112
-
113
- it("should select specific attribute value", () => {
114
- const result = evaluate("/root/item[1]/@id", doc);
115
- assert.strictEqual(result.length, 1);
116
- assert.strictEqual(result[0].value, "1");
117
- });
118
- });
119
-
120
- describe("Axes", () => {
121
- it("should use child axis (default)", () => {
122
- const result = select("/root/child::item", doc);
123
- assert.strictEqual(result.length, 3);
124
- });
125
-
126
- it("should use descendant axis", () => {
127
- const result = select("/root/descendant::child", doc);
128
- assert.strictEqual(result.length, 2);
129
- });
130
-
131
- it("should use ancestor axis", () => {
132
- const child = selectFirst('//child[@name="a"]', doc);
133
- const ancestors = select("ancestor::*", child);
134
- assert.ok(ancestors.length >= 2);
135
- });
136
-
137
- it("should use following-sibling axis", () => {
138
- const item1 = selectFirst("/root/item[1]", doc);
139
- const siblings = select("following-sibling::item", item1);
140
- assert.strictEqual(siblings.length, 2);
141
- });
142
-
143
- it("should use preceding-sibling axis", () => {
144
- const item3 = selectFirst("/root/item[3]", doc);
145
- const siblings = select("preceding-sibling::item", item3);
146
- assert.strictEqual(siblings.length, 2);
147
- });
148
- });
149
-
150
- describe("String functions", () => {
151
- it("should evaluate string()", () => {
152
- const result = evaluate("string(/root/item[1])", doc);
153
- assert.strictEqual(result, "First");
154
- });
155
-
156
- it("should evaluate concat()", () => {
157
- const result = evaluate('concat("Hello", " ", "World")', doc);
158
- assert.strictEqual(result, "Hello World");
159
- });
160
-
161
- it("should evaluate contains()", () => {
162
- const result = evaluate('contains("Hello World", "World")', doc);
163
- assert.strictEqual(result, true);
164
- });
165
-
166
- it("should evaluate starts-with()", () => {
167
- const result = evaluate('starts-with("Hello", "He")', doc);
168
- assert.strictEqual(result, true);
169
- });
170
-
171
- it("should evaluate substring()", () => {
172
- const result = evaluate('substring("Hello", 2, 3)', doc);
173
- assert.strictEqual(result, "ell");
174
- });
175
-
176
- it("should evaluate string-length()", () => {
177
- const result = evaluate('string-length("Hello")', doc);
178
- assert.strictEqual(result, 5);
179
- });
180
-
181
- it("should evaluate normalize-space()", () => {
182
- const result = evaluate('normalize-space(" hello world ")', doc);
183
- assert.strictEqual(result, "hello world");
184
- });
185
-
186
- it("should evaluate translate()", () => {
187
- const result = evaluate('translate("abc", "abc", "ABC")', doc);
188
- assert.strictEqual(result, "ABC");
189
- });
190
-
191
- it("should evaluate substring-before()", () => {
192
- const result = evaluate('substring-before("hello-world", "-")', doc);
193
- assert.strictEqual(result, "hello");
194
- });
195
-
196
- it("should evaluate substring-after()", () => {
197
- const result = evaluate('substring-after("hello-world", "-")', doc);
198
- assert.strictEqual(result, "world");
199
- });
200
- });
201
-
202
- describe("Number functions", () => {
203
- it("should evaluate number()", () => {
204
- const result = evaluate('number("42")', doc);
205
- assert.strictEqual(result, 42);
206
- });
207
-
208
- it("should evaluate sum()", () => {
209
- const result = evaluate("sum(//num)", doc);
210
- assert.strictEqual(result, 60);
211
- });
212
-
213
- it("should evaluate floor()", () => {
214
- const result = evaluate("floor(3.7)", doc);
215
- assert.strictEqual(result, 3);
216
- });
217
-
218
- it("should evaluate ceiling()", () => {
219
- const result = evaluate("ceiling(3.2)", doc);
220
- assert.strictEqual(result, 4);
221
- });
222
-
223
- it("should evaluate round()", () => {
224
- const result = evaluate("round(3.5)", doc);
225
- assert.strictEqual(result, 4);
226
- });
227
- });
228
-
229
- describe("Boolean functions", () => {
230
- it("should evaluate boolean()", () => {
231
- const result = evaluate("boolean(1)", doc);
232
- assert.strictEqual(result, true);
233
- });
234
-
235
- it("should evaluate not()", () => {
236
- const result = evaluate("not(false())", doc);
237
- assert.strictEqual(result, true);
238
- });
239
-
240
- it("should evaluate true()", () => {
241
- const result = evaluate("true()", doc);
242
- assert.strictEqual(result, true);
243
- });
244
-
245
- it("should evaluate false()", () => {
246
- const result = evaluate("false()", doc);
247
- assert.strictEqual(result, false);
248
- });
249
- });
250
-
251
- describe("Node set functions", () => {
252
- it("should evaluate count()", () => {
253
- const result = evaluate("count(/root/item)", doc);
254
- assert.strictEqual(result, 3);
255
- });
256
-
257
- it("should evaluate position()", () => {
258
- const result = select("/root/item[position()=2]", doc);
259
- assert.strictEqual(result.length, 1);
260
- assert.strictEqual(result[0].getAttribute("id"), "2");
261
- });
262
-
263
- it("should evaluate last()", () => {
264
- const result = select("/root/item[position()=last()]", doc);
265
- assert.strictEqual(result.length, 1);
266
- assert.strictEqual(result[0].getAttribute("id"), "3");
267
- });
268
-
269
- it("should evaluate local-name()", () => {
270
- const result = evaluate("local-name(/root/item[1])", doc);
271
- assert.strictEqual(result, "item");
272
- });
273
-
274
- it("should evaluate name()", () => {
275
- const result = evaluate("name(/root/item[1])", doc);
276
- assert.strictEqual(result, "item");
277
- });
278
- });
279
-
280
- describe("Operators", () => {
281
- it("should evaluate arithmetic +", () => {
282
- const result = evaluate("1 + 2", doc);
283
- assert.strictEqual(result, 3);
284
- });
285
-
286
- it("should evaluate arithmetic -", () => {
287
- const result = evaluate("5 - 3", doc);
288
- assert.strictEqual(result, 2);
289
- });
290
-
291
- it("should evaluate arithmetic *", () => {
292
- const result = evaluate("4 * 3", doc);
293
- assert.strictEqual(result, 12);
294
- });
295
-
296
- it("should evaluate div", () => {
297
- const result = evaluate("10 div 2", doc);
298
- assert.strictEqual(result, 5);
299
- });
300
-
301
- it("should evaluate mod", () => {
302
- const result = evaluate("10 mod 3", doc);
303
- assert.strictEqual(result, 1);
304
- });
305
-
306
- it("should evaluate comparison =", () => {
307
- const result = evaluate("1 = 1", doc);
308
- assert.strictEqual(result, true);
309
- });
310
-
311
- it("should evaluate comparison !=", () => {
312
- const result = evaluate("1 != 2", doc);
313
- assert.strictEqual(result, true);
314
- });
315
-
316
- it("should evaluate comparison <", () => {
317
- const result = evaluate("1 < 2", doc);
318
- assert.strictEqual(result, true);
319
- });
320
-
321
- it("should evaluate comparison >", () => {
322
- const result = evaluate("2 > 1", doc);
323
- assert.strictEqual(result, true);
324
- });
325
-
326
- it("should evaluate and", () => {
327
- const result = evaluate("true() and true()", doc);
328
- assert.strictEqual(result, true);
329
- });
330
-
331
- it("should evaluate or", () => {
332
- const result = evaluate("true() or false()", doc);
333
- assert.strictEqual(result, true);
334
- });
335
-
336
- it("should evaluate or with false left side (cover right side evaluation)", () => {
337
- const result = evaluate("false() or true()", doc);
338
- assert.strictEqual(result, true);
339
- });
340
-
341
- it("should evaluate or with both sides false", () => {
342
- const result = evaluate("false() or false()", doc);
343
- assert.strictEqual(result, false);
344
- });
345
-
346
- it("should evaluate union |", () => {
347
- const result = select("/root/item | /root/nested", doc);
348
- assert.strictEqual(result.length, 4);
349
- });
350
- });
351
-
352
- describe("Variables", () => {
353
- it("should evaluate variable reference", () => {
354
- const result = evaluate("$myVar", doc, { variables: { myVar: 42 } });
355
- assert.strictEqual(result, 42);
356
- });
357
-
358
- it("should use variable in expression", () => {
359
- const result = evaluate("$x + $y", doc, { variables: { x: 10, y: 5 } });
360
- assert.strictEqual(result, 15);
361
- });
362
-
363
- it("should throw for undefined variable", () => {
364
- assert.throws(() => {
365
- evaluate("$undefined", doc);
366
- }, /Undefined variable/);
367
- });
368
- });
369
-
370
- describe("Unary expressions", () => {
371
- it("should evaluate unary minus", () => {
372
- const result = evaluate("-5", doc);
373
- assert.strictEqual(result, -5);
374
- });
375
-
376
- it("should evaluate unary minus with expression", () => {
377
- const result = evaluate("-(3 + 2)", doc);
378
- assert.strictEqual(result, -5);
379
- });
380
- });
381
-
382
- describe("Additional axes", () => {
383
- it("should use ancestor-or-self axis", () => {
384
- const child = selectFirst('//child[@name="a"]', doc);
385
- const ancestorsOrSelf = select("ancestor-or-self::*", child);
386
- assert.ok(ancestorsOrSelf.length >= 3);
387
- // Results are in document order, so root comes first
388
- assert.strictEqual(
389
- ancestorsOrSelf[ancestorsOrSelf.length - 1].nodeName,
390
- "child",
391
- );
392
- });
393
-
394
- it("should use following axis", () => {
395
- const item1 = selectFirst("/root/item[1]", doc);
396
- const following = select("following::item", item1);
397
- assert.strictEqual(following.length, 2);
398
- });
399
-
400
- it("should use preceding axis", () => {
401
- const item3 = selectFirst("/root/item[3]", doc);
402
- const preceding = select("preceding::item", item3);
403
- assert.strictEqual(preceding.length, 2);
404
- });
405
-
406
- it("should use namespace axis (empty result)", () => {
407
- const root = selectFirst("/root", doc);
408
- const namespaces = select("namespace::*", root);
409
- assert.strictEqual(namespaces.length, 0);
410
- });
411
- });
412
-
413
- describe("Relational operators", () => {
414
- it("should evaluate <=", () => {
415
- assert.strictEqual(evaluate("1 <= 2", doc), true);
416
- assert.strictEqual(evaluate("2 <= 2", doc), true);
417
- assert.strictEqual(evaluate("3 <= 2", doc), false);
418
- });
419
-
420
- it("should evaluate >=", () => {
421
- assert.strictEqual(evaluate("2 >= 1", doc), true);
422
- assert.strictEqual(evaluate("2 >= 2", doc), true);
423
- assert.strictEqual(evaluate("1 >= 2", doc), false);
424
- });
425
- });
426
-
427
- describe("Node-set comparisons", () => {
428
- it("should compare node-set to node-set", () => {
429
- const result = evaluate("/root/item/@id = /root/item/@id", doc);
430
- assert.strictEqual(result, true);
431
- });
432
-
433
- it("should compare node-set to string", () => {
434
- const result = evaluate('/root/item[1] = "First"', doc);
435
- assert.strictEqual(result, true);
436
- });
437
-
438
- it("should compare string to node-set", () => {
439
- const result = evaluate('"First" = /root/item[1]', doc);
440
- assert.strictEqual(result, true);
441
- });
442
-
443
- it("should handle empty node-set comparison", () => {
444
- const result = evaluate('/root/nonexistent = "test"', doc);
445
- assert.strictEqual(result, false);
446
- });
447
- });
448
-
449
- describe("Boolean comparisons", () => {
450
- it("should compare booleans with =", () => {
451
- const result = evaluate("true() = true()", doc);
452
- assert.strictEqual(result, true);
453
- });
454
-
455
- it("should compare booleans with !=", () => {
456
- const result = evaluate("true() != false()", doc);
457
- assert.strictEqual(result, true);
458
- });
459
-
460
- it("should compare boolean with number", () => {
461
- const result = evaluate("true() = 1", doc);
462
- assert.strictEqual(result, true);
463
- });
464
- });
465
-
466
- describe("Additional functions", () => {
467
- it("should evaluate id() function", () => {
468
- const docWithId = createDOM(`<?xml version="1.0"?>
469
- <root>
470
- <item id="test-id">Test</item>
471
- </root>
472
- `);
473
- const result = select('id("test-id")', docWithId);
474
- assert.strictEqual(result.length, 1);
475
- assert.strictEqual(result[0].textContent, "Test");
476
- });
477
-
478
- it("should evaluate namespace-uri()", () => {
479
- const result = evaluate("namespace-uri(/root)", doc);
480
- assert.strictEqual(result, "");
481
- });
482
-
483
- it("should evaluate namespace-uri() without args", () => {
484
- const item = selectFirst("/root/item[1]", doc);
485
- const result = evaluate("namespace-uri()", item);
486
- assert.strictEqual(result, "");
487
- });
488
-
489
- it("should evaluate lang() function", () => {
490
- const docWithLang = createDOM(`<?xml version="1.0"?>
491
- <root lang="en">
492
- <item>Test</item>
493
- </root>
494
- `);
495
- const item = selectFirst("/root/item", docWithLang);
496
- const result = evaluate('lang("en")', item);
497
- assert.strictEqual(result, true);
498
- });
499
-
500
- it("should evaluate lang() with sublanguage", () => {
501
- const docWithLang = createDOM(`<?xml version="1.0"?>
502
- <root lang="en-US">
503
- <item>Test</item>
504
- </root>
505
- `);
506
- const item = selectFirst("/root/item", docWithLang);
507
- const result = evaluate('lang("en")', item);
508
- assert.strictEqual(result, true);
509
- });
510
-
511
- it("should evaluate number() without args", () => {
512
- const docWithNum = createDOM(`<?xml version="1.0"?>
513
- <root>42</root>
514
- `);
515
- const root = selectFirst("/root", docWithNum);
516
- const result = evaluate("number()", root);
517
- assert.strictEqual(result, 42);
518
- });
519
-
520
- it("should evaluate local-name() without args", () => {
521
- const item = selectFirst("/root/item[1]", doc);
522
- const result = evaluate("local-name()", item);
523
- assert.strictEqual(result, "item");
524
- });
525
-
526
- it("should evaluate name() without args", () => {
527
- const item = selectFirst("/root/item[1]", doc);
528
- const result = evaluate("name()", item);
529
- assert.strictEqual(result, "item");
530
- });
531
-
532
- it("should evaluate string-length() without args", () => {
533
- const item = selectFirst("/root/item[1]", doc);
534
- const result = evaluate("string-length()", item);
535
- assert.strictEqual(result, 5); // "First"
536
- });
537
-
538
- it("should evaluate normalize-space() without args", () => {
539
- const docWithSpaces = createDOM(`<?xml version="1.0"?>
540
- <root> hello world </root>
541
- `);
542
- const root = selectFirst("/root", docWithSpaces);
543
- const result = evaluate("normalize-space()", root);
544
- assert.strictEqual(result, "hello world");
545
- });
546
- });
547
-
548
- describe("Node type tests", () => {
549
- it("should match text nodes", () => {
550
- const result = select("/root/item[1]/text()", doc);
551
- assert.strictEqual(result.length, 1);
552
- assert.strictEqual(result[0].nodeType, 3);
553
- });
554
-
555
- it("should match comment nodes", () => {
556
- const docWithComment = createDOM(`<?xml version="1.0"?>
557
- <root><!-- comment --><item>Test</item></root>
558
- `);
559
- const result = select("/root/comment()", docWithComment);
560
- assert.strictEqual(result.length, 1);
561
- assert.strictEqual(result[0].nodeType, 8);
562
- });
563
-
564
- it("should match processing-instruction nodes", () => {
565
- const docWithPI = createDOM(`<?xml version="1.0"?>
566
- <root><?pi-target data?><item>Test</item></root>
567
- `);
568
- const result = select("/root/processing-instruction()", docWithPI);
569
- assert.strictEqual(result.length, 1);
570
- assert.strictEqual(result[0].nodeType, 7);
571
- });
572
-
573
- it("should match any node with node()", () => {
574
- const result = select("/root/node()", doc);
575
- assert.ok(result.length > 0);
576
- });
577
- });
578
-
579
- describe("Type conversions", () => {
580
- it("should convert boolean to number", () => {
581
- const result = evaluate("number(true())", doc);
582
- assert.strictEqual(result, 1);
583
- });
584
-
585
- it("should convert false to 0", () => {
586
- const result = evaluate("number(false())", doc);
587
- assert.strictEqual(result, 0);
588
- });
589
-
590
- it("should handle NaN in string conversion", () => {
591
- const result = evaluate('string(number("not a number"))', doc);
592
- assert.strictEqual(result, "NaN");
593
- });
594
-
595
- it("should handle Infinity in string conversion", () => {
596
- const result = evaluate("string(1 div 0)", doc);
597
- assert.strictEqual(result, "Infinity");
598
- });
599
-
600
- it("should convert empty string to NaN", () => {
601
- const result = evaluate('number("")', doc);
602
- assert.ok(isNaN(result));
603
- });
604
-
605
- it("should convert boolean to string", () => {
606
- const result = evaluate("string(true())", doc);
607
- assert.strictEqual(result, "true");
608
- });
609
-
610
- it("should convert empty node-set to empty string", () => {
611
- const result = evaluate("string(/root/nonexistent)", doc);
612
- assert.strictEqual(result, "");
613
- });
614
- });
615
-
616
- describe("Edge cases", () => {
617
- it("should handle substring with negative start", () => {
618
- const result = evaluate('substring("Hello", -1, 5)', doc);
619
- assert.strictEqual(result, "Hel");
620
- });
621
-
622
- it("should handle translate removing characters", () => {
623
- const result = evaluate('translate("hello", "aeiou", "")', doc);
624
- assert.strictEqual(result, "hll");
625
- });
626
-
627
- it("should handle division by zero", () => {
628
- const result = evaluate("1 div 0", doc);
629
- assert.strictEqual(result, Infinity);
630
- });
631
-
632
- it("should handle round(-0.5)", () => {
633
- const result = evaluate("round(-0.5)", doc);
634
- assert.strictEqual(Object.is(result, -0), true);
635
- });
636
-
637
- it("should handle round(NaN)", () => {
638
- const result = evaluate('round(number("NaN"))', doc);
639
- assert.ok(isNaN(result));
640
- });
641
-
642
- it("should return empty string for invalid substring params", () => {
643
- const result = evaluate('substring("Hello", number("NaN"))', doc);
644
- assert.strictEqual(result, "");
645
- });
646
-
647
- it("should handle substring with length <= 0", () => {
648
- const result = evaluate('substring("Hello", 2, 0)', doc);
649
- assert.strictEqual(result, "");
650
- });
651
- });
652
-
653
- describe("Error handling", () => {
654
- it("should throw for unknown function", () => {
655
- assert.throws(() => {
656
- evaluate("unknownFunction()", doc);
657
- }, /Unknown function/);
658
- });
659
-
660
- it("should handle sum of non-array", () => {
661
- const result = evaluate('sum("not-a-nodeset")', doc);
662
- assert.ok(isNaN(result));
663
- });
664
- });
665
-
666
- describe("Security", () => {
667
- it("should prevent prototype pollution via __proto__", () => {
668
- assert.throws(() => {
669
- evaluate("$__proto__", doc, { variables: { __proto__: "test" } });
670
- }, /Forbidden variable name/);
671
- });
672
-
673
- it("should prevent prototype pollution via constructor", () => {
674
- assert.throws(() => {
675
- evaluate("$constructor", doc, { variables: { constructor: "test" } });
676
- }, /Forbidden variable name/);
677
- });
678
-
679
- it("should prevent prototype pollution via prototype", () => {
680
- assert.throws(() => {
681
- evaluate("$prototype", doc, { variables: { prototype: "test" } });
682
- }, /Forbidden variable name/);
683
- });
684
-
685
- it("should use hasOwnProperty for variable lookup", () => {
686
- assert.throws(() => {
687
- evaluate("$toString", doc, { variables: {} });
688
- }, /Undefined variable/);
689
- });
690
-
691
- it("should reject invalid AST", () => {
692
- const evaluator = new XPathEvaluator();
693
- const context = new XPathContext(doc);
694
-
695
- assert.throws(() => {
696
- evaluator.evaluate(null, context);
697
- }, /Invalid AST/);
698
-
699
- assert.throws(() => {
700
- evaluator.evaluate({}, context);
701
- }, /Invalid AST.*missing type/);
702
- });
703
-
704
- it("should handle deeply nested expressions safely", () => {
705
- // Build a deeply nested expression
706
- let expr = "1";
707
- for (let i = 0; i < 50; i++) {
708
- expr = `(${expr} + 1)`;
709
- }
710
- // Should work within limits
711
- const result = evaluate(expr, doc);
712
- assert.strictEqual(result, 51);
713
- });
714
-
715
- it("should throw when max recursion depth exceeded", () => {
716
- const evaluator = new XPathEvaluator({ maxRecursionDepth: 5 });
717
- const context = new XPathContext(doc);
718
-
719
- // Build a deeply nested expression that exceeds the limit
720
- let expr = "1";
721
- for (let i = 0; i < 10; i++) {
722
- expr = `(${expr} + 1)`;
723
- }
724
- const ast = parse(expr);
725
-
726
- assert.throws(() => {
727
- evaluator.evaluate(ast, context);
728
- }, /Maximum recursion depth exceeded/);
729
- });
730
-
731
- it("should throw when string exceeds max length", () => {
732
- const evaluator = new XPathEvaluator({ maxStringLength: 10 });
733
- const context = new XPathContext(doc);
734
-
735
- // Create a literal AST node with a long string
736
- const ast = {
737
- type: "Literal",
738
- value: "a".repeat(20),
739
- };
740
-
741
- assert.throws(() => {
742
- evaluator.evaluate(ast, context);
743
- }, /String exceeds maximum length/);
744
- });
745
-
746
- it("should throw when result set exceeds max size", () => {
747
- // Create a large XML document
748
- const items = Array(50).fill("<item>x</item>").join("");
749
- const largeDoc = createDOM(`<?xml version="1.0"?><root>${items}</root>`);
750
-
751
- const evaluator = new XPathEvaluator({ maxResultSize: 10 });
752
- const context = new XPathContext(largeDoc);
753
- const ast = parse("//item");
754
-
755
- assert.throws(() => {
756
- evaluator.evaluate(ast, context);
757
- }, /Result set exceeds maximum size/);
758
- });
759
-
760
- it("should prevent prototype pollution via __defineGetter__", () => {
761
- assert.throws(() => {
762
- evaluate("$__defineGetter__", doc, {
763
- variables: { __defineGetter__: "test" },
764
- });
765
- }, /Forbidden variable name/);
766
- });
767
-
768
- it("should prevent prototype pollution via __defineSetter__", () => {
769
- assert.throws(() => {
770
- evaluate("$__defineSetter__", doc, {
771
- variables: { __defineSetter__: "test" },
772
- });
773
- }, /Forbidden variable name/);
774
- });
775
-
776
- it("should prevent prototype pollution via __lookupGetter__", () => {
777
- assert.throws(() => {
778
- evaluate("$__lookupGetter__", doc, {
779
- variables: { __lookupGetter__: "test" },
780
- });
781
- }, /Forbidden variable name/);
782
- });
783
-
784
- it("should prevent prototype pollution via __lookupSetter__", () => {
785
- assert.throws(() => {
786
- evaluate("$__lookupSetter__", doc, {
787
- variables: { __lookupSetter__: "test" },
788
- });
789
- }, /Forbidden variable name/);
790
- });
791
-
792
- it("should not inherit from Object.prototype for variables", () => {
793
- // Variables like 'hasOwnProperty' should not be found via prototype chain
794
- assert.throws(() => {
795
- evaluate("$hasOwnProperty", doc, { variables: {} });
796
- }, /Undefined variable/);
797
- });
798
-
799
- it("should not inherit valueOf from Object.prototype", () => {
800
- assert.throws(() => {
801
- evaluate("$valueOf", doc, { variables: {} });
802
- }, /Undefined variable/);
803
- });
804
-
805
- it("should handle prefixed forbidden variable names", () => {
806
- assert.throws(() => {
807
- evaluate("$ns:__proto__", doc, {
808
- variables: { "ns:__proto__": "test" },
809
- });
810
- }, /Forbidden variable name/);
811
- });
812
-
813
- it("should validate string in literal with max length", () => {
814
- const evaluator = new XPathEvaluator({ maxStringLength: 10 });
815
- const context = new XPathContext(doc);
816
- // Direct literal exceeding limit
817
- const ast = { type: "Literal", value: "a".repeat(15) };
818
-
819
- assert.throws(() => {
820
- evaluator.evaluate(ast, context);
821
- }, /String exceeds maximum length/);
822
- });
823
-
824
- it("should reset recursion depth after error", () => {
825
- const evaluator = new XPathEvaluator({ maxRecursionDepth: 3 });
826
- const context = new XPathContext(doc);
827
-
828
- // First call should fail
829
- let expr = "1";
830
- for (let i = 0; i < 10; i++) {
831
- expr = `(${expr} + 1)`;
832
- }
833
- const ast = parse(expr);
834
-
835
- try {
836
- evaluator.evaluate(ast, context);
837
- } catch {
838
- // Expected to fail
839
- }
840
-
841
- // Second simple call should work (recursion depth reset)
842
- const simpleAst = parse("1 + 1");
843
- const result = evaluator.evaluate(simpleAst, context);
844
- assert.strictEqual(result, 2);
845
- });
846
-
847
- it("should handle circular reference in variables safely", () => {
848
- const circular = {};
849
- circular.self = circular;
850
-
851
- // Should not cause infinite loop - just evaluate to the object
852
- const result = evaluate("$obj", doc, { variables: { obj: circular } });
853
- assert.strictEqual(result, circular);
854
- });
855
- });
856
-
857
- describe("Strict Mode Validation", () => {
858
- it("should reject AST with null type", () => {
859
- const evaluator = new XPathEvaluator();
860
- const context = new XPathContext(doc);
861
-
862
- assert.throws(() => {
863
- evaluator.evaluate({ type: null }, context);
864
- }, /Invalid AST.*missing type/);
865
- });
866
-
867
- it("should reject AST with undefined type", () => {
868
- const evaluator = new XPathEvaluator();
869
- const context = new XPathContext(doc);
870
-
871
- assert.throws(() => {
872
- evaluator.evaluate({ type: undefined }, context);
873
- }, /Invalid AST.*missing type/);
874
- });
875
-
876
- it("should reject AST that is an array", () => {
877
- const evaluator = new XPathEvaluator();
878
- const context = new XPathContext(doc);
879
-
880
- assert.throws(() => {
881
- evaluator.evaluate([], context);
882
- }, /Invalid AST.*missing type/);
883
- });
884
-
885
- it("should reject primitive AST values", () => {
886
- const evaluator = new XPathEvaluator();
887
- const context = new XPathContext(doc);
888
-
889
- assert.throws(() => {
890
- evaluator.evaluate("string", context);
891
- }, /Invalid AST/);
892
-
893
- assert.throws(() => {
894
- evaluator.evaluate(123, context);
895
- }, /Invalid AST/);
896
-
897
- assert.throws(() => {
898
- evaluator.evaluate(true, context);
899
- }, /Invalid AST/);
900
- });
901
-
902
- it("should handle AST with symbol type safely", () => {
903
- const evaluator = new XPathEvaluator();
904
- const context = new XPathContext(doc);
905
-
906
- assert.throws(() => {
907
- evaluator.evaluate({ type: Symbol("test") }, context);
908
- }, /Unknown AST node type/);
909
- });
910
-
911
- it("should handle context with valid node", () => {
912
- const evaluator = new XPathEvaluator();
913
- const ast = parse(".");
914
-
915
- // Context with valid node
916
- const context = new XPathContext(doc);
917
- const result = evaluator.evaluate(ast, context);
918
- assert.ok(result);
919
- });
920
- });
921
-
922
- describe("Input Sanitization", () => {
923
- it("should handle expressions with unicode characters", () => {
924
- const unicodeDoc = createDOM("<root><item>Здравствуй мир</item></root>");
925
- const result = select("/root/item", unicodeDoc);
926
- assert.strictEqual(result.length, 1);
927
- assert.strictEqual(result[0].textContent, "Здравствуй мир");
928
- });
929
-
930
- it("should handle element names with unicode", () => {
931
- const unicodeDoc = createDOM("<корень><элемент>Текст</элемент></корень>");
932
- const result = select("/корень/элемент", unicodeDoc);
933
- assert.strictEqual(result.length, 1);
934
- });
935
-
936
- it("should handle emoji in text content", () => {
937
- const emojiDoc = createDOM("<root><item>Hello 🌍 World</item></root>");
938
- const result = evaluate("string(/root/item)", emojiDoc);
939
- assert.strictEqual(result, "Hello 🌍 World");
940
- });
941
-
942
- it("should handle null bytes in string safely", () => {
943
- const result = evaluate('contains("test\\u0000string", "test")', doc);
944
- assert.strictEqual(result, true);
945
- });
946
-
947
- it("should handle very long element names", () => {
948
- const longName = "a".repeat(100);
949
- const longDoc = createDOM(
950
- `<root><${longName}>content</${longName}></root>`,
951
- );
952
- const result = select(`/root/${longName}`, longDoc);
953
- assert.strictEqual(result.length, 1);
954
- });
955
-
956
- it("should handle deeply nested XML safely", () => {
957
- let nested = "<item>value</item>";
958
- for (let i = 0; i < 20; i++) {
959
- nested = `<level${i}>${nested}</level${i}>`;
960
- }
961
- const deepDoc = createDOM(`<root>${nested}</root>`);
962
- const result = select("//item", deepDoc);
963
- assert.strictEqual(result.length, 1);
964
- });
965
-
966
- it("should handle XML with many attributes", () => {
967
- const attrs = Array(50)
968
- .fill(0)
969
- .map((_, i) => `attr${i}="value${i}"`)
970
- .join(" ");
971
- const manyAttrsDoc = createDOM(
972
- `<root><item ${attrs}>content</item></root>`,
973
- );
974
- const result = evaluate("count(/root/item/@*)", manyAttrsDoc);
975
- assert.strictEqual(result, 50);
976
- });
977
-
978
- it("should handle whitespace-only text nodes", () => {
979
- const wsDoc = createDOM("<root> \n\t </root>");
980
- const result = evaluate("normalize-space(/root)", wsDoc);
981
- assert.strictEqual(result, "");
982
- });
983
-
984
- it("should handle special XML characters in text", () => {
985
- // Test handling of special characters that would be escaped
986
- const specialDoc = createDOM(
987
- "<root>&lt;special&gt;content&lt;/special&gt;</root>",
988
- );
989
- const result = evaluate("string(/root)", specialDoc);
990
- assert.ok(result.includes("<special>"));
991
- });
992
- });
993
-
994
- describe("DoS Prevention", () => {
995
- it("should limit string concatenation", () => {
996
- const evaluator = new XPathEvaluator({ maxStringLength: 100 });
997
- const context = new XPathContext(doc);
998
-
999
- // Try to build a very long string
1000
- const ast = parse(
1001
- 'concat("a", "a", "a", "a", "a", "a", "a", "a", "a", "a")',
1002
- );
1003
- const result = evaluator.evaluate(ast, context);
1004
- assert.strictEqual(result.length, 10);
1005
- });
1006
-
1007
- it("should prevent exponential blowup in union", () => {
1008
- // Create document with moderate number of items
1009
- const items = Array(20).fill("<item>x</item>").join("");
1010
- const unionDoc = createDOM(`<root>${items}</root>`);
1011
-
1012
- // Multiple unions should still be bounded
1013
- const result = select("//item | //item | //item", unionDoc);
1014
- // Union should deduplicate
1015
- assert.strictEqual(result.length, 20);
1016
- });
1017
-
1018
- it("should handle pathological predicate expressions", () => {
1019
- // Predicate that could be slow if not optimized
1020
- const result = select("/root/item[position() = last()]", doc);
1021
- assert.strictEqual(result.length, 1);
1022
- });
1023
-
1024
- it("should bound ancestor axis traversal", () => {
1025
- // Deep document
1026
- let nested = "<leaf>x</leaf>";
1027
- for (let i = 0; i < 30; i++) {
1028
- nested = `<level>${nested}</level>`;
1029
- }
1030
- const deepDoc = createDOM(`<root>${nested}</root>`);
1031
-
1032
- const leaf = selectFirst("//leaf", deepDoc);
1033
- const ancestors = select("ancestor::*", leaf);
1034
- assert.ok(ancestors.length <= 32);
1035
- });
1036
-
1037
- it("should handle count on large node sets", () => {
1038
- const items = Array(100).fill("<item>x</item>").join("");
1039
- const largeDoc = createDOM(`<root>${items}</root>`);
1040
-
1041
- const result = evaluate("count(//item)", largeDoc);
1042
- assert.strictEqual(result, 100);
1043
- });
1044
-
1045
- it("should validate result size in location path", () => {
1046
- const items = Array(50).fill("<item>x</item>").join("");
1047
- const largeDoc = createDOM(`<root>${items}</root>`);
1048
-
1049
- const evaluator = new XPathEvaluator({ maxResultSize: 10 });
1050
- const context = new XPathContext(largeDoc);
1051
- const ast = parse("/root/item");
1052
-
1053
- assert.throws(() => {
1054
- evaluator.evaluate(ast, context);
1055
- }, /Result set exceeds maximum size/);
1056
- });
1057
- });
1058
-
1059
- describe("XPathLimits constants", () => {
1060
- it("should export XPathLimits with default values", () => {
1061
- assert.strictEqual(XPathLimits.MAX_RECURSION_DEPTH, 100);
1062
- assert.strictEqual(XPathLimits.MAX_RESULT_SIZE, 10000);
1063
- assert.strictEqual(XPathLimits.MAX_STRING_LENGTH, 1000000);
1064
- });
1065
-
1066
- it("should allow custom limits via constructor", () => {
1067
- const evaluator = new XPathEvaluator({
1068
- maxRecursionDepth: 50,
1069
- maxResultSize: 500,
1070
- maxStringLength: 5000,
1071
- });
1072
- assert.strictEqual(evaluator.maxRecursionDepth, 50);
1073
- assert.strictEqual(evaluator.maxResultSize, 500);
1074
- assert.strictEqual(evaluator.maxStringLength, 5000);
1075
- });
1076
-
1077
- it("should use defaults when options not provided", () => {
1078
- const evaluator = new XPathEvaluator();
1079
- assert.strictEqual(evaluator.maxRecursionDepth, 100);
1080
- assert.strictEqual(evaluator.maxResultSize, 10000);
1081
- assert.strictEqual(evaluator.maxStringLength, 1000000);
1082
- });
1083
- });
1084
-
1085
- describe("Namespaces", () => {
1086
- it("should match prefix:* namespace wildcard", () => {
1087
- const nsDoc = createDOM(`<?xml version="1.0"?>
1088
- <root xmlns:ns="http://example.com/ns">
1089
- <ns:item>First</ns:item>
1090
- <ns:item>Second</ns:item>
1091
- </root>
1092
- `);
1093
- const result = select("/root/ns:*", nsDoc, {
1094
- namespaces: { ns: "http://example.com/ns" },
1095
- });
1096
- assert.strictEqual(result.length, 2);
1097
- });
1098
-
1099
- it("should match prefixed element name with namespace", () => {
1100
- const nsDoc = createDOM(`<?xml version="1.0"?>
1101
- <root xmlns:ns="http://example.com/ns">
1102
- <ns:item>First</ns:item>
1103
- <other>Second</other>
1104
- </root>
1105
- `);
1106
- const result = select("/root/ns:item", nsDoc, {
1107
- namespaces: { ns: "http://example.com/ns" },
1108
- });
1109
- assert.strictEqual(result.length, 1);
1110
- });
1111
- });
1112
-
1113
- describe("Filter expressions", () => {
1114
- it("should evaluate filter with predicate", () => {
1115
- const result = select("(/root/item)[2]", doc);
1116
- assert.strictEqual(result.length, 1);
1117
- assert.strictEqual(result[0].getAttribute("id"), "2");
1118
- });
1119
-
1120
- it("should evaluate filter with path continuation", () => {
1121
- const result = select("(/root/nested)/child", doc);
1122
- assert.strictEqual(result.length, 2);
1123
- });
1124
- });
1125
-
1126
- describe("Processing instruction tests", () => {
1127
- it("should match named processing-instruction", () => {
1128
- const docWithPI = createDOM(`<?xml version="1.0"?>
1129
- <root><?php echo "test"?><?xml-stylesheet href="style.css"?></root>
1130
- `);
1131
- const result = select('/root/processing-instruction("php")', docWithPI);
1132
- assert.strictEqual(result.length, 1);
1133
- });
1134
- });
1135
-
1136
- describe("Additional type conversions", () => {
1137
- it("should convert node to number via string value", () => {
1138
- const numDoc = createDOM(`<?xml version="1.0"?><root>42</root>`);
1139
- const root = selectFirst("/root", numDoc);
1140
- const evaluator = new XPathEvaluator();
1141
- const result = evaluator.toNumber(root);
1142
- assert.strictEqual(result, 42);
1143
- });
1144
-
1145
- it("should convert -Infinity to string", () => {
1146
- const result = evaluate("string(-1 div 0)", doc);
1147
- assert.strictEqual(result, "-Infinity");
1148
- });
1149
-
1150
- it("should convert node to string directly", () => {
1151
- const evaluator = new XPathEvaluator();
1152
- const item = selectFirst("/root/item[1]", doc);
1153
- const result = evaluator.toString(item);
1154
- assert.strictEqual(result, "First");
1155
- });
1156
-
1157
- it("should return empty string for unknown node type", () => {
1158
- const evaluator = new XPathEvaluator();
1159
- // Mock a node with unknown type
1160
- const unknownNode = { nodeType: 99 };
1161
- const result = evaluator.getStringValue(unknownNode);
1162
- assert.strictEqual(result, "");
1163
- });
1164
-
1165
- it("should convert null/undefined to false in toBoolean", () => {
1166
- const evaluator = new XPathEvaluator();
1167
- assert.strictEqual(evaluator.toBoolean(null), false);
1168
- assert.strictEqual(evaluator.toBoolean(undefined), false);
1169
- });
1170
-
1171
- it("should convert node to true in toBoolean", () => {
1172
- const evaluator = new XPathEvaluator();
1173
- const item = selectFirst("/root/item[1]", doc);
1174
- assert.strictEqual(evaluator.toBoolean(item), true);
1175
- });
1176
-
1177
- it("should convert array to number via first element", () => {
1178
- const evaluator = new XPathEvaluator();
1179
- const items = select("/root/item", doc);
1180
- // First item has text "First" which is NaN
1181
- const result = evaluator.toNumber(items);
1182
- assert.ok(isNaN(result));
1183
- });
1184
- });
1185
-
1186
- describe("Additional lang() tests", () => {
1187
- it("should return false when no lang attribute", () => {
1188
- const result = evaluate('lang("en")', doc);
1189
- assert.strictEqual(result, false);
1190
- });
1191
-
1192
- it("should match xml:lang attribute", () => {
1193
- const docWithXmlLang = createDOM(`<?xml version="1.0"?>
1194
- <root xml:lang="de">
1195
- <item>Test</item>
1196
- </root>
1197
- `);
1198
- const item = selectFirst("/root/item", docWithXmlLang);
1199
- const result = evaluate('lang("de")', item);
1200
- assert.strictEqual(result, true);
1201
- });
1202
- });
1203
-
1204
- describe("string() function", () => {
1205
- it("should evaluate string() without args", () => {
1206
- const item = selectFirst("/root/item[1]", doc);
1207
- const result = evaluate("string()", item);
1208
- assert.strictEqual(result, "First");
1209
- });
1210
- });
1211
-
1212
- describe("substring() function", () => {
1213
- it("should evaluate substring without length argument", () => {
1214
- const result = evaluate('substring("Hello World", 7)', doc);
1215
- assert.strictEqual(result, "World");
1216
- });
1217
-
1218
- it("should handle substring with NaN length", () => {
1219
- const result = evaluate('substring("Hello", 1, number("NaN"))', doc);
1220
- assert.strictEqual(result, "");
1221
- });
1222
- });
1223
-
1224
- describe("Node-set comparison edge cases", () => {
1225
- it("should return false for empty left node-set comparison", () => {
1226
- const result = evaluate("/root/nonexistent = /root/item", doc);
1227
- assert.strictEqual(result, false);
1228
- });
1229
-
1230
- it("should return false for empty right node-set comparison", () => {
1231
- const result = evaluate("/root/item = /root/nonexistent", doc);
1232
- assert.strictEqual(result, false);
1233
- });
1234
-
1235
- it("should compare two different node-sets returning false", () => {
1236
- const result = evaluate("/root/item = /root/numbers/num", doc);
1237
- assert.strictEqual(result, false);
1238
- });
1239
- });
1240
-
1241
- describe("Error cases for unknown types", () => {
1242
- it("should throw for unknown AST node type", () => {
1243
- const evaluator = new XPathEvaluator();
1244
- const context = new XPathContext(doc);
1245
- const invalidAst = { type: "UnknownType" };
1246
-
1247
- assert.throws(() => {
1248
- evaluator.evaluate(invalidAst, context);
1249
- }, /Unknown AST node type/);
1250
- });
1251
-
1252
- it("should throw for unknown multiplicative operator", () => {
1253
- const evaluator = new XPathEvaluator();
1254
- const context = new XPathContext(doc);
1255
- const ast = {
1256
- type: "MultiplicativeExpr",
1257
- operator: "unknown",
1258
- left: { type: "Number", value: 1 },
1259
- right: { type: "Number", value: 2 },
1260
- };
1261
-
1262
- assert.throws(() => {
1263
- evaluator.evaluate(ast, context);
1264
- }, /Unknown multiplicative operator/);
1265
- });
1266
-
1267
- it("should throw for unknown axis", () => {
1268
- const evaluator = new XPathEvaluator();
1269
- assert.throws(() => {
1270
- evaluator.getAxisNodes("unknownAxis", doc);
1271
- }, /Unknown axis/);
1272
- });
1273
-
1274
- it("should throw for unknown comparison operator", () => {
1275
- const evaluator = new XPathEvaluator();
1276
- assert.throws(() => {
1277
- evaluator.comparePrimitive(1, 2, "??");
1278
- }, /Unknown comparison operator/);
1279
- });
1280
- });
1281
-
1282
- describe("HTML case-insensitive matching", () => {
1283
- it("should match element names case-insensitively in HTML", () => {
1284
- const htmlDoc = new JSDOM("<html><body><DIV>Test</DIV></body></html>", {
1285
- contentType: "text/html",
1286
- }).window.document;
1287
-
1288
- const result = select("//div", htmlDoc);
1289
- assert.ok(result.length >= 1);
1290
- });
1291
- });
1292
-
1293
- describe("Document position fallback", () => {
1294
- it("should sort nodes using fallback when compareDocumentPosition not available", () => {
1295
- const evaluator = new XPathEvaluator();
1296
-
1297
- // Create mock nodes without compareDocumentPosition
1298
- const mockParent = { childNodes: [], parentNode: null };
1299
- const mockNode1 = { parentNode: mockParent };
1300
- const mockNode2 = { parentNode: mockParent };
1301
- mockParent.childNodes = [mockNode1, mockNode2];
1302
-
1303
- // Test the fallback directly
1304
- const result = evaluator.compareDocumentPositionFallback(
1305
- mockNode1,
1306
- mockNode2,
1307
- );
1308
- assert.strictEqual(result, 4); // mockNode1 before mockNode2
1309
-
1310
- const result2 = evaluator.compareDocumentPositionFallback(
1311
- mockNode2,
1312
- mockNode1,
1313
- );
1314
- assert.strictEqual(result2, 2); // mockNode2 after mockNode1
1315
- });
1316
-
1317
- it("should handle nodes at different depths", () => {
1318
- const evaluator = new XPathEvaluator();
1319
-
1320
- const grandparent = { childNodes: [], parentNode: null };
1321
- const parent = { childNodes: [], parentNode: grandparent };
1322
- const child = { parentNode: parent };
1323
- grandparent.childNodes = [parent];
1324
- parent.childNodes = [child];
1325
-
1326
- // Child is deeper than grandparent
1327
- const result = evaluator.compareDocumentPositionFallback(
1328
- grandparent,
1329
- child,
1330
- );
1331
- assert.ok(result === 4 || result === 2); // Position relationship exists
1332
- });
1333
- });
1334
-
1335
- describe("toString edge cases", () => {
1336
- it('should convert 0 to string "0"', () => {
1337
- const result = evaluate("string(0)", doc);
1338
- assert.strictEqual(result, "0");
1339
- });
1340
-
1341
- it("should convert negative zero to string", () => {
1342
- const result = evaluate("string(0 - 0)", doc);
1343
- assert.strictEqual(result, "0");
1344
- });
1345
- });
1346
-
1347
- describe("Attribute axis edge cases", () => {
1348
- it("should return empty array for node without attributes property", () => {
1349
- const evaluator = new XPathEvaluator();
1350
- const textNode = { nodeType: 3 }; // Text node has no attributes
1351
- const result = evaluator.getAxisNodes("attribute", textNode);
1352
- assert.deepStrictEqual(result, []);
1353
- });
1354
- });
1355
-
1356
- describe("matchNodeTest edge cases", () => {
1357
- it("should return false for unknown node test type", () => {
1358
- const evaluator = new XPathEvaluator();
1359
- const context = new XPathContext(doc);
1360
- const unknownTest = { type: "UnknownTestType" };
1361
-
1362
- const result = evaluator.matchNodeTest(unknownTest, doc, context);
1363
- assert.strictEqual(result, false);
1364
- });
1365
-
1366
- it("should return false for unknown node type test", () => {
1367
- const evaluator = new XPathEvaluator();
1368
- const result = evaluator.matchNodeTypeTest("unknownNodeType", doc);
1369
- assert.strictEqual(result, false);
1370
- });
1371
- });
1372
-
1373
- describe("evalPathExpr edge cases", () => {
1374
- it("should return empty array when no filter in path expression", () => {
1375
- const evaluator = new XPathEvaluator();
1376
- const context = new XPathContext(doc);
1377
- const ast = { type: "PathExpr" }; // No filter property
1378
-
1379
- const result = evaluator.evaluate(ast, context);
1380
- assert.deepStrictEqual(result, []);
1381
- });
1382
- });
1383
-
1384
- describe("toNumber edge cases", () => {
1385
- it("should return NaN for non-numeric value", () => {
1386
- const evaluator = new XPathEvaluator();
1387
- const result = evaluator.toNumber({});
1388
- assert.ok(isNaN(result));
1389
- });
1390
- });
1391
-
1392
- describe("Sort edge cases", () => {
1393
- it("should handle sorting identical nodes", () => {
1394
- const item = selectFirst("/root/item[1]", doc);
1395
- const evaluator = new XPathEvaluator();
1396
- const sorted = evaluator.sortByDocumentOrder([item, item]);
1397
- assert.strictEqual(sorted.length, 2);
1398
- });
1399
- });
1400
-
1401
- describe("Right node-set comparison", () => {
1402
- it("should compare primitive to right node-set returning false", () => {
1403
- // Test case where right is nodeset but comparison fails
1404
- const result = evaluate('"nonexistent" = /root/item', doc);
1405
- assert.strictEqual(result, false);
1406
- });
1407
- });
1408
-
1409
- describe("toString regular number coverage", () => {
1410
- it("should convert regular positive number to string", () => {
1411
- const evaluator = new XPathEvaluator();
1412
- // Test line 624 - regular number (not NaN, Infinity, -Infinity, or 0)
1413
- const result = evaluator.toString(42);
1414
- assert.strictEqual(result, "42");
1415
- });
1416
-
1417
- it("should convert regular negative number to string", () => {
1418
- const evaluator = new XPathEvaluator();
1419
- const result = evaluator.toString(-123);
1420
- assert.strictEqual(result, "-123");
1421
- });
1422
-
1423
- it("should convert decimal number to string", () => {
1424
- const evaluator = new XPathEvaluator();
1425
- const result = evaluator.toString(3.14159);
1426
- assert.strictEqual(result, "3.14159");
1427
- });
1428
- });
1429
-
1430
- describe("toString fallback for unknown types", () => {
1431
- it("should convert Symbol to string representation", () => {
1432
- const evaluator = new XPathEvaluator();
1433
- // Test line 634 - value is not number, boolean, array, or node
1434
- const result = evaluator.toString("plain text");
1435
- assert.strictEqual(result, "plain text");
1436
- });
1437
-
1438
- it("should convert null-like object to string", () => {
1439
- const evaluator = new XPathEvaluator();
1440
- // Custom object that doesn't have nodeType
1441
- const result = evaluator.toString({ custom: "object" });
1442
- assert.strictEqual(result, "[object Object]");
1443
- });
1444
- });
1445
-
1446
- describe("Sort identical nodes coverage", () => {
1447
- it("should return 0 when comparing same node with compareDocumentPosition", () => {
1448
- const item = selectFirst("/root/item[1]", doc);
1449
- const evaluator = new XPathEvaluator();
1450
- // Test line 755 - comparing node to itself should return 0
1451
- const sorted = evaluator.sortByDocumentOrder([item, item, item]);
1452
- assert.strictEqual(sorted.length, 3);
1453
- assert.strictEqual(sorted[0], item);
1454
- assert.strictEqual(sorted[1], item);
1455
- });
1456
-
1457
- it("should handle single element array", () => {
1458
- const item = selectFirst("/root/item[1]", doc);
1459
- const evaluator = new XPathEvaluator();
1460
- const sorted = evaluator.sortByDocumentOrder([item]);
1461
- assert.strictEqual(sorted.length, 1);
1462
- });
1463
-
1464
- it("should handle empty array", () => {
1465
- const evaluator = new XPathEvaluator();
1466
- const sorted = evaluator.sortByDocumentOrder([]);
1467
- assert.strictEqual(sorted.length, 0);
1468
- });
1469
- });
1470
-
1471
- describe("Tokenizer Token.toString()", () => {
1472
- it("should return string representation of token", () => {
1473
- // Test tokenizer line 88-89
1474
- const token = new Token(TokenType.NAME, "test", 5);
1475
- const str = token.toString();
1476
- assert.strictEqual(str, 'Token(NAME, "test", pos=5)');
1477
- });
1478
-
1479
- it("should handle numeric value in toString", () => {
1480
- const token = new Token(TokenType.NUMBER, 42, 0);
1481
- const str = token.toString();
1482
- assert.strictEqual(str, "Token(NUMBER, 42, pos=0)");
1483
- });
1484
-
1485
- it("should handle null value in toString", () => {
1486
- const token = new Token(TokenType.EOF, null, 10);
1487
- const str = token.toString();
1488
- assert.strictEqual(str, "Token(EOF, null, pos=10)");
1489
- });
1490
- });
1491
-
1492
- describe("Tokenizer error cases", () => {
1493
- it("should throw error for unexpected character", () => {
1494
- // Test tokenizer lines 259-262
1495
- assert.throws(() => {
1496
- tokenize("test#invalid");
1497
- }, /Unexpected character '#'/);
1498
- });
1499
-
1500
- it("should throw error for unterminated string literal with double quotes", () => {
1501
- // Test tokenizer lines 274-276
1502
- assert.throws(() => {
1503
- tokenize('"unterminated');
1504
- }, /Unterminated string literal/);
1505
- });
1506
-
1507
- it("should throw error for unterminated string literal with single quotes", () => {
1508
- assert.throws(() => {
1509
- tokenize("'unterminated");
1510
- }, /Unterminated string literal/);
1511
- });
1512
- });
1513
-
1514
- describe("Operator disambiguation", () => {
1515
- it("should treat div as element name after //", () => {
1516
- // div is an element name, not division operator
1517
- const divDoc = createDOM("<root><div>Content</div></root>");
1518
- const result = select("//div", divDoc);
1519
- assert.strictEqual(result.length, 1);
1520
- assert.strictEqual(result[0].textContent, "Content");
1521
- });
1522
-
1523
- it("should treat mod as element name at start", () => {
1524
- const modDoc = createDOM("<root><mod>Modular</mod></root>");
1525
- const result = select("/root/mod", modDoc);
1526
- assert.strictEqual(result.length, 1);
1527
- });
1528
-
1529
- it("should treat and/or as element names after operators", () => {
1530
- const andOrDoc = createDOM(
1531
- "<root><and>AndContent</and><or>OrContent</or></root>",
1532
- );
1533
- const andResult = select("/root/and", andOrDoc);
1534
- const orResult = select("/root/or", andOrDoc);
1535
- assert.strictEqual(andResult.length, 1);
1536
- assert.strictEqual(orResult.length, 1);
1537
- });
1538
-
1539
- it("should treat div as operator after number", () => {
1540
- const result = evaluate("10 div 2", doc);
1541
- assert.strictEqual(result, 5);
1542
- });
1543
-
1544
- it("should treat mod as operator after closing paren", () => {
1545
- const result = evaluate("(10) mod 3", doc);
1546
- assert.strictEqual(result, 1);
1547
- });
1548
-
1549
- it("should treat and as operator after boolean", () => {
1550
- const result = evaluate("true() and false()", doc);
1551
- assert.strictEqual(result, false);
1552
- });
1553
-
1554
- it("should treat or as operator after name reference", () => {
1555
- const result = evaluate("1 = 1 or 1 = 2", doc);
1556
- assert.strictEqual(result, true);
1557
- });
1558
- });
1559
-
1560
- describe("Disconnected nodes sorting", () => {
1561
- it("should handle sorting nodes from different documents", () => {
1562
- const doc2 = createDOM("<other><element>Test</element></other>");
1563
- const node1 = selectFirst("/root/item[1]", doc);
1564
- const node2 = selectFirst("/other/element", doc2);
1565
-
1566
- const evaluator = new XPathEvaluator();
1567
- // When nodes are from different documents, compareDocumentPosition
1568
- // returns DOCUMENT_POSITION_DISCONNECTED (1) without bits 2 or 4
1569
- // This should hit line 755 (return 0)
1570
- const sorted = evaluator.sortByDocumentOrder([node1, node2]);
1571
- assert.strictEqual(sorted.length, 2);
1572
- });
1573
-
1574
- it("should return 0 when compareDocumentPosition returns 0", () => {
1575
- const evaluator = new XPathEvaluator();
1576
- // Create mock nodes that return 0 from compareDocumentPosition
1577
- // This tests line 755 directly
1578
- const mockNode1 = {
1579
- compareDocumentPosition: () => 0,
1580
- };
1581
- const mockNode2 = {
1582
- compareDocumentPosition: () => 0,
1583
- };
1584
-
1585
- const sorted = evaluator.sortByDocumentOrder([mockNode1, mockNode2]);
1586
- assert.strictEqual(sorted.length, 2);
1587
- });
1588
-
1589
- it("should return 0 when nodes are in contains relationship without preceding/following", () => {
1590
- const evaluator = new XPathEvaluator();
1591
- // DOCUMENT_POSITION_CONTAINS (8) or DOCUMENT_POSITION_CONTAINED_BY (16)
1592
- // without bits 2 or 4 (edge case)
1593
- const mockNode1 = {
1594
- compareDocumentPosition: () => 8, // contains only
1595
- };
1596
- const mockNode2 = {
1597
- compareDocumentPosition: () => 16, // contained_by only
1598
- };
1599
-
1600
- const sorted = evaluator.sortByDocumentOrder([mockNode1, mockNode2]);
1601
- assert.strictEqual(sorted.length, 2);
1602
- });
1603
- });
1604
-
1605
- describe("Parser edge cases", () => {
1606
- it("should parse empty predicate", () => {
1607
- // Predicates with just whitespace or simple expressions
1608
- const result = evaluate("(/root/item)[1]", doc);
1609
- assert.ok(result);
1610
- });
1611
-
1612
- it("should parse deeply nested parentheses", () => {
1613
- const result = evaluate("(((1 + 2)))", doc);
1614
- assert.strictEqual(result, 3);
1615
- });
1616
-
1617
- it("should parse union expression", () => {
1618
- const result = select("/root/item | /root/nested", doc);
1619
- assert.strictEqual(result.length, 4); // 3 items + 1 nested
1620
- });
1621
-
1622
- it("should parse complex filter expression", () => {
1623
- const result = select("(/root/item)[position() > 1]", doc);
1624
- assert.strictEqual(result.length, 2);
1625
- });
1626
-
1627
- it("should parse multiple predicates", () => {
1628
- const result = select('/root/item[@id][@id="2"]', doc);
1629
- assert.strictEqual(result.length, 1);
1630
- });
1631
-
1632
- it("should parse namespace prefix", () => {
1633
- const _nsDoc = createDOM(
1634
- `<root xmlns:ns="http://example.com"><ns:item>Test</ns:item></root>`,
1635
- );
1636
- // Even without namespace resolver, parsing should work
1637
- assert.doesNotThrow(() => {
1638
- parse("//ns:item");
1639
- });
1640
- });
1641
-
1642
- it("should parse abbreviated descendant axis", () => {
1643
- const result = select(".//child", doc);
1644
- assert.ok(result.length > 0);
1645
- });
1646
-
1647
- it("should parse parent axis abbreviation", () => {
1648
- const nested = selectFirst("/root/nested", doc);
1649
- const result = select("..", nested);
1650
- assert.strictEqual(result[0].nodeName, "root");
1651
- });
1652
-
1653
- it("should parse self axis abbreviation", () => {
1654
- const nested = selectFirst("/root/nested", doc);
1655
- const result = select(".", nested);
1656
- assert.strictEqual(result[0].nodeName, "nested");
1657
- });
1658
- });
1659
-
1660
- describe("Function edge cases", () => {
1661
- it("should handle substring with negative start", () => {
1662
- const result = evaluate('substring("12345", -1, 5)', doc);
1663
- // Per XPath spec: chars at pos >= -1 and < (-1 + 5) = 4, so positions 1,2,3
1664
- assert.strictEqual(result, "123");
1665
- });
1666
-
1667
- it("should handle floor with positive infinity", () => {
1668
- const result = evaluate("floor(1 div 0)", doc);
1669
- assert.strictEqual(result, Infinity);
1670
- });
1671
-
1672
- it("should handle ceiling with negative infinity", () => {
1673
- const result = evaluate("ceiling(-1 div 0)", doc);
1674
- assert.strictEqual(result, -Infinity);
1675
- });
1676
-
1677
- it("should handle round with NaN", () => {
1678
- const result = evaluate("round(0 div 0)", doc);
1679
- assert.ok(isNaN(result));
1680
- });
1681
-
1682
- it("should handle translate with missing characters", () => {
1683
- const result = evaluate('translate("abc", "abc", "AB")', doc);
1684
- // 'a' -> 'A', 'b' -> 'B', 'c' has no replacement so removed
1685
- assert.strictEqual(result, "AB");
1686
- });
1687
-
1688
- it("should handle contains with empty string", () => {
1689
- const result = evaluate('contains("test", "")', doc);
1690
- assert.strictEqual(result, true);
1691
- });
1692
-
1693
- it("should handle starts-with with empty string", () => {
1694
- const result = evaluate('starts-with("test", "")', doc);
1695
- assert.strictEqual(result, true);
1696
- });
1697
-
1698
- it("should handle concat with single argument", () => {
1699
- const result = evaluate('concat("single")', doc);
1700
- assert.strictEqual(result, "single");
1701
- });
1702
- });
1703
-
1704
- describe("Parser error cases", () => {
1705
- it("should throw error for unexpected token after expression", () => {
1706
- // Lines 57-60: trailing junk after valid expression
1707
- assert.throws(() => {
1708
- parse("1 + 2 @");
1709
- }, /Unexpected token/);
1710
- });
1711
-
1712
- it("should throw error for missing name after colon in name test", () => {
1713
- // Lines 372-374: prefix: without name or *
1714
- assert.throws(() => {
1715
- parse("//ns: ");
1716
- }, /Expected name/);
1717
- });
1718
-
1719
- it("should throw error for missing variable name", () => {
1720
- // Lines 407-408: $ without name
1721
- assert.throws(() => {
1722
- parse("$");
1723
- }, /Expected variable name/);
1724
- });
1725
-
1726
- it("should throw error for unexpected token in primary expression", () => {
1727
- // Lines 445-448
1728
- assert.throws(() => {
1729
- parse("]");
1730
- }, /Unexpected token/);
1731
- });
1732
-
1733
- it("should throw error for missing expected token", () => {
1734
- // Lines 525-528: expect() failure
1735
- assert.throws(() => {
1736
- parse("(1 + 2");
1737
- }, /Expected RPAREN/);
1738
- });
1739
- });
1740
-
1741
- describe("Parser advanced features", () => {
1742
- it("should parse descendant path after filter expression", () => {
1743
- // Lines 224-231: filter followed by //step
1744
- const result = select("(/root/item)[1]//text()", doc);
1745
- assert.ok(result.length >= 0);
1746
- });
1747
-
1748
- it("should parse prefixed variable reference", () => {
1749
- // Lines 413-419: $prefix:name
1750
- assert.doesNotThrow(() => {
1751
- parse("$ns:variable");
1752
- });
1753
- });
1754
-
1755
- it("should throw for invalid prefixed function syntax", () => {
1756
- // Lines 458-464 are hard to reach - the tokenizer distinguishes functions
1757
- // by checking for '(' after name. prefix:fn() is tokenized as NAME:NAME()
1758
- // This hits the name test error path instead
1759
- assert.throws(() => {
1760
- parse("fn:custom-function()");
1761
- }, /Expected name or/);
1762
- });
1763
-
1764
- it("should parse complex filter with descendant", () => {
1765
- const result = select("(//item)[position() <= 2]//text()", doc);
1766
- assert.ok(result.length >= 0);
1767
- });
1768
-
1769
- it("should handle advance at end of tokens", () => {
1770
- // Line 507: edge case for advance() when at end
1771
- // This is implicitly tested but let's add explicit test
1772
- assert.doesNotThrow(() => {
1773
- parse("1");
1774
- });
1775
- });
1776
-
1777
- it("should throw for prefixed variable without local name", () => {
1778
- // Lines 415-416: $prefix: without local name
1779
- assert.throws(() => {
1780
- parse("$ns:");
1781
- }, /Expected local name/);
1782
- });
1783
-
1784
- it("should throw for invalid name test with at sign", () => {
1785
- // Lines 357-358 - parseNameTest expects NAME but gets something else
1786
- assert.throws(() => {
1787
- parse("//@");
1788
- }, /Expected name/);
1789
- });
1790
- });
1791
-
1792
- describe("Index.js edge cases", () => {
1793
- it("should return single node in array when result is single node", () => {
1794
- // Line 47 in index.js: when result has nodeType but isn't array
1795
- // This path is hard to hit since evaluate usually returns arrays for paths
1796
- const result = select("/root", doc);
1797
- assert.ok(Array.isArray(result));
1798
- });
1799
-
1800
- it("should return empty array when result is not array and not node", () => {
1801
- // Line 48 in index.js: return [] when result is primitive (string, number, boolean)
1802
- // select() is meant for node selection, so primitives return empty array
1803
- const result = select("1 + 1", doc); // Returns number 2, not a node
1804
- assert.deepStrictEqual(result, []);
1805
- });
1806
-
1807
- it("should return empty array for string result", () => {
1808
- const result = select("string(/root/item[1])", doc); // Returns "First"
1809
- assert.deepStrictEqual(result, []);
1810
- });
1811
-
1812
- it("should return empty array for boolean result", () => {
1813
- const result = select("true()", doc); // Returns true
1814
- assert.deepStrictEqual(result, []);
1815
- });
1816
- });
1817
- });
1818
-
1819
- describe("XPathParser edge cases", () => {
1820
- it("should return last token when advance() called at end", async () => {
1821
- const { XPathParser } = await import("./parser.js");
1822
- const tokens = tokenize("foo");
1823
- const parser = new XPathParser(tokens);
1824
-
1825
- // Parse the expression which consumes all tokens
1826
- parser.parse();
1827
-
1828
- // Now at end, advance() should return the last token (EOF)
1829
- const result = parser.advance();
1830
- assert.strictEqual(result.type, TokenType.EOF);
1831
- });
1832
-
1833
- it("should handle advance() at end of tokens gracefully", async () => {
1834
- const { XPathParser } = await import("./parser.js");
1835
- // Single element expression
1836
- const tokens = tokenize("a");
1837
- const parser = new XPathParser(tokens);
1838
-
1839
- // Consume all tokens
1840
- while (!parser.isAtEnd()) {
1841
- parser.advance();
1842
- }
1843
-
1844
- // Call advance when already at end - should return last token
1845
- const token1 = parser.advance();
1846
- const token2 = parser.advance();
1847
-
1848
- // Both should return the EOF token
1849
- assert.strictEqual(token1.type, TokenType.EOF);
1850
- assert.strictEqual(token2.type, TokenType.EOF);
1851
- });
1852
- });