@tradik/xslt-processor 1.1.1 → 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.
- package/LICENSE.md +1 -1
- package/README.md +102 -757
- package/bin/lib/decode.js +15 -0
- package/bin/lib/dom.js +177 -0
- package/bin/lib/loaders.js +127 -0
- package/bin/lib/options.js +17 -0
- package/bin/lib/output.js +114 -0
- package/bin/lib/paths.js +3 -3
- package/bin/lib/transform.js +124 -33
- package/bin/xslt.js +26 -27
- package/dist/xslt-processor.browser.js +8784 -2720
- package/dist/xslt-processor.browser.js.map +4 -4
- package/dist/xslt-processor.browser.min.js +13 -6
- package/dist/xslt-processor.browser.min.js.map +4 -4
- package/dist/xslt-processor.cjs +8789 -2723
- package/dist/xslt-processor.cjs.map +4 -4
- package/dist/xslt-processor.d.cts +380 -21
- package/dist/xslt-processor.d.ts +380 -21
- package/dist/xslt-processor.js +8770 -2722
- package/dist/xslt-processor.js.map +4 -4
- package/package.json +51 -11
- package/src/XSLTProcessor.js +343 -66
- package/src/async/abort.js +63 -0
- package/src/async/documentUris.js +128 -0
- package/src/async/loaders.js +134 -0
- package/src/async/preload.js +159 -0
- package/src/async/processor.js +206 -0
- package/src/async/stream.js +125 -0
- package/src/bridge/engine.js +221 -0
- package/src/bridge/loader.js +78 -0
- package/src/bridge/results.js +75 -0
- package/src/bridge/version.js +63 -0
- package/src/index.js +16 -4
- package/src/io/decode.js +140 -0
- package/src/io/readSource.js +167 -0
- package/src/xpath/axes.js +562 -0
- package/src/xpath/documentOrder.js +270 -0
- package/src/xpath/evaluator.js +475 -355
- package/src/xpath/index.js +8 -2
- package/src/xpath/namespaceNodes.js +172 -0
- package/src/xpath/nodeSetFunctions.js +169 -0
- package/src/xpath/parser.js +30 -5
- package/src/xpath/strings.js +183 -0
- package/src/xpath/tokenizer.js +37 -23
- package/src/xslt/attributeSets.js +95 -0
- package/src/xslt/avt.js +103 -0
- package/src/xslt/computedNames.js +91 -0
- package/src/xslt/copying.js +212 -0
- package/src/xslt/declarationNames.js +80 -0
- package/src/xslt/domParsing.js +95 -0
- package/src/xslt/elements.js +1 -1
- package/src/xslt/engine/bindings.js +195 -0
- package/src/xslt/engine/context.js +105 -0
- package/src/xslt/engine/controlFlow.js +145 -0
- package/src/xslt/engine/copyInstructions.js +133 -0
- package/src/xslt/engine/declarations.js +233 -0
- package/src/xslt/engine/functionSupport.js +103 -0
- package/src/xslt/engine/methods.js +33 -0
- package/src/xslt/engine/nodeConstruction.js +187 -0
- package/src/xslt/engine/numbering.js +104 -0
- package/src/xslt/engine/outputDeclaration.js +77 -0
- package/src/xslt/engine/sequenceConstructor.js +228 -0
- package/src/xslt/engine/stylesheetLoading.js +208 -0
- package/src/xslt/engine/templateInvocation.js +253 -0
- package/src/xslt/engine/templateRules.js +243 -0
- package/src/xslt/engine/textInstructions.js +171 -0
- package/src/xslt/engine/topLevel.js +130 -0
- package/src/xslt/engine/transformation.js +263 -0
- package/src/xslt/engine/workStack.js +245 -0
- package/src/xslt/engine.js +176 -2020
- package/src/xslt/exslt/arguments.js +99 -0
- package/src/xslt/exslt/calendar.js +120 -0
- package/src/xslt/exslt/common.js +44 -0
- package/src/xslt/exslt/dateCalc.js +261 -0
- package/src/xslt/exslt/dateFormat.js +150 -0
- package/src/xslt/exslt/dateParse.js +265 -0
- package/src/xslt/exslt/dates.js +259 -0
- package/src/xslt/exslt/duration.js +207 -0
- package/src/xslt/exslt/dynamic.js +59 -0
- package/src/xslt/exslt/index.js +59 -0
- package/src/xslt/exslt/math.js +177 -0
- package/src/xslt/exslt/sets.js +96 -0
- package/src/xslt/exslt/stringOps.js +163 -0
- package/src/xslt/exslt/strings.js +147 -0
- package/src/xslt/exslt/uri.js +92 -0
- package/src/xslt/formatNumber.js +22 -9
- package/src/xslt/forwardsCompatible.js +75 -0
- package/src/xslt/functions.js +94 -15
- package/src/xslt/index.js +7 -1
- package/src/xslt/keys.js +51 -28
- package/src/xslt/literalResult.js +63 -7
- package/src/xslt/matchScope.js +116 -0
- package/src/xslt/number.js +171 -78
- package/src/xslt/numberFormat.js +124 -26
- package/src/xslt/outputNames.js +58 -0
- package/src/xslt/patternCompiler.js +175 -0
- package/src/xslt/patterns.js +324 -0
- package/src/xslt/qname.js +90 -0
- package/src/xslt/resultDocument.js +98 -0
- package/src/xslt/resultNamespaces.js +219 -0
- package/src/xslt/resultTree.js +143 -6
- package/src/xslt/serializer/baseWriter.js +173 -66
- package/src/xslt/serializer/chunks.js +120 -0
- package/src/xslt/serializer/constants.js +14 -0
- package/src/xslt/serializer/encoding.js +327 -0
- package/src/xslt/serializer/escape.js +49 -12
- package/src/xslt/serializer/frames.js +168 -0
- package/src/xslt/serializer/htmlDoctype.js +102 -0
- package/src/xslt/serializer/htmlEntities.js +77 -0
- package/src/xslt/serializer/htmlSerializer.js +123 -25
- package/src/xslt/serializer/settings.js +89 -13
- package/src/xslt/serializer/textSerializer.js +58 -10
- package/src/xslt/serializer/xhtmlDocument.js +103 -0
- package/src/xslt/serializer/xmlSerializer.js +113 -13
- package/src/xslt/serializer.js +50 -17
- package/src/xslt/sort.js +151 -0
- package/src/xslt/spaceNameTests.js +115 -0
- package/src/xslt/stylesheetChecks.js +206 -0
- package/src/xslt/stylesheetNamespaces.js +266 -0
- package/src/xslt/variables.js +152 -0
- package/src/xslt/whitespace.js +43 -27
- package/LICENSE +0 -29
package/src/xpath/evaluator.js
CHANGED
|
@@ -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
|
|
@@ -92,7 +204,18 @@ export class XPathContext {
|
|
|
92
204
|
* XPath Evaluator
|
|
93
205
|
*/
|
|
94
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
|
+
*/
|
|
95
215
|
constructor(options = {}) {
|
|
216
|
+
this.legacyNameTests = options.legacyNameTests === true;
|
|
217
|
+
this.resetNamespaceNodes();
|
|
218
|
+
this.resetDocumentOrder();
|
|
96
219
|
this.functions = Object.assign(
|
|
97
220
|
Object.create(null),
|
|
98
221
|
this.initCoreFunctions(),
|
|
@@ -105,6 +228,18 @@ export class XPathEvaluator {
|
|
|
105
228
|
this.recursionDepth = 0;
|
|
106
229
|
}
|
|
107
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
|
+
|
|
108
243
|
/**
|
|
109
244
|
* Evaluate XPath expression against a context
|
|
110
245
|
* @throws {Error} If recursion depth exceeds limit
|
|
@@ -119,6 +254,11 @@ export class XPathEvaluator {
|
|
|
119
254
|
throw new Error("Invalid AST: missing type property");
|
|
120
255
|
}
|
|
121
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
|
+
}
|
|
122
262
|
this.recursionDepth++;
|
|
123
263
|
if (this.recursionDepth > this.maxRecursionDepth) {
|
|
124
264
|
this.recursionDepth = 0;
|
|
@@ -144,9 +284,8 @@ export class XPathEvaluator {
|
|
|
144
284
|
case NodeType.AND_EXPR:
|
|
145
285
|
return this.evalAndExpr(ast, context);
|
|
146
286
|
case NodeType.EQUALITY_EXPR:
|
|
147
|
-
return this.evalEqualityExpr(ast, context);
|
|
148
287
|
case NodeType.RELATIONAL_EXPR:
|
|
149
|
-
return this.
|
|
288
|
+
return this.evalComparisonExpr(ast, context);
|
|
150
289
|
case NodeType.ADDITIVE_EXPR:
|
|
151
290
|
return this.evalAdditiveExpr(ast, context);
|
|
152
291
|
case NodeType.MULTIPLICATIVE_EXPR:
|
|
@@ -210,14 +349,15 @@ export class XPathEvaluator {
|
|
|
210
349
|
);
|
|
211
350
|
}
|
|
212
351
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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) {
|
|
221
361
|
const left = this.evaluate(ast.left, context);
|
|
222
362
|
const right = this.evaluate(ast.right, context);
|
|
223
363
|
return this.compareValues(left, right, ast.operator);
|
|
@@ -298,10 +438,9 @@ export class XPathEvaluator {
|
|
|
298
438
|
let nodes;
|
|
299
439
|
|
|
300
440
|
if (ast.absolute) {
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
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)];
|
|
305
444
|
} else {
|
|
306
445
|
nodes = [context.node];
|
|
307
446
|
}
|
|
@@ -314,192 +453,190 @@ export class XPathEvaluator {
|
|
|
314
453
|
return nodes;
|
|
315
454
|
}
|
|
316
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
|
+
*/
|
|
317
468
|
evalStepOnNodes(step, nodes, context) {
|
|
318
469
|
const allNodes = Array.isArray(nodes) ? nodes : [nodes];
|
|
319
|
-
|
|
470
|
+
const reverse = REVERSE_AXES.has(step.axis);
|
|
320
471
|
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
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
|
+
}
|
|
324
477
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
+
}
|
|
328
486
|
}
|
|
487
|
+
this.validateResultSize(result);
|
|
329
488
|
}
|
|
330
489
|
|
|
331
|
-
|
|
332
|
-
const uniqueResult = [...new Set(result)];
|
|
333
|
-
this.validateResultSize(uniqueResult);
|
|
334
|
-
return this.sortByDocumentOrder(uniqueResult);
|
|
490
|
+
return this.sortByDocumentOrder(result);
|
|
335
491
|
}
|
|
336
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
|
+
*/
|
|
337
505
|
evalStep(step, context) {
|
|
338
|
-
|
|
339
|
-
|
|
506
|
+
const { predicates } = step;
|
|
507
|
+
const position = this.positionalPredicate(step);
|
|
508
|
+
let nodes;
|
|
509
|
+
let first = 0;
|
|
340
510
|
|
|
341
|
-
|
|
342
|
-
|
|
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
|
+
}
|
|
343
519
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
nodes = this.filterByPredicate(nodes, predicate, context);
|
|
520
|
+
for (let i = first; i < predicates.length; i++) {
|
|
521
|
+
nodes = this.filterByPredicate(nodes, predicates[i], context);
|
|
347
522
|
}
|
|
348
523
|
|
|
349
524
|
return nodes;
|
|
350
525
|
}
|
|
351
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
|
+
*/
|
|
352
585
|
getAxisNodes(axis, node) {
|
|
353
586
|
switch (axis) {
|
|
354
587
|
case "child":
|
|
355
|
-
return
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return
|
|
359
|
-
|
|
588
|
+
return childAxis(node);
|
|
589
|
+
case "parent": {
|
|
590
|
+
const parent = parentOf(node);
|
|
591
|
+
return parent ? [parent] : [];
|
|
592
|
+
}
|
|
360
593
|
case "self":
|
|
361
594
|
return [node];
|
|
362
|
-
|
|
363
595
|
case "descendant":
|
|
364
|
-
return
|
|
365
|
-
|
|
596
|
+
return descendantAxis(node, false);
|
|
366
597
|
case "descendant-or-self":
|
|
367
|
-
return
|
|
368
|
-
|
|
598
|
+
return descendantAxis(node, true);
|
|
369
599
|
case "ancestor":
|
|
370
|
-
return
|
|
371
|
-
|
|
600
|
+
return ancestorAxis(node, false);
|
|
372
601
|
case "ancestor-or-self":
|
|
373
|
-
return
|
|
374
|
-
|
|
602
|
+
return ancestorAxis(node, true);
|
|
375
603
|
case "following-sibling":
|
|
376
|
-
return
|
|
377
|
-
|
|
604
|
+
return followingSiblingAxis(node);
|
|
378
605
|
case "preceding-sibling":
|
|
379
|
-
return
|
|
380
|
-
|
|
606
|
+
return precedingSiblingAxis(node);
|
|
381
607
|
case "following":
|
|
382
|
-
return
|
|
383
|
-
|
|
608
|
+
return followingAxis(node);
|
|
384
609
|
case "preceding":
|
|
385
|
-
return
|
|
386
|
-
|
|
610
|
+
return precedingAxis(node);
|
|
387
611
|
case "attribute":
|
|
388
|
-
|
|
389
|
-
return Array.from(node.attributes);
|
|
390
|
-
}
|
|
391
|
-
return [];
|
|
392
|
-
|
|
612
|
+
return attributeAxis(node);
|
|
393
613
|
case "namespace":
|
|
394
|
-
|
|
395
|
-
return [];
|
|
396
|
-
|
|
614
|
+
return namespaceAxis(node, this.namespaceNodes);
|
|
397
615
|
default:
|
|
398
616
|
throw new Error(`Unknown axis: ${axis}`);
|
|
399
617
|
}
|
|
400
618
|
}
|
|
401
619
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
return result;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
getAncestors(node, includeSelf) {
|
|
420
|
-
const result = includeSelf ? [node] : [];
|
|
421
|
-
let current = node.parentNode;
|
|
422
|
-
|
|
423
|
-
while (current) {
|
|
424
|
-
result.push(current);
|
|
425
|
-
current = current.parentNode;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
return result;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
getFollowingSiblings(node) {
|
|
432
|
-
const result = [];
|
|
433
|
-
if (!node) return result;
|
|
434
|
-
let current = node.nextSibling;
|
|
435
|
-
|
|
436
|
-
while (current) {
|
|
437
|
-
result.push(current);
|
|
438
|
-
current = current.nextSibling;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return result;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
getPrecedingSiblings(node) {
|
|
445
|
-
const result = [];
|
|
446
|
-
if (!node) return result;
|
|
447
|
-
let current = node.previousSibling;
|
|
448
|
-
|
|
449
|
-
while (current) {
|
|
450
|
-
result.push(current);
|
|
451
|
-
current = current.previousSibling;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return result.reverse();
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
getFollowing(node) {
|
|
458
|
-
const result = [];
|
|
459
|
-
let current = node;
|
|
460
|
-
|
|
461
|
-
// Go to next sibling, or ancestor's next sibling
|
|
462
|
-
while (current) {
|
|
463
|
-
if (current.nextSibling) {
|
|
464
|
-
current = current.nextSibling;
|
|
465
|
-
result.push(current);
|
|
466
|
-
// Add all descendants
|
|
467
|
-
result.push(...this.getDescendants(current, false));
|
|
468
|
-
} else {
|
|
469
|
-
current = current.parentNode;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
return result;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
getPreceding(node) {
|
|
477
|
-
const result = [];
|
|
478
|
-
let current = node;
|
|
479
|
-
|
|
480
|
-
while (current) {
|
|
481
|
-
if (current.previousSibling) {
|
|
482
|
-
current = current.previousSibling;
|
|
483
|
-
// Add descendants in reverse order, then the node
|
|
484
|
-
const descendants = this.getDescendants(current, false);
|
|
485
|
-
result.unshift(...descendants.reverse());
|
|
486
|
-
result.unshift(current);
|
|
487
|
-
} else {
|
|
488
|
-
current = current.parentNode;
|
|
489
|
-
if (current && current.nodeType !== 9) {
|
|
490
|
-
// Not document
|
|
491
|
-
// Don't add ancestors
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return result;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
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) {
|
|
500
632
|
switch (nodeTest.type) {
|
|
501
|
-
case NodeType.NAME_TEST:
|
|
502
|
-
|
|
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
|
+
}
|
|
503
640
|
|
|
504
641
|
case NodeType.NODE_TYPE_TEST:
|
|
505
642
|
return this.matchNodeTypeTest(nodeTest.nodeType, node);
|
|
@@ -512,43 +649,64 @@ export class XPathEvaluator {
|
|
|
512
649
|
}
|
|
513
650
|
}
|
|
514
651
|
|
|
515
|
-
|
|
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);
|
|
516
667
|
// Only element and attribute nodes have names
|
|
517
|
-
if (
|
|
518
|
-
return false;
|
|
519
|
-
}
|
|
668
|
+
if (type !== 1 && type !== 2) return false;
|
|
520
669
|
|
|
521
|
-
const name = nodeTest
|
|
522
|
-
const prefix = nodeTest.prefix;
|
|
670
|
+
const { name, prefix } = nodeTest;
|
|
523
671
|
|
|
524
|
-
|
|
525
|
-
if (name === "*" && !prefix) {
|
|
526
|
-
return true;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Get node's local name and namespace
|
|
530
|
-
const nodeName = node.localName || node.nodeName;
|
|
531
|
-
const nodeNs = node.namespaceURI || null;
|
|
672
|
+
if (!prefix) return this.matchUnprefixedName(name, node, type);
|
|
532
673
|
|
|
533
|
-
|
|
534
|
-
if (
|
|
535
|
-
|
|
536
|
-
|
|
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}`);
|
|
537
679
|
}
|
|
680
|
+
if ((node.namespaceURI || null) !== namespaceUri) return false;
|
|
681
|
+
return name === "*" || (node.localName || node.nodeName) === name;
|
|
682
|
+
}
|
|
538
683
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
+
);
|
|
547
704
|
}
|
|
548
|
-
|
|
549
|
-
//
|
|
550
|
-
|
|
551
|
-
|
|
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);
|
|
552
710
|
}
|
|
553
711
|
|
|
554
712
|
matchNodeTypeTest(nodeType, node) {
|
|
@@ -619,11 +777,17 @@ export class XPathEvaluator {
|
|
|
619
777
|
* core function. Each function is called as `fn(args, context)` with the
|
|
620
778
|
* evaluator as `this`.
|
|
621
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
|
+
*
|
|
622
785
|
* @param {Object<string, Function>} functions - Functions by name
|
|
623
786
|
* @returns {XPathEvaluator} This evaluator, to allow chaining
|
|
624
787
|
*
|
|
625
788
|
* @example
|
|
626
|
-
* evaluator.registerFunctions({ 'my
|
|
789
|
+
* evaluator.registerFunctions({ '{urn:my}double': (args, ctx) => 2 });
|
|
790
|
+
* // callable as my:double() when my is bound to urn:my
|
|
627
791
|
*/
|
|
628
792
|
registerFunctions(functions) {
|
|
629
793
|
for (const [name, fn] of Object.entries(functions)) {
|
|
@@ -632,14 +796,58 @@ export class XPathEvaluator {
|
|
|
632
796
|
return this;
|
|
633
797
|
}
|
|
634
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
|
+
|
|
635
842
|
evalFunctionCall(ast, context) {
|
|
636
|
-
const
|
|
843
|
+
const fn = this.resolveFunction(ast.name, ast.prefix, context.namespaces);
|
|
637
844
|
|
|
638
|
-
if (!
|
|
845
|
+
if (!fn) {
|
|
846
|
+
const name = ast.prefix ? `${ast.prefix}:${ast.name}` : ast.name;
|
|
639
847
|
throw new Error(`Unknown function: ${name}`);
|
|
640
848
|
}
|
|
641
849
|
|
|
642
|
-
return
|
|
850
|
+
return fn.call(this, ast.args, context);
|
|
643
851
|
}
|
|
644
852
|
|
|
645
853
|
// Type conversion functions
|
|
@@ -655,12 +863,7 @@ export class XPathEvaluator {
|
|
|
655
863
|
toNumber(value) {
|
|
656
864
|
if (typeof value === "number") return value;
|
|
657
865
|
if (typeof value === "boolean") return value ? 1 : 0;
|
|
658
|
-
if (typeof value === "string")
|
|
659
|
-
const trimmed = value.trim();
|
|
660
|
-
if (trimmed === "") return NaN;
|
|
661
|
-
const num = Number(trimmed);
|
|
662
|
-
return num;
|
|
663
|
-
}
|
|
866
|
+
if (typeof value === "string") return parseXPathNumber(value);
|
|
664
867
|
if (Array.isArray(value)) {
|
|
665
868
|
return this.toNumber(this.toString(value));
|
|
666
869
|
}
|
|
@@ -672,13 +875,7 @@ export class XPathEvaluator {
|
|
|
672
875
|
|
|
673
876
|
toString(value) {
|
|
674
877
|
if (typeof value === "string") return value;
|
|
675
|
-
if (typeof value === "number")
|
|
676
|
-
if (isNaN(value)) return "NaN";
|
|
677
|
-
if (value === Infinity) return "Infinity";
|
|
678
|
-
if (value === -Infinity) return "-Infinity";
|
|
679
|
-
if (value === 0) return "0";
|
|
680
|
-
return String(value);
|
|
681
|
-
}
|
|
878
|
+
if (typeof value === "number") return formatXPathNumber(value);
|
|
682
879
|
if (typeof value === "boolean") return value ? "true" : "false";
|
|
683
880
|
if (Array.isArray(value)) {
|
|
684
881
|
if (value.length === 0) return "";
|
|
@@ -712,11 +909,21 @@ export class XPathEvaluator {
|
|
|
712
909
|
return text;
|
|
713
910
|
}
|
|
714
911
|
|
|
715
|
-
case 2: // Attribute
|
|
716
912
|
case 3: // Text
|
|
717
|
-
case 4:
|
|
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
|
|
718
924
|
case 7: // Processing Instruction
|
|
719
925
|
case 8: // Comment
|
|
926
|
+
case NAMESPACE_NODE: // The namespace URI
|
|
720
927
|
return node.nodeValue || "";
|
|
721
928
|
|
|
722
929
|
default:
|
|
@@ -729,6 +936,18 @@ export class XPathEvaluator {
|
|
|
729
936
|
const leftIsNodeSet = Array.isArray(left);
|
|
730
937
|
const rightIsNodeSet = Array.isArray(right);
|
|
731
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
|
+
|
|
732
951
|
// Node-set comparisons
|
|
733
952
|
if (leftIsNodeSet && rightIsNodeSet) {
|
|
734
953
|
for (const l of left) {
|
|
@@ -804,46 +1023,46 @@ export class XPathEvaluator {
|
|
|
804
1023
|
}
|
|
805
1024
|
}
|
|
806
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
|
+
*/
|
|
807
1038
|
sortByDocumentOrder(nodes) {
|
|
808
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);
|
|
809
1048
|
|
|
810
|
-
return nodes.sort((a, b) =>
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
const position = a.compareDocumentPosition
|
|
814
|
-
? a.compareDocumentPosition(b)
|
|
815
|
-
: this.compareDocumentPositionFallback(a, b);
|
|
816
|
-
|
|
817
|
-
if (position & 4) return -1; // a before b
|
|
818
|
-
if (position & 2) return 1; // a after b
|
|
819
|
-
return 0;
|
|
820
|
-
});
|
|
1049
|
+
return nodes.sort((a, b) =>
|
|
1050
|
+
a === b ? 0 : compareNodeOrder(a, b, compareDomPositions),
|
|
1051
|
+
);
|
|
821
1052
|
}
|
|
822
1053
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
return path;
|
|
836
|
-
};
|
|
837
|
-
|
|
838
|
-
const pathA = getPath(a);
|
|
839
|
-
const pathB = getPath(b);
|
|
840
|
-
|
|
841
|
-
for (let i = 0; i < Math.min(pathA.length, pathB.length); i++) {
|
|
842
|
-
if (pathA[i] < pathB[i]) return 4; // a before b
|
|
843
|
-
if (pathA[i] > pathB[i]) return 2; // a after b
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
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;
|
|
847
1066
|
}
|
|
848
1067
|
|
|
849
1068
|
/**
|
|
@@ -854,54 +1073,7 @@ export class XPathEvaluator {
|
|
|
854
1073
|
// Node set functions
|
|
855
1074
|
last: (args, ctx) => ctx.size,
|
|
856
1075
|
position: (args, ctx) => ctx.position,
|
|
857
|
-
|
|
858
|
-
const nodeSet = this.evaluate(args[0], ctx);
|
|
859
|
-
return Array.isArray(nodeSet) ? nodeSet.length : 1;
|
|
860
|
-
},
|
|
861
|
-
id: (args, ctx) => {
|
|
862
|
-
const value = this.toString(this.evaluate(args[0], ctx));
|
|
863
|
-
const doc = ctx.node.ownerDocument || ctx.node;
|
|
864
|
-
const ids = value.split(/\s+/).filter((id) => id);
|
|
865
|
-
const result = [];
|
|
866
|
-
for (const id of ids) {
|
|
867
|
-
const el = doc.getElementById(id);
|
|
868
|
-
if (el) result.push(el);
|
|
869
|
-
}
|
|
870
|
-
return result;
|
|
871
|
-
},
|
|
872
|
-
"local-name": (args, ctx) => {
|
|
873
|
-
let node;
|
|
874
|
-
if (args.length === 0) {
|
|
875
|
-
node = ctx.node;
|
|
876
|
-
} else {
|
|
877
|
-
const nodeSet = this.evaluate(args[0], ctx);
|
|
878
|
-
node = Array.isArray(nodeSet) ? nodeSet[0] : nodeSet;
|
|
879
|
-
}
|
|
880
|
-
if (!node) return "";
|
|
881
|
-
return node.localName || node.nodeName || "";
|
|
882
|
-
},
|
|
883
|
-
"namespace-uri": (args, ctx) => {
|
|
884
|
-
let node;
|
|
885
|
-
if (args.length === 0) {
|
|
886
|
-
node = ctx.node;
|
|
887
|
-
} else {
|
|
888
|
-
const nodeSet = this.evaluate(args[0], ctx);
|
|
889
|
-
node = Array.isArray(nodeSet) ? nodeSet[0] : nodeSet;
|
|
890
|
-
}
|
|
891
|
-
if (!node) return "";
|
|
892
|
-
return node.namespaceURI || "";
|
|
893
|
-
},
|
|
894
|
-
name: (args, ctx) => {
|
|
895
|
-
let node;
|
|
896
|
-
if (args.length === 0) {
|
|
897
|
-
node = ctx.node;
|
|
898
|
-
} else {
|
|
899
|
-
const nodeSet = this.evaluate(args[0], ctx);
|
|
900
|
-
node = Array.isArray(nodeSet) ? nodeSet[0] : nodeSet;
|
|
901
|
-
}
|
|
902
|
-
if (!node) return "";
|
|
903
|
-
return node.nodeName || "";
|
|
904
|
-
},
|
|
1076
|
+
...createNodeSetFunctions(this),
|
|
905
1077
|
|
|
906
1078
|
// String functions
|
|
907
1079
|
string: (args, ctx) => {
|
|
@@ -939,61 +1111,32 @@ export class XPathEvaluator {
|
|
|
939
1111
|
},
|
|
940
1112
|
substring: (args, ctx) => {
|
|
941
1113
|
const str = this.toString(this.evaluate(args[0], ctx));
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
// XPath uses 1-based indexing
|
|
950
|
-
start = start - 1;
|
|
951
|
-
|
|
952
|
-
if (isNaN(start)) return "";
|
|
953
|
-
if (start < 0) {
|
|
954
|
-
if (length !== undefined) {
|
|
955
|
-
length = length + start;
|
|
956
|
-
}
|
|
957
|
-
start = 0;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
if (length !== undefined) {
|
|
961
|
-
if (isNaN(length) || length <= 0) return "";
|
|
962
|
-
return str.substring(start, start + length);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
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);
|
|
966
1120
|
},
|
|
967
1121
|
"string-length": (args, ctx) => {
|
|
968
1122
|
const str =
|
|
969
1123
|
args.length === 0
|
|
970
1124
|
? this.toString([ctx.node])
|
|
971
1125
|
: this.toString(this.evaluate(args[0], ctx));
|
|
972
|
-
return str
|
|
1126
|
+
return codePointLength(str);
|
|
973
1127
|
},
|
|
974
1128
|
"normalize-space": (args, ctx) => {
|
|
975
1129
|
const str =
|
|
976
1130
|
args.length === 0
|
|
977
1131
|
? this.toString([ctx.node])
|
|
978
1132
|
: this.toString(this.evaluate(args[0], ctx));
|
|
979
|
-
return str
|
|
1133
|
+
return normalizeXmlSpace(str);
|
|
980
1134
|
},
|
|
981
1135
|
translate: (args, ctx) => {
|
|
982
1136
|
const str = this.toString(this.evaluate(args[0], ctx));
|
|
983
1137
|
const from = this.toString(this.evaluate(args[1], ctx));
|
|
984
1138
|
const to = this.toString(this.evaluate(args[2], ctx));
|
|
985
|
-
|
|
986
|
-
let result = "";
|
|
987
|
-
for (const char of str) {
|
|
988
|
-
const idx = from.indexOf(char);
|
|
989
|
-
if (idx === -1) {
|
|
990
|
-
result += char;
|
|
991
|
-
} else if (idx < to.length) {
|
|
992
|
-
result += to[idx];
|
|
993
|
-
}
|
|
994
|
-
// If idx >= to.length, character is removed
|
|
995
|
-
}
|
|
996
|
-
return result;
|
|
1139
|
+
return xpathTranslate(str, from, to);
|
|
997
1140
|
},
|
|
998
1141
|
|
|
999
1142
|
// Boolean functions
|
|
@@ -1005,21 +1148,6 @@ export class XPathEvaluator {
|
|
|
1005
1148
|
},
|
|
1006
1149
|
true: () => true,
|
|
1007
1150
|
false: () => false,
|
|
1008
|
-
lang: (args, ctx) => {
|
|
1009
|
-
const lang = this.toString(this.evaluate(args[0], ctx)).toLowerCase();
|
|
1010
|
-
let node = ctx.node;
|
|
1011
|
-
|
|
1012
|
-
while (node && node.nodeType === 1) {
|
|
1013
|
-
const xmlLang =
|
|
1014
|
-
node.getAttribute("xml:lang") || node.getAttribute("lang");
|
|
1015
|
-
if (xmlLang) {
|
|
1016
|
-
const nodeLang = xmlLang.toLowerCase();
|
|
1017
|
-
return nodeLang === lang || nodeLang.startsWith(lang + "-");
|
|
1018
|
-
}
|
|
1019
|
-
node = node.parentNode;
|
|
1020
|
-
}
|
|
1021
|
-
return false;
|
|
1022
|
-
},
|
|
1023
1151
|
|
|
1024
1152
|
// Number functions
|
|
1025
1153
|
number: (args, ctx) => {
|
|
@@ -1028,14 +1156,6 @@ export class XPathEvaluator {
|
|
|
1028
1156
|
}
|
|
1029
1157
|
return this.toNumber(this.evaluate(args[0], ctx));
|
|
1030
1158
|
},
|
|
1031
|
-
sum: (args, ctx) => {
|
|
1032
|
-
const nodeSet = this.evaluate(args[0], ctx);
|
|
1033
|
-
if (!Array.isArray(nodeSet)) return NaN;
|
|
1034
|
-
return nodeSet.reduce(
|
|
1035
|
-
(sum, node) => sum + this.toNumber(this.getStringValue(node)),
|
|
1036
|
-
0,
|
|
1037
|
-
);
|
|
1038
|
-
},
|
|
1039
1159
|
floor: (args, ctx) => {
|
|
1040
1160
|
return Math.floor(this.toNumber(this.evaluate(args[0], ctx)));
|
|
1041
1161
|
},
|