@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.
Files changed (169) hide show
  1. package/CHANGELOG.md +77 -1
  2. package/dist/dom/accessibility.d.ts +28 -0
  3. package/dist/dom/actions/index.d.ts +3 -0
  4. package/dist/dom/actions/masonry.d.ts +3 -0
  5. package/dist/dom/actions/prefer-horizontal-scroll.d.ts +3 -0
  6. package/dist/dom/actions/sticky.d.ts +3 -0
  7. package/dist/dom/bounding-box.d.ts +39 -0
  8. package/dist/dom/clipboard.d.ts +5 -0
  9. package/dist/dom/cookie.d.ts +5 -0
  10. package/dist/dom/css-vars.d.ts +4 -0
  11. package/dist/dom/events.d.ts +18 -0
  12. package/dist/dom/focusable.d.ts +76 -0
  13. package/dist/dom/font-size.d.ts +5 -0
  14. package/dist/dom/get-element.d.ts +100 -0
  15. package/dist/dom/hash.d.ts +1 -0
  16. package/dist/dom/index.d.ts +22 -0
  17. package/dist/dom/keyboard.d.ts +30 -0
  18. package/dist/dom/local-store.d.ts +29 -0
  19. package/dist/dom/media.d.ts +3 -0
  20. package/dist/dom/observers/index.d.ts +3 -0
  21. package/dist/dom/observers/intersection-observer.d.ts +8 -0
  22. package/dist/dom/observers/mutation-observer.d.ts +7 -0
  23. package/dist/dom/observers/observer.d.ts +1 -0
  24. package/dist/dom/observers/resize-observer.d.ts +38 -0
  25. package/dist/dom/pkce.d.ts +6 -0
  26. package/dist/dom/query-params.d.ts +4 -0
  27. package/dist/dom/random-string.d.ts +7 -0
  28. package/dist/dom/sanitize.d.ts +8 -0
  29. package/dist/dom/session-store.d.ts +5 -0
  30. package/dist/dom/trap-focus.d.ts +28 -0
  31. package/dist/dom/ui/aria-current.d.ts +5 -0
  32. package/dist/dom/ui/inconsistent-button-fix.d.ts +12 -0
  33. package/dist/dom/ui/index.d.ts +4 -0
  34. package/dist/dom/ui/scroll-container.d.ts +1 -0
  35. package/dist/dom/ui/traverse-and-scramble.d.ts +19 -0
  36. package/dist/lib/arrays/index.d.ts +5 -0
  37. package/dist/lib/arrays/item-position.d.ts +14 -0
  38. package/dist/lib/arrays/join.d.ts +13 -0
  39. package/dist/lib/arrays/shuffle.d.ts +1 -0
  40. package/dist/lib/arrays/sort.d.ts +30 -0
  41. package/dist/lib/arrays/unique.d.ts +1 -0
  42. package/dist/lib/auth/index.d.ts +1 -0
  43. package/dist/lib/auth/route-manager.d.ts +25 -0
  44. package/dist/lib/checks.d.ts +30 -0
  45. package/dist/lib/date/days.d.ts +5 -0
  46. package/dist/lib/date/index.d.ts +3 -0
  47. package/dist/lib/date/months.d.ts +9 -0
  48. package/dist/lib/date/time.d.ts +8 -0
  49. package/dist/lib/form/form-data.d.ts +7 -0
  50. package/dist/lib/form/index.d.ts +2 -0
  51. package/dist/lib/form/sanitize.d.ts +58 -0
  52. package/dist/lib/functions/debounce.d.ts +1 -0
  53. package/dist/lib/functions/env.d.ts +1 -0
  54. package/dist/lib/functions/functional.d.ts +4 -0
  55. package/dist/lib/functions/index.d.ts +5 -0
  56. package/dist/lib/functions/throttle.d.ts +1 -0
  57. package/dist/lib/functions/timeout.d.ts +3 -0
  58. package/dist/lib/hash.d.ts +1 -0
  59. package/dist/lib/index.d.ts +14 -0
  60. package/dist/lib/numbers/index.d.ts +2 -0
  61. package/dist/lib/numbers/math.d.ts +2 -0
  62. package/dist/lib/objects/camelcase-keys.d.ts +1 -0
  63. package/dist/lib/objects/empty.d.ts +2 -0
  64. package/dist/lib/objects/equal.d.ts +2 -0
  65. package/dist/lib/objects/extend.d.ts +4 -0
  66. package/dist/lib/objects/flatten.d.ts +4 -0
  67. package/dist/lib/objects/index.d.ts +13 -0
  68. package/dist/lib/objects/json.d.ts +1 -0
  69. package/dist/lib/objects/loop.d.ts +2 -0
  70. package/dist/lib/objects/mix/mix.d.ts +1 -0
  71. package/dist/lib/objects/nested-property.d.ts +22 -0
  72. package/dist/lib/objects/normalize-object.d.ts +1 -0
  73. package/dist/lib/objects/omit-empty.d.ts +1 -0
  74. package/dist/lib/objects/size.d.ts +1 -0
  75. package/dist/lib/objects/split.d.ts +12 -0
  76. package/dist/lib/promises/index.d.ts +1 -0
  77. package/dist/lib/promises/reject.d.ts +3 -0
  78. package/dist/lib/sse.d.ts +16 -0
  79. package/dist/lib/strings/convert-case/convert-case.d.ts +8 -0
  80. package/dist/lib/strings/index.d.ts +5 -0
  81. package/dist/lib/strings/markdown.d.ts +23 -0
  82. package/dist/lib/strings/name.d.ts +5 -0
  83. package/dist/lib/strings/pluralize.d.ts +1 -0
  84. package/dist/lib/strings/query-string.d.ts +8 -0
  85. package/dist/lib/style/index.d.ts +1 -0
  86. package/dist/lib/symbols/index.d.ts +1 -0
  87. package/dist/lib/symbols/symbols.d.ts +2 -0
  88. package/dist/node/common.d.ts +1 -0
  89. package/dist/node/dirname.d.ts +7 -0
  90. package/dist/node/file-cache.d.ts +54 -0
  91. package/dist/node/file.d.ts +2 -0
  92. package/dist/node/hash.d.ts +1 -0
  93. package/dist/node/index.d.ts +7 -0
  94. package/dist/node/pkce.d.ts +6 -0
  95. package/dist/node/random-string.d.ts +1 -0
  96. package/dist/node/sanitize.d.ts +8 -0
  97. package/dom/accessibility.js +46 -13
  98. package/{actions → dom/actions}/index.js +0 -2
  99. package/dom/actions/masonry.js +42 -0
  100. package/{actions → dom/actions}/prefer-horizontal-scroll.js +8 -8
  101. package/{actions → dom/actions}/sticky.js +3 -6
  102. package/dom/bounding-box.js +35 -7
  103. package/dom/cookie.js +8 -7
  104. package/dom/css-vars.js +8 -0
  105. package/dom/events.js +2 -1
  106. package/dom/focusable.js +119 -10
  107. package/dom/font-size.js +21 -10
  108. package/dom/get-element.js +112 -5
  109. package/dom/index.js +3 -0
  110. package/dom/keyboard.js +13 -8
  111. package/dom/local-store.js +37 -0
  112. package/dom/media.js +1 -0
  113. package/dom/observers/index.js +3 -0
  114. package/dom/observers/intersection-observer.js +43 -0
  115. package/dom/observers/mutation-observer.js +51 -0
  116. package/dom/observers/observer.js +18 -0
  117. package/dom/observers/resize-observer.js +64 -0
  118. package/dom/random-string.js +1 -1
  119. package/dom/random-string.test.js +13 -0
  120. package/dom/sanitize.js +16 -0
  121. package/dom/trap-focus.js +18 -3
  122. package/dom/ui/inconsistent-button-fix.js +14 -2
  123. package/dom/ui/traverse-and-scramble.js +29 -8
  124. package/eslint.config.js +2 -0
  125. package/lib/arrays/index.js +5 -62
  126. package/lib/arrays/item-position.js +23 -0
  127. package/lib/arrays/join.js +20 -0
  128. package/lib/arrays/shuffle.js +21 -0
  129. package/lib/arrays/sort.js +88 -0
  130. package/lib/arrays/sort.spec.js +123 -0
  131. package/lib/arrays/unique.js +3 -0
  132. package/lib/checks.js +42 -0
  133. package/lib/date/index.js +2 -1
  134. package/lib/date/time.js +50 -0
  135. package/lib/form/sanitize.js +68 -35
  136. package/lib/form/sanitize.spec.js +126 -0
  137. package/lib/hash.js +18 -0
  138. package/lib/hash.spec.js +163 -0
  139. package/lib/index.js +2 -0
  140. package/lib/numbers/index.js +2 -0
  141. package/lib/numbers/math.js +9 -0
  142. package/lib/objects/equal.js +9 -0
  143. package/lib/objects/index.js +1 -0
  144. package/lib/objects/mix/mix.js +2 -0
  145. package/lib/objects/mix/mix.test.js +24 -0
  146. package/lib/objects/nested-property.js +5 -0
  147. package/lib/objects/split.js +1 -1
  148. package/lib/sse.js +61 -0
  149. package/lib/strings/index.js +1 -0
  150. package/lib/strings/markdown.js +29 -10
  151. package/lib/strings/name.js +24 -0
  152. package/lib/strings/name.test.js +67 -0
  153. package/node/dirname.js +1 -1
  154. package/node/file-cache.js +46 -7
  155. package/node/index.js +1 -0
  156. package/node/random-string.js +6 -6
  157. package/node/random-string.test.js +15 -0
  158. package/node/sanitize.js +13 -0
  159. package/node/sanitize.spec.js +142 -0
  160. package/package.json +38 -13
  161. package/tsconfig.json +23 -0
  162. package/.eslintrc.cjs +0 -3
  163. package/.turbo/turbo-lint.log +0 -10
  164. package/.turbo/turbo-test.log +0 -10
  165. package/actions/intersection-observer.js +0 -37
  166. package/actions/masonry.js +0 -75
  167. package/actions/mutation-observer.js +0 -42
  168. package/actions/resize-observer.js +0 -33
  169. 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
