@gesslar/sassy 0.19.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/src/Data.js ADDED
@@ -0,0 +1,545 @@
1
+ /**
2
+ * @file Data utility functions for type checking, object manipulation, and array operations.
3
+ * Provides comprehensive utilities for working with JavaScript data types and structures.
4
+ */
5
+
6
+ import TypeSpec from "./Type.js"
7
+ import Sass from "./Sass.js"
8
+ import Valid from "./Valid.js"
9
+
10
+ export default class Data {
11
+ /**
12
+ * Array of JavaScript primitive type names.
13
+ * Includes basic types and object categories from the typeof operator.
14
+ *
15
+ * @type {string[]}
16
+ */
17
+ static primitives = Object.freeze([
18
+ // Primitives
19
+ "undefined",
20
+ "boolean",
21
+ "number",
22
+ "bigint",
23
+ "string",
24
+ "symbol",
25
+
26
+ // Object Categories from typeof
27
+ "object",
28
+ "function",
29
+ ])
30
+
31
+ /**
32
+ * Array of JavaScript constructor names for built-in objects.
33
+ * Includes common object types and typed arrays.
34
+ *
35
+ * @type {string[]}
36
+ */
37
+ static constructors = Object.freeze([
38
+ // Object Constructors
39
+ "Object",
40
+ "Array",
41
+ "Function",
42
+ "Date",
43
+ "RegExp",
44
+ "Error",
45
+ "Map",
46
+ "Set",
47
+ "WeakMap",
48
+ "WeakSet",
49
+ "Promise",
50
+ "Int8Array",
51
+ "Uint8Array",
52
+ "Float32Array",
53
+ "Float64Array",
54
+ ])
55
+
56
+ /**
57
+ * Combined array of all supported data types (primitives and constructors in lowercase).
58
+ * Used for type validation throughout the utility functions.
59
+ *
60
+ * @type {string[]}
61
+ */
62
+ static dataTypes = Object.freeze([...Data.primitives, ...Data.constructors.map(c => c.toLowerCase())])
63
+
64
+ /**
65
+ * Array of type names that can be checked for emptiness.
66
+ * These types have meaningful empty states that can be tested.
67
+ *
68
+ * @type {string[]}
69
+ */
70
+ static emptyableTypes = Object.freeze(["string", "array", "object"])
71
+
72
+ /**
73
+ * Appends a string to another string if it does not already end with it.
74
+ *
75
+ * @param {string} string - The string to append to
76
+ * @param {string} append - The string to append
77
+ * @returns {string} The appended string
78
+ */
79
+ static appendString(string, append) {
80
+ return string.endsWith(append) ? string : `${string}${append}`
81
+ }
82
+
83
+ /**
84
+ * Prepends a string to another string if it does not already start with it.
85
+ *
86
+ * @param {string} string - The string to prepend to
87
+ * @param {string} prepend - The string to prepend
88
+ * @returns {string} The prepended string
89
+ */
90
+ static prependString(string, prepend) {
91
+ return string.startsWith(prepend) ? string : `${prepend}${string}`
92
+ }
93
+
94
+ /**
95
+ * Checks if all elements in an array are of a specified type
96
+ *
97
+ * @param {Array} arr - The array to check
98
+ * @param {string} type - The type to check for (optional, defaults to the
99
+ * type of the first element)
100
+ * @returns {boolean} Whether all elements are of the specified type
101
+ */
102
+ static isArrayUniform(arr, type) {
103
+ return arr.every(
104
+ (item, _index, arr) => typeof item === (type || typeof arr[0]),
105
+ )
106
+ }
107
+
108
+ /**
109
+ * Checks if an array is unique
110
+ *
111
+ * @param {Array} arr - The array of which to remove duplicates
112
+ * @returns {Array} The unique elements of the array
113
+ */
114
+ static isArrayUnique(arr) {
115
+ return arr.filter((item, index, self) => self.indexOf(item) === index)
116
+ }
117
+
118
+ /**
119
+ * Returns the intersection of two arrays.
120
+ *
121
+ * @param {Array} arr1 - The first array.
122
+ * @param {Array} arr2 - The second array.
123
+ * @returns {Array} The intersection of the two arrays.
124
+ */
125
+ static arrayIntersection(arr1, arr2) {
126
+ const [short,long] = [arr1,arr2].sort((a,b) => a.length - b.length)
127
+
128
+ return short.filter(value => long.includes(value))
129
+ }
130
+
131
+ /**
132
+ * Checks whether two arrays have any elements in common.
133
+ *
134
+ * This function returns `true` if at least one element from `arr1` exists in
135
+ * `arr2`, and `false` otherwise. It optimizes by iterating over the shorter
136
+ * array for efficiency.
137
+ *
138
+ * Example:
139
+ * arrayIntersects([1, 2, 3], [3, 4, 5]) // returns true
140
+ * arrayIntersects(["a", "b"], ["c", "d"]) // returns false
141
+ *
142
+ * @param {Array} arr1 - The first array to check for intersection.
143
+ * @param {Array} arr2 - The second array to check for intersection.
144
+ * @returns {boolean} True if any element is shared between the arrays, false otherwise.
145
+ */
146
+ static arrayIntersects(arr1, arr2) {
147
+ const [short,long] = [arr1,arr2].sort((a,b) => a.length - b.length)
148
+
149
+ return !!short.find(value => long.includes(value))
150
+ }
151
+
152
+ /**
153
+ * Pads an array to a specified length with a value. This operation
154
+ * occurs in-place.
155
+ *
156
+ * @param {Array} arr - The array to pad.
157
+ * @param {number} length - The length to pad the array to.
158
+ * @param {any} value - The value to pad the array with.
159
+ * @param {number} position - The position to pad the array at.
160
+ * @returns {Array} The padded array.
161
+ */
162
+ static arrayPad(arr, length, value, position = 0) {
163
+ const diff = length - arr.length
164
+ if(diff <= 0)
165
+ return arr
166
+
167
+ const padding = Array(diff).fill(value)
168
+
169
+ if(position === 0)
170
+ // prepend - default
171
+ return padding.concat(arr)
172
+ else if(position === -1)
173
+ // append
174
+ return arr.concat(padding) // somewhere in the middle - THAT IS ILLEGAL
175
+ else
176
+ throw Sass.new("Invalid position")
177
+ }
178
+
179
+ /**
180
+ * Clones an object
181
+ *
182
+ * @param {object} obj - The object to clone
183
+ * @param {boolean} freeze - Whether to freeze the cloned object
184
+ * @returns {object} The cloned object
185
+ */
186
+ static cloneObject(obj, freeze = false) {
187
+ const result = {}
188
+
189
+ for(const [key, value] of Object.entries(obj)) {
190
+ if(Data.isType(value, "object"))
191
+ result[key] = Data.cloneObject(value)
192
+ else
193
+ result[key] = value
194
+ }
195
+
196
+ return freeze ? Object.freeze(result) : result
197
+ }
198
+
199
+ /**
200
+ * Allocates an object from a source array and a spec array or function.
201
+ *
202
+ * @param {any} source The source array
203
+ * @param {any|Function} spec The spec array or function
204
+ * @returns {Promise<object>} The allocated object
205
+ */
206
+ static async allocateObject(source, spec) {
207
+ // Data
208
+ const workSource = [],
209
+ workSpec = [],
210
+ result = {}
211
+
212
+ if(!Data.isType(source, "array", {allowEmpty: false}))
213
+ throw Sass.new("Source must be an array.")
214
+
215
+ workSource.push(...source)
216
+
217
+ if(
218
+ !Data.isType(spec, "array", {allowEmpty: false}) &&
219
+ !Data.isType(spec, "function")
220
+ )
221
+ throw Sass.new("Spec must be an array or a function.")
222
+
223
+ if(Data.isType(spec, "function")) {
224
+ const specResult = await spec(workSource)
225
+
226
+ if(!Data.isType(specResult, "array"))
227
+ throw Sass.new("Spec resulting from function must be an array.")
228
+
229
+ workSpec.push(...specResult)
230
+ } else if(Data.isType(spec, "array", {allowEmpty: false})) {
231
+ workSpec.push(...spec)
232
+ }
233
+
234
+ if(workSource.length !== workSpec.length)
235
+ throw Sass.new("Source and spec must have the same number of elements.")
236
+
237
+ // Objects must always be indexed by strings.
238
+ workSource.map((element, index, arr) => (arr[index] = String(element)))
239
+
240
+ // Check that all keys are strings
241
+ if(!Data.isArrayUniform(workSource, "string"))
242
+ throw Sass.new("Indices of an Object must be of type string.")
243
+
244
+ workSource.forEach((element, index) => (result[element] = workSpec[index]))
245
+
246
+ return result
247
+ }
248
+
249
+ /**
250
+ * Maps an object using a transformer function
251
+ *
252
+ * @param {object} original The original object
253
+ * @param {Function} transformer The transformer function
254
+ * @param {boolean} mutate Whether to mutate the original object
255
+ * @returns {Promise<object>} The mapped object
256
+ */
257
+ static async mapObject(original, transformer, mutate = false) {
258
+ Valid.validType(original, "object", {allowEmpty: true})
259
+ Valid.validType(transformer, "function")
260
+ Valid.validType(mutate, "boolean")
261
+
262
+ const result = mutate ? original : {}
263
+
264
+ for(const [key, value] of Object.entries(original))
265
+ result[key] = Data.isType(value, "object")
266
+ ? await Data.mapObject(value, transformer, mutate)
267
+ : (result[key] = await transformer(key, value))
268
+
269
+ return result
270
+ }
271
+
272
+ /**
273
+ * Checks if an object is empty
274
+ *
275
+ * @param {object} obj - The object to check
276
+ * @returns {boolean} Whether the object is empty
277
+ */
278
+ static isObjectEmpty(obj) {
279
+ return Object.keys(obj).length === 0
280
+ }
281
+
282
+ /**
283
+ * Creates a type spec from a string. A type spec is an array of objects
284
+ * defining the type of a value and whether an array is expected.
285
+ *
286
+ * @param {string} string - The string to parse into a type spec.
287
+ * @param {object} options - Additional options for parsing.
288
+ * @returns {object[]} An array of type specs.
289
+ */
290
+ static newTypeSpec(string, options) {
291
+ return new TypeSpec(string, options)
292
+ }
293
+
294
+ /**
295
+ * Checks if a value is of a specified type
296
+ *
297
+ * @param {any} value The value to check
298
+ * @param {string|TypeSpec} type The type to check for
299
+ * @param {object} options Additional options for checking
300
+ * @returns {boolean} Whether the value is of the specified type
301
+ */
302
+ static isType(value, type, options = {}) {
303
+ const typeSpec = type instanceof TypeSpec ? type : Data.newTypeSpec(type, options)
304
+
305
+ return typeSpec.match(value, options)
306
+ }
307
+
308
+ /**
309
+ * Checks if a type is valid
310
+ *
311
+ * @param {string} type - The type to check
312
+ * @returns {boolean} Whether the type is valid
313
+ */
314
+ static isValidType(type) {
315
+ return Data.dataTypes.includes(type)
316
+ }
317
+
318
+ /**
319
+ * Checks if a value is of a specified type. Unlike the type function, this
320
+ * function does not parse the type string, and only checks for primitive
321
+ * or constructor types.
322
+ *
323
+ * @param {any} value - The value to check
324
+ * @param {string} type - The type to check for
325
+ * @returns {boolean} Whether the value is of the specified type
326
+ */
327
+ static isBaseType(value, type) {
328
+ if(!Data.isValidType(type))
329
+ return false
330
+
331
+ const valueType = Data.typeOf(value)
332
+
333
+ switch(type.toLowerCase()) {
334
+ case "array":
335
+ return Array.isArray(value) // Native array check
336
+ case "string":
337
+ return valueType === "string"
338
+ case "boolean":
339
+ return valueType === "boolean"
340
+ case "number":
341
+ return valueType === "number" && !isNaN(value) // Excludes NaN
342
+ case "object":
343
+ return value !== null && valueType === "object" && !Array.isArray(value) // Excludes arrays and null
344
+ case "function":
345
+ return valueType === "function"
346
+ case "symbol":
347
+ return valueType === "symbol" // ES6 Symbol type
348
+ case "bigint":
349
+ return valueType === "bigint" // BigInt support
350
+ case "null":
351
+ return value === null // Explicit null check
352
+ case "undefined":
353
+ return valueType === "undefined" // Explicit undefined check
354
+ default:
355
+ return false // Unknown type
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Returns the type of a value, whether it be a primitive, object, or function.
361
+ *
362
+ * @param {any} value - The value to check
363
+ * @returns {string} The type of the value
364
+ */
365
+ static typeOf(value) {
366
+ return Array.isArray(value) ? "array" : typeof value
367
+ }
368
+
369
+ /**
370
+ * Checks a value is undefined or null.
371
+ *
372
+ * @param {any} value The value to check
373
+ * @returns {boolean} Whether the value is undefined or null
374
+ */
375
+ static isNothing(value) {
376
+ return value === undefined || value === null
377
+ }
378
+
379
+ /**
380
+ * Checks if a value is empty. This function is used to check if an object,
381
+ * array, or string is empty. Null and undefined values are considered empty.
382
+ *
383
+ * @param {any} value The value to check
384
+ * @param {boolean} checkForNothing Whether to check for null or undefined
385
+ * values
386
+ * @returns {boolean} Whether the value is empty
387
+ */
388
+ static isEmpty(value, checkForNothing = true) {
389
+ const type = Data.typeOf(value)
390
+
391
+ if(checkForNothing && Data.isNothing(value))
392
+ return true
393
+
394
+ if(!Data.emptyableTypes.includes(type))
395
+ return false
396
+
397
+ switch(type) {
398
+ case "array":
399
+ return value.length === 0
400
+ case "object":
401
+ return Object.keys(value).length === 0
402
+ case "string":
403
+ return value.trim().length === 0
404
+ default:
405
+ return false
406
+ }
407
+ }
408
+
409
+ /**
410
+ * Freezes an object and all of its properties recursively.
411
+ *
412
+ * @param {object} obj The object to freeze.
413
+ * @returns {object} The frozen object.
414
+ */
415
+ static deepFreezeObject(obj) {
416
+ if(obj === null || typeof obj !== "object")
417
+ return obj // Skip null and non-objects
418
+
419
+ // Retrieve and freeze properties
420
+ const propNames = Object.getOwnPropertyNames(obj)
421
+
422
+ for(const name of propNames) {
423
+ const value = obj[name]
424
+
425
+ // Recursively freeze nested objects
426
+ if(value && typeof value === "object")
427
+ Data.deepFreezeObject(value)
428
+ }
429
+
430
+ // Freeze the object itself
431
+ return Object.freeze(obj)
432
+ }
433
+
434
+ /**
435
+ * Ensures that a nested path of objects exists within the given object.
436
+ * Creates empty objects along the path if they don't exist.
437
+ *
438
+ * @param {object} obj - The object to check/modify
439
+ * @param {Array<string>} keys - Array of keys representing the path to ensure
440
+ * @returns {object} Reference to the deepest nested object in the path
441
+ */
442
+ static assureObjectPath(obj, keys) {
443
+ let current = obj // a moving reference to internal objects within obj
444
+ const len = keys.length
445
+ for(let i = 0; i < len; i++) {
446
+ const elem = keys[i]
447
+ if(!current[elem])
448
+ current[elem] = {}
449
+
450
+ current = current[elem]
451
+ }
452
+
453
+ // Return the current pointer
454
+ return current
455
+ }
456
+
457
+ /**
458
+ * Sets a value in a nested object structure using an array of keys; creating
459
+ * the structure if it does not exist.
460
+ *
461
+ * @param {object} obj - The target object to set the value in
462
+ * @param {string[]} keys - Array of keys representing the path to the target property
463
+ * @param {*} value - The value to set at the target location
464
+ */
465
+ static setNestedValue(obj, keys, value) {
466
+ const nested = Data.assureObjectPath(obj, keys.slice(0, -1))
467
+
468
+ nested[keys[keys.length-1]] = value
469
+ }
470
+
471
+ /**
472
+ * Deeply merges two or more objects. Arrays are replaced, not merged.
473
+ *
474
+ * @param {...object} sources - Objects to merge (left to right)
475
+ * @returns {object} The merged object
476
+ */
477
+ static mergeObject(...sources) {
478
+ const isObject = obj => obj && typeof obj === "object" && !Array.isArray(obj)
479
+ return sources.reduce((acc, obj) => {
480
+ if(!isObject(obj))
481
+ return acc
482
+
483
+ Object.keys(obj).forEach(key => {
484
+ const accVal = acc[key]
485
+ const objVal = obj[key]
486
+
487
+ if(isObject(accVal) && isObject(objVal))
488
+ acc[key] = Data.mergeObject(accVal, objVal)
489
+ else
490
+ acc[key] = objVal
491
+ })
492
+
493
+ return acc
494
+ }, {})
495
+ }
496
+
497
+ /**
498
+ * Checks if all elements in an array are strings.
499
+ *
500
+ * @param {Array} arr - The array to check.
501
+ * @returns {boolean} Returns true if all elements are strings, false otherwise.
502
+ * @example
503
+ * uniformStringArray(['a', 'b', 'c']) // returns true
504
+ * uniformStringArray(['a', 1, 'c']) // returns false
505
+ */
506
+ static uniformStringArray(arr) {
507
+ return Array.isArray(arr) && arr.every(item => typeof item === "string")
508
+ }
509
+
510
+ /**
511
+ * Filters an array asynchronously using a predicate function.
512
+ * Applies the predicate to all items in parallel and returns filtered results.
513
+ *
514
+ * @param {Array} arr - The array to filter
515
+ * @param {Function} predicate - Async predicate function that returns a promise resolving to boolean
516
+ * @returns {Promise<Array>} Promise resolving to the filtered array
517
+ */
518
+ static async asyncFilter(arr, predicate) {
519
+ const results = await Promise.all(arr.map(predicate))
520
+ return arr.filter((_, index) => results[index])
521
+ }
522
+
523
+ /**
524
+ * Shallowly merges multiple arrays, deduplicating while preserving order.
525
+ *
526
+ * @param {...any[]} sources - Arrays to merge
527
+ * @returns {Array} A new merged array
528
+ * @throws {Error} If the sources are not all arrays
529
+ */
530
+ static mergeArray(...sources) {
531
+ if(sources.some(source => !Array.isArray(source)))
532
+ throw Sass.new("All sources to mergeArray must be arrays.")
533
+
534
+ return sources.reduce((acc, curr) => {
535
+ const accSet = new Set(acc)
536
+
537
+ curr.forEach(value => {
538
+ accSet.has(value) && accSet.delete(value)
539
+ accSet.add(value)
540
+ })
541
+
542
+ return Array.from(accSet)
543
+ }, [])
544
+ }
545
+ }