bitwrench 2.0.14 → 2.0.16
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/README.md +57 -21
- package/dist/bitwrench-bccl.cjs.js +3746 -0
- package/dist/bitwrench-bccl.cjs.min.js +40 -0
- package/dist/bitwrench-bccl.esm.js +3741 -0
- package/dist/bitwrench-bccl.esm.min.js +40 -0
- package/dist/bitwrench-bccl.umd.js +3752 -0
- package/dist/bitwrench-bccl.umd.min.js +40 -0
- package/dist/bitwrench-code-edit.cjs.js +99 -49
- package/dist/bitwrench-code-edit.cjs.min.js +23 -0
- package/dist/bitwrench-code-edit.es5.js +79 -16
- package/dist/bitwrench-code-edit.es5.min.js +9 -2
- package/dist/bitwrench-code-edit.esm.js +99 -49
- package/dist/bitwrench-code-edit.esm.min.js +9 -2
- package/dist/bitwrench-code-edit.umd.js +99 -49
- package/dist/bitwrench-code-edit.umd.min.js +9 -2
- package/dist/bitwrench-lean.cjs.js +4923 -3248
- package/dist/bitwrench-lean.cjs.min.js +35 -6
- package/dist/bitwrench-lean.es5.js +6325 -4580
- package/dist/bitwrench-lean.es5.min.js +32 -3
- package/dist/bitwrench-lean.esm.js +4923 -3248
- package/dist/bitwrench-lean.esm.min.js +35 -6
- package/dist/bitwrench-lean.umd.js +4923 -3248
- package/dist/bitwrench-lean.umd.min.js +35 -6
- package/dist/bitwrench.cjs.js +5082 -3667
- package/dist/bitwrench.cjs.min.js +38 -8
- package/dist/bitwrench.css +2289 -6034
- package/dist/bitwrench.es5.js +6862 -5346
- package/dist/bitwrench.es5.min.js +34 -5
- package/dist/bitwrench.esm.js +5082 -3667
- package/dist/bitwrench.esm.min.js +38 -8
- package/dist/bitwrench.min.css +1 -0
- package/dist/bitwrench.umd.js +5082 -3667
- package/dist/bitwrench.umd.min.js +38 -8
- package/dist/builds.json +184 -74
- package/dist/bwserve.cjs.js +646 -0
- package/dist/bwserve.esm.js +638 -0
- package/dist/sri.json +36 -26
- package/package.json +23 -6
- package/readme.html +71 -32
- package/src/bitwrench-bccl-entry.js +72 -0
- package/src/{bitwrench-components-v2.js → bitwrench-bccl.js} +396 -647
- package/src/bitwrench-code-edit.js +98 -48
- package/src/bitwrench-color-utils.js +24 -18
- package/src/bitwrench-components-stub.js +4 -1
- package/src/bitwrench-file-ops.js +180 -0
- package/src/bitwrench-lean.js +2 -2
- package/src/bitwrench-styles.js +1287 -4029
- package/src/bitwrench-utils.js +458 -0
- package/src/bitwrench.js +2070 -1292
- package/src/bwserve/client.js +182 -0
- package/src/bwserve/index.js +352 -0
- package/src/bwserve/shell.js +103 -0
- package/src/cli/index.js +36 -15
- package/src/cli/layout-default.js +18 -18
- package/src/cli/serve.js +325 -0
- package/src/generate-css.js +73 -53
- package/src/version.js +3 -3
- package/src/bitwrench-component-base.js +0 -736
- package/src/bitwrench-components-inline.js +0 -374
- package/src/bitwrench-components.js +0 -610
- /package/bin/{bitwrench.js → bwcli.js} +0 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bitwrench v2 Utility Functions
|
|
3
|
+
*
|
|
4
|
+
* Pure utility functions with no DOM dependencies. These work identically
|
|
5
|
+
* in Node.js and browsers: type detection, math, array ops, text generation,
|
|
6
|
+
* timing helpers.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from bitwrench.js to keep the core focused on DOM/TACO/state.
|
|
9
|
+
*
|
|
10
|
+
* @module bitwrench-utils
|
|
11
|
+
* @license BSD-2-Clause
|
|
12
|
+
* @author M A Chatterjee <deftio [at] deftio [dot] com>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Enhanced type detection that distinguishes arrays, dates, regexps, and more.
|
|
17
|
+
*
|
|
18
|
+
* Goes beyond `typeof` by using `Object.prototype.toString` to identify
|
|
19
|
+
* specific object types. Returns lowercase strings for primitives and arrays,
|
|
20
|
+
* PascalCase for built-in classes (Date, RegExp, Map, Set, etc.).
|
|
21
|
+
*
|
|
22
|
+
* @param {*} x - Value to examine
|
|
23
|
+
* @param {boolean} [baseTypeOnly=false] - If true, return only the base type ("object" for all objects)
|
|
24
|
+
* @returns {string} Type name
|
|
25
|
+
* @category Core
|
|
26
|
+
* @example
|
|
27
|
+
* typeOf("hello") // => "string"
|
|
28
|
+
* typeOf(42) // => "number"
|
|
29
|
+
* typeOf([1, 2, 3]) // => "array"
|
|
30
|
+
* typeOf(new Date()) // => "Date"
|
|
31
|
+
* typeOf({a: 1}) // => "Object"
|
|
32
|
+
* typeOf([1,2], true) // => "object"
|
|
33
|
+
*/
|
|
34
|
+
export function typeOf(x, baseTypeOnly) {
|
|
35
|
+
if (x === null) return "null";
|
|
36
|
+
|
|
37
|
+
const basic = typeof x;
|
|
38
|
+
|
|
39
|
+
if (basic !== "object") {
|
|
40
|
+
return basic; // covers: string, number, boolean, undefined, function, symbol, bigint
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (baseTypeOnly) return basic;
|
|
44
|
+
|
|
45
|
+
const stringTag = Object.prototype.toString.call(x);
|
|
46
|
+
|
|
47
|
+
const typeMap = {
|
|
48
|
+
'[object Array]': 'array',
|
|
49
|
+
'[object Date]': 'Date',
|
|
50
|
+
'[object RegExp]': 'RegExp',
|
|
51
|
+
'[object Error]': 'Error',
|
|
52
|
+
'[object Promise]': 'Promise',
|
|
53
|
+
'[object Map]': 'Map',
|
|
54
|
+
'[object Set]': 'Set',
|
|
55
|
+
'[object WeakMap]': 'WeakMap',
|
|
56
|
+
'[object WeakSet]': 'WeakSet',
|
|
57
|
+
'[object ArrayBuffer]': 'ArrayBuffer',
|
|
58
|
+
'[object DataView]': 'DataView',
|
|
59
|
+
'[object Int8Array]': 'Int8Array',
|
|
60
|
+
'[object Uint8Array]': 'Uint8Array',
|
|
61
|
+
'[object Uint8ClampedArray]': 'Uint8ClampedArray',
|
|
62
|
+
'[object Int16Array]': 'Int16Array',
|
|
63
|
+
'[object Uint16Array]': 'Uint16Array',
|
|
64
|
+
'[object Int32Array]': 'Int32Array',
|
|
65
|
+
'[object Uint32Array]': 'Uint32Array',
|
|
66
|
+
'[object Float32Array]': 'Float32Array',
|
|
67
|
+
'[object Float64Array]': 'Float64Array'
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (typeMap[stringTag]) {
|
|
71
|
+
return typeMap[stringTag];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check for custom bitwrench types
|
|
75
|
+
if (x._bw_type) {
|
|
76
|
+
return x._bw_type;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Try constructor name
|
|
80
|
+
if (x.constructor && x.constructor.name) {
|
|
81
|
+
return x.constructor.name;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return basic;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Map/scale a value from one range to another (linear interpolation).
|
|
89
|
+
*
|
|
90
|
+
* @param {number} x - Input value
|
|
91
|
+
* @param {number} in0 - Input range start
|
|
92
|
+
* @param {number} in1 - Input range end
|
|
93
|
+
* @param {number} out0 - Output range start
|
|
94
|
+
* @param {number} out1 - Output range end
|
|
95
|
+
* @param {Object} [options] - Mapping options
|
|
96
|
+
* @param {boolean} [options.clip=false] - Clamp result to output range
|
|
97
|
+
* @param {number} [options.expScale=1] - Exponential scaling factor
|
|
98
|
+
* @returns {number} Mapped value
|
|
99
|
+
* @category Math
|
|
100
|
+
* @example
|
|
101
|
+
* mapScale(50, 0, 100, 0, 1) // => 0.5
|
|
102
|
+
* mapScale(75, 0, 100, 0, 255) // => 191.25
|
|
103
|
+
*/
|
|
104
|
+
export function mapScale(x, in0, in1, out0, out1, options = {}) {
|
|
105
|
+
const { clip: doClip = false, expScale = 1 } = options;
|
|
106
|
+
|
|
107
|
+
// Normalize to 0-1
|
|
108
|
+
let normalized = (x - in0) / (in1 - in0);
|
|
109
|
+
|
|
110
|
+
// Apply exponential scaling
|
|
111
|
+
if (expScale !== 1) {
|
|
112
|
+
normalized = Math.pow(normalized, expScale);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Map to output range
|
|
116
|
+
let result = normalized * (out1 - out0) + out0;
|
|
117
|
+
|
|
118
|
+
// Clip if requested
|
|
119
|
+
if (doClip) {
|
|
120
|
+
const min = Math.min(out0, out1);
|
|
121
|
+
const max = Math.max(out0, out1);
|
|
122
|
+
result = Math.max(min, Math.min(max, result));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Clamp a value between min and max bounds.
|
|
130
|
+
*
|
|
131
|
+
* @param {number} value - Value to clamp
|
|
132
|
+
* @param {number} min - Minimum allowed value
|
|
133
|
+
* @param {number} max - Maximum allowed value
|
|
134
|
+
* @returns {number} Clamped value
|
|
135
|
+
* @category Math
|
|
136
|
+
* @example
|
|
137
|
+
* clip(150, 0, 100) // => 100
|
|
138
|
+
* clip(-5, 0, 100) // => 0
|
|
139
|
+
*/
|
|
140
|
+
export function clip(value, min, max) {
|
|
141
|
+
return Math.max(min, Math.min(max, value));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Use a dictionary as a switch statement, with support for function values.
|
|
146
|
+
*
|
|
147
|
+
* @param {*} x - Key to look up
|
|
148
|
+
* @param {Object} choices - Dictionary of choices (values can be functions)
|
|
149
|
+
* @param {*} def - Default value if key not found
|
|
150
|
+
* @returns {*} Value or function result
|
|
151
|
+
* @category Array Utilities
|
|
152
|
+
* @example
|
|
153
|
+
* var colors = { red: 1, blue: 2, aqua: function(z) { return z + 'marine'; } };
|
|
154
|
+
* choice('red', colors, '0') // => 1
|
|
155
|
+
* choice('aqua', colors) // => 'aquamarine'
|
|
156
|
+
*/
|
|
157
|
+
export function choice(x, choices, def) {
|
|
158
|
+
const z = (x in choices) ? choices[x] : def;
|
|
159
|
+
return typeOf(z) === "function" ? z(x) : z;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Return unique elements of an array (preserves first occurrence order).
|
|
164
|
+
*
|
|
165
|
+
* @param {Array} x - Input array
|
|
166
|
+
* @returns {Array} Array with unique elements
|
|
167
|
+
* @category Array Utilities
|
|
168
|
+
* @example
|
|
169
|
+
* arrayUniq([1, 2, 2, 3, 1]) // => [1, 2, 3]
|
|
170
|
+
*/
|
|
171
|
+
export function arrayUniq(x) {
|
|
172
|
+
if (typeOf(x) !== "array") return [];
|
|
173
|
+
return x.filter((v, i, arr) => arr.indexOf(v) === i);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Return the intersection of two arrays (elements present in both).
|
|
178
|
+
*
|
|
179
|
+
* @param {Array} a - First array
|
|
180
|
+
* @param {Array} b - Second array
|
|
181
|
+
* @returns {Array} Unique elements found in both a and b
|
|
182
|
+
* @category Array Utilities
|
|
183
|
+
* @example
|
|
184
|
+
* arrayBinA([1, 2, 3], [2, 3, 4]) // => [2, 3]
|
|
185
|
+
*/
|
|
186
|
+
export function arrayBinA(a, b) {
|
|
187
|
+
if (typeOf(a) !== "array" || typeOf(b) !== "array") return [];
|
|
188
|
+
return arrayUniq(a.filter(n => b.indexOf(n) !== -1));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Return elements of b that are not present in a (set difference).
|
|
193
|
+
*
|
|
194
|
+
* @param {Array} a - First array (the "exclude" set)
|
|
195
|
+
* @param {Array} b - Second array (source of results)
|
|
196
|
+
* @returns {Array} Unique elements in b but not in a
|
|
197
|
+
* @category Array Utilities
|
|
198
|
+
* @example
|
|
199
|
+
* arrayBNotInA([1, 2, 3], [2, 3, 4, 5]) // => [4, 5]
|
|
200
|
+
*/
|
|
201
|
+
export function arrayBNotInA(a, b) {
|
|
202
|
+
if (typeOf(a) !== "array" || typeOf(b) !== "array") return [];
|
|
203
|
+
return arrayUniq(b.filter(n => a.indexOf(n) < 0));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Interpolate between an array of colors based on a value in a range.
|
|
208
|
+
*
|
|
209
|
+
* @param {number} x - Value to interpolate
|
|
210
|
+
* @param {number} in0 - Input range start
|
|
211
|
+
* @param {number} in1 - Input range end
|
|
212
|
+
* @param {Array} colors - Array of CSS color strings to interpolate between
|
|
213
|
+
* @param {number} [stretch] - Exponential scaling factor (1 = linear)
|
|
214
|
+
* @param {Function} colorParseFn - Color parse function (injected to avoid circular dep)
|
|
215
|
+
* @returns {Array} Interpolated color as [r, g, b, a, "rgb"]
|
|
216
|
+
* @category Color
|
|
217
|
+
* @example
|
|
218
|
+
* colorInterp(50, 0, 100, ['#ff0000', '#00ff00'], undefined, bw.colorParse)
|
|
219
|
+
*/
|
|
220
|
+
export function colorInterp(x, in0, in1, colors, stretch, colorParseFn) {
|
|
221
|
+
let c = Array.isArray(colors) ? colors : ["#000", "#fff"];
|
|
222
|
+
c = c.length === 0 ? ["#000", "#fff"] : c;
|
|
223
|
+
if (c.length === 1) return c[0];
|
|
224
|
+
|
|
225
|
+
// Convert all colors to RGB format
|
|
226
|
+
c = c.map(col => colorParseFn(col));
|
|
227
|
+
|
|
228
|
+
const a = mapScale(x, in0, in1, 0, c.length - 1, { clip: true, expScale: stretch });
|
|
229
|
+
const i = clip(Math.floor(a), 0, c.length - 2);
|
|
230
|
+
const r = a - i;
|
|
231
|
+
|
|
232
|
+
const interp = (idx) => mapScale(r, 0, 1, c[i][idx], c[i + 1][idx], { clip: true });
|
|
233
|
+
return [interp(0), interp(1), interp(2), interp(3), "rgb"];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Generate Lorem Ipsum placeholder text.
|
|
238
|
+
*
|
|
239
|
+
* @param {number} [numChars] - Number of characters (random 25-150 if not provided)
|
|
240
|
+
* @param {number} [startSpot] - Starting index in Lorem text (random if undefined)
|
|
241
|
+
* @param {boolean} [startWithCapitalLetter=true] - Start with a capital letter
|
|
242
|
+
* @returns {string} Lorem ipsum text
|
|
243
|
+
* @category Text Generation
|
|
244
|
+
* @example
|
|
245
|
+
* loremIpsum(50)
|
|
246
|
+
* // => "Lorem ipsum dolor sit amet, consectetur adipiscin"
|
|
247
|
+
*/
|
|
248
|
+
export function loremIpsum(numChars, startSpot, startWithCapitalLetter = true) {
|
|
249
|
+
const lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. ";
|
|
250
|
+
|
|
251
|
+
// If numChars not provided, generate random length between 25-150
|
|
252
|
+
if (typeof numChars !== "number") {
|
|
253
|
+
numChars = Math.floor(Math.random() * 125) + 25;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// If startSpot is undefined, randomize it
|
|
257
|
+
if (startSpot === undefined) {
|
|
258
|
+
startSpot = Math.floor(Math.random() * lorem.length);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
startSpot = startSpot % lorem.length;
|
|
262
|
+
|
|
263
|
+
// Track how many characters we skip to honor numChars
|
|
264
|
+
let skippedChars = 0;
|
|
265
|
+
// Move startSpot to the next non-whitespace and non-punctuation character
|
|
266
|
+
while (lorem[startSpot] === ' ' || /[.,:;!?]/.test(lorem[startSpot])) {
|
|
267
|
+
startSpot = (startSpot + 1) % lorem.length;
|
|
268
|
+
skippedChars++;
|
|
269
|
+
// Prevent infinite loop in case entire lorem is spaces/punctuation
|
|
270
|
+
if (skippedChars >= lorem.length) {
|
|
271
|
+
startSpot = 0;
|
|
272
|
+
skippedChars = 0;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
let l = lorem.substring(startSpot) + lorem.substring(0, startSpot);
|
|
278
|
+
|
|
279
|
+
let result = "";
|
|
280
|
+
let remaining = numChars + skippedChars; // Add skipped chars to honor original numChars
|
|
281
|
+
|
|
282
|
+
while (remaining > 0) {
|
|
283
|
+
result += remaining < l.length ? l.substring(0, remaining) : l;
|
|
284
|
+
remaining -= l.length;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Trim to exact numChars length
|
|
288
|
+
if (result.length > numChars) {
|
|
289
|
+
result = result.substring(0, numChars);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Ensure no trailing space
|
|
293
|
+
if (result[result.length - 1] === " ") {
|
|
294
|
+
result = result.substring(0, result.length - 1) + ".";
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Ensure capital letter at start if requested
|
|
298
|
+
if (startWithCapitalLetter) {
|
|
299
|
+
let c = result[0].toUpperCase();
|
|
300
|
+
c = /[A-Z]/.test(c) ? c : "L"; // Use "L" as default if first char isn't a letter
|
|
301
|
+
result = c + result.substring(1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Create a multidimensional array filled with a value or function result.
|
|
309
|
+
*
|
|
310
|
+
* @param {*} value - Value or function to fill array with
|
|
311
|
+
* @param {number|Array} dims - Dimensions (number for 1D, array for multi-D)
|
|
312
|
+
* @returns {Array} Multidimensional array
|
|
313
|
+
* @category Array Utilities
|
|
314
|
+
* @example
|
|
315
|
+
* multiArray(0, [4, 5]) // 4x5 array of 0s
|
|
316
|
+
* multiArray(Math.random, [3, 4]) // 3x4 array of random numbers
|
|
317
|
+
*/
|
|
318
|
+
export function multiArray(value, dims) {
|
|
319
|
+
const v = () => typeOf(value) === "function" ? value() : value;
|
|
320
|
+
dims = typeof dims === "number" ? [dims] : dims;
|
|
321
|
+
|
|
322
|
+
const createArray = (dim) => {
|
|
323
|
+
if (dim >= dims.length) return v();
|
|
324
|
+
|
|
325
|
+
const arr = [];
|
|
326
|
+
for (let i = 0; i < dims[dim]; i++) {
|
|
327
|
+
arr[i] = createArray(dim + 1);
|
|
328
|
+
}
|
|
329
|
+
return arr;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
return createArray(0);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Natural sort comparison function for use with `Array.sort()`.
|
|
337
|
+
*
|
|
338
|
+
* Sorts strings with embedded numbers in human-expected order
|
|
339
|
+
* (e.g. "file2" before "file10") instead of lexicographic order.
|
|
340
|
+
*
|
|
341
|
+
* @param {*} as - First value
|
|
342
|
+
* @param {*} bs - Second value
|
|
343
|
+
* @returns {number} Sort order (-1, 0, 1)
|
|
344
|
+
* @category Array Utilities
|
|
345
|
+
* @example
|
|
346
|
+
* ['item10', 'item2', 'item1'].sort(naturalCompare)
|
|
347
|
+
* // => ['item1', 'item2', 'item10']
|
|
348
|
+
*/
|
|
349
|
+
export function naturalCompare(as, bs) {
|
|
350
|
+
// Handle numbers
|
|
351
|
+
if (isFinite(as) && isFinite(bs)) {
|
|
352
|
+
return Math.sign(as - bs);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const a = String(as).toLowerCase();
|
|
356
|
+
const b = String(bs).toLowerCase();
|
|
357
|
+
|
|
358
|
+
if (a === b) return as > bs ? 1 : 0;
|
|
359
|
+
|
|
360
|
+
// If no digits, simple string compare
|
|
361
|
+
if (!/\d/.test(a) || !/\d/.test(b)) {
|
|
362
|
+
return a > b ? 1 : -1;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Split into chunks of digits/non-digits
|
|
366
|
+
const aParts = a.match(/(\d+|\D+)/g) || [];
|
|
367
|
+
const bParts = b.match(/(\d+|\D+)/g) || [];
|
|
368
|
+
|
|
369
|
+
const len = Math.min(aParts.length, bParts.length);
|
|
370
|
+
|
|
371
|
+
for (let i = 0; i < len; i++) {
|
|
372
|
+
const aPart = aParts[i];
|
|
373
|
+
const bPart = bParts[i];
|
|
374
|
+
|
|
375
|
+
if (aPart !== bPart) {
|
|
376
|
+
// Both numeric
|
|
377
|
+
if (/^\d+$/.test(aPart) && /^\d+$/.test(bPart)) {
|
|
378
|
+
// Handle leading zeros
|
|
379
|
+
let aNum = aPart;
|
|
380
|
+
let bNum = bPart;
|
|
381
|
+
|
|
382
|
+
if (aPart[0] === "0") aNum = "0." + aPart;
|
|
383
|
+
if (bPart[0] === "0") bNum = "0." + bPart;
|
|
384
|
+
|
|
385
|
+
return parseFloat(aNum) - parseFloat(bNum);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// String comparison
|
|
389
|
+
return aPart > bPart ? 1 : -1;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Different lengths
|
|
394
|
+
return aParts.length - bParts.length;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Run `setInterval` with a maximum number of repetitions.
|
|
399
|
+
*
|
|
400
|
+
* @param {Function} callback - Function to call (receives iteration index)
|
|
401
|
+
* @param {number} delay - Delay between calls in ms
|
|
402
|
+
* @param {number} repetitions - Maximum number of times to call
|
|
403
|
+
* @returns {number} Interval ID (can be passed to clearInterval)
|
|
404
|
+
* @category Timing
|
|
405
|
+
* @example
|
|
406
|
+
* setIntervalX(function(i) {
|
|
407
|
+
* console.log('Iteration', i);
|
|
408
|
+
* }, 1000, 5); // Runs 5 times, 1 second apart
|
|
409
|
+
*/
|
|
410
|
+
export function setIntervalX(callback, delay, repetitions) {
|
|
411
|
+
let count = 0;
|
|
412
|
+
const intervalID = setInterval(function() {
|
|
413
|
+
callback(count);
|
|
414
|
+
|
|
415
|
+
if (++count >= repetitions) {
|
|
416
|
+
clearInterval(intervalID);
|
|
417
|
+
}
|
|
418
|
+
}, delay);
|
|
419
|
+
|
|
420
|
+
return intervalID;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Repeat a test function until it returns truthy, or give up after max attempts.
|
|
425
|
+
*
|
|
426
|
+
* @param {Function} testFn - Test function that returns truthy when done
|
|
427
|
+
* @param {Function} successFn - Called with test result when test passes
|
|
428
|
+
* @param {Function} [failFn] - Called on each failed test attempt
|
|
429
|
+
* @param {number} [delay=250] - Delay between attempts in ms
|
|
430
|
+
* @param {number} [maxReps=10] - Maximum number of attempts
|
|
431
|
+
* @param {Function} [lastFn] - Called when done with (success, count)
|
|
432
|
+
* @returns {string|number} "err" if invalid params, otherwise interval ID
|
|
433
|
+
* @category Timing
|
|
434
|
+
*/
|
|
435
|
+
export function repeatUntil(testFn, successFn, failFn, delay = 250, maxReps = 10, lastFn) {
|
|
436
|
+
if (typeof testFn !== "function") return "err";
|
|
437
|
+
|
|
438
|
+
let count = 0;
|
|
439
|
+
|
|
440
|
+
const intervalID = setInterval(function() {
|
|
441
|
+
const result = testFn();
|
|
442
|
+
count++;
|
|
443
|
+
|
|
444
|
+
if (result) {
|
|
445
|
+
clearInterval(intervalID);
|
|
446
|
+
if (successFn) successFn(result);
|
|
447
|
+
if (lastFn) lastFn(true, count);
|
|
448
|
+
} else if (count >= maxReps) {
|
|
449
|
+
clearInterval(intervalID);
|
|
450
|
+
if (failFn) failFn();
|
|
451
|
+
if (lastFn) lastFn(false, count);
|
|
452
|
+
} else {
|
|
453
|
+
if (failFn) failFn();
|
|
454
|
+
}
|
|
455
|
+
}, delay);
|
|
456
|
+
|
|
457
|
+
return intervalID;
|
|
458
|
+
}
|