@tradik/xslt-processor 1.0.3 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +110 -520
  3. package/bin/lib/decode.js +15 -0
  4. package/bin/lib/dom.js +177 -0
  5. package/bin/lib/loaders.js +127 -0
  6. package/bin/lib/options.js +131 -0
  7. package/bin/lib/output.js +114 -0
  8. package/bin/lib/paths.js +186 -0
  9. package/bin/lib/transform.js +206 -0
  10. package/bin/xslt.js +73 -168
  11. package/dist/xslt-processor.browser.js +9564 -1585
  12. package/dist/xslt-processor.browser.js.map +4 -4
  13. package/dist/xslt-processor.browser.min.js +13 -2
  14. package/dist/xslt-processor.browser.min.js.map +4 -4
  15. package/dist/xslt-processor.cjs +9572 -1586
  16. package/dist/xslt-processor.cjs.map +4 -4
  17. package/dist/xslt-processor.d.cts +658 -0
  18. package/dist/xslt-processor.d.ts +459 -12
  19. package/dist/xslt-processor.js +9546 -1582
  20. package/dist/xslt-processor.js.map +4 -4
  21. package/package.json +71 -20
  22. package/src/XSLTProcessor.js +494 -48
  23. package/src/async/abort.js +63 -0
  24. package/src/async/documentUris.js +128 -0
  25. package/src/async/loaders.js +134 -0
  26. package/src/async/preload.js +159 -0
  27. package/src/async/processor.js +206 -0
  28. package/src/async/stream.js +125 -0
  29. package/src/bridge/engine.js +221 -0
  30. package/src/bridge/loader.js +78 -0
  31. package/src/bridge/results.js +75 -0
  32. package/src/bridge/version.js +63 -0
  33. package/src/index.js +26 -8
  34. package/src/io/decode.js +140 -0
  35. package/src/io/readSource.js +167 -0
  36. package/src/xpath/axes.js +562 -0
  37. package/src/xpath/documentOrder.js +270 -0
  38. package/src/xpath/evaluator.js +518 -357
  39. package/src/xpath/index.js +8 -2
  40. package/src/xpath/namespaceNodes.js +172 -0
  41. package/src/xpath/nodeSetFunctions.js +169 -0
  42. package/src/xpath/parser.js +30 -5
  43. package/src/xpath/strings.js +183 -0
  44. package/src/xpath/tokenizer.js +37 -23
  45. package/src/xslt/attributeSets.js +95 -0
  46. package/src/xslt/avt.js +103 -0
  47. package/src/xslt/computedNames.js +91 -0
  48. package/src/xslt/copying.js +212 -0
  49. package/src/xslt/declarationNames.js +80 -0
  50. package/src/xslt/domParsing.js +95 -0
  51. package/src/xslt/elements.js +57 -0
  52. package/src/xslt/engine/bindings.js +195 -0
  53. package/src/xslt/engine/context.js +105 -0
  54. package/src/xslt/engine/controlFlow.js +145 -0
  55. package/src/xslt/engine/copyInstructions.js +133 -0
  56. package/src/xslt/engine/declarations.js +233 -0
  57. package/src/xslt/engine/functionSupport.js +103 -0
  58. package/src/xslt/engine/methods.js +33 -0
  59. package/src/xslt/engine/nodeConstruction.js +187 -0
  60. package/src/xslt/engine/numbering.js +104 -0
  61. package/src/xslt/engine/outputDeclaration.js +77 -0
  62. package/src/xslt/engine/sequenceConstructor.js +228 -0
  63. package/src/xslt/engine/stylesheetLoading.js +208 -0
  64. package/src/xslt/engine/templateInvocation.js +253 -0
  65. package/src/xslt/engine/templateRules.js +243 -0
  66. package/src/xslt/engine/textInstructions.js +171 -0
  67. package/src/xslt/engine/topLevel.js +130 -0
  68. package/src/xslt/engine/transformation.js +263 -0
  69. package/src/xslt/engine/workStack.js +245 -0
  70. package/src/xslt/engine.js +184 -1736
  71. package/src/xslt/exslt/arguments.js +99 -0
  72. package/src/xslt/exslt/calendar.js +120 -0
  73. package/src/xslt/exslt/common.js +44 -0
  74. package/src/xslt/exslt/dateCalc.js +261 -0
  75. package/src/xslt/exslt/dateFormat.js +150 -0
  76. package/src/xslt/exslt/dateParse.js +265 -0
  77. package/src/xslt/exslt/dates.js +259 -0
  78. package/src/xslt/exslt/duration.js +207 -0
  79. package/src/xslt/exslt/dynamic.js +59 -0
  80. package/src/xslt/exslt/index.js +59 -0
  81. package/src/xslt/exslt/math.js +177 -0
  82. package/src/xslt/exslt/sets.js +96 -0
  83. package/src/xslt/exslt/stringOps.js +163 -0
  84. package/src/xslt/exslt/strings.js +147 -0
  85. package/src/xslt/exslt/uri.js +92 -0
  86. package/src/xslt/formatNumber.js +233 -0
  87. package/src/xslt/forwardsCompatible.js +75 -0
  88. package/src/xslt/functions.js +270 -0
  89. package/src/xslt/index.js +38 -1
  90. package/src/xslt/keys.js +164 -0
  91. package/src/xslt/literalResult.js +223 -0
  92. package/src/xslt/matchScope.js +116 -0
  93. package/src/xslt/number.js +271 -0
  94. package/src/xslt/numberFormat.js +253 -0
  95. package/src/xslt/outputNames.js +58 -0
  96. package/src/xslt/patternCompiler.js +175 -0
  97. package/src/xslt/patterns.js +324 -0
  98. package/src/xslt/qname.js +90 -0
  99. package/src/xslt/resultDocument.js +98 -0
  100. package/src/xslt/resultNamespaces.js +219 -0
  101. package/src/xslt/resultTree.js +211 -0
  102. package/src/xslt/serializer/baseWriter.js +390 -0
  103. package/src/xslt/serializer/chunks.js +120 -0
  104. package/src/xslt/serializer/constants.js +92 -0
  105. package/src/xslt/serializer/encoding.js +327 -0
  106. package/src/xslt/serializer/escape.js +135 -0
  107. package/src/xslt/serializer/frames.js +168 -0
  108. package/src/xslt/serializer/htmlDoctype.js +102 -0
  109. package/src/xslt/serializer/htmlEntities.js +77 -0
  110. package/src/xslt/serializer/htmlSerializer.js +239 -0
  111. package/src/xslt/serializer/indent.js +51 -0
  112. package/src/xslt/serializer/namespaces.js +68 -0
  113. package/src/xslt/serializer/rawText.js +41 -0
  114. package/src/xslt/serializer/settings.js +179 -0
  115. package/src/xslt/serializer/textSerializer.js +77 -0
  116. package/src/xslt/serializer/xhtmlDocument.js +103 -0
  117. package/src/xslt/serializer/xmlSerializer.js +227 -0
  118. package/src/xslt/serializer.js +90 -0
  119. package/src/xslt/sort.js +151 -0
  120. package/src/xslt/spaceNameTests.js +115 -0
  121. package/src/xslt/stylesheetChecks.js +206 -0
  122. package/src/xslt/stylesheetNamespaces.js +266 -0
  123. package/src/xslt/templatePriority.js +45 -0
  124. package/src/xslt/uri.js +68 -0
  125. package/src/xslt/variables.js +152 -0
  126. package/src/xslt/whitespace.js +200 -0
  127. package/LICENSE +0 -29
  128. package/src/XSLTProcessor.test.js +0 -930
  129. package/src/xpath/evaluator.test.js +0 -1852
  130. package/src/xpath/tokenizer.test.js +0 -224
  131. package/src/xslt/engine.test.js +0 -3130
@@ -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
+ }