@gesslar/toolkit 0.7.1 → 1.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 (53) hide show
  1. package/README.md +35 -0
  2. package/package.json +22 -10
  3. package/src/browser/index.js +10 -0
  4. package/src/{lib → browser/lib}/Data.js +24 -0
  5. package/src/browser/lib/Sass.js +168 -0
  6. package/src/browser/lib/Tantrum.js +115 -0
  7. package/src/browser/lib/Util.js +257 -0
  8. package/src/browser/lib/Valid.js +76 -0
  9. package/src/index.js +14 -12
  10. package/src/lib/Cache.js +2 -3
  11. package/src/lib/Contract.js +3 -4
  12. package/src/lib/FS.js +15 -20
  13. package/src/lib/FileObject.js +71 -14
  14. package/src/lib/Glog.js +2 -2
  15. package/src/lib/Sass.js +2 -91
  16. package/src/lib/Schemer.js +2 -2
  17. package/src/lib/Tantrum.js +3 -70
  18. package/src/lib/Terms.js +2 -3
  19. package/src/lib/Util.js +2 -252
  20. package/src/lib/Valid.js +17 -20
  21. package/src/types/browser/index.d.ts +8 -0
  22. package/src/types/browser/index.d.ts.map +1 -0
  23. package/src/types/browser/lib/Collection.d.ts +246 -0
  24. package/src/types/browser/lib/Collection.d.ts.map +1 -0
  25. package/src/types/browser/lib/Data.d.ts +206 -0
  26. package/src/types/browser/lib/Data.d.ts.map +1 -0
  27. package/src/types/browser/lib/Sass.d.ts +62 -0
  28. package/src/types/browser/lib/Sass.d.ts.map +1 -0
  29. package/src/types/browser/lib/Tantrum.d.ts +51 -0
  30. package/src/types/browser/lib/Tantrum.d.ts.map +1 -0
  31. package/src/types/browser/lib/TypeSpec.d.ts +92 -0
  32. package/src/types/browser/lib/TypeSpec.d.ts.map +1 -0
  33. package/src/types/browser/lib/Util.d.ts +129 -0
  34. package/src/types/browser/lib/Util.d.ts.map +1 -0
  35. package/src/types/browser/lib/Valid.d.ts +33 -0
  36. package/src/types/browser/lib/Valid.d.ts.map +1 -0
  37. package/src/types/index.d.ts +10 -10
  38. package/src/types/lib/Data.d.ts +17 -0
  39. package/src/types/lib/Data.d.ts.map +1 -1
  40. package/src/types/lib/FS.d.ts +2 -2
  41. package/src/types/lib/FS.d.ts.map +1 -1
  42. package/src/types/lib/FileObject.d.ts +31 -1
  43. package/src/types/lib/FileObject.d.ts.map +1 -1
  44. package/src/types/lib/Sass.d.ts +2 -55
  45. package/src/types/lib/Sass.d.ts.map +1 -1
  46. package/src/types/lib/Tantrum.d.ts +3 -44
  47. package/src/types/lib/Tantrum.d.ts.map +1 -1
  48. package/src/types/lib/Util.d.ts +3 -124
  49. package/src/types/lib/Util.d.ts.map +1 -1
  50. package/src/types/lib/Valid.d.ts +1 -1
  51. package/src/types/lib/Valid.d.ts.map +1 -1
  52. /package/src/{lib → browser/lib}/Collection.js +0 -0
  53. /package/src/{lib → browser/lib}/TypeSpec.js +0 -0
package/README.md CHANGED
@@ -7,6 +7,41 @@ but the kind that says "yumyum."
7
7
  There are file and directory abstractions, uhmm, there's also some terminal
8
8
  things and validity checkers, lots of data functions.
9
9
 
