@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,1012 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XPath 1.0 Evaluator
|
|
3
|
+
* Based on W3C XPath 1.0 Specification: http://www.w3.org/TR/1999/REC-xpath-19991116
|
|
4
|
+
*
|
|
5
|
+
* Evaluates XPath AST against DOM nodes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
import { NodeType } from "./parser.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* XPath result types matching W3C spec
|
|
14
|
+
*/
|
|
15
|
+
export const XPathResultType = {
|
|
16
|
+
ANY_TYPE: 0,
|
|
17
|
+
NUMBER_TYPE: 1,
|
|
18
|
+
STRING_TYPE: 2,
|
|
19
|
+
BOOLEAN_TYPE: 3,
|
|
20
|
+
UNORDERED_NODE_ITERATOR_TYPE: 4,
|
|
21
|
+
ORDERED_NODE_ITERATOR_TYPE: 5,
|
|
22
|
+
UNORDERED_NODE_SNAPSHOT_TYPE: 6,
|
|
23
|
+
ORDERED_NODE_SNAPSHOT_TYPE: 7,
|
|
24
|
+
ANY_UNORDERED_NODE_TYPE: 8,
|
|
25
|
+
FIRST_ORDERED_NODE_TYPE: 9,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Security limits to prevent DoS attacks
|
|
30
|
+
*/
|
|
31
|
+
export const XPathLimits = {
|
|
32
|
+
MAX_RECURSION_DEPTH: 100,
|
|
33
|
+
MAX_RESULT_SIZE: 10000,
|
|
34
|
+
MAX_STRING_LENGTH: 1000000,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Forbidden variable names to prevent prototype pollution
|
|
39
|
+
*/
|
|
40
|
+
const FORBIDDEN_VARIABLE_NAMES = Object.freeze([
|
|
41
|
+
"__proto__",
|
|
42
|
+
"constructor",
|
|
43
|
+
"prototype",
|
|
44
|
+
"__defineGetter__",
|
|
45
|
+
"__defineSetter__",
|
|
46
|
+
"__lookupGetter__",
|
|
47
|
+
"__lookupSetter__",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* XPath evaluation context
|
|
52
|
+
*/
|
|
53
|
+
export class XPathContext {
|
|
54
|
+
constructor(node, position = 1, size = 1, variables = {}, namespaces = {}) {
|
|
55
|
+
this.node = node;
|
|
56
|
+
this.position = position;
|
|
57
|
+
this.size = size;
|
|
58
|
+
this.variables = variables;
|
|
59
|
+
this.namespaces = namespaces;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
clone(overrides = {}) {
|
|
63
|
+
return new XPathContext(
|
|
64
|
+
overrides.node ?? this.node,
|
|
65
|
+
overrides.position ?? this.position,
|
|
66
|
+
overrides.size ?? this.size,
|
|
67
|
+
overrides.variables ?? this.variables,
|
|
68
|
+
overrides.namespaces ?? this.namespaces,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* XPath Evaluator
|
|
75
|
+
*/
|
|
76
|
+
export class XPathEvaluator {
|
|
77
|
+
constructor(options = {}) {
|
|
78
|
+
this.functions = this.initCoreFunctions();
|
|
79
|
+
this.maxRecursionDepth =
|
|
80
|
+
options.maxRecursionDepth ?? XPathLimits.MAX_RECURSION_DEPTH;
|
|
81
|
+
this.maxResultSize = options.maxResultSize ?? XPathLimits.MAX_RESULT_SIZE;
|
|
82
|
+
this.maxStringLength =
|
|
83
|
+
options.maxStringLength ?? XPathLimits.MAX_STRING_LENGTH;
|
|
84
|
+
this.recursionDepth = 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Evaluate XPath expression against a context
|
|
89
|
+
* @throws {Error} If recursion depth exceeds limit
|
|
90
|
+
* @throws {Error} If AST is invalid
|
|
91
|
+
*/
|
|
92
|
+
evaluate(ast, context) {
|
|
93
|
+
if (!ast || typeof ast !== "object") {
|
|
94
|
+
throw new Error("Invalid AST: expected object");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!ast.type) {
|
|
98
|
+
throw new Error("Invalid AST: missing type property");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.recursionDepth++;
|
|
102
|
+
if (this.recursionDepth > this.maxRecursionDepth) {
|
|
103
|
+
this.recursionDepth = 0;
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Maximum recursion depth exceeded (${this.maxRecursionDepth})`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
return this.evaluateInternal(ast, context);
|
|
111
|
+
} finally {
|
|
112
|
+
this.recursionDepth--;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Internal evaluation (after validation)
|
|
118
|
+
*/
|
|
119
|
+
evaluateInternal(ast, context) {
|
|
120
|
+
switch (ast.type) {
|
|
121
|
+
case NodeType.OR_EXPR:
|
|
122
|
+
return this.evalOrExpr(ast, context);
|
|
123
|
+
case NodeType.AND_EXPR:
|
|
124
|
+
return this.evalAndExpr(ast, context);
|
|
125
|
+
case NodeType.EQUALITY_EXPR:
|
|
126
|
+
return this.evalEqualityExpr(ast, context);
|
|
127
|
+
case NodeType.RELATIONAL_EXPR:
|
|
128
|
+
return this.evalRelationalExpr(ast, context);
|
|
129
|
+
case NodeType.ADDITIVE_EXPR:
|
|
130
|
+
return this.evalAdditiveExpr(ast, context);
|
|
131
|
+
case NodeType.MULTIPLICATIVE_EXPR:
|
|
132
|
+
return this.evalMultiplicativeExpr(ast, context);
|
|
133
|
+
case NodeType.UNARY_EXPR:
|
|
134
|
+
return this.evalUnaryExpr(ast, context);
|
|
135
|
+
case NodeType.UNION_EXPR:
|
|
136
|
+
return this.evalUnionExpr(ast, context);
|
|
137
|
+
case NodeType.PATH_EXPR:
|
|
138
|
+
return this.evalPathExpr(ast, context);
|
|
139
|
+
case NodeType.LOCATION_PATH:
|
|
140
|
+
return this.evalLocationPath(ast, context);
|
|
141
|
+
case NodeType.VARIABLE_REF:
|
|
142
|
+
return this.evalVariableRef(ast, context);
|
|
143
|
+
case NodeType.LITERAL:
|
|
144
|
+
return this.validateString(ast.value);
|
|
145
|
+
case NodeType.NUMBER:
|
|
146
|
+
return ast.value;
|
|
147
|
+
case NodeType.FUNCTION_CALL:
|
|
148
|
+
return this.evalFunctionCall(ast, context);
|
|
149
|
+
default:
|
|
150
|
+
throw new Error(`Unknown AST node type: ${String(ast.type)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Validate and limit string length
|
|
156
|
+
*/
|
|
157
|
+
validateString(str) {
|
|
158
|
+
if (typeof str === "string" && str.length > this.maxStringLength) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`String exceeds maximum length (${this.maxStringLength})`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return str;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Validate result size
|
|
168
|
+
*/
|
|
169
|
+
validateResultSize(nodes) {
|
|
170
|
+
if (Array.isArray(nodes) && nodes.length > this.maxResultSize) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Result set exceeds maximum size (${this.maxResultSize})`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return nodes;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
evalOrExpr(ast, context) {
|
|
179
|
+
return (
|
|
180
|
+
this.toBoolean(this.evaluate(ast.left, context)) ||
|
|
181
|
+
this.toBoolean(this.evaluate(ast.right, context))
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
evalAndExpr(ast, context) {
|
|
186
|
+
return (
|
|
187
|
+
this.toBoolean(this.evaluate(ast.left, context)) &&
|
|
188
|
+
this.toBoolean(this.evaluate(ast.right, context))
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
evalEqualityExpr(ast, context) {
|
|
193
|
+
const left = this.evaluate(ast.left, context);
|
|
194
|
+
const right = this.evaluate(ast.right, context);
|
|
195
|
+
const isEqual = this.compareValues(left, right, "=");
|
|
196
|
+
return ast.operator === "=" ? isEqual : !isEqual;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
evalRelationalExpr(ast, context) {
|
|
200
|
+
const left = this.evaluate(ast.left, context);
|
|
201
|
+
const right = this.evaluate(ast.right, context);
|
|
202
|
+
return this.compareValues(left, right, ast.operator);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
evalAdditiveExpr(ast, context) {
|
|
206
|
+
const left = this.toNumber(this.evaluate(ast.left, context));
|
|
207
|
+
const right = this.toNumber(this.evaluate(ast.right, context));
|
|
208
|
+
return ast.operator === "+" ? left + right : left - right;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
evalMultiplicativeExpr(ast, context) {
|
|
212
|
+
const left = this.toNumber(this.evaluate(ast.left, context));
|
|
213
|
+
const right = this.toNumber(this.evaluate(ast.right, context));
|
|
214
|
+
|
|
215
|
+
switch (ast.operator) {
|
|
216
|
+
case "*":
|
|
217
|
+
return left * right;
|
|
218
|
+
case "div":
|
|
219
|
+
return left / right;
|
|
220
|
+
case "mod":
|
|
221
|
+
return left % right;
|
|
222
|
+
default:
|
|
223
|
+
throw new Error(`Unknown multiplicative operator: ${ast.operator}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
evalUnaryExpr(ast, context) {
|
|
228
|
+
const value = this.toNumber(this.evaluate(ast.operand, context));
|
|
229
|
+
return -value;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
evalUnionExpr(ast, context) {
|
|
233
|
+
const left = this.evaluate(ast.left, context);
|
|
234
|
+
const right = this.evaluate(ast.right, context);
|
|
235
|
+
|
|
236
|
+
const leftNodes = Array.isArray(left) ? left : [left];
|
|
237
|
+
const rightNodes = Array.isArray(right) ? right : [right];
|
|
238
|
+
|
|
239
|
+
// Union in document order, no duplicates
|
|
240
|
+
const seen = new Set();
|
|
241
|
+
const result = [];
|
|
242
|
+
|
|
243
|
+
for (const node of [...leftNodes, ...rightNodes]) {
|
|
244
|
+
if (!seen.has(node)) {
|
|
245
|
+
seen.add(node);
|
|
246
|
+
result.push(node);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this.validateResultSize(result);
|
|
251
|
+
return this.sortByDocumentOrder(result);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
evalPathExpr(ast, context) {
|
|
255
|
+
if (ast.filter) {
|
|
256
|
+
let result = this.evaluate(ast.filter, context);
|
|
257
|
+
|
|
258
|
+
if (ast.predicates) {
|
|
259
|
+
for (const pred of ast.predicates) {
|
|
260
|
+
result = this.filterByPredicate(result, pred, context);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (ast.steps) {
|
|
265
|
+
for (const step of ast.steps) {
|
|
266
|
+
result = this.evalStepOnNodes(step, result, context);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
evalLocationPath(ast, context) {
|
|
277
|
+
let nodes;
|
|
278
|
+
|
|
279
|
+
if (ast.absolute) {
|
|
280
|
+
// Start from document root node (not document element)
|
|
281
|
+
// XPath absolute paths start from the document node
|
|
282
|
+
const doc = context.node.ownerDocument || context.node;
|
|
283
|
+
nodes = [doc];
|
|
284
|
+
} else {
|
|
285
|
+
nodes = [context.node];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
for (const step of ast.steps) {
|
|
289
|
+
nodes = this.evalStepOnNodes(step, nodes, context);
|
|
290
|
+
this.validateResultSize(nodes);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return nodes;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
evalStepOnNodes(step, nodes, context) {
|
|
297
|
+
const allNodes = Array.isArray(nodes) ? nodes : [nodes];
|
|
298
|
+
let result = [];
|
|
299
|
+
|
|
300
|
+
for (const node of allNodes) {
|
|
301
|
+
const stepNodes = this.evalStep(step, context.clone({ node }));
|
|
302
|
+
result = result.concat(stepNodes);
|
|
303
|
+
|
|
304
|
+
// Early validation to prevent excessive memory use
|
|
305
|
+
if (result.length > this.maxResultSize * 2) {
|
|
306
|
+
this.validateResultSize(result);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Remove duplicates and sort by document order
|
|
311
|
+
const uniqueResult = [...new Set(result)];
|
|
312
|
+
this.validateResultSize(uniqueResult);
|
|
313
|
+
return this.sortByDocumentOrder(uniqueResult);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
evalStep(step, context) {
|
|
317
|
+
// Get nodes along axis
|
|
318
|
+
let nodes = this.getAxisNodes(step.axis, context.node);
|
|
319
|
+
|
|
320
|
+
// Filter by node test
|
|
321
|
+
nodes = nodes.filter((n) => this.matchNodeTest(step.nodeTest, n, context));
|
|
322
|
+
|
|
323
|
+
// Apply predicates
|
|
324
|
+
for (const predicate of step.predicates) {
|
|
325
|
+
nodes = this.filterByPredicate(nodes, predicate, context);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return nodes;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
getAxisNodes(axis, node) {
|
|
332
|
+
switch (axis) {
|
|
333
|
+
case "child":
|
|
334
|
+
return Array.from(node.childNodes || []);
|
|
335
|
+
|
|
336
|
+
case "parent":
|
|
337
|
+
return node.parentNode ? [node.parentNode] : [];
|
|
338
|
+
|
|
339
|
+
case "self":
|
|
340
|
+
return [node];
|
|
341
|
+
|
|
342
|
+
case "descendant":
|
|
343
|
+
return this.getDescendants(node, false);
|
|
344
|
+
|
|
345
|
+
case "descendant-or-self":
|
|
346
|
+
return this.getDescendants(node, true);
|
|
347
|
+
|
|
348
|
+
case "ancestor":
|
|
349
|
+
return this.getAncestors(node, false);
|
|
350
|
+
|
|
351
|
+
case "ancestor-or-self":
|
|
352
|
+
return this.getAncestors(node, true);
|
|
353
|
+
|
|
354
|
+
case "following-sibling":
|
|
355
|
+
return this.getFollowingSiblings(node);
|
|
356
|
+
|
|
357
|
+
case "preceding-sibling":
|
|
358
|
+
return this.getPrecedingSiblings(node);
|
|
359
|
+
|
|
360
|
+
case "following":
|
|
361
|
+
return this.getFollowing(node);
|
|
362
|
+
|
|
363
|
+
case "preceding":
|
|
364
|
+
return this.getPreceding(node);
|
|
365
|
+
|
|
366
|
+
case "attribute":
|
|
367
|
+
if (node.attributes) {
|
|
368
|
+
return Array.from(node.attributes);
|
|
369
|
+
}
|
|
370
|
+
return [];
|
|
371
|
+
|
|
372
|
+
case "namespace":
|
|
373
|
+
// Namespace axis - not commonly used
|
|
374
|
+
return [];
|
|
375
|
+
|
|
376
|
+
default:
|
|
377
|
+
throw new Error(`Unknown axis: ${axis}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
getDescendants(node, includeSelf) {
|
|
382
|
+
const result = includeSelf ? [node] : [];
|
|
383
|
+
const stack = Array.from(node.childNodes || []).reverse();
|
|
384
|
+
|
|
385
|
+
while (stack.length > 0) {
|
|
386
|
+
const current = stack.pop();
|
|
387
|
+
result.push(current);
|
|
388
|
+
if (current.childNodes) {
|
|
389
|
+
for (let i = current.childNodes.length - 1; i >= 0; i--) {
|
|
390
|
+
stack.push(current.childNodes[i]);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return result;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
getAncestors(node, includeSelf) {
|
|
399
|
+
const result = includeSelf ? [node] : [];
|
|
400
|
+
let current = node.parentNode;
|
|
401
|
+
|
|
402
|
+
while (current) {
|
|
403
|
+
result.push(current);
|
|
404
|
+
current = current.parentNode;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return result;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
getFollowingSiblings(node) {
|
|
411
|
+
const result = [];
|
|
412
|
+
if (!node) return result;
|
|
413
|
+
let current = node.nextSibling;
|
|
414
|
+
|
|
415
|
+
while (current) {
|
|
416
|
+
result.push(current);
|
|
417
|
+
current = current.nextSibling;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return result;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
getPrecedingSiblings(node) {
|
|
424
|
+
const result = [];
|
|
425
|
+
if (!node) return result;
|
|
426
|
+
let current = node.previousSibling;
|
|
427
|
+
|
|
428
|
+
while (current) {
|
|
429
|
+
result.push(current);
|
|
430
|
+
current = current.previousSibling;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return result.reverse();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
getFollowing(node) {
|
|
437
|
+
const result = [];
|
|
438
|
+
let current = node;
|
|
439
|
+
|
|
440
|
+
// Go to next sibling, or ancestor's next sibling
|
|
441
|
+
while (current) {
|
|
442
|
+
if (current.nextSibling) {
|
|
443
|
+
current = current.nextSibling;
|
|
444
|
+
result.push(current);
|
|
445
|
+
// Add all descendants
|
|
446
|
+
result.push(...this.getDescendants(current, false));
|
|
447
|
+
} else {
|
|
448
|
+
current = current.parentNode;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return result;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
getPreceding(node) {
|
|
456
|
+
const result = [];
|
|
457
|
+
let current = node;
|
|
458
|
+
|
|
459
|
+
while (current) {
|
|
460
|
+
if (current.previousSibling) {
|
|
461
|
+
current = current.previousSibling;
|
|
462
|
+
// Add descendants in reverse order, then the node
|
|
463
|
+
const descendants = this.getDescendants(current, false);
|
|
464
|
+
result.unshift(...descendants.reverse());
|
|
465
|
+
result.unshift(current);
|
|
466
|
+
} else {
|
|
467
|
+
current = current.parentNode;
|
|
468
|
+
if (current && current.nodeType !== 9) {
|
|
469
|
+
// Not document
|
|
470
|
+
// Don't add ancestors
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
matchNodeTest(nodeTest, node, context) {
|
|
479
|
+
switch (nodeTest.type) {
|
|
480
|
+
case NodeType.NAME_TEST:
|
|
481
|
+
return this.matchNameTest(nodeTest, node, context);
|
|
482
|
+
|
|
483
|
+
case NodeType.NODE_TYPE_TEST:
|
|
484
|
+
return this.matchNodeTypeTest(nodeTest.nodeType, node);
|
|
485
|
+
|
|
486
|
+
case NodeType.PI_TEST:
|
|
487
|
+
return node.nodeType === 7 && node.nodeName === nodeTest.name;
|
|
488
|
+
|
|
489
|
+
default:
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
matchNameTest(nodeTest, node, context) {
|
|
495
|
+
// Only element and attribute nodes have names
|
|
496
|
+
if (node.nodeType !== 1 && node.nodeType !== 2) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const name = nodeTest.name;
|
|
501
|
+
const prefix = nodeTest.prefix;
|
|
502
|
+
|
|
503
|
+
// Wildcard
|
|
504
|
+
if (name === "*" && !prefix) {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Get node's local name and namespace
|
|
509
|
+
const nodeName = node.localName || node.nodeName;
|
|
510
|
+
const nodeNs = node.namespaceURI || null;
|
|
511
|
+
|
|
512
|
+
// Prefix:* matches all nodes in namespace
|
|
513
|
+
if (name === "*" && prefix) {
|
|
514
|
+
const ns = context.namespaces[prefix];
|
|
515
|
+
return nodeNs === ns;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Simple name match
|
|
519
|
+
if (!prefix) {
|
|
520
|
+
// For elements, match local name (case-insensitive for HTML)
|
|
521
|
+
const doc = node.ownerDocument;
|
|
522
|
+
if (doc && doc.contentType === "text/html" && node.nodeType === 1) {
|
|
523
|
+
return nodeName.toLowerCase() === name.toLowerCase();
|
|
524
|
+
}
|
|
525
|
+
return nodeName === name;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Prefixed name match
|
|
529
|
+
const ns = context.namespaces[prefix];
|
|
530
|
+
return nodeName === name && nodeNs === ns;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
matchNodeTypeTest(nodeType, node) {
|
|
534
|
+
switch (nodeType) {
|
|
535
|
+
case "node":
|
|
536
|
+
return true;
|
|
537
|
+
case "text":
|
|
538
|
+
return node.nodeType === 3;
|
|
539
|
+
case "comment":
|
|
540
|
+
return node.nodeType === 8;
|
|
541
|
+
case "processing-instruction":
|
|
542
|
+
return node.nodeType === 7;
|
|
543
|
+
default:
|
|
544
|
+
return false;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
filterByPredicate(nodes, predicate, context) {
|
|
549
|
+
const result = [];
|
|
550
|
+
const nodeArray = Array.isArray(nodes) ? nodes : [nodes];
|
|
551
|
+
const size = nodeArray.length;
|
|
552
|
+
|
|
553
|
+
for (let i = 0; i < nodeArray.length; i++) {
|
|
554
|
+
const node = nodeArray[i];
|
|
555
|
+
const predicateContext = context.clone({
|
|
556
|
+
node,
|
|
557
|
+
position: i + 1,
|
|
558
|
+
size,
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const value = this.evaluate(predicate.expr, predicateContext);
|
|
562
|
+
|
|
563
|
+
// If predicate evaluates to a number, it's a position test
|
|
564
|
+
if (typeof value === "number") {
|
|
565
|
+
if (value === i + 1) {
|
|
566
|
+
result.push(node);
|
|
567
|
+
}
|
|
568
|
+
} else if (this.toBoolean(value)) {
|
|
569
|
+
result.push(node);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return result;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
evalVariableRef(ast, context) {
|
|
577
|
+
const name = ast.prefix ? `${ast.prefix}:${ast.name}` : ast.name;
|
|
578
|
+
|
|
579
|
+
// Security: Prevent prototype pollution
|
|
580
|
+
if (
|
|
581
|
+
FORBIDDEN_VARIABLE_NAMES.includes(name) ||
|
|
582
|
+
FORBIDDEN_VARIABLE_NAMES.includes(ast.name)
|
|
583
|
+
) {
|
|
584
|
+
throw new Error(`Forbidden variable name: $${name}`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (!Object.prototype.hasOwnProperty.call(context.variables, name)) {
|
|
588
|
+
throw new Error(`Undefined variable: $${name}`);
|
|
589
|
+
}
|
|
590
|
+
return context.variables[name];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
evalFunctionCall(ast, context) {
|
|
594
|
+
const name = ast.prefix ? `${ast.prefix}:${ast.name}` : ast.name;
|
|
595
|
+
const fn = this.functions[name];
|
|
596
|
+
|
|
597
|
+
if (!fn) {
|
|
598
|
+
throw new Error(`Unknown function: ${name}`);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return fn.call(this, ast.args, context);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Type conversion functions
|
|
605
|
+
toBoolean(value) {
|
|
606
|
+
if (typeof value === "boolean") return value;
|
|
607
|
+
if (typeof value === "number") return value !== 0 && !isNaN(value);
|
|
608
|
+
if (typeof value === "string") return value.length > 0;
|
|
609
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
610
|
+
if (value && value.nodeType) return true;
|
|
611
|
+
return Boolean(value);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
toNumber(value) {
|
|
615
|
+
if (typeof value === "number") return value;
|
|
616
|
+
if (typeof value === "boolean") return value ? 1 : 0;
|
|
617
|
+
if (typeof value === "string") {
|
|
618
|
+
const trimmed = value.trim();
|
|
619
|
+
if (trimmed === "") return NaN;
|
|
620
|
+
const num = Number(trimmed);
|
|
621
|
+
return num;
|
|
622
|
+
}
|
|
623
|
+
if (Array.isArray(value)) {
|
|
624
|
+
return this.toNumber(this.toString(value));
|
|
625
|
+
}
|
|
626
|
+
if (value && value.nodeType) {
|
|
627
|
+
return this.toNumber(this.getStringValue(value));
|
|
628
|
+
}
|
|
629
|
+
return NaN;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
toString(value) {
|
|
633
|
+
if (typeof value === "string") return value;
|
|
634
|
+
if (typeof value === "number") {
|
|
635
|
+
if (isNaN(value)) return "NaN";
|
|
636
|
+
if (value === Infinity) return "Infinity";
|
|
637
|
+
if (value === -Infinity) return "-Infinity";
|
|
638
|
+
if (value === 0) return "0";
|
|
639
|
+
return String(value);
|
|
640
|
+
}
|
|
641
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
642
|
+
if (Array.isArray(value)) {
|
|
643
|
+
if (value.length === 0) return "";
|
|
644
|
+
return this.getStringValue(value[0]);
|
|
645
|
+
}
|
|
646
|
+
if (value && value.nodeType) {
|
|
647
|
+
return this.getStringValue(value);
|
|
648
|
+
}
|
|
649
|
+
return String(value);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
getStringValue(node) {
|
|
653
|
+
if (!node) return "";
|
|
654
|
+
|
|
655
|
+
switch (node.nodeType) {
|
|
656
|
+
case 1: // Element
|
|
657
|
+
case 9: // Document
|
|
658
|
+
case 11: {
|
|
659
|
+
// Document Fragment
|
|
660
|
+
let text = "";
|
|
661
|
+
const walker = (n) => {
|
|
662
|
+
if (n.nodeType === 3) {
|
|
663
|
+
text += n.nodeValue || "";
|
|
664
|
+
} else if (n.childNodes) {
|
|
665
|
+
for (const child of n.childNodes) {
|
|
666
|
+
walker(child);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
walker(node);
|
|
671
|
+
return text;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
case 2: // Attribute
|
|
675
|
+
case 3: // Text
|
|
676
|
+
case 4: // CDATA
|
|
677
|
+
case 7: // Processing Instruction
|
|
678
|
+
case 8: // Comment
|
|
679
|
+
return node.nodeValue || "";
|
|
680
|
+
|
|
681
|
+
default:
|
|
682
|
+
return "";
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Comparison helper
|
|
687
|
+
compareValues(left, right, operator) {
|
|
688
|
+
const leftIsNodeSet = Array.isArray(left);
|
|
689
|
+
const rightIsNodeSet = Array.isArray(right);
|
|
690
|
+
|
|
691
|
+
// Node-set comparisons
|
|
692
|
+
if (leftIsNodeSet && rightIsNodeSet) {
|
|
693
|
+
for (const l of left) {
|
|
694
|
+
for (const r of right) {
|
|
695
|
+
if (
|
|
696
|
+
this.comparePrimitive(
|
|
697
|
+
this.getStringValue(l),
|
|
698
|
+
this.getStringValue(r),
|
|
699
|
+
operator,
|
|
700
|
+
)
|
|
701
|
+
) {
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (leftIsNodeSet) {
|
|
710
|
+
for (const l of left) {
|
|
711
|
+
if (this.comparePrimitive(this.getStringValue(l), right, operator)) {
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
if (rightIsNodeSet) {
|
|
719
|
+
for (const r of right) {
|
|
720
|
+
if (this.comparePrimitive(left, this.getStringValue(r), operator)) {
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return this.comparePrimitive(left, right, operator);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
comparePrimitive(left, right, operator) {
|
|
731
|
+
// If comparing for equality with different types
|
|
732
|
+
if (operator === "=" || operator === "!=") {
|
|
733
|
+
// If one is boolean, convert both to boolean
|
|
734
|
+
if (typeof left === "boolean" || typeof right === "boolean") {
|
|
735
|
+
const result = this.toBoolean(left) === this.toBoolean(right);
|
|
736
|
+
return operator === "=" ? result : !result;
|
|
737
|
+
}
|
|
738
|
+
// If one is number, convert both to number
|
|
739
|
+
if (typeof left === "number" || typeof right === "number") {
|
|
740
|
+
const result = this.toNumber(left) === this.toNumber(right);
|
|
741
|
+
return operator === "=" ? result : !result;
|
|
742
|
+
}
|
|
743
|
+
// Otherwise compare as strings
|
|
744
|
+
const result = this.toString(left) === this.toString(right);
|
|
745
|
+
return operator === "=" ? result : !result;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Relational operators always compare as numbers
|
|
749
|
+
const leftNum = this.toNumber(left);
|
|
750
|
+
const rightNum = this.toNumber(right);
|
|
751
|
+
|
|
752
|
+
switch (operator) {
|
|
753
|
+
case "<":
|
|
754
|
+
return leftNum < rightNum;
|
|
755
|
+
case "<=":
|
|
756
|
+
return leftNum <= rightNum;
|
|
757
|
+
case ">":
|
|
758
|
+
return leftNum > rightNum;
|
|
759
|
+
case ">=":
|
|
760
|
+
return leftNum >= rightNum;
|
|
761
|
+
default:
|
|
762
|
+
throw new Error(`Unknown comparison operator: ${operator}`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
sortByDocumentOrder(nodes) {
|
|
767
|
+
if (nodes.length <= 1) return nodes;
|
|
768
|
+
|
|
769
|
+
return nodes.sort((a, b) => {
|
|
770
|
+
if (a === b) return 0;
|
|
771
|
+
|
|
772
|
+
const position = a.compareDocumentPosition
|
|
773
|
+
? a.compareDocumentPosition(b)
|
|
774
|
+
: this.compareDocumentPositionFallback(a, b);
|
|
775
|
+
|
|
776
|
+
if (position & 4) return -1; // a before b
|
|
777
|
+
if (position & 2) return 1; // a after b
|
|
778
|
+
return 0;
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
compareDocumentPositionFallback(a, b) {
|
|
783
|
+
// Simple fallback for environments without compareDocumentPosition
|
|
784
|
+
const getPath = (node) => {
|
|
785
|
+
const path = [];
|
|
786
|
+
let current = node;
|
|
787
|
+
while (current) {
|
|
788
|
+
if (current.parentNode) {
|
|
789
|
+
const siblings = Array.from(current.parentNode.childNodes);
|
|
790
|
+
path.unshift(siblings.indexOf(current));
|
|
791
|
+
}
|
|
792
|
+
current = current.parentNode;
|
|
793
|
+
}
|
|
794
|
+
return path;
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
const pathA = getPath(a);
|
|
798
|
+
const pathB = getPath(b);
|
|
799
|
+
|
|
800
|
+
for (let i = 0; i < Math.min(pathA.length, pathB.length); i++) {
|
|
801
|
+
if (pathA[i] < pathB[i]) return 4; // a before b
|
|
802
|
+
if (pathA[i] > pathB[i]) return 2; // a after b
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return pathA.length < pathB.length ? 4 : 2;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Initialize XPath 1.0 core functions
|
|
810
|
+
*/
|
|
811
|
+
initCoreFunctions() {
|
|
812
|
+
return {
|
|
813
|
+
// Node set functions
|
|
814
|
+
last: (args, ctx) => ctx.size,
|
|
815
|
+
position: (args, ctx) => ctx.position,
|
|
816
|
+
count: (args, ctx) => {
|
|
817
|
+
const nodeSet = this.evaluate(args[0], ctx);
|
|
818
|
+
return Array.isArray(nodeSet) ? nodeSet.length : 1;
|
|
819
|
+
},
|
|
820
|
+
id: (args, ctx) => {
|
|
821
|
+
const value = this.toString(this.evaluate(args[0], ctx));
|
|
822
|
+
const doc = ctx.node.ownerDocument || ctx.node;
|
|
823
|
+
const ids = value.split(/\s+/).filter((id) => id);
|
|
824
|
+
const result = [];
|
|
825
|
+
for (const id of ids) {
|
|
826
|
+
const el = doc.getElementById(id);
|
|
827
|
+
if (el) result.push(el);
|
|
828
|
+
}
|
|
829
|
+
return result;
|
|
830
|
+
},
|
|
831
|
+
"local-name": (args, ctx) => {
|
|
832
|
+
let node;
|
|
833
|
+
if (args.length === 0) {
|
|
834
|
+
node = ctx.node;
|
|
835
|
+
} else {
|
|
836
|
+
const nodeSet = this.evaluate(args[0], ctx);
|
|
837
|
+
node = Array.isArray(nodeSet) ? nodeSet[0] : nodeSet;
|
|
838
|
+
}
|
|
839
|
+
if (!node) return "";
|
|
840
|
+
return node.localName || node.nodeName || "";
|
|
841
|
+
},
|
|
842
|
+
"namespace-uri": (args, ctx) => {
|
|
843
|
+
let node;
|
|
844
|
+
if (args.length === 0) {
|
|
845
|
+
node = ctx.node;
|
|
846
|
+
} else {
|
|
847
|
+
const nodeSet = this.evaluate(args[0], ctx);
|
|
848
|
+
node = Array.isArray(nodeSet) ? nodeSet[0] : nodeSet;
|
|
849
|
+
}
|
|
850
|
+
if (!node) return "";
|
|
851
|
+
return node.namespaceURI || "";
|
|
852
|
+
},
|
|
853
|
+
name: (args, ctx) => {
|
|
854
|
+
let node;
|
|
855
|
+
if (args.length === 0) {
|
|
856
|
+
node = ctx.node;
|
|
857
|
+
} else {
|
|
858
|
+
const nodeSet = this.evaluate(args[0], ctx);
|
|
859
|
+
node = Array.isArray(nodeSet) ? nodeSet[0] : nodeSet;
|
|
860
|
+
}
|
|
861
|
+
if (!node) return "";
|
|
862
|
+
return node.nodeName || "";
|
|
863
|
+
},
|
|
864
|
+
|
|
865
|
+
// String functions
|
|
866
|
+
string: (args, ctx) => {
|
|
867
|
+
if (args.length === 0) {
|
|
868
|
+
return this.toString([ctx.node]);
|
|
869
|
+
}
|
|
870
|
+
return this.toString(this.evaluate(args[0], ctx));
|
|
871
|
+
},
|
|
872
|
+
concat: (args, ctx) => {
|
|
873
|
+
return args
|
|
874
|
+
.map((arg) => this.toString(this.evaluate(arg, ctx)))
|
|
875
|
+
.join("");
|
|
876
|
+
},
|
|
877
|
+
"starts-with": (args, ctx) => {
|
|
878
|
+
const str = this.toString(this.evaluate(args[0], ctx));
|
|
879
|
+
const prefix = this.toString(this.evaluate(args[1], ctx));
|
|
880
|
+
return str.startsWith(prefix);
|
|
881
|
+
},
|
|
882
|
+
contains: (args, ctx) => {
|
|
883
|
+
const str = this.toString(this.evaluate(args[0], ctx));
|
|
884
|
+
const substr = this.toString(this.evaluate(args[1], ctx));
|
|
885
|
+
return str.includes(substr);
|
|
886
|
+
},
|
|
887
|
+
"substring-before": (args, ctx) => {
|
|
888
|
+
const str = this.toString(this.evaluate(args[0], ctx));
|
|
889
|
+
const substr = this.toString(this.evaluate(args[1], ctx));
|
|
890
|
+
const idx = str.indexOf(substr);
|
|
891
|
+
return idx === -1 ? "" : str.substring(0, idx);
|
|
892
|
+
},
|
|
893
|
+
"substring-after": (args, ctx) => {
|
|
894
|
+
const str = this.toString(this.evaluate(args[0], ctx));
|
|
895
|
+
const substr = this.toString(this.evaluate(args[1], ctx));
|
|
896
|
+
const idx = str.indexOf(substr);
|
|
897
|
+
return idx === -1 ? "" : str.substring(idx + substr.length);
|
|
898
|
+
},
|
|
899
|
+
substring: (args, ctx) => {
|
|
900
|
+
const str = this.toString(this.evaluate(args[0], ctx));
|
|
901
|
+
let start = Math.round(this.toNumber(this.evaluate(args[1], ctx)));
|
|
902
|
+
let length;
|
|
903
|
+
|
|
904
|
+
if (args.length > 2) {
|
|
905
|
+
length = Math.round(this.toNumber(this.evaluate(args[2], ctx)));
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// XPath uses 1-based indexing
|
|
909
|
+
start = start - 1;
|
|
910
|
+
|
|
911
|
+
if (isNaN(start)) return "";
|
|
912
|
+
if (start < 0) {
|
|
913
|
+
if (length !== undefined) {
|
|
914
|
+
length = length + start;
|
|
915
|
+
}
|
|
916
|
+
start = 0;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (length !== undefined) {
|
|
920
|
+
if (isNaN(length) || length <= 0) return "";
|
|
921
|
+
return str.substring(start, start + length);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return str.substring(start);
|
|
925
|
+
},
|
|
926
|
+
"string-length": (args, ctx) => {
|
|
927
|
+
const str =
|
|
928
|
+
args.length === 0
|
|
929
|
+
? this.toString([ctx.node])
|
|
930
|
+
: this.toString(this.evaluate(args[0], ctx));
|
|
931
|
+
return str.length;
|
|
932
|
+
},
|
|
933
|
+
"normalize-space": (args, ctx) => {
|
|
934
|
+
const str =
|
|
935
|
+
args.length === 0
|
|
936
|
+
? this.toString([ctx.node])
|
|
937
|
+
: this.toString(this.evaluate(args[0], ctx));
|
|
938
|
+
return str.trim().replace(/\s+/g, " ");
|
|
939
|
+
},
|
|
940
|
+
translate: (args, ctx) => {
|
|
941
|
+
const str = this.toString(this.evaluate(args[0], ctx));
|
|
942
|
+
const from = this.toString(this.evaluate(args[1], ctx));
|
|
943
|
+
const to = this.toString(this.evaluate(args[2], ctx));
|
|
944
|
+
|
|
945
|
+
let result = "";
|
|
946
|
+
for (const char of str) {
|
|
947
|
+
const idx = from.indexOf(char);
|
|
948
|
+
if (idx === -1) {
|
|
949
|
+
result += char;
|
|
950
|
+
} else if (idx < to.length) {
|
|
951
|
+
result += to[idx];
|
|
952
|
+
}
|
|
953
|
+
// If idx >= to.length, character is removed
|
|
954
|
+
}
|
|
955
|
+
return result;
|
|
956
|
+
},
|
|
957
|
+
|
|
958
|
+
// Boolean functions
|
|
959
|
+
boolean: (args, ctx) => {
|
|
960
|
+
return this.toBoolean(this.evaluate(args[0], ctx));
|
|
961
|
+
},
|
|
962
|
+
not: (args, ctx) => {
|
|
963
|
+
return !this.toBoolean(this.evaluate(args[0], ctx));
|
|
964
|
+
},
|
|
965
|
+
true: () => true,
|
|
966
|
+
false: () => false,
|
|
967
|
+
lang: (args, ctx) => {
|
|
968
|
+
const lang = this.toString(this.evaluate(args[0], ctx)).toLowerCase();
|
|
969
|
+
let node = ctx.node;
|
|
970
|
+
|
|
971
|
+
while (node && node.nodeType === 1) {
|
|
972
|
+
const xmlLang =
|
|
973
|
+
node.getAttribute("xml:lang") || node.getAttribute("lang");
|
|
974
|
+
if (xmlLang) {
|
|
975
|
+
const nodeLang = xmlLang.toLowerCase();
|
|
976
|
+
return nodeLang === lang || nodeLang.startsWith(lang + "-");
|
|
977
|
+
}
|
|
978
|
+
node = node.parentNode;
|
|
979
|
+
}
|
|
980
|
+
return false;
|
|
981
|
+
},
|
|
982
|
+
|
|
983
|
+
// Number functions
|
|
984
|
+
number: (args, ctx) => {
|
|
985
|
+
if (args.length === 0) {
|
|
986
|
+
return this.toNumber([ctx.node]);
|
|
987
|
+
}
|
|
988
|
+
return this.toNumber(this.evaluate(args[0], ctx));
|
|
989
|
+
},
|
|
990
|
+
sum: (args, ctx) => {
|
|
991
|
+
const nodeSet = this.evaluate(args[0], ctx);
|
|
992
|
+
if (!Array.isArray(nodeSet)) return NaN;
|
|
993
|
+
return nodeSet.reduce(
|
|
994
|
+
(sum, node) => sum + this.toNumber(this.getStringValue(node)),
|
|
995
|
+
0,
|
|
996
|
+
);
|
|
997
|
+
},
|
|
998
|
+
floor: (args, ctx) => {
|
|
999
|
+
return Math.floor(this.toNumber(this.evaluate(args[0], ctx)));
|
|
1000
|
+
},
|
|
1001
|
+
ceiling: (args, ctx) => {
|
|
1002
|
+
return Math.ceil(this.toNumber(this.evaluate(args[0], ctx)));
|
|
1003
|
+
},
|
|
1004
|
+
round: (args, ctx) => {
|
|
1005
|
+
const num = this.toNumber(this.evaluate(args[0], ctx));
|
|
1006
|
+
if (isNaN(num)) return NaN;
|
|
1007
|
+
if (num === -0.5) return -0;
|
|
1008
|
+
return Math.round(num);
|
|
1009
|
+
},
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
}
|