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