@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.
@@ -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
+ }