@isrd-isi-edu/ermrestjs 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +55 -0
  3. package/dist/ermrest.d.ts +3481 -0
  4. package/dist/ermrest.js +45 -0
  5. package/dist/ermrest.js.gz +0 -0
  6. package/dist/ermrest.js.map +1 -0
  7. package/dist/ermrest.min.js +45 -0
  8. package/dist/ermrest.min.js.gz +0 -0
  9. package/dist/ermrest.min.js.map +1 -0
  10. package/dist/ermrest.ver.txt +1 -0
  11. package/dist/stats.html +4949 -0
  12. package/js/ag_reference.js +1483 -0
  13. package/js/core.js +4931 -0
  14. package/js/datapath.js +336 -0
  15. package/js/export.js +956 -0
  16. package/js/filters.js +192 -0
  17. package/js/format.js +344 -0
  18. package/js/hatrac.js +1130 -0
  19. package/js/json_ld_validator.js +285 -0
  20. package/js/parser.js +2320 -0
  21. package/js/setup/node.js +27 -0
  22. package/js/utils/helpers.js +2300 -0
  23. package/js/utils/json_ld_schema.js +680 -0
  24. package/js/utils/pseudocolumn_helpers.js +2196 -0
  25. package/package.json +79 -0
  26. package/src/index.ts +204 -0
  27. package/src/models/comment.ts +14 -0
  28. package/src/models/deferred-promise.ts +16 -0
  29. package/src/models/display-name.ts +5 -0
  30. package/src/models/errors.ts +408 -0
  31. package/src/models/path-prefix-alias-mapping.ts +130 -0
  32. package/src/models/reference/bulk-create-foreign-key-object.ts +133 -0
  33. package/src/models/reference/citation.ts +98 -0
  34. package/src/models/reference/contextualize.ts +535 -0
  35. package/src/models/reference/google-dataset-metadata.ts +72 -0
  36. package/src/models/reference/index.ts +14 -0
  37. package/src/models/reference/page.ts +520 -0
  38. package/src/models/reference/reference-aggregate-fn.ts +37 -0
  39. package/src/models/reference/reference.ts +2813 -0
  40. package/src/models/reference/related-reference.ts +467 -0
  41. package/src/models/reference/tuple.ts +652 -0
  42. package/src/models/reference-column/asset-pseudo-column.ts +498 -0
  43. package/src/models/reference-column/column-aggregate.ts +313 -0
  44. package/src/models/reference-column/facet-column.ts +1380 -0
  45. package/src/models/reference-column/foreign-key-pseudo-column.ts +626 -0
  46. package/src/models/reference-column/inbound-foreign-key-pseudo-column.ts +131 -0
  47. package/src/models/reference-column/index.ts +13 -0
  48. package/src/models/reference-column/key-pseudo-column.ts +236 -0
  49. package/src/models/reference-column/pseudo-column.ts +850 -0
  50. package/src/models/reference-column/reference-column.ts +740 -0
  51. package/src/models/source-object-node.ts +156 -0
  52. package/src/models/source-object-wrapper.ts +694 -0
  53. package/src/models/table-source-definitions.ts +98 -0
  54. package/src/services/authn.ts +43 -0
  55. package/src/services/catalog.ts +37 -0
  56. package/src/services/config.ts +202 -0
  57. package/src/services/error.ts +247 -0
  58. package/src/services/handlebars.ts +607 -0
  59. package/src/services/history.ts +136 -0
  60. package/src/services/http.ts +536 -0
  61. package/src/services/logger.ts +70 -0
  62. package/src/services/mustache.ts +0 -0
  63. package/src/utils/column-utils.ts +308 -0
  64. package/src/utils/constants.ts +526 -0
  65. package/src/utils/markdown-utils.ts +855 -0
  66. package/src/utils/reference-utils.ts +1658 -0
  67. package/src/utils/template-utils.ts +0 -0
  68. package/src/utils/type-utils.ts +89 -0
  69. package/src/utils/value-utils.ts +127 -0
  70. package/tsconfig.json +30 -0
  71. package/vite.config.mts +104 -0
