@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
package/lib/hash.spec.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { isHashedValue } from './hash.js'
|
|
3
|
+
|
|
4
|
+
describe('isHashedValue', () => {
|
|
5
|
+
describe('SHA256 hashes', () => {
|
|
6
|
+
it('returns true for valid SHA256 hash', () => {
|
|
7
|
+
const validSha256 =
|
|
8
|
+
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
|
|
9
|
+
expect(isHashedValue(validSha256, 'sha256')).toBe(true)
|
|
10
|
+
expect(isHashedValue(validSha256)).toBe(true) // default type
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('returns true for valid SHA256 hash with uppercase letters', () => {
|
|
14
|
+
const validSha256 =
|
|
15
|
+
'A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86F7F7A27AE3'
|
|
16
|
+
expect(isHashedValue(validSha256, 'sha256')).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('returns true for valid SHA256 hash with mixed case', () => {
|
|
20
|
+
const validSha256 =
|
|
21
|
+
'A665a45920422F9d417E4867efdc4FB8a04a1F3fff1FA07e998E86f7f7A27ae3'
|
|
22
|
+
expect(isHashedValue(validSha256, 'sha256')).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('returns false for SHA256 hash with wrong length', () => {
|
|
26
|
+
const shortHash =
|
|
27
|
+
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae'
|
|
28
|
+
const longHash =
|
|
29
|
+
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae33'
|
|
30
|
+
expect(isHashedValue(shortHash, 'sha256')).toBe(false)
|
|
31
|
+
expect(isHashedValue(longHash, 'sha256')).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('returns false for SHA256 hash with invalid characters', () => {
|
|
35
|
+
const invalidHash =
|
|
36
|
+
'g665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
|
|
37
|
+
expect(isHashedValue(invalidHash, 'sha256')).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('SHA1 hashes', () => {
|
|
42
|
+
it('returns true for valid SHA1 hash', () => {
|
|
43
|
+
const validSha1 = '356a192b7913b04c54574d18c28d46e6395428ab'
|
|
44
|
+
expect(isHashedValue(validSha1, 'sha1')).toBe(true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns true for valid SHA1 hash with uppercase letters', () => {
|
|
48
|
+
const validSha1 = '356A192B7913B04C54574D18C28D46E6395428AB'
|
|
49
|
+
expect(isHashedValue(validSha1, 'sha1')).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('returns false for SHA1 hash with wrong length', () => {
|
|
53
|
+
const shortHash = '356a192b7913b04c54574d18c28d46e6395428a'
|
|
54
|
+
const longHash = '356a192b7913b04c54574d18c28d46e6395428abb'
|
|
55
|
+
expect(isHashedValue(shortHash, 'sha1')).toBe(false)
|
|
56
|
+
expect(isHashedValue(longHash, 'sha1')).toBe(false)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('returns false for SHA1 hash with invalid characters', () => {
|
|
60
|
+
const invalidHash = '356g192b7913b04c54574d18c28d46e6395428ab'
|
|
61
|
+
expect(isHashedValue(invalidHash, 'sha1')).toBe(false)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('MD5 hashes', () => {
|
|
66
|
+
it('returns true for valid MD5 hash', () => {
|
|
67
|
+
const validMd5 = '5d41402abc4b2a76b9719d911017c592'
|
|
68
|
+
expect(isHashedValue(validMd5, 'md5')).toBe(true)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns true for valid MD5 hash with uppercase letters', () => {
|
|
72
|
+
const validMd5 = '5D41402ABC4B2A76B9719D911017C592'
|
|
73
|
+
expect(isHashedValue(validMd5, 'md5')).toBe(true)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('returns false for MD5 hash with wrong length', () => {
|
|
77
|
+
const shortHash = '5d41402abc4b2a76b9719d911017c59'
|
|
78
|
+
const longHash = '5d41402abc4b2a76b9719d911017c5922'
|
|
79
|
+
expect(isHashedValue(shortHash, 'md5')).toBe(false)
|
|
80
|
+
expect(isHashedValue(longHash, 'md5')).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('returns false for MD5 hash with invalid characters', () => {
|
|
84
|
+
const invalidHash = '5g41402abc4b2a76b9719d911017c592'
|
|
85
|
+
expect(isHashedValue(invalidHash, 'md5')).toBe(false)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('Type parameter handling', () => {
|
|
90
|
+
it('handles case insensitive type parameter', () => {
|
|
91
|
+
const validSha256 =
|
|
92
|
+
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
|
|
93
|
+
expect(isHashedValue(validSha256, 'SHA256')).toBe(true)
|
|
94
|
+
expect(isHashedValue(validSha256, 'Sha256')).toBe(true)
|
|
95
|
+
expect(isHashedValue(validSha256, 'sHa256')).toBe(true)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('returns null for unsupported hash type', () => {
|
|
99
|
+
const validSha256 =
|
|
100
|
+
'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
|
|
101
|
+
expect(isHashedValue(validSha256, 'sha512')).toBe(null)
|
|
102
|
+
expect(isHashedValue(validSha256, 'blake2b')).toBe(null)
|
|
103
|
+
expect(isHashedValue(validSha256, 'unknown')).toBe(null)
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('Input validation', () => {
|
|
108
|
+
it('returns null for non-string inputs', () => {
|
|
109
|
+
expect(isHashedValue(null)).toBe(null)
|
|
110
|
+
expect(isHashedValue(undefined)).toBe(null)
|
|
111
|
+
expect(isHashedValue(123)).toBe(null)
|
|
112
|
+
expect(isHashedValue([])).toBe(null)
|
|
113
|
+
expect(isHashedValue({})).toBe(null)
|
|
114
|
+
expect(isHashedValue(true)).toBe(null)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('handles empty string', () => {
|
|
118
|
+
expect(isHashedValue('')).toBe(false)
|
|
119
|
+
expect(isHashedValue('', 'sha1')).toBe(false)
|
|
120
|
+
expect(isHashedValue('', 'md5')).toBe(false)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('trims whitespace from input', () => {
|
|
124
|
+
const validSha256 =
|
|
125
|
+
' a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3 '
|
|
126
|
+
expect(isHashedValue(validSha256, 'sha256')).toBe(true)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('returns false for strings with only whitespace', () => {
|
|
130
|
+
expect(isHashedValue(' ')).toBe(false)
|
|
131
|
+
expect(isHashedValue('\t\n')).toBe(false)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('Edge cases', () => {
|
|
136
|
+
it('returns false for strings that are all zeros but correct length', () => {
|
|
137
|
+
const zeroSha256 =
|
|
138
|
+
'0000000000000000000000000000000000000000000000000000000000000000'
|
|
139
|
+
const zeroSha1 = '0000000000000000000000000000000000000000'
|
|
140
|
+
const zeroMd5 = '00000000000000000000000000000000'
|
|
141
|
+
|
|
142
|
+
expect(isHashedValue(zeroSha256, 'sha256')).toBe(true)
|
|
143
|
+
expect(isHashedValue(zeroSha1, 'sha1')).toBe(true)
|
|
144
|
+
expect(isHashedValue(zeroMd5, 'md5')).toBe(true)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('returns false for correct length but non-hex characters', () => {
|
|
148
|
+
const nonHexSha256 =
|
|
149
|
+
'g665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3'
|
|
150
|
+
const nonHexSha1 = '356g192b7913b04c54574d18c28d46e6395428ab'
|
|
151
|
+
const nonHexMd5 = '5g41402abc4b2a76b9719d911017c592'
|
|
152
|
+
|
|
153
|
+
expect(isHashedValue(nonHexSha256, 'sha256')).toBe(false)
|
|
154
|
+
expect(isHashedValue(nonHexSha1, 'sha1')).toBe(false)
|
|
155
|
+
expect(isHashedValue(nonHexMd5, 'md5')).toBe(false)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('handles special characters correctly', () => {
|
|
159
|
+
const specialChars = '!@#$%^&*()_+-=[]{}|;:,.<>?/~`'
|
|
160
|
+
expect(isHashedValue(specialChars, 'sha256')).toBe(false)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
})
|
package/lib/index.js
CHANGED
|
@@ -4,9 +4,11 @@ export * from './checks.js'
|
|
|
4
4
|
export * from './date/index.js'
|
|
5
5
|
export * from './form/index.js'
|
|
6
6
|
export * from './functions/index.js'
|
|
7
|
+
export * from './hash.js'
|
|
7
8
|
export * from './numbers/index.js'
|
|
8
9
|
export * from './objects/index.js'
|
|
9
10
|
export * from './promises/index.js'
|
|
11
|
+
export * from './sse.js'
|
|
10
12
|
export * from './strings/index.js'
|
|
11
13
|
export * from './style/index.js'
|
|
12
14
|
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,27 @@ 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
|
+
})
|
|
341
|
+
|
|
342
|
+
it('should not skip null and false properties', () => {
|
|
343
|
+
const obj1 = { a: 1, b: 2, c: 3 }
|
|
344
|
+
const obj2 = { a: null, b: false, c: undefined }
|
|
345
|
+
const expected = { a: null, b: false, c: undefined }
|
|
346
|
+
|
|
347
|
+
expect(mix(obj1, obj2)).toEqual(expected)
|
|
348
|
+
})
|
|
@@ -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/objects/split.js
CHANGED
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/index.js
CHANGED
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
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function parseFullName(name) {
|
|
2
|
+
const parts = name.trim().split(/\s+/)
|
|
3
|
+
|
|
4
|
+
if (parts.length === 1) {
|
|
5
|
+
return { firstName: parts[0], middleName: '', lastName: '' }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (parts.length === 2) {
|
|
9
|
+
return { firstName: parts[0], middleName: '', lastName: parts[1] }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (parts.length === 3) {
|
|
13
|
+
const firstName = parts.slice(0, 2).join(' ')
|
|
14
|
+
const lastName = parts[2]
|
|
15
|
+
return { firstName, middleName: '', lastName }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 4+ words: first 2 words = firstName, last word = lastName, middle = middleName
|
|
19
|
+
const firstName = parts.slice(0, 2).join(' ')
|
|
20
|
+
const lastName = parts[parts.length - 1]
|
|
21
|
+
const middleName = parts.slice(2, -1).join(' ')
|
|
22
|
+
|
|
23
|
+
return { firstName, middleName, lastName }
|
|
24
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { parseFullName } from './name.js'
|
|
3
|
+
|
|
4
|
+
describe('parseFullName', () => {
|
|
5
|
+
test('handles single name', () => {
|
|
6
|
+
const result = parseFullName('John')
|
|
7
|
+
expect(result).toEqual({
|
|
8
|
+
firstName: 'John',
|
|
9
|
+
middleName: '',
|
|
10
|
+
lastName: '',
|
|
11
|
+
})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('handles two names', () => {
|
|
15
|
+
const result = parseFullName('John Doe')
|
|
16
|
+
expect(result).toEqual({
|
|
17
|
+
firstName: 'John',
|
|
18
|
+
middleName: '',
|
|
19
|
+
lastName: 'Doe',
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('handles three names (Chinese pattern)', () => {
|
|
24
|
+
const result = parseFullName('Wee Keat Liew')
|
|
25
|
+
expect(result).toEqual({
|
|
26
|
+
firstName: 'Wee Keat',
|
|
27
|
+
middleName: '',
|
|
28
|
+
lastName: 'Liew',
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('handles four names with middle name', () => {
|
|
33
|
+
const result = parseFullName('Mary Jane Michael Doe')
|
|
34
|
+
expect(result).toEqual({
|
|
35
|
+
firstName: 'Mary Jane',
|
|
36
|
+
middleName: 'Michael',
|
|
37
|
+
lastName: 'Doe',
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('handles five names with multiple middle names', () => {
|
|
42
|
+
const result = parseFullName('Mary Jane Catherine Elizabeth Doe')
|
|
43
|
+
expect(result).toEqual({
|
|
44
|
+
firstName: 'Mary Jane',
|
|
45
|
+
middleName: 'Catherine Elizabeth',
|
|
46
|
+
lastName: 'Doe',
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('handles extra whitespace', () => {
|
|
51
|
+
const result = parseFullName(' John Doe ')
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
firstName: 'John',
|
|
54
|
+
middleName: '',
|
|
55
|
+
lastName: 'Doe',
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('handles multiple spaces between words', () => {
|
|
60
|
+
const result = parseFullName('Wee Keat Liew')
|
|
61
|
+
expect(result).toEqual({
|
|
62
|
+
firstName: 'Wee Keat',
|
|
63
|
+
middleName: '',
|
|
64
|
+
lastName: 'Liew',
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
})
|
package/node/dirname.js
CHANGED
package/node/file-cache.js
CHANGED
|
@@ -3,7 +3,18 @@ import glob from 'glob-promise'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { sort } from '../lib/arrays/index.js'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Creates a file cache instance for managing cached files in a directory
|
|
8
|
+
* @param {string} dir - Relative path to cache directory
|
|
9
|
+
* @returns {Promise<object>} Cache instance
|
|
10
|
+
* @property {function(string): Promise<number>} cacheAt - Returns the cached file's modified timestamp, or directory's last modified time if no file specified
|
|
11
|
+
* @property {function(string): string} getAbsPath - Returns absolute path for a file in cache
|
|
12
|
+
* @property {function(string): Promise<boolean>} hasFile - Checks if a file exists in cache
|
|
13
|
+
* @property {function(string): Promise<Buffer|undefined>} loadFile - Loads file contents from cache
|
|
14
|
+
* @property {function(string, string|Buffer): Promise<void>} saveFile - Saves data to cache file
|
|
15
|
+
* @property {function(string): Promise<object>} loadJSON - Loads and parses JSON from cache file
|
|
16
|
+
* @property {function(string, object): Promise<void>} saveJSON - Saves object as JSON to cache file
|
|
17
|
+
*/
|
|
7
18
|
export async function createCache(dir) {
|
|
8
19
|
const absDirpath = path.join(process.cwd(), dir)
|
|
9
20
|
await fs.mkdir(absDirpath, { recursive: true })
|
|
@@ -56,7 +67,7 @@ export async function createCache(dir) {
|
|
|
56
67
|
async loadJSON(file) {
|
|
57
68
|
const data = await this.loadFile(file)
|
|
58
69
|
try {
|
|
59
|
-
return JSON.parse(data)
|
|
70
|
+
return JSON.parse(data.toString())
|
|
60
71
|
} catch (err) {
|
|
61
72
|
console.warn(`Error parsing JSON: ${file}`)
|
|
62
73
|
return {}
|
|
@@ -69,7 +80,19 @@ export async function createCache(dir) {
|
|
|
69
80
|
}
|
|
70
81
|
}
|
|
71
82
|
|
|
72
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Legacy file cache API for managing a single cached file
|
|
85
|
+
* @param {Object} options
|
|
86
|
+
* @param {string} options.dir - Relative path to cache directory
|
|
87
|
+
* @param {string} options.file - Filename to cache
|
|
88
|
+
* @returns {Promise<{
|
|
89
|
+
* modifiedAt: number,
|
|
90
|
+
* load: () => Promise<Buffer>,
|
|
91
|
+
* save: (data: string|Buffer) => Promise<void>,
|
|
92
|
+
* loadJSON: () => Promise<object>,
|
|
93
|
+
* saveJSON: (data: object) => Promise<void>
|
|
94
|
+
* }>}
|
|
95
|
+
*/
|
|
73
96
|
export async function fileCache({ dir, file }) {
|
|
74
97
|
// Makes the path if it doesn't exist
|
|
75
98
|
await fs.mkdir(path.join(process.cwd(), dir), { recursive: true })
|
|
@@ -94,7 +117,8 @@ export async function fileCache({ dir, file }) {
|
|
|
94
117
|
},
|
|
95
118
|
|
|
96
119
|
async loadJSON() {
|
|
97
|
-
|
|
120
|
+
const data = await fs.readFile(cachePath)
|
|
121
|
+
return JSON.parse(data.toString())
|
|
98
122
|
},
|
|
99
123
|
async saveJSON(data) {
|
|
100
124
|
await fs.writeFile(cachePath, JSON.stringify(data))
|
|
@@ -102,6 +126,11 @@ export async function fileCache({ dir, file }) {
|
|
|
102
126
|
}
|
|
103
127
|
}
|
|
104
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Gets the most recent modification time of files matching the glob pattern
|
|
131
|
+
* @param {string|string[]} globPath - Glob pattern(s) to match files
|
|
132
|
+
* @returns {Promise<number>} Timestamp of most recently modified file, or 0 if no files found
|
|
133
|
+
*/
|
|
105
134
|
export async function getLastModifiedTime(globPath) {
|
|
106
135
|
let files
|
|
107
136
|
|
|
@@ -116,13 +145,18 @@ export async function getLastModifiedTime(globPath) {
|
|
|
116
145
|
files = a.flat()
|
|
117
146
|
}
|
|
118
147
|
|
|
119
|
-
|
|
120
|
-
|
|
148
|
+
const stats = await Promise.all(files.map(file => fs.stat(file)))
|
|
149
|
+
const timestamps = stats.map(stat => Number(stat.mtimeMs))
|
|
121
150
|
|
|
122
|
-
const sorted = sort(
|
|
151
|
+
const sorted = sort(timestamps, { props: 'mtimeMs', reverse: true })
|
|
123
152
|
return sorted[0] || 0
|
|
124
153
|
}
|
|
125
154
|
|
|
155
|
+
/**
|
|
156
|
+
* @deprecated Use getLastModifiedTime instead
|
|
157
|
+
* @param {string|string[]} globPath - Glob pattern(s) to match files
|
|
158
|
+
* @returns {Promise<number>} Timestamp of most recently modified file, or 0 if no files found
|
|
159
|
+
*/
|
|
126
160
|
export async function getLatestModifiedTime(globPath) {
|
|
127
161
|
console.warn(
|
|
128
162
|
'`getLatestModifiedTime` has been renamed to `getLastModifiedTime` and will be deprecated in future versions. Please use `getLastModifiedTime` instead.',
|
|
@@ -130,6 +164,11 @@ export async function getLatestModifiedTime(globPath) {
|
|
|
130
164
|
return getLastModifiedTime(globPath)
|
|
131
165
|
}
|
|
132
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Removes leading slash from a path string
|
|
169
|
+
* @param {string} x - Path string to process
|
|
170
|
+
* @returns {string} Path without leading slash
|
|
171
|
+
*/
|
|
133
172
|
export function removeFirstSlash(x) {
|
|
134
173
|
return x.startsWith('/') ? x.slice(1) : x
|
|
135
174
|
}
|
package/node/index.js
CHANGED
package/node/random-string.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
// For Node only
|
|
2
1
|
import crypto from 'node:crypto'
|
|
3
2
|
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
export function randomString(length = 32) {
|
|
4
|
+
// Calculate bytes needed for desired character length
|
|
5
|
+
// Base64: 4 chars per 3 bytes, so we need ceil(length * 3 / 4) bytes
|
|
6
|
+
// Add extra bytes to ensure we have enough after encoding
|
|
7
|
+
const bytesNeeded = Math.ceil((length * 3) / 4) + 3
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
return crypto.randomBytes(length).toString('hex')
|
|
9
|
+
return crypto.randomBytes(bytesNeeded).toString('base64').slice(0, length)
|
|
10
10
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { randomString } from './random-string.js'
|
|
3
|
+
|
|
4
|
+
describe('randomString', () => {
|
|
5
|
+
it('should generate a random string', () => {
|
|
6
|
+
const str = randomString(32)
|
|
7
|
+
expect(str).toBeDefined()
|
|
8
|
+
expect(str.length).toBe(32)
|
|
9
|
+
|
|
10
|
+
const str2 = randomString(40)
|
|
11
|
+
expect(str2).toBeDefined()
|
|
12
|
+
expect(str2.length).toBe(40)
|
|
13
|
+
expect(str2).not.toBe(str)
|
|
14
|
+
})
|
|
15
|
+
})
|
package/node/sanitize.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import sanitizeHtml from 'sanitize-html'
|
|
2
|
+
import { sanitize as sanitizeLib } from '../lib/form/sanitize.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sanitizes values using sanitize-html
|
|
6
|
+
* @param {*} value - Value to sanitize (string, array, or object)
|
|
7
|
+
* @param {Object} [options={}] - sanitize-html options
|
|
8
|
+
* @returns {*} Sanitized value
|
|
9
|
+
* @throws {Error} If input is a string but sanitize-html fails
|
|
10
|
+
*/
|
|
11
|
+
export function sanitize(value, options = {}) {
|
|
12
|
+
return sanitizeLib(value, { sanitizer: sanitizeHtml, ...options })
|
|
13
|
+
}
|