bare-script 2.0.2

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/lib/library.js ADDED
@@ -0,0 +1,978 @@
1
+ // Licensed under the MIT License
2
+ // https://github.com/craigahobbs/bare-script/blob/main/LICENSE
3
+
4
+ import {validateType, validateTypeModel} from 'schema-markdown/lib/schema.js';
5
+ import {jsonStringifySortKeys} from 'schema-markdown/lib/encode.js';
6
+ import {parseSchemaMarkdown} from 'schema-markdown/lib/parser.js';
7
+ import {typeModel} from 'schema-markdown/lib/typeModel.js';
8
+
9
+
10
+ /* eslint-disable id-length */
11
+
12
+
13
+ // The default maximum statements for executeScript
14
+ export const defaultMaxStatements = 1e9;
15
+
16
+
17
+ // The built-in script functions
18
+ export const scriptFunctions = {
19
+ //
20
+ // Array functions
21
+ //
22
+
23
+ // $function: arrayCopy
24
+ // $group: Array
25
+ // $doc: Create a copy of an array
26
+ // $arg array: The array to copy
27
+ // $return: The array copy
28
+ 'arrayCopy': ([array]) => (Array.isArray(array) ? [...array] : null),
29
+
30
+ // $function: arrayExtend
31
+ // $group: Array
32
+ // $doc: Extend one array with another
33
+ // $arg array: The array to extend
34
+ // $arg array2: The array to extend with
35
+ // $return: The extended array
36
+ 'arrayExtend': ([array, array2]) => {
37
+ if (!Array.isArray(array)) {
38
+ return null;
39
+ }
40
+ array.push(...array2);
41
+ return array;
42
+ },
43
+
44
+ // $function: arrayGet
45
+ // $group: Array
46
+ // $doc: Get an array element
47
+ // $arg array: The array
48
+ // $arg index: The array element's index
49
+ // $return: The array element
50
+ 'arrayGet': ([array, index]) => (Array.isArray(array) ? array[index] ?? null : null),
51
+
52
+ // $function: arrayIndexOf
53
+ // $group: Array
54
+ // $doc: Find the index of a value in an array
55
+ // $arg array: The array
56
+ // $arg value: The value to find in the array
57
+ // $arg index: Optional (default is 0). The index at which to start the search.
58
+ // $return: The first index of the value in the array; -1 if not found.
59
+ 'arrayIndexOf': ([array, value, index = 0]) => (Array.isArray(array) ? array.indexOf(value, index) : null),
60
+
61
+ // $function: arrayJoin
62
+ // $group: Array
63
+ // $doc: Join an array with a separator string
64
+ // $arg array: The array
65
+ // $arg separator: The separator string
66
+ // $return: The joined string
67
+ 'arrayJoin': ([array, separator]) => (Array.isArray(array) ? array.join(separator) : null),
68
+
69
+ // $function: arrayLastIndexOf
70
+ // $group: Array
71
+ // $doc: Find the last index of a value in an array
72
+ // $arg array: The array
73
+ // $arg value: The value to find in the array
74
+ // $arg index: Optional (default is the end of the array). The index at which to start the search.
75
+ // $return: The last index of the value in the array; -1 if not found.
76
+ 'arrayLastIndexOf': ([array, value, index = null]) => (
77
+ Array.isArray(array) ? (index === null ? array.lastIndexOf(value) : array.lastIndexOf(value, index)) : null
78
+ ),
79
+
80
+ // $function: arrayLength
81
+ // $group: Array
82
+ // $doc: Get the length of an array
83
+ // $arg array: The array
84
+ // $return: The array's length
85
+ 'arrayLength': ([array]) => (Array.isArray(array) ? array.length : null),
86
+
87
+ // $function: arrayNew
88
+ // $group: Array
89
+ // $doc: Create a new array
90
+ // $arg args: The new array's values
91
+ // $return: The new array
92
+ 'arrayNew': (args) => args,
93
+
94
+ // $function: arrayNewSize
95
+ // $group: Array
96
+ // $doc: Create a new array of a specific size
97
+ // $arg size: Optional (default is 0). The new array's size.
98
+ // $arg value: Optional (default is 0). The value with which to fill the new array.
99
+ // $return: The new array
100
+ 'arrayNewSize': ([size = 0, value = 0]) => new Array(size).fill(value),
101
+
102
+ // $function: arrayPop
103
+ // $group: Array
104
+ // $doc: Remove the last element of the array and return it
105
+ // $arg array: The array
106
+ // $return: The last element of the array; null if the array is empty.
107
+ 'arrayPop': ([array]) => (Array.isArray(array) ? array.pop() ?? null : null),
108
+
109
+ // $function: arrayPush
110
+ // $group: Array
111
+ // $doc: Add one or more values to the end of the array
112
+ // $arg array: The array
113
+ // $arg values: The values to add to the end of the array
114
+ // $return: The new length of the array
115
+ 'arrayPush': ([array, ...values]) => (Array.isArray(array) ? array.push(...values) : null),
116
+
117
+ // $function: arraySet
118
+ // $group: Array
119
+ // $doc: Set an array element value
120
+ // $arg array: The array
121
+ // $arg index: The index of the element to set
122
+ // $arg value: The value to set
123
+ // $return: The value
124
+ 'arraySet': ([array, index, value]) => {
125
+ if (Array.isArray(array)) {
126
+ array[index] = value;
127
+ return value;
128
+ }
129
+ return null;
130
+ },
131
+
132
+ // $function: arraySlice
133
+ // $group: Array
134
+ // $doc: Copy a portion of an array
135
+ // $arg array: The array
136
+ // $arg start: Optional (default is 0). The start index of the slice.
137
+ // $arg end: Optional (default is the end of the array). The end index of the slice.
138
+ // $return: The new array slice
139
+ 'arraySlice': ([array, start, end]) => (Array.isArray(array) ? array.slice(start, end) : null),
140
+
141
+ // $function: arraySort
142
+ // $group: Array
143
+ // $doc: Sort an array
144
+ // $arg array: The array
145
+ // $arg compareFn: Optional (default is null). The comparison function.
146
+ // $return: The sorted array
147
+ 'arraySort': ([array, compareFn = null], options) => (
148
+ Array.isArray(array) ? (compareFn === null ? array.sort() : array.sort((...args) => compareFn(args, options))) : null
149
+ ),
150
+
151
+
152
+ //
153
+ // Console functions
154
+ //
155
+
156
+ // $function: consoleLog
157
+ // $group: Console
158
+ // $doc: Log a message to the console
159
+ // $arg string: The message
160
+ 'consoleLog': ([string], options) => {
161
+ if (options !== null && 'logFn' in options) {
162
+ options.logFn(string);
163
+ }
164
+ },
165
+
166
+ // $function: consoleLogDebug
167
+ // $group: Console
168
+ // $doc: Log a debug message
169
+ // $arg string: The message
170
+ 'consoleLogDebug': ([string], options) => {
171
+ if (options !== null && 'logFn' in options && options.debug) {
172
+ options.logFn(string);
173
+ }
174
+ },
175
+
176
+
177
+ //
178
+ // Datetime functions
179
+ //
180
+
181
+ // $function: datetimeDay
182
+ // $group: Datetime
183
+ // $doc: Get the day of the month of a datetime
184
+ // $arg datetime: The datetime
185
+ // $return: The day of the month
186
+ 'datetimeDay': ([datetime]) => (datetime instanceof Date ? datetime.getDate() : null),
187
+
188
+ // $function: datetimeHour
189
+ // $group: Datetime
190
+ // $doc: Get the hour of a datetime
191
+ // $arg datetime: The datetime
192
+ // $return: The hour
193
+ 'datetimeHour': ([datetime]) => (datetime instanceof Date ? datetime.getHours() : null),
194
+
195
+ // $function: datetimeISOFormat
196
+ // $group: Datetime
197
+ // $doc: Format the datetime as an ISO date/time string
198
+ // $arg datetime: The datetime
199
+ // $arg isDate: If true, format the datetime as an ISO date
200
+ // $return: The formatted datetime string
201
+ 'datetimeISOFormat': ([datetime, isDate = false]) => {
202
+ let result = null;
203
+ if (datetime instanceof Date) {
204
+ result = datetime.toISOString();
205
+ if (isDate) {
206
+ result = result.slice(0, result.indexOf('T'));
207
+ }
208
+ }
209
+ return result;
210
+ },
211
+
212
+ // $function: datetimeMinute
213
+ // $group: Datetime
214
+ // $doc: Get the number of minutes of a datetime
215
+ // $arg datetime: The datetime
216
+ // $return: The number of minutes
217
+ 'datetimeMinute': ([datetime]) => (datetime instanceof Date ? datetime.getMinutes() : null),
218
+
219
+ // $function: datetimeMonth
220
+ // $group: Datetime
221
+ // $doc: Get the number of the month (1-12) of a datetime
222
+ // $arg datetime: The datetime
223
+ // $return: The number of the month
224
+ 'datetimeMonth': ([datetime]) => (datetime instanceof Date ? datetime.getMonth() + 1 : null),
225
+
226
+ // $function: datetimeNew
227
+ // $group: Datetime
228
+ // $doc: Create a new datetime
229
+ // $arg year: The full year
230
+ // $arg month: The month (1-12)
231
+ // $arg day: The day of the month
232
+ // $arg hours: Optional (default is 0). The hour (0-23)
233
+ // $arg minutes: Optional (default is 0). The number of minutes.
234
+ // $arg seconds: Optional (default is 0). The number of seconds.
235
+ // $arg milliseconds: Optional (default is 0). The number of milliseconds.
236
+ // $return: The new datetime
237
+ 'datetimeNew': ([year, month, day, hours = 0, minutes = 0, seconds = 0, milliseconds = 0]) => (
238
+ new Date(year, month - 1, day, hours, minutes, seconds, milliseconds)
239
+ ),
240
+
241
+ // $function: datetimeNewUTC
242
+ // $group: Datetime
243
+ // $doc: Create a new UTC datetime
244
+ // $arg year: The full year
245
+ // $arg month: The month (1-12)
246
+ // $arg day: The day of the month
247
+ // $arg hours: Optional (default is 0). The hour (0-23)
248
+ // $arg minutes: Optional (default is 0). The number of minutes.
249
+ // $arg seconds: Optional (default is 0). The number of seconds.
250
+ // $arg milliseconds: Optional (default is 0). The number of milliseconds.
251
+ // $return: The new UTC datetime
252
+ 'datetimeNewUTC': ([year, month, day, hours = 0, minutes = 0, seconds = 0, milliseconds = 0]) => (
253
+ new Date(Date.UTC(year, month - 1, day, hours, minutes, seconds, milliseconds))
254
+ ),
255
+
256
+ // $function: datetimeNow
257
+ // $group: Datetime
258
+ // $doc: Get the current datetime
259
+ // $return: The current datetime
260
+ 'datetimeNow': () => new Date(),
261
+
262
+ // $function: datetimeSecond
263
+ // $group: Datetime
264
+ // $doc: Get the number of seconds of a datetime
265
+ // $arg datetime: The datetime
266
+ // $return: The number of seconds
267
+ 'datetimeSecond': ([datetime]) => (datetime instanceof Date ? datetime.getSeconds() : null),
268
+
269
+ // $function: datetimeToday
270
+ // $group: Datetime
271
+ // $doc: Get today's datetime
272
+ // $return: Today's datetime
273
+ 'datetimeToday': () => {
274
+ const now = new Date();
275
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate());
276
+ },
277
+
278
+ // $function: datetimeYear
279
+ // $group: Datetime
280
+ // $doc: Get the full year of a datetime
281
+ // $arg datetime: The datetime
282
+ // $return: The full year
283
+ 'datetimeYear': ([datetime]) => (datetime instanceof Date ? datetime.getFullYear() : null),
284
+
285
+
286
+ //
287
+ // HTTP functions
288
+ //
289
+
290
+ // $function: httpFetch
291
+ // $group: HTTP
292
+ // $doc: Retrieve a remote JSON or text resource
293
+ // $arg url: The resource URL or array of URLs
294
+ // $arg options: Optional (default is null). The [fetch options](https://developer.mozilla.org/en-US/docs/Web/API/fetch#parameters).
295
+ // $arg isText: Optional (default is false). If true, retrieve the resource as text.
296
+ // $return: The resource object/string or array of objects/strings; null if an error occurred.
297
+ 'httpFetch': async ([url, fetchOptions = null, isText = false], options) => {
298
+ const isArray = Array.isArray(url);
299
+ const urls = (isArray ? url : [url]).map((mURL) => (options !== null && 'urlFn' in options ? options.urlFn(mURL) : mURL));
300
+ const responses = await Promise.all(urls.map(async (fURL) => {
301
+ try {
302
+ return 'fetchFn' in options ? await (fetchOptions ? options.fetchFn(fURL, fetchOptions) : options.fetchFn(fURL)) : null;
303
+ } catch {
304
+ return null;
305
+ }
306
+ }));
307
+ const values = await Promise.all(responses.map(async (response) => {
308
+ try {
309
+ return response !== null && response.ok ? await (isText ? response.text() : response.json()) : null;
310
+ } catch {
311
+ return null;
312
+ }
313
+ }));
314
+
315
+ // Log failures
316
+ for (const [ixValue, value] of values.entries()) {
317
+ if (value === null && options !== null && 'logFn' in options && options.debug) {
318
+ const errorURL = urls[ixValue];
319
+ options.logFn(`BareScript: Function "httpFetch" failed for ${isText ? 'text' : 'JSON'} resource "${errorURL}"`);
320
+ }
321
+ }
322
+
323
+ return isArray ? values : values[0];
324
+ },
325
+
326
+
327
+ //
328
+ // JSON functions
329
+ //
330
+
331
+ // $function: jsonParse
332
+ // $group: JSON
333
+ // $doc: Convert a JSON string to an object
334
+ // $arg string: The JSON string
335
+ // $return: The object
336
+ 'jsonParse': ([string]) => JSON.parse(string),
337
+
338
+ // $function: jsonStringify
339
+ // $group: JSON
340
+ // $doc: Convert an object to a JSON string
341
+ // $arg value: The object
342
+ // $arg space: Optional (default is null). The indentation string or number.
343
+ // $return: The JSON string
344
+ 'jsonStringify': ([value, space]) => jsonStringifySortKeys(value, space),
345
+
346
+
347
+ //
348
+ // Math functions
349
+ //
350
+
351
+ // $function: mathAbs
352
+ // $group: Math
353
+ // $doc: Compute the absolute value of a number
354
+ // $arg x: The number
355
+ // $return: The absolute value of the number
356
+ 'mathAbs': ([x]) => Math.abs(x),
357
+
358
+ // $function: mathAcos
359
+ // $group: Math
360
+ // $doc: Compute the arccosine, in radians, of a number
361
+ // $arg x: The number
362
+ // $return: The arccosine, in radians, of the number
363
+ 'mathAcos': ([x]) => Math.acos(x),
364
+
365
+ // $function: mathAsin
366
+ // $group: Math
367
+ // $doc: Compute the arcsine, in radians, of a number
368
+ // $arg x: The number
369
+ // $return: The arcsine, in radians, of the number
370
+ 'mathAsin': ([x]) => Math.asin(x),
371
+
372
+ // $function: mathAtan
373
+ // $group: Math
374
+ // $doc: Compute the arctangent, in radians, of a number
375
+ // $arg x: The number
376
+ // $return: The arctangent, in radians, of the number
377
+ 'mathAtan': ([x]) => Math.atan(x),
378
+
379
+ // $function: mathAtan2
380
+ // $group: Math
381
+ // $doc: Compute the angle, in radians, between (0, 0) and a point
382
+ // $arg y: The Y-coordinate of the point
383
+ // $arg x: The X-coordinate of the point
384
+ // $return: The angle, in radians
385
+ 'mathAtan2': ([y, x]) => Math.atan2(y, x),
386
+
387
+ // $function: mathCeil
388
+ // $group: Math
389
+ // $doc: Compute the ceiling of a number (round up to the next highest integer)
390
+ // $arg x: The number
391
+ // $return: The ceiling of the number
392
+ 'mathCeil': ([x]) => Math.ceil(x),
393
+
394
+ // $function: mathCos
395
+ // $group: Math
396
+ // $doc: Compute the cosine of an angle, in radians
397
+ // $arg x: The angle, in radians
398
+ // $return: The cosine of the angle
399
+ 'mathCos': ([x]) => Math.cos(x),
400
+
401
+ // $function: mathFloor
402
+ // $group: Math
403
+ // $doc: Compute the floor of a number (round down to the next lowest integer)
404
+ // $arg x: The number
405
+ // $return: The floor of the number
406
+ 'mathFloor': ([x]) => Math.floor(x),
407
+
408
+ // $function: mathLn
409
+ // $group: Math
410
+ // $doc: Compute the natural logarithm (base e) of a number
411
+ // $arg x: The number
412
+ // $return: The natural logarithm of the number
413
+ 'mathLn': ([x]) => Math.log(x),
414
+
415
+ // $function: mathLog
416
+ // $group: Math
417
+ // $doc: Compute the logarithm (base 10) of a number
418
+ // $arg x: The number
419
+ // $arg base: Optional (default is 10). The logarithm base.
420
+ // $return: The logarithm of the number
421
+ 'mathLog': ([x, base = 10]) => Math.log(x) / Math.log(base),
422
+
423
+ // $function: mathMax
424
+ // $group: Math
425
+ // $doc: Compute the maximum value of all arguments
426
+ // $arg values: All arguments
427
+ // $return: The maximum value
428
+ 'mathMax': (values) => Math.max(...values),
429
+
430
+ // $function: mathMin
431
+ // $group: Math
432
+ // $doc: Compute the minimum value of all arguments
433
+ // $arg values: All arguments
434
+ // $return: The minimum value
435
+ 'mathMin': (values) => Math.min(...values),
436
+
437
+ // $function: mathPi
438
+ // $group: Math
439
+ // $doc: Return the number pi
440
+ // $return: The number pi
441
+ 'mathPi': () => Math.PI,
442
+
443
+ // $function: mathRandom
444
+ // $group: Math
445
+ // $doc: Compute a random number between 0 and 1, inclusive
446
+ // $return: A random number
447
+ 'mathRandom': () => Math.random(),
448
+
449
+ // $function: mathRound
450
+ // $group: Math
451
+ // $doc: Round a number to a certain number of decimal places
452
+ // $arg x: The number
453
+ // $arg digits: Optional (default is 0). The number of decimal digits to round to.
454
+ // $return: The rounded number
455
+ 'mathRound': ([x, digits = 0]) => {
456
+ const multiplier = 10 ** digits;
457
+ return Math.round(x * multiplier) / multiplier;
458
+ },
459
+
460
+ // $function: mathSign
461
+ // $group: Math
462
+ // $doc: Compute the sign of a number
463
+ // $arg x: The number
464
+ // $return: -1 for a negative number, 1 for a positive number, and 0 for zero
465
+ 'mathSign': ([x]) => Math.sign(x),
466
+
467
+ // $function: mathSin
468
+ // $group: Math
469
+ // $doc: Compute the sine of an angle, in radians
470
+ // $arg x: The angle, in radians
471
+ // $return: The sine of the angle
472
+ 'mathSin': ([x]) => Math.sin(x),
473
+
474
+ // $function: mathSqrt
475
+ // $group: Math
476
+ // $doc: Compute the square root of a number
477
+ // $arg x: The number
478
+ // $return: The square root of the number
479
+ 'mathSqrt': ([x]) => Math.sqrt(x),
480
+
481
+ // $function: mathTan
482
+ // $group: Math
483
+ // $doc: Compute the tangent of an angle, in radians
484
+ // $arg x: The angle, in radians
485
+ // $return: The tangent of the angle
486
+ 'mathTan': ([x]) => Math.tan(x),
487
+
488
+
489
+ //
490
+ // Number functions
491
+ //
492
+
493
+ // $function: numberParseFloat
494
+ // $group: Number
495
+ // $doc: Parse a string as a floating point number
496
+ // $arg string: The string
497
+ // $return: The number
498
+ 'numberParseFloat': ([string]) => Number.parseFloat(string),
499
+
500
+ // $function: numberParseInt
501
+ // $group: Number
502
+ // $doc: Parse a string as an integer
503
+ // $arg string: The string
504
+ // $arg radix: Optional (default is 10). The number base.
505
+ // $return: The integer
506
+ 'numberParseInt': ([string, radix = 10]) => Number.parseInt(string, radix),
507
+
508
+ // $function: numberToFixed
509
+ // $group: Number
510
+ // $doc: Format a number using fixed-point notation
511
+ // $arg x: The number
512
+ // $arg digits: Optional (default is 2). The number of digits to appear after the decimal point.
513
+ // $arg trim: Optional (default is false). If true, trim trailing zeroes and decimal point.
514
+ // $return: The fixed-point notation string
515
+ 'numberToFixed': ([x, digits = 2, trim = false]) => {
516
+ let result = null;
517
+ if (typeof x === 'number') {
518
+ result = x.toFixed(digits);
519
+ if (trim) {
520
+ result = result.replace(rNumberCleanup, '');
521
+ }
522
+ }
523
+ return result;
524
+ },
525
+
526
+
527
+ //
528
+ // Object functions
529
+ //
530
+
531
+ // $function: objectAssign
532
+ // $group: Object
533
+ // $doc: Assign the keys/values of one object to another
534
+ // $arg object: The object to assign to
535
+ // $arg object2: The object to assign
536
+ // $return: The updated object
537
+ 'objectAssign': ([object, object2]) => {
538
+ if (object !== null && typeof object === 'object' && !Array.isArray(object) &&
539
+ object2 !== null && typeof object2 === 'object' && !Array.isArray(object2)) {
540
+ return Object.assign(object, object2);
541
+ }
542
+ return null;
543
+ },
544
+
545
+ // $function: objectCopy
546
+ // $group: Object
547
+ // $doc: Create a copy of an object
548
+ // $arg object: The object to copy
549
+ // $return: The object copy
550
+ 'objectCopy': ([object]) => (object !== null && typeof object === 'object' && !Array.isArray(object) ? {...object} : null),
551
+
552
+ // $function: objectDelete
553
+ // $group: Object
554
+ // $doc: Delete an object key
555
+ // $arg object: The object
556
+ // $arg key: The key to delete
557
+ 'objectDelete': ([object, key]) => {
558
+ if (object !== null && typeof object === 'object' && !Array.isArray(object)) {
559
+ delete object[key];
560
+ }
561
+ },
562
+
563
+ // $function: objectGet
564
+ // $group: Object
565
+ // $doc: Get an object key's value
566
+ // $arg object: The object
567
+ // $arg key: The key
568
+ // $arg defaultValue: The default value (optional)
569
+ // $return: The value or null if the key does not exist
570
+ 'objectGet': ([object, key, defaultValue = null]) => (
571
+ object !== null && typeof object === 'object' ? (Object.hasOwn(object, key) ? object[key] : defaultValue) : null
572
+ ),
573
+
574
+ // $function: objectHas
575
+ // $group: Object
576
+ // $doc: Test if an object contains a key
577
+ // $arg object: The object
578
+ // $arg key: The key
579
+ // $return: true if the object contains the key, false otherwise
580
+ 'objectHas': ([object, key]) => (object !== null && typeof object === 'object' && Object.hasOwn(object, key)),
581
+
582
+ // $function: objectKeys
583
+ // $group: Object
584
+ // $doc: Get an object's keys
585
+ // $arg object: The object
586
+ // $return: The array of keys
587
+ 'objectKeys': ([object]) => (object !== null && typeof object === 'object' && !Array.isArray(object) ? Object.keys(object) : null),
588
+
589
+ // $function: objectNew
590
+ // $group: Object
591
+ // $doc: Create a new object
592
+ // $arg keyValues: The object's initial key and value arguments
593
+ // $return: The new object
594
+ 'objectNew': (keyValues) => {
595
+ const object = {};
596
+ for (let ix = 0; ix < keyValues.length; ix += 2) {
597
+ object[keyValues[ix]] = (ix + 1 < keyValues.length ? keyValues[ix + 1] : null);
598
+ }
599
+ return object;
600
+ },
601
+
602
+ // $function: objectSet
603
+ // $group: Object
604
+ // $doc: Set an object key's value
605
+ // $arg object: The object
606
+ // $arg key: The key
607
+ // $arg value: The value to set
608
+ // $return: The value to set
609
+ 'objectSet': ([object, key, value]) => {
610
+ if (object !== null && typeof object === 'object' && !Array.isArray(object)) {
611
+ object[key] = value;
612
+ return value;
613
+ }
614
+ return null;
615
+ },
616
+
617
+
618
+ //
619
+ // Regular expression functions
620
+ //
621
+
622
+ // $function: regexEscape
623
+ // $group: Regex
624
+ // $doc: Escape a string for use in a regular expression
625
+ // $arg string: The string to escape
626
+ // $return: The escaped string
627
+ 'regexEscape': ([string]) => (typeof string === 'string' ? string.replace(reRegexEscape, '\\$&') : null),
628
+
629
+ // $function: regexMatch
630
+ // $group: Regex
631
+ // $doc: Find the first match of a regular expression in a string
632
+ // $arg regex: The regular expression
633
+ // $arg string: The string
634
+ // $return: The [match object
635
+ // $return: ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match#return_value)
636
+ // $return: or null if no matches are found
637
+ 'regexMatch': ([regex, string]) => (typeof string === 'string' ? string.match(regex) : null),
638
+
639
+ // $function: regexMatchAll
640
+ // $group: Regex
641
+ // $doc: Find all matches of regular expression in a string
642
+ // $arg regex: The regular expression
643
+ // $arg string: The string
644
+ // $return: The [match object
645
+ // $return: ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match#return_value)
646
+ // $return: array or null if no matches are found
647
+ 'regexMatchAll': ([regex, string]) => (typeof string === 'string' ? Array.from(string.matchAll(regex)) : null),
648
+
649
+ // $function: regexNew
650
+ // $group: Regex
651
+ // $doc: Create a regular expression
652
+ // $arg pattern: The regular expression pattern string
653
+ // $arg flags: The [regular expression flags
654
+ // $arg flags: ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#advanced_searching_with_flags)
655
+ // $return: The regular expression or null if the pattern is invalid
656
+ 'regexNew': ([pattern, flags]) => new RegExp(pattern, flags),
657
+
658
+ // $function: regexTest
659
+ // $group: Regex
660
+ // $doc: Test if a regular expression matches a string
661
+ // $arg regex: The regular expression
662
+ // $arg string: The string
663
+ // $return: true if the regular expression matches, false otherwise
664
+ 'regexTest': ([regex, string]) => (regex instanceof RegExp ? regex.test(string) : null),
665
+
666
+
667
+ //
668
+ // Runtime functions
669
+ //
670
+
671
+ // $function: runtimeGetGlobal
672
+ // $group: Runtime
673
+ // $doc: Get a global variable value
674
+ // $arg name: The global variable name
675
+ // $return: The global variable's value or null if it does not exist
676
+ 'runtimeGetGlobal': ([name], options) => {
677
+ const globals = (options !== null ? (options.globals ?? null) : null);
678
+ return (globals !== null ? (globals[name] ?? null) : null);
679
+ },
680
+
681
+ // $function: runtimeSetGlobal
682
+ // $group: Runtime
683
+ // $doc: Set a global variable value
684
+ // $arg name: The global variable name
685
+ // $arg value: The global variable's value
686
+ // $return: The global variable's value
687
+ 'runtimeSetGlobal': ([name, value], options) => {
688
+ if (options !== null) {
689
+ const globals = options.globals ?? null;
690
+ if (globals !== null) {
691
+ globals[name] = value;
692
+ }
693
+ }
694
+ return value;
695
+ },
696
+
697
+
698
+ //
699
+ // Schema functions
700
+ //
701
+
702
+ // $function: schemaParse
703
+ // $group: Schema
704
+ // $doc: Parse the [Schema Markdown](https://craigahobbs.github.io/schema-markdown-js/language/) text
705
+ // $arg lines: The [Schema Markdown](https://craigahobbs.github.io/schema-markdown-js/language/)
706
+ // $arg lines: text lines (may contain nested arrays of un-split lines)
707
+ // $return: The schema's [type model](https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types')
708
+ 'schemaParse': (lines) => parseSchemaMarkdown(lines),
709
+
710
+ // $function: schemaParseEx
711
+ // $group: Schema
712
+ // $doc: Parse the [Schema Markdown](https://craigahobbs.github.io/schema-markdown-js/language/) text with options
713
+ // $arg lines: The array of [Schema Markdown](https://craigahobbs.github.io/schema-markdown-js/language/)
714
+ // $arg lines: text lines (may contain nested arrays of un-split lines)
715
+ // $arg types: Optional. The [type model](https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types').
716
+ // $arg filename: Optional (default is ""). The file name.
717
+ // $return: The schema's [type model](https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types')
718
+ 'schemaParseEx': ([lines, types = {}, filename = '']) => parseSchemaMarkdown(lines, {types, filename}),
719
+
720
+ // $function: schemaTypeModel
721
+ // $group: Schema
722
+ // $doc: Get the [Schema Markdown Type Model](https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types')
723
+ // $return: The [Schema Markdown Type Model](https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types')
724
+ 'schemaTypeModel': () => typeModel,
725
+
726
+ // $function: schemaValidate
727
+ // $group: Schema
728
+ // $doc: Validate an object to a schema type
729
+ // $arg types: The [type model](https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types')
730
+ // $arg typeName: The type name
731
+ // $arg value: The object to validate
732
+ // $return: The validated object or null if validation fails
733
+ 'schemaValidate': ([types, typeName, value]) => validateType(types, typeName, value),
734
+
735
+ // $function: schemaValidateTypeModel
736
+ // $group: Schema
737
+ // $doc: Validate a [Schema Markdown Type Model](https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types')
738
+ // $arg types: The [type model](https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types') to validate
739
+ // $return: The validated [type model](https://craigahobbs.github.io/schema-markdown-doc/doc/#var.vName='Types')
740
+ 'schemaValidateTypeModel': ([types]) => validateTypeModel(types),
741
+
742
+
743
+ //
744
+ // String functions
745
+ //
746
+
747
+ // $function: stringCharCodeAt
748
+ // $group: String
749
+ // $doc: Get a string index's character code
750
+ // $arg string: The string
751
+ // $arg index: The character index
752
+ // $return: The character code
753
+ 'stringCharCodeAt': ([string, index]) => (typeof string === 'string' ? string.charCodeAt(index) : null),
754
+
755
+ // $function: stringEndsWith
756
+ // $group: String
757
+ // $doc: Determine if a string ends with a search string
758
+ // $arg string: The string
759
+ // $arg searchString: The search string
760
+ // $return: true if the string ends with the search string, false otherwise
761
+ 'stringEndsWith': ([string, searchString]) => (typeof string === 'string' ? string.endsWith(searchString) : null),
762
+
763
+ // $function: stringFromCharCode
764
+ // $group: String
765
+ // $doc: Create a string from the character code arguments
766
+ // $arg charCodes: The character codes
767
+ // $return: The new string
768
+ 'stringFromCharCode': (charCodes) => String.fromCharCode(...charCodes),
769
+
770
+ // $function: stringIndexOf
771
+ // $group: String
772
+ // $doc: Find the first index of a search string in a string
773
+ // $arg string: The string
774
+ // $arg searchString: The search string
775
+ // $arg index: Optional (default is 0). The index at which to start the search.
776
+ // $return: The first index of the search string; -1 if not found.
777
+ 'stringIndexOf': ([string, searchString, index]) => (typeof string === 'string' ? string.indexOf(searchString, index) : null),
778
+
779
+ // $function: stringLastIndexOf
780
+ // $group: String
781
+ // $doc: Find the last index of a search string in a string
782
+ // $arg string: The string
783
+ // $arg searchString: The search string
784
+ // $arg index: Optional (default is the end of the string). The index at which to start the search.
785
+ // $return: The last index of the search string; -1 if not found.
786
+ 'stringLastIndexOf': ([string, searchString, index]) => (
787
+ typeof string === 'string' ? string.lastIndexOf(searchString, index) : null
788
+ ),
789
+
790
+ // $function: stringLength
791
+ // $group: String
792
+ // $doc: Get the length of a string
793
+ // $arg string: The string
794
+ // $return: The string's length
795
+ 'stringLength': ([string]) => (typeof string === 'string' ? string.length : null),
796
+
797
+ // $function: stringLower
798
+ // $group: String
799
+ // $doc: Convert a string to lower-case
800
+ // $arg string: The string
801
+ // $return: The lower-case string
802
+ 'stringLower': ([string]) => (typeof string === 'string' ? string.toLowerCase() : null),
803
+
804
+ // $function: stringNew
805
+ // $group: String
806
+ // $doc: Create a new string from a value
807
+ // $arg value: The value
808
+ // $return: The new string
809
+ 'stringNew': ([value]) => `${value}`,
810
+
811
+ // $function: stringRepeat
812
+ // $group: String
813
+ // $doc: Repeat a string
814
+ // $arg string: The string to repeat
815
+ // $arg count: The number of times to repeat the string
816
+ // $return: The repeated string
817
+ 'stringRepeat': ([string, count]) => (typeof string === 'string' ? string.repeat(count) : null),
818
+
819
+ // $function: stringReplace
820
+ // $group: String
821
+ // $doc: Replace all instances of a string with another string
822
+ // $arg string: The string to update
823
+ // $arg substr: The string to replace
824
+ // $arg newSubstr: The replacement string
825
+ // $return: The updated string
826
+ 'stringReplace': ([string, substr, newSubstr], options) => {
827
+ if (typeof string !== 'string') {
828
+ return null;
829
+ }
830
+ if (typeof newSubstr === 'function') {
831
+ const replacerFunction = (...args) => newSubstr(args, options);
832
+ return string.replaceAll(substr, replacerFunction);
833
+ }
834
+ return string.replaceAll(substr, newSubstr);
835
+ },
836
+
837
+ // $function: stringSlice
838
+ // $group: String
839
+ // $doc: Copy a portion of a string
840
+ // $arg string: The string
841
+ // $arg start: Optional (default is 0). The start index of the slice.
842
+ // $arg end: Optional (default is the end of the string). The end index of the slice.
843
+ // $return: The new string slice
844
+ 'stringSlice': ([string, beginIndex, endIndex]) => (typeof string === 'string' ? string.slice(beginIndex, endIndex) : null),
845
+
846
+ // $function: stringSplit
847
+ // $group: String
848
+ // $doc: Split a string
849
+ // $arg string: The string to split
850
+ // $arg separator: The separator string or regular expression
851
+ // $arg limit: The maximum number of strings to split into
852
+ // $return: The array of split-out strings
853
+ 'stringSplit': ([string, separator, limit]) => (typeof string === 'string' ? string.split(separator, limit) : null),
854
+
855
+ // $function: stringStartsWith
856
+ // $group: String
857
+ // $doc: Determine if a string starts with a search string
858
+ // $arg string: The string
859
+ // $arg searchString: The search string
860
+ // $return: true if the string starts with the search string, false otherwise
861
+ 'stringStartsWith': ([string, searchString]) => (typeof string === 'string' ? string.startsWith(searchString) : null),
862
+
863
+ // $function: stringTrim
864
+ // $group: String
865
+ // $doc: Trim the whitespace from the beginning and end of a string
866
+ // $arg string: The string
867
+ // $return: The trimmed string
868
+ 'stringTrim': ([string]) => (typeof string === 'string' ? string.trim() : null),
869
+
870
+ // $function: stringUpper
871
+ // $group: String
872
+ // $doc: Convert a string to upper-case
873
+ // $arg string: The string
874
+ // $return: The upper-case string
875
+ 'stringUpper': ([string]) => (typeof string === 'string' ? string.toUpperCase() : null),
876
+
877
+
878
+ //
879
+ // URL functions
880
+ //
881
+
882
+ // $function: urlEncode
883
+ // $group: URL
884
+ // $doc: Encode a URL
885
+ // $arg url: The URL string
886
+ // $arg extra: Optional (default is true). If true, encode extra characters for wider compatibility.
887
+ // $return: The encoded URL string
888
+ 'urlEncode': ([url, extra = true]) => {
889
+ let urlEncoded = encodeURI(url);
890
+ if (extra) {
891
+ // Replace ')' with '%29' for Markdown links
892
+ urlEncoded = urlEncoded.replaceAll(')', '%29');
893
+ }
894
+ return urlEncoded;
895
+ },
896
+
897
+ // $function: urlEncodeComponent
898
+ // $group: URL
899
+ // $doc: Encode a URL component
900
+ // $arg url: The URL component string
901
+ // $arg extra: Optional (default is true). If true, encode extra characters for wider compatibility.
902
+ // $return: The encoded URL component string
903
+ 'urlEncodeComponent': ([urlComponent, extra = true]) => {
904
+ let urlComponentEncoded = encodeURIComponent(urlComponent);
905
+ if (extra) {
906
+ // Replace ')' with '%29' for Markdown links
907
+ urlComponentEncoded = urlComponentEncoded.replaceAll(')', '%29');
908
+ }
909
+ return urlComponentEncoded;
910
+ }
911
+ };
912
+
913
+
914
+ // Regex escape regular expression
915
+ const reRegexEscape = /[.*+?^${}()|[\]\\]/g;
916
+
917
+
918
+ // Fixed-number trim regular expression
919
+ const rNumberCleanup = /\.0*$/;
920
+
921
+
922
+ // The built-in expression function name script function name map
923
+ export const expressionFunctionMap = {
924
+ 'abs': 'mathAbs',
925
+ 'acos': 'mathAcos',
926
+ 'asin': 'mathAsin',
927
+ 'atan': 'mathAtan',
928
+ 'atan2': 'mathAtan2',
929
+ 'ceil': 'mathCeil',
930
+ 'charCodeAt': 'stringCharCodeAt',
931
+ 'cos': 'mathCos',
932
+ 'date': 'datetimeNew',
933
+ 'day': 'datetimeDay',
934
+ 'endsWith': 'stringEndsWith',
935
+ 'if': 'if',
936
+ 'indexOf': 'stringIndexOf',
937
+ 'fixed': 'numberToFixed',
938
+ 'floor': 'mathFloor',
939
+ 'fromCharCode': 'stringFromCharCode',
940
+ 'hour': 'datetimeHour',
941
+ 'lastIndexOf': 'stringLastIndexOf',
942
+ 'len': 'stringLength',
943
+ 'lower': 'stringLower',
944
+ 'ln': 'mathLn',
945
+ 'log': 'mathLog',
946
+ 'max': 'mathMax',
947
+ 'min': 'mathMin',
948
+ 'minute': 'datetimeMinute',
949
+ 'month': 'datetimeMonth',
950
+ 'now': 'datetimeNow',
951
+ 'parseInt': 'numberParseInt',
952
+ 'parseFloat': 'numberParseFloat',
953
+ 'pi': 'mathPi',
954
+ 'rand': 'mathRandom',
955
+ 'replace': 'stringReplace',
956
+ 'rept': 'stringRepeat',
957
+ 'round': 'mathRound',
958
+ 'second': 'datetimeSecond',
959
+ 'sign': 'mathSign',
960
+ 'sin': 'mathSin',
961
+ 'slice': 'stringSlice',
962
+ 'sqrt': 'mathSqrt',
963
+ 'startsWith': 'stringStartsWith',
964
+ 'text': 'stringNew',
965
+ 'tan': 'mathTan',
966
+ 'today': 'datetimeToday',
967
+ 'trim': 'stringTrim',
968
+ 'upper': 'stringUpper',
969
+ 'urlEncode': 'urlEncode',
970
+ 'urlEncodeComponent': 'urlEncodeComponent',
971
+ 'year': 'datetimeYear'
972
+ };
973
+
974
+
975
+ // The built-in expression functions
976
+ export const expressionFunctions = Object.fromEntries(Object.entries(expressionFunctionMap).map(
977
+ ([exprFnName, scriptFnName]) => [exprFnName, scriptFunctions[scriptFnName] ?? null]
978
+ ).filter(([, exprFn]) => exprFn !== null));