@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,207 @@
1
+ /**
2
+ * xs:duration values of the EXSLT dates-and-times module, ported from
3
+ * libexslt `date.c`: parsing, formatting and addition.
4
+ *
5
+ * A duration is `{mon, day, sec}`: years are folded into months, hours and
6
+ * minutes into seconds, and `0 <= sec < 86400`. A negative duration has
7
+ * negative months/days and borrows one day for a non-zero `sec`.
8
+ */
9
+
10
+ "use strict";
11
+
12
+ import { SECS_PER_DAY } from "./calendar.js";
13
+
14
+ /** Designators in the order they may appear; "T" precedes index 3. */
15
+ const DESIGNATORS = "YMDHMS";
16
+
17
+ /** Index of the first time designator. */
18
+ const TIME_PART = 3;
19
+
20
+ /** Index of the seconds designator, the only one allowing a fraction. */
21
+ const SECONDS_PART = 5;
22
+
23
+ /** Seconds per unit of the time designators, by index. */
24
+ const UNIT_SECONDS = [0, 0, 0, 3600, 60, 1];
25
+
26
+ /**
27
+ * Create a zero duration.
28
+ *
29
+ * @returns {{mon: number, day: number, sec: number}} The duration
30
+ */
31
+ export function createDuration() {
32
+ return { mon: 0, day: 0, sec: 0 };
33
+ }
34
+
35
+ /**
36
+ * Read the number before a designator: digits and an optional fraction.
37
+ *
38
+ * @param {string} text - The duration text
39
+ * @param {number} at - Start index
40
+ * @returns {{num: number, fraction: number, hasDigits: boolean, hasFraction: boolean, end: number}} The number
41
+ */
42
+ function readNumber(text, at) {
43
+ const [whole, digits, dot, fractionDigits] = /^(\d*)(\.?)(\d*)/.exec(
44
+ text.substring(at),
45
+ );
46
+ let fraction = 0;
47
+ let scale = 1;
48
+ for (const digit of dot ? fractionDigits : "") {
49
+ scale /= 10;
50
+ fraction += Number(digit) * scale;
51
+ }
52
+ return {
53
+ num: Number(digits || "0"),
54
+ fraction,
55
+ hasDigits: digits !== "" || (dot !== "" && fractionDigits !== ""),
56
+ hasFraction: dot !== "",
57
+ end: at + whole.length,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Parse an xs:duration such as "-P1Y2M3DT4H5M6.5S"
63
+ * (libexslt `exsltDateParseDuration`).
64
+ *
65
+ * @param {string} text - The lexical form
66
+ * @returns {{mon: number, day: number, sec: number}|null} The duration, null when invalid
67
+ */
68
+ export function parseDuration(text) {
69
+ const negative = text.startsWith("-");
70
+ let at = negative ? 1 : 0;
71
+ if (text.charAt(at) !== "P" || at + 1 === text.length) return null;
72
+ at++;
73
+
74
+ const dur = createDuration();
75
+ let seq = 0;
76
+ let secs = 0;
77
+ let fraction = 0;
78
+ while (at < text.length) {
79
+ if (seq >= DESIGNATORS.length) return null;
80
+ if (text.charAt(at) === "T") {
81
+ if (seq > TIME_PART) return null;
82
+ at++;
83
+ seq = TIME_PART;
84
+ } else if (seq === TIME_PART) {
85
+ return null;
86
+ }
87
+
88
+ const number = readNumber(text, at);
89
+ at = number.end;
90
+ while (text.charAt(at) !== DESIGNATORS[seq]) {
91
+ seq++;
92
+ if (seq === TIME_PART || seq === DESIGNATORS.length) return null;
93
+ }
94
+ at++;
95
+ if (!number.hasDigits || (number.hasFraction && seq !== SECONDS_PART)) {
96
+ return null;
97
+ }
98
+ if (!Number.isSafeInteger(number.num)) return null;
99
+ fraction += number.fraction;
100
+
101
+ if (seq === 0) dur.mon = number.num * 12;
102
+ else if (seq === 1) dur.mon += number.num;
103
+ else if (seq === 2) dur.day = number.num;
104
+ else {
105
+ const seconds = number.num * UNIT_SECONDS[seq];
106
+ dur.day += Math.floor(seconds / SECS_PER_DAY);
107
+ secs += seconds % SECS_PER_DAY;
108
+ }
109
+ seq++;
110
+ }
111
+
112
+ dur.day += Math.floor(secs / SECS_PER_DAY);
113
+ dur.sec = (secs % SECS_PER_DAY) + fraction;
114
+ if (negative) {
115
+ // 0 - x rather than -x: integers in C have no negative zero
116
+ dur.mon = 0 - dur.mon;
117
+ dur.day = 0 - dur.day;
118
+ if (dur.sec !== 0) {
119
+ dur.sec = SECS_PER_DAY - dur.sec;
120
+ dur.day -= 1;
121
+ }
122
+ }
123
+ return dur;
124
+ }
125
+
126
+ /**
127
+ * Decimal digits of a fraction of a second, without trailing zeros
128
+ * (libexslt `exsltFormatNanoseconds`).
129
+ *
130
+ * @param {number} nanoseconds - Nanoseconds, 0-999999999
131
+ * @returns {string} "" for zero, else "." and up to nine digits
132
+ */
133
+ export function formatNanoseconds(nanoseconds) {
134
+ if (nanoseconds <= 0) return "";
135
+ return `.${String(nanoseconds).padStart(9, "0").replace(/0+$/, "")}`;
136
+ }
137
+
138
+ /**
139
+ * Format a duration (libexslt `exsltDateFormatDuration`), e.g. "P1Y2MT3S".
140
+ *
141
+ * @param {{mon: number, day: number, sec: number}} dur - The duration
142
+ * @returns {string} The lexical form, "P0D" for a zero duration
143
+ */
144
+ export function formatDuration(dur) {
145
+ if (dur.sec === 0 && dur.day === 0 && dur.mon === 0) return "P0D";
146
+
147
+ let { sec: secs, day: days, mon: months } = dur;
148
+ let sign = "";
149
+ if (days < 0) {
150
+ if (secs !== 0) {
151
+ secs = SECS_PER_DAY - secs;
152
+ days += 1;
153
+ }
154
+ days = -days;
155
+ sign = "-";
156
+ }
157
+ if (months < 0) {
158
+ months = -months;
159
+ sign = "-";
160
+ }
161
+
162
+ let result = `${sign}P`;
163
+ const years = Math.trunc(months / 12);
164
+ months -= years * 12;
165
+ if (years > 0) result += `${years}Y`;
166
+ if (months !== 0) result += `${months}M`;
167
+ if (days !== 0) result += `${days}D`;
168
+
169
+ let intSecs = Math.floor(secs);
170
+ let nanoseconds = Math.floor((secs - intSecs) * 1e9 + 0.5);
171
+ if (nanoseconds >= 1e9) {
172
+ nanoseconds -= 1e9;
173
+ intSecs += 1;
174
+ }
175
+ if (intSecs === 0 && nanoseconds === 0) return result;
176
+
177
+ result += "T";
178
+ const hours = Math.floor(intSecs / 3600);
179
+ const minutes = Math.floor((intSecs % 3600) / 60);
180
+ intSecs %= 60;
181
+ if (hours > 0) result += `${hours}H`;
182
+ if (minutes > 0) result += `${minutes}M`;
183
+ if (intSecs > 0 || nanoseconds > 0) {
184
+ result += `${intSecs}${formatNanoseconds(nanoseconds)}S`;
185
+ }
186
+ return result;
187
+ }
188
+
189
+ /**
190
+ * Add two durations (libexslt `_exsltDateAddDurCalc`).
191
+ *
192
+ * @param {{mon: number, day: number, sec: number}} x - First duration
193
+ * @param {{mon: number, day: number, sec: number}} y - Second duration
194
+ * @returns {{mon: number, day: number, sec: number}|null} The sum, null when
195
+ * months and days would have opposite signs (an indeterminate result)
196
+ */
197
+ export function addDurations(x, y) {
198
+ const sum = { mon: x.mon + y.mon, day: x.day + y.day, sec: x.sec + y.sec };
199
+ if (sum.sec >= SECS_PER_DAY) {
200
+ sum.sec -= SECS_PER_DAY;
201
+ sum.day += 1;
202
+ }
203
+ if (sum.day >= 0) {
204
+ return (sum.day > 0 || sum.sec > 0) && sum.mon < 0 ? null : sum;
205
+ }
206
+ return sum.mon > 0 ? null : sum;
207
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * EXSLT dynamic module (http://exslt.org/dynamic): `dyn:evaluate()` only,
3
+ * following libexslt `dynamic.c`.
4
+ *
5
+ * Security: a stylesheet is trusted code, but the string handed to
6
+ * `dyn:evaluate()` often comes from the transformed document. The function
7
+ * is therefore disabled unless the engine opts in with
8
+ * `engine.enableDynamicEvaluate = true`; while disabled,
9
+ * `function-available('dyn:evaluate')` is false and a call throws. The
10
+ * expression runs with the evaluator's recursion and result size limits.
11
+ */
12
+
13
+ "use strict";
14
+
15
+ import { expandedFunctionName } from "../../xpath/evaluator.js";
16
+ import { parse } from "../../xpath/parser.js";
17
+ import { EXSLT_DYNAMIC, checkArity } from "./arguments.js";
18
+
19
+ /**
20
+ * Build the EXSLT dynamic functions.
21
+ *
22
+ * @param {import('../../xpath/evaluator.js').XPathEvaluator} evaluator - Evaluates the expressions
23
+ * @param {() => boolean} isEnabled - Whether `dyn:evaluate()` may run, asked at each call
24
+ * @returns {Object<string, Function>} Functions keyed by expanded name
25
+ */
26
+ export function createDynamicFunctions(evaluator, isEnabled) {
27
+ /**
28
+ * `dyn:evaluate(string)`: evaluate the string as an XPath expression in
29
+ * the current context (context node, position, size, variables and
30
+ * namespaces). An empty string, or an expression that fails to parse,
31
+ * yields an empty node-set, as in libexslt. Evaluation errors propagate,
32
+ * so the evaluator's limits cannot be defeated by swallowing them.
33
+ *
34
+ * @param {Array} args - Argument expressions
35
+ * @param {import('../../xpath/evaluator.js').XPathContext} ctx - Evaluation context
36
+ * @returns {*} The value of the expression
37
+ * @throws {Error} When dynamic evaluation is disabled
38
+ */
39
+ const evaluate = (args, ctx) => {
40
+ if (!isEnabled()) {
41
+ throw new Error(
42
+ "dyn:evaluate() is disabled; set enableDynamicEvaluate on the engine to allow it",
43
+ );
44
+ }
45
+ checkArity("dyn:evaluate", args, 1);
46
+ const expression = evaluator.toString(evaluator.evaluate(args[0], ctx));
47
+ if (expression === "") return [];
48
+ let ast;
49
+ try {
50
+ ast = parse(expression);
51
+ } catch {
52
+ return [];
53
+ }
54
+ return evaluator.evaluate(ast, ctx);
55
+ };
56
+ evaluate.isAvailable = isEnabled;
57
+
58
+ return { [expandedFunctionName(EXSLT_DYNAMIC, "evaluate")]: evaluate };
59
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * EXSLT extension functions (https://exslt.org/), the set libexslt provides
3
+ * to libxslt and so to Chrome's native XSLTProcessor: common, math, sets,
4
+ * strings, dates-and-times and dynamic.
5
+ *
6
+ * `../functions.js` merges this map into the XSLT function library, so the
7
+ * functions are found by expanded name through any prefix bound to their
8
+ * namespace and are reported by `function-available()`.
9
+ *
10
+ * Engine properties read at call time (all optional):
11
+ * - `clock()`: returns the current instant as a Date (or any object with
12
+ * `getTime()` and `getTimezoneOffset()`), for deterministic dates;
13
+ * - `enableDynamicEvaluate`: `true` enables `dyn:evaluate()`.
14
+ */
15
+
16
+ "use strict";
17
+
18
+ import { createCommonFunctions } from "./common.js";
19
+ import { createDatesFunctions, systemClock } from "./dates.js";
20
+ import { createDynamicFunctions } from "./dynamic.js";
21
+ import { createMathFunctions } from "./math.js";
22
+ import { createSetsFunctions } from "./sets.js";
23
+ import { createStringsFunctions } from "./strings.js";
24
+
25
+ export {
26
+ EXSLT_COMMON,
27
+ EXSLT_DATES,
28
+ EXSLT_DYNAMIC,
29
+ EXSLT_MATH,
30
+ EXSLT_SETS,
31
+ EXSLT_STRINGS,
32
+ } from "./arguments.js";
33
+
34
+ /**
35
+ * Build the EXSLT function map of an engine.
36
+ *
37
+ * @param {{xpathEvaluator: import('../../xpath/evaluator.js').XPathEvaluator, clock?: Function, enableDynamicEvaluate?: boolean}} engine - The engine
38
+ * @returns {Object<string, Function>} Functions keyed by expanded name
39
+ *
40
+ * @example
41
+ * evaluator.registerFunctions(createExsltFunctions(engine));
42
+ */
43
+ export function createExsltFunctions(engine) {
44
+ const evaluator = engine.xpathEvaluator;
45
+ const clock = () =>
46
+ typeof engine.clock === "function" ? engine.clock() : systemClock();
47
+
48
+ return {
49
+ ...createCommonFunctions(evaluator),
50
+ ...createMathFunctions(evaluator),
51
+ ...createSetsFunctions(evaluator),
52
+ ...createStringsFunctions(evaluator),
53
+ ...createDatesFunctions(evaluator, clock),
54
+ ...createDynamicFunctions(
55
+ evaluator,
56
+ () => engine.enableDynamicEvaluate === true,
57
+ ),
58
+ };
59
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * EXSLT math module (http://exslt.org/math), following libexslt `math.c`.
3
+ *
4
+ * - `min()` / `max()` are NaN for an empty node-set or when any node is NaN.
5
+ * - `highest()` / `lowest()` are empty in the same cases.
6
+ * - `constant(name, precision)` truncates the constant's decimal text to
7
+ * `precision` characters (the leading "3." of PI counts), NaN otherwise.
8
+ * - Every numeric function returns NaN for a NaN argument.
9
+ */
10
+
11
+ "use strict";
12
+
13
+ import { expandedFunctionName } from "../../xpath/evaluator.js";
14
+ import { parseXPathNumber } from "../../xpath/strings.js";
15
+ import {
16
+ EXSLT_MATH,
17
+ checkArity,
18
+ inDocumentOrder,
19
+ toNodeSet,
20
+ } from "./arguments.js";
21
+
22
+ /** Decimal text of the constants known to `math:constant()`. */
23
+ const CONSTANTS = Object.freeze({
24
+ PI: "3.1415926535897932384626433832795028841971693993751",
25
+ E: "2.71828182845904523536028747135266249775724709369996",
26
+ SQRRT2: "1.41421356237309504880168872420969807856967187537694",
27
+ LN2: "0.69314718055994530941723212145817656807550013436025",
28
+ LN10: "2.30258509299404568402",
29
+ LOG2E: "1.4426950408889634074",
30
+ SQRT1_2: "0.70710678118654752440",
31
+ });
32
+
33
+ /**
34
+ * `math:constant(name, precision)`.
35
+ *
36
+ * @param {string} name - Constant name, case-sensitive
37
+ * @param {number} precision - Number of characters of the decimal text
38
+ * @returns {number} The truncated constant, NaN for an unknown name
39
+ */
40
+ export function mathConstant(name, precision) {
41
+ if (Number.isNaN(precision) || precision < 1) return Number.NaN;
42
+ if (!Object.hasOwn(CONSTANTS, name)) return Number.NaN;
43
+ const text = CONSTANTS[name];
44
+ return parseXPathNumber(
45
+ text.substring(0, Math.min(text.length, Math.trunc(precision))),
46
+ );
47
+ }
48
+
49
+ /**
50
+ * `math:power(base, power)` with C `pow()` semantics: 1 raised to anything,
51
+ * and -1 raised to an infinity, are 1 (JavaScript gives NaN).
52
+ *
53
+ * @param {number} base - The base
54
+ * @param {number} power - The exponent
55
+ * @returns {number} The power, NaN when either argument is NaN
56
+ */
57
+ export function mathPower(base, power) {
58
+ if (Number.isNaN(base) || Number.isNaN(power)) return Number.NaN;
59
+ if (base === 1 || (base === -1 && !Number.isFinite(power))) return 1;
60
+ return Math.pow(base, power);
61
+ }
62
+
63
+ /**
64
+ * Extreme nodes of a node-set: those whose number is the best one.
65
+ *
66
+ * @param {number[]} values - Numbers of the nodes, in node order
67
+ * @param {(a: number, b: number) => boolean} better - Whether a beats b
68
+ * @returns {number[]} Indexes of the extreme nodes, empty when any is NaN
69
+ */
70
+ function extremeIndexes(values, better) {
71
+ if (values.length === 0 || values.some(Number.isNaN)) return [];
72
+ let best = values[0];
73
+ let indexes = [];
74
+ values.forEach((value, index) => {
75
+ if (better(value, best)) {
76
+ best = value;
77
+ indexes = [index];
78
+ } else if (value === best) {
79
+ indexes.push(index);
80
+ }
81
+ });
82
+ return indexes;
83
+ }
84
+
85
+ /** One-argument numeric functions and their implementation. */
86
+ const UNARY = Object.freeze({
87
+ abs: Math.abs,
88
+ sqrt: Math.sqrt,
89
+ log: Math.log,
90
+ sin: Math.sin,
91
+ cos: Math.cos,
92
+ tan: Math.tan,
93
+ asin: Math.asin,
94
+ acos: Math.acos,
95
+ atan: Math.atan,
96
+ exp: Math.exp,
97
+ });
98
+
99
+ /**
100
+ * Build the EXSLT math functions.
101
+ *
102
+ * @param {import('../../xpath/evaluator.js').XPathEvaluator} evaluator - Evaluates the arguments
103
+ * @param {{random?: () => number}} [options] - `random` replaces `Math.random` (tests)
104
+ * @returns {Object<string, Function>} Functions keyed by expanded name
105
+ */
106
+ export function createMathFunctions(evaluator, options = {}) {
107
+ const random = options.random ?? Math.random;
108
+ const number = (arg, ctx) => evaluator.toNumber(evaluator.evaluate(arg, ctx));
109
+
110
+ /**
111
+ * Numbers of the nodes of a node-set argument, in document order.
112
+ *
113
+ * @param {string} name - Function name, for errors
114
+ * @param {Array} args - Argument expressions
115
+ * @param {object} ctx - Evaluation context
116
+ * @returns {{nodes: Node[], values: number[]}} The nodes and their numbers
117
+ */
118
+ const numbered = (name, args, ctx) => {
119
+ checkArity(name, args, 1);
120
+ const nodes = inDocumentOrder(
121
+ evaluator,
122
+ toNodeSet(name, evaluator.evaluate(args[0], ctx)),
123
+ );
124
+ const values = nodes.map((node) =>
125
+ evaluator.toNumber(evaluator.getStringValue(node)),
126
+ );
127
+ return { nodes, values };
128
+ };
129
+
130
+ const extremeValue = (name, better) => (args, ctx) => {
131
+ const { values } = numbered(name, args, ctx);
132
+ const indexes = extremeIndexes(values, better);
133
+ return indexes.length === 0 ? Number.NaN : values[indexes[0]];
134
+ };
135
+
136
+ const extremeNodes = (name, better) => (args, ctx) => {
137
+ const { nodes, values } = numbered(name, args, ctx);
138
+ return extremeIndexes(values, better).map((index) => nodes[index]);
139
+ };
140
+
141
+ const less = (a, b) => a < b;
142
+ const greater = (a, b) => a > b;
143
+ const key = (local) => expandedFunctionName(EXSLT_MATH, local);
144
+
145
+ const functions = {
146
+ [key("min")]: extremeValue("math:min", less),
147
+ [key("max")]: extremeValue("math:max", greater),
148
+ [key("lowest")]: extremeNodes("math:lowest", less),
149
+ [key("highest")]: extremeNodes("math:highest", greater),
150
+ [key("constant")]: (args, ctx) => {
151
+ checkArity("math:constant", args, 2);
152
+ const name = evaluator.toString(evaluator.evaluate(args[0], ctx));
153
+ return mathConstant(name, number(args[1], ctx));
154
+ },
155
+ [key("random")]: (args) => {
156
+ checkArity("math:random", args, 0);
157
+ return random();
158
+ },
159
+ [key("power")]: (args, ctx) => {
160
+ checkArity("math:power", args, 2);
161
+ return mathPower(number(args[0], ctx), number(args[1], ctx));
162
+ },
163
+ [key("atan2")]: (args, ctx) => {
164
+ checkArity("math:atan2", args, 2);
165
+ return Math.atan2(number(args[0], ctx), number(args[1], ctx));
166
+ },
167
+ };
168
+
169
+ for (const [local, fn] of Object.entries(UNARY)) {
170
+ functions[key(local)] = (args, ctx) => {
171
+ checkArity(`math:${local}`, args, 1);
172
+ return fn(number(args[0], ctx));
173
+ };
174
+ }
175
+
176
+ return functions;
177
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * EXSLT sets module (http://exslt.org/sets), following libexslt `sets.c` and
3
+ * the libxml2 node-set primitives it calls.
4
+ *
5
+ * Every argument must be a node-set and every node-set result is in document
6
+ * order. `leading()` / `trailing()` return the first node-set unchanged when
7
+ * the second is empty, and an empty node-set when the first node of the
8
+ * second is not in the first.
9
+ */
10
+
11
+ "use strict";
12
+
13
+ import { expandedFunctionName } from "../../xpath/evaluator.js";
14
+ import {
15
+ EXSLT_SETS,
16
+ checkArity,
17
+ inDocumentOrder,
18
+ toNodeSet,
19
+ } from "./arguments.js";
20
+
21
+ /**
22
+ * Build the EXSLT sets functions.
23
+ *
24
+ * @param {import('../../xpath/evaluator.js').XPathEvaluator} evaluator - Evaluates the arguments
25
+ * @returns {Object<string, Function>} Functions keyed by expanded name
26
+ */
27
+ export function createSetsFunctions(evaluator) {
28
+ /**
29
+ * Evaluate the node-set arguments of a call, each in document order.
30
+ *
31
+ * @param {string} name - Function name, for errors
32
+ * @param {Array} args - Argument expressions
33
+ * @param {object} ctx - Evaluation context
34
+ * @param {number} count - Required number of arguments
35
+ * @returns {Node[][]} The node-sets
36
+ */
37
+ const nodeSets = (name, args, ctx, count) => {
38
+ checkArity(name, args, count);
39
+ return args.map((arg) =>
40
+ inDocumentOrder(evaluator, toNodeSet(name, evaluator.evaluate(arg, ctx))),
41
+ );
42
+ };
43
+
44
+ /**
45
+ * `leading()` / `trailing()`: the nodes of the first node-set before or
46
+ * after the first node of the second one.
47
+ *
48
+ * @param {string} name - Function name, for errors
49
+ * @param {boolean} before - True for leading, false for trailing
50
+ * @returns {Function} The XPath function
51
+ */
52
+ const around = (name, before) => (args, ctx) => {
53
+ const [nodes, others] = nodeSets(name, args, ctx, 2);
54
+ if (others.length === 0) return nodes;
55
+
56
+ const index = nodes.indexOf(others[0]);
57
+ if (index === -1) return [];
58
+ return before ? nodes.slice(0, index) : nodes.slice(index + 1);
59
+ };
60
+
61
+ const key = (local) => expandedFunctionName(EXSLT_SETS, local);
62
+
63
+ return {
64
+ [key("difference")]: (args, ctx) => {
65
+ const [nodes, others] = nodeSets("set:difference", args, ctx, 2);
66
+ const excluded = new Set(others);
67
+ return nodes.filter((node) => !excluded.has(node));
68
+ },
69
+
70
+ [key("intersection")]: (args, ctx) => {
71
+ const [nodes, others] = nodeSets("set:intersection", args, ctx, 2);
72
+ const included = new Set(others);
73
+ return nodes.filter((node) => included.has(node));
74
+ },
75
+
76
+ [key("distinct")]: (args, ctx) => {
77
+ const [nodes] = nodeSets("set:distinct", args, ctx, 1);
78
+ const seen = new Set();
79
+ return nodes.filter((node) => {
80
+ const value = evaluator.getStringValue(node);
81
+ if (seen.has(value)) return false;
82
+ seen.add(value);
83
+ return true;
84
+ });
85
+ },
86
+
87
+ [key("has-same-node")]: (args, ctx) => {
88
+ const [nodes, others] = nodeSets("set:has-same-node", args, ctx, 2);
89
+ const included = new Set(others);
90
+ return nodes.some((node) => included.has(node));
91
+ },
92
+
93
+ [key("leading")]: around("set:leading", true),
94
+ [key("trailing")]: around("set:trailing", false),
95
+ };
96
+ }