@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.
- package/dist-cjs/index.d.ts +1350 -80
- package/dist-cjs/index.js +5 -5
- package/dist-cjs/lib/ExecutionQueue.js +79 -0
- package/dist-cjs/lib/ExecutionQueue.js.map +2 -2
- package/dist-cjs/lib/PerformanceTracker.js +43 -0
- package/dist-cjs/lib/PerformanceTracker.js.map +2 -2
- package/dist-cjs/lib/array.js +3 -1
- package/dist-cjs/lib/array.js.map +2 -2
- package/dist-cjs/lib/bind.js.map +2 -2
- package/dist-cjs/lib/cache.js +27 -5
- package/dist-cjs/lib/cache.js.map +2 -2
- package/dist-cjs/lib/control.js +12 -0
- package/dist-cjs/lib/control.js.map +2 -2
- package/dist-cjs/lib/debounce.js.map +2 -2
- package/dist-cjs/lib/error.js.map +2 -2
- package/dist-cjs/lib/file.js +76 -11
- package/dist-cjs/lib/file.js.map +2 -2
- package/dist-cjs/lib/function.js.map +2 -2
- package/dist-cjs/lib/hash.js.map +2 -2
- package/dist-cjs/lib/id.js.map +2 -2
- package/dist-cjs/lib/iterable.js.map +2 -2
- package/dist-cjs/lib/json-value.js.map +1 -1
- package/dist-cjs/lib/media/apng.js.map +2 -2
- package/dist-cjs/lib/media/avif.js.map +2 -2
- package/dist-cjs/lib/media/gif.js.map +2 -2
- package/dist-cjs/lib/media/media.js +130 -4
- package/dist-cjs/lib/media/media.js.map +2 -2
- package/dist-cjs/lib/media/png.js +141 -0
- package/dist-cjs/lib/media/png.js.map +2 -2
- package/dist-cjs/lib/media/webp.js +1 -0
- package/dist-cjs/lib/media/webp.js.map +2 -2
- package/dist-cjs/lib/network.js.map +2 -2
- package/dist-cjs/lib/number.js.map +2 -2
- package/dist-cjs/lib/object.js +1 -1
- package/dist-cjs/lib/object.js.map +2 -2
- package/dist-cjs/lib/perf.js.map +2 -2
- package/dist-cjs/lib/reordering.js.map +2 -2
- package/dist-cjs/lib/retry.js.map +2 -2
- package/dist-cjs/lib/sort.js.map +2 -2
- package/dist-cjs/lib/storage.js.map +2 -2
- package/dist-cjs/lib/stringEnum.js.map +2 -2
- package/dist-cjs/lib/throttle.js.map +2 -2
- package/dist-cjs/lib/timers.js +103 -4
- package/dist-cjs/lib/timers.js.map +2 -2
- package/dist-cjs/lib/types.js.map +1 -1
- package/dist-cjs/lib/url.js.map +2 -2
- package/dist-cjs/lib/value.js.map +2 -2
- package/dist-cjs/lib/version.js.map +2 -2
- package/dist-cjs/lib/warn.js.map +2 -2
- package/dist-esm/index.d.mts +1350 -80
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/ExecutionQueue.mjs +79 -0
- package/dist-esm/lib/ExecutionQueue.mjs.map +2 -2
- package/dist-esm/lib/PerformanceTracker.mjs +43 -0
- package/dist-esm/lib/PerformanceTracker.mjs.map +2 -2
- package/dist-esm/lib/array.mjs +3 -1
- package/dist-esm/lib/array.mjs.map +2 -2
- package/dist-esm/lib/bind.mjs.map +2 -2
- package/dist-esm/lib/cache.mjs +27 -5
- package/dist-esm/lib/cache.mjs.map +2 -2
- package/dist-esm/lib/control.mjs +12 -0
- package/dist-esm/lib/control.mjs.map +2 -2
- package/dist-esm/lib/debounce.mjs.map +2 -2
- package/dist-esm/lib/error.mjs.map +2 -2
- package/dist-esm/lib/file.mjs +76 -11
- package/dist-esm/lib/file.mjs.map +2 -2
- package/dist-esm/lib/function.mjs.map +2 -2
- package/dist-esm/lib/hash.mjs.map +2 -2
- package/dist-esm/lib/id.mjs.map +2 -2
- package/dist-esm/lib/iterable.mjs.map +2 -2
- package/dist-esm/lib/media/apng.mjs.map +2 -2
- package/dist-esm/lib/media/avif.mjs.map +2 -2
- package/dist-esm/lib/media/gif.mjs.map +2 -2
- package/dist-esm/lib/media/media.mjs +130 -4
- package/dist-esm/lib/media/media.mjs.map +2 -2
- package/dist-esm/lib/media/png.mjs +141 -0
- package/dist-esm/lib/media/png.mjs.map +2 -2
- package/dist-esm/lib/media/webp.mjs +1 -0
- package/dist-esm/lib/media/webp.mjs.map +2 -2
- package/dist-esm/lib/network.mjs.map +2 -2
- package/dist-esm/lib/number.mjs.map +2 -2
- package/dist-esm/lib/object.mjs.map +2 -2
- package/dist-esm/lib/perf.mjs.map +2 -2
- package/dist-esm/lib/reordering.mjs.map +2 -2
- package/dist-esm/lib/retry.mjs.map +2 -2
- package/dist-esm/lib/sort.mjs.map +2 -2
- package/dist-esm/lib/storage.mjs.map +2 -2
- package/dist-esm/lib/stringEnum.mjs.map +2 -2
- package/dist-esm/lib/throttle.mjs.map +2 -2
- package/dist-esm/lib/timers.mjs +103 -4
- package/dist-esm/lib/timers.mjs.map +2 -2
- package/dist-esm/lib/url.mjs.map +2 -2
- package/dist-esm/lib/value.mjs.map +2 -2
- package/dist-esm/lib/version.mjs.map +2 -2
- package/dist-esm/lib/warn.mjs.map +2 -2
- package/package.json +1 -1
- package/src/lib/ExecutionQueue.test.ts +162 -20
- package/src/lib/ExecutionQueue.ts +110 -1
- package/src/lib/PerformanceTracker.test.ts +124 -0
- package/src/lib/PerformanceTracker.ts +63 -1
- package/src/lib/array.test.ts +263 -1
- package/src/lib/array.ts +183 -14
- package/src/lib/bind.test.ts +47 -0
- package/src/lib/bind.ts +69 -4
- package/src/lib/cache.test.ts +73 -0
- package/src/lib/cache.ts +47 -6
- package/src/lib/control.test.ts +50 -0
- package/src/lib/control.ts +198 -9
- package/src/lib/debounce.ts +28 -3
- package/src/lib/error.test.ts +60 -0
- package/src/lib/error.ts +27 -1
- package/src/lib/file.test.ts +49 -0
- package/src/lib/file.ts +117 -12
- package/src/lib/function.ts +11 -0
- package/src/lib/hash.test.ts +99 -0
- package/src/lib/hash.ts +69 -2
- package/src/lib/id.test.ts +32 -0
- package/src/lib/id.ts +53 -5
- package/src/lib/iterable.test.ts +25 -0
- package/src/lib/iterable.ts +4 -5
- package/src/lib/json-value.ts +71 -4
- package/src/lib/media/apng.test.ts +67 -0
- package/src/lib/media/apng.ts +38 -21
- package/src/lib/media/avif.test.ts +26 -0
- package/src/lib/media/avif.ts +34 -0
- package/src/lib/media/gif.test.ts +52 -0
- package/src/lib/media/gif.ts +25 -2
- package/src/lib/media/media.test.ts +58 -0
- package/src/lib/media/media.ts +220 -11
- package/src/lib/media/png.ts +162 -1
- package/src/lib/media/webp.test.ts +81 -0
- package/src/lib/media/webp.ts +33 -1
- package/src/lib/network.test.ts +38 -0
- package/src/lib/network.ts +6 -0
- package/src/lib/number.test.ts +74 -0
- package/src/lib/number.ts +29 -5
- package/src/lib/object.test.ts +236 -0
- package/src/lib/object.ts +194 -14
- package/src/lib/perf.ts +75 -3
- package/src/lib/reordering.test.ts +168 -0
- package/src/lib/reordering.ts +62 -4
- package/src/lib/retry.test.ts +77 -0
- package/src/lib/retry.ts +47 -1
- package/src/lib/sort.test.ts +36 -0
- package/src/lib/sort.ts +22 -1
- package/src/lib/storage.test.ts +130 -0
- package/src/lib/storage.tsx +54 -8
- package/src/lib/stringEnum.ts +20 -1
- package/src/lib/throttle.ts +46 -8
- package/src/lib/timers.test.ts +75 -0
- package/src/lib/timers.ts +124 -5
- package/src/lib/types.ts +126 -4
- package/src/lib/url.test.ts +44 -0
- package/src/lib/url.ts +40 -1
- package/src/lib/value.test.ts +102 -0
- package/src/lib/value.ts +67 -3
- package/src/lib/version.test.ts +494 -56
- package/src/lib/version.ts +36 -1
- package/src/lib/warn.test.ts +64 -0
- 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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
103
|
+
* Generate a unique ID using a modified nanoid algorithm.
|
|
70
104
|
*
|
|
71
|
-
*
|
|
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
|
+
})
|
package/src/lib/iterable.ts
CHANGED
|
@@ -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 =
|
|
8
|
-
* const B =
|
|
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 {
|
package/src/lib/json-value.ts
CHANGED
|
@@ -1,10 +1,77 @@
|
|
|
1
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
})
|
package/src/lib/media/apng.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
51
|
-
*
|
|
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
|
-
* @
|
|
54
|
-
*
|
|
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
|
+
})
|
package/src/lib/media/avif.ts
CHANGED
|
@@ -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
|