@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,75 @@
1
+ /**
2
+ * Forwards-compatible processing and xsl:fallback (XSLT 1.0 sections 2.5
3
+ * and 15).
4
+ *
5
+ * An element is processed in forwards-compatible mode when the nearest
6
+ * enclosing `version` declaration (the `version` attribute of
7
+ * `xsl:stylesheet`/`xsl:transform`, or the `xsl:version` attribute of a
8
+ * literal result element) is not "1.0". In that mode unknown top-level XSLT
9
+ * elements are ignored. An unknown XSLT instruction is replaced by its
10
+ * `xsl:fallback` children when it has some (in any mode); otherwise it is an
11
+ * error only if it is actually instantiated.
12
+ *
13
+ * @module xslt/forwardsCompatible
14
+ */
15
+
16
+ "use strict";
17
+
18
+ import { XSLT_NAMESPACE } from "./elements.js";
19
+ import { xsltLocalName } from "./stylesheetChecks.js";
20
+
21
+ const modes = new WeakMap();
22
+
23
+ /**
24
+ * The version declared on one stylesheet element, if any.
25
+ *
26
+ * @param {Element} element - A stylesheet element
27
+ * @returns {string|null} The declared version, null when none
28
+ */
29
+ export function declaredVersion(element) {
30
+ const localName = xsltLocalName(element);
31
+ if (localName === "stylesheet" || localName === "transform") {
32
+ return element.getAttribute("version");
33
+ }
34
+ if (localName !== null) return null;
35
+ return element.getAttributeNS?.(XSLT_NAMESPACE, "version") ?? null;
36
+ }
37
+
38
+ /**
39
+ * Whether a stylesheet element is processed in forwards-compatible mode.
40
+ * Cached per element.
41
+ *
42
+ * @param {Node|null} node - A stylesheet node
43
+ * @returns {boolean} True when the version in effect is not "1.0"
44
+ *
45
+ * @example
46
+ * isForwardsCompatible(instruction); // true under version="2.0"
47
+ */
48
+ export function isForwardsCompatible(node) {
49
+ if (node?.nodeType !== 1) return false;
50
+
51
+ let forwards = modes.get(node);
52
+ if (forwards === undefined) {
53
+ const version = declaredVersion(node);
54
+ forwards =
55
+ version === null || version === ""
56
+ ? isForwardsCompatible(node.parentNode)
57
+ : version.trim() !== "1.0";
58
+ modes.set(node, forwards);
59
+ }
60
+ return forwards;
61
+ }
62
+
63
+ /**
64
+ * The `xsl:fallback` children of an instruction, in document order.
65
+ *
66
+ * @param {Element} instruction - An XSLT instruction
67
+ * @returns {Element[]} The fallback elements
68
+ */
69
+ export function fallbackChildren(instruction) {
70
+ const fallbacks = [];
71
+ for (let child = instruction.firstChild; child; child = child.nextSibling) {
72
+ if (xsltLocalName(child) === "fallback") fallbacks.push(child);
73
+ }
74
+ return fallbacks;
75
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * XSLT-defined XPath functions.
3
+ *
4
+ * XSLT 1.0 section 12 adds functions to the XPath function library, and the
5
+ * EXSLT `node-set()` extension (also under the msxsl namespace) is registered
6
+ * by expanded name next to them, together with the other EXSLT functions of
7
+ * `./exslt/index.js`. They live here rather than in `src/xpath` so
8
+ * that module stays a pure XPath 1.0 implementation; the engine registers this
9
+ * map on its evaluator through
10
+ * {@link XPathEvaluator#registerFunctions}.
11
+ */
12
+
13
+ "use strict";
14
+
15
+ import { formatNumber, DEFAULT_DECIMAL_FORMAT } from "./formatNumber.js";
16
+ import { isXsltElementAvailable, XSLT_NAMESPACE } from "./elements.js";
17
+ import {
18
+ expandedFunctionName,
19
+ resolveNamespacePrefix,
20
+ } from "../xpath/evaluator.js";
21
+ import { rootNodeOf } from "../xpath/axes.js";
22
+ import { EXSLT_COMMON, createExsltFunctions } from "./exslt/index.js";
23
+
24
+ /** Namespace of the EXSLT common module (`exsl:node-set()`). */
25
+ export const EXSLT_COMMON_NAMESPACE = EXSLT_COMMON;
26
+
27
+ /** Namespace of the MSXML extension functions (`msxsl:node-set()`). */
28
+ export const MSXSL_NAMESPACE = "urn:schemas-microsoft-com:xslt";
29
+
30
+ /** Vendor identification reported by `system-property()`. */
31
+ export const VENDOR = "@tradik/xslt-processor";
32
+
33
+ /** Vendor URL reported by `system-property()`. */
34
+ export const VENDOR_URL = "https://github.com/spagu/XSLT-Processor";
35
+
36
+ /**
37
+ * Values reported by `system-property()`, keyed by property name.
38
+ *
39
+ * The XSLT version is reported as the string `"1"`; XPath 1.0 converts it to
40
+ * the number 1 wherever a numeric comparison or arithmetic is used.
41
+ */
42
+ const SYSTEM_PROPERTIES = Object.freeze({
43
+ "xsl:version": "1",
44
+ "xsl:vendor": VENDOR,
45
+ "xsl:vendor-url": VENDOR_URL,
46
+ });
47
+
48
+ /**
49
+ * Get the document that owns a node.
50
+ *
51
+ * @param {Node} node - Any node
52
+ * @returns {Document} The owning document, or the node when it is a document
53
+ */
54
+ function ownerDocumentOf(node) {
55
+ return node.ownerDocument || node;
56
+ }
57
+
58
+ /**
59
+ * Convert an evaluated argument to the list of strings it denotes.
60
+ *
61
+ * Node-sets yield the string value of every node, other types yield one string.
62
+ *
63
+ * @param {XPathEvaluator} evaluator - The evaluator providing the conversions
64
+ * @param {(value: *) => string} stringify - The XPath `string()` conversion
65
+ * @param {*} value - An evaluated XPath value
66
+ * @returns {string[]} The string values
67
+ */
68
+ function toStringList(evaluator, stringify, value) {
69
+ if (Array.isArray(value)) {
70
+ return value.map((node) => evaluator.getStringValue(node));
71
+ }
72
+ return [stringify(value)];
73
+ }
74
+
75
+ /**
76
+ * Split a QName into its prefix and local part.
77
+ *
78
+ * @param {string} qname - A possibly prefixed name
79
+ * @returns {{prefix: (string|null), localName: string}} The parts of the name
80
+ */
81
+ function splitQName(qname) {
82
+ const colon = qname.indexOf(":");
83
+ if (colon === -1) return { prefix: null, localName: qname };
84
+ return {
85
+ prefix: qname.substring(0, colon),
86
+ localName: qname.substring(colon + 1),
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Expanded name key of the decimal format named by the third argument of
92
+ * `format-number()`, with the prefix resolved in the expression's scope
93
+ * (XSLT 1.0 section 12.3); the table is keyed the same way (see
94
+ * declarationNames.js).
95
+ *
96
+ * @param {string} qname - The decimal format QName
97
+ * @param {{namespaces: Object<string, string>}} ctx - The XPath context
98
+ * @returns {string} `{uri}local`, or the local name in no namespace
99
+ * @throws {Error} When the prefix is not declared
100
+ */
101
+ function decimalFormatKey(qname, ctx) {
102
+ const { prefix, localName } = splitQName(qname);
103
+ if (prefix === null) return localName;
104
+ const namespaceUri = resolveNamespacePrefix(prefix, ctx.namespaces);
105
+ if (!namespaceUri) {
106
+ throw new Error(
107
+ `format-number(): undeclared namespace prefix "${prefix}" in "${qname}"`,
108
+ );
109
+ }
110
+ return `{${namespaceUri}}${localName}`;
111
+ }
112
+
113
+ /**
114
+ * Build the XSLT function map for an engine.
115
+ *
116
+ * @param {import('./engine.js').XsltEngine} engine - The engine providing loaders, keys and formats
117
+ * @returns {Object<string, Function>} Functions ready for `registerFunctions`
118
+ *
119
+ * @example
120
+ * evaluator.registerFunctions(createXsltFunctions(engine));
121
+ */
122
+ export function createXsltFunctions(engine) {
123
+ const evaluator = engine.xpathEvaluator;
124
+
125
+ /**
126
+ * The XPath `string()` conversion of the evaluator.
127
+ *
128
+ * `XPathEvaluator#toString` shadows `Object#toString` and takes the value to
129
+ * convert as its argument, so it is bound once under an unambiguous name.
130
+ *
131
+ * @type {(value: *) => string}
132
+ */
133
+ const stringify = evaluator.toString.bind(evaluator);
134
+ const evaluate = (arg, ctx) => evaluator.evaluate(arg, ctx);
135
+ const asString = (arg, ctx) => stringify(evaluate(arg, ctx));
136
+
137
+ /**
138
+ * `exsl:node-set(object)` - EXSLT common, also known as `msxsl:node-set()`.
139
+ *
140
+ * A result tree fragment (a DocumentFragment in this engine) becomes a
141
+ * node-set holding its root node, so `exsl:node-set($rtf)/item` selects the
142
+ * fragment's top-level `item` elements. A node-set is returned unchanged; any
143
+ * other value becomes a node-set holding one text node with its string value.
144
+ *
145
+ * @param {Array} args - Argument expressions
146
+ * @param {import('../xpath/evaluator.js').XPathContext} ctx - Evaluation context
147
+ * @returns {Node[]} The node-set
148
+ */
149
+ const nodeSet = (args, ctx) => {
150
+ const value = evaluate(args[0], ctx);
151
+ if (Array.isArray(value)) return value;
152
+ if (value?.nodeType) return [value];
153
+ return [ownerDocumentOf(ctx.node).createTextNode(stringify(value))];
154
+ };
155
+
156
+ return {
157
+ ...createExsltFunctions(engine),
158
+ [expandedFunctionName(EXSLT_COMMON_NAMESPACE, "node-set")]: nodeSet,
159
+ [expandedFunctionName(MSXSL_NAMESPACE, "node-set")]: nodeSet,
160
+
161
+ /**
162
+ * `document(object, base?)` - load external XML documents.
163
+ *
164
+ * An empty URI denotes the stylesheet itself. Without a document loader, or
165
+ * when the loader returns null, the result is an empty node-set. The
166
+ * optional second argument is read as a base URI string.
167
+ */
168
+ document: (args, ctx) => {
169
+ const baseUri = args.length > 1 ? asString(args[1], ctx) : engine.baseUri;
170
+ const uris = toStringList(evaluator, stringify, evaluate(args[0], ctx));
171
+ const result = [];
172
+
173
+ for (const uri of uris) {
174
+ const doc = engine.loadDocument(uri, baseUri || engine.baseUri);
175
+ if (doc && !result.includes(doc)) result.push(doc);
176
+ }
177
+
178
+ return result;
179
+ },
180
+
181
+ /**
182
+ * `key(name, value)` - look up nodes through an `xsl:key` index of the
183
+ * tree containing the context node: its document, or the fragment of a
184
+ * result tree fragment converted with `exsl:node-set()`.
185
+ */
186
+ key: (args, ctx) => {
187
+ const name = asString(args[0], ctx);
188
+ const values = toStringList(evaluator, stringify, evaluate(args[1], ctx));
189
+ return engine.keyRegistry.lookup(name, values, rootNodeOf(ctx.node));
190
+ },
191
+
192
+ /** `format-number(number, pattern, decimalFormat?)`. */
193
+ "format-number": (args, ctx) => {
194
+ const value = evaluator.toNumber(evaluate(args[0], ctx));
195
+ const pattern = asString(args[1], ctx);
196
+ const formatName =
197
+ args.length > 2 ? decimalFormatKey(asString(args[2], ctx), ctx) : "";
198
+ const format =
199
+ engine.decimalFormats[formatName] || DEFAULT_DECIMAL_FORMAT;
200
+ return formatNumber(value, pattern, format);
201
+ },
202
+
203
+ /** `current()` - the XSLT current node, not the XPath context node. */
204
+ current: (args, ctx) => {
205
+ const currentNode = ctx.hostContext?.currentNode;
206
+ return currentNode ? [currentNode] : [ctx.node];
207
+ },
208
+
209
+ /** `generate-id(node-set?)` - a stable id for the life of the transform. */
210
+ "generate-id": (args, ctx) => {
211
+ let node = ctx.node;
212
+
213
+ if (args.length > 0) {
214
+ const nodeSet = evaluate(args[0], ctx);
215
+ node = Array.isArray(nodeSet) ? nodeSet[0] : nodeSet;
216
+ }
217
+
218
+ return node ? engine.generateId(node) : "";
219
+ },
220
+
221
+ /** `system-property(name)` - XSLT version and vendor information. */
222
+ "system-property": (args, ctx) => {
223
+ const name = asString(args[0], ctx);
224
+ return Object.hasOwn(SYSTEM_PROPERTIES, name)
225
+ ? SYSTEM_PROPERTIES[name]
226
+ : "";
227
+ },
228
+
229
+ /**
230
+ * `function-available(name)` - reflects the evaluator function table. A
231
+ * prefixed name is resolved through the stylesheet's namespace bindings,
232
+ * just as a call of that function would be. A function can report itself
233
+ * unavailable through an `isAvailable()` property (`dyn:evaluate()` does
234
+ * until the engine enables it).
235
+ */
236
+ "function-available": (args, ctx) => {
237
+ const { prefix, localName } = splitQName(asString(args[0], ctx));
238
+ const fn = evaluator.resolveFunction(localName, prefix, ctx.namespaces);
239
+ return fn !== null && fn.isAvailable?.() !== false;
240
+ },
241
+
242
+ /** `element-available(name)` - reflects the XSLT elements the engine runs. */
243
+ "element-available": (args, ctx) => {
244
+ const { prefix, localName } = splitQName(asString(args[0], ctx));
245
+ // An unprefixed name is in the default namespace (XSLT 1.0 section
246
+ // 15), which may be the XSLT namespace itself (libxslt bug-200)
247
+ const namespaceUri = prefix
248
+ ? (ctx.namespaces[prefix] ?? (prefix === "xsl" ? XSLT_NAMESPACE : null))
249
+ : ctx.namespaces[""] || null;
250
+ if (!namespaceUri) return false;
251
+
252
+ if (namespaceUri === XSLT_NAMESPACE) {
253
+ return isXsltElementAvailable(localName);
254
+ }
255
+ // Extension elements with an implementation (registerExtensionElement)
256
+ return Boolean(
257
+ engine.extensionElements?.has(`{${namespaceUri}}${localName}`),
258
+ );
259
+ },
260
+
261
+ /**
262
+ * `unparsed-entity-uri(name)` - always empty.
263
+ *
264
+ * Unparsed entity declarations are not exposed by the DOM, so this
265
+ * processor cannot resolve them; returning the empty string keeps
266
+ * stylesheets that call the function working.
267
+ */
268
+ "unparsed-entity-uri": () => "",
269
+ };
270
+ }
package/src/xslt/index.js CHANGED
@@ -3,4 +3,41 @@
3
3
  * Based on W3C XSLT 1.0 Specification: http://www.w3.org/TR/1999/REC-xslt-19991116
4
4
  */
5
5
 
6
- export { XsltContext, XsltEngine } from "./engine.js";
6
+ export {
7
+ XSLT_MAX_EXPRESSION_DEPTH,
8
+ XSLT_MAX_RESULT_SIZE,
9
+ XSLT_MAX_TEMPLATE_DEPTH,
10
+ XsltContext,
11
+ XsltEngine,
12
+ } from "./engine.js";
13
+
14
+ // XSLT vocabulary and function library (advanced usage)
15
+ export {
16
+ XSLT_ELEMENTS,
17
+ XSLT_NAMESPACE,
18
+ isXsltElementAvailable,
19
+ } from "./elements.js";
20
+ export { VENDOR, VENDOR_URL, createXsltFunctions } from "./functions.js";
21
+ export { KeyIndexRegistry } from "./keys.js";
22
+ export { DEFAULT_DECIMAL_FORMAT, formatNumber } from "./formatNumber.js";
23
+ export { countXsltNumber } from "./number.js";
24
+ export { formatXsltNumber, toRoman } from "./numberFormat.js";
25
+ export { WhitespaceFilter, stripWhitespaceNodes } from "./whitespace.js";
26
+ export {
27
+ NamespaceAliasMap,
28
+ getXsltAttribute,
29
+ lookupNamespaceUri,
30
+ shouldCopyAttribute,
31
+ } from "./literalResult.js";
32
+ export {
33
+ createResultDocument,
34
+ importResultFragment,
35
+ importResultNode,
36
+ } from "./resultTree.js";
37
+ export { isAbsoluteUri, resolveUri, stripFragment } from "./uri.js";
38
+ export {
39
+ serializeResult,
40
+ markRawText,
41
+ isRawText,
42
+ resolveOutputSettings,
43
+ } from "./serializer.js";
@@ -0,0 +1,164 @@
1
+ /**
2
+ * `xsl:key` indexing for the XSLT `key()` function.
3
+ *
4
+ * Indexes are built lazily, once per (root node, key name) pair, and cached in
5
+ * a `WeakMap` so source documents stay garbage collectable. The root node is
6
+ * the document of the context node, or the DocumentFragment of a result tree
7
+ * fragment converted with `exsl:node-set()` (see `rootNodeOf` in xpath/axes). The registry is
8
+ * deliberately decoupled from the engine: pattern matching and `use` evaluation
9
+ * are injected as callbacks.
10
+ */
11
+
12
+ "use strict";
13
+
14
+ /**
15
+ * @typedef {Object} KeyDefinition
16
+ * @property {string} match - Pattern of the nodes to index
17
+ * @property {string} use - Expression computing the key values
18
+ * @property {Object<string, string>} [namespaces] - Prefixes in scope on the xsl:key element
19
+ */
20
+
21
+ /**
22
+ * @typedef {Object} KeyIndex
23
+ * @property {Map<string, Node[]>} buckets - Key value to nodes, in document order
24
+ * @property {Map<Node, number>} order - Document order position of every node
25
+ */
26
+
27
+ /**
28
+ * Lazily built, per-tree indexes for all declared keys.
29
+ */
30
+ export class KeyIndexRegistry {
31
+ /**
32
+ * @param {Object} options - Registry configuration
33
+ * @param {Object<string, (KeyDefinition|KeyDefinition[])>} options.keys - Declared keys by name; several xsl:key elements may share a name
34
+ * @param {(node: Node, pattern: string, definition: KeyDefinition) => boolean} options.matchesPattern - XSLT pattern matcher
35
+ * @param {(node: Node, expression: string, definition: KeyDefinition) => string[]} options.evaluateUse - `use` evaluator returning key values
36
+ */
37
+ constructor({ keys, matchesPattern, evaluateUse }) {
38
+ this.keys = keys;
39
+ this.matchesPattern = matchesPattern;
40
+ this.evaluateUse = evaluateUse;
41
+ this.cache = new WeakMap();
42
+ }
43
+
44
+ /**
45
+ * Drop every cached index, for example after the key declarations changed.
46
+ *
47
+ * @returns {void}
48
+ *
49
+ * @example
50
+ * registry.clear();
51
+ */
52
+ clear() {
53
+ this.cache = new WeakMap();
54
+ }
55
+
56
+ /**
57
+ * Look up the nodes indexed under one or more key values.
58
+ *
59
+ * @param {string} name - The key name
60
+ * @param {string|string[]} values - One key value, or several to union
61
+ * @param {Node} doc - Root of the tree to search (Document or DocumentFragment)
62
+ * @returns {Node[]} Matching nodes in document order, without duplicates
63
+ * @throws {Error} When the key name was never declared
64
+ *
65
+ * @example
66
+ * registry.lookup('byId', 'a1', xmlDoc);
67
+ */
68
+ lookup(name, values, doc) {
69
+ if (!Object.hasOwn(this.keys, name)) {
70
+ throw new Error(`Undefined key: ${name}`);
71
+ }
72
+
73
+ const { buckets, order } = this.getIndex(name, doc);
74
+ const wanted = Array.isArray(values) ? values : [values];
75
+ if (wanted.length === 1) return [...(buckets.get(wanted[0]) || [])];
76
+
77
+ const found = new Set();
78
+ for (const value of wanted) {
79
+ for (const node of buckets.get(value) || []) found.add(node);
80
+ }
81
+ return [...found].sort((a, b) => order.get(a) - order.get(b));
82
+ }
83
+
84
+ /**
85
+ * Get (building if needed) the index of one key for one document.
86
+ *
87
+ * @param {string} name - The key name
88
+ * @param {Node} doc - Root of the tree being indexed
89
+ * @returns {KeyIndex} The index
90
+ */
91
+ getIndex(name, doc) {
92
+ let byName = this.cache.get(doc);
93
+ if (!byName) {
94
+ byName = new Map();
95
+ this.cache.set(doc, byName);
96
+ }
97
+
98
+ let index = byName.get(name);
99
+ if (!index) {
100
+ index = this.buildIndex(name, doc);
101
+ byName.set(name, index);
102
+ }
103
+
104
+ return index;
105
+ }
106
+
107
+ /**
108
+ * Build the index of one key for one document, merging every declaration
109
+ * of that key name.
110
+ *
111
+ * @param {string} name - The key name
112
+ * @param {Node} doc - Root of the tree being indexed
113
+ * @returns {KeyIndex} The index
114
+ */
115
+ buildIndex(name, doc) {
116
+ const definitions = [this.keys[name]].flat();
117
+ const buckets = new Map();
118
+ const order = new Map();
119
+
120
+ for (const node of documentOrderNodes(doc)) {
121
+ order.set(node, order.size);
122
+
123
+ for (const definition of definitions) {
124
+ if (!this.matchesPattern(node, definition.match, definition)) continue;
125
+
126
+ for (const value of this.evaluateUse(
127
+ node,
128
+ definition.use,
129
+ definition,
130
+ )) {
131
+ const bucket = buckets.get(value);
132
+ if (!bucket) buckets.set(value, [node]);
133
+ else if (bucket[bucket.length - 1] !== node) bucket.push(node);
134
+ }
135
+ }
136
+ }
137
+
138
+ return { buckets, order };
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Walk a document in document order, including attribute nodes.
144
+ *
145
+ * @param {Node} root - The document or subtree root
146
+ * @yields {Node} Every node of the subtree
147
+ */
148
+ function* documentOrderNodes(root) {
149
+ const stack = [root];
150
+
151
+ while (stack.length > 0) {
152
+ const current = stack.pop();
153
+ yield current;
154
+
155
+ if (current.nodeType === 1 && current.attributes) {
156
+ for (const attribute of current.attributes) yield attribute;
157
+ }
158
+
159
+ const children = current.childNodes;
160
+ if (children) {
161
+ for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
162
+ }
163
+ }
164
+ }