@tldraw/utils 4.1.0-next.b6dfe9bccde9 → 4.1.0-next.b73a0d46b63f

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 (160) hide show
  1. package/dist-cjs/index.d.ts +1350 -80
  2. package/dist-cjs/index.js +5 -5
  3. package/dist-cjs/lib/ExecutionQueue.js +79 -0
  4. package/dist-cjs/lib/ExecutionQueue.js.map +2 -2
  5. package/dist-cjs/lib/PerformanceTracker.js +43 -0
  6. package/dist-cjs/lib/PerformanceTracker.js.map +2 -2
  7. package/dist-cjs/lib/array.js +3 -1
  8. package/dist-cjs/lib/array.js.map +2 -2
  9. package/dist-cjs/lib/bind.js.map +2 -2
  10. package/dist-cjs/lib/cache.js +27 -5
  11. package/dist-cjs/lib/cache.js.map +2 -2
  12. package/dist-cjs/lib/control.js +12 -0
  13. package/dist-cjs/lib/control.js.map +2 -2
  14. package/dist-cjs/lib/debounce.js.map +2 -2
  15. package/dist-cjs/lib/error.js.map +2 -2
  16. package/dist-cjs/lib/file.js +76 -11
  17. package/dist-cjs/lib/file.js.map +2 -2
  18. package/dist-cjs/lib/function.js.map +2 -2
  19. package/dist-cjs/lib/hash.js.map +2 -2
  20. package/dist-cjs/lib/id.js.map +2 -2
  21. package/dist-cjs/lib/iterable.js.map +2 -2
  22. package/dist-cjs/lib/json-value.js.map +1 -1
  23. package/dist-cjs/lib/media/apng.js.map +2 -2
  24. package/dist-cjs/lib/media/avif.js.map +2 -2
  25. package/dist-cjs/lib/media/gif.js.map +2 -2
  26. package/dist-cjs/lib/media/media.js +130 -4
  27. package/dist-cjs/lib/media/media.js.map +2 -2
  28. package/dist-cjs/lib/media/png.js +141 -0
  29. package/dist-cjs/lib/media/png.js.map +2 -2
  30. package/dist-cjs/lib/media/webp.js +1 -0
  31. package/dist-cjs/lib/media/webp.js.map +2 -2
  32. package/dist-cjs/lib/network.js.map +2 -2
  33. package/dist-cjs/lib/number.js.map +2 -2
  34. package/dist-cjs/lib/object.js +1 -1
  35. package/dist-cjs/lib/object.js.map +2 -2
  36. package/dist-cjs/lib/perf.js.map +2 -2
  37. package/dist-cjs/lib/reordering.js.map +2 -2
  38. package/dist-cjs/lib/retry.js.map +2 -2
  39. package/dist-cjs/lib/sort.js.map +2 -2
  40. package/dist-cjs/lib/storage.js.map +2 -2
  41. package/dist-cjs/lib/stringEnum.js.map +2 -2
  42. package/dist-cjs/lib/throttle.js.map +2 -2
  43. package/dist-cjs/lib/timers.js +103 -4
  44. package/dist-cjs/lib/timers.js.map +2 -2
  45. package/dist-cjs/lib/types.js.map +1 -1
  46. package/dist-cjs/lib/url.js.map +2 -2
  47. package/dist-cjs/lib/value.js.map +2 -2
  48. package/dist-cjs/lib/version.js.map +2 -2
  49. package/dist-cjs/lib/warn.js.map +2 -2
  50. package/dist-esm/index.d.mts +1350 -80
  51. package/dist-esm/index.mjs +1 -1
  52. package/dist-esm/lib/ExecutionQueue.mjs +79 -0
  53. package/dist-esm/lib/ExecutionQueue.mjs.map +2 -2
  54. package/dist-esm/lib/PerformanceTracker.mjs +43 -0
  55. package/dist-esm/lib/PerformanceTracker.mjs.map +2 -2
  56. package/dist-esm/lib/array.mjs +3 -1
  57. package/dist-esm/lib/array.mjs.map +2 -2
  58. package/dist-esm/lib/bind.mjs.map +2 -2
  59. package/dist-esm/lib/cache.mjs +27 -5
  60. package/dist-esm/lib/cache.mjs.map +2 -2
  61. package/dist-esm/lib/control.mjs +12 -0
  62. package/dist-esm/lib/control.mjs.map +2 -2
  63. package/dist-esm/lib/debounce.mjs.map +2 -2
  64. package/dist-esm/lib/error.mjs.map +2 -2
  65. package/dist-esm/lib/file.mjs +76 -11
  66. package/dist-esm/lib/file.mjs.map +2 -2
  67. package/dist-esm/lib/function.mjs.map +2 -2
  68. package/dist-esm/lib/hash.mjs.map +2 -2
  69. package/dist-esm/lib/id.mjs.map +2 -2
  70. package/dist-esm/lib/iterable.mjs.map +2 -2
  71. package/dist-esm/lib/media/apng.mjs.map +2 -2
  72. package/dist-esm/lib/media/avif.mjs.map +2 -2
  73. package/dist-esm/lib/media/gif.mjs.map +2 -2
  74. package/dist-esm/lib/media/media.mjs +130 -4
  75. package/dist-esm/lib/media/media.mjs.map +2 -2
  76. package/dist-esm/lib/media/png.mjs +141 -0
  77. package/dist-esm/lib/media/png.mjs.map +2 -2
  78. package/dist-esm/lib/media/webp.mjs +1 -0
  79. package/dist-esm/lib/media/webp.mjs.map +2 -2
  80. package/dist-esm/lib/network.mjs.map +2 -2
  81. package/dist-esm/lib/number.mjs.map +2 -2
  82. package/dist-esm/lib/object.mjs.map +2 -2
  83. package/dist-esm/lib/perf.mjs.map +2 -2
  84. package/dist-esm/lib/reordering.mjs.map +2 -2
  85. package/dist-esm/lib/retry.mjs.map +2 -2
  86. package/dist-esm/lib/sort.mjs.map +2 -2
  87. package/dist-esm/lib/storage.mjs.map +2 -2
  88. package/dist-esm/lib/stringEnum.mjs.map +2 -2
  89. package/dist-esm/lib/throttle.mjs.map +2 -2
  90. package/dist-esm/lib/timers.mjs +103 -4
  91. package/dist-esm/lib/timers.mjs.map +2 -2
  92. package/dist-esm/lib/url.mjs.map +2 -2
  93. package/dist-esm/lib/value.mjs.map +2 -2
  94. package/dist-esm/lib/version.mjs.map +2 -2
  95. package/dist-esm/lib/warn.mjs.map +2 -2
  96. package/package.json +1 -1
  97. package/src/lib/ExecutionQueue.test.ts +162 -20
  98. package/src/lib/ExecutionQueue.ts +110 -1
  99. package/src/lib/PerformanceTracker.test.ts +124 -0
  100. package/src/lib/PerformanceTracker.ts +63 -1
  101. package/src/lib/array.test.ts +263 -1
  102. package/src/lib/array.ts +183 -14
  103. package/src/lib/bind.test.ts +47 -0
  104. package/src/lib/bind.ts +69 -4
  105. package/src/lib/cache.test.ts +73 -0
  106. package/src/lib/cache.ts +47 -6
  107. package/src/lib/control.test.ts +50 -0
  108. package/src/lib/control.ts +198 -9
  109. package/src/lib/debounce.ts +28 -3
  110. package/src/lib/error.test.ts +60 -0
  111. package/src/lib/error.ts +27 -1
  112. package/src/lib/file.test.ts +49 -0
  113. package/src/lib/file.ts +117 -12
  114. package/src/lib/function.ts +11 -0
  115. package/src/lib/hash.test.ts +99 -0
  116. package/src/lib/hash.ts +69 -2
  117. package/src/lib/id.test.ts +32 -0
  118. package/src/lib/id.ts +53 -5
  119. package/src/lib/iterable.test.ts +25 -0
  120. package/src/lib/iterable.ts +4 -5
  121. package/src/lib/json-value.ts +71 -4
  122. package/src/lib/media/apng.test.ts +67 -0
  123. package/src/lib/media/apng.ts +38 -21
  124. package/src/lib/media/avif.test.ts +26 -0
  125. package/src/lib/media/avif.ts +34 -0
  126. package/src/lib/media/gif.test.ts +52 -0
  127. package/src/lib/media/gif.ts +25 -2
  128. package/src/lib/media/media.test.ts +58 -0
  129. package/src/lib/media/media.ts +220 -11
  130. package/src/lib/media/png.ts +162 -1
  131. package/src/lib/media/webp.test.ts +81 -0
  132. package/src/lib/media/webp.ts +33 -1
  133. package/src/lib/network.test.ts +38 -0
  134. package/src/lib/network.ts +6 -0
  135. package/src/lib/number.test.ts +74 -0
  136. package/src/lib/number.ts +29 -5
  137. package/src/lib/object.test.ts +236 -0
  138. package/src/lib/object.ts +194 -14
  139. package/src/lib/perf.ts +75 -3
  140. package/src/lib/reordering.test.ts +168 -0
  141. package/src/lib/reordering.ts +62 -4
  142. package/src/lib/retry.test.ts +77 -0
  143. package/src/lib/retry.ts +47 -1
  144. package/src/lib/sort.test.ts +36 -0
  145. package/src/lib/sort.ts +22 -1
  146. package/src/lib/storage.test.ts +130 -0
  147. package/src/lib/storage.tsx +54 -8
  148. package/src/lib/stringEnum.ts +20 -1
  149. package/src/lib/throttle.ts +46 -8
  150. package/src/lib/timers.test.ts +75 -0
  151. package/src/lib/timers.ts +124 -5
  152. package/src/lib/types.ts +126 -4
  153. package/src/lib/url.test.ts +44 -0
  154. package/src/lib/url.ts +40 -1
  155. package/src/lib/value.test.ts +102 -0
  156. package/src/lib/value.ts +67 -3
  157. package/src/lib/version.test.ts +494 -56
  158. package/src/lib/version.ts +36 -1
  159. package/src/lib/warn.test.ts +64 -0
  160. package/src/lib/warn.ts +43 -2
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getHashForBuffer, getHashForObject, getHashForString, lns } from './hash'
3
+
4
+ describe('getHashForString', () => {
5
+ it('should produce consistent hashes for the same input', () => {
6
+ const input = 'hello world'
7
+ const hash1 = getHashForString(input)
8
+ const hash2 = getHashForString(input)
9
+
10
+ expect(hash1).toBe(hash2)
11
+ })
12
+
13
+ it('should produce different hashes for different inputs', () => {
14
+ const hash1 = getHashForString('hello')
15
+ const hash2 = getHashForString('world')
16
+
17
+ expect(hash1).not.toBe(hash2)
18
+ })
19
+
20
+ it('should handle empty strings', () => {
21
+ const result = getHashForString('')
22
+ expect(result).toBe('0')
23
+ })
24
+ })
25
+
26
+ describe('getHashForObject', () => {
27
+ it('should produce consistent hashes for equivalent objects', () => {
28
+ const obj1 = { name: 'John', age: 30 }
29
+ const obj2 = { name: 'John', age: 30 }
30
+
31
+ const hash1 = getHashForObject(obj1)
32
+ const hash2 = getHashForObject(obj2)
33
+
34
+ expect(hash1).toBe(hash2)
35
+ })
36
+
37
+ it('should be sensitive to property order', () => {
38
+ const obj1 = { a: 1, b: 2 }
39
+ const obj2 = { b: 2, a: 1 }
40
+
41
+ const hash1 = getHashForObject(obj1)
42
+ const hash2 = getHashForObject(obj2)
43
+
44
+ expect(hash1).not.toBe(hash2)
45
+ })
46
+ })
47
+
48
+ describe('getHashForBuffer', () => {
49
+ it('should produce consistent hashes for equivalent buffers', () => {
50
+ const buffer1 = new ArrayBuffer(4)
51
+ const buffer2 = new ArrayBuffer(4)
52
+ const view1 = new DataView(buffer1)
53
+ const view2 = new DataView(buffer2)
54
+
55
+ view1.setUint32(0, 0x12345678)
56
+ view2.setUint32(0, 0x12345678)
57
+
58
+ const hash1 = getHashForBuffer(buffer1)
59
+ const hash2 = getHashForBuffer(buffer2)
60
+
61
+ expect(hash1).toBe(hash2)
62
+ })
63
+
64
+ it('should produce different hashes for different buffer contents', () => {
65
+ const buffer1 = new ArrayBuffer(4)
66
+ const buffer2 = new ArrayBuffer(4)
67
+ const view1 = new DataView(buffer1)
68
+ const view2 = new DataView(buffer2)
69
+
70
+ view1.setUint32(0, 0x12345678)
71
+ view2.setUint32(0, 0x87654321)
72
+
73
+ const hash1 = getHashForBuffer(buffer1)
74
+ const hash2 = getHashForBuffer(buffer2)
75
+
76
+ expect(hash1).not.toBe(hash2)
77
+ })
78
+
79
+ it('should handle empty buffers', () => {
80
+ const buffer = new ArrayBuffer(0)
81
+ const result = getHashForBuffer(buffer)
82
+ expect(result).toBe('0')
83
+ })
84
+ })
85
+
86
+ describe('lns', () => {
87
+ it('should produce consistent results for the same input', () => {
88
+ const input = 'test123'
89
+ const result1 = lns(input)
90
+ const result2 = lns(input)
91
+
92
+ expect(result1).toBe(result2)
93
+ })
94
+
95
+ it('should handle empty strings', () => {
96
+ const result = lns('')
97
+ expect(result).toBe('')
98
+ })
99
+ })
package/src/lib/hash.ts CHANGED
@@ -1,6 +1,20 @@
1
1
  /**
2
2
  * Hash a string using the FNV-1a algorithm.
3
3
  *
4
+ * Generates a deterministic hash value for a given string using a variant of the FNV-1a
5
+ * (Fowler-Noll-Vo) algorithm. The hash is returned as a string representation of a 32-bit integer.
6
+ *
7
+ * @param string - The input string to hash
8
+ * @returns A string representation of the 32-bit hash value
9
+ * @example
10
+ * ```ts
11
+ * const hash = getHashForString('hello world')
12
+ * console.log(hash) // '-862545276'
13
+ *
14
+ * // Same input always produces same hash
15
+ * const hash2 = getHashForString('hello world')
16
+ * console.log(hash === hash2) // true
17
+ * ```
4
18
  * @public
5
19
  */
