bitwrench 2.0.13 → 2.0.15

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 (46) hide show
  1. package/README.md +4 -4
  2. package/dist/bitwrench-code-edit.cjs.js +46 -46
  3. package/dist/bitwrench-code-edit.cjs.min.js +16 -0
  4. package/dist/bitwrench-code-edit.es5.js +8 -8
  5. package/dist/bitwrench-code-edit.es5.min.js +2 -2
  6. package/dist/bitwrench-code-edit.esm.js +46 -46
  7. package/dist/bitwrench-code-edit.esm.min.js +2 -2
  8. package/dist/bitwrench-code-edit.umd.js +46 -46
  9. package/dist/bitwrench-code-edit.umd.min.js +2 -2
  10. package/dist/bitwrench-lean.cjs.js +5011 -3419
  11. package/dist/bitwrench-lean.cjs.min.js +35 -6
  12. package/dist/bitwrench-lean.es5.js +6218 -4272
  13. package/dist/bitwrench-lean.es5.min.js +32 -3
  14. package/dist/bitwrench-lean.esm.js +5011 -3419
  15. package/dist/bitwrench-lean.esm.min.js +35 -6
  16. package/dist/bitwrench-lean.umd.js +5011 -3419
  17. package/dist/bitwrench-lean.umd.min.js +35 -6
  18. package/dist/bitwrench.cjs.js +6966 -4662
  19. package/dist/bitwrench.cjs.min.js +38 -8
  20. package/dist/bitwrench.css +2453 -4784
  21. package/dist/bitwrench.es5.js +9592 -6813
  22. package/dist/bitwrench.es5.min.js +34 -5
  23. package/dist/bitwrench.esm.js +6966 -4662
  24. package/dist/bitwrench.esm.min.js +38 -8
  25. package/dist/bitwrench.min.css +1 -0
  26. package/dist/bitwrench.umd.js +6966 -4662
  27. package/dist/bitwrench.umd.min.js +38 -8
  28. package/dist/builds.json +89 -67
  29. package/dist/sri.json +28 -26
  30. package/package.json +7 -5
  31. package/readme.html +14 -14
  32. package/src/{bitwrench-components-v2.js → bitwrench-bccl.js} +1311 -600
  33. package/src/bitwrench-code-edit.js +45 -45
  34. package/src/bitwrench-color-utils.js +154 -27
  35. package/src/bitwrench-components-stub.js +4 -1
  36. package/src/bitwrench-file-ops.js +180 -0
  37. package/src/bitwrench-lean.js +2 -2
  38. package/src/bitwrench-styles.js +1468 -3494
  39. package/src/bitwrench-utils.js +458 -0
  40. package/src/bitwrench.js +1795 -1349
  41. package/src/cli/layout-default.js +18 -18
  42. package/src/generate-css.js +73 -53
  43. package/src/version.js +3 -3
  44. package/src/bitwrench-component-base.js +0 -736
  45. package/src/bitwrench-components-inline.js +0 -374
  46. package/src/bitwrench-components.js +0 -610
@@ -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
+ }