@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,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'
@@ -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,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
+ }
@@ -15,5 +15,5 @@ export function splitObject(obj, keys) {
15
15
  }
16
16
  })
17
17
 
18
- return { picked, omitted }
18
+ return { picked, omitted, p: picked, o: omitted }
19
19
  }
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,4 +1,5 @@
1
1
  export * from './convert-case/convert-case.js'
2
2
  export * from './markdown.js'
3
+ export * from './name.js'
3
4
  export * from './pluralize.js'
4
5
  export * from './query-string.js'
@@ -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
 
@@ -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
@@ -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
@@ -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
- // Newer and nicer API
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
- // Legacy, not so nice API
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
- return JSON.parse(await fs.readFile(cachePath))
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
- files = await Promise.all(files.map(fs.stat))
120
- files = files.map(file => file.mtimeMs)
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(files, { sortBy: 'mtimeMs', order: 'desc' })
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
@@ -4,3 +4,4 @@ export * from './file-cache.js'
4
4
  export * from './hash.js'
5
5
  export * from './pkce.js'
6
6
  export * from './random-string.js'
7
+ export * from './sanitize.js'
@@ -1,10 +1,10 @@
1
- // For Node only
2
1
  import crypto from 'node:crypto'
3
2
 
4
- export async function randomString(length = 32) {
5
- return crypto.randomBytes(length).toString('base64')
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
- export function randomStringSync(length = 32) {
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
+ })
@@ -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
+ }