@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.
- package/LICENSE.md +1 -1
- package/README.md +110 -520
- 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 +131 -0
- package/bin/lib/output.js +114 -0
- package/bin/lib/paths.js +186 -0
- package/bin/lib/transform.js +206 -0
- package/bin/xslt.js +73 -168
- package/dist/xslt-processor.browser.js +9564 -1585
- package/dist/xslt-processor.browser.js.map +4 -4
- package/dist/xslt-processor.browser.min.js +13 -2
- package/dist/xslt-processor.browser.min.js.map +4 -4
- package/dist/xslt-processor.cjs +9572 -1586
- package/dist/xslt-processor.cjs.map +4 -4
- package/dist/xslt-processor.d.cts +658 -0
- package/dist/xslt-processor.d.ts +459 -12
- package/dist/xslt-processor.js +9546 -1582
- package/dist/xslt-processor.js.map +4 -4
- package/package.json +71 -20
- package/src/XSLTProcessor.js +494 -48
- 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 +26 -8
- 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 +518 -357
- 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 +57 -0
- 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 +184 -1736
- 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 +233 -0
- package/src/xslt/forwardsCompatible.js +75 -0
- package/src/xslt/functions.js +270 -0
- package/src/xslt/index.js +38 -1
- package/src/xslt/keys.js +164 -0
- package/src/xslt/literalResult.js +223 -0
- package/src/xslt/matchScope.js +116 -0
- package/src/xslt/number.js +271 -0
- package/src/xslt/numberFormat.js +253 -0
- 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 +211 -0
- package/src/xslt/serializer/baseWriter.js +390 -0
- package/src/xslt/serializer/chunks.js +120 -0
- package/src/xslt/serializer/constants.js +92 -0
- package/src/xslt/serializer/encoding.js +327 -0
- package/src/xslt/serializer/escape.js +135 -0
- 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 +239 -0
- package/src/xslt/serializer/indent.js +51 -0
- package/src/xslt/serializer/namespaces.js +68 -0
- package/src/xslt/serializer/rawText.js +41 -0
- package/src/xslt/serializer/settings.js +179 -0
- package/src/xslt/serializer/textSerializer.js +77 -0
- package/src/xslt/serializer/xhtmlDocument.js +103 -0
- package/src/xslt/serializer/xmlSerializer.js +227 -0
- package/src/xslt/serializer.js +90 -0
- 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/templatePriority.js +45 -0
- package/src/xslt/uri.js +68 -0
- package/src/xslt/variables.js +152 -0
- package/src/xslt/whitespace.js +200 -0
- package/LICENSE +0 -29
- package/src/XSLTProcessor.test.js +0 -930
- package/src/xpath/evaluator.test.js +0 -1852
- package/src/xpath/tokenizer.test.js +0 -224
- package/src/xslt/engine.test.js +0 -3130
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
|
|
@@ -51,12 +163,29 @@ const FORBIDDEN_VARIABLE_NAMES = Object.freeze([
|
|
|
51
163
|
* XPath evaluation context
|
|
52
164
|
*/
|
|
53
165
|
export class XPathContext {
|
|
54
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
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
|
-
|
|
470
|
+
const reverse = REVERSE_AXES.has(step.axis);
|
|
299
471
|
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
318
|
-
|
|
506
|
+
const { predicates } = step;
|
|
507
|
+
const position = this.positionalPredicate(step);
|
|
508
|
+
let nodes;
|
|
509
|
+
let first = 0;
|
|
319
510
|
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
return
|
|
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
|
|
344
|
-
|
|
596
|
+
return descendantAxis(node, false);
|
|
345
597
|
case "descendant-or-self":
|
|
346
|
-
return
|
|
347
|
-
|
|
598
|
+
return descendantAxis(node, true);
|
|
348
599
|
case "ancestor":
|
|
349
|
-
return
|
|
350
|
-
|
|
600
|
+
return ancestorAxis(node, false);
|
|
351
601
|
case "ancestor-or-self":
|
|
352
|
-
return
|
|
353
|
-
|
|
602
|
+
return ancestorAxis(node, true);
|
|
354
603
|
case "following-sibling":
|
|
355
|
-
return
|
|
356
|
-
|
|
604
|
+
return followingSiblingAxis(node);
|
|
357
605
|
case "preceding-sibling":
|
|
358
|
-
return
|
|
359
|
-
|
|
606
|
+
return precedingSiblingAxis(node);
|
|
360
607
|
case "following":
|
|
361
|
-
return
|
|
362
|
-
|
|
608
|
+
return followingAxis(node);
|
|
363
609
|
case "preceding":
|
|
364
|
-
return
|
|
365
|
-
|
|
610
|
+
return precedingAxis(node);
|
|
366
611
|
case "attribute":
|
|
367
|
-
|
|
368
|
-
return Array.from(node.attributes);
|
|
369
|
-
}
|
|
370
|
-
return [];
|
|
371
|
-
|
|
612
|
+
return attributeAxis(node);
|
|
372
613
|
case "namespace":
|
|
373
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
504
|
-
if (name === "*" && !prefix) {
|
|
505
|
-
return true;
|
|
506
|
-
}
|
|
670
|
+
const { name, prefix } = nodeTest;
|
|
507
671
|
|
|
508
|
-
|
|
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
|
-
|
|
513
|
-
if (
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
//
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
|
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
|
|
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
|
},
|