@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,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XPath 1.0 Module
|
|
3
|
+
* Based on W3C XPath 1.0 Specification: http://www.w3.org/TR/1999/REC-xpath-19991116
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
export { TokenType, Token, XPathTokenizer, tokenize } from "./tokenizer.js";
|
|
9
|
+
export { NodeType, ASTNode, XPathParser, parse } from "./parser.js";
|
|
10
|
+
export {
|
|
11
|
+
XPathResultType,
|
|
12
|
+
XPathContext,
|
|
13
|
+
XPathEvaluator,
|
|
14
|
+
XPathLimits,
|
|
15
|
+
} from "./evaluator.js";
|
|
16
|
+
|
|
17
|
+
import { parse } from "./parser.js";
|
|
18
|
+
import { XPathEvaluator, XPathContext } from "./evaluator.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Evaluate an XPath expression against a node
|
|
22
|
+
*
|
|
23
|
+
* @param {string} expression - XPath expression
|
|
24
|
+
* @param {Node} contextNode - Context node
|
|
25
|
+
* @param {Object} options - Options (variables, namespaces)
|
|
26
|
+
* @returns {*} Evaluation result
|
|
27
|
+
*/
|
|
28
|
+
export function evaluate(expression, contextNode, options = {}) {
|
|
29
|
+
const ast = parse(expression);
|
|
30
|
+
const evaluator = new XPathEvaluator();
|
|
31
|
+
const context = new XPathContext(
|
|
32
|
+
contextNode,
|
|
33
|
+
1,
|
|
34
|
+
1,
|
|
35
|
+
options.variables || {},
|
|
36
|
+
options.namespaces || {},
|
|
37
|
+
);
|
|
38
|
+
return evaluator.evaluate(ast, context);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Select nodes matching an XPath expression
|
|
43
|
+
*
|
|
44
|
+
* @param {string} expression - XPath expression
|
|
45
|
+
* @param {Node} contextNode - Context node
|
|
46
|
+
* @param {Object} options - Options
|
|
47
|
+
* @returns {Node[]} Matching nodes
|
|
48
|
+
*/
|
|
49
|
+
export function select(expression, contextNode, options = {}) {
|
|
50
|
+
const result = evaluate(expression, contextNode, options);
|
|
51
|
+
if (Array.isArray(result)) return result;
|
|
52
|
+
if (result && result.nodeType) return [result];
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Select first node matching an XPath expression
|
|
58
|
+
*
|
|
59
|
+
* @param {string} expression - XPath expression
|
|
60
|
+
* @param {Node} contextNode - Context node
|
|
61
|
+
* @param {Object} options - Options
|
|
62
|
+
* @returns {Node|null} First matching node or null
|
|
63
|
+
*/
|
|
64
|
+
export function selectFirst(expression, contextNode, options = {}) {
|
|
65
|
+
const nodes = select(expression, contextNode, options);
|
|
66
|
+
return nodes.length > 0 ? nodes[0] : null;
|
|
67
|
+
}
|
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XPath 1.0 Parser
|
|
3
|
+
* Based on W3C XPath 1.0 Specification: http://www.w3.org/TR/1999/REC-xpath-19991116
|
|
4
|
+
*
|
|
5
|
+
* Converts token stream into an Abstract Syntax Tree (AST).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
import { TokenType, tokenize } from "./tokenizer.js";
|
|
11
|
+
|
|
12
|
+
export const NodeType = {
|
|
13
|
+
// Expression types
|
|
14
|
+
OR_EXPR: "OrExpr",
|
|
15
|
+
AND_EXPR: "AndExpr",
|
|
16
|
+
EQUALITY_EXPR: "EqualityExpr",
|
|
17
|
+
RELATIONAL_EXPR: "RelationalExpr",
|
|
18
|
+
ADDITIVE_EXPR: "AdditiveExpr",
|
|
19
|
+
MULTIPLICATIVE_EXPR: "MultiplicativeExpr",
|
|
20
|
+
UNARY_EXPR: "UnaryExpr",
|
|
21
|
+
UNION_EXPR: "UnionExpr",
|
|
22
|
+
|
|
23
|
+
// Path expressions
|
|
24
|
+
PATH_EXPR: "PathExpr",
|
|
25
|
+
LOCATION_PATH: "LocationPath",
|
|
26
|
+
STEP: "Step",
|
|
27
|
+
PREDICATE: "Predicate",
|
|
28
|
+
|
|
29
|
+
// Primaries
|
|
30
|
+
VARIABLE_REF: "VariableRef",
|
|
31
|
+
LITERAL: "Literal",
|
|
32
|
+
NUMBER: "Number",
|
|
33
|
+
FUNCTION_CALL: "FunctionCall",
|
|
34
|
+
|
|
35
|
+
// Node tests
|
|
36
|
+
NAME_TEST: "NameTest",
|
|
37
|
+
NODE_TYPE_TEST: "NodeTypeTest",
|
|
38
|
+
PI_TEST: "ProcessingInstructionTest",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export class ASTNode {
|
|
42
|
+
constructor(type, props = {}) {
|
|
43
|
+
this.type = type;
|
|
44
|
+
Object.assign(this, props);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class XPathParser {
|
|
49
|
+
constructor(tokens) {
|
|
50
|
+
this.tokens = tokens;
|
|
51
|
+
this.position = 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
parse() {
|
|
55
|
+
const expr = this.parseExpr();
|
|
56
|
+
if (!this.isAtEnd()) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Unexpected token ${this.peek().type} at position ${this.peek().position}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return expr;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Expr ::= OrExpr
|
|
65
|
+
parseExpr() {
|
|
66
|
+
return this.parseOrExpr();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// OrExpr ::= AndExpr | OrExpr 'or' AndExpr
|
|
70
|
+
parseOrExpr() {
|
|
71
|
+
let left = this.parseAndExpr();
|
|
72
|
+
|
|
73
|
+
while (this.match(TokenType.OR)) {
|
|
74
|
+
const right = this.parseAndExpr();
|
|
75
|
+
left = new ASTNode(NodeType.OR_EXPR, { left, right });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return left;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// AndExpr ::= EqualityExpr | AndExpr 'and' EqualityExpr
|
|
82
|
+
parseAndExpr() {
|
|
83
|
+
let left = this.parseEqualityExpr();
|
|
84
|
+
|
|
85
|
+
while (this.match(TokenType.AND)) {
|
|
86
|
+
const right = this.parseEqualityExpr();
|
|
87
|
+
left = new ASTNode(NodeType.AND_EXPR, { left, right });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return left;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// EqualityExpr ::= RelationalExpr | EqualityExpr '=' RelationalExpr | EqualityExpr '!=' RelationalExpr
|
|
94
|
+
parseEqualityExpr() {
|
|
95
|
+
let left = this.parseRelationalExpr();
|
|
96
|
+
|
|
97
|
+
while (true) {
|
|
98
|
+
if (this.match(TokenType.EQUALS)) {
|
|
99
|
+
const right = this.parseRelationalExpr();
|
|
100
|
+
left = new ASTNode(NodeType.EQUALITY_EXPR, {
|
|
101
|
+
operator: "=",
|
|
102
|
+
left,
|
|
103
|
+
right,
|
|
104
|
+
});
|
|
105
|
+
} else if (this.match(TokenType.NOT_EQUALS)) {
|
|
106
|
+
const right = this.parseRelationalExpr();
|
|
107
|
+
left = new ASTNode(NodeType.EQUALITY_EXPR, {
|
|
108
|
+
operator: "!=",
|
|
109
|
+
left,
|
|
110
|
+
right,
|
|
111
|
+
});
|
|
112
|
+
} else {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return left;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// RelationalExpr ::= AdditiveExpr | RelationalExpr '<' AdditiveExpr | ...
|
|
121
|
+
parseRelationalExpr() {
|
|
122
|
+
let left = this.parseAdditiveExpr();
|
|
123
|
+
|
|
124
|
+
while (true) {
|
|
125
|
+
if (this.match(TokenType.LT)) {
|
|
126
|
+
const right = this.parseAdditiveExpr();
|
|
127
|
+
left = new ASTNode(NodeType.RELATIONAL_EXPR, {
|
|
128
|
+
operator: "<",
|
|
129
|
+
left,
|
|
130
|
+
right,
|
|
131
|
+
});
|
|
132
|
+
} else if (this.match(TokenType.LTE)) {
|
|
133
|
+
const right = this.parseAdditiveExpr();
|
|
134
|
+
left = new ASTNode(NodeType.RELATIONAL_EXPR, {
|
|
135
|
+
operator: "<=",
|
|
136
|
+
left,
|
|
137
|
+
right,
|
|
138
|
+
});
|
|
139
|
+
} else if (this.match(TokenType.GT)) {
|
|
140
|
+
const right = this.parseAdditiveExpr();
|
|
141
|
+
left = new ASTNode(NodeType.RELATIONAL_EXPR, {
|
|
142
|
+
operator: ">",
|
|
143
|
+
left,
|
|
144
|
+
right,
|
|
145
|
+
});
|
|
146
|
+
} else if (this.match(TokenType.GTE)) {
|
|
147
|
+
const right = this.parseAdditiveExpr();
|
|
148
|
+
left = new ASTNode(NodeType.RELATIONAL_EXPR, {
|
|
149
|
+
operator: ">=",
|
|
150
|
+
left,
|
|
151
|
+
right,
|
|
152
|
+
});
|
|
153
|
+
} else {
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return left;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// AdditiveExpr ::= MultiplicativeExpr | AdditiveExpr '+' MultiplicativeExpr | AdditiveExpr '-' MultiplicativeExpr
|
|
162
|
+
parseAdditiveExpr() {
|
|
163
|
+
let left = this.parseMultiplicativeExpr();
|
|
164
|
+
|
|
165
|
+
while (true) {
|
|
166
|
+
if (this.match(TokenType.PLUS)) {
|
|
167
|
+
const right = this.parseMultiplicativeExpr();
|
|
168
|
+
left = new ASTNode(NodeType.ADDITIVE_EXPR, {
|
|
169
|
+
operator: "+",
|
|
170
|
+
left,
|
|
171
|
+
right,
|
|
172
|
+
});
|
|
173
|
+
} else if (this.match(TokenType.MINUS)) {
|
|
174
|
+
const right = this.parseMultiplicativeExpr();
|
|
175
|
+
left = new ASTNode(NodeType.ADDITIVE_EXPR, {
|
|
176
|
+
operator: "-",
|
|
177
|
+
left,
|
|
178
|
+
right,
|
|
179
|
+
});
|
|
180
|
+
} else {
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return left;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// MultiplicativeExpr ::= UnaryExpr | MultiplicativeExpr '*' UnaryExpr | ...
|
|
189
|
+
parseMultiplicativeExpr() {
|
|
190
|
+
let left = this.parseUnaryExpr();
|
|
191
|
+
|
|
192
|
+
while (true) {
|
|
193
|
+
if (this.match(TokenType.STAR)) {
|
|
194
|
+
const right = this.parseUnaryExpr();
|
|
195
|
+
left = new ASTNode(NodeType.MULTIPLICATIVE_EXPR, {
|
|
196
|
+
operator: "*",
|
|
197
|
+
left,
|
|
198
|
+
right,
|
|
199
|
+
});
|
|
200
|
+
} else if (this.match(TokenType.DIV)) {
|
|
201
|
+
const right = this.parseUnaryExpr();
|
|
202
|
+
left = new ASTNode(NodeType.MULTIPLICATIVE_EXPR, {
|
|
203
|
+
operator: "div",
|
|
204
|
+
left,
|
|
205
|
+
right,
|
|
206
|
+
});
|
|
207
|
+
} else if (this.match(TokenType.MOD)) {
|
|
208
|
+
const right = this.parseUnaryExpr();
|
|
209
|
+
left = new ASTNode(NodeType.MULTIPLICATIVE_EXPR, {
|
|
210
|
+
operator: "mod",
|
|
211
|
+
left,
|
|
212
|
+
right,
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return left;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// UnaryExpr ::= UnionExpr | '-' UnaryExpr
|
|
223
|
+
parseUnaryExpr() {
|
|
224
|
+
if (this.match(TokenType.MINUS)) {
|
|
225
|
+
const operand = this.parseUnaryExpr();
|
|
226
|
+
return new ASTNode(NodeType.UNARY_EXPR, { operator: "-", operand });
|
|
227
|
+
}
|
|
228
|
+
return this.parseUnionExpr();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// UnionExpr ::= PathExpr | UnionExpr '|' PathExpr
|
|
232
|
+
parseUnionExpr() {
|
|
233
|
+
let left = this.parsePathExpr();
|
|
234
|
+
|
|
235
|
+
while (this.match(TokenType.PIPE)) {
|
|
236
|
+
const right = this.parsePathExpr();
|
|
237
|
+
left = new ASTNode(NodeType.UNION_EXPR, { left, right });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return left;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// PathExpr ::= LocationPath | FilterExpr | FilterExpr '/' RelativeLocationPath | FilterExpr '//' RelativeLocationPath
|
|
244
|
+
parsePathExpr() {
|
|
245
|
+
// Check for absolute location path
|
|
246
|
+
if (this.check(TokenType.SLASH) || this.check(TokenType.DOUBLE_SLASH)) {
|
|
247
|
+
return this.parseLocationPath();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check if this looks like a location path starting with step
|
|
251
|
+
if (this.isStepStart()) {
|
|
252
|
+
return this.parseLocationPath();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Otherwise try to parse as filter expression
|
|
256
|
+
const filter = this.parseFilterExpr();
|
|
257
|
+
|
|
258
|
+
// Check for path continuation
|
|
259
|
+
if (this.check(TokenType.SLASH) || this.check(TokenType.DOUBLE_SLASH)) {
|
|
260
|
+
const steps = [];
|
|
261
|
+
|
|
262
|
+
while (
|
|
263
|
+
this.check(TokenType.SLASH) ||
|
|
264
|
+
this.check(TokenType.DOUBLE_SLASH)
|
|
265
|
+
) {
|
|
266
|
+
const isDescendant = this.match(TokenType.DOUBLE_SLASH);
|
|
267
|
+
if (!isDescendant) this.advance(); // consume /
|
|
268
|
+
|
|
269
|
+
if (isDescendant) {
|
|
270
|
+
steps.push(
|
|
271
|
+
new ASTNode(NodeType.STEP, {
|
|
272
|
+
axis: "descendant-or-self",
|
|
273
|
+
nodeTest: new ASTNode(NodeType.NODE_TYPE_TEST, {
|
|
274
|
+
nodeType: "node",
|
|
275
|
+
}),
|
|
276
|
+
predicates: [],
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
steps.push(this.parseStep());
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return new ASTNode(NodeType.PATH_EXPR, { filter, steps });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return filter;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// LocationPath ::= RelativeLocationPath | AbsoluteLocationPath
|
|
291
|
+
parseLocationPath() {
|
|
292
|
+
let absolute = false;
|
|
293
|
+
const steps = [];
|
|
294
|
+
|
|
295
|
+
if (this.match(TokenType.DOUBLE_SLASH)) {
|
|
296
|
+
absolute = true;
|
|
297
|
+
// //foo is equivalent to /descendant-or-self::node()/foo
|
|
298
|
+
steps.push(
|
|
299
|
+
new ASTNode(NodeType.STEP, {
|
|
300
|
+
axis: "descendant-or-self",
|
|
301
|
+
nodeTest: new ASTNode(NodeType.NODE_TYPE_TEST, { nodeType: "node" }),
|
|
302
|
+
predicates: [],
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
if (!this.isAtEnd() && !this.check(TokenType.EOF)) {
|
|
306
|
+
steps.push(this.parseStep());
|
|
307
|
+
}
|
|
308
|
+
} else if (this.match(TokenType.SLASH)) {
|
|
309
|
+
absolute = true;
|
|
310
|
+
if (this.isStepStart()) {
|
|
311
|
+
steps.push(this.parseStep());
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
steps.push(this.parseStep());
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
while (this.check(TokenType.SLASH) || this.check(TokenType.DOUBLE_SLASH)) {
|
|
318
|
+
const isDescendant = this.match(TokenType.DOUBLE_SLASH);
|
|
319
|
+
if (!isDescendant) this.advance(); // consume /
|
|
320
|
+
|
|
321
|
+
if (isDescendant) {
|
|
322
|
+
steps.push(
|
|
323
|
+
new ASTNode(NodeType.STEP, {
|
|
324
|
+
axis: "descendant-or-self",
|
|
325
|
+
nodeTest: new ASTNode(NodeType.NODE_TYPE_TEST, {
|
|
326
|
+
nodeType: "node",
|
|
327
|
+
}),
|
|
328
|
+
predicates: [],
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
steps.push(this.parseStep());
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return new ASTNode(NodeType.LOCATION_PATH, { absolute, steps });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Step ::= AxisSpecifier NodeTest Predicate* | AbbreviatedStep
|
|
340
|
+
parseStep() {
|
|
341
|
+
// Handle abbreviated steps
|
|
342
|
+
if (this.match(TokenType.DOT)) {
|
|
343
|
+
return new ASTNode(NodeType.STEP, {
|
|
344
|
+
axis: "self",
|
|
345
|
+
nodeTest: new ASTNode(NodeType.NODE_TYPE_TEST, { nodeType: "node" }),
|
|
346
|
+
predicates: [],
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (this.match(TokenType.DOUBLE_DOT)) {
|
|
351
|
+
return new ASTNode(NodeType.STEP, {
|
|
352
|
+
axis: "parent",
|
|
353
|
+
nodeTest: new ASTNode(NodeType.NODE_TYPE_TEST, { nodeType: "node" }),
|
|
354
|
+
predicates: [],
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Parse axis
|
|
359
|
+
let axis = "child"; // default axis
|
|
360
|
+
|
|
361
|
+
if (this.match(TokenType.AT)) {
|
|
362
|
+
axis = "attribute";
|
|
363
|
+
} else if (this.check(TokenType.AXIS)) {
|
|
364
|
+
axis = this.advance().value;
|
|
365
|
+
this.expect(TokenType.DOUBLE_COLON);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Parse node test
|
|
369
|
+
const nodeTest = this.parseNodeTest();
|
|
370
|
+
|
|
371
|
+
// Parse predicates
|
|
372
|
+
const predicates = [];
|
|
373
|
+
while (this.check(TokenType.LBRACKET)) {
|
|
374
|
+
predicates.push(this.parsePredicate());
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return new ASTNode(NodeType.STEP, { axis, nodeTest, predicates });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// NodeTest ::= NameTest | NodeType '(' ')' | 'processing-instruction' '(' Literal ')'
|
|
381
|
+
parseNodeTest() {
|
|
382
|
+
if (this.check(TokenType.NODE_TYPE)) {
|
|
383
|
+
const nodeType = this.advance().value;
|
|
384
|
+
this.expect(TokenType.LPAREN);
|
|
385
|
+
|
|
386
|
+
if (
|
|
387
|
+
nodeType === "processing-instruction" &&
|
|
388
|
+
this.check(TokenType.LITERAL)
|
|
389
|
+
) {
|
|
390
|
+
const name = this.advance().value;
|
|
391
|
+
this.expect(TokenType.RPAREN);
|
|
392
|
+
return new ASTNode(NodeType.PI_TEST, { name });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
this.expect(TokenType.RPAREN);
|
|
396
|
+
return new ASTNode(NodeType.NODE_TYPE_TEST, { nodeType });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// NameTest
|
|
400
|
+
return this.parseNameTest();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// NameTest ::= '*' | NCName ':' '*' | QName
|
|
404
|
+
parseNameTest() {
|
|
405
|
+
if (this.match(TokenType.STAR)) {
|
|
406
|
+
return new ASTNode(NodeType.NAME_TEST, { name: "*", prefix: null });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!this.check(TokenType.NAME)) {
|
|
410
|
+
throw new Error(`Expected name at position ${this.peek().position}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const name = this.advance().value;
|
|
414
|
+
|
|
415
|
+
// Check for prefix:*
|
|
416
|
+
if (this.match(TokenType.COLON)) {
|
|
417
|
+
if (this.match(TokenType.STAR)) {
|
|
418
|
+
return new ASTNode(NodeType.NAME_TEST, { name: "*", prefix: name });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (this.check(TokenType.NAME)) {
|
|
422
|
+
const localName = this.advance().value;
|
|
423
|
+
return new ASTNode(NodeType.NAME_TEST, {
|
|
424
|
+
name: localName,
|
|
425
|
+
prefix: name,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
throw new Error(
|
|
430
|
+
`Expected name or * after : at position ${this.peek().position}`,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return new ASTNode(NodeType.NAME_TEST, { name, prefix: null });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Predicate ::= '[' Expr ']'
|
|
438
|
+
parsePredicate() {
|
|
439
|
+
this.expect(TokenType.LBRACKET);
|
|
440
|
+
const expr = this.parseExpr();
|
|
441
|
+
this.expect(TokenType.RBRACKET);
|
|
442
|
+
return new ASTNode(NodeType.PREDICATE, { expr });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// FilterExpr ::= PrimaryExpr | FilterExpr Predicate
|
|
446
|
+
parseFilterExpr() {
|
|
447
|
+
let primary = this.parsePrimaryExpr();
|
|
448
|
+
|
|
449
|
+
while (this.check(TokenType.LBRACKET)) {
|
|
450
|
+
const predicate = this.parsePredicate();
|
|
451
|
+
primary = new ASTNode(NodeType.PATH_EXPR, {
|
|
452
|
+
filter: primary,
|
|
453
|
+
predicates: [predicate],
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return primary;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// PrimaryExpr ::= VariableReference | '(' Expr ')' | Literal | Number | FunctionCall
|
|
461
|
+
parsePrimaryExpr() {
|
|
462
|
+
// Variable reference
|
|
463
|
+
if (this.match(TokenType.DOLLAR)) {
|
|
464
|
+
if (!this.check(TokenType.NAME)) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
`Expected variable name at position ${this.peek().position}`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const name = this.advance().value;
|
|
470
|
+
let prefix = null;
|
|
471
|
+
|
|
472
|
+
if (this.match(TokenType.COLON)) {
|
|
473
|
+
prefix = name;
|
|
474
|
+
if (!this.check(TokenType.NAME)) {
|
|
475
|
+
throw new Error(
|
|
476
|
+
`Expected local name at position ${this.peek().position}`,
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
const localName = this.advance().value;
|
|
480
|
+
return new ASTNode(NodeType.VARIABLE_REF, { name: localName, prefix });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return new ASTNode(NodeType.VARIABLE_REF, { name, prefix });
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Parenthesized expression
|
|
487
|
+
if (this.match(TokenType.LPAREN)) {
|
|
488
|
+
const expr = this.parseExpr();
|
|
489
|
+
this.expect(TokenType.RPAREN);
|
|
490
|
+
return expr;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Literal
|
|
494
|
+
if (this.check(TokenType.LITERAL)) {
|
|
495
|
+
return new ASTNode(NodeType.LITERAL, { value: this.advance().value });
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Number
|
|
499
|
+
if (this.check(TokenType.NUMBER)) {
|
|
500
|
+
return new ASTNode(NodeType.NUMBER, { value: this.advance().value });
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Function call
|
|
504
|
+
if (this.check(TokenType.FUNCTION)) {
|
|
505
|
+
return this.parseFunctionCall();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
throw new Error(
|
|
509
|
+
`Unexpected token ${this.peek().type} at position ${this.peek().position}`,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// FunctionCall ::= FunctionName '(' ( Argument ( ',' Argument )* )? ')'
|
|
514
|
+
// Note: Prefixed function calls (prefix:fn()) are not supported because
|
|
515
|
+
// the tokenizer identifies functions by NAME followed by '(' - prefixed
|
|
516
|
+
// names like 'fn:name()' are tokenized as NAME:NAME() which is parsed
|
|
517
|
+
// as a location path, not a function call.
|
|
518
|
+
parseFunctionCall() {
|
|
519
|
+
const name = this.advance().value;
|
|
520
|
+
return this.parseFunctionCallArgs(name, null);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
parseFunctionCallArgs(name, prefix) {
|
|
524
|
+
this.expect(TokenType.LPAREN);
|
|
525
|
+
|
|
526
|
+
const args = [];
|
|
527
|
+
if (!this.check(TokenType.RPAREN)) {
|
|
528
|
+
args.push(this.parseExpr());
|
|
529
|
+
while (this.match(TokenType.COMMA)) {
|
|
530
|
+
args.push(this.parseExpr());
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
this.expect(TokenType.RPAREN);
|
|
535
|
+
|
|
536
|
+
return new ASTNode(NodeType.FUNCTION_CALL, { name, prefix, args });
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Helper methods
|
|
540
|
+
isStepStart() {
|
|
541
|
+
const type = this.peek().type;
|
|
542
|
+
return (
|
|
543
|
+
type === TokenType.NAME ||
|
|
544
|
+
type === TokenType.STAR ||
|
|
545
|
+
type === TokenType.AT ||
|
|
546
|
+
type === TokenType.DOT ||
|
|
547
|
+
type === TokenType.DOUBLE_DOT ||
|
|
548
|
+
type === TokenType.AXIS ||
|
|
549
|
+
type === TokenType.NODE_TYPE
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
peek() {
|
|
554
|
+
return this.tokens[this.position];
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
advance() {
|
|
558
|
+
if (!this.isAtEnd()) {
|
|
559
|
+
return this.tokens[this.position++];
|
|
560
|
+
}
|
|
561
|
+
return this.tokens[this.position];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
check(type) {
|
|
565
|
+
if (this.isAtEnd()) return false;
|
|
566
|
+
return this.peek().type === type;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
match(type) {
|
|
570
|
+
if (this.check(type)) {
|
|
571
|
+
this.advance();
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
expect(type) {
|
|
578
|
+
if (!this.check(type)) {
|
|
579
|
+
throw new Error(
|
|
580
|
+
`Expected ${type} but got ${this.peek().type} at position ${this.peek().position}`,
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
return this.advance();
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
isAtEnd() {
|
|
587
|
+
return this.peek().type === TokenType.EOF;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export function parse(expression) {
|
|
592
|
+
const tokens = tokenize(expression);
|
|
593
|
+
const parser = new XPathParser(tokens);
|
|
594
|
+
return parser.parse();
|
|
595
|
+
}
|