@splendidlabz/utils 1.5.0 → 1.5.1

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 (156) hide show
  1. package/CHANGELOG.md +71 -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 +11 -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 +29 -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/index.d.ts +13 -0
  59. package/dist/lib/numbers/index.d.ts +2 -0
  60. package/dist/lib/numbers/math.d.ts +2 -0
  61. package/dist/lib/objects/camelcase-keys.d.ts +1 -0
  62. package/dist/lib/objects/empty.d.ts +2 -0
  63. package/dist/lib/objects/equal.d.ts +2 -0
  64. package/dist/lib/objects/extend.d.ts +4 -0
  65. package/dist/lib/objects/flatten.d.ts +4 -0
  66. package/dist/lib/objects/index.d.ts +13 -0
  67. package/dist/lib/objects/json.d.ts +1 -0
  68. package/dist/lib/objects/loop.d.ts +2 -0
  69. package/dist/lib/objects/mix/mix.d.ts +1 -0
  70. package/dist/lib/objects/nested-property.d.ts +22 -0
  71. package/dist/lib/objects/normalize-object.d.ts +1 -0
  72. package/dist/lib/objects/omit-empty.d.ts +1 -0
  73. package/dist/lib/objects/size.d.ts +1 -0
  74. package/dist/lib/objects/split.d.ts +10 -0
  75. package/dist/lib/promises/index.d.ts +1 -0
  76. package/dist/lib/promises/reject.d.ts +3 -0
  77. package/dist/lib/sse.d.ts +16 -0
  78. package/dist/lib/strings/convert-case/convert-case.d.ts +8 -0
  79. package/dist/lib/strings/index.d.ts +4 -0
  80. package/dist/lib/strings/markdown.d.ts +23 -0
  81. package/dist/lib/strings/pluralize.d.ts +1 -0
  82. package/dist/lib/strings/query-string.d.ts +8 -0
  83. package/dist/lib/style/index.d.ts +1 -0
  84. package/dist/lib/symbols/index.d.ts +1 -0
  85. package/dist/lib/symbols/symbols.d.ts +2 -0
  86. package/dist/node/common.d.ts +1 -0
  87. package/dist/node/dirname.d.ts +7 -0
  88. package/dist/node/file-cache.d.ts +54 -0
  89. package/dist/node/file.d.ts +2 -0
  90. package/dist/node/hash.d.ts +1 -0
  91. package/dist/node/index.d.ts +7 -0
  92. package/dist/node/pkce.d.ts +6 -0
  93. package/dist/node/random-string.d.ts +2 -0
  94. package/dist/node/sanitize.d.ts +8 -0
  95. package/dom/accessibility.js +46 -13
  96. package/{actions → dom/actions}/index.js +0 -2
  97. package/dom/actions/masonry.js +42 -0
  98. package/{actions → dom/actions}/prefer-horizontal-scroll.js +8 -8
  99. package/{actions → dom/actions}/sticky.js +3 -6
  100. package/dom/bounding-box.js +35 -7
  101. package/dom/css-vars.js +8 -0
  102. package/dom/events.js +2 -1
  103. package/dom/focusable.js +119 -10
  104. package/dom/font-size.js +21 -10
  105. package/dom/get-element.js +112 -5
  106. package/dom/index.js +3 -0
  107. package/dom/keyboard.js +13 -8
  108. package/dom/local-store.js +37 -0
  109. package/dom/media.js +1 -0
  110. package/dom/observers/index.js +3 -0
  111. package/dom/observers/intersection-observer.js +43 -0
  112. package/dom/observers/mutation-observer.js +51 -0
  113. package/dom/observers/observer.js +18 -0
  114. package/dom/observers/resize-observer.js +64 -0
  115. package/dom/sanitize.js +16 -0
  116. package/dom/trap-focus.js +18 -3
  117. package/dom/ui/inconsistent-button-fix.js +14 -2
  118. package/dom/ui/traverse-and-scramble.js +29 -8
  119. package/eslint.config.js +2 -0
  120. package/lib/arrays/index.js +5 -62
  121. package/lib/arrays/item-position.js +23 -0
  122. package/lib/arrays/join.js +20 -0
  123. package/lib/arrays/shuffle.js +21 -0
  124. package/lib/arrays/sort.js +88 -0
  125. package/lib/arrays/sort.spec.js +123 -0
  126. package/lib/arrays/unique.js +3 -0
  127. package/lib/checks.js +26 -0
  128. package/lib/date/index.js +2 -1
  129. package/lib/date/time.js +50 -0
  130. package/lib/form/sanitize.js +68 -35
  131. package/lib/form/sanitize.spec.js +126 -0
  132. package/lib/index.js +1 -0
  133. package/lib/numbers/index.js +2 -0
  134. package/lib/numbers/math.js +9 -0
  135. package/lib/objects/equal.js +9 -0
  136. package/lib/objects/index.js +1 -0
  137. package/lib/objects/mix/mix.js +2 -0
  138. package/lib/objects/mix/mix.test.js +16 -0
  139. package/lib/objects/nested-property.js +5 -0
  140. package/lib/sse.js +61 -0
  141. package/lib/strings/markdown.js +29 -10
  142. package/node/dirname.js +1 -1
  143. package/node/file-cache.js +46 -7
  144. package/node/index.js +1 -0
  145. package/node/sanitize.js +13 -0
  146. package/node/sanitize.spec.js +142 -0
  147. package/package.json +38 -13
  148. package/tsconfig.json +23 -0
  149. package/.eslintrc.cjs +0 -3
  150. package/.turbo/turbo-lint.log +0 -10
  151. package/.turbo/turbo-test.log +0 -10
  152. package/actions/intersection-observer.js +0 -37
  153. package/actions/masonry.js +0 -75
  154. package/actions/mutation-observer.js +0 -42
  155. package/actions/resize-observer.js +0 -33
  156. package/lib/index.test.js +0 -9
