@tradik/xslt-processor 1.0.2 → 1.1.1

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 (47) hide show
  1. package/README.md +292 -47
  2. package/bin/lib/options.js +114 -0
  3. package/bin/lib/paths.js +186 -0
  4. package/bin/lib/transform.js +115 -0
  5. package/bin/xslt.js +68 -162
  6. package/dist/xslt-processor.browser.js +2073 -163
  7. package/dist/xslt-processor.browser.js.map +4 -4
  8. package/dist/xslt-processor.browser.min.js +6 -2
  9. package/dist/xslt-processor.browser.min.js.map +4 -4
  10. package/dist/xslt-processor.cjs +2077 -162
  11. package/dist/xslt-processor.cjs.map +4 -4
  12. package/dist/xslt-processor.d.cts +299 -0
  13. package/dist/xslt-processor.d.ts +92 -4
  14. package/dist/xslt-processor.js +2072 -161
  15. package/dist/xslt-processor.js.map +4 -4
  16. package/package.json +27 -16
  17. package/src/XSLTProcessor.js +177 -8
  18. package/src/index.js +11 -5
  19. package/src/xpath/evaluator.js +48 -7
  20. package/src/xslt/elements.js +57 -0
  21. package/src/xslt/engine.js +474 -185
  22. package/src/xslt/formatNumber.js +220 -0
  23. package/src/xslt/functions.js +191 -0
  24. package/src/xslt/index.js +31 -0
  25. package/src/xslt/keys.js +141 -0
  26. package/src/xslt/literalResult.js +167 -0
  27. package/src/xslt/number.js +178 -0
  28. package/src/xslt/numberFormat.js +155 -0
  29. package/src/xslt/resultTree.js +74 -0
  30. package/src/xslt/serializer/baseWriter.js +283 -0
  31. package/src/xslt/serializer/constants.js +78 -0
  32. package/src/xslt/serializer/escape.js +98 -0
  33. package/src/xslt/serializer/htmlSerializer.js +141 -0
  34. package/src/xslt/serializer/indent.js +51 -0
  35. package/src/xslt/serializer/namespaces.js +68 -0
  36. package/src/xslt/serializer/rawText.js +41 -0
  37. package/src/xslt/serializer/settings.js +103 -0
  38. package/src/xslt/serializer/textSerializer.js +29 -0
  39. package/src/xslt/serializer/xmlSerializer.js +127 -0
  40. package/src/xslt/serializer.js +57 -0
  41. package/src/xslt/templatePriority.js +45 -0
  42. package/src/xslt/uri.js +68 -0
  43. package/src/xslt/whitespace.js +184 -0
  44. package/src/XSLTProcessor.test.js +0 -930
  45. package/src/xpath/evaluator.test.js +0 -1852
  46. package/src/xpath/tokenizer.test.js +0 -224
  47. package/src/xslt/engine.test.js +0 -3130
