@tradik/xslt-processor 1.0.3 → 1.3.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.
Files changed (131) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +110 -520
  3. package/bin/lib/decode.js +15 -0
  4. package/bin/lib/dom.js +177 -0
  5. package/bin/lib/loaders.js +127 -0
  6. package/bin/lib/options.js +131 -0
  7. package/bin/lib/output.js +114 -0
  8. package/bin/lib/paths.js +186 -0
  9. package/bin/lib/transform.js +206 -0
  10. package/bin/xslt.js +73 -168
  11. package/dist/xslt-processor.browser.js +9564 -1585
  12. package/dist/xslt-processor.browser.js.map +4 -4
  13. package/dist/xslt-processor.browser.min.js +13 -2
  14. package/dist/xslt-processor.browser.min.js.map +4 -4
  15. package/dist/xslt-processor.cjs +9572 -1586
  16. package/dist/xslt-processor.cjs.map +4 -4
  17. package/dist/xslt-processor.d.cts +658 -0
  18. package/dist/xslt-processor.d.ts +459 -12
  19. package/dist/xslt-processor.js +9546 -1582
  20. package/dist/xslt-processor.js.map +4 -4
  21. package/package.json +71 -20
  22. package/src/XSLTProcessor.js +494 -48
  23. package/src/async/abort.js +63 -0
  24. package/src/async/documentUris.js +128 -0
  25. package/src/async/loaders.js +134 -0
  26. package/src/async/preload.js +159 -0
  27. package/src/async/processor.js +206 -0
  28. package/src/async/stream.js +125 -0
  29. package/src/bridge/engine.js +221 -0
  30. package/src/bridge/loader.js +78 -0
  31. package/src/bridge/results.js +75 -0
  32. package/src/bridge/version.js +63 -0
  33. package/src/index.js +26 -8
  34. package/src/io/decode.js +140 -0
  35. package/src/io/readSource.js +167 -0
  36. package/src/xpath/axes.js +562 -0
  37. package/src/xpath/documentOrder.js +270 -0
  38. package/src/xpath/evaluator.js +518 -357
  39. package/src/xpath/index.js +8 -2
  40. package/src/xpath/namespaceNodes.js +172 -0
  41. package/src/xpath/nodeSetFunctions.js +169 -0
  42. package/src/xpath/parser.js +30 -5
  43. package/src/xpath/strings.js +183 -0
  44. package/src/xpath/tokenizer.js +37 -23
  45. package/src/xslt/attributeSets.js +95 -0
  46. package/src/xslt/avt.js +103 -0
  47. package/src/xslt/computedNames.js +91 -0
  48. package/src/xslt/copying.js +212 -0
  49. package/src/xslt/declarationNames.js +80 -0
  50. package/src/xslt/domParsing.js +95 -0
  51. package/src/xslt/elements.js +57 -0
  52. package/src/xslt/engine/bindings.js +195 -0
  53. package/src/xslt/engine/context.js +105 -0
  54. package/src/xslt/engine/controlFlow.js +145 -0
  55. package/src/xslt/engine/copyInstructions.js +133 -0
  56. package/src/xslt/engine/declarations.js +233 -0
  57. package/src/xslt/engine/functionSupport.js +103 -0
  58. package/src/xslt/engine/methods.js +33 -0
  59. package/src/xslt/engine/nodeConstruction.js +187 -0
  60. package/src/xslt/engine/numbering.js +104 -0
  61. package/src/xslt/engine/outputDeclaration.js +77 -0
  62. package/src/xslt/engine/sequenceConstructor.js +228 -0
  63. package/src/xslt/engine/stylesheetLoading.js +208 -0
  64. package/src/xslt/engine/templateInvocation.js +253 -0
  65. package/src/xslt/engine/templateRules.js +243 -0
  66. package/src/xslt/engine/textInstructions.js +171 -0
  67. package/src/xslt/engine/topLevel.js +130 -0
  68. package/src/xslt/engine/transformation.js +263 -0
  69. package/src/xslt/engine/workStack.js +245 -0
  70. package/src/xslt/engine.js +184 -1736
  71. package/src/xslt/exslt/arguments.js +99 -0
  72. package/src/xslt/exslt/calendar.js +120 -0
  73. package/src/xslt/exslt/common.js +44 -0
  74. package/src/xslt/exslt/dateCalc.js +261 -0
  75. package/src/xslt/exslt/dateFormat.js +150 -0
  76. package/src/xslt/exslt/dateParse.js +265 -0
  77. package/src/xslt/exslt/dates.js +259 -0
  78. package/src/xslt/exslt/duration.js +207 -0
  79. package/src/xslt/exslt/dynamic.js +59 -0
  80. package/src/xslt/exslt/index.js +59 -0
  81. package/src/xslt/exslt/math.js +177 -0
  82. package/src/xslt/exslt/sets.js +96 -0
  83. package/src/xslt/exslt/stringOps.js +163 -0
  84. package/src/xslt/exslt/strings.js +147 -0
  85. package/src/xslt/exslt/uri.js +92 -0
  86. package/src/xslt/formatNumber.js +233 -0
  87. package/src/xslt/forwardsCompatible.js +75 -0
  88. package/src/xslt/functions.js +270 -0
  89. package/src/xslt/index.js +38 -1
  90. package/src/xslt/keys.js +164 -0
  91. package/src/xslt/literalResult.js +223 -0
  92. package/src/xslt/matchScope.js +116 -0
  93. package/src/xslt/number.js +271 -0
  94. package/src/xslt/numberFormat.js +253 -0
  95. package/src/xslt/outputNames.js +58 -0
  96. package/src/xslt/patternCompiler.js +175 -0
  97. package/src/xslt/patterns.js +324 -0
  98. package/src/xslt/qname.js +90 -0
  99. package/src/xslt/resultDocument.js +98 -0
  100. package/src/xslt/resultNamespaces.js +219 -0
  101. package/src/xslt/resultTree.js +211 -0
  102. package/src/xslt/serializer/baseWriter.js +390 -0
  103. package/src/xslt/serializer/chunks.js +120 -0
  104. package/src/xslt/serializer/constants.js +92 -0
  105. package/src/xslt/serializer/encoding.js +327 -0
  106. package/src/xslt/serializer/escape.js +135 -0
  107. package/src/xslt/serializer/frames.js +168 -0
  108. package/src/xslt/serializer/htmlDoctype.js +102 -0
  109. package/src/xslt/serializer/htmlEntities.js +77 -0
  110. package/src/xslt/serializer/htmlSerializer.js +239 -0
  111. package/src/xslt/serializer/indent.js +51 -0
  112. package/src/xslt/serializer/namespaces.js +68 -0
  113. package/src/xslt/serializer/rawText.js +41 -0
  114. package/src/xslt/serializer/settings.js +179 -0
  115. package/src/xslt/serializer/textSerializer.js +77 -0
  116. package/src/xslt/serializer/xhtmlDocument.js +103 -0
  117. package/src/xslt/serializer/xmlSerializer.js +227 -0
  118. package/src/xslt/serializer.js +90 -0
  119. package/src/xslt/sort.js +151 -0
  120. package/src/xslt/spaceNameTests.js +115 -0
  121. package/src/xslt/stylesheetChecks.js +206 -0
  122. package/src/xslt/stylesheetNamespaces.js +266 -0
  123. package/src/xslt/templatePriority.js +45 -0
  124. package/src/xslt/uri.js +68 -0
  125. package/src/xslt/variables.js +152 -0
  126. package/src/xslt/whitespace.js +200 -0
  127. package/LICENSE +0 -29
  128. package/src/XSLTProcessor.test.js +0 -930
  129. package/src/xpath/evaluator.test.js +0 -1852
  130. package/src/xpath/tokenizer.test.js +0 -224
  131. package/src/xslt/engine.test.js +0 -3130