@@ -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,37 @@
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
  }
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/index.js CHANGED
@@ -7,6 +7,7 @@ export * from './functions/index.js'
7
7
  export * from './numbers/index.js'
8
8
  export * from './objects/index.js'
9
9
  export * from './promises/index.js'
10
+ export * from './sse.js'
10
11
  export * from './strings/index.js'
11
12
  export * from './style/index.js'
12
13
  export * from './symbols/index.js'
@@ -1,3 +1,5 @@
1
+ export * from './math.js'
2
+
1
3
  export function splitUnit(string) {
2
4
  // If there are no units, return the number straight.
3
5
  if (typeof string === 'number') return [string, null]
@@ -0,0 +1,9 @@
1
+ // Convert radians to degrees
2
+ export function toDegrees(angle) {
3
+ return angle * (180 / Math.PI)
4
+ }
5
+
6
+ // Convert degrees to radians
7
+ export function toRadians(angle) {
8
+ return angle * (Math.PI / 180)
9
+ }
@@ -0,0 +1,9 @@
1
+ import deepEqual from 'deep-eql'
2
+
3
+ export function isEqual(a, b) {
4
+ return deepEqual(a, b)
5
+ }
6
+
7
+ export function notEqual(a, b) {
8
+ return !isEqual(a, b)
9
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './camelcase-keys.js'
2
2
  export * from './empty.js'
3
+ export * from './equal.js'
3
4
  export * from './extend.js'
4
5
  export * from './flatten.js'
5
6
  export * from './json.js'
@@ -1,6 +1,8 @@
1
1
  export function mix(...sources) {
2
2
  const result = {}
3
3
  for (const source of sources) {
4
+ // Skip null or undefined sources
5
+ if (source == null) continue
4
6
  merge(result, source)
5
7
  }
6
8
  return result
@@ -322,3 +322,19 @@ it('Safe Merge', () => {
322
322
  expect(mixed.__proto__).toEqual({})
323
323
  /* eslint-enable */
324
324
  })
325
+
326
+ it('should skip null and undefined sources', () => {
327
+ const obj1 = { a: 1 }
328
+ const obj2 = { b: 2 }
329
+ const expected = { a: 1, b: 2 }
330
+
331
+ expect(mix(obj1, null, obj2)).toEqual(expected)
332
+ expect(mix(null, obj1, obj2)).toEqual(expected)
333
+ expect(mix(obj1, obj2, null)).toEqual(expected)
334
+
335
+ expect(mix(obj1, undefined, obj2)).toEqual(expected)
336
+ expect(mix(undefined, obj1, obj2)).toEqual(expected)
337
+ expect(mix(obj1, obj2, undefined)).toEqual(expected)
338
+
339
+ expect(mix(null, obj1, undefined, obj2, null)).toEqual(expected)
340
+ })
@@ -34,3 +34,8 @@ export function getNestedProperty2(object, path) {
34
34
  const pathArray = path.split('.')
35
35
  return getNestedProperty(object, pathArray)
36
36
  }
37
+
38
+ // Same as dot syntax, but terser and more readable name
39
+ export function getNestedValue(object, path) {
40
+ return path.split('.').reduce((acc, part) => acc?.[part], object)
41
+ }
package/lib/sse.js ADDED
@@ -0,0 +1,61 @@
1
+ // Server Sent Events (SSE)
2
+ import { parseJSON } from './objects/json.js'
3
+ import { omitEmpty } from './objects/omit-empty.js'
4
+
5
+ export function createSSE({ event, data, id, retry }) {
6
+ const eventString = event ? `event: ${event}\n` : ''
7
+ const idString = id ? `id: ${id}\n` : ''
8
+ const retryString = retry ? `retry: ${retry}\n` : ''
9
+
10
+ if (typeof data === 'object') data = JSON.stringify(data)
11
+ const dataString = data ? `data: ${data}\n` : ''
12
+
13
+ return `${eventString}${dataString}${idString}${retryString}\n`
14
+ }
15
+
16
+ /**
17
+ * Parses a single SSE message into a structured object
18
+ * @param {string} message - The raw SSE message
19
+ * @returns {Object|null} The parsed message or null if invalid
20
+ * @property {string} [event] - The event type if specified
21
+ * @property {string|Object} data - The message data (parsed as JSON if possible)
22
+ * @property {string} [id] - The message ID if specified
23
+ * @property {number} [retry] - The retry interval if specified
24
+ */
25
+ export function parseSSE(message) {
26
+ const lines = message.split('\n')
27
+ const result = {
28
+ event: 'message', // Default event type
29
+ data: '',
30
+ id: null,
31
+ retry: null
32
+ }
33
+
34
+ for (const line of lines) {
35
+ if (line.startsWith('data:')) {
36
+ const data = line.slice(5).trim()
37
+ result.data = parseJSON(data)
38
+ continue
39
+ }
40
+
41
+ if (line.startsWith('event:')) {
42
+ result.event = line.slice(6).trim()
43
+ continue
44
+ }
45
+
46
+ if (line.startsWith('id:')) {
47
+ result.id = line.slice(3).trim()
48
+ continue
49
+ }
50
+
51
+ if (line.startsWith('retry:')) {
52
+ result.retry = parseInt(line.slice(6).trim(), 10)
53
+ continue
54
+ }
55
+ }
56
+
57
+
58
+ const ret = omitEmpty(result)
59
+ return ret.data ? ret : null
60
+ }
61
+
@@ -1,13 +1,34 @@
1
+ import { marked } from 'marked'
2
+ import { gfmHeadingId } from 'marked-gfm-heading-id'
3
+ import { mangle } from 'marked-mangle'
4
+
1
5
  /**
2
- * Removes leading whitespaces from a Markdown content so they can be rendered appropriately.
3
- * @param {string} content - The content to treat
4
- * @param {Boolean} options.inline - Whether content is inline, not block
5
- * @returns
6
+ * Converts markdown content to HTML with whitespace handling
7
+ * @param {string} content - Markdown content to convert
8
+ * @param {Object} [options={}] - Conversion options
9
+ * @param {boolean} [options.inline=false] - Whether content should be treated as inline markdown
10
+ * @returns {string|Promise<string>} HTML string or Promise that resolves to HTML string
6
11
  */
7
- export function treatMarkdownWhitespace(
8
- content,
9
- { inline, isSlot = false } = {},
10
- ) {
12
+ export function markdown(content, options = {}) {
13
+ const defaultOptions = { inline: false }
14
+ const opts = { ...defaultOptions, ...options }
15
+
16
+ // Prepare Marked
17
+ marked.use(mangle())
18
+ marked.use(gfmHeadingId())
19
+
20
+ const { content: treated } = treatMarkdownWhitespace(content, opts)
21
+ return marked.parse(treated)
22
+ }
23
+
24
+ /**
25
+ * Handles whitespace in markdown content to ensure proper rendering
26
+ * @param {string} content - Markdown content to process
27
+ * @param {Object} [options={}] - Processing options
28
+ * @param {boolean} [options.inline=false] - Whether content is inline markdown
29
+ * @returns {{ content: string, inline: boolean }} Processed content and inline status
30
+ */
31
+ export function treatMarkdownWhitespace(content, { inline } = {}) {
11
32
  if (!content) return { content: '', inline }
12
33
  const lines = content.split('\n')
13
34
 
@@ -19,9 +40,7 @@ export function treatMarkdownWhitespace(
19
40
  const firstLineWithLeadingSpacesIndex = lines.findIndex(line =>
20
41
  line.match(/^\s+/),
21
42
  )
22
-
23
43
  const firstLineWithLeadingSpaces = lines[firstLineWithLeadingSpacesIndex]
24
-
25
44
  const indentation =
26
45
  firstLineWithLeadingSpaces?.match(/^\s+/)?.[0]?.length ?? 0
27
46
 
package/node/dirname.js CHANGED
@@ -1,5 +1,5 @@
1
+ import path from 'node:path'
1
2
  import { fileURLToPath } from 'node:url'
2
- import path from 'path'
3
3
 
4
4
  /**
5
5
  * Returns __dirname