@fuzdev/fuz_util 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/array.d.ts +15 -0
- package/dist/array.d.ts.map +1 -0
- package/dist/array.js +25 -0
- package/dist/async.d.ts +62 -0
- package/dist/async.d.ts.map +1 -0
- package/dist/async.js +147 -0
- package/dist/colors.d.ts +41 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +106 -0
- package/dist/counter.d.ts +7 -0
- package/dist/counter.d.ts.map +1 -0
- package/dist/counter.js +7 -0
- package/dist/deep_equal.d.ts +18 -0
- package/dist/deep_equal.d.ts.map +1 -0
- package/dist/deep_equal.js +152 -0
- package/dist/dom.d.ts +35 -0
- package/dist/dom.d.ts.map +1 -0
- package/dist/dom.js +95 -0
- package/dist/error.d.ts +15 -0
- package/dist/error.d.ts.map +1 -0
- package/dist/error.js +18 -0
- package/dist/fetch.d.ts +81 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +162 -0
- package/dist/fs.d.ts +34 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +73 -0
- package/dist/function.d.ts +27 -0
- package/dist/function.d.ts.map +1 -0
- package/dist/function.js +21 -0
- package/dist/git.d.ts +132 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +288 -0
- package/dist/id.d.ts +18 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +18 -0
- package/dist/iterator.d.ts +5 -0
- package/dist/iterator.d.ts.map +1 -0
- package/dist/iterator.js +9 -0
- package/dist/json.d.ts +30 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +44 -0
- package/dist/library_json.d.ts +42 -0
- package/dist/library_json.d.ts.map +1 -0
- package/dist/library_json.js +76 -0
- package/dist/log.d.ts +188 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +393 -0
- package/dist/map.d.ts +12 -0
- package/dist/map.d.ts.map +1 -0
- package/dist/map.js +14 -0
- package/dist/maths.d.ts +85 -0
- package/dist/maths.d.ts.map +1 -0
- package/dist/maths.js +87 -0
- package/dist/object.d.ts +46 -0
- package/dist/object.d.ts.map +1 -0
- package/dist/object.js +89 -0
- package/dist/package_json.d.ts +90 -0
- package/dist/package_json.d.ts.map +1 -0
- package/dist/package_json.js +112 -0
- package/dist/path.d.ts +63 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +83 -0
- package/dist/print.d.ts +52 -0
- package/dist/print.d.ts.map +1 -0
- package/dist/print.js +89 -0
- package/dist/process.d.ts +77 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +148 -0
- package/dist/random.d.ts +25 -0
- package/dist/random.d.ts.map +1 -0
- package/dist/random.js +35 -0
- package/dist/random_alea.d.ts +23 -0
- package/dist/random_alea.d.ts.map +1 -0
- package/dist/random_alea.js +95 -0
- package/dist/regexp.d.ts +12 -0
- package/dist/regexp.d.ts.map +1 -0
- package/dist/regexp.js +16 -0
- package/dist/result.d.ts +64 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +48 -0
- package/dist/source_json.d.ts +375 -0
- package/dist/source_json.d.ts.map +1 -0
- package/dist/source_json.js +189 -0
- package/dist/string.d.ts +51 -0
- package/dist/string.d.ts.map +1 -0
- package/dist/string.js +92 -0
- package/dist/throttle.d.ts +26 -0
- package/dist/throttle.d.ts.map +1 -0
- package/dist/throttle.js +53 -0
- package/dist/timings.d.ts +33 -0
- package/dist/timings.d.ts.map +1 -0
- package/dist/timings.js +75 -0
- package/dist/types.d.ts +77 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/url.d.ts +10 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +8 -0
- package/package.json +125 -0
- package/src/lib/array.ts +30 -0
- package/src/lib/async.ts +182 -0
- package/src/lib/colors.ts +132 -0
- package/src/lib/counter.ts +11 -0
- package/src/lib/deep_equal.ts +155 -0
- package/src/lib/dom.ts +108 -0
- package/src/lib/error.ts +22 -0
- package/src/lib/fetch.ts +231 -0
- package/src/lib/fs.ts +128 -0
- package/src/lib/function.ts +32 -0
- package/src/lib/git.ts +390 -0
- package/src/lib/id.ts +30 -0
- package/src/lib/iterator.ts +8 -0
- package/src/lib/json.ts +61 -0
- package/src/lib/library_json.ts +122 -0
- package/src/lib/log.ts +469 -0
- package/src/lib/map.ts +18 -0
- package/src/lib/maths.ts +91 -0
- package/src/lib/object.ts +110 -0
- package/src/lib/package_json.ts +135 -0
- package/src/lib/path.ts +137 -0
- package/src/lib/print.ts +111 -0
- package/src/lib/process.ts +207 -0
- package/src/lib/random.ts +48 -0
- package/src/lib/random_alea.ts +107 -0
- package/src/lib/regexp.ts +17 -0
- package/src/lib/result.ts +67 -0
- package/src/lib/source_json.ts +209 -0
- package/src/lib/string.ts +99 -0
- package/src/lib/throttle.ts +70 -0
- package/src/lib/timings.ts +93 -0
- package/src/lib/types.ts +99 -0
- package/src/lib/url.ts +14 -0
package/src/lib/async.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
export type AsyncStatus = 'initial' | 'pending' | 'success' | 'failure';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Waits for the given `duration` before resolving.
|
|
5
|
+
*/
|
|
6
|
+
export const wait = (duration = 0): Promise<void> =>
|
|
7
|
+
new Promise((resolve) => setTimeout(resolve, duration));
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Checks if `value` is a `Promise`.
|
|
11
|
+
*/
|
|
12
|
+
export const is_promise = (value: any): value is Promise<any> =>
|
|
13
|
+
value && typeof value.then === 'function';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a deferred object with a promise and its resolve/reject handlers.
|
|
17
|
+
*/
|
|
18
|
+
export interface Deferred<T> {
|
|
19
|
+
promise: Promise<T>;
|
|
20
|
+
resolve: (value: T) => void;
|
|
21
|
+
reject: (reason: any) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates a object with a `promise` and its `resolve`/`reject` handlers.
|
|
26
|
+
*/
|
|
27
|
+
export const create_deferred = <T>(): Deferred<T> => {
|
|
28
|
+
let resolve!: (value: T) => void;
|
|
29
|
+
let reject!: (reason: any) => void;
|
|
30
|
+
const promise: Promise<T> = new Promise((res, rej) => {
|
|
31
|
+
resolve = res;
|
|
32
|
+
reject = rej;
|
|
33
|
+
});
|
|
34
|
+
return {promise, resolve, reject};
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Maps over items with controlled concurrency, preserving input order.
|
|
39
|
+
*
|
|
40
|
+
* @param items array of items to process
|
|
41
|
+
* @param fn async function to apply to each item
|
|
42
|
+
* @param concurrency maximum number of concurrent operations
|
|
43
|
+
* @returns promise resolving to array of results in same order as input
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* const results = await map_concurrent(
|
|
48
|
+
* file_paths,
|
|
49
|
+
* async (path) => readFile(path, 'utf8'),
|
|
50
|
+
* 5, // max 5 concurrent reads
|
|
51
|
+
* );
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export const map_concurrent = async <T, R>(
|
|
55
|
+
items: Array<T>,
|
|
56
|
+
fn: (item: T, index: number) => Promise<R>,
|
|
57
|
+
concurrency: number,
|
|
58
|
+
): Promise<Array<R>> => {
|
|
59
|
+
if (concurrency < 1) {
|
|
60
|
+
throw new Error('concurrency must be at least 1');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const results: Array<R> = new Array(items.length);
|
|
64
|
+
let next_index = 0;
|
|
65
|
+
let active_count = 0;
|
|
66
|
+
let rejected = false;
|
|
67
|
+
let reject_error: unknown;
|
|
68
|
+
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const run_next = (): void => {
|
|
71
|
+
// Stop spawning if we've rejected
|
|
72
|
+
if (rejected) return;
|
|
73
|
+
|
|
74
|
+
// Check if we're done
|
|
75
|
+
if (next_index >= items.length && active_count === 0) {
|
|
76
|
+
resolve(results);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Spawn workers up to concurrency limit
|
|
81
|
+
while (active_count < concurrency && next_index < items.length) {
|
|
82
|
+
const index = next_index++;
|
|
83
|
+
const item = items[index]!;
|
|
84
|
+
active_count++;
|
|
85
|
+
|
|
86
|
+
fn(item, index)
|
|
87
|
+
.then((result) => {
|
|
88
|
+
if (rejected) return;
|
|
89
|
+
results[index] = result;
|
|
90
|
+
active_count--;
|
|
91
|
+
run_next();
|
|
92
|
+
})
|
|
93
|
+
.catch((error) => {
|
|
94
|
+
if (rejected) return;
|
|
95
|
+
rejected = true;
|
|
96
|
+
reject_error = error;
|
|
97
|
+
reject(reject_error); // eslint-disable-line @typescript-eslint/prefer-promise-reject-errors
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Handle empty array
|
|
103
|
+
if (items.length === 0) {
|
|
104
|
+
resolve(results);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
run_next();
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Like `map_concurrent` but collects all results/errors instead of failing fast.
|
|
114
|
+
* Returns an array of settlement objects matching the `Promise.allSettled` pattern.
|
|
115
|
+
*
|
|
116
|
+
* @param items array of items to process
|
|
117
|
+
* @param fn async function to apply to each item
|
|
118
|
+
* @param concurrency maximum number of concurrent operations
|
|
119
|
+
* @returns promise resolving to array of `PromiseSettledResult` objects in input order
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* const results = await map_concurrent_settled(urls, fetch, 5);
|
|
124
|
+
* for (const [i, result] of results.entries()) {
|
|
125
|
+
* if (result.status === 'fulfilled') {
|
|
126
|
+
* console.log(`${urls[i]}: ${result.value.status}`);
|
|
127
|
+
* } else {
|
|
128
|
+
* console.error(`${urls[i]}: ${result.reason}`);
|
|
129
|
+
* }
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export const map_concurrent_settled = async <T, R>(
|
|
134
|
+
items: Array<T>,
|
|
135
|
+
fn: (item: T, index: number) => Promise<R>,
|
|
136
|
+
concurrency: number,
|
|
137
|
+
): Promise<Array<PromiseSettledResult<R>>> => {
|
|
138
|
+
if (concurrency < 1) {
|
|
139
|
+
throw new Error('concurrency must be at least 1');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const results: Array<PromiseSettledResult<R>> = new Array(items.length);
|
|
143
|
+
let next_index = 0;
|
|
144
|
+
let active_count = 0;
|
|
145
|
+
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
const run_next = (): void => {
|
|
148
|
+
// Check if we're done
|
|
149
|
+
if (next_index >= items.length && active_count === 0) {
|
|
150
|
+
resolve(results);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Spawn workers up to concurrency limit
|
|
155
|
+
while (active_count < concurrency && next_index < items.length) {
|
|
156
|
+
const index = next_index++;
|
|
157
|
+
const item = items[index]!;
|
|
158
|
+
active_count++;
|
|
159
|
+
|
|
160
|
+
fn(item, index)
|
|
161
|
+
.then((value) => {
|
|
162
|
+
results[index] = {status: 'fulfilled', value};
|
|
163
|
+
})
|
|
164
|
+
.catch((reason: unknown) => {
|
|
165
|
+
results[index] = {status: 'rejected', reason};
|
|
166
|
+
})
|
|
167
|
+
.finally(() => {
|
|
168
|
+
active_count--;
|
|
169
|
+
run_next();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Handle empty array
|
|
175
|
+
if (items.length === 0) {
|
|
176
|
+
resolve(results);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
run_next();
|
|
181
|
+
});
|
|
182
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type {Flavored} from './types.js';
|
|
2
|
+
import {round} from './maths.js';
|
|
3
|
+
|
|
4
|
+
// TODO for high-performance usecases, we may want to add variants for any that return a new array to reuse a single array
|
|
5
|
+
// I've run into cases where this is a massive perceptible UX difference
|
|
6
|
+
|
|
7
|
+
// https://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion
|
|
8
|
+
|
|
9
|
+
export type Hsl = readonly [Hue, Saturation, Lightness];
|
|
10
|
+
export type Hue = Flavored<number, 'Hue'>; // [0, 1]
|
|
11
|
+
export type Saturation = Flavored<number, 'Saturation'>; // [0, 1]
|
|
12
|
+
export type Lightness = Flavored<number, 'Lightness'>; // [0, 1]
|
|
13
|
+
|
|
14
|
+
export type Rgb = readonly [Red, Green, Blue];
|
|
15
|
+
export type Red = Flavored<number, 'Red'>; // [0, 255]
|
|
16
|
+
export type Green = Flavored<number, 'Green'>; // [0, 255]
|
|
17
|
+
export type Blue = Flavored<number, 'Blue'>; // [0, 255]
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Converts an RGB color to a hex color.
|
|
21
|
+
*/
|
|
22
|
+
export const rgb_to_hex = (r: number, g: number, b: number): number => (r << 16) + (g << 8) + b;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Converts a hex color to an RGB color.
|
|
26
|
+
*/
|
|
27
|
+
export const hex_to_rgb = (hex: number): Rgb => [(hex >> 16) & 255, (hex >> 8) & 255, hex & 255];
|
|
28
|
+
|
|
29
|
+
export const hex_string_to_rgb = (hex: string): Rgb => {
|
|
30
|
+
var h = hex[0] === '#' ? hex.substring(1) : hex;
|
|
31
|
+
if (h.length !== 6 && h.length !== 8) throw new Error('invalid hex string');
|
|
32
|
+
return [parseInt(h[0]! + h[1]!, 16), parseInt(h[2]! + h[3]!, 16), parseInt(h[4]! + h[5]!, 16)];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const rgb_to_hex_string = (r: number, g: number, b: number): string =>
|
|
36
|
+
'#' + to_hex_component(r) + to_hex_component(g) + to_hex_component(b);
|
|
37
|
+
|
|
38
|
+
export const to_hex_component = (v: number): string => {
|
|
39
|
+
var h = v.toString(16);
|
|
40
|
+
return h.length === 1 ? '0' + h : h;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Converts an RGB color value to HSL. Conversion formula
|
|
45
|
+
* adapted from http://wikipedia.org/wiki/HSL_color_space.
|
|
46
|
+
* Values r/g/b are in the range [0,255] and
|
|
47
|
+
* returns h/s/l in the range [0,1].
|
|
48
|
+
*/
|
|
49
|
+
export const rgb_to_hsl = (r: number, g: number, b: number): Hsl => {
|
|
50
|
+
var r2 = r / 255;
|
|
51
|
+
var g2 = g / 255;
|
|
52
|
+
var b2 = b / 255;
|
|
53
|
+
var max = Math.max(r2, g2, b2);
|
|
54
|
+
var min = Math.min(r2, g2, b2);
|
|
55
|
+
var l: Lightness = (max + min) / 2;
|
|
56
|
+
var h!: Hue, s: Saturation;
|
|
57
|
+
if (max === min) {
|
|
58
|
+
h = s = 0; // achromatic
|
|
59
|
+
} else {
|
|
60
|
+
var d = max - min;
|
|
61
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
62
|
+
switch (max) {
|
|
63
|
+
case r2:
|
|
64
|
+
h = (g2 - b2) / d + (g2 < b2 ? 6 : 0);
|
|
65
|
+
break;
|
|
66
|
+
case g2:
|
|
67
|
+
h = (b2 - r2) / d + 2;
|
|
68
|
+
break;
|
|
69
|
+
case b2:
|
|
70
|
+
h = (r2 - g2) / d + 4;
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
h /= 6;
|
|
74
|
+
}
|
|
75
|
+
return [h, round(s, 2), round(l, 2)];
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Converts an HSL color value to RGB. Conversion formula
|
|
80
|
+
* adapted from http://wikipedia.org/wiki/HSL_color_space.
|
|
81
|
+
* Values h/s/l are in the range [0,1] and
|
|
82
|
+
* returns r/g/b in the range [0,255].
|
|
83
|
+
*/
|
|
84
|
+
export const hsl_to_rgb = (h: Hue, s: Saturation, l: Lightness): Rgb => {
|
|
85
|
+
var r: number, g: number, b: number;
|
|
86
|
+
if (s === 0) {
|
|
87
|
+
r = g = b = l; // achromatic
|
|
88
|
+
} else {
|
|
89
|
+
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
90
|
+
var p = 2 * l - q;
|
|
91
|
+
r = hue_to_rgb_component(p, q, h + 1 / 3);
|
|
92
|
+
g = hue_to_rgb_component(p, q, h);
|
|
93
|
+
b = hue_to_rgb_component(p, q, h - 1 / 3);
|
|
94
|
+
}
|
|
95
|
+
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export const hue_to_rgb_component = (p: number, q: number, t: number): number => {
|
|
99
|
+
var t2 = t < 0 ? t + 1 : t > 1 ? t - 1 : t;
|
|
100
|
+
if (t2 < 1 / 6) return p + (q - p) * 6 * t2;
|
|
101
|
+
if (t2 < 1 / 2) return q;
|
|
102
|
+
if (t2 < 2 / 3) return p + (q - p) * (2 / 3 - t2) * 6;
|
|
103
|
+
return p;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const hsl_to_hex = (h: Hue, s: Saturation, l: Lightness): number => {
|
|
107
|
+
var rgb = hsl_to_rgb(h, s, l); // TODO could safely use the optimized variant
|
|
108
|
+
return rgb_to_hex(rgb[0], rgb[1], rgb[2]);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const hsl_to_hex_string = (h: Hue, s: Saturation, l: Lightness): string => {
|
|
112
|
+
var rgb = hsl_to_rgb(h, s, l); // TODO could safely use the optimized variant
|
|
113
|
+
return rgb_to_hex_string(rgb[0], rgb[1], rgb[2]);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const hsl_to_string = (h: Hue, s: Saturation, l: Lightness): string =>
|
|
117
|
+
`hsl(${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%)`;
|
|
118
|
+
|
|
119
|
+
export const hex_string_to_hsl = (hex: string): Hsl => {
|
|
120
|
+
var rgb = hex_string_to_rgb(hex); // TODO could safely use the optimized variant
|
|
121
|
+
return rgb_to_hsl(rgb[0], rgb[1], rgb[2]);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const HSL_STRING_MATCHER = /^(hsl\()?\s*(\d+),?\s*(\d+)%,?\s*(\d+)%/;
|
|
125
|
+
|
|
126
|
+
export const parse_hsl_string = (hsl: string): Hsl => {
|
|
127
|
+
var match = HSL_STRING_MATCHER.exec(hsl);
|
|
128
|
+
if (!match) throw new Error('invalid HSL string');
|
|
129
|
+
return [Number(match[2]) / 360, Number(match[3]) / 100, Number(match[4]) / 100];
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// TODO either add an hsla variant or support alpha in the hsl variant
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type Counter = () => number;
|
|
2
|
+
|
|
3
|
+
export type CreateCounter = (initial?: number) => Counter;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a counter constructor function, starting at `0`.
|
|
7
|
+
*/
|
|
8
|
+
export const create_counter: CreateCounter = (count = 0) => {
|
|
9
|
+
let c = count;
|
|
10
|
+
return () => c++;
|
|
11
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep equality comparison that checks both structure and type.
|
|
3
|
+
*
|
|
4
|
+
* Key behaviors:
|
|
5
|
+
*
|
|
6
|
+
* - Compares by constructor to prevent type confusion (security: `{}` ≠ `[]`, `{}` ≠ `new Map()`, `new ClassA()` ≠ `new ClassB()`)
|
|
7
|
+
* - Prevents asymmetry bugs: `deep_equal(a, b)` always equals `deep_equal(b, a)`
|
|
8
|
+
* - Compares only enumerable own properties (ignores prototypes, symbols, non-enumerable)
|
|
9
|
+
* - Special handling for: Date (timestamp), Number/Boolean (boxed primitives), Error (message/name)
|
|
10
|
+
* - Promises always return false (cannot be meaningfully compared)
|
|
11
|
+
* - Maps/Sets compare by reference for object keys/values
|
|
12
|
+
*
|
|
13
|
+
* @param a first value to compare
|
|
14
|
+
* @param b second value to compare
|
|
15
|
+
* @returns true if deeply equal, false otherwise
|
|
16
|
+
*/
|
|
17
|
+
export const deep_equal = (a: unknown, b: unknown): boolean => {
|
|
18
|
+
if (Object.is(a, b)) return true;
|
|
19
|
+
|
|
20
|
+
const a_type = typeof a;
|
|
21
|
+
|
|
22
|
+
if (a_type !== typeof b) return false;
|
|
23
|
+
|
|
24
|
+
switch (a_type) {
|
|
25
|
+
case 'string':
|
|
26
|
+
case 'number':
|
|
27
|
+
case 'bigint':
|
|
28
|
+
case 'boolean':
|
|
29
|
+
case 'symbol':
|
|
30
|
+
case 'undefined':
|
|
31
|
+
case 'function':
|
|
32
|
+
return false;
|
|
33
|
+
default:
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// a_type === 'object'
|
|
38
|
+
|
|
39
|
+
if (a === null || b === null) return false;
|
|
40
|
+
|
|
41
|
+
// Constructor equality check prevents type confusion and ensures symmetry
|
|
42
|
+
// This means: {} ≠ [], {} ≠ new Map(), new ClassA() ≠ new ClassB()
|
|
43
|
+
// Security: prevents structural type confusion in equality checks
|
|
44
|
+
// Cache constructor for reuse in subsequent checks (avoids repeated property access)
|
|
45
|
+
const a_ctor = (a as any).constructor;
|
|
46
|
+
if (a_ctor !== (b as any).constructor) return false;
|
|
47
|
+
|
|
48
|
+
// Regular arrays: inline length check before function call (fast-fail for mismatched lengths)
|
|
49
|
+
// Use Array.isArray() instead of instanceof Array (JIT-optimized, works cross-realm)
|
|
50
|
+
if (Array.isArray(a)) {
|
|
51
|
+
const len = a.length;
|
|
52
|
+
if ((b as any).length !== len) return false;
|
|
53
|
+
for (let i = 0; i < len; i++) {
|
|
54
|
+
if (!deep_equal(a[i], (b as any)[i])) return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Use cached constructor for type checks (faster than instanceof - avoids prototype chain walk)
|
|
60
|
+
if (a_ctor === Set) {
|
|
61
|
+
if ((a as Set<unknown>).size !== (b as Set<unknown>).size) return false;
|
|
62
|
+
for (const a_value of a as Set<unknown>) {
|
|
63
|
+
if (!(b as Set<unknown>).has(a_value)) return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
if (a_ctor === Map) {
|
|
68
|
+
if ((a as Map<unknown, unknown>).size !== (b as Map<unknown, unknown>).size) return false;
|
|
69
|
+
for (const [key, a_value] of a as Map<unknown, unknown>) {
|
|
70
|
+
if (
|
|
71
|
+
!(b as Map<unknown, unknown>).has(key) ||
|
|
72
|
+
!deep_equal(a_value, (b as Map<unknown, unknown>).get(key))
|
|
73
|
+
)
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
if (a_ctor === RegExp) {
|
|
79
|
+
return (
|
|
80
|
+
(a as RegExp).source === (b as RegExp).source && (a as RegExp).flags === (b as RegExp).flags
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Date objects: compare by timestamp value
|
|
85
|
+
if (a_ctor === Date) {
|
|
86
|
+
// Using Object.is to handle NaN correctly (invalid dates)
|
|
87
|
+
return Object.is((a as Date).getTime(), (b as Date).getTime());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ArrayBuffer: convert to Uint8Array views for byte-by-byte comparison
|
|
91
|
+
if (a_ctor === ArrayBuffer) {
|
|
92
|
+
if ((a as ArrayBuffer).byteLength !== (b as ArrayBuffer).byteLength) return false;
|
|
93
|
+
const a_view = new Uint8Array(a as ArrayBuffer);
|
|
94
|
+
const b_view = new Uint8Array(b as ArrayBuffer);
|
|
95
|
+
const len = (a as ArrayBuffer).byteLength;
|
|
96
|
+
for (let i = 0; i < len; i++) {
|
|
97
|
+
if (a_view[i] !== b_view[i]) return false;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// TypedArrays: specialized fast path (no recursion needed - elements are always primitives)
|
|
103
|
+
// ArrayBuffer.isView() catches TypedArrays (Uint8Array, Int32Array, Float64Array, etc.)
|
|
104
|
+
// DataView is also caught by isView but needs special handling (no indexed access)
|
|
105
|
+
if (ArrayBuffer.isView(a)) {
|
|
106
|
+
// DataView: compare byte-by-byte using getUint8
|
|
107
|
+
if (a_ctor === DataView) {
|
|
108
|
+
const byte_len = (a as DataView).byteLength;
|
|
109
|
+
if ((b as DataView).byteLength !== byte_len) return false;
|
|
110
|
+
for (let i = 0; i < byte_len; i++) {
|
|
111
|
+
if ((a as DataView).getUint8(i) !== (b as DataView).getUint8(i)) return false;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
// TypedArrays: use indexed access (much faster)
|
|
116
|
+
if ((b as any).length !== (a as any).length) return false;
|
|
117
|
+
const len = (a as any).length;
|
|
118
|
+
for (let i = 0; i < len; i++) {
|
|
119
|
+
if ((a as any)[i] !== (b as any)[i]) return false;
|
|
120
|
+
}
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Boxed Number objects: compare by primitive value
|
|
125
|
+
if (a_ctor === Number) {
|
|
126
|
+
return Object.is(a!.valueOf(), (b as number).valueOf());
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Boxed Boolean objects: compare by primitive value
|
|
130
|
+
if (a_ctor === Boolean) {
|
|
131
|
+
return a!.valueOf() === (b as boolean).valueOf();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Error objects: compare by message and name
|
|
135
|
+
if (a_ctor === Error) {
|
|
136
|
+
return (a as Error).message === (b as Error).message && (a as Error).name === (b as Error).name;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Promise objects: cannot be meaningfully compared for deep equality
|
|
140
|
+
if (a_ctor === Promise) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Plain objects: compare enumerable own properties
|
|
145
|
+
const a_keys = Object.keys(a!);
|
|
146
|
+
const a_keys_length = a_keys.length;
|
|
147
|
+
if (a_keys_length !== Object.keys(b!).length) return false;
|
|
148
|
+
for (let i = 0; i < a_keys_length; i++) {
|
|
149
|
+
const key = a_keys[i]!;
|
|
150
|
+
if (!(key in (b as any))) return false;
|
|
151
|
+
if (!deep_equal((a as any)[key], (b as any)[key])) return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return true;
|
|
155
|
+
};
|
package/src/lib/dom.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if the given element is editable.
|
|
3
|
+
* Returns `true` for text-based input types, textareas, and contenteditable elements.
|
|
4
|
+
*/
|
|
5
|
+
export const is_editable = (el: any): boolean => {
|
|
6
|
+
if (!el) return false;
|
|
7
|
+
const {tagName} = el;
|
|
8
|
+
return (
|
|
9
|
+
(tagName === 'INPUT' && el.type !== 'hidden') ||
|
|
10
|
+
tagName === 'TEXTAREA' ||
|
|
11
|
+
el.contentEditable === 'true' ||
|
|
12
|
+
el.contentEditable === '' // Some browsers treat empty string as `'true'`
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns `true` if the element is within a `contenteditable` ancestor.
|
|
18
|
+
*/
|
|
19
|
+
export const inside_editable = (el: Element): boolean => {
|
|
20
|
+
const found = el.closest('[contenteditable]');
|
|
21
|
+
return found !== null && (found as any).contentEditable !== 'false';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Checks if the element is interactive (clickable, focusable, or otherwise accepts user input).
|
|
26
|
+
* Returns `true` for buttons, links, form controls,
|
|
27
|
+
* and elements with interactive attributes and ARIA roles.
|
|
28
|
+
*/
|
|
29
|
+
export const is_interactive = (el: any): boolean => {
|
|
30
|
+
if (!el) return false;
|
|
31
|
+
|
|
32
|
+
const {tagName} = el;
|
|
33
|
+
if (
|
|
34
|
+
tagName === 'BUTTON' ||
|
|
35
|
+
tagName === 'SELECT' ||
|
|
36
|
+
tagName === 'TEXTAREA' ||
|
|
37
|
+
tagName === 'A' ||
|
|
38
|
+
tagName === 'AUDIO' ||
|
|
39
|
+
tagName === 'VIDEO' ||
|
|
40
|
+
(tagName === 'INPUT' && el.type !== 'hidden')
|
|
41
|
+
) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const role = el.getAttribute?.('role');
|
|
46
|
+
return (
|
|
47
|
+
(role &&
|
|
48
|
+
(role === 'button' ||
|
|
49
|
+
role === 'link' ||
|
|
50
|
+
role === 'menuitem' ||
|
|
51
|
+
role === 'option' ||
|
|
52
|
+
role === 'switch' ||
|
|
53
|
+
role === 'tab')) ||
|
|
54
|
+
(el.hasAttribute?.('tabindex') && el.getAttribute('tabindex') !== '-1') ||
|
|
55
|
+
el.contentEditable === 'true' ||
|
|
56
|
+
el.contentEditable === '' ||
|
|
57
|
+
el.getAttribute?.('draggable') === 'true'
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Stops an event from bubbling and doing default behavior.
|
|
63
|
+
* @param event
|
|
64
|
+
* @param immediate defaults to `true` to use `stopImmediatePropagation` over `stopPropagation`
|
|
65
|
+
* @param preventDefault defaults to `true`
|
|
66
|
+
* @mutates event calls preventDefault(), stopPropagation(), or stopImmediatePropagation()
|
|
67
|
+
* @returns
|
|
68
|
+
*/
|
|
69
|
+
export const swallow = <
|
|
70
|
+
T extends Pick<Event, 'preventDefault' | 'stopPropagation' | 'stopImmediatePropagation'>,
|
|
71
|
+
>(
|
|
72
|
+
event: T,
|
|
73
|
+
immediate = true,
|
|
74
|
+
preventDefault = true,
|
|
75
|
+
): T => {
|
|
76
|
+
if (preventDefault) event.preventDefault();
|
|
77
|
+
if (immediate) {
|
|
78
|
+
event.stopImmediatePropagation();
|
|
79
|
+
} else {
|
|
80
|
+
event.stopPropagation();
|
|
81
|
+
}
|
|
82
|
+
return event;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// TODO improve these types, the motivation was the strictness of Svelte DOM types
|
|
86
|
+
/**
|
|
87
|
+
* Handles the value of an event's target and invokes a callback.
|
|
88
|
+
* Defaults to swallowing the event to prevent default actions and propagation.
|
|
89
|
+
* @mutates event calls `swallow()` which mutates the event if `swallow_event` is true
|
|
90
|
+
*/
|
|
91
|
+
export const handle_target_value =
|
|
92
|
+
(cb: (value: any, event: any) => void, swallow_event = true) =>
|
|
93
|
+
(e: any): void => {
|
|
94
|
+
if (swallow_event) swallow(e);
|
|
95
|
+
cb(e.target.value, e);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Returns a boolean indicating if the current browser window, if any, is iframed inside of another.
|
|
100
|
+
*/
|
|
101
|
+
export const is_iframed = (): boolean => {
|
|
102
|
+
if (typeof window === 'undefined') return false;
|
|
103
|
+
try {
|
|
104
|
+
return window.self !== window.top; // some browsers may throw here due to the same origin policy
|
|
105
|
+
} catch (_err) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
};
|
package/src/lib/error.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error for asserting unreachable code paths in TypeScript.
|
|
3
|
+
* Useful for exhaustive matching.
|
|
4
|
+
*/
|
|
5
|
+
export class UnreachableError extends Error {
|
|
6
|
+
constructor(value: never, message = `Unreachable case: ${value}`, options?: ErrorOptions) {
|
|
7
|
+
super(message, options);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Beyond terseness, this is useful because `throw` is not an expression,
|
|
13
|
+
* and therefore can't be used in places like Svelte markup without a workaround,
|
|
14
|
+
* at least until this proposal is accepted and widely available:
|
|
15
|
+
* https://github.com/tc39/proposal-throw-expressions
|
|
16
|
+
*/
|
|
17
|
+
export const unreachable: (value: never, message?: string) => asserts value is never = (
|
|
18
|
+
value,
|
|
19
|
+
message,
|
|
20
|
+
) => {
|
|
21
|
+
throw new UnreachableError(value, message);
|
|
22
|
+
};
|