@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.
- package/README.md +292 -47
- package/bin/lib/options.js +114 -0
- package/bin/lib/paths.js +186 -0
- package/bin/lib/transform.js +115 -0
- package/bin/xslt.js +68 -162
- package/dist/xslt-processor.browser.js +2073 -163
- package/dist/xslt-processor.browser.js.map +4 -4
- package/dist/xslt-processor.browser.min.js +6 -2
- package/dist/xslt-processor.browser.min.js.map +4 -4
- package/dist/xslt-processor.cjs +2077 -162
- package/dist/xslt-processor.cjs.map +4 -4
- package/dist/xslt-processor.d.cts +299 -0
- package/dist/xslt-processor.d.ts +92 -4
- package/dist/xslt-processor.js +2072 -161
- package/dist/xslt-processor.js.map +4 -4
- package/package.json +27 -16
- package/src/XSLTProcessor.js +177 -8
- package/src/index.js +11 -5
- package/src/xpath/evaluator.js +48 -7
- package/src/xslt/elements.js +57 -0
- package/src/xslt/engine.js +474 -185
- package/src/xslt/formatNumber.js +220 -0
- package/src/xslt/functions.js +191 -0
- package/src/xslt/index.js +31 -0
- package/src/xslt/keys.js +141 -0
- package/src/xslt/literalResult.js +167 -0
- package/src/xslt/number.js +178 -0
- package/src/xslt/numberFormat.js +155 -0
- package/src/xslt/resultTree.js +74 -0
- package/src/xslt/serializer/baseWriter.js +283 -0
- package/src/xslt/serializer/constants.js +78 -0
- package/src/xslt/serializer/escape.js +98 -0
- package/src/xslt/serializer/htmlSerializer.js +141 -0
- package/src/xslt/serializer/indent.js +51 -0
- package/src/xslt/serializer/namespaces.js +68 -0
- package/src/xslt/serializer/rawText.js +41 -0
- package/src/xslt/serializer/settings.js +103 -0
- package/src/xslt/serializer/textSerializer.js +29 -0
- package/src/xslt/serializer/xmlSerializer.js +127 -0
- package/src/xslt/serializer.js +57 -0
- package/src/xslt/templatePriority.js +45 -0
- package/src/xslt/uri.js +68 -0
- package/src/xslt/whitespace.js +184 -0
- package/src/XSLTProcessor.test.js +0 -930
- package/src/xpath/evaluator.test.js +0 -1852
- package/src/xpath/tokenizer.test.js +0 -224
- 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><special>content</special></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
|
-
});
|