@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,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsing of the XML Schema date/time lexical forms used by the EXSLT
|
|
3
|
+
* dates-and-times module, ported from libexslt `date.c` (`exsltDateParse`).
|
|
4
|
+
* Date values are described in `./calendar.js`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
import { DateType, createDate, isValidDate, isValidTime } from "./calendar.js";
|
|
10
|
+
|
|
11
|
+
/** Largest integer the parsers accept, the analogue of C `LONG_MAX`. */
|
|
12
|
+
const MAX_NUMBER = Number.MAX_SAFE_INTEGER;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A cursor over the text being parsed, mirroring libexslt's `cur` pointer.
|
|
16
|
+
*/
|
|
17
|
+
class Cursor {
|
|
18
|
+
/** @param {string} text - The text */
|
|
19
|
+
constructor(text) {
|
|
20
|
+
this.text = text;
|
|
21
|
+
this.at = 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @returns {string} The current character, "" at the end */
|
|
25
|
+
get char() {
|
|
26
|
+
return this.text.charAt(this.at);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** @returns {boolean} Whether the current character is a digit */
|
|
30
|
+
get isDigit() {
|
|
31
|
+
const char = this.char;
|
|
32
|
+
return char >= "0" && char <= "9";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read two digits (libexslt `PARSE_2_DIGITS`), advancing past them.
|
|
37
|
+
*
|
|
38
|
+
* @param {(value: number) => boolean} valid - Range check
|
|
39
|
+
* @returns {number|null} The value, null when invalid
|
|
40
|
+
*/
|
|
41
|
+
twoDigits(valid) {
|
|
42
|
+
const digits = this.text.substring(this.at, this.at + 2);
|
|
43
|
+
this.at += 2;
|
|
44
|
+
if (!/^\d\d$/.test(digits)) return null;
|
|
45
|
+
const value = Number(digits);
|
|
46
|
+
return valid(value) ? value : null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const inRange = (min, max) => (value) => value >= min && value <= max;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse a time zone ("", "Z" or "±hh:mm"); updates `dt` and the cursor only
|
|
54
|
+
* as far as libexslt does.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} dt - Date value
|
|
57
|
+
* @param {Cursor} cur - Cursor at the time zone
|
|
58
|
+
* @returns {boolean} True when a time zone was parsed
|
|
59
|
+
*/
|
|
60
|
+
function parseTimeZone(dt, cur) {
|
|
61
|
+
const sign = cur.char;
|
|
62
|
+
if (sign === "") {
|
|
63
|
+
dt.tzFlag = false;
|
|
64
|
+
dt.tzo = 0;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (sign === "Z") {
|
|
68
|
+
dt.tzFlag = true;
|
|
69
|
+
dt.tzo = 0;
|
|
70
|
+
cur.at++;
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (sign !== "+" && sign !== "-") return false;
|
|
74
|
+
|
|
75
|
+
const probe = new Cursor(cur.text);
|
|
76
|
+
probe.at = cur.at + 1;
|
|
77
|
+
const hours = probe.twoDigits(inRange(0, 23));
|
|
78
|
+
if (hours === null || probe.char !== ":") return false;
|
|
79
|
+
probe.at++;
|
|
80
|
+
dt.tzo = hours * 60;
|
|
81
|
+
const minutes = probe.twoDigits(inRange(0, 59));
|
|
82
|
+
if (minutes === null) return false;
|
|
83
|
+
dt.tzo = (dt.tzo + minutes) * (sign === "-" ? -1 : 1);
|
|
84
|
+
cur.at = probe.at;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parse "hh:mm:ss(.s+)?" into `dt` (libexslt `_exsltDateParseTime`).
|
|
90
|
+
*
|
|
91
|
+
* @param {object} dt - Date value
|
|
92
|
+
* @param {Cursor} cur - Cursor at the time
|
|
93
|
+
* @returns {boolean} True when a valid time was parsed
|
|
94
|
+
*/
|
|
95
|
+
function parseTime(dt, cur) {
|
|
96
|
+
const hour = cur.twoDigits(inRange(0, 23));
|
|
97
|
+
if (hour === null || cur.char !== ":") return false;
|
|
98
|
+
cur.at++;
|
|
99
|
+
dt.hour = hour;
|
|
100
|
+
const min = cur.twoDigits(inRange(0, 59));
|
|
101
|
+
if (min === null) return false;
|
|
102
|
+
dt.min = min;
|
|
103
|
+
if (cur.char !== ":") return false;
|
|
104
|
+
cur.at++;
|
|
105
|
+
|
|
106
|
+
const seconds = cur.twoDigits(() => true);
|
|
107
|
+
if (seconds === null) return false;
|
|
108
|
+
let sec = seconds;
|
|
109
|
+
if (cur.char === ".") {
|
|
110
|
+
cur.at++;
|
|
111
|
+
if (!cur.isDigit) return false;
|
|
112
|
+
let scale = 1;
|
|
113
|
+
while (cur.isDigit) {
|
|
114
|
+
scale /= 10;
|
|
115
|
+
sec += Number(cur.char) * scale;
|
|
116
|
+
cur.at++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
dt.sec = sec;
|
|
120
|
+
return isValidTime(dt);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parse a year of at least four digits (libexslt `_exsltDateParseGYear`).
|
|
125
|
+
*
|
|
126
|
+
* @param {object} dt - Date value
|
|
127
|
+
* @param {Cursor} cur - Cursor at the year
|
|
128
|
+
* @returns {boolean} True when a valid year was parsed
|
|
129
|
+
*/
|
|
130
|
+
function parseYear(dt, cur) {
|
|
131
|
+
const match = /^(-?)(\d+)/.exec(cur.text.substring(cur.at));
|
|
132
|
+
if (!match) return false;
|
|
133
|
+
const digits = match[2];
|
|
134
|
+
// At least four digits, and no leading zero beyond four
|
|
135
|
+
const badLength =
|
|
136
|
+
digits.length < 4 || (digits.length > 4 && digits[0] === "0");
|
|
137
|
+
if (badLength) return false;
|
|
138
|
+
const year = Number(digits);
|
|
139
|
+
if (year === 0 || year >= MAX_NUMBER / 10) return false;
|
|
140
|
+
dt.year = match[1] ? 1 - year : year;
|
|
141
|
+
cur.at += match[0].length;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Outcomes of {@link typedByTimeZone}. */
|
|
146
|
+
const Ending = Object.freeze({
|
|
147
|
+
TYPED: "typed",
|
|
148
|
+
NONE: "none",
|
|
149
|
+
INVALID: "invalid",
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Parse a time zone that must end the text (libexslt `RETURN_TYPE_IF_VALID`).
|
|
154
|
+
*
|
|
155
|
+
* @param {object} dt - Date value, typed on success
|
|
156
|
+
* @param {Cursor} cur - Cursor after the date fields
|
|
157
|
+
* @param {number} type - Type of the value on success
|
|
158
|
+
* @returns {string} TYPED when the rest is a time zone, INVALID when a time
|
|
159
|
+
* zone is followed by more text, NONE when parsing may go on
|
|
160
|
+
*/
|
|
161
|
+
function typedByTimeZone(dt, cur, type) {
|
|
162
|
+
if (!["", "Z", "+", "-"].includes(cur.char)) return Ending.NONE;
|
|
163
|
+
if (!parseTimeZone(dt, cur)) return Ending.NONE;
|
|
164
|
+
if (cur.at !== cur.text.length) return Ending.INVALID;
|
|
165
|
+
dt.type = type;
|
|
166
|
+
return Ending.TYPED;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Finish a form that must end with a time zone.
|
|
171
|
+
*
|
|
172
|
+
* @param {object} dt - Date value
|
|
173
|
+
* @param {Cursor} cur - Cursor after the date fields
|
|
174
|
+
* @param {number} type - Type of the value on success
|
|
175
|
+
* @returns {object|null} The typed value, null when invalid
|
|
176
|
+
*/
|
|
177
|
+
function endWithTimeZone(dt, cur, type) {
|
|
178
|
+
return typedByTimeZone(dt, cur, type) === Ending.TYPED ? dt : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Parse "--MM-DD", "--MM--" or "---DD", each followed by a time zone.
|
|
183
|
+
*
|
|
184
|
+
* @param {object} dt - Date value
|
|
185
|
+
* @param {Cursor} cur - Cursor after the leading "--"
|
|
186
|
+
* @returns {object|null} The date value, null when invalid
|
|
187
|
+
*/
|
|
188
|
+
function parseTruncatedDate(dt, cur) {
|
|
189
|
+
if (cur.char === "-") {
|
|
190
|
+
cur.at++;
|
|
191
|
+
dt.day = cur.twoDigits(inRange(1, 31));
|
|
192
|
+
return dt.day === null ? null : endWithTimeZone(dt, cur, DateType.GDAY);
|
|
193
|
+
}
|
|
194
|
+
dt.mon = cur.twoDigits(inRange(1, 12));
|
|
195
|
+
if (dt.mon === null || cur.char !== "-") return null;
|
|
196
|
+
cur.at++;
|
|
197
|
+
if (cur.char === "-") {
|
|
198
|
+
cur.at++;
|
|
199
|
+
return endWithTimeZone(dt, cur, DateType.GMONTH);
|
|
200
|
+
}
|
|
201
|
+
dt.day = cur.twoDigits(inRange(1, 31));
|
|
202
|
+
return dt.day === null ? null : endWithTimeZone(dt, cur, DateType.GMONTHDAY);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Fields after the year, each preceded by "-": the type a time zone after
|
|
207
|
+
* the previous field gives, and the parser of the field.
|
|
208
|
+
*/
|
|
209
|
+
const DATE_FIELDS = [
|
|
210
|
+
[
|
|
211
|
+
DateType.GYEAR,
|
|
212
|
+
(dt, cur) => {
|
|
213
|
+
dt.mon = cur.twoDigits(inRange(1, 12));
|
|
214
|
+
return dt.mon !== null;
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
[
|
|
218
|
+
DateType.GYEARMONTH,
|
|
219
|
+
(dt, cur) => {
|
|
220
|
+
dt.day = cur.twoDigits(inRange(1, 31));
|
|
221
|
+
return dt.day !== null && isValidDate(dt);
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Parse any date/time form libexslt accepts (`exsltDateParse`): xs:dateTime,
|
|
228
|
+
* xs:date, xs:time, xs:gYearMonth, xs:gYear, xs:gMonthDay, xs:gMonth
|
|
229
|
+
* ("--MM--") and xs:gDay.
|
|
230
|
+
*
|
|
231
|
+
* @param {string} text - The lexical form
|
|
232
|
+
* @returns {object|null} The date value, null when invalid
|
|
233
|
+
*/
|
|
234
|
+
export function parseDate(text) {
|
|
235
|
+
const dt = createDate(0);
|
|
236
|
+
const cur = new Cursor(text);
|
|
237
|
+
|
|
238
|
+
if (text.startsWith("--")) {
|
|
239
|
+
cur.at = 2;
|
|
240
|
+
return parseTruncatedDate(dt, cur);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (cur.isDigit && parseTime(dt, cur)) {
|
|
244
|
+
const ending = typedByTimeZone(dt, cur, DateType.TIME);
|
|
245
|
+
if (ending !== Ending.NONE) return ending === Ending.TYPED ? dt : null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
cur.at = 0;
|
|
249
|
+
if (!parseYear(dt, cur)) return null;
|
|
250
|
+
for (const [type, parseField] of DATE_FIELDS) {
|
|
251
|
+
const ending = typedByTimeZone(dt, cur, type);
|
|
252
|
+
if (ending !== Ending.NONE) return ending === Ending.TYPED ? dt : null;
|
|
253
|
+
if (cur.char !== "-") return null;
|
|
254
|
+
cur.at++;
|
|
255
|
+
if (!parseField(dt, cur)) return null;
|
|
256
|
+
}
|
|
257
|
+
const ending = typedByTimeZone(dt, cur, DateType.DATE);
|
|
258
|
+
if (ending !== Ending.NONE) return ending === Ending.TYPED ? dt : null;
|
|
259
|
+
if (cur.char !== "T") return null;
|
|
260
|
+
cur.at++;
|
|
261
|
+
if (!parseTime(dt, cur) || !parseTimeZone(dt, cur)) return null;
|
|
262
|
+
if (cur.at !== text.length || !isValidTime(dt)) return null;
|
|
263
|
+
dt.type = DateType.DATETIME;
|
|
264
|
+
return dt;
|
|
265
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EXSLT dates-and-times module (http://exslt.org/dates-and-times): the
|
|
3
|
+
* functions libexslt `date.c` implements. `format-date()`, `parse-date()`
|
|
4
|
+
* and the `date:date-format` element are not implemented by libexslt either.
|
|
5
|
+
*
|
|
6
|
+
* Invalid input yields the empty string for string results and NaN for
|
|
7
|
+
* numbers (and for `leap-year()`, as in libexslt). Without an argument the
|
|
8
|
+
* functions use the current local date and time, read from the clock.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
"use strict";
|
|
12
|
+
|
|
13
|
+
import { expandedFunctionName } from "../../xpath/evaluator.js";
|
|
14
|
+
import { EXSLT_DATES, checkArity, toNodeSet } from "./arguments.js";
|
|
15
|
+
import {
|
|
16
|
+
DateType,
|
|
17
|
+
SECS_PER_DAY,
|
|
18
|
+
createDate,
|
|
19
|
+
dayInYear,
|
|
20
|
+
isLeapYear,
|
|
21
|
+
} from "./calendar.js";
|
|
22
|
+
import { parseDate } from "./dateParse.js";
|
|
23
|
+
import {
|
|
24
|
+
addDuration,
|
|
25
|
+
dateDifference,
|
|
26
|
+
dayOfWeek,
|
|
27
|
+
hasYear,
|
|
28
|
+
secondsOf,
|
|
29
|
+
weekInMonth,
|
|
30
|
+
weekInYear,
|
|
31
|
+
} from "./dateCalc.js";
|
|
32
|
+
import {
|
|
33
|
+
formatByType,
|
|
34
|
+
formatDateOnly,
|
|
35
|
+
formatDateTime,
|
|
36
|
+
formatTimeOnly,
|
|
37
|
+
} from "./dateFormat.js";
|
|
38
|
+
import {
|
|
39
|
+
addDurations,
|
|
40
|
+
createDuration,
|
|
41
|
+
formatDuration,
|
|
42
|
+
parseDuration,
|
|
43
|
+
} from "./duration.js";
|
|
44
|
+
|
|
45
|
+
const MONTH_NAMES = [
|
|
46
|
+
"January",
|
|
47
|
+
"February",
|
|
48
|
+
"March",
|
|
49
|
+
"April",
|
|
50
|
+
"May",
|
|
51
|
+
"June",
|
|
52
|
+
"July",
|
|
53
|
+
"August",
|
|
54
|
+
"September",
|
|
55
|
+
"October",
|
|
56
|
+
"November",
|
|
57
|
+
"December",
|
|
58
|
+
];
|
|
59
|
+
const DAY_NAMES = [
|
|
60
|
+
"Sunday",
|
|
61
|
+
"Monday",
|
|
62
|
+
"Tuesday",
|
|
63
|
+
"Wednesday",
|
|
64
|
+
"Thursday",
|
|
65
|
+
"Friday",
|
|
66
|
+
"Saturday",
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const { DATETIME, DATE, TIME, GYEAR, GYEARMONTH, GMONTH, GMONTHDAY, GDAY } =
|
|
70
|
+
DateType;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The default clock: now, or `SOURCE_DATE_EPOCH` (seconds, read as UTC)
|
|
74
|
+
* when set, as libexslt does for reproducible builds.
|
|
75
|
+
*
|
|
76
|
+
* @returns {{getTime: () => number, getTimezoneOffset: () => number}} A Date-like instant
|
|
77
|
+
*/
|
|
78
|
+
export function systemClock() {
|
|
79
|
+
const epoch = Number.parseInt(
|
|
80
|
+
globalThis.process?.env?.SOURCE_DATE_EPOCH ?? "",
|
|
81
|
+
10,
|
|
82
|
+
);
|
|
83
|
+
if (Number.isNaN(epoch)) return new Date();
|
|
84
|
+
return { getTime: () => epoch * 1000, getTimezoneOffset: () => 0 };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The local date and time of an instant as a dateTime value, with the local
|
|
89
|
+
* offset but no explicit time zone (libexslt `exsltDateCurrent`).
|
|
90
|
+
*
|
|
91
|
+
* @param {{getTime: () => number, getTimezoneOffset: () => number}} now - A Date-like instant
|
|
92
|
+
* @returns {object} The date value
|
|
93
|
+
*/
|
|
94
|
+
export function currentDate(now) {
|
|
95
|
+
const tzo = -now.getTimezoneOffset();
|
|
96
|
+
const local = new Date(now.getTime() + tzo * 60000);
|
|
97
|
+
const dt = createDate(DATETIME);
|
|
98
|
+
dt.year = local.getUTCFullYear();
|
|
99
|
+
dt.mon = local.getUTCMonth() + 1;
|
|
100
|
+
dt.day = local.getUTCDate();
|
|
101
|
+
dt.hour = local.getUTCHours();
|
|
102
|
+
dt.min = local.getUTCMinutes();
|
|
103
|
+
dt.sec = local.getUTCSeconds();
|
|
104
|
+
dt.tzo = tzo;
|
|
105
|
+
return dt;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build the EXSLT dates-and-times functions.
|
|
110
|
+
*
|
|
111
|
+
* @param {import('../../xpath/evaluator.js').XPathEvaluator} evaluator - Evaluates the arguments
|
|
112
|
+
* @param {() => {getTime: () => number, getTimezoneOffset: () => number}} [clock] - Current instant
|
|
113
|
+
* @returns {Object<string, Function>} Functions keyed by expanded name
|
|
114
|
+
*/
|
|
115
|
+
export function createDatesFunctions(evaluator, clock = systemClock) {
|
|
116
|
+
const string = (arg, ctx) => evaluator.toString(evaluator.evaluate(arg, ctx));
|
|
117
|
+
const key = (local) => expandedFunctionName(EXSLT_DATES, local);
|
|
118
|
+
const functions = {};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Register a function of an optional date argument.
|
|
122
|
+
*
|
|
123
|
+
* @param {string} local - Local name
|
|
124
|
+
* @param {number[]} types - Types accepted from the argument
|
|
125
|
+
* @param {(dt: object) => *} compute - Result from the date value
|
|
126
|
+
* @param {*} invalid - Result for an invalid or unaccepted argument
|
|
127
|
+
*/
|
|
128
|
+
const define = (local, types, compute, invalid) => {
|
|
129
|
+
functions[key(local)] = (args, ctx) => {
|
|
130
|
+
checkArity(`date:${local}`, args, 0, 1);
|
|
131
|
+
const dt =
|
|
132
|
+
args.length === 0
|
|
133
|
+
? currentDate(clock())
|
|
134
|
+
: parseDate(string(args[0], ctx));
|
|
135
|
+
if (dt === null || !types.includes(dt.type)) return invalid;
|
|
136
|
+
return compute(dt) ?? invalid;
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const yearTypes = [DATETIME, DATE, GYEARMONTH, GYEAR];
|
|
141
|
+
const dayTypes = [DATETIME, DATE];
|
|
142
|
+
const timeTypes = [DATETIME, TIME];
|
|
143
|
+
const monthTypes = [DATETIME, DATE, GYEARMONTH, GMONTH, GMONTHDAY];
|
|
144
|
+
const weekday = (dt) =>
|
|
145
|
+
dayOfWeek(dayInYear(dt.day, dt.mon, dt.year), dt.year);
|
|
146
|
+
|
|
147
|
+
define("date", dayTypes, formatDateOnly, "");
|
|
148
|
+
define("time", timeTypes, formatTimeOnly, "");
|
|
149
|
+
define(
|
|
150
|
+
"year",
|
|
151
|
+
yearTypes,
|
|
152
|
+
(dt) => (dt.year <= 0 ? dt.year - 1 : dt.year),
|
|
153
|
+
Number.NaN,
|
|
154
|
+
);
|
|
155
|
+
define("leap-year", yearTypes, (dt) => isLeapYear(dt.year), Number.NaN);
|
|
156
|
+
define("month-in-year", monthTypes, (dt) => dt.mon, Number.NaN);
|
|
157
|
+
define("month-name", monthTypes, (dt) => MONTH_NAMES[dt.mon - 1], "");
|
|
158
|
+
define(
|
|
159
|
+
"month-abbreviation",
|
|
160
|
+
monthTypes,
|
|
161
|
+
(dt) => MONTH_NAMES[dt.mon - 1].substring(0, 3),
|
|
162
|
+
"",
|
|
163
|
+
);
|
|
164
|
+
define("week-in-year", dayTypes, weekInYear, Number.NaN);
|
|
165
|
+
define("week-in-month", dayTypes, weekInMonth, Number.NaN);
|
|
166
|
+
define(
|
|
167
|
+
"day-in-year",
|
|
168
|
+
dayTypes,
|
|
169
|
+
(dt) => dayInYear(dt.day, dt.mon, dt.year),
|
|
170
|
+
Number.NaN,
|
|
171
|
+
);
|
|
172
|
+
define(
|
|
173
|
+
"day-in-month",
|
|
174
|
+
[DATETIME, DATE, GMONTHDAY, GDAY],
|
|
175
|
+
(dt) => dt.day,
|
|
176
|
+
Number.NaN,
|
|
177
|
+
);
|
|
178
|
+
define(
|
|
179
|
+
"day-of-week-in-month",
|
|
180
|
+
dayTypes,
|
|
181
|
+
(dt) => Math.trunc((dt.day - 1) / 7) + 1,
|
|
182
|
+
Number.NaN,
|
|
183
|
+
);
|
|
184
|
+
define("day-in-week", dayTypes, (dt) => weekday(dt) + 1, Number.NaN);
|
|
185
|
+
define("day-name", dayTypes, (dt) => DAY_NAMES[weekday(dt)], "");
|
|
186
|
+
define(
|
|
187
|
+
"day-abbreviation",
|
|
188
|
+
dayTypes,
|
|
189
|
+
(dt) => DAY_NAMES[weekday(dt)].substring(0, 3),
|
|
190
|
+
"",
|
|
191
|
+
);
|
|
192
|
+
define("hour-in-day", timeTypes, (dt) => dt.hour, Number.NaN);
|
|
193
|
+
define("minute-in-hour", timeTypes, (dt) => dt.min, Number.NaN);
|
|
194
|
+
define("second-in-minute", timeTypes, (dt) => dt.sec, Number.NaN);
|
|
195
|
+
|
|
196
|
+
functions[key("date-time")] = (args) => {
|
|
197
|
+
checkArity("date:date-time", args, 0);
|
|
198
|
+
return formatDateTime(currentDate(clock())) ?? "";
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
functions[key("seconds")] = (args, ctx) => {
|
|
202
|
+
checkArity("date:seconds", args, 0, 1);
|
|
203
|
+
return secondsOf(
|
|
204
|
+
args.length === 0 ? currentDate(clock()) : string(args[0], ctx),
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
functions[key("duration")] = (args, ctx) => {
|
|
209
|
+
checkArity("date:duration", args, 0, 1);
|
|
210
|
+
const secs =
|
|
211
|
+
args.length === 0
|
|
212
|
+
? secondsOf(currentDate(clock()))
|
|
213
|
+
: evaluator.toNumber(string(args[0], ctx));
|
|
214
|
+
if (!Number.isFinite(secs)) return "";
|
|
215
|
+
const dur = createDuration();
|
|
216
|
+
dur.day = Math.floor(secs / SECS_PER_DAY);
|
|
217
|
+
dur.sec = secs - dur.day * SECS_PER_DAY;
|
|
218
|
+
return formatDuration(dur);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
functions[key("add")] = (args, ctx) => {
|
|
222
|
+
checkArity("date:add", args, 2);
|
|
223
|
+
const dt = parseDate(string(args[0], ctx));
|
|
224
|
+
const dur = parseDuration(string(args[1], ctx));
|
|
225
|
+
if (dt === null || !hasYear(dt) || dur === null) return "";
|
|
226
|
+
return formatByType(addDuration(dt, dur)) ?? "";
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
functions[key("add-duration")] = (args, ctx) => {
|
|
230
|
+
checkArity("date:add-duration", args, 2);
|
|
231
|
+
const x = parseDuration(string(args[0], ctx));
|
|
232
|
+
const y = parseDuration(string(args[1], ctx));
|
|
233
|
+
const sum = x === null || y === null ? null : addDurations(x, y);
|
|
234
|
+
return sum === null ? "" : formatDuration(sum);
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
functions[key("difference")] = (args, ctx) => {
|
|
238
|
+
checkArity("date:difference", args, 2);
|
|
239
|
+
const x = parseDate(string(args[0], ctx));
|
|
240
|
+
const y = parseDate(string(args[1], ctx));
|
|
241
|
+
const dur = x === null || y === null ? null : dateDifference(x, y, false);
|
|
242
|
+
return dur === null ? "" : formatDuration(dur);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
functions[key("sum")] = (args, ctx) => {
|
|
246
|
+
checkArity("date:sum", args, 1);
|
|
247
|
+
const nodes = toNodeSet("date:sum", evaluator.evaluate(args[0], ctx));
|
|
248
|
+
if (nodes.length === 0) return "";
|
|
249
|
+
let total = createDuration();
|
|
250
|
+
for (const node of nodes) {
|
|
251
|
+
const dur = parseDuration(evaluator.getStringValue(node));
|
|
252
|
+
total = dur === null ? null : addDurations(total, dur);
|
|
253
|
+
if (total === null) return "";
|
|
254
|
+
}
|
|
255
|
+
return formatDuration(total);
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return functions;
|
|
259
|
+
}
|