10
+ ## Usage
11
+
12
+ **For Node.js projects:**
13
+
14
+ ```javascript
15
+ import { Data, FileObject, Cache } from '@gesslar/toolkit'
16
+ // or explicitly:
17
+ import { Data, FileObject } from '@gesslar/toolkit/node'
18
+ ```
19
+
20
+ **For browsers or Tauri apps:**
21
+
22
+ ```javascript
23
+ import { Data, Collection, Util } from '@gesslar/toolkit/browser'
24
+ ```
25
+
26
+ The browser version includes: Data, Collection, Util, Type (TypeSpec), Valid,
27
+ Sass, and Tantrum. Node-only modules (FileObject, Cache, FS, etc.) are not
28
+ available in the browser version.
29
+
30
+ **Browser via CDN (no install):**
31
+
32
+ - jsDelivr (runtime only):
33
+ `https://cdn.jsdelivr.net/npm/@gesslar/toolkit/browser`
34
+ - esm.sh (runtime + optional types):
35
+ `https://esm.sh/@gesslar/toolkit@0.8.0/browser`
36
+ `https://esm.sh/@gesslar/toolkit@0.8.0/browser?dts` (serves `.d.ts` for editors)
37
+
38
+ Notes:
39
+
40
+ - Nothing to configure in this repo for CDN use; both URLs just work.
41
+ - TypeScript editors do not pick up types from jsDelivr. If you want inline
42
+ types without installing from npm, use the esm.sh `?dts` URL or install the
43
+ package locally for development and use the CDN at runtime.
44
+
10
45
  Basically, if you want it, it is most definitely here, and working 100% and
11
46
  absolutely none of that is true. There are only a few classes here, but they're
12
47
  pretty. And if you bug-shame them, I will _come for you like_ ...
package/package.json CHANGED
@@ -1,11 +1,21 @@
1
1
  {
2
2
  "name": "@gesslar/toolkit",
3
- "version": "0.7.1",
3
+ "version": "1.0.0",
4
4
  "description": "Get in, bitches, we're going toolkitting.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
7
7
  "exports": {
8
8
  ".": {
9
+ "types": "./src/types/index.d.ts",
10
+ "browser": "./src/browser/index.js",
11
+ "node": "./src/index.js",
12
+ "default": "./src/index.js"
13
+ },
14
+ "./browser": {
15
+ "types": "./src/types/index.d.ts",
16
+ "default": "./src/browser/index.js"
17
+ },
18
+ "./node": {
9
19
  "types": "./src/types/index.d.ts",
10
20
  "default": "./src/index.js"
11
21
  }
@@ -17,7 +27,7 @@
17
27
  ],
18
28
  "sideEffects": false,
19
29
  "engines": {
20
- "node": ">=22"
30
+ "node": ">=20"
21
31
  },
22
32
  "scripts": {
23
33
  "types:build": "tsc -p tsconfig.types.json",
@@ -25,7 +35,7 @@
25
35
  "lint:fix": "eslint src/ --fix",
26
36
  "submit": "npm publish --access public",
27
37
  "update": "npx npm-check-updates -u && npm install",
28
- "test": "node --test tests/unit/*.test.js",
38
+ "test": "node --test tests/**/*.test.js",
29
39
  "pr": "gt submit --publish --restack --ai"
30
40
  },
31
41
  "repository": {
@@ -52,16 +62,18 @@
52
62
  "dependencies": {
53
63
  "@gesslar/colours": "^0.0.1",
54
64
  "ajv": "^8.17.1",
55
- "globby": "^15.0.0",
65
+ "globby": "^16.0.0",
56
66
  "json5": "^2.2.3",
57
67
  "yaml": "^2.8.1"
58
68
  },
59
69
  "devDependencies": {
60
- "@stylistic/eslint-plugin": "^5.5.0",
61
- "@types/node": "^24.9.1",
62
- "@typescript-eslint/eslint-plugin": "^8.46.2",
63
- "@typescript-eslint/parser": "^8.46.2",
64
- "eslint": "^9.38.0",
65
- "eslint-plugin-jsdoc": "^61.1.8"
70
+ "@gesslar/uglier": "^0.0.5",
71
+ "@stylistic/eslint-plugin": "^5.6.1",
72
+ "@types/node": "^24.10.1",
73
+ "@typescript-eslint/eslint-plugin": "^8.48.0",
74
+ "@typescript-eslint/parser": "^8.48.0",
75
+ "eslint": "^9.39.1",
76
+ "eslint-plugin-jsdoc": "^61.4.1",
77
+ "globals": "^16.5.0"
66
78
  }
67
79
  }
@@ -0,0 +1,10 @@
1
+ // Browser-compatible utilities
2
+ // Pure JavaScript modules that work in browsers, Tauri, and Node.js
3
+
4
+ export {default as Collection} from "./lib/Collection.js"
5
+ export {default as Data} from "./lib/Data.js"
6
+ export {default as Sass} from "./lib/Sass.js"
7
+ export {default as Tantrum} from "./lib/Tantrum.js"
8
+ export {default as Type} from "./lib/TypeSpec.js"
9
+ export {default as Util} from "./lib/Util.js"
10
+ export {default as Valid} from "./lib/Valid.js"
@@ -410,4 +410,28 @@ export default class Data {
410
410
 
411
411
  return proto === current
412
412
  }
413
+
414
+ /**
415
+ * Checks if a value is binary data.
416
+ * Returns true for ArrayBuffer, TypedArrays (Uint8Array, Int16Array, etc.),
417
+ * Blob, and Node Buffer instances.
418
+ *
419
+ * @param {unknown} value - The value to check
420
+ * @returns {boolean} True if the value is binary data, false otherwise
421
+ * @example
422
+ * Data.isBinary(new Uint8Array([1, 2, 3])) // true
423
+ * Data.isBinary(new ArrayBuffer(10)) // true
424
+ * Data.isBinary(Buffer.from('hello')) // true
425
+ * Data.isBinary(new Blob(['text'])) // true
426
+ * Data.isBinary('string') // false
427
+ * Data.isBinary({}) // false
428
+ * Data.isBinary(undefined) // false
429
+ */
430
+ static isBinary(value) {
431
+ return (value !== undefined) &&
432
+ (
433
+ ArrayBuffer.isView(value) ||
434
+ Data.isType(value, "ArrayBuffer|Blob|Buffer")
435
+ )
436
+ }
413
437
  }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * @file Sass.js
