@splendidlabz/utils 1.5.0 → 1.6.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/CHANGELOG.md +77 -1
- package/dist/dom/accessibility.d.ts +28 -0
- package/dist/dom/actions/index.d.ts +3 -0
- package/dist/dom/actions/masonry.d.ts +3 -0
- package/dist/dom/actions/prefer-horizontal-scroll.d.ts +3 -0
- package/dist/dom/actions/sticky.d.ts +3 -0
- package/dist/dom/bounding-box.d.ts +39 -0
- package/dist/dom/clipboard.d.ts +5 -0
- package/dist/dom/cookie.d.ts +5 -0
- package/dist/dom/css-vars.d.ts +4 -0
- package/dist/dom/events.d.ts +18 -0
- package/dist/dom/focusable.d.ts +76 -0
- package/dist/dom/font-size.d.ts +5 -0
- package/dist/dom/get-element.d.ts +100 -0
- package/dist/dom/hash.d.ts +1 -0
- package/dist/dom/index.d.ts +22 -0
- package/dist/dom/keyboard.d.ts +30 -0
- package/dist/dom/local-store.d.ts +29 -0
- package/dist/dom/media.d.ts +3 -0
- package/dist/dom/observers/index.d.ts +3 -0
- package/dist/dom/observers/intersection-observer.d.ts +8 -0
- package/dist/dom/observers/mutation-observer.d.ts +7 -0
- package/dist/dom/observers/observer.d.ts +1 -0
- package/dist/dom/observers/resize-observer.d.ts +38 -0
- package/dist/dom/pkce.d.ts +6 -0
- package/dist/dom/query-params.d.ts +4 -0
- package/dist/dom/random-string.d.ts +7 -0
- package/dist/dom/sanitize.d.ts +8 -0
- package/dist/dom/session-store.d.ts +5 -0
- package/dist/dom/trap-focus.d.ts +28 -0
- package/dist/dom/ui/aria-current.d.ts +5 -0
- package/dist/dom/ui/inconsistent-button-fix.d.ts +12 -0
- package/dist/dom/ui/index.d.ts +4 -0
- package/dist/dom/ui/scroll-container.d.ts +1 -0
- package/dist/dom/ui/traverse-and-scramble.d.ts +19 -0
- package/dist/lib/arrays/index.d.ts +5 -0
- package/dist/lib/arrays/item-position.d.ts +14 -0
- package/dist/lib/arrays/join.d.ts +13 -0
- package/dist/lib/arrays/shuffle.d.ts +1 -0
- package/dist/lib/arrays/sort.d.ts +30 -0
- package/dist/lib/arrays/unique.d.ts +1 -0
- package/dist/lib/auth/index.d.ts +1 -0
- package/dist/lib/auth/route-manager.d.ts +25 -0
- package/dist/lib/checks.d.ts +30 -0
- package/dist/lib/date/days.d.ts +5 -0
- package/dist/lib/date/index.d.ts +3 -0
- package/dist/lib/date/months.d.ts +9 -0
- package/dist/lib/date/time.d.ts +8 -0
- package/dist/lib/form/form-data.d.ts +7 -0
- package/dist/lib/form/index.d.ts +2 -0
- package/dist/lib/form/sanitize.d.ts +58 -0
- package/dist/lib/functions/debounce.d.ts +1 -0
- package/dist/lib/functions/env.d.ts +1 -0
- package/dist/lib/functions/functional.d.ts +4 -0
- package/dist/lib/functions/index.d.ts +5 -0
- package/dist/lib/functions/throttle.d.ts +1 -0
- package/dist/lib/functions/timeout.d.ts +3 -0
- package/dist/lib/hash.d.ts +1 -0
- package/dist/lib/index.d.ts +14 -0
- package/dist/lib/numbers/index.d.ts +2 -0
- package/dist/lib/numbers/math.d.ts +2 -0
- package/dist/lib/objects/camelcase-keys.d.ts +1 -0
- package/dist/lib/objects/empty.d.ts +2 -0
- package/dist/lib/objects/equal.d.ts +2 -0
- package/dist/lib/objects/extend.d.ts +4 -0
- package/dist/lib/objects/flatten.d.ts +4 -0
- package/dist/lib/objects/index.d.ts +13 -0
- package/dist/lib/objects/json.d.ts +1 -0
- package/dist/lib/objects/loop.d.ts +2 -0
- package/dist/lib/objects/mix/mix.d.ts +1 -0
- package/dist/lib/objects/nested-property.d.ts +22 -0
- package/dist/lib/objects/normalize-object.d.ts +1 -0
- package/dist/lib/objects/omit-empty.d.ts +1 -0
- package/dist/lib/objects/size.d.ts +1 -0
- package/dist/lib/objects/split.d.ts +12 -0
- package/dist/lib/promises/index.d.ts +1 -0
- package/dist/lib/promises/reject.d.ts +3 -0
- package/dist/lib/sse.d.ts +16 -0
- package/dist/lib/strings/convert-case/convert-case.d.ts +8 -0
- package/dist/lib/strings/index.d.ts +5 -0
- package/dist/lib/strings/markdown.d.ts +23 -0
- package/dist/lib/strings/name.d.ts +5 -0
- package/dist/lib/strings/pluralize.d.ts +1 -0
- package/dist/lib/strings/query-string.d.ts +8 -0
- package/dist/lib/style/index.d.ts +1 -0
- package/dist/lib/symbols/index.d.ts +1 -0
- package/dist/lib/symbols/symbols.d.ts +2 -0
- package/dist/node/common.d.ts +1 -0
- package/dist/node/dirname.d.ts +7 -0
- package/dist/node/file-cache.d.ts +54 -0
- package/dist/node/file.d.ts +2 -0
- package/dist/node/hash.d.ts +1 -0
- package/dist/node/index.d.ts +7 -0
- package/dist/node/pkce.d.ts +6 -0
- package/dist/node/random-string.d.ts +1 -0
- package/dist/node/sanitize.d.ts +8 -0
- package/dom/accessibility.js +46 -13
- package/{actions → dom/actions}/index.js +0 -2
- package/dom/actions/masonry.js +42 -0
- package/{actions → dom/actions}/prefer-horizontal-scroll.js +8 -8
- package/{actions → dom/actions}/sticky.js +3 -6
- package/dom/bounding-box.js +35 -7
- package/dom/cookie.js +8 -7
- package/dom/css-vars.js +8 -0
- package/dom/events.js +2 -1
- package/dom/focusable.js +119 -10
- package/dom/font-size.js +21 -10
- package/dom/get-element.js +112 -5
- package/dom/index.js +3 -0
- package/dom/keyboard.js +13 -8
- package/dom/local-store.js +37 -0
- package/dom/media.js +1 -0
- package/dom/observers/index.js +3 -0
- package/dom/observers/intersection-observer.js +43 -0
- package/dom/observers/mutation-observer.js +51 -0
- package/dom/observers/observer.js +18 -0
- package/dom/observers/resize-observer.js +64 -0
- package/dom/random-string.js +1 -1
- package/dom/random-string.test.js +13 -0
- package/dom/sanitize.js +16 -0
- package/dom/trap-focus.js +18 -3
- package/dom/ui/inconsistent-button-fix.js +14 -2
- package/dom/ui/traverse-and-scramble.js +29 -8
- package/eslint.config.js +2 -0
- package/lib/arrays/index.js +5 -62
- package/lib/arrays/item-position.js +23 -0
- package/lib/arrays/join.js +20 -0
- package/lib/arrays/shuffle.js +21 -0
- package/lib/arrays/sort.js +88 -0
- package/lib/arrays/sort.spec.js +123 -0
- package/lib/arrays/unique.js +3 -0
- package/lib/checks.js +42 -0
- package/lib/date/index.js +2 -1
- package/lib/date/time.js +50 -0
- package/lib/form/sanitize.js +68 -35
- package/lib/form/sanitize.spec.js +126 -0
- package/lib/hash.js +18 -0
- package/lib/hash.spec.js +163 -0
- package/lib/index.js +2 -0
- package/lib/numbers/index.js +2 -0
- package/lib/numbers/math.js +9 -0
- package/lib/objects/equal.js +9 -0
- package/lib/objects/index.js +1 -0
- package/lib/objects/mix/mix.js +2 -0
- package/lib/objects/mix/mix.test.js +24 -0
- package/lib/objects/nested-property.js +5 -0
- package/lib/objects/split.js +1 -1
- package/lib/sse.js +61 -0
- package/lib/strings/index.js +1 -0
- package/lib/strings/markdown.js +29 -10
- package/lib/strings/name.js +24 -0
- package/lib/strings/name.test.js +67 -0
- package/node/dirname.js +1 -1
- package/node/file-cache.js +46 -7
- package/node/index.js +1 -0
- package/node/random-string.js +6 -6
- package/node/random-string.test.js +15 -0
- package/node/sanitize.js +13 -0
- package/node/sanitize.spec.js +142 -0
- package/package.json +38 -13
- package/tsconfig.json +23 -0
- package/.eslintrc.cjs +0 -3
- package/.turbo/turbo-lint.log +0 -10
- package/.turbo/turbo-test.log +0 -10
- package/actions/intersection-observer.js +0 -37
- package/actions/masonry.js +0 -75
- package/actions/mutation-observer.js +0 -42
- package/actions/resize-observer.js +0 -33
- package/lib/index.test.js +0 -9
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { getNestedValue } from '../objects/nested-property.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sorts an array of objects, numbers, or strings without mutating the original array.
|
|
5
|
+
* Supports nested properties, multiple sort keys, and various data types.
|
|
6
|
+
*
|
|
7
|
+
* @template T
|
|
8
|
+
* @param {T[]} array - Array to sort
|
|
9
|
+
* @param {Object} [options] - Sort options
|
|
10
|
+
* @param {string|string[]|null} [options.props=null] - Property or array of properties to sort by. If null, sorts simple arrays
|
|
11
|
+
* @param {boolean} [options.reverse=false] - Whether to sort in reverse order
|
|
12
|
+
* @returns {T[]} New sorted array
|
|
13
|
+
* @example
|
|
14
|
+
* // Sort simple array
|
|
15
|
+
* sort([3, 1, 2]) // [1, 2, 3]
|
|
16
|
+
*
|
|
17
|
+
* // Sort objects by single property
|
|
18
|
+
* sort(users, { props: 'name' })
|
|
19
|
+
*
|
|
20
|
+
* // Sort objects by multiple properties
|
|
21
|
+
* sort(users, { props: ['age', 'name'] })
|
|
22
|
+
*
|
|
23
|
+
* // Sort in reverse order
|
|
24
|
+
* sort(users, { props: 'age', reverse: true })
|
|
25
|
+
*
|
|
26
|
+
* // Sort by nested property
|
|
27
|
+
* sort(users, { props: 'address.city' })
|
|
28
|
+
*/
|
|
29
|
+
export function sort(array, { props = null, reverse = false } = {}) {
|
|
30
|
+
const clone = array.slice()
|
|
31
|
+
|
|
32
|
+
// Handle simple arrays (numbers or strings)
|
|
33
|
+
if (props === null) {
|
|
34
|
+
return clone.sort((a, b) => {
|
|
35
|
+
const comparison = compareValues(a, b)
|
|
36
|
+
return reverse ? -comparison : comparison
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Handle objects in arrays
|
|
41
|
+
const properties = Array.isArray(props) ? props : [props]
|
|
42
|
+
|
|
43
|
+
return clone.sort((a, b) => {
|
|
44
|
+
for (const property of properties) {
|
|
45
|
+
const aValue = getNestedValue(a, property)
|
|
46
|
+
const bValue = getNestedValue(b, property)
|
|
47
|
+
|
|
48
|
+
// Skip if values are equal
|
|
49
|
+
if (aValue === bValue) continue
|
|
50
|
+
|
|
51
|
+
// Push null/undefined values to the end
|
|
52
|
+
if (aValue == null) return 1
|
|
53
|
+
if (bValue == null) return -1
|
|
54
|
+
|
|
55
|
+
// Compare values and handle reverse sort
|
|
56
|
+
const comparison = compareValues(aValue, bValue)
|
|
57
|
+
return reverse ? -comparison : comparison
|
|
58
|
+
}
|
|
59
|
+
return 0
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Compares two values for sorting, handling different data types.
|
|
65
|
+
* Supports numbers, strings, dates, and falls back to string comparison.
|
|
66
|
+
*
|
|
67
|
+
* @param {*} a - First value to compare
|
|
68
|
+
* @param {*} b - Second value to compare
|
|
69
|
+
* @returns {number} -1 if a < b, 0 if a === b, 1 if a > b
|
|
70
|
+
* @private
|
|
71
|
+
*/
|
|
72
|
+
function compareValues(a, b) {
|
|
73
|
+
// Compare numbers
|
|
74
|
+
if (typeof a === 'number' && typeof b === 'number') return a - b
|
|
75
|
+
|
|
76
|
+
// Compare strings
|
|
77
|
+
if (typeof a === 'string' && typeof b === 'string') return a.localeCompare(b)
|
|
78
|
+
|
|
79
|
+
// Compare dates
|
|
80
|
+
const aDate = new Date(a)
|
|
81
|
+
const bDate = new Date(b)
|
|
82
|
+
const aTime = aDate.getTime()
|
|
83
|
+
const bTime = bDate.getTime()
|
|
84
|
+
if (!isNaN(aTime) && !isNaN(bTime)) return aTime - bTime
|
|
85
|
+
|
|
86
|
+
// Convert into strings and compare
|
|
87
|
+
return String(a).localeCompare(String(b))
|
|
88
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { sort } from './index.js'
|
|
4
|
+
|
|
5
|
+
describe('Sort by', _ => {
|
|
6
|
+
it('Standard array - numbers', async () => {
|
|
7
|
+
const array = [3, 1, 2]
|
|
8
|
+
const sorted = sort(array)
|
|
9
|
+
const reversed = sort(array, { reverse: true })
|
|
10
|
+
expect(sorted).toEqual([1, 2, 3])
|
|
11
|
+
expect(reversed).toEqual([3, 2, 1])
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('Standard array - strings', async () => {
|
|
15
|
+
const array = ['c', 'a', 'b']
|
|
16
|
+
const sorted = sort(array)
|
|
17
|
+
const reversed = sort(array, { reverse: true })
|
|
18
|
+
expect(sorted).toEqual(['a', 'b', 'c'])
|
|
19
|
+
expect(reversed).toEqual(['c', 'b', 'a'])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('Standard array - dates', async () => {
|
|
23
|
+
const array = [
|
|
24
|
+
new Date('2021-05-18'),
|
|
25
|
+
new Date('2022-01-01'),
|
|
26
|
+
new Date('2025-03-09'),
|
|
27
|
+
]
|
|
28
|
+
const sorted = sort(array)
|
|
29
|
+
const reversed = sort(array, { reverse: true })
|
|
30
|
+
expect(sorted).toEqual([
|
|
31
|
+
new Date('2021-05-18'),
|
|
32
|
+
new Date('2022-01-01'),
|
|
33
|
+
new Date('2025-03-09'),
|
|
34
|
+
])
|
|
35
|
+
expect(reversed).toEqual([
|
|
36
|
+
new Date('2025-03-09'),
|
|
37
|
+
new Date('2022-01-01'),
|
|
38
|
+
new Date('2021-05-18'),
|
|
39
|
+
])
|
|
40
|
+
})
|
|
41
|
+
it('Array of objects — simple integer', async () => {
|
|
42
|
+
const array = [
|
|
43
|
+
{ name: 'Ace', age: 30 },
|
|
44
|
+
{ name: 'Betty', age: 25 },
|
|
45
|
+
{ name: 'Candy', age: 35 },
|
|
46
|
+
]
|
|
47
|
+
const sorted = sort(array, { props: 'age' })
|
|
48
|
+
const reversed = sort(array, { props: 'age', reverse: true })
|
|
49
|
+
expect(sorted).toEqual([
|
|
50
|
+
{ name: 'Betty', age: 25 },
|
|
51
|
+
{ name: 'Ace', age: 30 },
|
|
52
|
+
{ name: 'Candy', age: 35 },
|
|
53
|
+
])
|
|
54
|
+
expect(reversed).toEqual([
|
|
55
|
+
{ name: 'Candy', age: 35 },
|
|
56
|
+
{ name: 'Ace', age: 30 },
|
|
57
|
+
{ name: 'Betty', age: 25 },
|
|
58
|
+
])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('Array of objects — simple string', async () => {
|
|
62
|
+
const array = [
|
|
63
|
+
{ name: 'Ace', age: 30 },
|
|
64
|
+
{ name: 'Betty', age: 25 },
|
|
65
|
+
{ name: 'Candy', age: 35 },
|
|
66
|
+
]
|
|
67
|
+
const sorted = sort(array, { props: 'name' })
|
|
68
|
+
const reversed = sort(array, { props: 'name', reverse: true })
|
|
69
|
+
expect(sorted).toEqual([
|
|
70
|
+
{ name: 'Ace', age: 30 },
|
|
71
|
+
{ name: 'Betty', age: 25 },
|
|
72
|
+
{ name: 'Candy', age: 35 },
|
|
73
|
+
])
|
|
74
|
+
expect(reversed).toEqual([
|
|
75
|
+
{ name: 'Candy', age: 35 },
|
|
76
|
+
{ name: 'Betty', age: 25 },
|
|
77
|
+
{ name: 'Ace', age: 30 },
|
|
78
|
+
])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('Double property', async () => {
|
|
82
|
+
const array = [
|
|
83
|
+
{ name: 'Ace', age: 30 },
|
|
84
|
+
{ name: 'Betty', age: 25 },
|
|
85
|
+
{ name: 'Candy', age: 35 },
|
|
86
|
+
{ name: 'Danny', age: 30 },
|
|
87
|
+
]
|
|
88
|
+
const sorted = sort(array, { props: ['age', 'name'] })
|
|
89
|
+
const reversed = sort(array, { props: ['age', 'name'], reverse: true })
|
|
90
|
+
expect(sorted).toEqual([
|
|
91
|
+
{ name: 'Betty', age: 25 },
|
|
92
|
+
{ name: 'Ace', age: 30 },
|
|
93
|
+
{ name: 'Danny', age: 30 },
|
|
94
|
+
{ name: 'Candy', age: 35 },
|
|
95
|
+
])
|
|
96
|
+
expect(reversed).toEqual([
|
|
97
|
+
{ name: 'Candy', age: 35 },
|
|
98
|
+
{ name: 'Danny', age: 30 },
|
|
99
|
+
{ name: 'Ace', age: 30 },
|
|
100
|
+
{ name: 'Betty', age: 25 },
|
|
101
|
+
])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('Nested value', async () => {
|
|
105
|
+
const array = [
|
|
106
|
+
{ data: { value: 1 } },
|
|
107
|
+
{ data: { value: 2 } },
|
|
108
|
+
{ data: { value: 3 } },
|
|
109
|
+
]
|
|
110
|
+
const sorted = sort(array, { props: 'data.value' })
|
|
111
|
+
const reversed = sort(array, { props: 'data.value', reverse: true })
|
|
112
|
+
expect(sorted).toEqual([
|
|
113
|
+
{ data: { value: 1 } },
|
|
114
|
+
{ data: { value: 2 } },
|
|
115
|
+
{ data: { value: 3 } },
|
|
116
|
+
])
|
|
117
|
+
expect(reversed).toEqual([
|
|
118
|
+
{ data: { value: 3 } },
|
|
119
|
+
{ data: { value: 2 } },
|
|
120
|
+
{ data: { value: 1 } },
|
|
121
|
+
])
|
|
122
|
+
})
|
|
123
|
+
})
|
package/lib/checks.js
CHANGED
|
@@ -1,11 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a value is a plain object (not an array or null)
|
|
3
|
+
* @param {unknown} x - The value to check
|
|
4
|
+
* @returns {boolean} True if the value is a plain object
|
|
5
|
+
* @example
|
|
6
|
+
* isObject({}) // true
|
|
7
|
+
* isObject([]) // false
|
|
8
|
+
* isObject(null) // false
|
|
9
|
+
*/
|
|
1
10
|
export function isObject(x) {
|
|
2
11
|
return typeof x === 'object' && !Array.isArray(x) && x !== null
|
|
3
12
|
}
|
|
4
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Checks if a value is an array
|
|
16
|
+
* @param {unknown} x - The value to check
|
|
17
|
+
* @returns {boolean} True if the value is an array
|
|
18
|
+
* @example
|
|
19
|
+
* isArray([]) // true
|
|
20
|
+
* isArray({}) // false
|
|
21
|
+
*/
|
|
5
22
|
export function isArray(x) {
|
|
6
23
|
return Array.isArray(x)
|
|
7
24
|
}
|
|
8
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Checks if a value is not a plain object
|
|
28
|
+
* @param {unknown} x - The value to check
|
|
29
|
+
* @returns {boolean} True if the value is not a plain object
|
|
30
|
+
* @example
|
|
31
|
+
* notObject([]) // true
|
|
32
|
+
* notObject(null) // true
|
|
33
|
+
* notObject({}) // false
|
|
34
|
+
*/
|
|
9
35
|
export function notObject(x) {
|
|
10
36
|
return !isObject(x)
|
|
11
37
|
}
|
|
38
|
+
|
|
39
|
+
export function getType(x) {
|
|
40
|
+
if (typeof x === 'string') return 'string'
|
|
41
|
+
if (typeof x === 'number') {
|
|
42
|
+
if (Number.isInteger(x)) return 'integer'
|
|
43
|
+
return 'float'
|
|
44
|
+
}
|
|
45
|
+
if (typeof x === 'boolean') return 'boolean'
|
|
46
|
+
if (typeof x === 'function') return 'function'
|
|
47
|
+
if (typeof x === 'undefined') return 'undefined'
|
|
48
|
+
if (typeof x === 'symbol') return 'symbol'
|
|
49
|
+
if (typeof x === 'bigint') return 'bigint'
|
|
50
|
+
if (Array.isArray(x)) return 'array'
|
|
51
|
+
if (typeof x === 'object') return 'object'
|
|
52
|
+
return 'unknown'
|
|
53
|
+
}
|
package/lib/date/index.js
CHANGED
package/lib/date/time.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { splitUnit } from '../numbers/index.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Convert a time string to milliseconds
|
|
5
|
+
* @param {string} arg - The time string to convert
|
|
6
|
+
* @returns {number}
|
|
7
|
+
*/
|
|
8
|
+
export function ms(arg) {
|
|
9
|
+
const [value, unit] = splitUnit(arg)
|
|
10
|
+
if (unit === 'd') return getTimeInMs(value, 'days')
|
|
11
|
+
if (unit === 'day') return getTimeInMs(value, 'days')
|
|
12
|
+
if (unit === 'days') return getTimeInMs(value, 'days')
|
|
13
|
+
if (unit === 'h') return getTimeInMs(value, 'hours')
|
|
14
|
+
if (unit === 'hr') return getTimeInMs(value, 'hours')
|
|
15
|
+
if (unit === 'hour') return getTimeInMs(value, 'hours')
|
|
16
|
+
if (unit === 'hours') return getTimeInMs(value, 'hours')
|
|
17
|
+
if (unit === 'm') return getTimeInMs(value, 'minutes')
|
|
18
|
+
if (unit === 'min') return getTimeInMs(value, 'minutes')
|
|
19
|
+
if (unit === 'minute') return getTimeInMs(value, 'minutes')
|
|
20
|
+
if (unit === 'minutes') return getTimeInMs(value, 'minutes')
|
|
21
|
+
if (unit === 's') return getTimeInMs(value, 'seconds')
|
|
22
|
+
if (unit === 'sec') return getTimeInMs(value, 'seconds')
|
|
23
|
+
if (unit === 'second') return getTimeInMs(value, 'seconds')
|
|
24
|
+
if (unit === 'seconds') return getTimeInMs(value, 'seconds')
|
|
25
|
+
|
|
26
|
+
// The rest are for milliseconds
|
|
27
|
+
return value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function seconds(arg) {
|
|
31
|
+
const value = ms(arg)
|
|
32
|
+
return value / 1000
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getTimeInMs(value, unit = 'ms') {
|
|
36
|
+
switch (unit) {
|
|
37
|
+
case 'ms':
|
|
38
|
+
return value
|
|
39
|
+
case 'seconds':
|
|
40
|
+
return value * 1000
|
|
41
|
+
case 'minutes':
|
|
42
|
+
return value * 1000 * 60
|
|
43
|
+
case 'hours':
|
|
44
|
+
return value * 1000 * 60 * 60
|
|
45
|
+
case 'days':
|
|
46
|
+
return value * 1000 * 60 * 60 * 24
|
|
47
|
+
case 'weeks':
|
|
48
|
+
return value * 1000 * 60 * 60 * 24 * 7
|
|
49
|
+
}
|
|
50
|
+
}
|
package/lib/form/sanitize.js
CHANGED
|
@@ -1,47 +1,80 @@
|
|
|
1
|
-
import DOMPurify from 'dompurify'
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
5
|
-
* @
|
|
6
|
-
* @
|
|
7
|
-
* @returns {string} The sanitized HTML string.
|
|
2
|
+
* @typedef {Object} SanitizeOptions
|
|
3
|
+
* @property {Function} [sanitizer] - Function to sanitize strings
|
|
4
|
+
* @property {*} [additional] - Any additional properties are passed through to the sanitizer function
|
|
8
5
|
*/
|
|
9
|
-
export function sanitize(html, options = {}) {
|
|
10
|
-
return DOMPurify.sanitize(html, options)
|
|
11
|
-
}
|
|
12
6
|
|
|
13
7
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
8
|
+
* Core sanitization function that handles different value types.
|
|
9
|
+
* Throws if no sanitizer function is provided.
|
|
10
|
+
* Any additional properties in options are passed through to the sanitizer function.
|
|
11
|
+
*
|
|
12
|
+
* @param {*} value - The value to sanitize
|
|
13
|
+
* @param {SanitizeOptions} [options={}] - Sanitization options
|
|
14
|
+
* @throws {Error} If sanitizer function is not provided
|
|
15
|
+
* @returns {*} Sanitized value
|
|
16
|
+
* @example
|
|
17
|
+
* // Basic sanitization
|
|
18
|
+
* sanitize('hello', { sanitizer: str => str.trim() })
|
|
19
|
+
*
|
|
20
|
+
* // With additional options passed to sanitizer
|
|
21
|
+
* sanitize('hello', {
|
|
22
|
+
* sanitizer: (str, length, char) => str.padEnd(length, char),
|
|
23
|
+
* length: 10,
|
|
24
|
+
* char: '-'
|
|
25
|
+
* })
|
|
18
26
|
*/
|
|
19
|
-
export function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return [
|
|
23
|
-
key,
|
|
24
|
-
value.map(item => sanitizeEntries(Object.entries(item), options)),
|
|
25
|
-
]
|
|
26
|
-
}
|
|
27
|
+
export function sanitize(value, options = {}) {
|
|
28
|
+
const { sanitizer, ...rest } = options
|
|
29
|
+
if (!sanitizer) throw new Error('sanitizer function is required')
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
if (typeof value === 'string') return sanitizer(value, rest)
|
|
32
|
+
if (Array.isArray(value)) return sanitizeArray(value, { sanitizer, ...rest })
|
|
33
|
+
if (value && typeof value === 'object') {
|
|
34
|
+
return Object.fromEntries(
|
|
35
|
+
Object.entries(value).map(([key, val]) => [
|
|
30
36
|
key,
|
|
31
|
-
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
sanitize(val, { sanitizer, ...rest }),
|
|
38
|
+
]),
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
return value
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Sanitizes an array by recursively sanitizing each element.
|
|
45
|
+
* Throws if no sanitizer function is provided.
|
|
46
|
+
* Any additional properties in options are passed through to the sanitizer function.
|
|
47
|
+
*
|
|
48
|
+
* @param {Array} arr - Array to sanitize
|
|
49
|
+
* @param {SanitizeOptions} options - Sanitization options
|
|
50
|
+
* @throws {Error} If sanitizer function is not provided
|
|
51
|
+
* @returns {Array} New array with sanitized values
|
|
52
|
+
*/
|
|
53
|
+
function sanitizeArray(arr, { sanitizer, ...rest }) {
|
|
54
|
+
if (!sanitizer) throw new Error('sanitizer function is required')
|
|
55
|
+
return arr.map(item => sanitize(item, { sanitizer, ...rest }))
|
|
37
56
|
}
|
|
38
57
|
|
|
39
58
|
/**
|
|
40
|
-
* Sanitizes all string values in an object.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
59
|
+
* Sanitizes all string values in an object recursively.
|
|
60
|
+
* Throws if no sanitizer function is not provided.
|
|
61
|
+
* Any additional properties in options are passed through to the sanitizer function.
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} obj - Object to sanitize
|
|
64
|
+
* @param {SanitizeOptions} [options={}] - Sanitization options
|
|
65
|
+
* @throws {Error} If sanitizer function is not provided
|
|
66
|
+
* @returns {Object} New object with sanitized values
|
|
44
67
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
68
|
+
function sanitizeObject(obj, { sanitizer, ...options } = {}) {
|
|
69
|
+
if (!sanitizer) throw new Error('sanitizer function is required')
|
|
70
|
+
if (Array.isArray(obj)) return sanitizeArray(obj, { sanitizer, ...options })
|
|
71
|
+
|
|
72
|
+
return Object.fromEntries(
|
|
73
|
+
Object.entries(obj).map(([key, value]) => [
|
|
74
|
+
key,
|
|
75
|
+
sanitize(value, { sanitizer, ...options }),
|
|
76
|
+
]),
|
|
77
|
+
)
|
|
47
78
|
}
|
|
79
|
+
|
|
80
|
+
export { sanitizeArray, sanitizeObject }
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import sanitizeHtml from 'sanitize-html'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { sanitize } from './sanitize.js'
|
|
4
|
+
|
|
5
|
+
// Real sanitizer using sanitize-html
|
|
6
|
+
function mockSanitizer(value, options = {}) {
|
|
7
|
+
if (typeof value !== 'string') return value
|
|
8
|
+
return sanitizeHtml(value, {
|
|
9
|
+
allowedTags: ['b', 'i', 'em', 'strong', 'a'],
|
|
10
|
+
allowedAttributes: {
|
|
11
|
+
a: ['href'],
|
|
12
|
+
},
|
|
13
|
+
...options,
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('sanitize', () => {
|
|
18
|
+
it('sanitizes HTML in string values', () => {
|
|
19
|
+
const input = {
|
|
20
|
+
name: '<script>alert("xss")</script>John',
|
|
21
|
+
bio: '<p>Hello <b>World</b> <script>alert("xss")</script></p>',
|
|
22
|
+
link: '<a href="javascript:alert(1)">Click me</a>',
|
|
23
|
+
safeLink: '<a href="https://example.com">Safe link</a>',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const expected = {
|
|
27
|
+
name: 'John',
|
|
28
|
+
bio: 'Hello <b>World</b> ',
|
|
29
|
+
link: '<a>Click me</a>',
|
|
30
|
+
safeLink: '<a href="https://example.com">Safe link</a>',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
expect(sanitize(input, { sanitizer: mockSanitizer })).toEqual(expected)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('sanitizes nested objects with HTML', () => {
|
|
37
|
+
const input = {
|
|
38
|
+
user: {
|
|
39
|
+
name: '<script>alert("xss")</script>John',
|
|
40
|
+
profile: {
|
|
41
|
+
bio: '<p>Hello <b>World</b></p>',
|
|
42
|
+
links: [
|
|
43
|
+
'<a href="javascript:alert(1)">Bad</a>',
|
|
44
|
+
'<a href="https://good.com">Good</a>',
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const expected = {
|
|
51
|
+
user: {
|
|
52
|
+
name: 'John',
|
|
53
|
+
profile: {
|
|
54
|
+
bio: 'Hello <b>World</b>',
|
|
55
|
+
links: ['<a>Bad</a>', '<a href="https://good.com">Good</a>'],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
expect(sanitize(input, { sanitizer: mockSanitizer })).toEqual(expected)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('preserves non-string values and sanitizes arrays correctly', () => {
|
|
64
|
+
const input = {
|
|
65
|
+
name: '<script>alert("xss")</script>John',
|
|
66
|
+
age: 25,
|
|
67
|
+
active: true,
|
|
68
|
+
email: null,
|
|
69
|
+
role: undefined,
|
|
70
|
+
tags: ['<script>alert(1)</script>admin', 'user'],
|
|
71
|
+
numbers: [1, 2, 3],
|
|
72
|
+
mixed: ['<b>bold</b>', 42, null, '<script>alert(1)</script>text'],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const expected = {
|
|
76
|
+
name: 'John',
|
|
77
|
+
age: 25,
|
|
78
|
+
active: true,
|
|
79
|
+
email: null,
|
|
80
|
+
role: undefined,
|
|
81
|
+
tags: ['admin', 'user'],
|
|
82
|
+
numbers: [1, 2, 3],
|
|
83
|
+
mixed: ['<b>bold</b>', 42, null, 'text'],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
expect(sanitize(input, { sanitizer: mockSanitizer })).toEqual(expected)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('throws error when sanitizer is not provided', () => {
|
|
90
|
+
const input = { name: '<script>alert(1)</script>John' }
|
|
91
|
+
expect(() => sanitize(input)).toThrow('sanitizer function is required')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('allows custom sanitizer options', () => {
|
|
95
|
+
const input = {
|
|
96
|
+
content: '<p>Hello <span>World</span> <script>alert(1)</script></p>',
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const defaultOptions = {
|
|
100
|
+
allowedTags: ['b', 'i', 'em', 'strong', 'a'],
|
|
101
|
+
allowedAttributes: {
|
|
102
|
+
a: ['href'],
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Test with custom allowed tags
|
|
107
|
+
const withSpan = sanitize(input, {
|
|
108
|
+
sanitizer: v =>
|
|
109
|
+
mockSanitizer(v, {
|
|
110
|
+
...defaultOptions,
|
|
111
|
+
allowedTags: [...defaultOptions.allowedTags, 'span'],
|
|
112
|
+
}),
|
|
113
|
+
})
|
|
114
|
+
expect(withSpan.content).toBe('Hello <span>World</span> ')
|
|
115
|
+
|
|
116
|
+
// Test with no allowed tags
|
|
117
|
+
const noTags = sanitize(input, {
|
|
118
|
+
sanitizer: v =>
|
|
119
|
+
mockSanitizer(v, {
|
|
120
|
+
...defaultOptions,
|
|
121
|
+
allowedTags: [],
|
|
122
|
+
}),
|
|
123
|
+
})
|
|
124
|
+
expect(noTags.content).toBe('Hello World ')
|
|
125
|
+
})
|
|
126
|
+
})
|
package/lib/hash.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function isHashedValue(value, type = 'sha256') {
|
|
2
|
+
if (typeof value !== 'string') return null
|
|
3
|
+
value = value.trim()
|
|
4
|
+
type = type.toLowerCase()
|
|
5
|
+
|
|
6
|
+
if (type === 'sha256') {
|
|
7
|
+
return value.length === 64 && /^[a-fA-F0-9]{64}$/.test(value)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (type === 'sha1') {
|
|
11
|
+
return value.length === 40 && /^[a-fA-F0-9]{40}$/.test(value)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (type === 'md5') {
|
|
15
|
+
return value.length === 32 && /^[a-fA-F0-9]{32}$/.test(value)
|
|
16
|
+
}
|
|
17
|
+
return null
|
|
18
|
+
}
|