@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
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Literal result element support: namespace aliasing and attribute filtering.
|
|
3
|
+
*
|
|
4
|
+
* `xsl:namespace-alias` rewrites the namespace of literal result elements and
|
|
5
|
+
* attributes, which is what makes it possible for a stylesheet to generate
|
|
6
|
+
* another stylesheet. Attribute filtering keeps XSLT-only attributes such as
|
|
7
|
+
* `xsl:use-attribute-sets` out of the result tree.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a namespace prefix against the declarations in scope of a node.
|
|
14
|
+
*
|
|
15
|
+
* Falls back to walking `xmlns` attributes when the DOM implementation does not
|
|
16
|
+
* provide `lookupNamespaceURI`.
|
|
17
|
+
*
|
|
18
|
+
* @param {Element} node - The element whose scope is searched
|
|
19
|
+
* @param {string|null} prefix - The prefix, or null for the default namespace
|
|
20
|
+
* @returns {string|null} The namespace URI, or null when undeclared
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* lookupNamespaceUri(stylesheetElement, 'xsl');
|
|
24
|
+
*/
|
|
25
|
+
export function lookupNamespaceUri(node, prefix) {
|
|
26
|
+
if (typeof node.lookupNamespaceURI === "function") {
|
|
27
|
+
const found = node.lookupNamespaceURI(prefix);
|
|
28
|
+
if (found) return found;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const attributeName = prefix ? `xmlns:${prefix}` : "xmlns";
|
|
32
|
+
let current = node;
|
|
33
|
+
|
|
34
|
+
while (current?.nodeType === 1) {
|
|
35
|
+
const value = current.getAttribute(attributeName);
|
|
36
|
+
if (value) return value;
|
|
37
|
+
current = current.parentNode;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The `xsl:namespace-alias` declarations of a stylesheet.
|
|
45
|
+
*
|
|
46
|
+
* Literal result names are aliased as libxslt (and so Chrome) does, see
|
|
47
|
+
* {@link NamespaceAliasMap#resolveLiteral}: the namespace URI is replaced and
|
|
48
|
+
* the prefix written in the stylesheet is kept (`axsl:stylesheet` with
|
|
49
|
+
* `xmlns:axsl` bound to the XSLT namespace); `result-prefix="#default"`
|
|
50
|
+
* without a default namespace in scope gives names in no namespace, and
|
|
51
|
+
* `stylesheet-prefix="#default"` without a default namespace in scope
|
|
52
|
+
* aliases the elements in no namespace, which then take the result prefix.
|
|
53
|
+
*/
|
|
54
|
+
export class NamespaceAliasMap {
|
|
55
|
+
constructor() {
|
|
56
|
+
this.byUri = new Map();
|
|
57
|
+
/** Alias of elements in no namespace (`#default` stylesheet prefix). */
|
|
58
|
+
this.noNamespaceAlias = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Record one `xsl:namespace-alias` declaration.
|
|
63
|
+
*
|
|
64
|
+
* @param {Element} node - The `xsl:namespace-alias` element
|
|
65
|
+
* @returns {void}
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* aliases.add(namespaceAliasElement);
|
|
69
|
+
*/
|
|
70
|
+
add(node) {
|
|
71
|
+
const stylesheetPrefix = node.getAttribute("stylesheet-prefix");
|
|
72
|
+
const resultPrefix = node.getAttribute("result-prefix");
|
|
73
|
+
if (!stylesheetPrefix || !resultPrefix) return;
|
|
74
|
+
|
|
75
|
+
const isDefaultSource = stylesheetPrefix === "#default";
|
|
76
|
+
const fromUri = lookupNamespaceUri(
|
|
77
|
+
node,
|
|
78
|
+
isDefaultSource ? null : stylesheetPrefix,
|
|
79
|
+
);
|
|
80
|
+
if (!fromUri && !isDefaultSource) return;
|
|
81
|
+
|
|
82
|
+
const isDefaultResult = resultPrefix === "#default";
|
|
83
|
+
const toUri = lookupNamespaceUri(
|
|
84
|
+
node,
|
|
85
|
+
isDefaultResult ? null : resultPrefix,
|
|
86
|
+
);
|
|
87
|
+
const alias = { uri: toUri, prefix: isDefaultResult ? null : resultPrefix };
|
|
88
|
+
|
|
89
|
+
if (fromUri) this.byUri.set(fromUri, alias);
|
|
90
|
+
else if (toUri) this.noNamespaceAlias = alias;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Whether any alias was declared.
|
|
95
|
+
*
|
|
96
|
+
* @returns {boolean} True when at least one alias is known
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* aliases.isEmpty();
|
|
100
|
+
*/
|
|
101
|
+
isEmpty() {
|
|
102
|
+
return this.byUri.size === 0 && this.noNamespaceAlias === null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether a namespace is the stylesheet side of an alias.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} uri - A namespace URI
|
|
109
|
+
* @returns {boolean} True when literal result names in it are aliased
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* aliases.isAliased('http://www.w3.org/1999/XSL/TransformAlias'); // true
|
|
113
|
+
*/
|
|
114
|
+
isAliased(uri) {
|
|
115
|
+
return this.byUri.has(uri);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Apply aliasing to a literal result name.
|
|
120
|
+
*
|
|
121
|
+
* @param {string|null} namespaceUri - The namespace of the stylesheet node
|
|
122
|
+
* @param {string} localName - The local name of the stylesheet node
|
|
123
|
+
* @returns {{namespaceUri: (string|null), qname: string}|null} The aliased name, or null when no alias applies
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* aliases.resolve('http://www.w3.org/1999/XSL/TransformAlias', 'stylesheet');
|
|
127
|
+
* // { namespaceUri: 'http://www.w3.org/1999/XSL/Transform', qname: 'xsl:stylesheet' }
|
|
128
|
+
*/
|
|
129
|
+
resolve(namespaceUri, localName) {
|
|
130
|
+
const alias = namespaceUri ? this.byUri.get(namespaceUri) : undefined;
|
|
131
|
+
if (!alias) return null;
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
namespaceUri: alias.uri,
|
|
135
|
+
qname: alias.prefix ? `${alias.prefix}:${localName}` : localName,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Apply aliasing to the name of a literal result element or attribute as
|
|
141
|
+
* libxslt does: the namespace URI is replaced and the stylesheet prefix
|
|
142
|
+
* kept. An element in no namespace takes the `#default` stylesheet-prefix
|
|
143
|
+
* alias with its result prefix; an alias to no namespace drops the prefix.
|
|
144
|
+
*
|
|
145
|
+
* @param {Element|Attr} node - The literal result element or attribute
|
|
146
|
+
* @returns {{namespaceUri: (string|null), qname: string}|null} The aliased
|
|
147
|
+
* name, or null when no alias applies
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* // xmlns:axsl="urn:alias", <xsl:namespace-alias stylesheet-prefix="axsl"
|
|
151
|
+
* // result-prefix="xsl"/>
|
|
152
|
+
* aliases.resolveLiteral(axslStylesheetElement);
|
|
153
|
+
* // { namespaceUri: 'http://www.w3.org/1999/XSL/Transform', qname: 'axsl:stylesheet' }
|
|
154
|
+
*/
|
|
155
|
+
resolveLiteral(node) {
|
|
156
|
+
const { localName } = node;
|
|
157
|
+
if (!node.namespaceURI) {
|
|
158
|
+
const alias = node.nodeType === 1 ? this.noNamespaceAlias : null;
|
|
159
|
+
return alias
|
|
160
|
+
? { namespaceUri: alias.uri, qname: `${alias.prefix}:${localName}` }
|
|
161
|
+
: null;
|
|
162
|
+
}
|
|
163
|
+
const alias = this.byUri.get(node.namespaceURI);
|
|
164
|
+
if (!alias) return null;
|
|
165
|
+
if (!alias.uri) return { namespaceUri: null, qname: localName };
|
|
166
|
+
return {
|
|
167
|
+
namespaceUri: alias.uri,
|
|
168
|
+
qname: node.prefix ? `${node.prefix}:${localName}` : localName,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Whether an attribute of a literal result element is copied to the output.
|
|
175
|
+
*
|
|
176
|
+
* Namespace declarations are re-created from the element namespaces themselves,
|
|
177
|
+
* and every XSLT attribute (`xsl:use-attribute-sets`, `xsl:version`,
|
|
178
|
+
* `xsl:exclude-result-prefixes`, `xsl:extension-element-prefixes`) is an
|
|
179
|
+
* instruction to the processor rather than result tree content.
|
|
180
|
+
*
|
|
181
|
+
* @param {Attr} attribute - The attribute of the stylesheet element
|
|
182
|
+
* @param {string} xsltNamespace - The XSLT namespace URI
|
|
183
|
+
* @returns {boolean} True when the attribute belongs in the result
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* shouldCopyAttribute(attr, 'http://www.w3.org/1999/XSL/Transform');
|
|
187
|
+
*/
|
|
188
|
+
export function shouldCopyAttribute(attribute, xsltNamespace) {
|
|
189
|
+
if (attribute.namespaceURI === xsltNamespace) return false;
|
|
190
|
+
if (attribute.name === "xmlns" || attribute.name.startsWith("xmlns:")) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
return !attribute.name.startsWith("xsl:");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Read an XSLT attribute from a literal result element.
|
|
198
|
+
*
|
|
199
|
+
* Works both for namespace aware DOMs and for documents where the attribute is
|
|
200
|
+
* only known by its `xsl:` qualified name.
|
|
201
|
+
*
|
|
202
|
+
* @param {Element} node - The literal result element
|
|
203
|
+
* @param {string} localName - The XSLT attribute local name
|
|
204
|
+
* @param {string} xsltNamespace - The XSLT namespace URI
|
|
205
|
+
* @returns {string|null} The attribute value, or null when absent
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* getXsltAttribute(element, 'use-attribute-sets', XSLT_NS);
|
|
209
|
+
*/
|
|
210
|
+
export function getXsltAttribute(node, localName, xsltNamespace) {
|
|
211
|
+
if (!node.attributes) return null;
|
|
212
|
+
|
|
213
|
+
for (const attribute of node.attributes) {
|
|
214
|
+
const matchesNamespace =
|
|
215
|
+
attribute.namespaceURI === xsltNamespace &&
|
|
216
|
+
(attribute.localName || attribute.name) === localName;
|
|
217
|
+
if (matchesNamespace || attribute.name === `xsl:${localName}`) {
|
|
218
|
+
return attribute.value;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers of the XSLT pattern matcher (see patterns.js): node relationships
|
|
3
|
+
* in the XPath data model and the per-call {@link MatchScope} that builds the
|
|
4
|
+
* XPath contexts predicates and `id()`/`key()` anchors are evaluated in.
|
|
5
|
+
*
|
|
6
|
+
* @module xslt/matchScope
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
import { XPathContext } from "../xpath/evaluator.js";
|
|
12
|
+
import { parentOf } from "../xpath/axes.js";
|
|
13
|
+
|
|
14
|
+
const XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
|
|
15
|
+
const EMPTY_VARIABLES = Object.freeze({});
|
|
16
|
+
const EMPTY_NAMESPACES = Object.freeze({});
|
|
17
|
+
|
|
18
|
+
export { parentOf };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The root of the tree holding a node (its document, or the top of a
|
|
22
|
+
* detached subtree or result tree fragment).
|
|
23
|
+
*
|
|
24
|
+
* @param {Node} node - Any node
|
|
25
|
+
* @returns {Node} The outermost ancestor
|
|
26
|
+
*/
|
|
27
|
+
export function rootOf(node) {
|
|
28
|
+
let root = node;
|
|
29
|
+
for (let parent = parentOf(root); parent; parent = parentOf(parent)) {
|
|
30
|
+
root = parent;
|
|
31
|
+
}
|
|
32
|
+
return root;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether a node is a root node (a document or a result tree fragment).
|
|
37
|
+
*
|
|
38
|
+
* @param {Node|null} node - Any node
|
|
39
|
+
* @returns {boolean} True for document and document fragment nodes
|
|
40
|
+
*/
|
|
41
|
+
export function isRoot(node) {
|
|
42
|
+
return node !== null && (node.nodeType === 9 || node.nodeType === 11);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether an attribute is a namespace declaration, which XPath does not
|
|
47
|
+
* expose on the attribute axis.
|
|
48
|
+
*
|
|
49
|
+
* @param {Attr} attribute - An attribute node
|
|
50
|
+
* @returns {boolean} True for `xmlns` and `xmlns:*` attributes
|
|
51
|
+
*/
|
|
52
|
+
export function isNamespaceDeclaration(attribute) {
|
|
53
|
+
return (
|
|
54
|
+
attribute.namespaceURI === XMLNS_NAMESPACE ||
|
|
55
|
+
attribute.name === "xmlns" ||
|
|
56
|
+
attribute.name.startsWith("xmlns:")
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* State of a single match call: lazily merges the XSLT variables once.
|
|
62
|
+
*/
|
|
63
|
+
export class MatchScope {
|
|
64
|
+
/**
|
|
65
|
+
* @param {object|null} host - XSLT context (variables, parameters, namespaces)
|
|
66
|
+
* @param {Object<string, string>} [namespaces] - Prefix bindings, when they
|
|
67
|
+
* differ from the host's (e.g. those in scope on an xsl:template)
|
|
68
|
+
*/
|
|
69
|
+
constructor(host, namespaces) {
|
|
70
|
+
this.host = host;
|
|
71
|
+
this.namespaces = namespaces ?? host?.namespaces ?? EMPTY_NAMESPACES;
|
|
72
|
+
this.variables = null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build an XPath context for evaluating a predicate or anchor call.
|
|
77
|
+
*
|
|
78
|
+
* @param {Node} node - Context node
|
|
79
|
+
* @param {number} position - Context position
|
|
80
|
+
* @param {number} size - Context size
|
|
81
|
+
* @returns {XPathContext} The evaluation context
|
|
82
|
+
*/
|
|
83
|
+
context(node, position, size) {
|
|
84
|
+
if (!this.variables) {
|
|
85
|
+
this.variables = this.host?.xpathVariables ?? {
|
|
86
|
+
...this.host?.variables,
|
|
87
|
+
...this.host?.parameters,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return new XPathContext(
|
|
91
|
+
node,
|
|
92
|
+
position,
|
|
93
|
+
size,
|
|
94
|
+
this.variables,
|
|
95
|
+
this.namespaces,
|
|
96
|
+
this.host,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build a cheap XPath context for node tests (namespaces only).
|
|
102
|
+
*
|
|
103
|
+
* @param {Node} node - Context node
|
|
104
|
+
* @returns {XPathContext} The evaluation context
|
|
105
|
+
*/
|
|
106
|
+
testContext(node) {
|
|
107
|
+
return new XPathContext(
|
|
108
|
+
node,
|
|
109
|
+
1,
|
|
110
|
+
1,
|
|
111
|
+
EMPTY_VARIABLES,
|
|
112
|
+
this.namespaces,
|
|
113
|
+
this.host,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `xsl:number` counting (XSLT 1.0 section 7.7).
|
|
3
|
+
*
|
|
4
|
+
* Counting is kept independent from the engine: callers pass a `matcher`
|
|
5
|
+
* callback that answers "does this node match this XSLT pattern", which keeps
|
|
6
|
+
* this module free of the XPath evaluator and easy to test in isolation.
|
|
7
|
+
*
|
|
8
|
+
* Numbering every node of a long list would be quadratic if each call counted
|
|
9
|
+
* from scratch, so a call may be given a memo (one per instruction and
|
|
10
|
+
* transformation, see {@link isMemoizable}) remembering the numbers already
|
|
11
|
+
* computed: a later call stops at the nearest numbered node. The tree is
|
|
12
|
+
* walked with previousSibling/lastChild/parentNode only.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
"use strict";
|
|
16
|
+
|
|
17
|
+
import { isParserArtifact } from "../xpath/axes.js";
|
|
18
|
+
|
|
19
|
+
/** Node types that participate in `xsl:number` counting. */
|
|
20
|
+
const COUNTABLE_NODE_TYPES = new Set([1, 3, 4, 7, 8]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether a node takes part in counting: an element, text, processing
|
|
24
|
+
* instruction or comment of the data model (not a parser artifact, see
|
|
25
|
+
* axes.js).
|
|
26
|
+
*
|
|
27
|
+
* @param {Node} node - Any node
|
|
28
|
+
* @returns {boolean} True for countable nodes
|
|
29
|
+
*/
|
|
30
|
+
function isCountable(node) {
|
|
31
|
+
return COUNTABLE_NODE_TYPES.has(node.nodeType) && !isParserArtifact(node);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The XPath node kind of a DOM node: CDATA sections are text nodes.
|
|
36
|
+
*
|
|
37
|
+
* @param {Node} node - Any node
|
|
38
|
+
* @returns {number} The DOM node type, 3 for CDATA sections
|
|
39
|
+
*/
|
|
40
|
+
function nodeKind(node) {
|
|
41
|
+
return node.nodeType === 4 ? 3 : node.nodeType;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default `count` pattern: nodes of the same kind as the numbered node and,
|
|
46
|
+
* for elements and attributes, the same expanded name; for processing
|
|
47
|
+
* instructions, the same target.
|
|
48
|
+
*
|
|
49
|
+
* @param {Node} candidate - The node being considered
|
|
50
|
+
* @param {Node} node - The node `xsl:number` is numbering
|
|
51
|
+
* @returns {boolean} True when the candidate is counted
|
|
52
|
+
*/
|
|
53
|
+
function matchesDefaultCount(candidate, node) {
|
|
54
|
+
if (nodeKind(candidate) !== nodeKind(node)) return false;
|
|
55
|
+
switch (node.nodeType) {
|
|
56
|
+
case 1:
|
|
57
|
+
case 2:
|
|
58
|
+
return (
|
|
59
|
+
candidate.localName === node.localName &&
|
|
60
|
+
(candidate.namespaceURI ?? null) === (node.namespaceURI ?? null)
|
|
61
|
+
);
|
|
62
|
+
case 7:
|
|
63
|
+
return candidate.target === node.target;
|
|
64
|
+
default:
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Key telling apart the node kinds the default `count` pattern selects, so
|
|
71
|
+
* memoized numbers are only reused for the same kind.
|
|
72
|
+
*
|
|
73
|
+
* @param {Node} node - The numbered node
|
|
74
|
+
* @returns {string} e.g. `1|urn:x|item`
|
|
75
|
+
*/
|
|
76
|
+
function defaultCountKey(node) {
|
|
77
|
+
if (node.nodeType === 7) return `7|${node.target}`;
|
|
78
|
+
if (node.nodeType === 1 || node.nodeType === 2) {
|
|
79
|
+
return `${node.nodeType}|${node.namespaceURI ?? ""}|${node.localName}`;
|
|
80
|
+
}
|
|
81
|
+
return String(nodeKind(node));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether the numbers of an instruction may be memoized: its patterns must
|
|
86
|
+
* not depend on variables or on the current node, whose values may differ
|
|
87
|
+
* between invocations of the same instruction.
|
|
88
|
+
*
|
|
89
|
+
* @param {string|null} count - The `count` pattern
|
|
90
|
+
* @param {string|null} from - The `from` pattern
|
|
91
|
+
* @returns {boolean} True when counting only depends on the source tree
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* isMemoizable("item", null); // true
|
|
95
|
+
* isMemoizable("item[@k=$k]", null); // false
|
|
96
|
+
*/
|
|
97
|
+
export function isMemoizable(count, from) {
|
|
98
|
+
return !/\$|current\s*\(/.test(`${count ?? ""} ${from ?? ""}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Count a node's preceding siblings satisfying the predicate, stopping at the
|
|
103
|
+
* nearest one whose position is memoized.
|
|
104
|
+
*
|
|
105
|
+
* @param {Node} node - The (counted) node whose position is computed
|
|
106
|
+
* @param {(candidate: Node) => boolean} isCounted - Counting predicate
|
|
107
|
+
* @param {WeakMap<Node, number>|null} positions - Memoized positions
|
|
108
|
+
* @returns {number} The 1-based position
|
|
109
|
+
*/
|
|
110
|
+
function siblingPosition(node, isCounted, positions) {
|
|
111
|
+
const own = positions?.get(node);
|
|
112
|
+
if (own !== undefined) return own;
|
|
113
|
+
|
|
114
|
+
let position = 1;
|
|
115
|
+
for (let sibling = node.previousSibling; sibling;) {
|
|
116
|
+
const known = positions?.get(sibling);
|
|
117
|
+
if (known !== undefined) {
|
|
118
|
+
position += known;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
if (isCountable(sibling) && isCounted(sibling)) {
|
|
122
|
+
position++;
|
|
123
|
+
}
|
|
124
|
+
sibling = sibling.previousSibling;
|
|
125
|
+
}
|
|
126
|
+
positions?.set(node, position);
|
|
127
|
+
return position;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Whether a node hangs off an element without being its child: an
|
|
132
|
+
* attribute or a namespace node, whose parent is `ownerElement`.
|
|
133
|
+
*
|
|
134
|
+
* @param {Node} node - Any node
|
|
135
|
+
* @returns {boolean} True for attribute and namespace nodes
|
|
136
|
+
*/
|
|
137
|
+
function isAttachedNode(node) {
|
|
138
|
+
return node.nodeType === 2 || node.nodeType === 13;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The node before another one in document order (its preceding node or its
|
|
143
|
+
* parent).
|
|
144
|
+
*
|
|
145
|
+
* @param {Node} node - A child node
|
|
146
|
+
* @returns {Node|null} The previous node, null at the root
|
|
147
|
+
*/
|
|
148
|
+
function previousInDocumentOrder(node) {
|
|
149
|
+
let previous = node.previousSibling;
|
|
150
|
+
if (!previous) return node.parentNode;
|
|
151
|
+
while (previous.lastChild) previous = previous.lastChild;
|
|
152
|
+
return previous;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Count the ancestors-or-self of a node according to `level="single"` or
|
|
157
|
+
* `level="multiple"`.
|
|
158
|
+
*
|
|
159
|
+
* @param {Node} node - The node being numbered
|
|
160
|
+
* @param {boolean} multiple - Whether every counted ancestor is numbered
|
|
161
|
+
* @param {(candidate: Node) => boolean} isCounted - Counting predicate
|
|
162
|
+
* @param {(candidate: Node) => boolean} isFrom - Boundary predicate
|
|
163
|
+
* @param {WeakMap<Node, number>|null} positions - Memoized positions
|
|
164
|
+
* @returns {number[]} Numbers from the outermost ancestor inwards
|
|
165
|
+
*/
|
|
166
|
+
function countAncestors(node, multiple, isCounted, isFrom, positions) {
|
|
167
|
+
const numbers = [];
|
|
168
|
+
let current = node;
|
|
169
|
+
|
|
170
|
+
while (current && current.nodeType !== 9) {
|
|
171
|
+
if (isFrom(current)) break;
|
|
172
|
+
if (isCounted(current)) {
|
|
173
|
+
numbers.unshift(siblingPosition(current, isCounted, positions));
|
|
174
|
+
if (!multiple) break;
|
|
175
|
+
}
|
|
176
|
+
current = isAttachedNode(current)
|
|
177
|
+
? current.ownerElement
|
|
178
|
+
: current.parentNode;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return numbers;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Count a node according to `level="any"`: walk backwards in document order
|
|
186
|
+
* until a `from` node, the root, or a node whose total is memoized. An
|
|
187
|
+
* attribute or namespace node counts itself, then its element and the nodes
|
|
188
|
+
* before it: other attributes are neither preceding nor ancestor nodes (as
|
|
189
|
+
* in libxslt).
|
|
190
|
+
*
|
|
191
|
+
* @param {Node} node - The node being numbered
|
|
192
|
+
* @param {(candidate: Node) => boolean} isCounted - Counting predicate
|
|
193
|
+
* @param {(candidate: Node) => boolean} isFrom - Boundary predicate
|
|
194
|
+
* @param {WeakMap<Node, number>|null} totals - Memoized totals
|
|
195
|
+
* @returns {number[]} A single number, or an empty list when nothing matches
|
|
196
|
+
*/
|
|
197
|
+
function countAny(node, isCounted, isFrom, totals) {
|
|
198
|
+
let total = 0;
|
|
199
|
+
let current = node;
|
|
200
|
+
if (isAttachedNode(node)) {
|
|
201
|
+
if (isFrom(node)) return [];
|
|
202
|
+
if (isCounted(node)) total++;
|
|
203
|
+
current = node.ownerElement;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
while (current && current.nodeType !== 9) {
|
|
207
|
+
const known = totals?.get(current);
|
|
208
|
+
if (known !== undefined) {
|
|
209
|
+
total += known;
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
if (isCountable(current)) {
|
|
213
|
+
if (isFrom(current)) break;
|
|
214
|
+
if (isCounted(current)) total++;
|
|
215
|
+
}
|
|
216
|
+
current = previousInDocumentOrder(current);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
totals?.set(node, total);
|
|
220
|
+
return total > 0 ? [total] : [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The memo tables of one kind of counted node.
|
|
225
|
+
*
|
|
226
|
+
* @param {Map<string, {positions: WeakMap, totals: WeakMap}>|null} memo - The instruction's memo
|
|
227
|
+
* @param {string} key - The counted kind
|
|
228
|
+
* @returns {{positions: WeakMap, totals: WeakMap}|null} The tables, null without memo
|
|
229
|
+
*/
|
|
230
|
+
function memoTables(memo, key) {
|
|
231
|
+
if (!memo) return null;
|
|
232
|
+
let tables = memo.get(key);
|
|
233
|
+
if (!tables) {
|
|
234
|
+
tables = { positions: new WeakMap(), totals: new WeakMap() };
|
|
235
|
+
memo.set(key, tables);
|
|
236
|
+
}
|
|
237
|
+
return tables;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Compute the number sequence for an `xsl:number` instruction.
|
|
242
|
+
*
|
|
243
|
+
* @param {Node} node - The current node
|
|
244
|
+
* @param {{level?: string, count?: string|null, from?: string|null}} options - Instruction attributes
|
|
245
|
+
* @param {(node: Node, pattern: string) => boolean} matcher - XSLT pattern matcher
|
|
246
|
+
* @param {Map|null} [memo] - Memo of the instruction for the current
|
|
247
|
+
* transformation (a Map owned by the caller), or null to count from scratch
|
|
248
|
+
* @returns {number[]} The computed numbers, outermost first
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* countXsltNumber(item, { level: 'any' }, matcher); // [2]
|
|
252
|
+
*/
|
|
253
|
+
export function countXsltNumber(node, options, matcher, memo = null) {
|
|
254
|
+
const { level = "single", count = null, from = null } = options;
|
|
255
|
+
const isCounted = count
|
|
256
|
+
? (candidate) => matcher(candidate, count)
|
|
257
|
+
: (candidate) => matchesDefaultCount(candidate, node);
|
|
258
|
+
const isFrom = from ? (candidate) => matcher(candidate, from) : () => false;
|
|
259
|
+
const tables = memoTables(memo, count ? "" : defaultCountKey(node));
|
|
260
|
+
|
|
261
|
+
if (level === "any") {
|
|
262
|
+
return countAny(node, isCounted, isFrom, tables?.totals ?? null);
|
|
263
|
+
}
|
|
264
|
+
return countAncestors(
|
|
265
|
+
node,
|
|
266
|
+
level === "multiple",
|
|
267
|
+
isCounted,
|
|
268
|
+
isFrom,
|
|
269
|
+
tables?.positions ?? null,
|
|
270
|
+
);
|
|
271
|
+
}
|