3
+ *
4
+ * Defines the Sass class, a custom error type for toolkit compilation
5
+ * errors.
6
+ *
7
+ * Supports error chaining, trace management, and formatted reporting for both
8
+ * user-friendly and verbose (nerd) output.
9
+ *
10
+ * Used throughout the toolkit for structured error handling and
11
+ * debugging.
12
+ */
13
+
14
+ import Tantrum from "./Tantrum.js"
15
+
16
+ /**
17
+ * Custom error class for toolkit errors.
18
+ * Provides error chaining, trace management, and formatted error reporting.
19
+ */
20
+ export default class Sass extends Error {
21
+ #trace = []
22
+
23
+ /**
24
+ * Creates a new Sass instance.
25
+ *
26
+ * @param {string} message - The error message
27
+ * @param {...unknown} [arg] - Additional arguments passed to parent Error constructor
28
+ */
29
+ constructor(message, ...arg) {
30
+ super(message, ...arg)
31
+
32
+ this.trace = message
33
+ }
34
+
35
+ /**
36
+ * Gets the error trace array.
37
+ *
38
+ * @returns {Array<string>} Array of trace messages
39
+ */
40
+ get trace() {
41
+ return this.#trace
42
+ }
43
+
44
+ /**
45
+ * Adds a message to the beginning of the trace array.
46
+ *
47
+ * @param {string} message - The trace message to add
48
+ */
49
+ set trace(message) {
50
+ this.#trace.unshift(message)
51
+ }
52
+
53
+ /**
54
+ * Adds a trace message and returns this instance for chaining.
55
+ *
56
+ * @param {string} message - The trace message to add
57
+ * @returns {this} This Sass instance for method chaining
58
+ */
59
+ addTrace(message) {
60
+ if(typeof message !== "string")
61
+ throw this.constructor.new(`Sass.addTrace expected string, got ${JSON.stringify(message)}`)
62
+
63
+ this.trace = message
64
+
65
+ return this
66
+ }
67
+
68
+ /**
69
+ * Reports the error to the console with formatted output.
70
+ * Optionally includes detailed stack trace information.
71
+ *
72
+ * @param {boolean} [nerdMode] - Whether to include detailed stack trace
73
+ */
74
+ report(nerdMode=false) {
75
+ console.error(
76
+ `[error] Something Went Wrong\n` +
77
+ this.trace.join("\n")
78
+ )
79
+
80
+ if(nerdMode) {
81
+ console.error(
82
+ "\n" +
83
+ `[error] Nerd Vittles\n` +
84
+ this.#fullBodyMassage(this.stack)
85
+ )
86
+
87
+ this.cause?.stack && console.error(
88
+ "\n" +
89
+ `[error] Rethrown From\n` +
90
+ this.#fullBodyMassage(this.cause?.stack)
91
+ )
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Formats the stack trace for display, removing the first line and
97
+ * formatting each line with appropriate indentation.
98
+ *
99
+ * Note: Returns formatted stack trace or undefined if no stack available.
100
+ *
101
+ * @param {string} stack - The error stack to massage.
102
+ * @returns {string|undefined} Formatted stack trace or undefined
103
+ */
104
+ #fullBodyMassage(stack) {
105
+ stack = stack ?? ""
106
+ // Remove the first line, it's already been reported
107
+ const {rest} = stack.match(/^.*?\n(?<rest>[\s\S]+)$/m)?.groups ?? {}
108
+ const lines = []
109
+
110
+ if(rest) {
111
+ lines.push(
112
+ ...rest
113
+ .split("\n")
114
+ .map(line => {
115
+ const at = line.match(/^\s{4}at\s(?<at>.*)$/)?.groups?.at ?? ""
116
+
117
+ return at
118
+ ? `* ${at}`
119
+ : line
120
+ })
121
+ )
122
+ }
123
+
124
+ return lines.join("\n")
125
+ }
126
+
127
+ /**
128
+ * Creates an Sass from an existing Error object with additional
129
+ * trace message.
130
+ *
131
+ * @param {Error} error - The original error object
132
+ * @param {string} message - Additional trace message to add
133
+ * @returns {Sass} New Sass instance with trace from the original error
134
+ * @throws {Sass} If the first parameter is not an Error instance
135
+ */
136
+ static from(error, message) {
137
+ if(!(error instanceof Error))
138
+ throw this.new("Sass.from must take an Error object.")
139
+
140
+ const oldMessage = error.message
141
+ const newError = new this(oldMessage, {cause: error}).addTrace(message)
142
+
143
+ return newError
144
+ }
145
+
146
+ /**
147
+ * Factory method to create or enhance Sass instances.
148
+ * If error parameter is provided, enhances existing Sass or wraps
149
+ * other errors. Otherwise creates a new Sass instance.
150
+ *
151
+ * @param {string} message - The error message
152
+ * @param {Error|Sass|Tantrum} [error] - Optional existing error to wrap or enhance
153
+ * @returns {Sass} New or enhanced Sass instance
154
+ */
155
+ static new(message, error) {
156
+ if(error) {
157
+ if(error instanceof Tantrum)
158
+ return Tantrum.new(message, error)
159
+
160
+ return error instanceof this
161
+ ? error.addTrace(message)
162
+ : this.from(error, message)
163
+ } else {
164
+
165
+ return new this(message)
166
+ }
167
+ }
168
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @file Tantrum.js
3
+ *
4
+ * Defines the Tantrum class, a custom AggregateError type for toolkit
5
+ * that collects multiple errors with Sass-style reporting.
6
+ *
7
+ * Auto-wraps plain Error objects in Sass instances while preserving
8
+ * existing Sass errors, providing consistent formatted output for
9
+ * multiple error scenarios.
10
+ */
11
+
12
+ import Sass from "./Sass.js"
13
+
14
+ /**
15
+ * Custom aggregate error class that extends AggregateError.
16
+ * Automatically wraps plain errors in Sass instances for consistent reporting.
17
+ */
18
+ export default class Tantrum extends AggregateError {
19
+ #trace = []
20
+ #sass
21
+
22
+ /**
23
+ * Creates a new Tantrum instance.
24
+ *
25
+ * @param {string} message - The aggregate error message
26
+ * @param {Array<Error|Sass>} errors - Array of errors to aggregate
27
+ * @param {Sass} sass - Sass constructor
28
+ */
29
+ constructor(message, errors = [], sass=Sass) {
30
+ // Auto-wrap plain errors in Sass, keep existing Sass instances
31
+ const wrappedErrors = errors.map(error => {
32
+ if(error instanceof sass)
33
+ return error
34
+
35
+ if(!(error instanceof Error))
36
+ throw new TypeError(`All items in errors array must be Error instances, got: ${typeof error}`)
37
+
38
+ return sass.new(error.message, error)
39
+ })
40
+
41
+ super(wrappedErrors, message)
42
+
43
+ this.name = "Tantrum"
44
+ this.#sass = sass
45
+ }
46
+
47
+ /**
48
+ * Adds a trace message and returns this instance for chaining.
49
+ *
50
+ * @param {string} message - The trace message to add
51
+ * @param {Error|Sass} [_error] - Optional error (currently unused, reserved for future use)
52
+ * @returns {this} This Tantrum instance for method chaining
53
+ */
54
+ addTrace(message, _error) {
55
+ if(typeof message !== "string")
56
+ throw this.#sass.new(`Tantrum.addTrace expected string, got ${JSON.stringify(message)}`)
57
+
58
+ this.trace = message
59
+
60
+ return this
61
+ }
62
+
63
+ /**
64
+ * Gets the error trace array.
65
+ *
66
+ * @returns {Array<string>} Array of trace messages
67
+ */
68
+ get trace() {
69
+ return this.#trace
70
+ }
71
+
72
+ /**
73
+ * Adds a message to the beginning of the trace array.
74
+ *
75
+ * @param {string} message - The trace message to add
76
+ */
77
+ set trace(message) {
78
+ this.#trace.unshift(message)
79
+ }
80
+
81
+ /**
82
+ * Reports all aggregated errors to the console with formatted output.
83
+ *
84
+ * @param {boolean} [nerdMode] - Whether to include detailed stack traces
85
+ */
86
+ report(nerdMode = false) {
87
+ console.error(
88
+ `[error] Tantrum Incoming (${this.errors.length} errors)\n` +
89
+ this.message
90
+ )
91
+
92
+ if(this.trace)
93
+ console.error(this.trace.join("\n"))
94
+
95
+ console.error()
96
+
97
+ this.errors.forEach(error => {
98
+ error.report(nerdMode)
99
+ })
100
+ }
101
+
102
+ /**
103
+ * Factory method to create a Tantrum instance.
104
+ *
105
+ * @param {string} message - The aggregate error message
106
+ * @param {Array<Error|Sass>} errors - Array of errors to aggregate
107
+ * @returns {Tantrum} New Tantrum instance
108
+ */
109
+ static new(message, errors = []) {
110
+ if(errors instanceof this)
111
+ return errors.addTrace(message)
112
+
113
+ return new this(message, errors)
114
+ }
115
+ }
@@ -0,0 +1,257 @@
1
+ import Sass from "./Sass.js"
2
+ import Valid from "./Valid.js"
3
+ import Collection from "./Collection.js"
4
+
5
+ /**
6
+ * Utility class providing common helper functions for string manipulation,
7
+ * timing, and option parsing.
8
+ */
9
+ export default class Util {
10
+ /**
11
+ * Capitalizes the first letter of a string.
12
+ *
13
+ * @param {string} text - The text to capitalize
14
+ * @returns {string} Text with first letter capitalized
15
+ */
16
+ static capitalize(text) {
17
+ if(typeof text !== "string")
18
+ throw new TypeError("Util.capitalize expects a string")
19
+
20
+ if(text.length === 0)
21
+ return ""
22
+
23
+ const [first, ...rest] = Array.from(text)
24
+
25
+ return `${first.toLocaleUpperCase()}${rest.join("")}`
26
+ }
27
+
28
+ /**
29
+ * Measure wall-clock time for an async function.
30
+ *
31
+ * @template T
32
+ * @param {() => Promise<T>} fn - Thunk returning a promise.
33
+ * @returns {Promise<{result: T, cost: number}>} Object containing result and elapsed ms (number, 1 decimal).
34
+ */
35
+ static async time(fn) {
36
+ const t0 = performance.now()
37
+ const result = await fn()
38
+ const cost = Math.round((performance.now() - t0) * 10) / 10
39
+
40
+ return {result, cost}
41
+ }
42
+
43
+ /**
44
+ * Right-align a string inside a fixed width (left pad with spaces).
45
+ * If the string exceeds width it is returned unchanged.
46
+ *
47
+ * @param {string|number} text - Text to align.
48
+ * @param {number} width - Target field width (default 80).
49
+ * @returns {string} Padded string.
50
+ */
51
+ static rightAlignText(text, width=80) {
52
+ const work = String(text)
53
+
54
+ if(work.length > width)
55
+ return work
56
+
57
+ const diff = width-work.length
58
+
59
+ return `${" ".repeat(diff)}${work}`
60
+ }
61
+
62
+ /**
63
+ * Centre-align a string inside a fixed width (pad with spaces on left).
64
+ * If the string exceeds width it is returned unchanged.
65
+ *
66
+ * @param {string|number} text - Text to align.
67
+ * @param {number} width - Target field width (default 80).
68
+ * @returns {string} Padded string with text centred.
69
+ */
70
+ static centreAlignText(text, width=80) {
71
+ const work = String(text)
72
+
73
+ if(work.length >= width)
74
+ return work
75
+
76
+ const totalPadding = width - work.length
77
+ const leftPadding = Math.floor(totalPadding / 2)
78
+ const rightPadding = totalPadding - leftPadding
79
+
80
+ return `${" ".repeat(leftPadding)}${work}${" ".repeat(rightPadding)}`
81
+ }
82
+
83
+ /**
84
+ * Asynchronously awaits all promises in parallel.
85
+ * Wrapper around Promise.all for consistency with other utility methods.
86
+ *
87
+ * @param {Array<Promise<unknown>>} promises - Array of promises to await
88
+ * @returns {Promise<Array<unknown>>} Results of all promises
89
+ */
90
+ static async awaitAll(promises) {
91
+ return await Promise.all(promises)
92
+ }
93
+
94
+ /**
95
+ * Settles all promises (both fulfilled and rejected) in parallel.
96
+ * Wrapper around Promise.allSettled for consistency with other utility methods.
97
+ *
98
+ * @param {Array<Promise<unknown>>} promises - Array of promises to settle
99
+ * @returns {Promise<Array<object>>} Results of all settled promises with status and value/reason
100
+ */
101
+ static async settleAll(promises) {
102
+ return await Promise.allSettled(promises)
103
+ }
104
+
105
+ /**
106
+ * Checks if any result in the settled promise array is rejected.
107
+ *
108
+ * @param {Array<object>} result - Array of settled promise results.
109
+ * @returns {boolean} True if any result is rejected, false otherwise.
110
+ */
111
+ static anyRejected(result) {
112
+ return result.some(r => r.status === "rejected")
113
+ }
114
+
115
+ /**
116
+ * Filters and returns all rejected results from a settled promise array.
117
+ *
118
+ * @param {Array<object>} result - Array of settled promise results.
119
+ * @returns {Array<object>} Array of rejected results.
120
+ */
121
+ static settledAndRejected(result) {
122
+ return result.filter(r => r.status === "rejected")
123
+ }
124
+
125
+ /**
126
+ * Extracts the rejection reasons from an array of rejected promise results.
127
+ *
128
+ * @param {Array<object>} rejected - Array of rejected results.
129
+ * @returns {Array<unknown>} Array of rejection reasons.
130
+ */
131
+ static rejectedReasons(rejected) {
132
+ return rejected.map(r => r.reason)
133
+ }
134
+
135
+ /**
136
+ * Throws a Sass error containing all rejection reasons from settled promises.
137
+ *
138
+ * @param {string} [_message] - Optional error message. Defaults to "GIGO"
139
+ * @param {Array<object>} rejected - Array of rejected results.
140
+ * @throws {Error} Throws a Sass error with rejection reasons.
141
+ */
142
+ static throwRejected(_message="GIGO", rejected) {
143
+ throw Sass.new(this.rejectedReasons(rejected))
144
+ }
145
+
146
+ /**
147
+ * Filters and returns all fulfilled results from a settled promise array.
148
+ *
149
+ * @param {Array<object>} result - Array of settled promise results.
150
+ * @returns {Array<object>} Array of fulfilled results.
151
+ */
152
+ static settledAndFulfilled(result) {
153
+ return result.filter(r => r.status === "fulfilled")
154
+ }
155
+
156
+ /**
157
+ * Extracts the values from all fulfilled results in a settled promise array.
158
+ *
159
+ * @param {Array<object>} result - Array of settled promise results.
160
+ * @returns {Array<unknown>} Array of fulfilled values.
161
+ */
162
+ static fulfilledValues(result) {
163
+ return this.settledAndFulfilled(result).map(r => r.value)
164
+ }
165
+
166
+ /**
167
+ * Returns the first promise to resolve or reject from an array of promises.
168
+ * Wrapper around Promise.race for consistency with other utility methods.
169
+ *
170
+ * @param {Array<Promise<unknown>>} promises - Array of promises to race
171
+ * @returns {Promise<unknown>} Result of the first settled promise
172
+ */
173
+ static async race(promises) {
174
+ return await Promise.race(promises)
175
+ }
176
+
177
+ /**
178
+ * Determine the Levenshtein distance between two string values
179
+ *
180
+ * @param {string} a The first value for comparison.
181
+ * @param {string} b The second value for comparison.
182
+ * @returns {number} The Levenshtein distance
183
+ */
184
+ static levenshteinDistance(a, b) {
185
+ const matrix = Array.from({length: a.length + 1}, (_, i) =>
186
+ Array.from({length: b.length + 1}, (_, j) =>
187
+ (i === 0 ? j : j === 0 ? i : 0)
188
+ )
189
+ )
190
+
191
+ for(let i = 1; i <= a.length; i++) {
192
+ for(let j = 1; j <= b.length; j++) {
193
+ matrix[i][j] =
194
+ a[i - 1] === b[j - 1]
195
+ ? matrix[i - 1][j - 1]
196
+ : 1 + Math.min(
197
+ matrix[i - 1][j], matrix[i][j - 1],
198
+ matrix[i - 1][j - 1]
199
+ )
200
+ }
201
+ }
202
+
203
+ return matrix[a.length][b.length]
204
+ }
205
+
206
+ /**
207
+ * Determine the closest match between a string and allowed values
208
+ * from the Levenshtein distance.
209
+ *
210
+ * @param {string} input The input string to resolve
211
+ * @param {Array<string>} allowedValues The values which are permitted
212
+ * @param {number} [threshold] Max edit distance for a "close match"
213
+ * @returns {string} Suggested, probable match.
214
+ */
215
+ static findClosestMatch(input, allowedValues, threshold=2) {
216
+ let closestMatch = null
217
+ let closestDistance = Infinity
218
+ let closestLengthDiff = Infinity
219
+
220
+ for(const value of allowedValues) {
221
+ const distance = this.levenshteinDistance(input, value)
222
+ const lengthDiff = Math.abs(input.length - value.length)
223
+
224
+ if(distance < closestDistance && distance <= threshold) {
225
+ closestMatch = value
226
+ closestDistance = distance
227
+ closestLengthDiff = lengthDiff
228
+ } else if(distance === closestDistance &&
229
+ distance <= threshold &&
230
+ lengthDiff < closestLengthDiff) {
231
+ closestMatch = value
232
+ closestLengthDiff = lengthDiff
233
+ }
234
+ }
235
+
236
+ return closestMatch
237
+ }
238
+
239
+ static regexify(input, trim=true, flags=[]) {
240
+ Valid.type(input, "String")
241
+ Valid.type(trim, "Boolean")
242
+ Valid.type(flags, "Array")
243
+
244
+ Valid.assert(
245
+ flags.length === 0 ||
246
+ (flags.length > 0 && Collection.isArrayUniform(flags, "String")),
247
+ "All flags must be strings")
248
+
249
+ return new RegExp(
250
+ input
251
+ .split(/\r\n|\r|\n/)
252
+ .map(i => trim ? i.trim() : i)
253
+ .filter(i => trim ? Boolean(i) : true)
254
+ .join("")
255
+ , flags?.join(""))
256
+ }
257
+ }