@controlium/utils 0.0.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.
@@ -0,0 +1,1135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Detokeniser = void 0;
4
+ const date_fns_1 = require("date-fns");
5
+ const date_fns_tz_1 = require("date-fns-tz");
6
+ const index_1 = require("../index");
7
+ // ─── PublicHolidays stub ─────────────────────────────────────────────────────
8
+ // TODO: Replace with a real import when a PublicHolidays module is provided by the consumer.
9
+ // The region string is an opaque identifier passed through by the caller (e.g. "us-east", "eu-london").
10
+ // eslint-disable-next-line @typescript-eslint/no-namespace
11
+ var PublicHolidays;
12
+ (function (PublicHolidays) {
13
+ function getIANAZone(_region) { throw new Error("PublicHolidays not yet implemented"); }
14
+ PublicHolidays.getIANAZone = getIANAZone;
15
+ async function isDatePublicHoliday(_date, _region) { throw new Error("PublicHolidays not yet implemented"); }
16
+ PublicHolidays.isDatePublicHoliday = isDatePublicHoliday;
17
+ async function getFirstPublicHolidayBetweenDates(_start, _end, _region) { throw new Error("PublicHolidays not yet implemented"); }
18
+ PublicHolidays.getFirstPublicHolidayBetweenDates = getFirstPublicHolidayBetweenDates;
19
+ })(PublicHolidays || (PublicHolidays = {}));
20
+ // ─── MockUtils stub ───────────────────────────────────────────────────────────
21
+ // TODO: Replace with real import when MockUtils is brought into the package
22
+ const MockUtils = {
23
+ interceptedRequests: undefined,
24
+ };
25
+ /**
26
+ * Processes strings containing tokens in the format `[[tokenType|expression|format]]`, replacing each
27
+ * token with its resolved value. Tokens can be nested — the innermost is always resolved first.
28
+ *
29
+ * ## Token syntax
30
+ * ```
31
+ * [[tokenType|expression|format]]
32
+ * ```
33
+ * - **tokenType** — identifies the built-in handler or is matched against registered callbacks
34
+ * - **expression** — what to compute (type-specific)
35
+ * - **format** — how to format the result (type-specific, often optional)
36
+ *
37
+ * The delimiter `|` and the `[[` / `]]` endstops are configurable via {@link Detokeniser.delimiter} and
38
+ * {@link Detokeniser.tokenStartEndChars}, but the defaults cover the vast majority of use cases.
39
+ *
40
+ * ---
41
+ * ## Built-in token types
42
+ *
43
+ * ### `random` — random data
44
+ * | Expression | Format | Example | Sample output |
45
+ * |---|---|---|---|
46
+ * | `digits` | count | `[[random\|digits\|6]]` | `482910` |
47
+ * | `letters` | count | `[[random\|letters\|4]]` | `xKpQ` |
48
+ * | `lowercaseletters` | count | `[[random\|lowercaseletters\|4]]` | `xkpq` |
49
+ * | `uppercaseletters` | count | `[[random\|uppercaseletters\|4]]` | `XKPQ` |
50
+ * | `alphanumerics` | count | `[[random\|alphanumerics\|8]]` | `a3Kx92Zp` |
51
+ * | `from(<chars>)` | count | `[[random\|from(aeiou)\|3]]` | `ioa` |
52
+ * | `float(min,max)` | decimal places | `[[random\|float(1.5,3.7)\|2]]` | `2.83` |
53
+ * | `date(fromEpoch,toEpoch)` | date format | `[[random\|date(0,1700000000000)\|yyyy-MM-dd]]` | `1994-07-12` |
54
+ *
55
+ * ### `date` / `date(<state>)` — date and time
56
+ * Format is a [date-fns format string](https://date-fns.org/docs/format), or the special values
57
+ * `epoch` (milliseconds since 1970-01-01) or `second-epoch`.
58
+ *
59
+ * | Expression | Example | Sample output |
60
+ * |---|---|---|
61
+ * | `today` / `now` | `[[date\|today\|dd-MM-yyyy]]` | `01-04-2026` |
62
+ * | `yesterday` | `[[date\|yesterday\|dd-MM-yyyy]]` | `31-03-2026` |
63
+ * | `tomorrow` | `[[date\|tomorrow\|dd-MM-yyyy]]` | `02-04-2026` |
64
+ * | `addYears(n)` | `[[date\|addYears(-1)\|yyyy]]` | `2025` |
65
+ * | `addMonths(n)` | `[[date\|addMonths(3)\|MMM yyyy]]` | `Jul 2026` |
66
+ * | `addDays(n)` | `[[date\|addDays(5)\|dd-MM-yyyy]]` | `06-04-2026` |
67
+ * | `addHours(n)` | `[[date\|addHours(2)\|HH:mm]]` | `14:30` |
68
+ * | `addMinutes(n)` | `[[date\|addMinutes(30)\|HH:mm]]` | `13:00` |
69
+ * | `random(fromEpoch,toEpoch)` | date format | `[[date\|random(0,1700000000000)\|yyyy-MM-dd]]` | random date |
70
+ * | `followingDay(epoch,dayName)` | date format | `[[date\|followingDay(1711929600000,wednesday)\|dd-MM-yyyy]]` | next Wednesday |
71
+ * | `yyyy-MM-dd` _(fixed date)_ | date format | `[[date\|2026-06-15\|EEEE]]` | `Monday` |
72
+ * | `timezoneOffset` _(with region)_ | _(none)_ | `[[date(us-east)\|timezoneOffset]]` | `-0500` |
73
+ *
74
+ * A region qualifier routes date formatting through the region's IANA timezone (resolved by the
75
+ * consumer-provided `PublicHolidays` module):
76
+ * ```
77
+ * [[date(eu-london)|today|HH:mm]] ← formatted in London time
78
+ * [[date(us-east)|addDays(1)|dd-MM-yyyy]]
79
+ * ```
80
+ *
81
+ * The following expressions are **async-only** — use {@link Detokeniser.doAsync} with a region qualifier:
82
+ * - `addWorkingDays(n)` — adds business days, skipping weekends and public holidays
83
+ * - `followingWorkingDay(epoch,dayName)` — as `followingDay` but skips weekends and public holidays
84
+ * - `nextPublicHoliday(fromEpoch,maxDays)` — next public holiday within a search window
85
+ *
86
+ * ```typescript
87
+ * await Detokeniser.doAsync('[[date(us-east)|addWorkingDays(5)|dd-MM-yyyy]]');
88
+ * await Detokeniser.doAsync('[[date(eu-london)|nextPublicHoliday([[date|today|epoch]],90)|dd-MM-yyyy]]');
89
+ * ```
90
+ *
91
+ * ### `setting` — retrieve a configured value
92
+ * Parameters are given as JSON key/value pairs after the delimiter. Resolution order:
93
+ * process env → npm config → context parameters → default value.
94
+ * ```
95
+ * [[setting|processEnvName: "MY_VAR"]]
96
+ * [[setting|processEnvName: "MY_VAR", defaultValue: "fallback"]]
97
+ * [[setting|npmPackageConfigName: "myconfig"]]
98
+ * [[setting|profileParameterName: "myParam", defaultValue: "none"]]
99
+ * ```
100
+ *
101
+ * ### `base64` — encode or decode
102
+ * ```
103
+ * [[base64|encode|Hello World]] → SGVsbG8gV29ybGQ=
104
+ * [[base64|decode|SGVsbG8gV29ybGQ=]] → Hello World
105
+ * ```
106
+ *
107
+ * ### `jwt` — generate a signed JWT
108
+ * ```
109
+ * [[jwt|{"sub":"1234","name":"Test"}|MySecret]]
110
+ * [[jwt|{"sub":"1234"}|MySecret|{"algorithm":"HS256"}]]
111
+ * ```
112
+ *
113
+ * ### `mockintercepts` — harvest a value from intercepted mock requests
114
+ * ```
115
+ * [[mockintercepts|$.requests[0].body.userId]]
116
+ * ```
117
+ *
118
+ * ---
119
+ * ## Nesting
120
+ * Tokens are resolved innermost-first, so nested tokens compose naturally:
121
+ * ```typescript
122
+ * // Date N random days from now, where N is itself a random digit
123
+ * Detokeniser.do('[[date|addDays([[random|digits|1]])|dd-MM-yyyy]]');
124
+ *
125
+ * // Next NSW public holiday from today (async — needs doAsync)
126
+ * await Detokeniser.doAsync('[[date(eu-london)|nextPublicHoliday([[date|today|epoch]],90)|dd-MM-yyyy]]');
127
+ * ```
128
+ *
129
+ * ---
130
+ * ## Escaping
131
+ * The escape character is `/` (configurable via `EscapeChar`). Escaping applies both inside and
132
+ * outside tokens. A double escape `//` produces a literal `/`.
133
+ *
134
+ * | Input | Output | Notes |
135
+ * |---|---|---|
136
+ * | `/[[` | `[[` | Literal `[[` — not treated as token start |
137
+ * | `/]]` | `]]` | Literal `]]` — not treated as token end |
138
+ * | `//` | `/` | Literal escape char |
139
+ * | `[[random\|from(xyz/[[)\|3]]` | 3 chars from `xyz[[` | `/[[` inside `from()` = `/[` (→`[`) + `[` = two `[` (higher weight) |
140
+ * | `[[random\|from(abc/))\|2]]` | 2 chars from `abc)` | `/)` inside `from()` = literal `)` |
141
+ *
142
+ * ---
143
+ * ## Extending with callbacks
144
+ * Custom token types are registered via {@link Detokeniser.addCallbackSync} (for sync processing) or
145
+ * {@link Detokeniser.addCallbackAsync} (for async processing). Callbacks are tried in registration
146
+ * order; return `undefined` to pass to the next callback. If all callbacks return `undefined` and no
147
+ * built-in handler matched, an error is thrown.
148
+ *
149
+ * @see {@link Detokeniser.addCallbackSync}
150
+ * @see {@link Detokeniser.addCallbackAsync}
151
+ * @see {@link Detokeniser.do}
152
+ * @see {@link Detokeniser.doAsync}
153
+ */
154
+ class Detokeniser {
155
+ /**
156
+ * Gets the current single-character delimiter used to separate token parts.
157
+ * Default: `|`
158
+ */
159
+ static get delimiter() {
160
+ return this._delimiter;
161
+ }
162
+ /**
163
+ * Sets the single-character delimiter used to separate token parts.
164
+ * The new value applies to all subsequent {@link Detokeniser.do} / {@link Detokeniser.doAsync} calls.
165
+ * @param newDelimiter - Exactly one character
166
+ * @throws If `newDelimiter` is not exactly one character
167
+ * @example
168
+ * Detokeniser.delimiter = ':';
169
+ * Detokeniser.do('[[random:digits:6]]'); // → e.g. '482910'
170
+ * Detokeniser.reset(); // restore default '|'
171
+ */
172
+ static set delimiter(newDelimiter) {
173
+ if (newDelimiter.length !== 1) {
174
+ const errTxt = `Invalid delimiter [${newDelimiter}]. Must be exactly 1 character!`;
175
+ index_1.Log.writeLine(index_1.LogLevels.Error, errTxt);
176
+ throw new Error(errTxt);
177
+ }
178
+ else {
179
+ this._delimiter = newDelimiter;
180
+ }
181
+ }
182
+ /**
183
+ * Sets the start and end token sequences.
184
+ * @param startEndChars - An even-length string whose first half is the start sequence and second half the end sequence.
185
+ * @example Detokeniser.tokenStartEndChars = "[[]]"; // start = "[[", end = "]]"
186
+ * @remarks Start and end sequences must differ. Minimum total length is 2 (one char each).
187
+ */
188
+ static set tokenStartEndChars(startEndChars) {
189
+ if (startEndChars.length < 2 || startEndChars.length % 2 !== 0) {
190
+ const errTxt = `Invalid start/end chars [${startEndChars}]. Must be an even number of characters (minimum 2) — first half as start sequence, second half as end sequence!`;
191
+ index_1.Log.writeLine(index_1.LogLevels.Error, errTxt);
192
+ throw new Error(errTxt);
193
+ }
194
+ const half = startEndChars.length / 2;
195
+ const start = startEndChars.substring(0, half);
196
+ const end = startEndChars.substring(half);
197
+ if (start === end) {
198
+ const errTxt = `Invalid start/end chars — start sequence [${start}] must differ from end sequence [${end}]!`;
199
+ index_1.Log.writeLine(index_1.LogLevels.Error, errTxt);
200
+ throw new Error(errTxt);
201
+ }
202
+ this._startTokenChar = start;
203
+ this._endTokenChar = end;
204
+ }
205
+ /**
206
+ * Gets the current token start/end sequences concatenated (e.g. `"[[]]"`).
207
+ */
208
+ static get tokenStartEndChars() {
209
+ return this._startTokenChar + this._endTokenChar;
210
+ }
211
+ /**
212
+ * Resets the Detokeniser to factory defaults:
213
+ * - Token endstops restored to `[[` / `]]`
214
+ * - Delimiter restored to `|`
215
+ * - Escape char restored to `/`
216
+ * - All registered sync and async callbacks cleared
217
+ *
218
+ * Call this in test teardown to guarantee a clean state between scenarios.
219
+ * @example
220
+ * afterEach(() => Detokeniser.reset());
221
+ */
222
+ static reset() {
223
+ this._endTokenChar = "]]";
224
+ this._startTokenChar = "[[";
225
+ this.EscapeChar = "/";
226
+ this._delimiter = "|";
227
+ if (this._syncCallbacks) {
228
+ this._syncCallbacks = [];
229
+ }
230
+ if (this._asyncCallbacks) {
231
+ this._asyncCallbacks = [];
232
+ }
233
+ this._asyncCallbacks = undefined;
234
+ this._syncCallbacks = undefined;
235
+ }
236
+ /**
237
+ * Registers a synchronous custom token handler.
238
+ *
239
+ * When {@link Detokeniser.do} encounters a token not handled by the built-in set, it invokes each
240
+ * registered sync callback in registration order. The first to return a non-`undefined` string wins.
241
+ * Return `undefined` to pass to the next callback. If all callbacks return `undefined`, an error is thrown.
242
+ *
243
+ * @param callback - `(delimiter: string, token: string) => string | undefined`
244
+ * - `delimiter` — current delimiter character (default `|`)
245
+ * - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"`
246
+ *
247
+ * @example
248
+ * // Handle [[env|VAR_NAME]] tokens
249
+ * Detokeniser.addCallbackSync((delimiter, token) => {
250
+ * const [type, name] = token.split(delimiter);
251
+ * if (type.toLowerCase() !== 'env') return undefined;
252
+ * return process.env[name] ?? '';
253
+ * });
254
+ * Detokeniser.do('Path: [[env|HOME]]'); // → 'Path: /home/user'
255
+ *
256
+ * @example
257
+ * // Multiple callbacks — each handles one type, passes on the rest
258
+ * Detokeniser.addCallbackSync((delimiter, token) => {
259
+ * const [type, value] = token.split(delimiter);
260
+ * if (type === 'upper') return value.toUpperCase();
261
+ * return undefined;
262
+ * });
263
+ * Detokeniser.addCallbackSync((delimiter, token) => {
264
+ * const [type, value] = token.split(delimiter);
265
+ * if (type === 'lower') return value.toLowerCase();
266
+ * return undefined;
267
+ * });
268
+ * Detokeniser.do('[[upper|hello]] [[lower|WORLD]]'); // → 'HELLO world'
269
+ *
270
+ * @see {@link Detokeniser.resetSyncCallbacks} to remove all sync callbacks
271
+ * @see {@link Detokeniser.addCallbackAsync} for async token handlers
272
+ */
273
+ static addCallbackSync(callback) {
274
+ if (!this._syncCallbacks) {
275
+ this._syncCallbacks = new Array();
276
+ }
277
+ this._syncCallbacks.push(callback);
278
+ }
279
+ /**
280
+ * Registers an asynchronous custom token handler.
281
+ *
282
+ * Works identically to {@link Detokeniser.addCallbackSync} but is invoked by {@link Detokeniser.doAsync}.
283
+ * Async callbacks are tried in registration order; the first to return a non-`undefined` value wins.
284
+ * Return `undefined` to pass to the next callback.
285
+ *
286
+ * @param asyncCallback - `(delimiter: string, token: string) => Promise<string | undefined>`
287
+ * - `delimiter` — current delimiter character (default `|`)
288
+ * - `token` — full token body without `[[` / `]]`, e.g. `"mytype|arg1|arg2"`
289
+ *
290
+ * @example
291
+ * // Handle [[db|table|column|whereClause]] tokens
292
+ * Detokeniser.addCallbackAsync(async (delimiter, token) => {
293
+ * const [type, table, column, where] = token.split(delimiter);
294
+ * if (type.toLowerCase() !== 'db') return undefined;
295
+ * const row = await db.query(`SELECT ${column} FROM ${table} WHERE ${where} LIMIT 1`);
296
+ * return String(row[column]);
297
+ * });
298
+ * const result = await Detokeniser.doAsync('ID: [[db|users|id|active=1]]');
299
+ *
300
+ * @example
301
+ * // Combine with nested tokens — inner tokens resolve before the callback is called
302
+ * Detokeniser.addCallbackAsync(async (delimiter, token) => {
303
+ * const [type, key] = token.split(delimiter);
304
+ * if (type !== 'cache') return undefined;
305
+ * return await redis.get(key);
306
+ * });
307
+ * // [[random|digits|8]] resolves first, then [[cache|...]] receives the result
308
+ * await Detokeniser.doAsync('Val: [[cache|prefix-[[random|digits|8]]]]');
309
+ *
310
+ * @see {@link Detokeniser.resetAsyncCallbacks} to remove all async callbacks
311
+ * @see {@link Detokeniser.addCallbackSync} for synchronous token handlers
312
+ */
313
+ static addCallbackAsync(asyncCallback) {
314
+ if (!this._asyncCallbacks) {
315
+ this._asyncCallbacks = new Array();
316
+ }
317
+ this._asyncCallbacks?.push(asyncCallback);
318
+ }
319
+ /**
320
+ * Removes all registered sync callbacks. Built-in token handlers are unaffected.
321
+ * Use between tests or scenarios to ensure callback isolation.
322
+ * @see {@link Detokeniser.reset} to also clear async callbacks and restore all defaults
323
+ */
324
+ static resetSyncCallbacks() {
325
+ if (this._syncCallbacks) {
326
+ this._syncCallbacks = [];
327
+ }
328
+ }
329
+ /**
330
+ * Removes all registered async callbacks. Built-in token handlers are unaffected.
331
+ * Use between tests or scenarios to ensure callback isolation.
332
+ * @see {@link Detokeniser.reset} to also clear sync callbacks and restore all defaults
333
+ */
334
+ static resetAsyncCallbacks() {
335
+ if (this._asyncCallbacks) {
336
+ this._asyncCallbacks = [];
337
+ }
338
+ }
339
+ /**
340
+ * Synchronously resolves all tokens in the given string and returns the result.
341
+ *
342
+ * Tokens are resolved innermost-first. Registered sync callbacks are invoked for token types not
343
+ * handled by the built-in set. Use {@link Detokeniser.doAsync} if you need async callbacks or any
344
+ * of the async-only date expressions (`addWorkingDays`, `followingWorkingDay`, `nextPublicHoliday`).
345
+ *
346
+ * @param tokenisedString - String potentially containing `[[...]]` tokens
347
+ * @param options - Optional processing options
348
+ * @returns The input string with all tokens replaced by their resolved values
349
+ * @throws If any token is malformed, unsupported, or a callback throws
350
+ *
351
+ * @example
352
+ * Detokeniser.do('Ref-[[random|digits|6]]'); // → e.g. 'Ref-482910'
353
+ * Detokeniser.do('Expires [[date|addDays(30)|dd/MM/yyyy]]'); // → e.g. 'Expires 01/05/2026'
354
+ * Detokeniser.do('[[random|uppercaseletters|3]]-[[random|digits|4]]'); // → e.g. 'XKP-7391'
355
+ *
356
+ * @example
357
+ * // Nested tokens — innermost resolved first
358
+ * Detokeniser.do('[[date|addDays([[random|digits|1]])|dd-MM-yyyy]]');
359
+ *
360
+ * @example
361
+ * // Context parameters for [[setting|...]] tokens
362
+ * Detokeniser.do('Hello [[setting|profileParameterName: "username"]]', {
363
+ * contextParameters: { username: 'Alice' }
364
+ * }); // → 'Hello Alice'
365
+ *
366
+ * @example
367
+ * // Escaping — produce literal [[ / ]] in output
368
+ * Detokeniser.do('Press /[[Enter/]] to continue'); // → 'Press [[Enter]] to continue'
369
+ */
370
+ static do(tokenisedString, options = {}) {
371
+ let deEscape = true;
372
+ try {
373
+ const runningString = this.doPreamble(tokenisedString);
374
+ // Loop until last token find found no tokens
375
+ while (runningString.currentToken.hasToken) {
376
+ // Process the last found token, prepend it to the text after the last found token then find any token in the resulting string
377
+ runningString.currentToken = new InnermostToken(this.doToken(runningString.currentToken.childToken.substring(this._startTokenChar.length), options) + runningString.currentToken.postamble, this._startTokenChar, this._endTokenChar, this.EscapeChar);
378
+ // Concatinate the last found tokens preable (or full text if none found) to the built string and recursivley call self to ensure full token resolution
379
+ runningString.outputString = this.do(runningString.outputString + runningString.currentToken.preamble);
380
+ deEscape = false;
381
+ }
382
+ //
383
+ // Okay, so there is a bug here. But it shall remain unfixed as it requires time.
384
+ //
385
+ // The bug is that is the resolved token string contains a sequence of characters that makes doDeEscapesIfRequired think it sees an escaped special char
386
+ // the it will de-escape it!! However, probablity of incidence is currently low. When/If it does happen (impact could be high!) it can then be fixed.
387
+ //
388
+ // IE.
389
+ // const encoded = Buffer.from('this/>escaped').toString('base64');
390
+ // const decoded = Detokeniser.do(`<base64;decode;${encoded}>`);
391
+ //
392
+ // decoded would now be "this>escaped". Should be "this/>escaped".
393
+ //
394
+ runningString.outputString = this.doDeEscapesIfRequired(tokenisedString, deEscape, runningString.outputString);
395
+ return runningString.outputString;
396
+ }
397
+ catch (err) {
398
+ const errText = `Error processing [${tokenisedString}]: ${typeof err === "string" ? err : err instanceof Error ? err.message : "<unknown>"}`;
399
+ index_1.Log.writeLine(index_1.LogLevels.Error, errText);
400
+ throw Error(errText);
401
+ }
402
+ }
403
+ /**
404
+ * Asynchronously resolves all tokens in the given string and returns a Promise of the result.
405
+ *
406
+ * Functionally equivalent to {@link Detokeniser.do} but additionally supports:
407
+ * - Async callbacks registered via {@link Detokeniser.addCallbackAsync}
408
+ * - Async-only date expressions: `addWorkingDays`, `followingWorkingDay`, `nextPublicHoliday`
409
+ *
410
+ * Note: sync callbacks registered via {@link Detokeniser.addCallbackSync} are **not** invoked
411
+ * during async processing — re-register them with {@link Detokeniser.addCallbackAsync} if needed.
412
+ *
413
+ * @param tokenisedString - String potentially containing `[[...]]` tokens
414
+ * @returns Promise resolving to the input string with all tokens replaced
415
+ * @throws If any token is malformed, unsupported, or a callback throws
416
+ *
417
+ * @example
418
+ * // Async-only date expressions require doAsync and a region qualifier
419
+ * await Detokeniser.doAsync('[[date(us-east)|addWorkingDays(5)|dd-MM-yyyy]]');
420
+ * await Detokeniser.doAsync('[[date(eu-london)|nextPublicHoliday([[date|today|epoch]],90)|dd-MM-yyyy]]');
421
+ *
422
+ * @example
423
+ * // Async callback for database-driven tokens
424
+ * Detokeniser.addCallbackAsync(async (delim, token) => {
425
+ * const [type, key] = token.split(delim);
426
+ * if (type !== 'db') return undefined;
427
+ * return await fetchFromDatabase(key);
428
+ * });
429
+ * await Detokeniser.doAsync('User: [[db|users.name.first]]');
430
+ */
431
+ static async doAsync(tokenisedString) {
432
+ let deEscape = true;
433
+ try {
434
+ const runningString = this.doPreamble(tokenisedString);
435
+ // Loop until last token find found no tokens
436
+ while (runningString.currentToken.hasToken) {
437
+ // Process the last found token, prepend it to the text after the last found token then find any token in the resulting string
438
+ runningString.currentToken = new InnermostToken((await this.asyncDoToken(runningString.currentToken.childToken.substring(this._startTokenChar.length))) +
439
+ runningString.currentToken.postamble, this._startTokenChar, this._endTokenChar, this.EscapeChar);
440
+ const logToken = runningString.currentToken.hasToken ? runningString.currentToken.childToken : '<No Token>';
441
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `Preamble:[${runningString.currentToken.preamble}] Token:[${logToken}] Postamble:[${runningString.currentToken.postamble}]`);
442
+ // Concatinate the last found tokens preable (or full text if none found) to the built string and recursivley call self to ensure full token resolution
443
+ runningString.outputString = await this.doAsync(runningString.outputString + runningString.currentToken.preamble);
444
+ deEscape = false;
445
+ }
446
+ runningString.outputString = this.doDeEscapesIfRequired(tokenisedString, deEscape, runningString.outputString);
447
+ return runningString.outputString;
448
+ }
449
+ catch (err) {
450
+ const errText = `Error processing [${tokenisedString}]: ${typeof err === "string" ? err : err instanceof Error ? err.message : "<unknown>"}`;
451
+ index_1.Log.writeLine(index_1.LogLevels.Error, errText);
452
+ throw Error(errText);
453
+ }
454
+ }
455
+ static doDeEscapesIfRequired(tokenisedString, deEscape, stringToProcess) {
456
+ // After all tokens are resolved, remove escaping from any chars that were escaped to prevent token recognition.
457
+ // Uses a 3-pass approach to correctly handle escaped escape chars (e.g. //) adjacent to other escapes (e.g. /[):
458
+ // Pass 1: replace // with \x00 placeholder
459
+ // Pass 2: de-escape each char that makes up the start/end token sequences
460
+ // Pass 3: restore \x00 back to the escape char
461
+ let processedString = stringToProcess;
462
+ if (deEscape && !index_1.StringUtils.isBlank(processedString)) {
463
+ const doubleEscapes = this.EscapeChar + this.EscapeChar;
464
+ processedString = index_1.StringUtils.replaceAll(processedString, doubleEscapes, "\x00");
465
+ const tokenChars = new Set([...this._startTokenChar, ...this._endTokenChar]);
466
+ for (const char of tokenChars) {
467
+ processedString = index_1.StringUtils.replaceAll(processedString, this.EscapeChar + char, char);
468
+ }
469
+ processedString = index_1.StringUtils.replaceAll(processedString, "\x00", this.EscapeChar);
470
+ }
471
+ if (tokenisedString !== processedString) {
472
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `Processed [${tokenisedString}]\nto [${processedString}]`);
473
+ }
474
+ return processedString;
475
+ }
476
+ static doPreamble(tokenisedString) {
477
+ // Drill down to the left-most deepest (if nested) token
478
+ const token = new InnermostToken(tokenisedString, this._startTokenChar, this._endTokenChar, this.EscapeChar);
479
+ // Get all text to the left of any token found in the string passed in. If no token was found Preamble will contain all text from passed string
480
+ const outputString = token.preamble;
481
+ return { currentToken: token, outputString };
482
+ }
483
+ static doTokenMain(token, options = {}) {
484
+ const doTokenReturn = {};
485
+ doTokenReturn.processedToken = undefined;
486
+ if (!token || token === "") {
487
+ throw new Error("Empty token! Token must be populated.");
488
+ }
489
+ const [tokenName, postAmble] = index_1.StringUtils.splitRemaining(token, this._delimiter, 2);
490
+ const loweredTokenName = tokenName.toLowerCase().trim();
491
+ const postAmbleDeEscaped = this.doDeEscapesIfRequired(postAmble, true, postAmble);
492
+ if (typeof doTokenReturn.processedToken === "undefined") {
493
+ switch (loweredTokenName) {
494
+ case "random":
495
+ if (index_1.StringUtils.isBlank(postAmbleDeEscaped))
496
+ throw new Error(`Random token [${token}] needs at least 2 parts (IE. {{random;type[;<length>]}} etc.)`);
497
+ doTokenReturn.processedToken = this.doRandomToken(postAmbleDeEscaped);
498
+ break;
499
+ case "setting":
500
+ {
501
+ if (index_1.StringUtils.isBlank(postAmbleDeEscaped))
502
+ throw new Error(`Setting token [${token}] needs at least 2 parts (IE. {{setting;processEnvName: "TEST_LOG_TO_CONSOLE")`);
503
+ doTokenReturn.processedToken = this.doSettingToken(postAmbleDeEscaped, options);
504
+ break;
505
+ }
506
+ case "mockintercepts":
507
+ if (index_1.StringUtils.isBlank(postAmbleDeEscaped))
508
+ throw new Error(`Request token [${token}] needs 2 parts {{mockintercepts;JSONPath}}`);
509
+ doTokenReturn.processedToken = this.doMockRequests(postAmbleDeEscaped);
510
+ break;
511
+ case "jwt":
512
+ if (index_1.StringUtils.isBlank(postAmbleDeEscaped))
513
+ throw new Error(`JWT token [${token}] needs at least 2 parts (IE. {{jwt;payload[;signature[;options]]}} etc.)`);
514
+ doTokenReturn.processedToken = this.doJWTToken(postAmbleDeEscaped);
515
+ break;
516
+ case "base64":
517
+ // aha, we actuall need three parts <base64;encode|decode;<string>>
518
+ if (!index_1.StringUtils.isBlank(postAmbleDeEscaped)) {
519
+ const [direction, value] = index_1.StringUtils.splitRemaining(postAmbleDeEscaped, this._delimiter, 2);
520
+ if (direction == "encode" || direction == "decode") {
521
+ doTokenReturn.processedToken = this.doBase64(value, direction);
522
+ break;
523
+ }
524
+ }
525
+ throw new Error(`BASE64 token [${token}] needs 3 parts (IE. {{base64;encode|decode;value}})`);
526
+ default:
527
+ if (loweredTokenName.startsWith("date")) {
528
+ doTokenReturn.tokenBodyIfDateToken = postAmbleDeEscaped;
529
+ if (loweredTokenName[4] === "(" && loweredTokenName.endsWith(")")) {
530
+ if (index_1.StringUtils.isBlank(postAmbleDeEscaped)) {
531
+ throw new Error(`Date token [${token}] needs 2 or 3 parts {{date(<state>);<offset>;<format>}} or {{date;timezone}}`);
532
+ }
533
+ else {
534
+ const state = index_1.StringUtils.trimChar(index_1.StringUtils.splitRemaining(tokenName, "(", 2)[1], ")");
535
+ const body = postAmbleDeEscaped;
536
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `Calling doDateToken. State = [${state}]. Body = [${body}]`);
537
+ doTokenReturn.state = state;
538
+ }
539
+ }
540
+ }
541
+ }
542
+ }
543
+ return doTokenReturn;
544
+ }
545
+ static doToken(token, options = {}) {
546
+ const doTokenReturn = this.doTokenMain(token, options);
547
+ let processedToken;
548
+ if (index_1.Utils.isNullOrUndefined(doTokenReturn.processedToken) && doTokenReturn.tokenBodyIfDateToken) {
549
+ processedToken = this.doDateToken(doTokenReturn.state, doTokenReturn.tokenBodyIfDateToken);
550
+ }
551
+ else {
552
+ processedToken = doTokenReturn.processedToken;
553
+ }
554
+ //
555
+ // If token still not been processed itterate through the callbacks, breaking as soon as it is processed
556
+ //
557
+ try {
558
+ if (index_1.Utils.isNullOrUndefined(processedToken)) {
559
+ if (typeof this._syncCallbacks != "undefined") {
560
+ this._syncCallbacks.every((callback) => {
561
+ processedToken = callback(this._delimiter, token);
562
+ return index_1.Utils.isNullOrUndefined(processedToken);
563
+ });
564
+ }
565
+ }
566
+ }
567
+ catch (e) {
568
+ throw new Error(`Error processing callback for [${token}]: ${e?.message ?? "<Unknown Error>"}}`);
569
+ }
570
+ if (index_1.Utils.isNullOrUndefined(processedToken)) {
571
+ throw new Error(`Unsupported token [${index_1.StringUtils.splitRemaining(token, this._delimiter, 2)[0]}]`);
572
+ }
573
+ else {
574
+ return processedToken;
575
+ }
576
+ }
577
+ static async asyncDoToken(token, options = {}) {
578
+ const doTokenReturn = this.doTokenMain(token, options);
579
+ let processedToken;
580
+ if (index_1.Utils.isNullOrUndefined(doTokenReturn.processedToken) && doTokenReturn.tokenBodyIfDateToken) {
581
+ processedToken = await this.asyncDoDateToken(doTokenReturn.state, doTokenReturn.tokenBodyIfDateToken);
582
+ }
583
+ else {
584
+ processedToken = doTokenReturn.processedToken;
585
+ }
586
+ //
587
+ // If token still not been processed itterate through the callbacks, breaking as soon as it is processed. Dirty
588
+ // ucky loop. But it works.... If you know a sexier way of doing it please change!!
589
+ //
590
+ try {
591
+ if (index_1.Utils.isNullOrUndefined(processedToken)) {
592
+ if (typeof this._asyncCallbacks != "undefined") {
593
+ for (let i = 0; i < this._asyncCallbacks.length; i++) {
594
+ processedToken = await this._asyncCallbacks[i](this._delimiter, token);
595
+ if (!index_1.Utils.isNullOrUndefined(processedToken))
596
+ break;
597
+ }
598
+ }
599
+ }
600
+ }
601
+ catch (e) {
602
+ index_1.Log.writeLine(index_1.LogLevels.Error, `Error processing async callback (ignoring and setting response to undefined):${e?.message ?? "<Unknown Error>"}}`);
603
+ processedToken = undefined;
604
+ }
605
+ if (index_1.Utils.isNullOrUndefined(processedToken)) {
606
+ throw new Error(`Unsupported token [${index_1.StringUtils.splitRemaining(token, this._delimiter, 2)[0]}]`);
607
+ }
608
+ else {
609
+ return processedToken;
610
+ }
611
+ }
612
+ static doSettingToken(tokenBody, options = {}) {
613
+ const bodyJSON = `{${tokenBody}}`;
614
+ if (!index_1.JsonUtils.isJson(bodyJSON, true)) {
615
+ const errText = `Token 'Setting'.\nExpected: parameters in name: value format (IE. ${this._startTokenChar}setting;processEnvName: "MY_SETTING"${this._endTokenChar}\nToken was: ${this._startTokenChar}setting;${tokenBody}${this._endTokenChar}\n\nValid Setting parameters include;\n processEnvName - Name of process env variable\n npmPackageConfigName - Name of NPM config var\n profileParameterName - Name of Cucumber profile parameter\n defaultValue - default value if cannot be found`;
616
+ index_1.Log.writeLine(index_1.LogLevels.Error, errText);
617
+ throw new Error(errText);
618
+ }
619
+ const result = index_1.Utils.getSetting(index_1.LogLevels.TestInformation, "From Detokeniser", index_1.JsonUtils.parse(bodyJSON, true), options.contextParameters);
620
+ if (index_1.Utils.isNullOrUndefined(result)) {
621
+ return result;
622
+ }
623
+ else {
624
+ return typeof result === 'object' ? JSON.stringify(result) : String(result);
625
+ }
626
+ }
627
+ /**
628
+ * Base64-encodes or decodes a string.
629
+ *
630
+ * This is the underlying handler for `[[base64|encode|...]]` and `[[base64|decode|...]]` tokens
631
+ * but is also exposed for direct use.
632
+ *
633
+ * @param original - The string to encode or decode
634
+ * @param direction - `"encode"` to base64-encode; `"decode"` to base64-decode
635
+ * @returns The encoded or decoded string
636
+ * @throws If the conversion fails (e.g. invalid base64 input for decode)
637
+ *
638
+ * @example
639
+ * Detokeniser.doBase64('Hello World', 'encode'); // → 'SGVsbG8gV29ybGQ='
640
+ * Detokeniser.doBase64('SGVsbG8gV29ybGQ=', 'decode'); // → 'Hello World'
641
+ */
642
+ static doBase64(original, direction) {
643
+ try {
644
+ return direction == "encode" ? Buffer.from(original).toString("base64") : Buffer.from(original, "base64").toString();
645
+ }
646
+ catch (err) {
647
+ const errText = `Converting:\n[${original}]\n ${direction == "encode" ? "to" : "from"} base64:\n {${err.message}}`;
648
+ index_1.Log.writeLine(index_1.LogLevels.Error, errText);
649
+ throw new Error(errText);
650
+ }
651
+ }
652
+ //
653
+ // So, this has been created quickly with little/no checking/testing. Lots and lots of error handling needs adding or testers will get
654
+ // errors/fails they have no idea how to fix!!!!
655
+ //
656
+ static doJWTToken(tokenBody) {
657
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `JWT token [${tokenBody}]`);
658
+ const typeAndLengthOrFormat = index_1.StringUtils.splitRemaining(tokenBody, this._delimiter, 3);
659
+ if (typeAndLengthOrFormat.length == 3) {
660
+ return index_1.Utils.createJWT(typeAndLengthOrFormat[0], typeAndLengthOrFormat[1], typeAndLengthOrFormat[2]);
661
+ }
662
+ else if (typeAndLengthOrFormat.length == 2) {
663
+ return index_1.Utils.createJWT(typeAndLengthOrFormat[0], typeAndLengthOrFormat[1]);
664
+ }
665
+ else {
666
+ return index_1.Utils.createJWT(typeAndLengthOrFormat[0], "DummySignature");
667
+ }
668
+ }
669
+ static doMockRequests(jsonPath) {
670
+ //
671
+ // NOTE. This is MockUtils only at the moment. When playwright is brought in to the library this MUST be made generic so user can harvest from Mock intercepts,
672
+ // whether MSW OR Playwright.
673
+ //
674
+ if (!MockUtils.interceptedRequests || MockUtils.interceptedRequests.length <= 0) {
675
+ const errMessage = "No Mock Intercepted requests to harvest from!?";
676
+ index_1.Log.writeLine(index_1.LogLevels.Error, errMessage);
677
+ throw new Error(errMessage);
678
+ }
679
+ const jsonProperties = index_1.JsonUtils.getPropertiesMatchingPath(MockUtils.interceptedRequests, jsonPath);
680
+ if (jsonProperties.length != 1) {
681
+ throw new Error(`Expected path (${jsonPath}) to match exactly one JSON node. However, path matched ${jsonProperties.length} nodes!`);
682
+ }
683
+ else {
684
+ return jsonProperties[0].value;
685
+ }
686
+ }
687
+ /**
688
+ * Extracts the content inside the first set of parentheses in `input`, respecting the escape char.
689
+ * An escaped `)` (i.e. `/)`) is treated as a literal `)` and does not end the content.
690
+ * @example parseParenContent("from(abc/))") → "abc)"
691
+ * @example parseParenContent("from(xyz/[[)") → "xyz[[" (with default escape char `/`)
692
+ */
693
+ static parseParenContent(input) {
694
+ const openParen = input.indexOf("(");
695
+ if (openParen === -1)
696
+ return "";
697
+ let result = "";
698
+ let i = openParen + 1;
699
+ while (i < input.length) {
700
+ if (input[i] === this.EscapeChar && i + 1 < input.length) {
701
+ result += input[i + 1];
702
+ i += 2;
703
+ }
704
+ else if (input[i] === ")") {
705
+ break;
706
+ }
707
+ else {
708
+ result += input[i];
709
+ i++;
710
+ }
711
+ }
712
+ return result;
713
+ }
714
+ static doRandomToken(tokenBody) {
715
+ const typeAndLengthOrFormat = index_1.StringUtils.splitRemaining(tokenBody, this._delimiter, 2);
716
+ let result = "";
717
+ let select = "";
718
+ const verb = typeAndLengthOrFormat[0].toLowerCase().trim();
719
+ if (verb.startsWith("date(")) {
720
+ const randomDate = this.doRandomDate(verb.substring(verb.indexOf("(") + 1, verb.indexOf(")")));
721
+ result = typeAndLengthOrFormat[1].toLowerCase() === "epoch" ? "" + randomDate : (0, date_fns_1.format)(randomDate, typeAndLengthOrFormat[1]);
722
+ }
723
+ else if (verb.startsWith("float(")) {
724
+ // ToDo: Current format is only for number of decimals. However in future could be precision,decimals if needed (IE. {random,float(4000,8000),3,4} precision 3 decimals 4...), defaulting
725
+ // to decimal places if only a single number given (then change would be non-breaking)....
726
+ if (isNaN(parseInt(typeAndLengthOrFormat[1])))
727
+ throw `Invalid Float format. Expect {{random.float(min;max),<number of decimals>}}. Format was: [${typeAndLengthOrFormat[1]}]`;
728
+ const numberOfDecimalPlaces = parseInt(typeAndLengthOrFormat[1]);
729
+ const powerDPs = Math.pow(10, numberOfDecimalPlaces);
730
+ result = (Math.trunc(this.doRandomFloat(verb.substring(verb.indexOf("(") + 1, verb.indexOf(")"))) * powerDPs) / powerDPs).toFixed(numberOfDecimalPlaces);
731
+ }
732
+ else {
733
+ if (verb.startsWith("from(")) {
734
+ select = this.parseParenContent(typeAndLengthOrFormat[0]);
735
+ }
736
+ else {
737
+ switch (verb) {
738
+ case "letters":
739
+ select = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
740
+ break;
741
+ case "lowercaseletters":
742
+ select = "abcdefghijklmnopqrstuvwxyz";
743
+ break;
744
+ case "uppercaseletters":
745
+ select = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
746
+ break;
747
+ case "digits":
748
+ select = "0123456789";
749
+ break;
750
+ case "alphanumerics":
751
+ select = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890";
752
+ break;
753
+ default:
754
+ throw `Unrecognised random Type [${typeAndLengthOrFormat[0]}] - Expect letters, lowercaseletters, uppercaseletters digits or alphanumerics`;
755
+ }
756
+ }
757
+ if (isNaN(parseInt(typeAndLengthOrFormat[1])) || parseInt(typeAndLengthOrFormat[1]) < 0)
758
+ throw `Invalid length part in Random token {{random;<type>;<length>}}. Length was: [${typeAndLengthOrFormat[1]}]`;
759
+ for (let count = 0; count < parseInt(typeAndLengthOrFormat[1]); count++) {
760
+ result += select[index_1.Utils.getRandomInt(0, select.length - 1)];
761
+ }
762
+ }
763
+ return result;
764
+ }
765
+ static doDateTokenPreamble(date, state, tokenBody) {
766
+ const offsetAndFormat = index_1.StringUtils.splitRemaining(tokenBody, this._delimiter, 2);
767
+ const stateIANAZone = state ? PublicHolidays.getIANAZone(state) : undefined;
768
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkInformation, `Time now is [${date}] (Epoch) and we are in [${stateIANAZone ?? "<No state defined>"}]`);
769
+ if (offsetAndFormat.length != 2 && !(offsetAndFormat.length == 1 && offsetAndFormat[0] == "timezoneoffset"))
770
+ throw "Date token does not have a format parameter; example: {date;today;dd-MM-yyyy}";
771
+ return {
772
+ offsetAndFormat: offsetAndFormat,
773
+ verb: (offsetAndFormat[0].includes("(") && offsetAndFormat[0].endsWith(")") ? offsetAndFormat[0].split("(")[0] : offsetAndFormat[0]).toLowerCase().trim(),
774
+ params: offsetAndFormat[0].includes("(") && offsetAndFormat[0].endsWith(")")
775
+ ? ((x) => {
776
+ return x.substring(0, x.length - 1);
777
+ })(offsetAndFormat[0].split("(")[1])
778
+ : undefined,
779
+ errParseDateOffset: "Invalid Active Date offset. Expect AddYears(n) AddMonths(n) or AddDays(n)",
780
+ errInvalidEpoch: "Invalid Epoch offset. Expect number of milliseconds since 1/1/1970",
781
+ errRandomParams: `Invalid Random params ([${offsetAndFormat[0]}]). Expect Random(<start date>,<end date>). Example: {date;random(1708990784000,1701360784000);yyy-MM-dd}`,
782
+ errFollowingDayParams: `FollowingDay requires epoch and day name. IE. FollowingDay(123456,wednesday) Got ${offsetAndFormat[0]}`,
783
+ errNextPublicHoldayParams: `Invalid Next Public Holiday Params. Expect nextPublicHoliday(searchFromDateEpoch,maxDaysInFuture). Got ${offsetAndFormat[0]}`,
784
+ errAddWorkingDaysParams: `Invalid Add Working Days Params. Expect addWorkingDays(numberOfDaysToAdd,[state/s]). Got Got ${offsetAndFormat[0]}`,
785
+ errInvalidDateVerb: `Invalid date verb [${offsetAndFormat[0]}]. Need: Random, Today, Now, Yesterday, Tomorrow, AddYears(n) etc... EG {date;AddDays(5);yyyy-MM-dd}`,
786
+ stateIANAZone: stateIANAZone,
787
+ };
788
+ }
789
+ static doDateTokenPostamble(offsetAndFormat, date, stateIANAZone) {
790
+ if (offsetAndFormat[1].toLowerCase() === "epoch") {
791
+ return date.toString();
792
+ }
793
+ if (offsetAndFormat[1].toLowerCase() === "second-epoch") {
794
+ return Math.floor(date / 1000).toString();
795
+ }
796
+ try {
797
+ //date += await getEpochOffset(state);
798
+ const processedDate = stateIANAZone ? (0, date_fns_tz_1.formatInTimeZone)(date, stateIANAZone, offsetAndFormat[1]) : (0, date_fns_1.format)(date, offsetAndFormat[1]);
799
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `Formatted [${offsetAndFormat[0]}] date [${date}] (Epoch) with [${offsetAndFormat[1]}] to: [${processedDate}]`);
800
+ return processedDate;
801
+ }
802
+ catch (err) {
803
+ const errorText = `Error formatting date [${date}] with format string[${offsetAndFormat[1]}]: ${err?.message ?? "<Unknown error>"}}`;
804
+ throw new Error(errorText + "/r" + (err?.stack ?? ""));
805
+ }
806
+ }
807
+ static doDateTokenNonAsyncVerbs(date, dateTokenPrep) {
808
+ let returnDate = date;
809
+ switch (dateTokenPrep.verb) {
810
+ case "random": {
811
+ if (index_1.Utils.isNullOrUndefined(dateTokenPrep.params)) {
812
+ throw new Error(dateTokenPrep.errRandomParams);
813
+ }
814
+ else {
815
+ returnDate = this.doRandomDate(dateTokenPrep.params);
816
+ }
817
+ break;
818
+ }
819
+ case "today":
820
+ case "now":
821
+ break;
822
+ case "yesterday":
823
+ returnDate = (0, date_fns_1.addDays)(date, -1).getTime();
824
+ break;
825
+ case "tomorrow":
826
+ returnDate = (0, date_fns_1.addDays)(date, 1).getTime();
827
+ break;
828
+ case "addyears":
829
+ returnDate = (0, date_fns_1.addYears)(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
830
+ break;
831
+ case "addmonths":
832
+ returnDate = (0, date_fns_1.addMonths)(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
833
+ break;
834
+ case "adddays":
835
+ returnDate = (0, date_fns_1.addDays)(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
836
+ break;
837
+ case "addhours":
838
+ returnDate = (0, date_fns_1.addHours)(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
839
+ break;
840
+ case "addMinutes":
841
+ returnDate = (0, date_fns_1.addMinutes)(date, this.getParsedDateOffset(dateTokenPrep.params, dateTokenPrep.errParseDateOffset)).getTime();
842
+ break;
843
+ case "followingday": {
844
+ if (index_1.Utils.isNullOrUndefined(dateTokenPrep.params)) {
845
+ throw new Error(dateTokenPrep.errFollowingDayParams);
846
+ }
847
+ else {
848
+ const currentAndDay = dateTokenPrep.params.split(",");
849
+ if (currentAndDay.length != 2)
850
+ throw new Error(dateTokenPrep.errFollowingDayParams);
851
+ returnDate = this.getFollowingDay(this.getParsedDateOffset(currentAndDay[0], dateTokenPrep.errInvalidEpoch), currentAndDay[1].toLowerCase(), dateTokenPrep.errFollowingDayParams).getTime();
852
+ }
853
+ break;
854
+ }
855
+ default:
856
+ if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(dateTokenPrep.verb)) {
857
+ // we have a fixed date. User must be wanted to just format a date....
858
+ const date = dateTokenPrep.verb.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
859
+ const year = Number(date[1]) ?? 0;
860
+ const month = (Number(date[2]) ?? 0) - 1;
861
+ const day = Number(date[3]) ?? 0;
862
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkInformation, `Date got fixed date:- >>${year}<<>>${month}<<>>${day}<<`);
863
+ const fullEpoch = new Date(Date.UTC(year, month, day, 0, 0, 0));
864
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkInformation, `... which is:- >>${fullEpoch}<<`);
865
+ returnDate = fullEpoch.getTime();
866
+ }
867
+ else {
868
+ throw dateTokenPrep.errInvalidDateVerb;
869
+ }
870
+ }
871
+ return returnDate;
872
+ }
873
+ static doDateToken(state, tokenBody) {
874
+ let date = new Date().getTime();
875
+ const dateTokenPrep = this.doDateTokenPreamble(date, state, tokenBody);
876
+ switch (dateTokenPrep.verb) {
877
+ case "timezoneoffset": {
878
+ return this.getOffset(state, date);
879
+ }
880
+ case "addworkingdays":
881
+ case "followingworkingday":
882
+ case "nextpublicholiday": {
883
+ const errorString = `${dateTokenPrep.verb} uses asynchoronous calls. Use Detokenise.asyncDo`;
884
+ index_1.Log.writeLine(index_1.LogLevels.Error, errorString);
885
+ throw `Detokeniser: ${errorString}`;
886
+ }
887
+ default:
888
+ date = this.doDateTokenNonAsyncVerbs(date, dateTokenPrep);
889
+ }
890
+ return this.doDateTokenPostamble(dateTokenPrep.offsetAndFormat, date, dateTokenPrep.stateIANAZone);
891
+ }
892
+ static async asyncDoDateToken(state, tokenBody) {
893
+ let date = new Date().getTime();
894
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `asyncDoDateToken - Calling preAmble: Dates [${date}], State [${state}], Token Body [${tokenBody}]`);
895
+ const dateTokenPrep = this.doDateTokenPreamble(date, state, tokenBody);
896
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `asyncDoDateToken - Back from doDateTokenPreamble. Verb ${dateTokenPrep.verb}`);
897
+ switch (dateTokenPrep.verb) {
898
+ case "timezoneoffset": {
899
+ return this.getOffset(state, date);
900
+ }
901
+ case "addworkingdays": {
902
+ // Adds days to current date not counting public holidays or weekends
903
+ try {
904
+ if (index_1.Utils.isNullOrUndefined(dateTokenPrep.params)) {
905
+ throw new Error(dateTokenPrep.errAddWorkingDaysParams);
906
+ }
907
+ const paramArray = dateTokenPrep.params.split(",");
908
+ if (paramArray.length < 1) {
909
+ throw new Error(dateTokenPrep.errAddWorkingDaysParams);
910
+ }
911
+ const numberOfDays = Number(paramArray[0]);
912
+ const ascending = !(numberOfDays < 0);
913
+ // Get the date not counting weekends
914
+ const startDate = date;
915
+ let endDate = startDate;
916
+ let daysRemaining = numberOfDays;
917
+ while (daysRemaining != 0) {
918
+ endDate = (0, date_fns_1.addDays)(endDate, ascending ? +1 : -1).getTime();
919
+ while ((await PublicHolidays.isDatePublicHoliday(endDate, state)) ||
920
+ Number(dateTokenPrep.stateIANAZone ? (0, date_fns_tz_1.formatInTimeZone)(endDate, dateTokenPrep.stateIANAZone, "e") : (0, date_fns_1.format)(endDate, "e")) == 1 ||
921
+ Number(dateTokenPrep.stateIANAZone ? (0, date_fns_tz_1.formatInTimeZone)(endDate, dateTokenPrep.stateIANAZone, "e") : (0, date_fns_1.format)(endDate, "e")) == 7) {
922
+ endDate = (0, date_fns_1.addDays)(endDate, ascending ? +1 : -1).getTime();
923
+ }
924
+ daysRemaining += ascending ? -1 : 1;
925
+ }
926
+ date = endDate;
927
+ }
928
+ catch (err) {
929
+ index_1.Log.writeLine(index_1.LogLevels.Error, `Error processing addworkingdays: ${err?.message ?? "<Unknown error>"}`);
930
+ }
931
+ break;
932
+ }
933
+ case "followingworkingday": {
934
+ if (index_1.Utils.isNullOrUndefined(dateTokenPrep.params)) {
935
+ throw new Error(dateTokenPrep.errFollowingDayParams);
936
+ }
937
+ else {
938
+ // So, first get the followingday...
939
+ const body = `followingDay(${dateTokenPrep.params});epoch`;
940
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `asyncDoDateToken - Calling doDateToken for followingDay. State = [${state}]. Body = [${body}]`);
941
+ let workingDate = Number(this.doDateToken(state, body));
942
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `got from doDateToken followingDay. workingDate = ${workingDate}`);
943
+ // Then, check it is not a weekend or public holiday anywhere. Move forward if it is...
944
+ while (Number(dateTokenPrep.stateIANAZone ? (0, date_fns_tz_1.formatInTimeZone)(workingDate, dateTokenPrep.stateIANAZone, "e") : (0, date_fns_1.format)(workingDate, "e")) == 7 ||
945
+ Number(dateTokenPrep.stateIANAZone ? (0, date_fns_tz_1.formatInTimeZone)(workingDate, dateTokenPrep.stateIANAZone, "e") : (0, date_fns_1.format)(workingDate, "e")) == 1 ||
946
+ (await PublicHolidays.isDatePublicHoliday(workingDate, state))) {
947
+ workingDate = (0, date_fns_1.addDays)(workingDate, 1).getTime();
948
+ }
949
+ date = workingDate;
950
+ }
951
+ break;
952
+ }
953
+ case "nextpublicholiday": {
954
+ if (index_1.Utils.isNullOrUndefined(dateTokenPrep.params)) {
955
+ throw new Error(dateTokenPrep.errNextPublicHoldayParams);
956
+ }
957
+ else {
958
+ const paramArray = dateTokenPrep.params.split(",");
959
+ if (paramArray.length != 2) {
960
+ throw new Error(dateTokenPrep.errNextPublicHoldayParams);
961
+ }
962
+ else {
963
+ date = (await this.getNextPublicHoliday(Number(paramArray[0]), state, Number(paramArray[1]))).getTime();
964
+ }
965
+ }
966
+ break;
967
+ }
968
+ default:
969
+ date = this.doDateTokenNonAsyncVerbs(date, dateTokenPrep);
970
+ }
971
+ return this.doDateTokenPostamble(dateTokenPrep.offsetAndFormat, date, dateTokenPrep.stateIANAZone);
972
+ }
973
+ static async getNextPublicHoliday(dateEpoch, state, maxDaysAway) {
974
+ // So, to do this we get all public holidays upto maxDaysAway; first holiday is day we want! easy!!!
975
+ if (state == undefined) {
976
+ const errMsg = "Cannot get next public holiday, State not defined (Use {Date(<state>);......})";
977
+ index_1.Log.writeLine(index_1.LogLevels.Error, errMsg);
978
+ throw new Error(errMsg);
979
+ }
980
+ const startDate = new Date(dateEpoch).getTime();
981
+ const endDate = (0, date_fns_1.addDays)(startDate, maxDaysAway).getTime();
982
+ const stateIANAZone = PublicHolidays.getIANAZone(state);
983
+ const nextPublicHoliday = await PublicHolidays.getFirstPublicHolidayBetweenDates(startDate, endDate, state);
984
+ if (index_1.Utils.isNullOrUndefined(nextPublicHoliday)) {
985
+ throw new Error(`No public holiday found between ${(0, date_fns_tz_1.formatInTimeZone)(startDate, stateIANAZone, "YYYY-MM-dd")} and ${(0, date_fns_tz_1.formatInTimeZone)(startDate, stateIANAZone, "YYYY-MM-dd")} for State: ${state} (${stateIANAZone})`);
986
+ }
987
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkInformation, `Found Public holiday: ${(0, date_fns_tz_1.formatInTimeZone)(nextPublicHoliday.date, stateIANAZone, "yyyy-MM-dd")} State: ${state} (${stateIANAZone})`);
988
+ return nextPublicHoliday.date;
989
+ }
990
+ static getFollowingDay(currentDateEpoch, requiredDayOfTheWeek, errorString) {
991
+ const dayName = requiredDayOfTheWeek.toLowerCase();
992
+ const requiredDayOfWeek = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"].findIndex((item) => {
993
+ if (item == dayName)
994
+ return true;
995
+ return false;
996
+ });
997
+ if (requiredDayOfWeek == -1)
998
+ throw new Error(errorString);
999
+ const actualDayOfWeek = new Date(currentDateEpoch).getUTCDay() - 1;
1000
+ if (actualDayOfWeek < requiredDayOfWeek) {
1001
+ return (0, date_fns_1.addDays)(currentDateEpoch, requiredDayOfWeek - actualDayOfWeek);
1002
+ }
1003
+ else {
1004
+ return (0, date_fns_1.addDays)(currentDateEpoch, requiredDayOfWeek + 7 - actualDayOfWeek);
1005
+ }
1006
+ }
1007
+ static getParsedDateOffset(numString, errorMessage) {
1008
+ if (index_1.Utils.isNullOrUndefined(numString)) {
1009
+ throw `${errorMessage} Got [No Params!]`;
1010
+ }
1011
+ else {
1012
+ const offsetValue = parseInt(numString?.trim());
1013
+ if (isNaN(offsetValue))
1014
+ throw `${errorMessage} Got [${numString?.trim()}]`;
1015
+ return offsetValue;
1016
+ }
1017
+ }
1018
+ static doRandomDate(maxAndMinDates) {
1019
+ const maxAndMin = maxAndMinDates.split(",");
1020
+ if (maxAndMin.length != 2 || Number.isNaN(+maxAndMin[0]) || Number.isNaN(+maxAndMin[1]))
1021
+ throw new Error(`Invalid Maximum and Minimum dates. Expect {random;date(fromEpoch,toEpoch);<format>}. Max/min was: [${maxAndMinDates}]`);
1022
+ const minDate = Number(maxAndMin[0]);
1023
+ const maxDate = Number(maxAndMin[1]);
1024
+ if (minDate > maxDate)
1025
+ throw new Error(`Minimum date greater than maximum!! Max/min was: [${maxAndMinDates}]`);
1026
+ return minDate + Math.abs(index_1.Utils.getRandomInt(0, this.numberOfDays(minDate, maxDate) - 1)) * 1000 * 60 * 60 * 24;
1027
+ }
1028
+ static getOffset(state, dateOfOffset) {
1029
+ if (state == undefined) {
1030
+ const errMsg = "Unable to get timezone offset as no state provided. Expect {Date(<state>);TimezoneOffset}";
1031
+ index_1.Log.writeLine(index_1.LogLevels.Error, errMsg);
1032
+ throw new Error(errMsg);
1033
+ }
1034
+ const offsetMilliseconds = (0, date_fns_tz_1.getTimezoneOffset)(PublicHolidays.getIANAZone(state), dateOfOffset);
1035
+ const offsetSeconds = Math.floor(offsetMilliseconds / 1000);
1036
+ const offsetHours = (0, date_fns_1.secondsToHours)(offsetSeconds);
1037
+ const offsetMinutes = (0, date_fns_1.secondsToMinutes)(offsetSeconds - offsetHours * 3600);
1038
+ const offset = (offsetSeconds < 0 ? "-" : "+") + index_1.Utils.pad(offsetHours, 2) + index_1.Utils.pad(offsetMinutes, 2);
1039
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkInformation, `Got current offset (${offset}) from UTC for [${state}]`);
1040
+ return offset;
1041
+ }
1042
+ static numberOfDays(minDate, maxDate) {
1043
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `Min date [${minDate}], Max date[${maxDate}]`);
1044
+ const MS_PER_DAY = 1000 * 60 * 60 * 24;
1045
+ const numberOfDays = Math.floor(Math.abs((minDate - maxDate) / MS_PER_DAY)) + 1;
1046
+ index_1.Log.writeLine(index_1.LogLevels.FrameworkDebug, `Min date [${minDate}], Max date[${maxDate}]. Number of days [${numberOfDays}]`);
1047
+ return numberOfDays;
1048
+ }
1049
+ static doRandomFloat(limits) {
1050
+ const minimumAndMaximum = limits.split(",");
1051
+ if (minimumAndMaximum.length != 2)
1052
+ throw new Error(`Invalid Maximum and Minimum floats. Expect {{random.float(min;max),<format>}}. Max/min was: [${limits}]`);
1053
+ const min = parseFloat(minimumAndMaximum[0]);
1054
+ const max = parseFloat(minimumAndMaximum[1]);
1055
+ if (isNaN(min))
1056
+ throw new Error(`Invalid Minimum float. Expect {{random.float(min;max),<format>}}. Max/min was: [${limits}]`);
1057
+ if (isNaN(max))
1058
+ throw new Error(`Invalid Maximum float. Expect {{random.float(min;max),<format>}}. Max/min was: [${limits}]`);
1059
+ return index_1.Utils.getRandomFloat(min, max);
1060
+ }
1061
+ }
1062
+ exports.Detokeniser = Detokeniser;
1063
+ Detokeniser._endTokenChar = "]]";
1064
+ Detokeniser._startTokenChar = "[[";
1065
+ Detokeniser.EscapeChar = "/";
1066
+ Detokeniser._delimiter = "|";
1067
+ Detokeniser._asyncCallbacks = undefined;
1068
+ Detokeniser._syncCallbacks = undefined;
1069
+ class InnermostToken {
1070
+ constructor(inputString, StartTokenChar, EndTokenChar, EscapeChar) {
1071
+ let startIndex = -1;
1072
+ let endIndex = -1;
1073
+ this.escapeChar = EscapeChar;
1074
+ // Find the first (leftmost) non-escaped end token sequence. Because we search left-to-right this naturally
1075
+ // gives us the innermost token when tokens are nested.
1076
+ for (let index = 0; index <= inputString.length - EndTokenChar.length; index++) {
1077
+ if (inputString.startsWith(EndTokenChar, index) && !this.isEscaped(inputString, index)) {
1078
+ endIndex = index;
1079
+ break;
1080
+ }
1081
+ }
1082
+ // Now scan right-to-left from just before the end sequence to find the matching non-escaped start sequence.
1083
+ if (endIndex !== -1) {
1084
+ for (let index = endIndex - 1; index >= 0; index--) {
1085
+ if (inputString.startsWith(StartTokenChar, index) && !this.isEscaped(inputString, index)) {
1086
+ startIndex = index;
1087
+ break;
1088
+ }
1089
+ }
1090
+ }
1091
+ if (startIndex !== -1 && endIndex !== -1) {
1092
+ this.tokenPreamble = inputString.substring(0, startIndex);
1093
+ this.tokenPostamble = inputString.substring(endIndex + EndTokenChar.length);
1094
+ this._childToken = inputString.substring(startIndex, endIndex); // includes StartTokenChar, excludes EndTokenChar
1095
+ }
1096
+ else {
1097
+ this.tokenPreamble = inputString;
1098
+ this.tokenPostamble = "";
1099
+ this._childToken = "";
1100
+ }
1101
+ this.foundToken = startIndex !== -1 && endIndex !== -1;
1102
+ }
1103
+ get preamble() {
1104
+ return this.tokenPreamble;
1105
+ }
1106
+ get postamble() {
1107
+ return this.tokenPostamble;
1108
+ }
1109
+ get childToken() {
1110
+ return this._childToken;
1111
+ }
1112
+ get hasToken() {
1113
+ return this.foundToken;
1114
+ }
1115
+ isEscaped(fullString, positionToTest) {
1116
+ let index;
1117
+ let escapeCharCount = 0;
1118
+ let returnIsEscaped = false;
1119
+ if (positionToTest > 0) {
1120
+ index = positionToTest;
1121
+ while (!(--index < 0)) {
1122
+ if (fullString[index] == this.escapeChar) {
1123
+ escapeCharCount++;
1124
+ }
1125
+ else {
1126
+ break;
1127
+ }
1128
+ }
1129
+ if (escapeCharCount % 2 == 1) {
1130
+ returnIsEscaped = true;
1131
+ }
1132
+ }
1133
+ return returnIsEscaped;
1134
+ }
1135
+ }