@@ -0,0 +1,607 @@
1
+ /* eslint-disable no-useless-escape */
2
+ import Handlebars from 'handlebars';
3
+ import moment from 'moment-timezone';
4
+
5
+ import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
6
+
7
+ import { _handlebarsHelpersList } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
8
+ import HistoryService from '@isrd-isi-edu/ermrestjs/src/services/history';
9
+
10
+ import {
11
+ _addErmrestVarsToTemplate,
12
+ _addTemplateVars,
13
+ _escapeMarkdownCharacters,
14
+ _formatUtils,
15
+ _getPath,
16
+ encodeFacet,
17
+ encodeFacetString,
18
+ } from '@isrd-isi-edu/ermrestjs/js/utils/helpers';
19
+ import AuthnService from '@isrd-isi-edu/ermrestjs/src/services/authn';
20
+ import { isObjectAndNotNull } from '@isrd-isi-edu/ermrestjs/src/utils/type-utils';
21
+ import { fixedEncodeURIComponent } from '@isrd-isi-edu/ermrestjs/src/utils/value-utils';
22
+ import printf from '@isrd-isi-edu/ermrestjs/js/format';
23
+ import { type Catalog } from '@isrd-isi-edu/ermrestjs/js/core';
24
+
25
+ export default class HandlebarsService {
26
+ private static _setupDone = false;
27
+ private static _handlebarsHelpersHash: Record<string, boolean> = {};
28
+ // Cache to store all the handlebar templates to reduce compute time
29
+ private static _handlebarsCompiledTemplates: Record<string, HandlebarsTemplateDelegate> = {};
30
+
31
+ static get handlebars() {
32
+ if (!HandlebarsService._setupDone) {
33
+ HandlebarsService._setupDone = true;
34
+
35
+ // inject the custom handlebars
36
+ HandlebarsService._injectCustomHandlebarHelpers();
37
+
38
+ // loop through handlebars defined list of helpers and check against the enum in ermrestJs
39
+ // if not in enum, set helper to false
40
+ // should help defend against new helpers being exposed without us being aware of it
41
+ Object.keys(Handlebars.helpers).forEach(function (key) {
42
+ HandlebarsService._handlebarsHelpersHash[key] = _handlebarsHelpersList.includes(key);
43
+ });
44
+ }
45
+
46
+ return Handlebars;
47
+ }
48
+
49
+ /**
50
+ * @function
51
+ * @public
52
+ * @param template The template string to transform
53
+ * @param keyValues The key-value pair of object to be used for template tags replacement.
54
+ * @param catalog The catalog object created by ermrestJS representing the current catalog from the url
55
+ * @param options Configuration options.
56
+ * @return {string} A string produced after templating
57
+ * @desc Calls the private function to return a string produced as a result of templating using `Handlebars`.
58
+ */
59
+ static render(template: string, keyValues: Record<string, any>, catalog: Catalog | { id: string }, options: any): string | null {
60
+ options = options || {};
61
+
62
+ const obj = _addTemplateVars(keyValues, catalog, options);
63
+ let content, _compiledTemplate;
64
+
65
+ // If we should validate, validate the template and if returns false, return null.
66
+ if (!options.avoidValidation && !HandlebarsService.validate(template, obj, catalog)) {
67
+ return null;
68
+ }
69
+
70
+ try {
71
+ // Read template from cache
72
+ _compiledTemplate = HandlebarsService._handlebarsCompiledTemplates[template];
73
+
74
+ // If template not found then add it to cache
75
+ if (!_compiledTemplate) {
76
+ const compileOptions = {
77
+ knownHelpersOnly: true,
78
+ knownHelpers: HandlebarsService._handlebarsHelpersHash,
79
+ };
80
+
81
+ HandlebarsService._handlebarsCompiledTemplates[template] = _compiledTemplate = HandlebarsService.handlebars.compile(template, compileOptions);
82
+ }
83
+
84
+ // Generate content from the template
85
+ content = _compiledTemplate(obj);
86
+ } catch (e) {
87
+ $log.error(e);
88
+ content = null;
89
+ }
90
+
91
+ return content;
92
+ }
93
+
94
+ /**
95
+ * Returns true if all the used keys have values.
96
+ *
97
+ * NOTE:
98
+ * This implementation is very limited and if conditional Handlebar statements
99
+ * of the form {{#if }}{{/if}} or {{^if VARNAME}}{{/if}} or {{#unless VARNAME}}{{/unless}} or {{^unless }}{{/unless}} found then it won't check
100
+ * for null values and will return true.s
101
+ *
102
+ * @param template mustache template
103
+ * @param keyValues key-value pairs
104
+ * @param catalog the catalog object
105
+ * @param ignoredColumns the columns that should be ignored (optional)
106
+ * @return true if all the used keys have values
107
+ */
108
+ static validate(template: string, keyValues: Record<string, any>, catalog: any, ignoredColumns?: string[]): boolean {
109
+ const conditionalRegex = /\{\{(((#|\^)([^\{\}]+))|(if|unless|else))([^\{\}]+)\}\}/;
110
+ let i, key, value;
111
+
112
+ // Inject ermrest internal utility objects such as date
113
+ // needs to be done in the case _validateTemplate is called without first calling _renderTemplate
114
+ _addErmrestVarsToTemplate(keyValues, catalog);
115
+
116
+ // If no conditional handlebars statements of the form {{#if VARNAME}}{{/if}} or {{^if VARNAME}}{{/if}} or {{#unless VARNAME}}{{/unless}} or {{^unless VARNAME}}{{/unless}} not found then do direct null check
117
+ if (!conditionalRegex.exec(template)) {
118
+ // Grab all placeholders ({{PROP_NAME}}) in the template
119
+ const placeholders = template.match(/\{\{([^\{\}\(\)\s]+)\}\}/gi);
120
+
121
+ // These will match the placeholders that are encapsulated in square brackets {{[string with space]}} or {{{[string with space]}}}
122
+ const specialPlaceholders = template.match(/\{\{((\[[^\{\}]+\])|(\{\[[^\{\}]+\]\}))\}\}/gi);
123
+
124
+ // If there are any placeholders
125
+ if (placeholders && placeholders.length) {
126
+ // Get unique placeholders
127
+ const uniquePlaceholders = placeholders.filter(function (item, i, ar) {
128
+ return ar.indexOf(item) === i && item !== 'else';
129
+ });
130
+
131
+ /*
132
+ * Iterate over all placeholders to set pattern as null if any of the
133
+ * values turn out to be null or undefined
134
+ */
135
+ for (i = 0; i < uniquePlaceholders.length; i++) {
136
+ // Grab actual key from the placeholder {{name}} = name, remove "{{" and "}}" from the string for key
137
+ key = uniquePlaceholders[i].substring(2, uniquePlaceholders[i].length - 2);
138
+
139
+ if (key[0] == '{') key = key.substring(1, key.length - 1);
140
+
141
+ // find the value.
142
+ value = _getPath(keyValues, key.trim());
143
+
144
+ // TODO since we're not going inside the object this logic of ignoredColumns is not needed anymore,
145
+ // it was a hack that was added for asset columns.
146
+ // If key is not in ingored columns value for the key is null or undefined then return null
147
+ if ((!Array.isArray(ignoredColumns) || ignoredColumns.indexOf(key) === -1) && (value === null || value === undefined)) {
148
+ return false;
149
+ }
150
+ }
151
+ }
152
+
153
+ // If there are any placeholders
154
+ if (specialPlaceholders && specialPlaceholders.length) {
155
+ // Get unique placeholders
156
+ const uniqueSpecialPlaceholders = specialPlaceholders.filter(function (item, i, ar) {
157
+ return ar.indexOf(item) === i && item !== 'else';
158
+ });
159
+
160
+ /*
161
+ * Iterate over all specialPlaceholders to set pattern as null if any of the
162
+ * values turn out to be null or undefined
163
+ */
164
+ for (i = 0; i < uniqueSpecialPlaceholders.length; i++) {
165
+ // Grab actual key from the placeholder {{name}} = name, remove "{{" and "}}" from the string for key
166
+ key = uniqueSpecialPlaceholders[i].substring(2, uniqueSpecialPlaceholders[i].length - 2);
167
+
168
+ if (key[0] == '{') key = key.substring(1, key.length - 1);
169
+
170
+ // Remove [] from the key {{[name]}} = name, remove "[" and "]" from the string for key
171
+ key = key.substring(1, key.length - 1);
172
+
173
+ // find the value.
174
+ value = _getPath(keyValues, key.trim());
175
+
176
+ // TODO since we're not going inside the object this logic of ignoredColumns is not needed anymore,
177
+ // it was a hack that was added for asset columns.
178
+ // If key is not in ingored columns value for the key is null or undefined then return null
179
+ if ((!Array.isArray(ignoredColumns) || ignoredColumns.indexOf(key) === -1) && (value === null || value === undefined)) {
180
+ return false;
181
+ }
182
+ }
183
+ }
184
+ }
185
+ return true;
186
+ }
187
+
188
+ private static _injectCustomHandlebarHelpers() {
189
+ // general purpose helpers
190
+ Handlebars.registerHelper({
191
+ /**
192
+ * escape markdown characters
193
+ * @ignore
194
+ * @returns escaped characeters
195
+ */
196
+ escape: function (...args) {
197
+ // last argument is options object provided by handlebars
198
+ const text = args.splice(0, args.length - 1).join('');
199
+ return _escapeMarkdownCharacters(text);
200
+ },
201
+
202
+ /**
203
+ * @ignore
204
+ * @returns url-encoded string
205
+ */
206
+ encode: function (...args) {
207
+ const text = args.splice(0, args.length - 1).join('');
208
+ return fixedEncodeURIComponent(text);
209
+ },
210
+
211
+ /**
212
+ * {{#encodeFacet}}
213
+ * str
214
+ * {{/encodeFacet}}
215
+ *
216
+ * or
217
+ *
218
+ * {{encodeFacet obj}}
219
+ *
220
+ * or
221
+ *
222
+ * {{encodeFacet str}}
223
+ *
224
+ * This is order of checking syntax (first applicaple rule):
225
+ * - first see if the block syntax with str inside it is used or not
226
+ * - if the input is an object we will encode it
227
+ * - try encoding as string (will return empty string if it wasn't a string)
228
+ * @ignore
229
+ * @returns encoded facet string that can be used in url
230
+ */
231
+ encodeFacet: function (options: Handlebars.HelperOptions | string | any) {
232
+ try {
233
+ return encodeFacetString(options.fn(this));
234
+ } catch {
235
+ if (isObjectAndNotNull(options)) {
236
+ return encodeFacet(options);
237
+ }
238
+ }
239
+ return encodeFacetString(options);
240
+ },
241
+
242
+ /**
243
+ * {{printf value "%4d" }}
244
+ * @ignore
245
+ */
246
+ printf: function (value, format) {
247
+ return printf({ format: format }, value);
248
+ },
249
+
250
+ /**
251
+ * {{formatDatetime value format}}
252
+ * @ignore
253
+ * @returns formatted string of `value` with corresponding `format`
254
+ */
255
+ formatDatetime: function (value, format) {
256
+ const m = moment(value);
257
+ // if we don't validate, it will return "invalid date"
258
+ if (m.isValid()) {
259
+ return m.format(format);
260
+ }
261
+ return '';
262
+ },
263
+
264
+ /**
265
+ * {{formatDate value format}}
266
+ * @deprecated use formatDatetime instead
267
+ * @returns formatted string of `value` with corresponding `format`
268
+ */
269
+ formatDate: function (value, format) {
270
+ const m = moment(value);
271
+ // if we don't validate, it will return "invalid date"
272
+ if (m.isValid()) {
273
+ return m.format(format);
274
+ }
275
+ return '';
276
+ },
277
+
278
+ /**
279
+ * {{#jsonStringify}}
280
+ * JSON Object
281
+ * {{/jsonStringify}}
282
+ *
283
+ * or
284
+ *
285
+ * {{#jsonStringify obj}}{{/jsonStringify}}
286
+ * @ignore
287
+ * @returns string representation of the given JSON object
288
+ */
289
+ jsonStringify: function (options) {
290
+ try {
291
+ return JSON.stringify(options.fn(this));
292
+ } catch {
293
+ return JSON.stringify(options);
294
+ }
295
+ },
296
+
297
+ /**
298
+ * {{#replace substr newSubstr}}
299
+ * string
300
+ * {{/replace}}
301
+ *
302
+ * {{replace value regexp flags="ig"}}
303
+ * @ignore
304
+ * @returns replaces each match of the regexp with newSubstr
305
+ */
306
+ replace: function (substr: string, newSubstr: string, options) {
307
+ let flags = 'g';
308
+ if (options && isObjectAndNotNull(options.hash) && typeof options.hash.flags === 'string') {
309
+ flags = options.hash.flags;
310
+ }
311
+ const regexpObj = new RegExp(substr, flags);
312
+ return options.fn(this).replace(regexpObj, newSubstr);
313
+ },
314
+
315
+ /**
316
+ * {{#if (regexMatch value regexp)}}
317
+ * .. content
318
+ * {{/if}}
319
+ *
320
+ * {{regexMatch value regexp flags="i"}}
321
+ * @ignore
322
+ * @returns boolean if the value matches the regexp
323
+ */
324
+ regexMatch: function (value, regexp, options) {
325
+ let flags = 'g';
326
+ if (options && isObjectAndNotNull(options.hash) && typeof options.hash.flags === 'string') {
327
+ flags = options.hash.flags;
328
+ }
329
+ const regexpObj = new RegExp(regexp, flags);
330
+ return regexpObj.test(value);
331
+ },
332
+
333
+ /**
334
+ * {{#each (regexFindFirst value regexp)}}
335
+ * {{this}}
336
+ * {{/each}}
337
+ *
338
+ * {{regexFindFirst value regexp flags="i"}}
339
+ * @ignore
340
+ * @returns first string from value that matches the regular expression or empty string
341
+ */
342
+ regexFindFirst: function (value, regexp, options) {
343
+ let flags = 'g';
344
+ if (options && isObjectAndNotNull(options.hash) && typeof options.hash.flags === 'string') {
345
+ flags = options.hash.flags;
346
+ }
347
+ const matches = regexpFindAll(value, regexp, flags);
348
+ return (matches && matches[0]) || '';
349
+ },
350
+
351
+ /**
352
+ * {{#each (regexFindAll value regexp)}}
353
+ * {{this}}
354
+ * {{/each}}
355
+ *
356
+ * {{regexFindFirst value regexp flags="ig"}}
357
+ * @ignore
358
+ * @returns array of strings from value that match the regular expression or
359
+ */
360
+ regexFindAll: function (value: string, regexp: string, options?: Handlebars.HelperOptions) {
361
+ let flags = 'g';
362
+ if (options && isObjectAndNotNull(options.hash) && typeof options.hash.flags === 'string') {
363
+ flags = options.hash.flags;
364
+ }
365
+ return regexpFindAll(value, regexp, flags) || [];
366
+ },
367
+
368
+ /**
369
+ * {{#toTitleCase}}
370
+ * string
371
+ * {{/toTitleCase}}
372
+ * @ignore
373
+ * @returns string representation of the given JSON object
374
+ */
375
+ toTitleCase: function (options: Handlebars.HelperOptions) {
376
+ const str = options.fn(this);
377
+ // \w matches any word character
378
+ // \S matches any non-whitespace character
379
+ return str.replace(/\w\S*/g, function (txt) {
380
+ return txt.charAt(0).toUpperCase() + txt.substr(1);
381
+ });
382
+ },
383
+
384
+ /**
385
+ * {{humanizeBytes value }}
386
+ * {{humanizeBytes value mode='si' }}
387
+ * {{humanizeBytes value precision=4}}
388
+ * {{humanizeBytes value tooltip=true }}
389
+ * @ignore
390
+ * @returns formatted string of `value` with corresponding `mode`
391
+ */
392
+ humanizeBytes: function (value: number, options: Handlebars.HelperOptions) {
393
+ let mode, precision, tooltip;
394
+ if (options && isObjectAndNotNull(options.hash)) {
395
+ mode = options.hash.mode;
396
+ precision = options.hash.precision;
397
+ tooltip = options.hash.tooltip;
398
+ }
399
+ return _formatUtils.humanizeBytes(value, mode, precision, tooltip);
400
+ },
401
+
402
+ /**
403
+ * {{stringLength value }}
404
+ * @ignore
405
+ * @returns the length of the given string
406
+ */
407
+ stringLength: function (value: string) {
408
+ return value.length;
409
+ },
410
+
411
+ /**
412
+ * {{isUserInAcl group1 group2 }}}
413
+ * {{isUserInAcl groupArray }}
414
+ * {{isUserInAcl "https:/some-group" "https://another-group" }}
415
+ *
416
+ * @returns a boolean indicating if the user is in any of the given groups
417
+ */
418
+ isUserInAcl: function (...args) {
419
+ const groups = args.reduce((acc, arg) => {
420
+ if (Array.isArray(arg)) {
421
+ return acc.concat(arg);
422
+ } else if (typeof arg === 'string') {
423
+ return acc.concat([arg]);
424
+ }
425
+ return acc;
426
+ }, []);
427
+
428
+ return AuthnService.isUserInAcl(groups);
429
+ },
430
+
431
+ /**
432
+ * {{dateTimeToSnapshot value}}
433
+ */
434
+ datetimeToSnapshot: function (value: string) {
435
+ const m = moment(value);
436
+ // if we don't validate, it will return "invalid date"
437
+ if (m.isValid()) {
438
+ try {
439
+ return HistoryService.datetimeISOToSnapshot(m.toISOString());
440
+ } catch (e) {
441
+ $log.error(`error while converting ${value} to snapshot`);
442
+ $log.error(e);
443
+ return '';
444
+ }
445
+ } else {
446
+ $log.error(`invalid timestamp passed to datetimeToSnapshot: ${value}`);
447
+ }
448
+ return '';
449
+ },
450
+
451
+ /**
452
+ * {{snapshotToDatetime value}}
453
+ * {{snapshotToDatetime value 'YYYY-MM-DD HH:mm:ss'}}
454
+ */
455
+ snapshotToDatetime: function (value: string, format?: string) {
456
+ try {
457
+ const iso = HistoryService.snapshotToDatetimeISO(value);
458
+ if (typeof format === 'string') {
459
+ return moment(iso).format(format);
460
+ }
461
+ return iso;
462
+ } catch (e) {
463
+ $log.error(`error in snapshotToDatetime while converting ${value} to ISO`);
464
+ $log.error(e);
465
+ return '';
466
+ }
467
+ },
468
+ });
469
+
470
+ // compare helpers
471
+ Handlebars.registerHelper({
472
+ /*
473
+ *{{#if (eq val1 val2)}}
474
+ * .. content
475
+ *{{/if}}
476
+ */
477
+ eq: function (...args) {
478
+ return reduceOp(args, (a, b) => a === b);
479
+ },
480
+ /*
481
+ *{{#if (ne val1 val2)}}
482
+ * .. content
483
+ *{{/if}}
484
+ */
485
+ ne: function (...args) {
486
+ return reduceOp(args, (a, b) => a !== b);
487
+ },
488
+ /*
489
+ *{{#if (lt val1 val2)}}
490
+ * .. content
491
+ *{{/if}}
492
+ */
493
+ lt: function (...args) {
494
+ return reduceOp(args, (a, b) => a < b);
495
+ },
496
+ /*
497
+ *{{#if (gt val1 val2)}}
498
+ * .. content
499
+ *{{/if}}
500
+ */
501
+ gt: function (...args) {
502
+ return reduceOp(args, (a, b) => a > b);
503
+ },
504
+ /*
505
+ *{{#if (lte val1 val2)}}
506
+ * .. content
507
+ *{{/if}}
508
+ */
509
+ lte: function (...args) {
510
+ return reduceOp(args, (a, b) => a <= b);
511
+ },
512
+ /*
513
+ *{{#if (gte val1 val2)}}
514
+ * .. content
515
+ *{{/if}}
516
+ */
517
+ gte: function (...args) {
518
+ return reduceOp(args, (a, b) => a >= b);
519
+ },
520
+ /*
521
+ *{{#if (and section1 section2)}}
522
+ * .. content
523
+ *{{/if}}
524
+ */
525
+ and: function (...args) {
526
+ return reduceOp(args, (a, b) => a && b);
527
+ },
528
+ /*
529
+ *{{#if (or section1 section2)}}
530
+ * .. content
531
+ *{{/if}}
532
+ */
533
+ or: function (...args) {
534
+ return reduceOp(args, (a: boolean, b: boolean) => a || b);
535
+ },
536
+ /*
537
+ *{{#if (not section1)}}
538
+ * .. content
539
+ *{{/if}}
540
+ */
541
+ not: function (a) {
542
+ return !a;
543
+ },
544
+ /*
545
+ *{{#ifCond value "===" value2}}
546
+ * Values are equal!
547
+ *{{else}}
548
+ * Values are different!
549
+ *{{/ifCond}}
550
+ */
551
+ ifCond: function (v1, operator: string, v2, options: Handlebars.HelperOptions) {
552
+ switch (operator) {
553
+ case '==':
554
+ // eslint-disable-next-line eqeqeq
555
+ return v1 == v2 ? options.fn(this) : options.inverse(this);
556
+ case '===':
557
+ return v1 === v2 ? options.fn(this) : options.inverse(this);
558
+ case '!=':
559
+ // eslint-disable-next-line eqeqeq
560
+ return v1 != v2 ? options.fn(this) : options.inverse(this);
561
+ case '!==':
562
+ return v1 !== v2 ? options.fn(this) : options.inverse(this);
563
+ case '<':
564
+ return v1 < v2 ? options.fn(this) : options.inverse(this);
565
+ case '<=':
566
+ return v1 <= v2 ? options.fn(this) : options.inverse(this);
567
+ case '>':
568
+ return v1 > v2 ? options.fn(this) : options.inverse(this);
569
+ case '>=':
570
+ return v1 >= v2 ? options.fn(this) : options.inverse(this);
571
+ case '&&':
572
+ return v1 && v2 ? options.fn(this) : options.inverse(this);
573
+ case '||':
574
+ return v1 || v2 ? options.fn(this) : options.inverse(this);
575
+ default:
576
+ return options.inverse(this);
577
+ }
578
+ },
579
+ });
580
+
581
+ // math helpers
582
+ Handlebars.registerHelper({
583
+ add: function (arg1: unknown, arg2: unknown) {
584
+ return Number(arg1) + Number(arg2);
585
+ },
586
+
587
+ subtract: function (arg1: unknown, arg2: unknown) {
588
+ return Number(arg1) - Number(arg2);
589
+ },
590
+ });
591
+ }
592
+ }
593
+
594
+ // allows recursive support of the given reducer function to be applied to args
595
+ const reduceOp = function (args: any[], reducer: (a: boolean, b: boolean) => boolean) {
596
+ args = Array.from(args);
597
+ args.pop(); // => options
598
+ const first = args.shift();
599
+ return args.reduce(reducer, first);
600
+ };
601
+
602
+ const regexpFindAll = function (value: string, regexp: string, flags: string) {
603
+ const regexpObj = new RegExp(regexp, flags);
604
+ const matches = value.match(regexpObj);
605
+
606
+ return matches;
607
+ };