@gesslar/toolkit 0.1.5 → 0.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/toolkit",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "Get in, bitches, we're going toolkitting.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
package/src/lib/Data.js CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import Sass from "./Sass.js"
10
10
  import TypeSpec from "./TypeSpec.js"
11
+ import Util from "./Util.js"
11
12
  import Valid from "./Valid.js"
12
13
 
13
14
  export default class Data {
@@ -19,17 +20,17 @@ export default class Data {
19
20
  */
20
21
  static primitives = Object.freeze([
21
22
  // Primitives
22
- "undefined",
23
- "null",
24
- "boolean",
25
- "number",
26
- "bigint",
27
- "string",
28
- "symbol",
23
+ "Undefined",
24
+ "Null",
25
+ "Boolean",
26
+ "Number",
27
+ "Bigint",
28
+ "String",
29
+ "Symbol",
29
30
 
30
31
  // Object Categories from typeof
31
- "object",
32
- "function",
32
+ "Object",
33
+ "Function",
33
34
  ])
34
35
 
35
36
  /**
@@ -67,7 +68,7 @@ export default class Data {
67
68
  */
68
69
  static dataTypes = Object.freeze([
69
70
  ...Data.primitives,
70
- ...Data.constructors.map(c => c.toLowerCase())
71
+ ...Data.constructors
71
72
  ])
72
73
 
73
74
  /**
@@ -76,7 +77,7 @@ export default class Data {
76
77
  *
77
78
  * @type {Array<string>}
78
79
  */
79
- static emptyableTypes = Object.freeze(["string", "array", "object"])
80
+ static emptyableTypes = Object.freeze(["String", "Array", "Object"])
80
81
 
81
82
  /**
82
83
  * Appends a string to another string if it does not already end with it.
@@ -109,8 +110,11 @@ export default class Data {
109
110
  * @returns {boolean} Whether all elements are of the specified type
110
111
  */
111
112
  static isArrayUniform(arr, type) {
113
+ const checkType = type ? Util.capitalize(type) : null
114
+
112
115
  return arr.every(
113
- (item, _index, arr) => typeof item === (type || typeof arr[0]),
116
+ (item, _index, arr) =>
117
+ Data.typeOf(item) === (checkType || Data.typeOf(arr[0])),
114
118
  )
115
119
  }
116
120
 
@@ -197,10 +201,10 @@ export default class Data {
197
201
  const result = {}
198
202
 
199
203
  for(const [key, value] of Object.entries(obj)) {
200
- if(Data.isType(value, "array")) {
204
+ if(Data.isType(value, "Array")) {
201
205
  // Clone arrays by mapping over them
202
206
  result[key] = value.map(item =>
203
- Data.isType(item, "object") || Data.isType(item, "array")
207
+ Data.isType(item, "object") || Data.isType(item, "Array")
204
208
  ? Data.cloneObject(item)
205
209
  : item
206
210
  )
@@ -227,25 +231,25 @@ export default class Data {
227
231
  workSpec = [],
228
232
  result = {}
229
233
 
230
- if(!Data.isType(source, "array", {allowEmpty: false}))
234
+ if(!Data.isType(source, "Array", {allowEmpty: false}))
231
235
  throw Sass.new("Source must be an array.")
232
236
 
233
237
  workSource.push(...source)
234
238
 
235
239
  if(
236
- !Data.isType(spec, "array", {allowEmpty: false}) &&
240
+ !Data.isType(spec, "Array", {allowEmpty: false}) &&
237
241
  !Data.isType(spec, "function")
238
242
  )
239
243
  throw Sass.new("Spec must be an array or a function.")
240
244
 
241
- if(Data.isType(spec, "function")) {
245
+ if(Data.isType(spec, "Function")) {
242
246
  const specResult = await spec(workSource)
243
247
 
244
- if(!Data.isType(specResult, "array"))
248
+ if(!Data.isType(specResult, "Array"))
245
249
  throw Sass.new("Spec resulting from function must be an array.")
246
250
 
247
251
  workSpec.push(...specResult)
248
- } else if(Data.isType(spec, "array", {allowEmpty: false})) {
252
+ } else if(Data.isType(spec, "Array", {allowEmpty: false})) {
249
253
  workSpec.push(...spec)
250
254
  }
251
255
 
@@ -256,7 +260,7 @@ export default class Data {
256
260
  workSource.map((element, index, arr) => (arr[index] = String(element)))
257
261
 
258
262
  // Check that all keys are strings
259
- if(!Data.isArrayUniform(workSource, "string"))
263
+ if(!Data.isArrayUniform(workSource, "String"))
260
264
  throw Sass.new("Indices of an Object must be of type string.")
261
265
 
262
266
  workSource.forEach((element, index) => (result[element] = workSpec[index]))
@@ -332,7 +336,13 @@ export default class Data {
332
336
  * @returns {boolean} Whether the type is valid
333
337
  */
334
338
  static isValidType(type) {
335
- return Data.dataTypes.includes(type)
339
+ // Allow built-in types
340
+ if(Data.dataTypes.includes(type)) {
341
+ return true
342
+ }
343
+
344
+ // Allow custom classes (PascalCase starting with capital letter)
345
+ return /^[A-Z][a-zA-Z0-9]*$/.test(type)
336
346
  }
337
347
 
338
348
  /**
@@ -349,16 +359,13 @@ export default class Data {
349
359
  return false
350
360
 
351
361
  const valueType = Data.typeOf(value)
352
- const normalizedType = type.toLowerCase()
353
362
 
354
363
  // Special cases that need extra validation
355
- switch(normalizedType) {
356
- case "number":
357
- return valueType === "number" && !isNaN(value) // Excludes NaN
358
- case "object":
359
- return valueType === "object" && value !== null && !Array.isArray(value) // Excludes arrays and null
364
+ switch(valueType) {
365
+ case "Number":
366
+ return valueType === "Number" && !isNaN(value) // Excludes NaN
360
367
  default:
361
- return valueType === normalizedType
368
+ return valueType === type
362
369
  }
363
370
  }
364
371
 
@@ -370,12 +377,13 @@ export default class Data {
370
377
  */
371
378
  static typeOf(value) {
372
379
  if(value === null)
373
- return "null"
380
+ return "Null"
374
381
 
375
- if(Array.isArray(value))
376
- return "array"
382
+ const type = typeof value
377
383
 
378
- return typeof value
384
+ return type === "object"
385
+ ? value.constructor.name
386
+ : type.charAt(0).toUpperCase() + type.slice(1)
379
387
  }
380
388
 
381
389
  /**
@@ -412,12 +420,12 @@ export default class Data {
412
420
  return false
413
421
 
414
422
  switch(type) {
415
- case "array":
423
+ case "Array":
416
424
  return value.length === 0
417
- case "object":
425
+ case "Object":
418
426
  // null was already handled above, so this should only be real objects
419
427
  return Object.keys(value).length === 0
420
- case "string":
428
+ case "String":
421
429
  return value.trim().length === 0
422
430
  default:
423
431
  return false
@@ -6,6 +6,7 @@
6
6
 
7
7
  import Sass from "./Sass.js"
8
8
  import Data from "./Data.js"
9
+ import Util from "./Util.js"
9
10
 
10
11
  /**
11
12
  * Type specification class for parsing and validating complex type definitions.
@@ -149,7 +150,7 @@ export default class TypeSpec {
149
150
  // Now, let's do some checking with the types, respecting the array flag
150
151
  // with the value
151
152
  const valueType = Data.typeOf(value)
152
- const isArray = valueType === "array"
153
+ const isArray = valueType === "Array"
153
154
 
154
155
  // We need to ensure that we match the type and the consistency of the types
155
156
  // in an array, if it is an array and an array is allowed.
@@ -166,8 +167,8 @@ export default class TypeSpec {
166
167
 
167
168
  // Handle array values
168
169
  if(isArray) {
169
- // Special case for generic "array" type
170
- if(allowedType === "array" && !allowedArray)
170
+ // Special case for generic "Array" type
171
+ if(allowedType === "Array" && !allowedArray)
171
172
  return allowEmpty || !empty
172
173
 
173
174
  // Must be an array type specification
@@ -203,16 +204,18 @@ export default class TypeSpec {
203
204
  const parts = string.split(delimiter)
204
205
 
205
206
  this.#specs = parts.map(part => {
206
- const typeMatches = /(\w+)(\[\])?/.exec(part)
207
+ const typeMatches = /^(\w+)(\[\])?$/.exec(part)
207
208
 
208
209
  if(!typeMatches || typeMatches.length !== 3)
209
210
  throw Sass.new(`Invalid type: ${part}`)
210
211
 
211
- if(!Data.isValidType(typeMatches[1]))
212
+ const typeName = Util.capitalize(typeMatches[1])
213
+
214
+ if(!Data.isValidType(typeName))
212
215
  throw Sass.new(`Invalid type: ${typeMatches[1]}`)
213
216
 
214
217
  return {
215
- typeName: typeMatches[1],
218
+ typeName,
216
219
  array: typeMatches[2] === "[]",
217
220
  }
218
221
  })
package/src/lib/Util.js CHANGED
@@ -230,4 +230,66 @@ export default class Util {
230
230
  )
231
231
  }
232
232
  }
233
+
234
+ /**
235
+ * Determine the Levenshtein distance between two string values
236
+ *
237
+ * @param {string} a The first value for comparison.
238
+ * @param {string} b The second value for comparison.
239
+ * @returns {number} The Levenshtein distance
240
+ */
241
+ static levenshteinDistance(a, b) {
242
+ const matrix = Array.from({length: a.length + 1}, (_, i) =>
243
+ Array.from({length: b.length + 1}, (_, j) =>
244
+ (i === 0 ? j : j === 0 ? i : 0)
245
+ )
246
+ )
247
+
248
+ for(let i = 1; i <= a.length; i++) {
249
+ for(let j = 1; j <= b.length; j++) {
250
+ matrix[i][j] =
251
+ a[i - 1] === b[j - 1]
252
+ ? matrix[i - 1][j - 1]
253
+ : 1 + Math.min(
254
+ matrix[i - 1][j], matrix[i][j - 1],
255
+ matrix[i - 1][j - 1]
256
+ )
257
+ }
258
+ }
259
+
260
+ return matrix[a.length][b.length]
261
+ }
262
+
263
+ /**
264
+ * Determine the closest match between a string and allowed values
265
+ * from the Levenshtein distance.
266
+ *
267
+ * @param {string} input The input string to resolve
268
+ * @param {Array<string>} allowedValues The values which are permitted
269
+ * @param {number} [threshold] Max edit distance for a "close match"
270
+ * @returns {string} Suggested, probable match.
271
+ */
272
+ static findClosestMatch(input, allowedValues, threshold=2) {
273
+ let closestMatch = null
274
+ let closestDistance = Infinity
275
+ let closestLengthDiff = Infinity
276
+
277
+ for(const value of allowedValues) {
278
+ const distance = Util.levenshteinDistance(input, value)
279
+ const lengthDiff = Math.abs(input.length - value.length)
280
+
281
+ if(distance < closestDistance && distance <= threshold) {
282
+ closestMatch = value
283
+ closestDistance = distance
284
+ closestLengthDiff = lengthDiff
285
+ } else if(distance === closestDistance &&
286
+ distance <= threshold &&
287
+ lengthDiff < closestLengthDiff) {
288
+ closestMatch = value
289
+ closestLengthDiff = lengthDiff
290
+ }
291
+ }
292
+
293
+ return closestMatch
294
+ }
233
295
  }
@@ -174,6 +174,46 @@ declare class Util {
174
174
  * @returns Resolves when all listeners have completed
175
175
  */
176
176
  static asyncEmitAnon(emitter: { listeners(event: string): Function[], on(event: string, listener: Function): any, emit(event: string, ...args: unknown[]): any }, event: string, ...args: unknown[]): Promise<void>
177
+
178
+ /**
179
+ * Determine the Levenshtein distance between two string values.
180
+ * The Levenshtein distance is the minimum number of single-character edits
181
+ * (insertions, deletions, or substitutions) required to change one string into another.
182
+ *
183
+ * @param a - The first string for comparison
184
+ * @param b - The second string for comparison
185
+ * @returns The Levenshtein distance (number of edits needed)
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * Util.levenshteinDistance("kitten", "sitting") // 3
190
+ * Util.levenshteinDistance("book", "back") // 2
191
+ * Util.levenshteinDistance("hello", "hello") // 0
192
+ * ```
193
+ */
194
+ static levenshteinDistance(a: string, b: string): number
195
+
196
+ /**
197
+ * Find the closest match between an input string and an array of allowed values
198
+ * using Levenshtein distance. Returns the closest match if it's within a threshold
199
+ * of 2 edits, otherwise returns null.
200
+ *
201
+ * Useful for fuzzy string matching, such as suggesting corrections for typos
202
+ * in command-line arguments or configuration values.
203
+ *
204
+ * @param input - The input string to find a match for
205
+ * @param allowedValues - Array of allowed string values to match against
206
+ * @param threshold - Maximum edit distance for a match (default: 2)
207
+ * @returns The closest matching string, or null if no match within threshold
208
+ *
209
+ * @example
210
+ * ```typescript
211
+ * const commands = ["help", "build", "test", "deploy"]
212
+ * Util.findClosestMatch("bulid", commands) // "build"
213
+ * Util.findClosestMatch("xyz", commands) // null
214
+ * ```
215
+ */
216
+ static findClosestMatch(input: string, allowedValues: string[], threshold?: number): string | null
177
217
  }
178
218
 
179
219
  export default Util