@@ -8,6 +8,118 @@
8
8
  "use strict";
9
9
 
10
10
  import { NodeType } from "./parser.js";
11
+ import {
12
+ AXIS_WALKERS,
13
+ REVERSE_AXES,
14
+ ancestorAxis,
15
+ attributeAxis,
16
+ childAxis,
17
+ descendantAxis,
18
+ followingAxis,
19
+ followingSiblingAxis,
20
+ precedingAxis,
21
+ precedingSiblingAxis,
22
+ isTextNode,
23
+ parentOf,
24
+ rootNodeOf,
25
+ } from "./axes.js";
26
+ import {
27
+ NAMESPACE_NODE,
28
+ matchNamespaceNameTest,
29
+ namespaceAxis,
30
+ } from "./namespaceNodes.js";
31
+ import {
32
+ DocumentOrderIndex,
33
+ compareDomPositions,
34
+ compareNodeOrder,
35
+ hasNativePositionComparison,
36
+ } from "./documentOrder.js";
37
+ import { createNodeSetFunctions } from "./nodeSetFunctions.js";
38
+ import {
39
+ codePointLength,
40
+ formatXPathNumber,
41
+ normalizeXmlSpace,
42
+ parseXPathNumber,
43
+ xpathSubstring,
44
+ xpathTranslate,
45
+ } from "./strings.js";
46
+
47
+ /**
48
+ * Namespace the `xml` prefix is bound to in every context; it cannot be
49
+ * undeclared or rebound (Namespaces in XML 1.0, section 3).
50
+ */
51
+ export const XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
52
+
53
+ /**
54
+ * Resolve a namespace prefix: `xml` is predeclared, every other prefix comes
55
+ * from the context bindings.
56
+ *
57
+ * @param {string} prefix - The prefix
58
+ * @param {Object<string, string>} namespaces - Bindings by prefix
59
+ * @returns {string|null} The namespace URI, or null when undeclared
60
+ */
61
+ export function resolveNamespacePrefix(prefix, namespaces) {
62
+ if (prefix === "xml") return XML_NAMESPACE;
63
+ return Object.hasOwn(namespaces, prefix) ? namespaces[prefix] : null;
64
+ }
65
+
66
+ /**
67
+ * Key of a function registered by expanded name, as used by
68
+ * {@link XPathEvaluator#registerFunctions}: `{namespace-uri}local-name`.
69
+ *
70
+ * @param {string} namespaceUri - The function's namespace
71
+ * @param {string} localName - The function's local name
72
+ * @returns {string} The registry key
73
+ *
74
+ * @example
75
+ * expandedFunctionName("http://exslt.org/common", "node-set");
76
+ * // "{http://exslt.org/common}node-set"
77
+ */
78
+ export function expandedFunctionName(namespaceUri, localName) {
79
+ return `{${namespaceUri}}${localName}`;
80
+ }
81
+
82
+ /**
83
+ * Whether an expression is a call of the core `position()` function.
84
+ *
85
+ * @param {object} expr - Expression AST node
86
+ * @returns {boolean} True for `position()`
87
+ */
88
+ function isPositionCall(expr) {
89
+ return (
90
+ expr.type === NodeType.FUNCTION_CALL &&
91
+ expr.name === "position" &&
92
+ !expr.prefix &&
93
+ expr.args.length === 0
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Whether a node has the principal node type of an axis, the only type a
99
+ * name test can match there: attributes on the attribute axis, namespace
100
+ * nodes on the namespace axis, elements elsewhere (XPath 1.0 section 2.3).
101
+ *
102
+ * @param {number} type - The candidate node's `nodeType`
103
+ * @param {string|null} axis - The axis, or null when the caller checks types
104
+ * @returns {boolean} True when a name test may match the node
105
+ */
106
+ function isPrincipalNodeType(type, axis) {
107
+ if (axis === null) return true;
108
+ if (axis === "attribute") return type === 2;
109
+ if (axis === "namespace") return type === NAMESPACE_NODE;
110
+ return type === 1;
111
+ }
112
+
113
+ /**
114
+ * Whether a node belongs to an HTML document (`contentType` "text/html").
115
+ * Two jsdom getters, so name tests read it last.
116
+ *
117
+ * @param {Node} node - An element
118
+ * @returns {boolean} True for a node of an HTML document
119
+ */
120
+ function isInHtmlDocument(node) {
121
+ return node.ownerDocument?.contentType === "text/html";
122
+ }
11
123
 
12
124
  /**
13
125
  * XPath result types matching W3C spec
@@ -51,12 +163,29 @@ const FORBIDDEN_VARIABLE_NAMES = Object.freeze([
51
163
  * XPath evaluation context
52
164
  */
53
165
  export class XPathContext {
54
- constructor(node, position = 1, size = 1, variables = {}, namespaces = {}) {
166
+ /**
167
+ * @param {Node} node - The context node
168
+ * @param {number} [position] - The context position (1-based)
169
+ * @param {number} [size] - The context size
170
+ * @param {Object} [variables] - Variable bindings by name
171
+ * @param {Object} [namespaces] - Namespace bindings by prefix
172
+ * @param {*} [hostContext] - Opaque context of the host language (XSLT),
173
+ * carried through unchanged so host defined functions can reach it
174
+ */
175
+ constructor(
176
+ node,
177
+ position = 1,
178
+ size = 1,
179
+ variables = {},
180
+ namespaces = {},
181
+ hostContext = null,
182
+ ) {
55
183
  this.node = node;
56
184
  this.position = position;
57
185
  this.size = size;
58
186
  this.variables = variables;
59
187
  this.namespaces = namespaces;
188
+ this.hostContext = hostContext;
60
189
  }
61
190
 
62
191
  clone(overrides = {}) {
@@ -66,6 +195,7 @@ export class XPathContext {
66
195
  overrides.size ?? this.size,
67
196
  overrides.variables ?? this.variables,
68
197
  overrides.namespaces ?? this.namespaces,
198
+ overrides.hostContext ?? this.hostContext,
69
199
  );
70
200
  }
71
201
  }
@@ -74,8 +204,22 @@ export class XPathContext {
74
204
  * XPath Evaluator
75
205
  */
76
206
  export class XPathEvaluator {
207
+ /**
208
+ * @param {object} [options] - Evaluator options
209
+ * @param {number} [options.maxRecursionDepth] - Deepest expression nesting
210
+ * @param {number} [options.maxResultSize] - Largest node-set of one step
211
+ * @param {number} [options.maxStringLength] - Longest string literal
212
+ * @param {boolean} [options.legacyNameTests] - Deprecated: let unprefixed
213
+ * name tests also match nodes in a namespace, as before 1.2.0
214
+ */
77
215
  constructor(options = {}) {
78
- this.functions = this.initCoreFunctions();
216
+ this.legacyNameTests = options.legacyNameTests === true;
217
+ this.resetNamespaceNodes();
218
+ this.resetDocumentOrder();
219
+ this.functions = Object.assign(
220
+ Object.create(null),
221
+ this.initCoreFunctions(),
222
+ );
79
223
  this.maxRecursionDepth =
80
224
  options.maxRecursionDepth ?? XPathLimits.MAX_RECURSION_DEPTH;
81
225
  this.maxResultSize = options.maxResultSize ?? XPathLimits.MAX_RESULT_SIZE;
@@ -84,6 +228,18 @@ export class XPathEvaluator {
84
228
  this.recursionDepth = 0;
85
229
  }
86
230
 
231
+ /**
232
+ * Forget the namespace nodes synthesized so far (see namespaceNodes.js),
233
+ * e.g. before a new transformation: the declarations of the source tree
234
+ * may have changed. Until then each element keeps the same namespace node
235
+ * objects, so they have a stable identity.
236
+ *
237
+ * @returns {void}
238
+ */
239
+ resetNamespaceNodes() {
240
+ this.namespaceNodes = new WeakMap();
241
+ }
242
+
87
243
  /**
88
244
  * Evaluate XPath expression against a context
89
245
  * @throws {Error} If recursion depth exceeds limit
@@ -98,6 +254,11 @@ export class XPathEvaluator {
98
254
  throw new Error("Invalid AST: missing type property");
99
255
  }
100
256
 
257
+ // An index created during an earlier evaluation may describe trees
258
+ // that have changed since; one set with resetDocumentOrder() stays
259
+ if (this.recursionDepth === 0 && this.ownsDocumentOrder) {
260
+ this.resetDocumentOrder();
261
+ }
101
262
  this.recursionDepth++;
102
263
  if (this.recursionDepth > this.maxRecursionDepth) {
103
264
  this.recursionDepth = 0;
@@ -123,9 +284,8 @@ export class XPathEvaluator {
123
284
  case NodeType.AND_EXPR:
124
285
  return this.evalAndExpr(ast, context);
125
286
  case NodeType.EQUALITY_EXPR:
126
- return this.evalEqualityExpr(ast, context);
127
287
  case NodeType.RELATIONAL_EXPR:
128
- return this.evalRelationalExpr(ast, context);
288
+ return this.evalComparisonExpr(ast, context);
129
289
  case NodeType.ADDITIVE_EXPR:
130
290
  return this.evalAdditiveExpr(ast, context);
131
291
  case NodeType.MULTIPLICATIVE_EXPR:
@@ -189,14 +349,15 @@ export class XPathEvaluator {
189
349
  );
190
350
  }
191
351
 
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) {
352
+ /**
353
+ * Evaluate `=`, `!=`, `<`, `<=`, `>` or `>=`. Comparisons involving
354
+ * node-sets are existential, so `!=` is not `not(=)` (XPath 3.4).
355
+ *
356
+ * @param {object} ast - Equality or relational expression node
357
+ * @param {XPathContext} context - Evaluation context
358
+ * @returns {boolean} The comparison result
359
+ */
360
+ evalComparisonExpr(ast, context) {
200
361
  const left = this.evaluate(ast.left, context);
201
362
  const right = this.evaluate(ast.right, context);
202
363
  return this.compareValues(left, right, ast.operator);
@@ -277,10 +438,9 @@ export class XPathEvaluator {
277
438
  let nodes;
278
439
 
279
440
  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];
441
+ // The root node of the tree containing the context node: a document,
442
+ // or the fragment of a result tree fragment turned into a node-set
443
+ nodes = [rootNodeOf(context.node)];
284
444
  } else {
285
445
  nodes = [context.node];
286
446
  }
@@ -293,192 +453,190 @@ export class XPathEvaluator {
293
453
  return nodes;
294
454
  }
295
455
 
456
+ /**
457
+ * Apply one location step to every node of a node-set.
458
+ *
459
+ * The result is in document order without duplicates. With a single context
460
+ * node the axis output is already duplicate free, so it only needs to be
461
+ * reversed for reverse axes; merging several context nodes needs a sort.
462
+ *
463
+ * @param {object} step - Step AST node
464
+ * @param {Node|Node[]} nodes - Context nodes
465
+ * @param {XPathContext} context - Evaluation context
466
+ * @returns {Node[]} The selected nodes in document order
467
+ */
296
468
  evalStepOnNodes(step, nodes, context) {
297
469
  const allNodes = Array.isArray(nodes) ? nodes : [nodes];
298
- let result = [];
470
+ const reverse = REVERSE_AXES.has(step.axis);
299
471
 
300
- for (const node of allNodes) {
301
- const stepNodes = this.evalStep(step, context.clone({ node }));
302
- result = result.concat(stepNodes);
472
+ if (allNodes.length === 1) {
473
+ const single = this.evalStep(step, context.clone({ node: allNodes[0] }));
474
+ this.validateResultSize(single);
475
+ return reverse ? single.reverse() : single;
476
+ }
303
477
 
304
- // Early validation to prevent excessive memory use
305
- if (result.length > this.maxResultSize * 2) {
306
- this.validateResultSize(result);
478
+ const seen = new Set();
479
+ const result = [];
480
+ for (const node of allNodes) {
481
+ for (const found of this.evalStep(step, context.clone({ node }))) {
482
+ if (!seen.has(found)) {
483
+ seen.add(found);
484
+ result.push(found);
485
+ }
307
486
  }
487
+ this.validateResultSize(result);
308
488
  }
309
489
 
310
- // Remove duplicates and sort by document order
311
- const uniqueResult = [...new Set(result)];
312
- this.validateResultSize(uniqueResult);
313
- return this.sortByDocumentOrder(uniqueResult);
490
+ return this.sortByDocumentOrder(result);
314
491
  }
315
492
 
493
+ /**
494
+ * Apply one location step to a single context node.
495
+ *
496
+ * When the first predicate selects one position (`[n]` or
497
+ * `[position() = n]`) and the axis can be walked lazily, the walk stops at
498
+ * the n-th node that passes the node test instead of materializing the
499
+ * whole axis: `following-sibling::x[1]` in a loop is then linear.
500
+ *
501
+ * @param {object} step - Step AST node
502
+ * @param {XPathContext} context - Evaluation context
503
+ * @returns {Node[]} The selected nodes in axis order
504
+ */
316
505
  evalStep(step, context) {
317
- // Get nodes along axis
318
- let nodes = this.getAxisNodes(step.axis, context.node);
506
+ const { predicates } = step;
507
+ const position = this.positionalPredicate(step);
508
+ let nodes;
509
+ let first = 0;
319
510
 
320
- // Filter by node test
321
- nodes = nodes.filter((n) => this.matchNodeTest(step.nodeTest, n, context));
511
+ if (position !== null && Object.hasOwn(AXIS_WALKERS, step.axis)) {
512
+ nodes = this.nthOnAxis(step, context, position);
513
+ first = 1;
514
+ } else {
515
+ nodes = this.getAxisNodes(step.axis, context.node).filter((n) =>
516
+ this.matchNodeTest(step.nodeTest, n, context, step.axis),
517
+ );
518
+ }
322
519
 
323
- // Apply predicates
324
- for (const predicate of step.predicates) {
325
- nodes = this.filterByPredicate(nodes, predicate, context);
520
+ for (let i = first; i < predicates.length; i++) {
521
+ nodes = this.filterByPredicate(nodes, predicates[i], context);
326
522
  }
327
523
 
328
524
  return nodes;
329
525
  }
330
526
 
527
+ /**
528
+ * The position selected by the first predicate of a step, when that
529
+ * predicate is a number literal `[n]` or `[position() = n]`.
530
+ *
531
+ * @param {object} step - Step AST node
532
+ * @returns {number|null} The position, or null for any other predicate
533
+ */
534
+ positionalPredicate(step) {
535
+ const expr = step.predicates[0]?.expr;
536
+ if (!expr) return null;
537
+ if (expr.type === NodeType.NUMBER) return expr.value;
538
+ if (expr.type !== NodeType.EQUALITY_EXPR || expr.operator !== "=") {
539
+ return null;
540
+ }
541
+ const { left, right } = expr;
542
+ if (isPositionCall(left) && right.type === NodeType.NUMBER) {
543
+ return right.value;
544
+ }
545
+ if (isPositionCall(right) && left.type === NodeType.NUMBER) {
546
+ return left.value;
547
+ }
548
+ return null;
549
+ }
550
+
551
+ /**
552
+ * The n-th node of an axis passing the node test of a step, found by
553
+ * walking the axis only as far as needed.
554
+ *
555
+ * @param {object} step - Step AST node
556
+ * @param {XPathContext} context - Evaluation context
557
+ * @param {number} position - The wanted proximity position
558
+ * @returns {Node[]} The node, or an empty node-set
559
+ */
560
+ nthOnAxis(step, context, position) {
561
+ // Positions are whole numbers from 1: [0], [1.5] or [NaN] select nothing
562
+ if (!Number.isInteger(position) || position < 1) return [];
563
+ let remaining = position;
564
+ let found = null;
565
+ AXIS_WALKERS[step.axis](context.node, (node) => {
566
+ if (!this.matchNodeTest(step.nodeTest, node, context, step.axis)) {
567
+ return false;
568
+ }
569
+ remaining--;
570
+ if (remaining > 0) return false;
571
+ found = node;
572
+ return true;
573
+ });
574
+ return found ? [found] : [];
575
+ }
576
+
577
+ /**
578
+ * Nodes on an axis (see axes.js). Reverse axes are returned nearest first,
579
+ * so predicates see proximity positions.
580
+ *
581
+ * @param {string} axis - Axis name
582
+ * @param {Node} node - Context node
583
+ * @returns {Node[]} The nodes on the axis
584
+ */
331
585
  getAxisNodes(axis, node) {
332
586
  switch (axis) {
333
587
  case "child":
334
- return Array.from(node.childNodes || []);
335
-
336
- case "parent":
337
- return node.parentNode ? [node.parentNode] : [];
338
-
588
+ return childAxis(node);
589
+ case "parent": {
590
+ const parent = parentOf(node);
591
+ return parent ? [parent] : [];
592
+ }
339
593
  case "self":
340
594
  return [node];
341
-
342
595
  case "descendant":
343
- return this.getDescendants(node, false);
344
-
596
+ return descendantAxis(node, false);
345
597
  case "descendant-or-self":
346
- return this.getDescendants(node, true);
347
-
598
+ return descendantAxis(node, true);
348
599
  case "ancestor":
349
- return this.getAncestors(node, false);
350
-
600
+ return ancestorAxis(node, false);
351
601
  case "ancestor-or-self":
352
- return this.getAncestors(node, true);
353
-
602
+ return ancestorAxis(node, true);
354
603
  case "following-sibling":
355
- return this.getFollowingSiblings(node);
356
-
604
+ return followingSiblingAxis(node);
357
605
  case "preceding-sibling":
358
- return this.getPrecedingSiblings(node);
359
-
606
+ return precedingSiblingAxis(node);
360
607
  case "following":
361
- return this.getFollowing(node);
362
-
608
+ return followingAxis(node);
363
609
  case "preceding":
364
- return this.getPreceding(node);
365
-
610
+ return precedingAxis(node);
366
611
  case "attribute":
367
- if (node.attributes) {
368
- return Array.from(node.attributes);
369
- }
370
- return [];
371
-
612
+ return attributeAxis(node);
372
613
  case "namespace":
373
- // Namespace axis - not commonly used
374
- return [];
375
-
614
+ return namespaceAxis(node, this.namespaceNodes);
376
615
  default:
377
616
  throw new Error(`Unknown axis: ${axis}`);
378
617
  }
379
618
  }
380
619
 
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) {
620
+ /**
621
+ * Match a node test against a node.
622
+ *
623
+ * @param {object} nodeTest - Node test AST node
624
+ * @param {Node} node - Candidate node
625
+ * @param {XPathContext} context - Evaluation context (for prefixes)
626
+ * @param {string|null} [axis] - Axis of the step, which decides the
627
+ * principal node type of name tests; null for XSLT patterns, whose
628
+ * matcher checks node types itself
629
+ * @returns {boolean} Whether the node matches
630
+ */
631
+ matchNodeTest(nodeTest, node, context, axis = null) {
479
632
  switch (nodeTest.type) {
480
- case NodeType.NAME_TEST:
481
- return this.matchNameTest(nodeTest, node, context);
633
+ case NodeType.NAME_TEST: {
634
+ const type = node.nodeType;
635
+ return (
636
+ isPrincipalNodeType(type, axis) &&
637
+ this.matchNameTest(nodeTest, node, context, type)
638
+ );
639
+ }
482
640
 
483
641
  case NodeType.NODE_TYPE_TEST:
484
642
  return this.matchNodeTypeTest(nodeTest.nodeType, node);
@@ -491,43 +649,64 @@ export class XPathEvaluator {
491
649
  }
492
650
  }
493
651
 
494
- matchNameTest(nodeTest, node, context) {
652
+ /**
653
+ * Match a name test against a node.
654
+ *
655
+ * Cheap checks run first: in jsdom every DOM getter crosses a wrapper, so
656
+ * `namespaceURI` and the owner document's content type are only read when
657
+ * the outcome depends on them.
658
+ *
659
+ * @param {object} nodeTest - Name test AST node
660
+ * @param {Node} node - Candidate node
661
+ * @param {XPathContext} context - Evaluation context (for prefixes)
662
+ * @param {number} [type] - The node's `nodeType`, when already read
663
+ * @returns {boolean} Whether the node matches
664
+ */
665
+ matchNameTest(nodeTest, node, context, type = node.nodeType) {
666
+ if (type === NAMESPACE_NODE) return matchNamespaceNameTest(nodeTest, node);
495
667
  // 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;
668
+ if (type !== 1 && type !== 2) return false;
502
669
 
503
- // Wildcard
504
- if (name === "*" && !prefix) {
505
- return true;
506
- }
670
+ const { name, prefix } = nodeTest;
507
671
 
508
- // Get node's local name and namespace
509
- const nodeName = node.localName || node.nodeName;
510
- const nodeNs = node.namespaceURI || null;
672
+ if (!prefix) return this.matchUnprefixedName(name, node, type);
511
673
 
512
- // Prefix:* matches all nodes in namespace
513
- if (name === "*" && prefix) {
514
- const ns = context.namespaces[prefix];
515
- return nodeNs === ns;
674
+ const namespaceUri = resolveNamespacePrefix(prefix, context.namespaces);
675
+ if (!namespaceUri) {
676
+ // An undeclared prefix is an error (XPath 1.0 section 2.3), as in
677
+ // libxslt; it must not silently match names in no namespace
678
+ throw new Error(`Undefined namespace prefix: ${prefix}`);
516
679
  }
680
+ if ((node.namespaceURI || null) !== namespaceUri) return false;
681
+ return name === "*" || (node.localName || node.nodeName) === name;
682
+ }
517
683
 
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;
684
+ /**
685
+ * Match a name test without prefix (`name` or `*`) against an element or
686
+ * attribute. The owner document's content type (two jsdom getters) is
687
+ * read last, only for HTML-specific outcomes.
688
+ *
689
+ * @param {string} name - The tested name, or "*"
690
+ * @param {Node} node - An element or attribute
691
+ * @param {number} type - The node's `nodeType`
692
+ * @returns {boolean} Whether the node matches
693
+ */
694
+ matchUnprefixedName(name, node, type) {
695
+ if (name === "*") return true;
696
+ const nodeName = node.localName || node.nodeName;
697
+ if (nodeName !== name) {
698
+ // HTML documents match element names case-insensitively
699
+ return (
700
+ type === 1 &&
701
+ nodeName.toLowerCase() === name.toLowerCase() &&
702
+ isInHtmlDocument(node)
703
+ );
526
704
  }
527
-
528
- // Prefixed name match
529
- const ns = context.namespaces[prefix];
530
- return nodeName === name && nodeNs === ns;
705
+ // A QName without prefix only matches nodes in no namespace (2.3).
706
+ // HTML elements are in the XHTML namespace in the DOM but in none for
707
+ // libxslt, which receives HTML documents re-parsed from their markup.
708
+ if (this.legacyNameTests || node.namespaceURI === null) return true;
709
+ return type === 1 && isInHtmlDocument(node);
531
710
  }
532
711
 
533
712
  matchNodeTypeTest(nodeType, node) {
@@ -535,7 +714,8 @@ export class XPathEvaluator {
535
714
  case "node":
536
715
  return true;
537
716
  case "text":
538
- return node.nodeType === 3;
717
+ // The XPath data model has no CDATA sections: they are text nodes
718
+ return node.nodeType === 3 || node.nodeType === 4;
539
719
  case "comment":
540
720
  return node.nodeType === 8;
541
721
  case "processing-instruction":
@@ -590,11 +770,80 @@ export class XPathEvaluator {
590
770
  return context.variables[name];
591
771
  }
592
772
 
773
+ /**
774
+ * Register additional functions, for example the XSLT function library.
775
+ *
776
+ * Existing names are overwritten, so a host language can also specialise a
777
+ * core function. Each function is called as `fn(args, context)` with the
778
+ * evaluator as `this`.
779
+ *
780
+ * Extension functions are keyed by expanded name, `{namespace-uri}local`
781
+ * (see {@link expandedFunctionName}), and are found through whatever prefix
782
+ * the expression binds to that namespace. A literal `prefix:local` key is
783
+ * still honoured when the prefix does not resolve to a registered function.
784
+ *
785
+ * @param {Object<string, Function>} functions - Functions by name
786
+ * @returns {XPathEvaluator} This evaluator, to allow chaining
787
+ *
788
+ * @example
789
+ * evaluator.registerFunctions({ '{urn:my}double': (args, ctx) => 2 });
790
+ * // callable as my:double() when my is bound to urn:my
791
+ */
792
+ registerFunctions(functions) {
793
+ for (const [name, fn] of Object.entries(functions)) {
794
+ this.functions[name] = fn;
795
+ }
796
+ return this;
797
+ }
798
+
799
+ /**
800
+ * Find the implementation of a possibly prefixed function name.
801
+ *
802
+ * @param {string} localName - The local part of the function name
803
+ * @param {string|null} prefix - The prefix, or null for core functions
804
+ * @param {Object<string, string>} namespaces - Prefix bindings
805
+ * @returns {Function|null} The function, or null when none is registered
806
+ */
807
+ resolveFunction(localName, prefix, namespaces) {
808
+ if (!prefix) return this.getFunction(localName);
809
+
810
+ const namespaceUri = resolveNamespacePrefix(prefix, namespaces);
811
+ const byUri =
812
+ namespaceUri === null
813
+ ? null
814
+ : this.getFunction(expandedFunctionName(namespaceUri, localName));
815
+ return byUri ?? this.getFunction(`${prefix}:${localName}`);
816
+ }
817
+
818
+ /**
819
+ * Whether a function is registered under an expanded name.
820
+ *
821
+ * @param {string} localName - The local part of the function name
822
+ * @param {string|null} [namespaceUri] - The namespace, null for core functions
823
+ * @returns {boolean} True when the function exists
824
+ */
825
+ hasFunction(localName, namespaceUri = null) {
826
+ const key = namespaceUri
827
+ ? expandedFunctionName(namespaceUri, localName)
828
+ : localName;
829
+ return this.getFunction(key) !== null;
830
+ }
831
+
832
+ /**
833
+ * Own entry of the function table, never an inherited property.
834
+ *
835
+ * @param {string} key - Registry key
836
+ * @returns {Function|null} The function, or null
837
+ */
838
+ getFunction(key) {
839
+ return Object.hasOwn(this.functions, key) ? this.functions[key] : null;
840
+ }
841
+
593
842
  evalFunctionCall(ast, context) {
594
- const name = ast.prefix ? `${ast.prefix}:${ast.name}` : ast.name;
595
- const fn = this.functions[name];
843
+ const fn = this.resolveFunction(ast.name, ast.prefix, context.namespaces);
596
844
 
597
845
  if (!fn) {
846
+ const name = ast.prefix ? `${ast.prefix}:${ast.name}` : ast.name;
598
847
  throw new Error(`Unknown function: ${name}`);
599
848
  }
600
849
 
@@ -614,12 +863,7 @@ export class XPathEvaluator {
614
863
  toNumber(value) {
615
864
  if (typeof value === "number") return value;
616
865
  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
- }
866
+ if (typeof value === "string") return parseXPathNumber(value);
623
867
  if (Array.isArray(value)) {
624
868
  return this.toNumber(this.toString(value));
625
869
  }
@@ -631,13 +875,7 @@ export class XPathEvaluator {
631
875
 
632
876
  toString(value) {
633
877
  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
- }
878
+ if (typeof value === "number") return formatXPathNumber(value);
641
879
  if (typeof value === "boolean") return value ? "true" : "false";
642
880
  if (Array.isArray(value)) {
643
881
  if (value.length === 0) return "";
@@ -659,7 +897,7 @@ export class XPathEvaluator {
659
897
  // Document Fragment
660
898
  let text = "";
661
899
  const walker = (n) => {
662
- if (n.nodeType === 3) {
900
+ if (n.nodeType === 3 || n.nodeType === 4) {
663
901
  text += n.nodeValue || "";
664
902
  } else if (n.childNodes) {
665
903
  for (const child of n.childNodes) {
@@ -671,11 +909,21 @@ export class XPathEvaluator {
671
909
  return text;
672
910
  }
673
911
 
674
- case 2: // Attribute
675
912
  case 3: // Text
676
- case 4: // CDATA
913
+ case 4: {
914
+ // CDATA. A run of adjacent text nodes is one XPath text node (5.7)
915
+ let text = node.nodeValue;
916
+ for (let next = node.nextSibling; isTextNode(next);) {
917
+ text += next.nodeValue;
918
+ next = next.nextSibling;
919
+ }
920
+ return text;
921
+ }
922
+
923
+ case 2: // Attribute
677
924
  case 7: // Processing Instruction
678
925
  case 8: // Comment
926
+ case NAMESPACE_NODE: // The namespace URI
679
927
  return node.nodeValue || "";
680
928
 
681
929
  default:
@@ -688,6 +936,18 @@ export class XPathEvaluator {
688
936
  const leftIsNodeSet = Array.isArray(left);
689
937
  const rightIsNodeSet = Array.isArray(right);
690
938
 
939
+ // A node-set compared with a boolean is converted with boolean() (3.4)
940
+ if (
941
+ (leftIsNodeSet && typeof right === "boolean") ||
942
+ (rightIsNodeSet && typeof left === "boolean")
943
+ ) {
944
+ return this.comparePrimitive(
945
+ this.toBoolean(left),
946
+ this.toBoolean(right),
947
+ operator,
948
+ );
949
+ }
950
+
691
951
  // Node-set comparisons
692
952
  if (leftIsNodeSet && rightIsNodeSet) {
693
953
  for (const l of left) {
@@ -763,46 +1023,46 @@ export class XPathEvaluator {
763
1023
  }
764
1024
  }
765
1025
 
1026
+ /**
1027
+ * Sort nodes in document order; attributes and namespace nodes sort after
1028
+ * their element and before its children (see documentOrder.js).
1029
+ *
1030
+ * With a {@link DocumentOrderIndex} (set by the XSLT engine for each
1031
+ * transformation, or created on first use when the DOM has no
1032
+ * `compareDocumentPosition`, as xmldom 0.8), nodes are sorted by their
1033
+ * precomputed positions; otherwise through `compareDocumentPosition`.
1034
+ *
1035
+ * @param {Node[]} nodes - The nodes, sorted in place
1036
+ * @returns {Node[]} The same array
1037
+ */
766
1038
  sortByDocumentOrder(nodes) {
767
1039
  if (nodes.length <= 1) return nodes;
1040
+ // Number the trees once per evaluation unless the DOM compares positions
1041
+ // natively: compareDocumentPosition written in JavaScript (xmldom,
1042
+ // jsdom) costs a tree walk per comparison
1043
+ if (!this.documentOrder && !hasNativePositionComparison(nodes[0])) {
1044
+ this.documentOrder = new DocumentOrderIndex();
1045
+ this.ownsDocumentOrder = true;
1046
+ }
1047
+ if (this.documentOrder) return this.documentOrder.sort(nodes);
768
1048
 
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
- });
1049
+ return nodes.sort((a, b) =>
1050
+ a === b ? 0 : compareNodeOrder(a, b, compareDomPositions),
1051
+ );
780
1052
  }
781
1053
 
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;
1054
+ /**
1055
+ * Use a new {@link DocumentOrderIndex} from now on, e.g. for a new
1056
+ * transformation (the trees may have changed since the previous one), or
1057
+ * none (null): `compareDocumentPosition` then orders the nodes whenever
1058
+ * the DOM has it.
1059
+ *
1060
+ * @param {DocumentOrderIndex|null} [index] - The index to use
1061
+ * @returns {void}
1062
+ */
1063
+ resetDocumentOrder(index = null) {
1064
+ this.documentOrder = index;
1065
+ this.ownsDocumentOrder = false;
806
1066
  }
807
1067
 
808
1068
  /**
@@ -813,54 +1073,7 @@ export class XPathEvaluator {
813
1073
  // Node set functions
814
1074
  last: (args, ctx) => ctx.size,
815
1075
  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
- },
1076
+ ...createNodeSetFunctions(this),
864
1077
 
865
1078
  // String functions
866
1079
  string: (args, ctx) => {
@@ -898,61 +1111,32 @@ export class XPathEvaluator {
898
1111
  },
899
1112
  substring: (args, ctx) => {
900
1113
  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);
1114
+ const start = this.toNumber(this.evaluate(args[1], ctx));
1115
+ const length =
1116
+ args.length > 2
1117
+ ? this.toNumber(this.evaluate(args[2], ctx))
1118
+ : undefined;
1119
+ return xpathSubstring(str, start, length);
925
1120
  },
926
1121
  "string-length": (args, ctx) => {
927
1122
  const str =
928
1123
  args.length === 0
929
1124
  ? this.toString([ctx.node])
930
1125
  : this.toString(this.evaluate(args[0], ctx));
931
- return str.length;
1126
+ return codePointLength(str);
932
1127
  },
933
1128
  "normalize-space": (args, ctx) => {
934
1129
  const str =
935
1130
  args.length === 0
936
1131
  ? this.toString([ctx.node])
937
1132
  : this.toString(this.evaluate(args[0], ctx));
938
- return str.trim().replace(/\s+/g, " ");
1133
+ return normalizeXmlSpace(str);
939
1134
  },
940
1135
  translate: (args, ctx) => {
941
1136
  const str = this.toString(this.evaluate(args[0], ctx));
942
1137
  const from = this.toString(this.evaluate(args[1], ctx));
943
1138
  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;
1139
+ return xpathTranslate(str, from, to);
956
1140
  },
957
1141
 
958
1142
  // Boolean functions
@@ -964,21 +1148,6 @@ export class XPathEvaluator {
964
1148
  },
965
1149
  true: () => true,
966
1150
  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
1151
 
983
1152
  // Number functions
984
1153
  number: (args, ctx) => {
@@ -987,14 +1156,6 @@ export class XPathEvaluator {
987
1156
  }
988
1157
  return this.toNumber(this.evaluate(args[0], ctx));
989
1158
  },
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
1159
  floor: (args, ctx) => {
999
1160
  return Math.floor(this.toNumber(this.evaluate(args[0], ctx)));
1000
1161
  },