@tradik/xslt-processor 1.0.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +1 -1
- package/README.md +110 -520
- package/bin/lib/decode.js +15 -0
- package/bin/lib/dom.js +177 -0
- package/bin/lib/loaders.js +127 -0
- package/bin/lib/options.js +131 -0
- package/bin/lib/output.js +114 -0
- package/bin/lib/paths.js +186 -0
- package/bin/lib/transform.js +206 -0
- package/bin/xslt.js +73 -168
- package/dist/xslt-processor.browser.js +9564 -1585
- package/dist/xslt-processor.browser.js.map +4 -4
- package/dist/xslt-processor.browser.min.js +13 -2
- package/dist/xslt-processor.browser.min.js.map +4 -4
- package/dist/xslt-processor.cjs +9572 -1586
- package/dist/xslt-processor.cjs.map +4 -4
- package/dist/xslt-processor.d.cts +658 -0
- package/dist/xslt-processor.d.ts +459 -12
- package/dist/xslt-processor.js +9546 -1582
- package/dist/xslt-processor.js.map +4 -4
- package/package.json +71 -20
- package/src/XSLTProcessor.js +494 -48
- package/src/async/abort.js +63 -0
- package/src/async/documentUris.js +128 -0
- package/src/async/loaders.js +134 -0
- package/src/async/preload.js +159 -0
- package/src/async/processor.js +206 -0
- package/src/async/stream.js +125 -0
- package/src/bridge/engine.js +221 -0
- package/src/bridge/loader.js +78 -0
- package/src/bridge/results.js +75 -0
- package/src/bridge/version.js +63 -0
- package/src/index.js +26 -8
- package/src/io/decode.js +140 -0
- package/src/io/readSource.js +167 -0
- package/src/xpath/axes.js +562 -0
- package/src/xpath/documentOrder.js +270 -0
- package/src/xpath/evaluator.js +518 -357
- package/src/xpath/index.js +8 -2
- package/src/xpath/namespaceNodes.js +172 -0
- package/src/xpath/nodeSetFunctions.js +169 -0
- package/src/xpath/parser.js +30 -5
- package/src/xpath/strings.js +183 -0
- package/src/xpath/tokenizer.js +37 -23
- package/src/xslt/attributeSets.js +95 -0
- package/src/xslt/avt.js +103 -0
- package/src/xslt/computedNames.js +91 -0
- package/src/xslt/copying.js +212 -0
- package/src/xslt/declarationNames.js +80 -0
- package/src/xslt/domParsing.js +95 -0
- package/src/xslt/elements.js +57 -0
- package/src/xslt/engine/bindings.js +195 -0
- package/src/xslt/engine/context.js +105 -0
- package/src/xslt/engine/controlFlow.js +145 -0
- package/src/xslt/engine/copyInstructions.js +133 -0
- package/src/xslt/engine/declarations.js +233 -0
- package/src/xslt/engine/functionSupport.js +103 -0
- package/src/xslt/engine/methods.js +33 -0
- package/src/xslt/engine/nodeConstruction.js +187 -0
- package/src/xslt/engine/numbering.js +104 -0
- package/src/xslt/engine/outputDeclaration.js +77 -0
- package/src/xslt/engine/sequenceConstructor.js +228 -0
- package/src/xslt/engine/stylesheetLoading.js +208 -0
- package/src/xslt/engine/templateInvocation.js +253 -0
- package/src/xslt/engine/templateRules.js +243 -0
- package/src/xslt/engine/textInstructions.js +171 -0
- package/src/xslt/engine/topLevel.js +130 -0
- package/src/xslt/engine/transformation.js +263 -0
- package/src/xslt/engine/workStack.js +245 -0
- package/src/xslt/engine.js +184 -1736
- package/src/xslt/exslt/arguments.js +99 -0
- package/src/xslt/exslt/calendar.js +120 -0
- package/src/xslt/exslt/common.js +44 -0
- package/src/xslt/exslt/dateCalc.js +261 -0
- package/src/xslt/exslt/dateFormat.js +150 -0
- package/src/xslt/exslt/dateParse.js +265 -0
- package/src/xslt/exslt/dates.js +259 -0
- package/src/xslt/exslt/duration.js +207 -0
- package/src/xslt/exslt/dynamic.js +59 -0
- package/src/xslt/exslt/index.js +59 -0
- package/src/xslt/exslt/math.js +177 -0
- package/src/xslt/exslt/sets.js +96 -0
- package/src/xslt/exslt/stringOps.js +163 -0
- package/src/xslt/exslt/strings.js +147 -0
- package/src/xslt/exslt/uri.js +92 -0
- package/src/xslt/formatNumber.js +233 -0
- package/src/xslt/forwardsCompatible.js +75 -0
- package/src/xslt/functions.js +270 -0
- package/src/xslt/index.js +38 -1
- package/src/xslt/keys.js +164 -0
- package/src/xslt/literalResult.js +223 -0
- package/src/xslt/matchScope.js +116 -0
- package/src/xslt/number.js +271 -0
- package/src/xslt/numberFormat.js +253 -0
- package/src/xslt/outputNames.js +58 -0
- package/src/xslt/patternCompiler.js +175 -0
- package/src/xslt/patterns.js +324 -0
- package/src/xslt/qname.js +90 -0
- package/src/xslt/resultDocument.js +98 -0
- package/src/xslt/resultNamespaces.js +219 -0
- package/src/xslt/resultTree.js +211 -0
- package/src/xslt/serializer/baseWriter.js +390 -0
- package/src/xslt/serializer/chunks.js +120 -0
- package/src/xslt/serializer/constants.js +92 -0
- package/src/xslt/serializer/encoding.js +327 -0
- package/src/xslt/serializer/escape.js +135 -0
- package/src/xslt/serializer/frames.js +168 -0
- package/src/xslt/serializer/htmlDoctype.js +102 -0
- package/src/xslt/serializer/htmlEntities.js +77 -0
- package/src/xslt/serializer/htmlSerializer.js +239 -0
- package/src/xslt/serializer/indent.js +51 -0
- package/src/xslt/serializer/namespaces.js +68 -0
- package/src/xslt/serializer/rawText.js +41 -0
- package/src/xslt/serializer/settings.js +179 -0
- package/src/xslt/serializer/textSerializer.js +77 -0
- package/src/xslt/serializer/xhtmlDocument.js +103 -0
- package/src/xslt/serializer/xmlSerializer.js +227 -0
- package/src/xslt/serializer.js +90 -0
- package/src/xslt/sort.js +151 -0
- package/src/xslt/spaceNameTests.js +115 -0
- package/src/xslt/stylesheetChecks.js +206 -0
- package/src/xslt/stylesheetNamespaces.js +266 -0
- package/src/xslt/templatePriority.js +45 -0
- package/src/xslt/uri.js +68 -0
- package/src/xslt/variables.js +152 -0
- package/src/xslt/whitespace.js +200 -0
- package/LICENSE +0 -29
- package/src/XSLTProcessor.test.js +0 -930
- package/src/xpath/evaluator.test.js +0 -1852
- package/src/xpath/tokenizer.test.js +0 -224
- package/src/xslt/engine.test.js +0 -3130
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `xsl:number` number-to-string conversion.
|
|
3
|
+
*
|
|
4
|
+
* Renders the number sequence produced by {@link countXsltNumber} using the
|
|
5
|
+
* `format` attribute of `xsl:number`: numeric tokens (`1`, `01`, and the
|
|
6
|
+
* same in any Unicode digit family, e.g. `٠١`), alphabetic tokens (`a`, `A`)
|
|
7
|
+
* and Roman numerals (`i`, `I`), together with the prefix, separators and
|
|
8
|
+
* suffix taken from the format string itself.
|
|
9
|
+
*
|
|
10
|
+
* Extreme values follow libxslt: a negative number is formatted as 0, NaN
|
|
11
|
+
* and Infinity as by `string()`, alphabetic and Roman tokens use decimals
|
|
12
|
+
* below 1 (and Roman ones above 5000), and every conversion takes O(log n)
|
|
13
|
+
* steps, so a huge value cannot stall the transformation.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
"use strict";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Convert a positive integer to a bijective base-26 alphabetic sequence.
|
|
20
|
+
*
|
|
21
|
+
* @param {number} value - The number to convert
|
|
22
|
+
* @param {boolean} upperCase - Whether to emit upper case letters
|
|
23
|
+
* @returns {string} The alphabetic representation, e.g. `27` becomes `aa`
|
|
24
|
+
*/
|
|
25
|
+
function toAlphabetic(value, upperCase) {
|
|
26
|
+
let remaining = value;
|
|
27
|
+
let result = "";
|
|
28
|
+
|
|
29
|
+
while (remaining > 0) {
|
|
30
|
+
const index = (remaining - 1) % 26;
|
|
31
|
+
result = String.fromCodePoint((upperCase ? 65 : 97) + index) + result;
|
|
32
|
+
remaining = Math.floor((remaining - 1) / 26);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Roman numeral building blocks, largest first. */
|
|
39
|
+
const ROMAN_NUMERALS = Object.freeze([
|
|
40
|
+
["M", 1000],
|
|
41
|
+
["CM", 900],
|
|
42
|
+
["D", 500],
|
|
43
|
+
["CD", 400],
|
|
44
|
+
["C", 100],
|
|
45
|
+
["XC", 90],
|
|
46
|
+
["L", 50],
|
|
47
|
+
["XL", 40],
|
|
48
|
+
["X", 10],
|
|
49
|
+
["IX", 9],
|
|
50
|
+
["V", 5],
|
|
51
|
+
["IV", 4],
|
|
52
|
+
["I", 1],
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
/** Largest number written with Roman numerals, as in libxslt. */
|
|
56
|
+
const MAX_ROMAN = 5000;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Convert a positive integer to a Roman numeral.
|
|
60
|
+
*
|
|
61
|
+
* @param {number} value - The number to convert
|
|
62
|
+
* @returns {string} The upper case Roman numeral
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* toRoman(2004); // 'MMIV'
|
|
66
|
+
*/
|
|
67
|
+
export function toRoman(value) {
|
|
68
|
+
let remaining = value;
|
|
69
|
+
let result = "";
|
|
70
|
+
|
|
71
|
+
for (const [numeral, amount] of ROMAN_NUMERALS) {
|
|
72
|
+
result += numeral.repeat(Math.floor(remaining / amount));
|
|
73
|
+
remaining %= amount;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Insert a grouping separator every `size` digits, counting from the right.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} digits - The decimal digits
|
|
83
|
+
* @param {{separator?: string, size?: number}} grouping - The grouping settings
|
|
84
|
+
* @returns {string} The grouped digits
|
|
85
|
+
*/
|
|
86
|
+
function groupDigits(digits, { separator, size }) {
|
|
87
|
+
if (!separator || Number.isNaN(size) || size <= 0) return digits;
|
|
88
|
+
|
|
89
|
+
let result = "";
|
|
90
|
+
for (let end = digits.length; end > 0; end -= size) {
|
|
91
|
+
const group = digits.slice(Math.max(0, end - size), end);
|
|
92
|
+
result = result ? `${group}${separator}${result}` : group;
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A Unicode decimal digit (general category Nd). */
|
|
98
|
+
const DECIMAL_DIGIT = /^\p{Nd}$/u;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether a code point is a Unicode decimal digit.
|
|
102
|
+
*
|
|
103
|
+
* @param {number} codePoint - Any code point
|
|
104
|
+
* @returns {boolean} True for characters of category Nd
|
|
105
|
+
*/
|
|
106
|
+
function isDecimalDigit(codePoint) {
|
|
107
|
+
return DECIMAL_DIGIT.test(String.fromCodePoint(codePoint));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The zero of the digit family of a decimal format token: a token whose
|
|
112
|
+
* last character has the digit value 1 and whose other characters are the
|
|
113
|
+
* zero of that family (XSLT 1.0 section 7.7.1), e.g. `1`, `01`, `٠١`.
|
|
114
|
+
* Digit families are runs of ten code points, which may follow each other
|
|
115
|
+
* (the mathematical digits), so the value is counted from the run start.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} token - A format token
|
|
118
|
+
* @returns {number|null} The code point of the family's zero, or null
|
|
119
|
+
*/
|
|
120
|
+
function decimalTokenZero(token) {
|
|
121
|
+
const digits = Array.from(token, (char) => char.codePointAt(0));
|
|
122
|
+
const one = digits.at(-1);
|
|
123
|
+
if (!isDecimalDigit(one)) return null;
|
|
124
|
+
let start = one;
|
|
125
|
+
while (isDecimalDigit(start - 1)) start--;
|
|
126
|
+
const zero = one - 1;
|
|
127
|
+
if ((one - start) % 10 !== 1) return null;
|
|
128
|
+
return digits.slice(0, -1).every((digit) => digit === zero) ? zero : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The decimal digits of a non-negative integer, without exponent notation.
|
|
133
|
+
*
|
|
134
|
+
* @param {number} value - A finite, non-negative integer
|
|
135
|
+
* @returns {string} Its ASCII decimal digits
|
|
136
|
+
*/
|
|
137
|
+
function decimalDigits(value) {
|
|
138
|
+
return Number.isSafeInteger(value) ? String(value) : BigInt(value).toString();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Write a number with decimal digits of a family, padded with zeros to a
|
|
143
|
+
* minimum width and grouped.
|
|
144
|
+
*
|
|
145
|
+
* @param {number} value - A finite, non-negative integer
|
|
146
|
+
* @param {number} zero - Code point of the family's zero
|
|
147
|
+
* @param {number} width - Minimum number of digits
|
|
148
|
+
* @param {{separator?: string, size?: number}} grouping - Digit grouping
|
|
149
|
+
* @returns {string} The rendered number
|
|
150
|
+
*/
|
|
151
|
+
function formatDecimal(value, zero, width, grouping) {
|
|
152
|
+
const ascii = decimalDigits(value).padStart(width, "0");
|
|
153
|
+
const digits =
|
|
154
|
+
zero === 0x30
|
|
155
|
+
? ascii
|
|
156
|
+
: Array.from(ascii, (digit) =>
|
|
157
|
+
String.fromCodePoint(zero + Number(digit)),
|
|
158
|
+
).join("");
|
|
159
|
+
return groupDigits(digits, grouping);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Render one number with a single `xsl:number` format token.
|
|
164
|
+
*
|
|
165
|
+
* @param {number} value - The number to render
|
|
166
|
+
* @param {string} token - The format token, e.g. `1`, `01`, `a`, `I`
|
|
167
|
+
* @param {{separator?: string, size?: number}} grouping - Digit grouping
|
|
168
|
+
* @returns {string} The rendered number
|
|
169
|
+
*/
|
|
170
|
+
function formatToken(value, token, grouping) {
|
|
171
|
+
if (Number.isNaN(value) || value === Infinity) return String(value);
|
|
172
|
+
// Negative numbers are an error that libxslt recovers from with 0
|
|
173
|
+
const number = value < 0 ? 0 : Math.round(value);
|
|
174
|
+
|
|
175
|
+
if (/^\d+$/.test(token)) {
|
|
176
|
+
return formatDecimal(number, 0x30, token.length, grouping);
|
|
177
|
+
}
|
|
178
|
+
const zero = decimalTokenZero(token);
|
|
179
|
+
if (zero !== null) {
|
|
180
|
+
return formatDecimal(number, zero, Array.from(token).length, grouping);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const alphabetic = token === "a" || token === "A";
|
|
184
|
+
const roman = token === "i" || token === "I";
|
|
185
|
+
if (number < 1 || (roman && number > MAX_ROMAN) || (!alphabetic && !roman)) {
|
|
186
|
+
return decimalDigits(number);
|
|
187
|
+
}
|
|
188
|
+
if (alphabetic) return toAlphabetic(number, token === "A");
|
|
189
|
+
return token === "I" ? toRoman(number) : toRoman(number).toLowerCase();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Split an `xsl:number` format string into prefix, tokens, separators, suffix.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} format - The format attribute value
|
|
196
|
+
* @returns {{prefix: string, suffix: string, tokens: string[], separators: string[]}} The parsed format
|
|
197
|
+
*/
|
|
198
|
+
function parseFormat(format) {
|
|
199
|
+
const parts = format.match(/[\p{L}\p{N}]+|[^\p{L}\p{N}]+/gu) || [];
|
|
200
|
+
const isToken = (part) => /^[\p{L}\p{N}]+$/u.test(part);
|
|
201
|
+
|
|
202
|
+
const tokens = [];
|
|
203
|
+
const separators = [];
|
|
204
|
+
let prefix = "";
|
|
205
|
+
let suffix = "";
|
|
206
|
+
|
|
207
|
+
for (const part of parts) {
|
|
208
|
+
if (isToken(part)) tokens.push(part);
|
|
209
|
+
else if (tokens.length === 0) prefix = part;
|
|
210
|
+
else separators.push(part);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (parts.length > 0 && tokens.length > 0 && !isToken(parts.at(-1))) {
|
|
214
|
+
suffix = separators.pop();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (tokens.length === 0) tokens.push("1");
|
|
218
|
+
|
|
219
|
+
return { prefix, suffix, tokens, separators };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Format a number sequence produced by {@link countXsltNumber}.
|
|
224
|
+
*
|
|
225
|
+
* Decimal tokens are grouped when both `grouping.separator` and a positive
|
|
226
|
+
* `grouping.size` are given (the `grouping-separator` and `grouping-size`
|
|
227
|
+
* attributes).
|
|
228
|
+
*
|
|
229
|
+
* @param {number[]} numbers - The numbers, outermost first
|
|
230
|
+
* @param {string} [format] - The `format` attribute value
|
|
231
|
+
* @param {{separator?: string, size?: number}} [grouping] - Digit grouping
|
|
232
|
+
* @returns {string} The formatted string, empty when there is nothing to number
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* formatXsltNumber([2, 3], '1.1'); // '2.3'
|
|
236
|
+
* formatXsltNumber([1234567], '1', { separator: ',', size: 3 }); // '1,234,567'
|
|
237
|
+
*/
|
|
238
|
+
export function formatXsltNumber(numbers, format = "1", grouping = {}) {
|
|
239
|
+
if (numbers.length === 0) return "";
|
|
240
|
+
|
|
241
|
+
const { prefix, suffix, tokens, separators } = parseFormat(format);
|
|
242
|
+
let result = prefix;
|
|
243
|
+
|
|
244
|
+
numbers.forEach((value, index) => {
|
|
245
|
+
if (index > 0) {
|
|
246
|
+
const separator = separators[index - 1] ?? separators.at(-1) ?? ".";
|
|
247
|
+
result += separator;
|
|
248
|
+
}
|
|
249
|
+
result += formatToken(value, tokens[index] ?? tokens.at(-1), grouping);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return result + suffix;
|
|
253
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QName-valued attributes of `xsl:output` (XSLT 1.0 section 16).
|
|
3
|
+
*
|
|
4
|
+
* `cdata-section-elements` lists QNames that are expanded with the namespace
|
|
5
|
+
* declarations in scope on the `xsl:output` element, the default namespace
|
|
6
|
+
* included for unprefixed names (section 16.1). The serializer then compares
|
|
7
|
+
* expanded names, so the prefixes used in the result do not matter.
|
|
8
|
+
*
|
|
9
|
+
* @module xslt/outputNames
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
import { isQName } from "./qname.js";
|
|
15
|
+
import { splitQName } from "./resultNamespaces.js";
|
|
16
|
+
import { resolvePrefix } from "./stylesheetNamespaces.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {{namespaceUri: (string|null), localName: string}} ExpandedName
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Expand the QNames of a `cdata-section-elements` attribute and add them to
|
|
24
|
+
* the names already declared (several xsl:output elements are merged, so the
|
|
25
|
+
* lists are united). Invalid names and undeclared prefixes are reported and
|
|
26
|
+
* skipped.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} value - Whitespace separated QNames
|
|
29
|
+
* @param {Object<string, string>} scope - Namespaces in scope on xsl:output
|
|
30
|
+
* @param {ExpandedName[]} declared - Names declared by earlier xsl:output elements
|
|
31
|
+
* @param {(message: string) => void} warn - Reports a name that is skipped
|
|
32
|
+
* @returns {ExpandedName[]} The union, without duplicates
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* cdataSectionNames("p:c d", { p: "urn:p" }, [], console.warn);
|
|
36
|
+
* // [{ namespaceUri: "urn:p", localName: "c" },
|
|
37
|
+
* // { namespaceUri: null, localName: "d" }]
|
|
38
|
+
*/
|
|
39
|
+
export function cdataSectionNames(value, scope, declared, warn) {
|
|
40
|
+
const names = [...declared];
|
|
41
|
+
const seen = new Set(names.map((n) => `{${n.namespaceUri}}${n.localName}`));
|
|
42
|
+
|
|
43
|
+
for (const qname of value.split(/[ \t\r\n]+/).filter(Boolean)) {
|
|
44
|
+
const { prefix, localName } = splitQName(qname);
|
|
45
|
+
const namespaceUri = resolvePrefix(scope, prefix);
|
|
46
|
+
if (!isQName(qname) || (prefix && !namespaceUri)) {
|
|
47
|
+
warn(
|
|
48
|
+
`xsl:output cdata-section-elements: "${qname}" is not a QName with a declared prefix and is ignored`,
|
|
49
|
+
);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const key = `{${namespaceUri}}${localName}`;
|
|
53
|
+
if (seen.has(key)) continue;
|
|
54
|
+
seen.add(key);
|
|
55
|
+
names.push({ namespaceUri, localName });
|
|
56
|
+
}
|
|
57
|
+
return names;
|
|
58
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compilation of XSLT 1.0 patterns (section 5.2).
|
|
3
|
+
*
|
|
4
|
+
* A pattern is parsed with the XPath parser and turned into a list of
|
|
5
|
+
* alternatives (one per union member). Each alternative has an anchor (the
|
|
6
|
+
* root for absolute paths, an `id()`/`key()` call, or none) and a list of
|
|
7
|
+
* child or attribute steps, each remembering the separator (`/` or `//`)
|
|
8
|
+
* that links it to the step on its left.
|
|
9
|
+
*
|
|
10
|
+
* @module xslt/patternCompiler
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { NodeType, parse } from "../xpath/parser.js";
|
|
14
|
+
|
|
15
|
+
/** Number of (literal) arguments of the anchor functions of patterns. */
|
|
16
|
+
const ANCHOR_ARITY = Object.freeze({ id: 1, key: 2 });
|
|
17
|
+
const POSITIONAL_FUNCTIONS = new Set(["position", "last"]);
|
|
18
|
+
|
|
19
|
+
/** Marker anchor of absolute location path patterns. */
|
|
20
|
+
export const ROOT = Symbol("root");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether an expression calls `position()` or `last()` anywhere.
|
|
24
|
+
*
|
|
25
|
+
* @param {*} ast - XPath AST node (or any nested value)
|
|
26
|
+
* @returns {boolean} True when the expression may depend on the position
|
|
27
|
+
*/
|
|
28
|
+
function usesPosition(ast) {
|
|
29
|
+
if (Array.isArray(ast)) return ast.some(usesPosition);
|
|
30
|
+
if (!ast || typeof ast !== "object") return false;
|
|
31
|
+
if (
|
|
32
|
+
ast.type === NodeType.FUNCTION_CALL &&
|
|
33
|
+
!ast.prefix &&
|
|
34
|
+
POSITIONAL_FUNCTIONS.has(ast.name)
|
|
35
|
+
) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return Object.values(ast).some(usesPosition);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether a step is the `descendant-or-self::node()` step that `//` expands to.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} step - Step AST node
|
|
45
|
+
* @returns {boolean} True for the abbreviated `//` step
|
|
46
|
+
*/
|
|
47
|
+
function isDescendantSeparator(step) {
|
|
48
|
+
return (
|
|
49
|
+
step.axis === "descendant-or-self" &&
|
|
50
|
+
step.nodeTest.type === NodeType.NODE_TYPE_TEST &&
|
|
51
|
+
step.nodeTest.nodeType === "node" &&
|
|
52
|
+
step.predicates.length === 0
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Split a parsed union into its alternatives.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} ast - XPath AST
|
|
60
|
+
* @param {object[]} [result] - Array to append to
|
|
61
|
+
* @returns {object[]} The alternatives in pattern order
|
|
62
|
+
*/
|
|
63
|
+
function unionAlternatives(ast, result = []) {
|
|
64
|
+
if (ast.type === NodeType.UNION_EXPR) {
|
|
65
|
+
unionAlternatives(ast.left, result);
|
|
66
|
+
unionAlternatives(ast.right, result);
|
|
67
|
+
} else {
|
|
68
|
+
result.push(ast);
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Whether an AST node is an `id()` or `key()` call.
|
|
75
|
+
*
|
|
76
|
+
* @param {object} ast - XPath AST node
|
|
77
|
+
* @returns {boolean} True for an anchor function call
|
|
78
|
+
*/
|
|
79
|
+
function isAnchorCall(ast) {
|
|
80
|
+
return (
|
|
81
|
+
ast?.type === NodeType.FUNCTION_CALL &&
|
|
82
|
+
!ast.prefix &&
|
|
83
|
+
Object.hasOwn(ANCHOR_ARITY, ast.name)
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check the arguments of an `id()`/`key()` pattern anchor: XSLT 1.0
|
|
89
|
+
* (section 5.2, production IdKeyPattern) only allows string literals, as
|
|
90
|
+
* libxslt enforces.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} call - Function call AST of the anchor
|
|
93
|
+
* @returns {object} The call
|
|
94
|
+
* @throws {Error} When the arguments are not the right number of literals
|
|
95
|
+
*/
|
|
96
|
+
function checkAnchorArguments(call) {
|
|
97
|
+
const arity = ANCHOR_ARITY[call.name];
|
|
98
|
+
const literals = call.args.every((arg) => arg.type === NodeType.LITERAL);
|
|
99
|
+
if (call.args.length !== arity || !literals) {
|
|
100
|
+
const noun = arity === 1 ? "literal" : "literals";
|
|
101
|
+
throw new Error(
|
|
102
|
+
`${call.name}() expects ${arity} ${noun} in a pattern (XSLT 1.0 section 5.2)`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return call;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Compile one location path pattern.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} ast - AST of one union alternative
|
|
112
|
+
* @returns {{anchor: (symbol|object|null), steps: object[]}} Compiled alternative
|
|
113
|
+
* @throws {Error} When the expression is not a valid pattern
|
|
114
|
+
*/
|
|
115
|
+
function compileAlternative(ast) {
|
|
116
|
+
let anchor;
|
|
117
|
+
let steps;
|
|
118
|
+
|
|
119
|
+
if (ast.type === NodeType.LOCATION_PATH) {
|
|
120
|
+
anchor = ast.absolute ? ROOT : null;
|
|
121
|
+
steps = ast.steps;
|
|
122
|
+
} else if (isAnchorCall(ast)) {
|
|
123
|
+
anchor = checkAnchorArguments(ast);
|
|
124
|
+
steps = [];
|
|
125
|
+
} else if (
|
|
126
|
+
ast.type === NodeType.PATH_EXPR &&
|
|
127
|
+
isAnchorCall(ast.filter) &&
|
|
128
|
+
!ast.predicates
|
|
129
|
+
) {
|
|
130
|
+
anchor = checkAnchorArguments(ast.filter);
|
|
131
|
+
steps = ast.steps;
|
|
132
|
+
} else {
|
|
133
|
+
throw new Error(`Unsupported pattern expression: ${ast.type}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const compiled = [];
|
|
137
|
+
let separator = anchor ? "/" : null;
|
|
138
|
+
|
|
139
|
+
for (const step of steps) {
|
|
140
|
+
if (isDescendantSeparator(step)) {
|
|
141
|
+
separator = "//";
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (step.axis !== "child" && step.axis !== "attribute") {
|
|
145
|
+
throw new Error(`Axis not allowed in a pattern: ${step.axis}`);
|
|
146
|
+
}
|
|
147
|
+
compiled.push({
|
|
148
|
+
axis: step.axis,
|
|
149
|
+
nodeTest: step.nodeTest,
|
|
150
|
+
separator,
|
|
151
|
+
predicates: step.predicates.map((predicate) => ({
|
|
152
|
+
expr: predicate.expr,
|
|
153
|
+
positional: usesPosition(predicate.expr),
|
|
154
|
+
})),
|
|
155
|
+
});
|
|
156
|
+
separator = "/";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (separator === "//") throw new Error("Pattern cannot end with //");
|
|
160
|
+
return { anchor, steps: compiled };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parse and compile a pattern string.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} pattern - The XSLT pattern
|
|
167
|
+
* @returns {object[]} The compiled alternatives
|
|
168
|
+
* @throws {Error} When the pattern is not valid
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* compilePattern('chapter/title | appendix//title');
|
|
172
|
+
*/
|
|
173
|
+
export function compilePattern(pattern) {
|
|
174
|
+
return unionAlternatives(parse(pattern)).map(compileAlternative);
|
|
175
|
+
}
|