@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.
- package/CHANGELOG.md +71 -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 +11 -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 +29 -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/index.d.ts +13 -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 +10 -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 +4 -0
- package/dist/lib/strings/markdown.d.ts +23 -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 +2 -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/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/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 +26 -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/index.js +1 -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 +16 -0
- package/lib/objects/nested-property.js +5 -0
- package/lib/sse.js +61 -0
- package/lib/strings/markdown.js +29 -10
- package/node/dirname.js +1 -1
- package/node/file-cache.js +46 -7
- package/node/index.js +1 -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,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,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
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/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'
|
package/lib/numbers/index.js
CHANGED
package/lib/objects/index.js
CHANGED
package/lib/objects/mix/mix.js
CHANGED
|
@@ -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
|
+
|
package/lib/strings/markdown.js
CHANGED
|
@@ -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
|
-
*
|
|
3
|
-
* @param {string} content -
|
|
4
|
-
* @param {
|
|
5
|
-
* @
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|