@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,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
+ }