@tradik/xslt-processor 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/LICENSE.md +28 -0
- package/README.md +634 -0
- package/bin/xslt.js +208 -0
- package/dist/xslt-processor.browser.js +3302 -0
- package/dist/xslt-processor.browser.js.map +7 -0
- package/dist/xslt-processor.browser.min.js +6 -0
- package/dist/xslt-processor.browser.min.js.map +7 -0
- package/dist/xslt-processor.cjs +3311 -0
- package/dist/xslt-processor.cjs.map +7 -0
- package/dist/xslt-processor.d.ts +211 -0
- package/dist/xslt-processor.js +3271 -0
- package/dist/xslt-processor.js.map +7 -0
- package/package.json +68 -0
- package/src/XSLTProcessor.js +368 -0
- package/src/XSLTProcessor.test.js +930 -0
- package/src/index.js +66 -0
- package/src/xpath/evaluator.js +1012 -0
- package/src/xpath/evaluator.test.js +1852 -0
- package/src/xpath/index.js +67 -0
- package/src/xpath/parser.js +595 -0
- package/src/xpath/tokenizer.js +383 -0
- package/src/xpath/tokenizer.test.js +224 -0
- package/src/xslt/engine.js +1812 -0
- package/src/xslt/engine.test.js +3130 -0
- package/src/xslt/index.js +6 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XPath 1.0 Tokenizer
|
|
3
|
+
* Based on W3C XPath 1.0 Specification: http://www.w3.org/TR/1999/REC-xpath-19991116
|
|
4
|
+
*
|
|
5
|
+
* Converts XPath expression strings into token streams for parsing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
export const TokenType = {
|
|
11
|
+
// Literals
|
|
12
|
+
NUMBER: "NUMBER",
|
|
13
|
+
LITERAL: "LITERAL",
|
|
14
|
+
|
|
15
|
+
// Operators
|
|
16
|
+
SLASH: "SLASH",
|
|
17
|
+
DOUBLE_SLASH: "DOUBLE_SLASH",
|
|
18
|
+
PIPE: "PIPE",
|
|
19
|
+
PLUS: "PLUS",
|
|
20
|
+
MINUS: "MINUS",
|
|
21
|
+
STAR: "STAR",
|
|
22
|
+
DIV: "DIV",
|
|
23
|
+
MOD: "MOD",
|
|
24
|
+
EQUALS: "EQUALS",
|
|
25
|
+
NOT_EQUALS: "NOT_EQUALS",
|
|
26
|
+
LT: "LT",
|
|
27
|
+
LTE: "LTE",
|
|
28
|
+
GT: "GT",
|
|
29
|
+
GTE: "GTE",
|
|
30
|
+
AND: "AND",
|
|
31
|
+
OR: "OR",
|
|
32
|
+
|
|
33
|
+
// Brackets
|
|
34
|
+
LPAREN: "LPAREN",
|
|
35
|
+
RPAREN: "RPAREN",
|
|
36
|
+
LBRACKET: "LBRACKET",
|
|
37
|
+
RBRACKET: "RBRACKET",
|
|
38
|
+
|
|
39
|
+
// Axes
|
|
40
|
+
AXIS: "AXIS",
|
|
41
|
+
AT: "AT",
|
|
42
|
+
DOT: "DOT",
|
|
43
|
+
DOUBLE_DOT: "DOUBLE_DOT",
|
|
44
|
+
DOUBLE_COLON: "DOUBLE_COLON",
|
|
45
|
+
|
|
46
|
+
// Names
|
|
47
|
+
NAME: "NAME",
|
|
48
|
+
NCNAME: "NCNAME",
|
|
49
|
+
PREFIX: "PREFIX",
|
|
50
|
+
FUNCTION: "FUNCTION",
|
|
51
|
+
NODE_TYPE: "NODE_TYPE",
|
|
52
|
+
|
|
53
|
+
// Other
|
|
54
|
+
COMMA: "COMMA",
|
|
55
|
+
DOLLAR: "DOLLAR",
|
|
56
|
+
COLON: "COLON",
|
|
57
|
+
EOF: "EOF",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const AXIS_NAMES = new Set([
|
|
61
|
+
"ancestor",
|
|
62
|
+
"ancestor-or-self",
|
|
63
|
+
"attribute",
|
|
64
|
+
"child",
|
|
65
|
+
"descendant",
|
|
66
|
+
"descendant-or-self",
|
|
67
|
+
"following",
|
|
68
|
+
"following-sibling",
|
|
69
|
+
"namespace",
|
|
70
|
+
"parent",
|
|
71
|
+
"preceding",
|
|
72
|
+
"preceding-sibling",
|
|
73
|
+
"self",
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
const NODE_TYPES = new Set([
|
|
77
|
+
"comment",
|
|
78
|
+
"text",
|
|
79
|
+
"processing-instruction",
|
|
80
|
+
"node",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const OPERATORS = new Set(["and", "or", "mod", "div"]);
|
|
84
|
+
|
|
85
|
+
export class Token {
|
|
86
|
+
constructor(type, value, position) {
|
|
87
|
+
this.type = type;
|
|
88
|
+
this.value = value;
|
|
89
|
+
this.position = position;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
toString() {
|
|
93
|
+
return `Token(${this.type}, ${JSON.stringify(this.value)}, pos=${this.position})`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Tokens that indicate the NEXT token should NOT be treated as an OperatorName
|
|
98
|
+
// Per XPath 1.0 spec: "If there is a preceding token and the preceding token is not one of
|
|
99
|
+
// @, ::, (, [, , or an Operator, then... an NCName must be recognized as an OperatorName."
|
|
100
|
+
// So after these tokens, OperatorNames (div, mod, and, or) should be treated as regular names.
|
|
101
|
+
const OPERATOR_CONTEXT_BLOCKERS = new Set([
|
|
102
|
+
TokenType.AT,
|
|
103
|
+
TokenType.DOUBLE_COLON,
|
|
104
|
+
TokenType.LPAREN,
|
|
105
|
+
TokenType.LBRACKET,
|
|
106
|
+
TokenType.COMMA,
|
|
107
|
+
// Operators - after an operator, next name token should be a name, not an operator
|
|
108
|
+
TokenType.SLASH,
|
|
109
|
+
TokenType.DOUBLE_SLASH,
|
|
110
|
+
TokenType.PIPE,
|
|
111
|
+
TokenType.PLUS,
|
|
112
|
+
TokenType.MINUS,
|
|
113
|
+
TokenType.STAR,
|
|
114
|
+
TokenType.DIV,
|
|
115
|
+
TokenType.MOD,
|
|
116
|
+
TokenType.EQUALS,
|
|
117
|
+
TokenType.NOT_EQUALS,
|
|
118
|
+
TokenType.LT,
|
|
119
|
+
TokenType.LTE,
|
|
120
|
+
TokenType.GT,
|
|
121
|
+
TokenType.GTE,
|
|
122
|
+
TokenType.AND,
|
|
123
|
+
TokenType.OR,
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
export class XPathTokenizer {
|
|
127
|
+
constructor(expression) {
|
|
128
|
+
this.expression = expression;
|
|
129
|
+
this.position = 0;
|
|
130
|
+
this.tokens = [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get the last token that was added (if any)
|
|
135
|
+
*/
|
|
136
|
+
getLastToken() {
|
|
137
|
+
return this.tokens.length > 0 ? this.tokens[this.tokens.length - 1] : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if the current context allows operator names (div, mod, and, or)
|
|
142
|
+
* Per XPath 1.0 spec disambiguation rules.
|
|
143
|
+
*/
|
|
144
|
+
isOperatorContext() {
|
|
145
|
+
const lastToken = this.getLastToken();
|
|
146
|
+
// No preceding token means we're at the start - not an operator context
|
|
147
|
+
if (!lastToken) return false;
|
|
148
|
+
// If preceding token is a "blocker", it's not an operator context
|
|
149
|
+
if (OPERATOR_CONTEXT_BLOCKERS.has(lastToken.type)) return false;
|
|
150
|
+
// Otherwise, it IS an operator context (after names, numbers, closing brackets, etc.)
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
tokenize() {
|
|
155
|
+
this.tokens = [];
|
|
156
|
+
this.position = 0;
|
|
157
|
+
|
|
158
|
+
while (this.position < this.expression.length) {
|
|
159
|
+
this.skipWhitespace();
|
|
160
|
+
if (this.position >= this.expression.length) break;
|
|
161
|
+
|
|
162
|
+
const token = this.nextToken();
|
|
163
|
+
if (token) {
|
|
164
|
+
this.tokens.push(token);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.tokens.push(new Token(TokenType.EOF, null, this.position));
|
|
169
|
+
return this.tokens;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
skipWhitespace() {
|
|
173
|
+
while (
|
|
174
|
+
this.position < this.expression.length &&
|
|
175
|
+
/\s/.test(this.expression[this.position])
|
|
176
|
+
) {
|
|
177
|
+
this.position++;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
peek(offset = 0) {
|
|
182
|
+
return this.expression[this.position + offset];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
consume() {
|
|
186
|
+
return this.expression[this.position++];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
nextToken() {
|
|
190
|
+
const startPos = this.position;
|
|
191
|
+
const char = this.peek();
|
|
192
|
+
|
|
193
|
+
// String literals
|
|
194
|
+
if (char === '"' || char === "'") {
|
|
195
|
+
return this.readStringLiteral();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Numbers
|
|
199
|
+
if (/[0-9]/.test(char) || (char === "." && /[0-9]/.test(this.peek(1)))) {
|
|
200
|
+
return this.readNumber();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Multi-character operators
|
|
204
|
+
if (char === "/" && this.peek(1) === "/") {
|
|
205
|
+
this.position += 2;
|
|
206
|
+
return new Token(TokenType.DOUBLE_SLASH, "//", startPos);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (char === "." && this.peek(1) === ".") {
|
|
210
|
+
this.position += 2;
|
|
211
|
+
return new Token(TokenType.DOUBLE_DOT, "..", startPos);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (char === ":" && this.peek(1) === ":") {
|
|
215
|
+
this.position += 2;
|
|
216
|
+
return new Token(TokenType.DOUBLE_COLON, "::", startPos);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (char === "!" && this.peek(1) === "=") {
|
|
220
|
+
this.position += 2;
|
|
221
|
+
return new Token(TokenType.NOT_EQUALS, "!=", startPos);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (char === "<" && this.peek(1) === "=") {
|
|
225
|
+
this.position += 2;
|
|
226
|
+
return new Token(TokenType.LTE, "<=", startPos);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (char === ">" && this.peek(1) === "=") {
|
|
230
|
+
this.position += 2;
|
|
231
|
+
return new Token(TokenType.GTE, ">=", startPos);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Single-character tokens
|
|
235
|
+
const singleCharTokens = {
|
|
236
|
+
"/": TokenType.SLASH,
|
|
237
|
+
"|": TokenType.PIPE,
|
|
238
|
+
"+": TokenType.PLUS,
|
|
239
|
+
"-": TokenType.MINUS,
|
|
240
|
+
"*": TokenType.STAR,
|
|
241
|
+
"=": TokenType.EQUALS,
|
|
242
|
+
"<": TokenType.LT,
|
|
243
|
+
">": TokenType.GT,
|
|
244
|
+
"(": TokenType.LPAREN,
|
|
245
|
+
")": TokenType.RPAREN,
|
|
246
|
+
"[": TokenType.LBRACKET,
|
|
247
|
+
"]": TokenType.RBRACKET,
|
|
248
|
+
"@": TokenType.AT,
|
|
249
|
+
".": TokenType.DOT,
|
|
250
|
+
",": TokenType.COMMA,
|
|
251
|
+
$: TokenType.DOLLAR,
|
|
252
|
+
":": TokenType.COLON,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
if (singleCharTokens[char]) {
|
|
256
|
+
this.position++;
|
|
257
|
+
return new Token(singleCharTokens[char], char, startPos);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Names and keywords
|
|
261
|
+
if (this.isNameStartChar(char)) {
|
|
262
|
+
return this.readName();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
throw new Error(
|
|
266
|
+
`Unexpected character '${char}' at position ${this.position} in expression: ${this.expression}`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
readStringLiteral() {
|
|
271
|
+
const startPos = this.position;
|
|
272
|
+
const quote = this.consume();
|
|
273
|
+
let value = "";
|
|
274
|
+
|
|
275
|
+
while (this.position < this.expression.length && this.peek() !== quote) {
|
|
276
|
+
value += this.consume();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (this.position >= this.expression.length) {
|
|
280
|
+
throw new Error(`Unterminated string literal at position ${startPos}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
this.consume(); // closing quote
|
|
284
|
+
return new Token(TokenType.LITERAL, value, startPos);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
readNumber() {
|
|
288
|
+
const startPos = this.position;
|
|
289
|
+
let value = "";
|
|
290
|
+
|
|
291
|
+
// Integer part
|
|
292
|
+
while (
|
|
293
|
+
this.position < this.expression.length &&
|
|
294
|
+
/[0-9]/.test(this.peek())
|
|
295
|
+
) {
|
|
296
|
+
value += this.consume();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Decimal part (handles both 1.5 and .5 style numbers)
|
|
300
|
+
// For .5 style: entry condition ensures digit follows, so this branch handles it
|
|
301
|
+
if (this.peek() === "." && /[0-9]/.test(this.peek(1))) {
|
|
302
|
+
value += this.consume(); // .
|
|
303
|
+
while (
|
|
304
|
+
this.position < this.expression.length &&
|
|
305
|
+
/[0-9]/.test(this.peek())
|
|
306
|
+
) {
|
|
307
|
+
value += this.consume();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return new Token(TokenType.NUMBER, parseFloat(value), startPos);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
readName() {
|
|
315
|
+
const startPos = this.position;
|
|
316
|
+
let value = "";
|
|
317
|
+
|
|
318
|
+
while (
|
|
319
|
+
this.position < this.expression.length &&
|
|
320
|
+
this.isNameChar(this.peek())
|
|
321
|
+
) {
|
|
322
|
+
value += this.consume();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Check for axis name followed by ::
|
|
326
|
+
this.skipWhitespace();
|
|
327
|
+
if (AXIS_NAMES.has(value) && this.peek() === ":" && this.peek(1) === ":") {
|
|
328
|
+
return new Token(TokenType.AXIS, value, startPos);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Check for function call (followed by '(')
|
|
332
|
+
const savedPos = this.position;
|
|
333
|
+
this.skipWhitespace();
|
|
334
|
+
if (this.peek() === "(") {
|
|
335
|
+
// Check if it's a node type test
|
|
336
|
+
if (NODE_TYPES.has(value)) {
|
|
337
|
+
return new Token(TokenType.NODE_TYPE, value, startPos);
|
|
338
|
+
}
|
|
339
|
+
return new Token(TokenType.FUNCTION, value, startPos);
|
|
340
|
+
}
|
|
341
|
+
this.position = savedPos;
|
|
342
|
+
|
|
343
|
+
// Check for operators - only in operator context per XPath 1.0 disambiguation rules
|
|
344
|
+
if (OPERATORS.has(value) && this.isOperatorContext()) {
|
|
345
|
+
const opTokens = {
|
|
346
|
+
and: TokenType.AND,
|
|
347
|
+
or: TokenType.OR,
|
|
348
|
+
mod: TokenType.MOD,
|
|
349
|
+
div: TokenType.DIV,
|
|
350
|
+
};
|
|
351
|
+
return new Token(opTokens[value], value, startPos);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return new Token(TokenType.NAME, value, startPos);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
isNameStartChar(char) {
|
|
358
|
+
if (!char) return false;
|
|
359
|
+
const code = char.charCodeAt(0);
|
|
360
|
+
return (
|
|
361
|
+
char === "_" ||
|
|
362
|
+
(code >= 65 && code <= 90) || // A-Z
|
|
363
|
+
(code >= 97 && code <= 122) || // a-z
|
|
364
|
+
code >= 0xc0 // Unicode letters
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
isNameChar(char) {
|
|
369
|
+
if (!char) return false;
|
|
370
|
+
const code = char.charCodeAt(0);
|
|
371
|
+
return (
|
|
372
|
+
this.isNameStartChar(char) ||
|
|
373
|
+
char === "-" ||
|
|
374
|
+
char === "." ||
|
|
375
|
+
(code >= 48 && code <= 57) // 0-9
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function tokenize(expression) {
|
|
381
|
+
const tokenizer = new XPathTokenizer(expression);
|
|
382
|
+
return tokenizer.tokenize();
|
|
383
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XPath Tokenizer Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import assert from "node:assert";
|
|
7
|
+
import { tokenize, TokenType } from "./tokenizer.js";
|
|
8
|
+
|
|
9
|
+
describe("XPath Tokenizer", () => {
|
|
10
|
+
describe("Simple paths", () => {
|
|
11
|
+
it("should tokenize simple element name", () => {
|
|
12
|
+
const tokens = tokenize("foo");
|
|
13
|
+
assert.strictEqual(tokens.length, 2);
|
|
14
|
+
assert.strictEqual(tokens[0].type, TokenType.NAME);
|
|
15
|
+
assert.strictEqual(tokens[0].value, "foo");
|
|
16
|
+
assert.strictEqual(tokens[1].type, TokenType.EOF);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("should tokenize root path", () => {
|
|
20
|
+
const tokens = tokenize("/");
|
|
21
|
+
assert.strictEqual(tokens[0].type, TokenType.SLASH);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should tokenize absolute path", () => {
|
|
25
|
+
const tokens = tokenize("/foo/bar");
|
|
26
|
+
assert.strictEqual(tokens[0].type, TokenType.SLASH);
|
|
27
|
+
assert.strictEqual(tokens[1].type, TokenType.NAME);
|
|
28
|
+
assert.strictEqual(tokens[1].value, "foo");
|
|
29
|
+
assert.strictEqual(tokens[2].type, TokenType.SLASH);
|
|
30
|
+
assert.strictEqual(tokens[3].type, TokenType.NAME);
|
|
31
|
+
assert.strictEqual(tokens[3].value, "bar");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should tokenize descendant path", () => {
|
|
35
|
+
const tokens = tokenize("//foo");
|
|
36
|
+
assert.strictEqual(tokens[0].type, TokenType.DOUBLE_SLASH);
|
|
37
|
+
assert.strictEqual(tokens[1].type, TokenType.NAME);
|
|
38
|
+
assert.strictEqual(tokens[1].value, "foo");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("Wildcards and special characters", () => {
|
|
43
|
+
it("should tokenize wildcard", () => {
|
|
44
|
+
const tokens = tokenize("*");
|
|
45
|
+
assert.strictEqual(tokens[0].type, TokenType.STAR);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should tokenize attribute selector", () => {
|
|
49
|
+
const tokens = tokenize("@id");
|
|
50
|
+
assert.strictEqual(tokens[0].type, TokenType.AT);
|
|
51
|
+
assert.strictEqual(tokens[1].type, TokenType.NAME);
|
|
52
|
+
assert.strictEqual(tokens[1].value, "id");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should tokenize dot (self)", () => {
|
|
56
|
+
const tokens = tokenize(".");
|
|
57
|
+
assert.strictEqual(tokens[0].type, TokenType.DOT);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should tokenize double dot (parent)", () => {
|
|
61
|
+
const tokens = tokenize("..");
|
|
62
|
+
assert.strictEqual(tokens[0].type, TokenType.DOUBLE_DOT);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("Predicates", () => {
|
|
67
|
+
it("should tokenize predicate brackets", () => {
|
|
68
|
+
const tokens = tokenize("foo[1]");
|
|
69
|
+
assert.strictEqual(tokens[0].type, TokenType.NAME);
|
|
70
|
+
assert.strictEqual(tokens[1].type, TokenType.LBRACKET);
|
|
71
|
+
assert.strictEqual(tokens[2].type, TokenType.NUMBER);
|
|
72
|
+
assert.strictEqual(tokens[2].value, 1);
|
|
73
|
+
assert.strictEqual(tokens[3].type, TokenType.RBRACKET);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("should tokenize complex predicate", () => {
|
|
77
|
+
const tokens = tokenize('item[@id="test"]');
|
|
78
|
+
assert.strictEqual(tokens[0].type, TokenType.NAME);
|
|
79
|
+
assert.strictEqual(tokens[1].type, TokenType.LBRACKET);
|
|
80
|
+
assert.strictEqual(tokens[2].type, TokenType.AT);
|
|
81
|
+
assert.strictEqual(tokens[3].type, TokenType.NAME);
|
|
82
|
+
assert.strictEqual(tokens[4].type, TokenType.EQUALS);
|
|
83
|
+
assert.strictEqual(tokens[5].type, TokenType.LITERAL);
|
|
84
|
+
assert.strictEqual(tokens[5].value, "test");
|
|
85
|
+
assert.strictEqual(tokens[6].type, TokenType.RBRACKET);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("Operators", () => {
|
|
90
|
+
it("should tokenize arithmetic operators", () => {
|
|
91
|
+
const tokens = tokenize("1 + 2 - 3 * 4");
|
|
92
|
+
assert.strictEqual(tokens[0].type, TokenType.NUMBER);
|
|
93
|
+
assert.strictEqual(tokens[1].type, TokenType.PLUS);
|
|
94
|
+
assert.strictEqual(tokens[2].type, TokenType.NUMBER);
|
|
95
|
+
assert.strictEqual(tokens[3].type, TokenType.MINUS);
|
|
96
|
+
assert.strictEqual(tokens[4].type, TokenType.NUMBER);
|
|
97
|
+
assert.strictEqual(tokens[5].type, TokenType.STAR);
|
|
98
|
+
assert.strictEqual(tokens[6].type, TokenType.NUMBER);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should tokenize comparison operators", () => {
|
|
102
|
+
const tokens = tokenize("a = b != c < d <= e > f >= g");
|
|
103
|
+
assert.strictEqual(tokens[1].type, TokenType.EQUALS);
|
|
104
|
+
assert.strictEqual(tokens[3].type, TokenType.NOT_EQUALS);
|
|
105
|
+
assert.strictEqual(tokens[5].type, TokenType.LT);
|
|
106
|
+
assert.strictEqual(tokens[7].type, TokenType.LTE);
|
|
107
|
+
assert.strictEqual(tokens[9].type, TokenType.GT);
|
|
108
|
+
assert.strictEqual(tokens[11].type, TokenType.GTE);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should tokenize logical operators", () => {
|
|
112
|
+
const tokens = tokenize("a and b or c");
|
|
113
|
+
assert.strictEqual(tokens[0].type, TokenType.NAME);
|
|
114
|
+
assert.strictEqual(tokens[1].type, TokenType.AND);
|
|
115
|
+
assert.strictEqual(tokens[2].type, TokenType.NAME);
|
|
116
|
+
assert.strictEqual(tokens[3].type, TokenType.OR);
|
|
117
|
+
assert.strictEqual(tokens[4].type, TokenType.NAME);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should tokenize div and mod", () => {
|
|
121
|
+
const tokens = tokenize("10 div 3 mod 2");
|
|
122
|
+
assert.strictEqual(tokens[1].type, TokenType.DIV);
|
|
123
|
+
assert.strictEqual(tokens[3].type, TokenType.MOD);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should tokenize union operator", () => {
|
|
127
|
+
const tokens = tokenize("a | b");
|
|
128
|
+
assert.strictEqual(tokens[1].type, TokenType.PIPE);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("Literals", () => {
|
|
133
|
+
it("should tokenize double-quoted string", () => {
|
|
134
|
+
const tokens = tokenize('"hello world"');
|
|
135
|
+
assert.strictEqual(tokens[0].type, TokenType.LITERAL);
|
|
136
|
+
assert.strictEqual(tokens[0].value, "hello world");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should tokenize single-quoted string", () => {
|
|
140
|
+
const tokens = tokenize("'hello world'");
|
|
141
|
+
assert.strictEqual(tokens[0].type, TokenType.LITERAL);
|
|
142
|
+
assert.strictEqual(tokens[0].value, "hello world");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should tokenize integer", () => {
|
|
146
|
+
const tokens = tokenize("42");
|
|
147
|
+
assert.strictEqual(tokens[0].type, TokenType.NUMBER);
|
|
148
|
+
assert.strictEqual(tokens[0].value, 42);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should tokenize decimal number", () => {
|
|
152
|
+
const tokens = tokenize("3.14");
|
|
153
|
+
assert.strictEqual(tokens[0].type, TokenType.NUMBER);
|
|
154
|
+
assert.strictEqual(tokens[0].value, 3.14);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should tokenize decimal starting with dot", () => {
|
|
158
|
+
const tokens = tokenize(".5");
|
|
159
|
+
assert.strictEqual(tokens[0].type, TokenType.NUMBER);
|
|
160
|
+
assert.strictEqual(tokens[0].value, 0.5);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("Functions", () => {
|
|
165
|
+
it("should tokenize function call", () => {
|
|
166
|
+
const tokens = tokenize("contains(a, b)");
|
|
167
|
+
assert.strictEqual(tokens[0].type, TokenType.FUNCTION);
|
|
168
|
+
assert.strictEqual(tokens[0].value, "contains");
|
|
169
|
+
assert.strictEqual(tokens[1].type, TokenType.LPAREN);
|
|
170
|
+
assert.strictEqual(tokens[2].type, TokenType.NAME);
|
|
171
|
+
assert.strictEqual(tokens[3].type, TokenType.COMMA);
|
|
172
|
+
assert.strictEqual(tokens[4].type, TokenType.NAME);
|
|
173
|
+
assert.strictEqual(tokens[5].type, TokenType.RPAREN);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should tokenize node type test", () => {
|
|
177
|
+
const tokens = tokenize("text()");
|
|
178
|
+
assert.strictEqual(tokens[0].type, TokenType.NODE_TYPE);
|
|
179
|
+
assert.strictEqual(tokens[0].value, "text");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should tokenize processing-instruction test", () => {
|
|
183
|
+
const tokens = tokenize("processing-instruction()");
|
|
184
|
+
assert.strictEqual(tokens[0].type, TokenType.NODE_TYPE);
|
|
185
|
+
assert.strictEqual(tokens[0].value, "processing-instruction");
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("Axes", () => {
|
|
190
|
+
it("should tokenize axis with double colon", () => {
|
|
191
|
+
const tokens = tokenize("child::foo");
|
|
192
|
+
assert.strictEqual(tokens[0].type, TokenType.AXIS);
|
|
193
|
+
assert.strictEqual(tokens[0].value, "child");
|
|
194
|
+
assert.strictEqual(tokens[1].type, TokenType.DOUBLE_COLON);
|
|
195
|
+
assert.strictEqual(tokens[2].type, TokenType.NAME);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should tokenize descendant-or-self axis", () => {
|
|
199
|
+
const tokens = tokenize("descendant-or-self::node()");
|
|
200
|
+
assert.strictEqual(tokens[0].type, TokenType.AXIS);
|
|
201
|
+
assert.strictEqual(tokens[0].value, "descendant-or-self");
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe("Variables", () => {
|
|
206
|
+
it("should tokenize variable reference", () => {
|
|
207
|
+
const tokens = tokenize("$myVar");
|
|
208
|
+
assert.strictEqual(tokens[0].type, TokenType.DOLLAR);
|
|
209
|
+
assert.strictEqual(tokens[1].type, TokenType.NAME);
|
|
210
|
+
assert.strictEqual(tokens[1].value, "myVar");
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
describe("Complex expressions", () => {
|
|
215
|
+
it("should tokenize complex XPath expression", () => {
|
|
216
|
+
const expr = '//div[@class="content"]/p[position() > 1 and @id]';
|
|
217
|
+
const tokens = tokenize(expr);
|
|
218
|
+
|
|
219
|
+
// Verify tokenization completed without error
|
|
220
|
+
assert.ok(tokens.length > 0);
|
|
221
|
+
assert.strictEqual(tokens[tokens.length - 1].type, TokenType.EOF);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
});
|