@@ -0,0 +1,220 @@
1
+ /**
2
+ * XSLT 1.0 `format-number()` picture string formatting.
3
+ *
4
+ * Implements the subset of the JDK `DecimalFormat` picture syntax that XSLT 1.0
5
+ * requires: grouping separator, decimal separator, minimum/maximum fraction
6
+ * digits, minimum integer digits, percent and per-mille scaling and an optional
7
+ * negative subpattern. All symbols are taken from an `xsl:decimal-format`
8
+ * declaration so alternative digits and separators are honoured.
9
+ */
10
+
11
+ "use strict";
12
+
13
+ /** Symbols of the unnamed, default `xsl:decimal-format`. */
14
+ export const DEFAULT_DECIMAL_FORMAT = Object.freeze({
15
+ decimalSeparator: ".",
16
+ groupingSeparator: ",",
17
+ percent: "%",
18
+ perMille: "‰",
19
+ zeroDigit: "0",
20
+ digit: "#",
21
+ patternSeparator: ";",
22
+ infinity: "Infinity",
23
+ nan: "NaN",
24
+ minusSign: "-",
25
+ });
26
+
27
+ /**
28
+ * Split a picture string into its positive and optional negative subpattern.
29
+ *
30
+ * @param {string} pattern - The picture string
31
+ * @param {Object} format - Decimal format symbols
32
+ * @returns {{positive: string, negative: (string|null)}} The subpatterns
33
+ */
34
+ function splitSubPatterns(pattern, format) {
35
+ const index = pattern.indexOf(format.patternSeparator);
36
+ if (index === -1) return { positive: pattern, negative: null };
37
+ return {
38
+ positive: pattern.substring(0, index),
39
+ negative: pattern.substring(index + format.patternSeparator.length),
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Parse a single subpattern into a formatting description.
45
+ *
46
+ * @param {string} subPattern - One subpattern of a picture string
47
+ * @param {Object} format - Decimal format symbols
48
+ * @returns {Object} Prefix, suffix, digit counts, grouping size and multiplier
49
+ */
50
+ function parseSubPattern(subPattern, format) {
51
+ const special = new Set([
52
+ format.digit,
53
+ format.zeroDigit,
54
+ format.groupingSeparator,
55
+ format.decimalSeparator,
56
+ ]);
57
+
58
+ let start = 0;
59
+ while (start < subPattern.length && !special.has(subPattern[start])) start++;
60
+
61
+ let end = start;
62
+ while (end < subPattern.length && special.has(subPattern[end])) end++;
63
+
64
+ const prefix = subPattern.substring(0, start);
65
+ const numeric = subPattern.substring(start, end);
66
+ const suffix = subPattern.substring(end);
67
+
68
+ const decimalIndex = numeric.indexOf(format.decimalSeparator);
69
+ const integerPart =
70
+ decimalIndex === -1 ? numeric : numeric.substring(0, decimalIndex);
71
+ const fractionPart =
72
+ decimalIndex === -1 ? "" : numeric.substring(decimalIndex + 1);
73
+
74
+ const groupingIndex = integerPart.lastIndexOf(format.groupingSeparator);
75
+ const affixes = prefix + suffix;
76
+
77
+ let multiplier = 1;
78
+ if (affixes.includes(format.percent)) multiplier = 100;
79
+ else if (affixes.includes(format.perMille)) multiplier = 1000;
80
+
81
+ return {
82
+ prefix,
83
+ suffix,
84
+ multiplier,
85
+ minInteger: countOccurrences(integerPart, format.zeroDigit),
86
+ minFraction: countOccurrences(fractionPart, format.zeroDigit),
87
+ maxFraction: Math.min(fractionPart.length, 100),
88
+ groupingSize:
89
+ groupingIndex === -1 ? 0 : integerPart.length - groupingIndex - 1,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Count occurrences of a character inside a string.
95
+ *
96
+ * @param {string} text - The text to scan
97
+ * @param {string} char - The character to count
98
+ * @returns {number} Number of occurrences
99
+ */
100
+ function countOccurrences(text, char) {
101
+ let total = 0;
102
+ for (const current of text) {
103
+ if (current === char) total++;
104
+ }
105
+ return total;
106
+ }
107
+
108
+ /**
109
+ * Insert grouping separators into a run of integer digits.
110
+ *
111
+ * @param {string} digits - Integer digits, most significant first
112
+ * @param {number} size - Grouping size, 0 disables grouping
113
+ * @param {string} separator - The grouping separator
114
+ * @returns {string} The grouped digits
115
+ */
116
+ function applyGrouping(digits, size, separator) {
117
+ if (size <= 0 || digits.length <= size) return digits;
118
+
119
+ let result = "";
120
+ for (let i = 0; i < digits.length; i++) {
121
+ const fromEnd = digits.length - i;
122
+ if (i > 0 && fromEnd % size === 0) result += separator;
123
+ result += digits[i];
124
+ }
125
+ return result;
126
+ }
127
+
128
+ /**
129
+ * Translate ASCII digits to the digits of the decimal format.
130
+ *
131
+ * @param {string} text - Text containing ASCII digits
132
+ * @param {string} zeroDigit - The format's zero digit
133
+ * @returns {string} Text using the format's digit family
134
+ */
135
+ function translateDigits(text, zeroDigit) {
136
+ const offset = zeroDigit.codePointAt(0) - 48;
137
+ if (offset === 0) return text;
138
+ return text.replaceAll(/\d/g, (digit) =>
139
+ String.fromCodePoint(digit.codePointAt(0) + offset),
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Format the magnitude of a finite number according to a parsed subpattern.
145
+ *
146
+ * @param {number} magnitude - Absolute, already scaled value
147
+ * @param {Object} spec - Parsed subpattern
148
+ * @param {Object} format - Decimal format symbols
149
+ * @returns {string} The formatted number without prefix or suffix
150
+ */
151
+ function formatMagnitude(magnitude, spec, format) {
152
+ const fixed = magnitude.toFixed(spec.maxFraction);
153
+ const [rawInteger, rawFraction = ""] = fixed.split(".");
154
+
155
+ let fraction = rawFraction;
156
+ while (fraction.length > spec.minFraction && fraction.endsWith("0")) {
157
+ fraction = fraction.slice(0, -1);
158
+ }
159
+
160
+ let integer = rawInteger.padStart(spec.minInteger, "0");
161
+ if (spec.minInteger === 0 && integer === "0" && fraction.length > 0) {
162
+ integer = "";
163
+ }
164
+
165
+ integer = applyGrouping(integer, spec.groupingSize, format.groupingSeparator);
166
+
167
+ const body =
168
+ fraction.length > 0
169
+ ? integer + format.decimalSeparator + fraction
170
+ : integer;
171
+
172
+ return translateDigits(body, format.zeroDigit);
173
+ }
174
+
175
+ /**
176
+ * Format a number using an XSLT 1.0 picture string.
177
+ *
178
+ * @param {number} value - The number to format
179
+ * @param {string} pattern - The picture string, e.g. `#,##0.00`
180
+ * @param {Object} [decimalFormat] - `xsl:decimal-format` symbols
181
+ * @returns {string} The formatted number
182
+ *
183
+ * @example
184
+ * formatNumber(1234.5, '#,##0.00'); // '1,234.50'
185
+ * formatNumber(-1234, '#,##0;(#,##0)'); // '(1,234)'
186
+ */
187
+ export function formatNumber(
188
+ value,
189
+ pattern,
190
+ decimalFormat = DEFAULT_DECIMAL_FORMAT,
191
+ ) {
192
+ const format = { ...DEFAULT_DECIMAL_FORMAT, ...decimalFormat };
193
+
194
+ if (typeof value !== "number" || Number.isNaN(value)) return format.nan;
195
+
196
+ const subPatterns = splitSubPatterns(pattern, format);
197
+ const positive = parseSubPattern(subPatterns.positive, format);
198
+ const isNegative = value < 0;
199
+
200
+ let spec = positive;
201
+ let prefix = positive.prefix;
202
+ let suffix = positive.suffix;
203
+
204
+ if (isNegative) {
205
+ if (subPatterns.negative !== null) {
206
+ spec = parseSubPattern(subPatterns.negative, format);
207
+ prefix = spec.prefix;
208
+ suffix = spec.suffix;
209
+ } else {
210
+ prefix = format.minusSign + positive.prefix;
211
+ }
212
+ }
213
+
214
+ const magnitude = Math.abs(value) * spec.multiplier;
215
+ const body = Number.isFinite(magnitude)
216
+ ? formatMagnitude(magnitude, spec, format)
217
+ : format.infinity;
218
+
219
+ return prefix + body + suffix;
220
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * XSLT-defined XPath functions.
3
+ *
4
+ * XSLT 1.0 section 12 adds functions to the XPath function library. They live
5
+ * here rather than in `src/xpath` so that module stays a pure XPath 1.0
6
+ * implementation; the engine registers this map on its evaluator through
7
+ * {@link XPathEvaluator#registerFunctions}.
8
+ */
9
+
10
+ "use strict";
11
+
12
+ import { formatNumber, DEFAULT_DECIMAL_FORMAT } from "./formatNumber.js";
13
+ import { isXsltElementAvailable, XSLT_NAMESPACE } from "./elements.js";
14
+
15
+ /** Vendor identification reported by `system-property()`. */
16
+ export const VENDOR = "@tradik/xslt-processor";
17
+
18
+ /** Vendor URL reported by `system-property()`. */
19
+ export const VENDOR_URL = "https://github.com/spagu/XSLT-Processor";
20
+
21
+ /**
22
+ * Values reported by `system-property()`, keyed by property name.
23
+ *
24
+ * The XSLT version is reported as the string `"1"`; XPath 1.0 converts it to
25
+ * the number 1 wherever a numeric comparison or arithmetic is used.
26
+ */
27
+ const SYSTEM_PROPERTIES = Object.freeze({
28
+ "xsl:version": "1",
29
+ "xsl:vendor": VENDOR,
30
+ "xsl:vendor-url": VENDOR_URL,
31
+ });
32
+
33
+ /**
34
+ * Get the document that owns a node.
35
+ *
36
+ * @param {Node} node - Any node
37
+ * @returns {Document} The owning document, or the node when it is a document
38
+ */
39
+ function ownerDocumentOf(node) {
40
+ return node.ownerDocument || node;
41
+ }
42
+
43
+ /**
44
+ * Convert an evaluated argument to the list of strings it denotes.
45
+ *
46
+ * Node-sets yield the string value of every node, other types yield one string.
47
+ *
48
+ * @param {XPathEvaluator} evaluator - The evaluator providing the conversions
49
+ * @param {(value: *) => string} stringify - The XPath `string()` conversion
50
+ * @param {*} value - An evaluated XPath value
51
+ * @returns {string[]} The string values
52
+ */
53
+ function toStringList(evaluator, stringify, value) {
54
+ if (Array.isArray(value)) {
55
+ return value.map((node) => evaluator.getStringValue(node));
56
+ }
57
+ return [stringify(value)];
58
+ }
59
+
60
+ /**
61
+ * Split a QName into its prefix and local part.
62
+ *
63
+ * @param {string} qname - A possibly prefixed name
64
+ * @returns {{prefix: (string|null), localName: string}} The parts of the name
65
+ */
66
+ function splitQName(qname) {
67
+ const colon = qname.indexOf(":");
68
+ if (colon === -1) return { prefix: null, localName: qname };
69
+ return {
70
+ prefix: qname.substring(0, colon),
71
+ localName: qname.substring(colon + 1),
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Build the XSLT function map for an engine.
77
+ *
78
+ * @param {import('./engine.js').XsltEngine} engine - The engine providing loaders, keys and formats
79
+ * @returns {Object<string, Function>} Functions ready for `registerFunctions`
80
+ *
81
+ * @example
82
+ * evaluator.registerFunctions(createXsltFunctions(engine));
83
+ */
84
+ export function createXsltFunctions(engine) {
85
+ const evaluator = engine.xpathEvaluator;
86
+
87
+ /**
88
+ * The XPath `string()` conversion of the evaluator.
89
+ *
90
+ * `XPathEvaluator#toString` shadows `Object#toString` and takes the value to
91
+ * convert as its argument, so it is bound once under an unambiguous name.
92
+ *
93
+ * @type {(value: *) => string}
94
+ */
95
+ const stringify = evaluator.toString.bind(evaluator);
96
+ const evaluate = (arg, ctx) => evaluator.evaluate(arg, ctx);
97
+ const asString = (arg, ctx) => stringify(evaluate(arg, ctx));
98
+
99
+ return {
100
+ /**
101
+ * `document(object, base?)` - load external XML documents.
102
+ *
103
+ * An empty URI denotes the stylesheet itself. Without a document loader, or
104
+ * when the loader returns null, the result is an empty node-set. The
105
+ * optional second argument is read as a base URI string.
106
+ */
107
+ document: (args, ctx) => {
108
+ const baseUri = args.length > 1 ? asString(args[1], ctx) : engine.baseUri;
109
+ const uris = toStringList(evaluator, stringify, evaluate(args[0], ctx));
110
+ const result = [];
111
+
112
+ for (const uri of uris) {
113
+ const doc = engine.loadDocument(uri, baseUri || engine.baseUri);
114
+ if (doc && !result.includes(doc)) result.push(doc);
115
+ }
116
+
117
+ return result;
118
+ },
119
+
120
+ /** `key(name, value)` - look up nodes through an `xsl:key` index. */
121
+ key: (args, ctx) => {
122
+ const name = asString(args[0], ctx);
123
+ const values = toStringList(evaluator, stringify, evaluate(args[1], ctx));
124
+ return engine.keyRegistry.lookup(name, values, ownerDocumentOf(ctx.node));
125
+ },
126
+
127
+ /** `format-number(number, pattern, decimalFormat?)`. */
128
+ "format-number": (args, ctx) => {
129
+ const value = evaluator.toNumber(evaluate(args[0], ctx));
130
+ const pattern = asString(args[1], ctx);
131
+ const formatName = args.length > 2 ? asString(args[2], ctx) : "";
132
+ const format =
133
+ engine.decimalFormats[formatName] || DEFAULT_DECIMAL_FORMAT;
134
+ return formatNumber(value, pattern, format);
135
+ },
136
+
137
+ /** `current()` - the XSLT current node, not the XPath context node. */
138
+ current: (args, ctx) => {
139
+ const currentNode = ctx.hostContext?.currentNode;
140
+ return currentNode ? [currentNode] : [ctx.node];
141
+ },
142
+
143
+ /** `generate-id(node-set?)` - a stable id for the life of the transform. */
144
+ "generate-id": (args, ctx) => {
145
+ let node = ctx.node;
146
+
147
+ if (args.length > 0) {
148
+ const nodeSet = evaluate(args[0], ctx);
149
+ node = Array.isArray(nodeSet) ? nodeSet[0] : nodeSet;
150
+ }
151
+
152
+ return node ? engine.generateId(node) : "";
153
+ },
154
+
155
+ /** `system-property(name)` - XSLT version and vendor information. */
156
+ "system-property": (args, ctx) => {
157
+ const name = asString(args[0], ctx);
158
+ return Object.hasOwn(SYSTEM_PROPERTIES, name)
159
+ ? SYSTEM_PROPERTIES[name]
160
+ : "";
161
+ },
162
+
163
+ /** `function-available(name)` - reflects the evaluator function table. */
164
+ "function-available": (args, ctx) => {
165
+ const name = asString(args[0], ctx);
166
+ return Object.hasOwn(evaluator.functions, name);
167
+ },
168
+
169
+ /** `element-available(name)` - reflects the XSLT elements the engine runs. */
170
+ "element-available": (args, ctx) => {
171
+ const { prefix, localName } = splitQName(asString(args[0], ctx));
172
+ if (!prefix) return false;
173
+
174
+ const namespaceUri =
175
+ ctx.namespaces[prefix] ?? (prefix === "xsl" ? XSLT_NAMESPACE : null);
176
+
177
+ return (
178
+ namespaceUri === XSLT_NAMESPACE && isXsltElementAvailable(localName)
179
+ );
180
+ },
181
+
182
+ /**
183
+ * `unparsed-entity-uri(name)` - always empty.
184
+ *
185
+ * Unparsed entity declarations are not exposed by the DOM, so this
186
+ * processor cannot resolve them; returning the empty string keeps
187
+ * stylesheets that call the function working.
188
+ */
189
+ "unparsed-entity-uri": () => "",
190
+ };
191
+ }
package/src/xslt/index.js CHANGED
@@ -4,3 +4,34 @@
4
4
  */
5
5
 
6
6
  export { XsltContext, XsltEngine } from "./engine.js";
7
+
8
+ // XSLT vocabulary and function library (advanced usage)
9
+ export {
10
+ XSLT_ELEMENTS,
11
+ XSLT_NAMESPACE,
12
+ isXsltElementAvailable,
13
+ } from "./elements.js";
14
+ export { VENDOR, VENDOR_URL, createXsltFunctions } from "./functions.js";
15
+ export { KeyIndexRegistry } from "./keys.js";
16
+ export { DEFAULT_DECIMAL_FORMAT, formatNumber } from "./formatNumber.js";
17
+ export { countXsltNumber } from "./number.js";
18
+ export { formatXsltNumber, toRoman } from "./numberFormat.js";
19
+ export { WhitespaceFilter, stripWhitespaceNodes } from "./whitespace.js";
20
+ export {
21
+ NamespaceAliasMap,
22
+ getXsltAttribute,
23
+ lookupNamespaceUri,
24
+ shouldCopyAttribute,
25
+ } from "./literalResult.js";
26
+ export {
27
+ createResultDocument,
28
+ importResultFragment,
29
+ importResultNode,
30
+ } from "./resultTree.js";
31
+ export { isAbsoluteUri, resolveUri, stripFragment } from "./uri.js";
32
+ export {
33
+ serializeResult,
34
+ markRawText,
35
+ isRawText,
36
+ resolveOutputSettings,
37
+ } from "./serializer.js";
@@ -0,0 +1,141 @@
1
+ /**
2
+ * `xsl:key` indexing for the XSLT `key()` function.
3
+ *
4
+ * Indexes are built lazily, once per (document, key name) pair, and cached in a
5
+ * `WeakMap` so source documents stay garbage collectable. The registry is
6
+ * deliberately decoupled from the engine: pattern matching and `use` evaluation
7
+ * are injected as callbacks.
8
+ */
9
+
10
+ "use strict";
11
+
12
+ /**
13
+ * Lazily built, per-document indexes for all declared keys.
14
+ */
15
+ export class KeyIndexRegistry {
16
+ /**
17
+ * @param {Object} options - Registry configuration
18
+ * @param {Object<string, {match: string, use: string}>} options.keys - Declared keys by name
19
+ * @param {(node: Node, pattern: string) => boolean} options.matchesPattern - XSLT pattern matcher
20
+ * @param {(node: Node, expression: string) => string[]} options.evaluateUse - `use` evaluator returning key values
21
+ */
22
+ constructor({ keys, matchesPattern, evaluateUse }) {
23
+ this.keys = keys;
24
+ this.matchesPattern = matchesPattern;
25
+ this.evaluateUse = evaluateUse;
26
+ this.cache = new WeakMap();
27
+ }
28
+
29
+ /**
30
+ * Drop every cached index, for example after the key declarations changed.
31
+ *
32
+ * @returns {void}
33
+ *
34
+ * @example
35
+ * registry.clear();
36
+ */
37
+ clear() {
38
+ this.cache = new WeakMap();
39
+ }
40
+
41
+ /**
42
+ * Look up the nodes indexed under one or more key values.
43
+ *
44
+ * @param {string} name - The key name
45
+ * @param {string|string[]} values - One key value, or several to union
46
+ * @param {Document} doc - The document to search
47
+ * @returns {Node[]} Matching nodes in document order, without duplicates
48
+ * @throws {Error} When the key name was never declared
49
+ *
50
+ * @example
51
+ * registry.lookup('byId', 'a1', xmlDoc);
52
+ */
53
+ lookup(name, values, doc) {
54
+ if (!Object.hasOwn(this.keys, name)) {
55
+ throw new Error(`Undefined key: ${name}`);
56
+ }
57
+
58
+ const index = this.getIndex(name, doc);
59
+ const wanted = Array.isArray(values) ? values : [values];
60
+ const result = [];
61
+
62
+ for (const value of wanted) {
63
+ for (const node of index.get(value) || []) {
64
+ if (!result.includes(node)) result.push(node);
65
+ }
66
+ }
67
+
68
+ return result;
69
+ }
70
+
71
+ /**
72
+ * Get (building if needed) the index of one key for one document.
73
+ *
74
+ * @param {string} name - The key name
75
+ * @param {Document} doc - The document being indexed
76
+ * @returns {Map<string, Node[]>} Key value to nodes
77
+ */
78
+ getIndex(name, doc) {
79
+ let byName = this.cache.get(doc);
80
+ if (!byName) {
81
+ byName = new Map();
82
+ this.cache.set(doc, byName);
83
+ }
84
+
85
+ let index = byName.get(name);
86
+ if (!index) {
87
+ index = this.buildIndex(name, doc);
88
+ byName.set(name, index);
89
+ }
90
+
91
+ return index;
92
+ }
93
+
94
+ /**
95
+ * Build the index of one key for one document.
96
+ *
97
+ * @param {string} name - The key name
98
+ * @param {Document} doc - The document being indexed
99
+ * @returns {Map<string, Node[]>} Key value to nodes
100
+ */
101
+ buildIndex(name, doc) {
102
+ const { match, use } = this.keys[name];
103
+ const index = new Map();
104
+
105
+ for (const node of documentOrderNodes(doc)) {
106
+ if (!this.matchesPattern(node, match)) continue;
107
+
108
+ for (const value of this.evaluateUse(node, use)) {
109
+ const bucket = index.get(value);
110
+ if (bucket) bucket.push(node);
111
+ else index.set(value, [node]);
112
+ }
113
+ }
114
+
115
+ return index;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Walk a document in document order, including attribute nodes.
121
+ *
122
+ * @param {Node} root - The document or subtree root
123
+ * @yields {Node} Every node of the subtree
124
+ */
125
+ function* documentOrderNodes(root) {
126
+ const stack = [root];
127
+
128
+ while (stack.length > 0) {
129
+ const current = stack.pop();
130
+ yield current;
131
+
132
+ if (current.nodeType === 1 && current.attributes) {
133
+ for (const attribute of current.attributes) yield attribute;
134
+ }
135
+
136
+ const children = current.childNodes;
137
+ if (children) {
138
+ for (let i = children.length - 1; i >= 0; i--) stack.push(children[i]);
139
+ }
140
+ }
141
+ }