@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.
- package/README.md +292 -47
- package/bin/lib/options.js +114 -0
- package/bin/lib/paths.js +186 -0
- package/bin/lib/transform.js +115 -0
- package/bin/xslt.js +68 -162
- package/dist/xslt-processor.browser.js +2073 -163
- package/dist/xslt-processor.browser.js.map +4 -4
- package/dist/xslt-processor.browser.min.js +6 -2
- package/dist/xslt-processor.browser.min.js.map +4 -4
- package/dist/xslt-processor.cjs +2077 -162
- package/dist/xslt-processor.cjs.map +4 -4
- package/dist/xslt-processor.d.cts +299 -0
- package/dist/xslt-processor.d.ts +92 -4
- package/dist/xslt-processor.js +2072 -161
- package/dist/xslt-processor.js.map +4 -4
- package/package.json +27 -16
- package/src/XSLTProcessor.js +177 -8
- package/src/index.js +11 -5
- package/src/xpath/evaluator.js +48 -7
- package/src/xslt/elements.js +57 -0
- package/src/xslt/engine.js +474 -185
- package/src/xslt/formatNumber.js +220 -0
- package/src/xslt/functions.js +191 -0
- package/src/xslt/index.js +31 -0
- package/src/xslt/keys.js +141 -0
- package/src/xslt/literalResult.js +167 -0
- package/src/xslt/number.js +178 -0
- package/src/xslt/numberFormat.js +155 -0
- package/src/xslt/resultTree.js +74 -0
- package/src/xslt/serializer/baseWriter.js +283 -0
- package/src/xslt/serializer/constants.js +78 -0
- package/src/xslt/serializer/escape.js +98 -0
- package/src/xslt/serializer/htmlSerializer.js +141 -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 +103 -0
- package/src/xslt/serializer/textSerializer.js +29 -0
- package/src/xslt/serializer/xmlSerializer.js +127 -0
- package/src/xslt/serializer.js +57 -0
- package/src/xslt/templatePriority.js +45 -0
- package/src/xslt/uri.js +68 -0
- package/src/xslt/whitespace.js +184 -0
- 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,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";
|
package/src/xslt/keys.js
ADDED
|
@@ -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
|
+
}
|