+ })
@@ -0,0 +1,3 @@
1
+ export function uniqueArray(array) {
2
+ return Array.from(new Set(array))
3
+ }
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
@@ -1,2 +1,3 @@
1
- export * from './months.js'
2
1
  export * from './days.js'
2
+ export * from './months.js'
3
+ export * from './time.js'
@@ -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
+ }
@@ -1,47 +1,80 @@
1
- import DOMPurify from 'dompurify'
2
-
3
1
  /**
4
- * Sanitizes HTML string using DOMPurify.
5
- * @param {string} html - The HTML string to sanitize.
6
- * @param {Object} [options={}] - DOMPurify configuration options.
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
- * Recursively sanitizes entries in an object or array.
15
- * @param {Array} entries - The entries to sanitize.
16
- * @param {Object} [options={}] - DOMPurify configuration options.
17
- * @returns {Array} The sanitized entries.
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 sanitizeEntries(entries, options = {}) {
20
- return entries.map(([key, value]) => {
21
- if (Array.isArray(value)) {
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
- if (value && typeof value === 'object') {
29
- return [
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
- Object.fromEntries(sanitizeEntries(Object.entries(value), options)),
32
- ]
33
- }
34
-
35
- return [key, sanitize(value, options)]
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
- * @param {Object} obj - The object to sanitize.
42
- * @param {Object} [options={}] - DOMPurify configuration options.
43
- * @returns {Object} A new object with all string values sanitized.
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
- export function sanitizeObject(obj, options = {}) {
46
- return Object.fromEntries(sanitizeEntries(Object.entries(obj), options))
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
+ }