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