6
20
  export function getHashForString(string: string) {
@@ -13,8 +27,24 @@ export function getHashForString(string: string) {
13
27
  }
14
28
 
15
29
  /**
16
- * Hash a string using the FNV-1a algorithm.
30
+ * Hash an object by converting it to JSON and then hashing the resulting string.
31
+ *
32
+ * Converts the object to a JSON string using JSON.stringify and then applies the same
33
+ * hashing algorithm as getHashForString. Useful for creating consistent hash values
34
+ * for objects, though the hash depends on JSON serialization order.
17
35
  *
36
+ * @param obj - The object to hash (any JSON-serializable value)
37
+ * @returns A string representation of the 32-bit hash value
38
+ * @example
39
+ * ```ts
40
+ * const hash1 = getHashForObject({ name: 'John', age: 30 })
41
+ * const hash2 = getHashForObject({ name: 'John', age: 30 })
42
+ * console.log(hash1 === hash2) // true
43
+ *
44
+ * // Arrays work too
45
+ * const arrayHash = getHashForObject([1, 2, 3, 'hello'])
46
+ * console.log(arrayHash) // '-123456789'
47
+ * ```
18
48
  * @public
19
49
  */
20
50
  export function getHashForObject(obj: any) {
@@ -24,6 +54,24 @@ export function getHashForObject(obj: any) {
24
54
  /**
25
55
  * Hash an ArrayBuffer using the FNV-1a algorithm.
26
56
  *
57
+ * Generates a deterministic hash value for binary data stored in an ArrayBuffer.
58
+ * Processes the buffer byte by byte using the same hashing algorithm as getHashForString.
59
+ * Useful for creating consistent identifiers for binary data like images or files.
60
+ *
61
+ * @param buffer - The ArrayBuffer containing binary data to hash
62
+ * @returns A string representation of the 32-bit hash value
63
+ * @example
64
+ * ```ts
65
+ * // Hash some binary data
66
+ * const data = new Uint8Array([1, 2, 3, 4, 5])
67
+ * const hash = getHashForBuffer(data.buffer)
68
+ * console.log(hash) // '123456789'
69
+ *
70
+ * // Hash image file data
71
+ * const fileBuffer = await file.arrayBuffer()
72
+ * const fileHash = getHashForBuffer(fileBuffer)
73
+ * console.log(fileHash) // Unique hash for the file
74
+ * ```
27
75
  * @public
28
76
  */
29
77
  export function getHashForBuffer(buffer: ArrayBuffer) {
@@ -36,7 +84,26 @@ export function getHashForBuffer(buffer: ArrayBuffer) {
36
84
  return hash + ''
37
85
  }
38
86
 
39
- /** @public */
87
+ /**
88
+ * Applies a string transformation algorithm that rearranges and modifies characters.
89
+ *
90
+ * Performs a series of character manipulations on the input string including
91
+ * character repositioning through splicing operations and numeric character transformations.
92
+ * This appears to be a custom encoding/obfuscation function.
93
+ *
94
+ * @param str - The input string to transform
95
+ * @returns The transformed string after applying all manipulations
96
+ * @example
97
+ * ```ts
98
+ * const result = lns('hello123')
99
+ * console.log(result) // Transformed string (exact output depends on algorithm)
100
+ *
101
+ * // Can be used for simple string obfuscation
102
+ * const obfuscated = lns('sensitive-data')
103
+ * console.log(obfuscated) // Obfuscated version
104
+ * ```
105
+ * @public
106
+ */
40
107
  export function lns(str: string) {
41
108
  const result = str.split('')
42
109
  result.push(...result.splice(0, Math.round(result.length / 5)))
@@ -0,0 +1,32 @@
1
+ import { afterEach, describe, expect, test } from 'vitest'
2
+ import { mockUniqueId, restoreUniqueId, uniqueId } from './id'
3
+
4
+ describe('mockUniqueId', () => {
5
+ afterEach(() => {
6
+ restoreUniqueId()
7
+ })
8
+
9
+ test('replaces uniqueId with custom implementation', () => {
10
+ mockUniqueId(() => 'test-id')
11
+
12
+ expect(uniqueId()).toBe('test-id')
13
+ expect(uniqueId(10)).toBe('test-id')
14
+ })
15
+ })
16
+
17
+ describe('restoreUniqueId', () => {
18
+ test('restores original uniqueId behavior after mocking', () => {
19
+ mockUniqueId(() => 'mocked-id')
20
+ expect(uniqueId()).toBe('mocked-id')
21
+
22
+ restoreUniqueId()
23
+
24
+ const id1 = uniqueId()
25
+ const id2 = uniqueId()
26
+
27
+ expect(id1).not.toBe('mocked-id')
28
+ expect(id2).not.toBe('mocked-id')
29
+ expect(id1).not.toBe(id2)
30
+ expect(id1).toHaveLength(21)
31
+ })
32
+ })
package/src/lib/id.ts CHANGED
@@ -55,25 +55,73 @@ function nanoid(size = 21) {
55
55
  }
56
56
 
57
57
  let impl = nanoid
58
- /** @internal */
58
+ /**
59
+ * Mock the unique ID generator with a custom implementation for testing.
60
+ *
61
+ * Replaces the internal ID generation function with a custom one. This is useful
62
+ * for testing scenarios where you need predictable or deterministic IDs.
63
+ *
64
+ * @param fn - The mock function that should return a string ID. Takes optional size parameter.
65
+ * @example
66
+ * ```ts
67
+ * // Mock with predictable IDs for testing
68
+ * mockUniqueId((size = 21) => 'test-id-' + size)
69
+ * console.log(uniqueId()) // 'test-id-21'
70
+ * console.log(uniqueId(10)) // 'test-id-10'
71
+ *
72
+ * // Restore original implementation when done
73
+ * restoreUniqueId()
74
+ * ```
75
+ * @internal
76
+ */
59
77
  export function mockUniqueId(fn: (size?: number) => string) {
60
78
  impl = fn
61
79
  }
62
80
 
63
- /** @internal */
81
+ /**
82
+ * Restore the original unique ID generator after mocking.
83
+ *
84
+ * Resets the ID generation function back to the original nanoid implementation.
85
+ * This should be called after testing to restore normal ID generation behavior.
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * // After mocking for tests
90
+ * mockUniqueId(() => 'mock-id')
91
+ *
92
+ * // Restore original behavior
93
+ * restoreUniqueId()
94
+ * console.log(uniqueId()) // Now generates real random IDs again
95
+ * ```
96
+ * @internal
97
+ */
64
98
  export function restoreUniqueId() {
65
99
  impl = nanoid
66
100
  }
67
101
 
68
102
  /**
69
- * Generate a unique id.
103
+ * Generate a unique ID using a modified nanoid algorithm.
70
104
  *
71
- * @example
105
+ * Generates a cryptographically secure random string ID using URL-safe characters.
106
+ * The default size is 21 characters, which provides a good balance of uniqueness
107
+ * and brevity. Uses the global crypto API for secure random number generation.
72
108
  *
109
+ * @param size - Optional length of the generated ID (defaults to 21 characters)
110
+ * @returns A unique string identifier
111
+ * @example
73
112
  * ```ts
113
+ * // Generate default 21-character ID
74
114
  * const id = uniqueId()
75
- * ```
115
+ * console.log(id) // 'V1StGXR8_Z5jdHi6B-myT'
76
116
  *
117
+ * // Generate shorter ID
118
+ * const shortId = uniqueId(10)
119
+ * console.log(shortId) // 'V1StGXR8_Z'
120
+ *
121
+ * // Generate longer ID
122
+ * const longId = uniqueId(32)
123
+ * console.log(longId) // 'V1StGXR8_Z5jdHi6B-myTVKahvjdx...'
124
+ * ```
77
125
  * @public
78
126
  */
79
127
  export function uniqueId(size?: number): string {
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getFirstFromIterable } from './iterable'
3
+
4
+ describe('getFirstFromIterable', () => {
5
+ it('should get first item from Set', () => {
6
+ const set = new Set([1, 2, 3])
7
+ expect(getFirstFromIterable(set)).toBe(1)
8
+ })
9
+
10
+ it('should get first value from Map', () => {
11
+ const map = new Map([
12
+ ['a', 1],
13
+ ['b', 2],
14
+ ])
15
+ expect(getFirstFromIterable(map)).toBe(1)
16
+ })
17
+
18
+ it('should preserve insertion order', () => {
19
+ const set = new Set()
20
+ set.add('third')
21
+ set.add('first')
22
+ set.add('second')
23
+ expect(getFirstFromIterable(set)).toBe('third')
24
+ })
25
+ })
@@ -1,19 +1,18 @@
1
1
  /**
2
2
  * Get the first item from an iterable Set or Map.
3
3
  *
4
+ * @param value - The iterable Set or Map to get the first item from
5
+ * @returns The first value from the Set or Map
4
6
  * @example
5
- *
6
7
  * ```ts
7
- * const A = getFirstItem(new Set([1, 2, 3])) // 1
8
- * const B = getFirstItem(
8
+ * const A = getFirstFromIterable(new Set([1, 2, 3])) // 1
9
+ * const B = getFirstFromIterable(
9
10
  * new Map([
10
11
  * ['a', 1],
11
12
  * ['b', 2],
12
13
  * ])
13
14
  * ) // 1
14
15
  * ```
15
- *
16
- * @param value - The iterable Set or Map.
17
16
  * @public
18
17
  */
19
18
  export function getFirstFromIterable<T = unknown>(set: Set<T> | Map<any, T>): T {
@@ -1,10 +1,77 @@
1
- /** @public */
1
+ /**
2
+ * A type that represents any valid JSON value. This includes primitives (boolean, null, string, number),
3
+ * arrays of JSON values, and objects with string keys and JSON values.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const jsonData: JsonValue = {
8
+ * name: "Alice",
9
+ * age: 30,
10
+ * active: true,
11
+ * tags: ["user", "premium"],
12
+ * metadata: null
13
+ * }
14
+ * ```
15
+ *
16
+ * @public
17
+ */
2
18
  export type JsonValue = JsonPrimitive | JsonArray | JsonObject
3
- /** @public */
19
+
20
+ /**
21
+ * A type representing JSON primitive values: boolean, null, string, or number.
22
+ * These are the atomic values that can appear in JSON data.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const primitives: JsonPrimitive[] = [
27
+ * true,
28
+ * null,
29
+ * "hello",
30
+ * 42
31
+ * ]
32
+ * ```
33
+ *
34
+ * @public
35
+ */
4
36
  export type JsonPrimitive = boolean | null | string | number
5
- /** @public */
37
+
38
+ /**
39
+ * A type representing a JSON array containing any valid JSON values.
40
+ * Arrays can contain mixed types of JSON values including nested arrays and objects.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * const jsonArray: JsonArray = [
45
+ * "text",
46
+ * 123,
47
+ * true,
48
+ * { nested: "object" },
49
+ * [1, 2, 3]
50
+ * ]
51
+ * ```
52
+ *
53
+ * @public
54
+ */
6
55
  export type JsonArray = JsonValue[]
7
- /** @public */
56
+
57
+ /**
58
+ * A type representing a JSON object with string keys and JSON values.
59
+ * Object values can be undefined to handle optional properties safely.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const jsonObject: JsonObject = {
64
+ * required: "value",
65
+ * optional: undefined,
66
+ * nested: {
67
+ * deep: "property"
68
+ * },
69
+ * array: [1, 2, 3]
70
+ * }
71
+ * ```
72
+ *
73
+ * @public
74
+ */
8
75
  export interface JsonObject {
9
76
  [key: string]: JsonValue | undefined
10
77
  }
@@ -0,0 +1,67 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { isApngAnimated } from './apng'
3
+
4
+ describe('isApngAnimated', () => {
5
+ it('returns false for empty ArrayBuffer', () => {
6
+ const buffer = new ArrayBuffer(0)
7
+ expect(isApngAnimated(buffer)).toBe(false)
8
+ })
9
+
10
+ it('returns false for buffer too small to be PNG', () => {
11
+ const buffer = new ArrayBuffer(8)
12
+ expect(isApngAnimated(buffer)).toBe(false)
13
+ })
14
+
15
+ it('returns false for non-PNG data', () => {
16
+ const buffer = new ArrayBuffer(20)
17
+ const view = new Uint8Array(buffer)
18
+ // JPEG signature instead
19
+ view.set([0xff, 0xd8, 0xff, 0xe0])
20
+ expect(isApngAnimated(buffer)).toBe(false)
21
+ })
22
+
23
+ it('returns false for regular PNG with IDAT but no acTL', () => {
24
+ const buffer = new ArrayBuffer(100)
25
+ const view = new Uint8Array(buffer)
26
+
27
+ // PNG signature
28
+ view.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 0)
29
+
30
+ // Add IDAT chunk at position 20
31
+ view.set(new TextEncoder().encode('IDAT'), 20)
32
+
33
+ expect(isApngAnimated(buffer)).toBe(false)
34
+ })
35
+
36
+ it('returns false when acTL comes after IDAT', () => {
37
+ const buffer = new ArrayBuffer(100)
38
+ const view = new Uint8Array(buffer)
39
+
40
+ // PNG signature
41
+ view.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 0)
42
+
43
+ // IDAT chunk at position 20
44
+ view.set(new TextEncoder().encode('IDAT'), 20)
45
+
46
+ // acTL chunk after IDAT - should not count as animated
47
+ view.set(new TextEncoder().encode('acTL'), 30)
48
+
49
+ expect(isApngAnimated(buffer)).toBe(false)
50
+ })
51
+
52
+ it('returns true for APNG with acTL before IDAT', () => {
53
+ const buffer = new ArrayBuffer(100)
54
+ const view = new Uint8Array(buffer)
55
+
56
+ // PNG signature
57
+ view.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 0)
58
+
59
+ // acTL chunk before IDAT
60
+ view.set(new TextEncoder().encode('acTL'), 15)
61
+
62
+ // IDAT chunk
63
+ view.set(new TextEncoder().encode('IDAT'), 30)
64
+
65
+ expect(isApngAnimated(buffer)).toBe(true)
66
+ })
67
+ })
@@ -3,6 +3,35 @@
3
3
  * Copyright (c) Philip van Heemstra
4
4
  */
5
5
 
6
+ /**
7
+ * Determines whether an ArrayBuffer contains an animated PNG (APNG) image.
8
+ *
9
+ * This function checks if the provided buffer contains a valid PNG file with animation
10
+ * control chunks (acTL) that precede the image data chunks (IDAT), which indicates
11
+ * it's an animated PNG rather than a static PNG.
12
+ *
13
+ * @param buffer - The ArrayBuffer containing the image data to analyze
14
+ * @returns True if the buffer contains an animated PNG, false otherwise
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * // Check if an uploaded file contains an animated PNG
19
+ * if (file.type === 'image/apng') {
20
+ * const isAnimated = isApngAnimated(await file.arrayBuffer())
21
+ * console.log(isAnimated ? 'Animated PNG' : 'Static PNG')
22
+ * }
23
+ * ```
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * // Use with fetch to check remote images
28
+ * const response = await fetch('image.png')
29
+ * const buffer = await response.arrayBuffer()
30
+ * const hasAnimation = isApngAnimated(buffer)
31
+ * ```
32
+ *
33
+ * @public
34
+ */
6
35
  export function isApngAnimated(buffer: ArrayBuffer): boolean {
7
36
  const view = new Uint8Array(buffer)
8
37
 
@@ -29,29 +58,17 @@ export function isApngAnimated(buffer: ArrayBuffer): boolean {
29
58
  }
30
59
 
31
60
  /**
32
- * Returns the index of the first occurrence of a sequence in an typed array, or -1 if it is not present.
33
- *
34
- * Works similar to `Array.prototype.indexOf()`, but it searches for a sequence of array values (bytes).
35
- * The bytes in the `haystack` array are decoded (UTF-8) and then used to search for `needle`.
36
- *
37
- * @param haystack `Uint8Array`
38
- * Array to search in.
39
- *
40
- * @param needle `string | RegExp`
41
- * The value to locate in the array.
42
- *
43
- * @param fromIndex `number`
44
- * The array index at which to begin the search.
45
- *
46
- * @param upToIndex `number`
47
- * The array index up to which to search.
48
- * If omitted, search until the end.
61
+ * Returns the index of the first occurrence of a string pattern in a Uint8Array, or -1 if not found.
49
62
  *
50
- * @param chunksize `number`
51
- * Size of the chunks used when searching (default 1024).
63
+ * Searches for a string pattern by decoding chunks of the byte array to UTF-8 text and using
64
+ * regular expression matching. Handles cases where the pattern might be split across chunk boundaries.
52
65
  *
53
- * @returns boolean
54
- * Whether the array holds Animated PNG data.
66
+ * @param haystack - The Uint8Array to search in
67
+ * @param needle - The string or RegExp pattern to locate
68
+ * @param fromIndex - The array index at which to begin the search
69
+ * @param upToIndex - The array index up to which to search (optional, defaults to array end)
70
+ * @param chunksize - Size of the chunks used when searching (default 1024 bytes)
71
+ * @returns The index position of the first match, or -1 if not found
55
72
  */
56
73
  function indexOfSubstring(
57
74
  haystack: Uint8Array,
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { isAvifAnimated } from './avif'
3
+
4
+ describe('isAvifAnimated', () => {
5
+ it('returns true when 4th byte equals 44', () => {
6
+ const buffer = new ArrayBuffer(10)
7
+ const view = new Uint8Array(buffer)
8
+ view[3] = 44
9
+
10
+ expect(isAvifAnimated(buffer)).toBe(true)
11
+ })
12
+
13
+ it('returns false when 4th byte is not 44', () => {
14
+ const buffer = new ArrayBuffer(10)
15
+ const view = new Uint8Array(buffer)
16
+ view[3] = 43
17
+
18
+ expect(isAvifAnimated(buffer)).toBe(false)
19
+ })
20
+
21
+ it('returns false for buffer smaller than 4 bytes', () => {
22
+ const buffer = new ArrayBuffer(3)
23
+ // Accessing index 3 should return undefined, which !== 44
24
+ expect(isAvifAnimated(buffer)).toBe(false)
25
+ })
26
+ })
@@ -1,3 +1,37 @@
1
+ /**
2
+ * Determines whether an ArrayBuffer contains an animated AVIF image.
3
+ *
4
+ * This function performs a simple check by examining the 4th byte of the buffer.
5
+ * AVIF animation is indicated when the byte at index 3 equals 44.
6
+ *
7
+ * @param buffer - The ArrayBuffer containing the AVIF image data to analyze
8
+ * @returns True if the buffer contains an animated AVIF, false otherwise
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * // Check if an AVIF file is animated
13
+ * const response = await fetch('image.avif')
14
+ * const buffer = await response.arrayBuffer()
15
+ * const isAnimated = isAvifAnimated(buffer)
16
+ * if (isAnimated) {
17
+ * console.log('This AVIF contains animation!')
18
+ * }
19
+ * ```
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * // Use with file input
24
+ * const fileInput = document.querySelector('input[type="file"]')
25
+ * fileInput.addEventListener('change', async (event) => {
26
+ * const file = event.target.files[0]
27
+ * const buffer = await file.arrayBuffer()
28
+ * const hasAnimation = isAvifAnimated(buffer)
29
+ * console.log(hasAnimation ? 'Animated AVIF' : 'Static AVIF')
30
+ * })
31
+ * ```
32
+ *
33
+ * @public
34
+ */
1
35
  export const isAvifAnimated = (buffer: ArrayBuffer) => {
2
36
  const view = new Uint8Array(buffer)
3
37
  return view[3] === 44