tiny-essentials 1.10.2 → 1.12.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.
Files changed (106) hide show
  1. package/README.md +13 -4
  2. package/dist/node_modules/firebase-functions/lib/common/trace.cjs +82 -0
  3. package/dist/node_modules/firebase-functions/lib/logger/common.cjs +57 -0
  4. package/dist/node_modules/firebase-functions/lib/logger/index.cjs +158 -0
  5. package/dist/{TinyEssentials.js → v1/TinyBasicsEs.js} +1726 -1362
  6. package/dist/v1/TinyBasicsEs.min.js +2 -0
  7. package/dist/v1/TinyDragDropDetector.js +307 -0
  8. package/dist/v1/TinyDragDropDetector.min.js +1 -0
  9. package/dist/{TinyBasicsEs.js → v1/TinyDragger.js} +1004 -506
  10. package/dist/v1/TinyDragger.min.js +2 -0
  11. package/dist/v1/TinyEssentials.js +7938 -0
  12. package/dist/v1/TinyEssentials.min.js +2 -0
  13. package/dist/v1/TinyEssentials.min.js.LICENSE.txt +8 -0
  14. package/dist/v1/TinyNotifyCenter.js +493 -0
  15. package/dist/v1/TinyNotifyCenter.min.js +1 -0
  16. package/dist/v1/TinyToastNotify.js +441 -0
  17. package/dist/v1/TinyToastNotify.min.js +1 -0
  18. package/dist/v1/TinyUploadClicker.js +5235 -0
  19. package/dist/v1/TinyUploadClicker.min.js +2 -0
  20. package/dist/v1/TinyUploadClicker.min.js.LICENSE.txt +8 -0
  21. package/dist/v1/basics/fileManager.cjs +524 -0
  22. package/dist/v1/basics/fileManager.d.mts +209 -0
  23. package/dist/v1/basics/fileManager.mjs +443 -0
  24. package/dist/v1/basics/fullScreen.cjs +134 -0
  25. package/dist/v1/basics/fullScreen.d.mts +8 -0
  26. package/dist/v1/basics/fullScreen.mjs +121 -0
  27. package/dist/v1/basics/html.cjs +262 -0
  28. package/dist/v1/basics/html.d.mts +63 -0
  29. package/dist/v1/basics/html.mjs +208 -0
  30. package/dist/v1/basics/index.cjs +40 -0
  31. package/dist/v1/basics/index.d.mts +38 -1
  32. package/dist/v1/basics/index.mjs +4 -1
  33. package/dist/v1/build/TinyDragDropDetector.cjs +7 -0
  34. package/dist/v1/build/TinyDragDropDetector.d.mts +3 -0
  35. package/dist/v1/build/TinyDragDropDetector.mjs +2 -0
  36. package/dist/v1/build/TinyDragger.cjs +7 -0
  37. package/dist/v1/build/TinyDragger.d.mts +3 -0
  38. package/dist/v1/build/TinyDragger.mjs +2 -0
  39. package/dist/v1/build/TinyNotifyCenter.cjs +7 -0
  40. package/dist/v1/build/TinyNotifyCenter.d.mts +3 -0
  41. package/dist/v1/build/TinyNotifyCenter.mjs +2 -0
  42. package/dist/v1/build/TinyToastNotify.cjs +7 -0
  43. package/dist/v1/build/TinyToastNotify.d.mts +3 -0
  44. package/dist/v1/build/TinyToastNotify.mjs +2 -0
  45. package/dist/v1/build/TinyUploadClicker.cjs +7 -0
  46. package/dist/v1/build/TinyUploadClicker.d.mts +3 -0
  47. package/dist/v1/build/TinyUploadClicker.mjs +2 -0
  48. package/dist/v1/css/TinyDraggerExample.css +21 -0
  49. package/dist/v1/css/TinyDraggerExample.min.css +1 -0
  50. package/dist/v1/css/TinyNotify.css +350 -0
  51. package/dist/v1/css/TinyNotify.min.css +1 -0
  52. package/dist/v1/index.cjs +48 -0
  53. package/dist/v1/index.d.mts +42 -1
  54. package/dist/v1/index.mjs +8 -1
  55. package/dist/v1/libs/TinyDragDropDetector.cjs +268 -0
  56. package/dist/v1/libs/TinyDragDropDetector.d.mts +123 -0
  57. package/dist/v1/libs/TinyDragDropDetector.mjs +228 -0
  58. package/dist/v1/libs/TinyDragger.cjs +801 -0
  59. package/dist/v1/libs/TinyDragger.d.mts +300 -0
  60. package/dist/v1/libs/TinyDragger.mjs +689 -0
  61. package/dist/v1/libs/TinyNotifyCenter.cjs +454 -0
  62. package/dist/v1/libs/TinyNotifyCenter.d.mts +175 -0
  63. package/dist/v1/libs/TinyNotifyCenter.mjs +413 -0
  64. package/dist/v1/libs/TinyToastNotify.cjs +402 -0
  65. package/dist/v1/libs/TinyToastNotify.d.mts +165 -0
  66. package/dist/v1/libs/TinyToastNotify.mjs +352 -0
  67. package/dist/v1/libs/TinyUploadClicker.cjs +223 -0
  68. package/dist/v1/libs/TinyUploadClicker.d.mts +80 -0
  69. package/dist/v1/libs/TinyUploadClicker.mjs +182 -0
  70. package/docs/{README.md → v1/README.md} +8 -0
  71. package/docs/v1/basics/fileManager.md +222 -0
  72. package/docs/v1/basics/fullScreen.md +183 -0
  73. package/docs/v1/basics/html.md +243 -0
  74. package/docs/{basics → v1/basics}/text.md +2 -2
  75. package/docs/v1/libs/TinyDragDropDetector.md +143 -0
  76. package/docs/v1/libs/TinyDragger.md +415 -0
  77. package/docs/{libs → v1/libs}/TinyLevelUp.md +1 -1
  78. package/docs/v1/libs/TinyNotifyCenter.md +311 -0
  79. package/docs/v1/libs/TinyToastNotify.md +309 -0
  80. package/docs/v1/libs/TinyUploadClicker.md +108 -0
  81. package/package.json +14 -2
  82. package/dist/TinyBasicsEs.min.js +0 -2
  83. package/dist/TinyEssentials.min.js +0 -2
  84. package/dist/v1/libs/TinyRateLimit.cjs +0 -196
  85. package/dist/v1/libs/TinyRateLimit.d.mts +0 -86
  86. package/dist/v1/libs/TinyRateLimit.mjs +0 -171
  87. /package/dist/{ColorSafeStringify.js → v1/ColorSafeStringify.js} +0 -0
  88. /package/dist/{ColorSafeStringify.min.js → v1/ColorSafeStringify.min.js} +0 -0
  89. /package/dist/{TinyBasicsEs.min.js.LICENSE.txt → v1/TinyBasicsEs.min.js.LICENSE.txt} +0 -0
  90. /package/dist/{TinyEssentials.min.js.LICENSE.txt → v1/TinyDragger.min.js.LICENSE.txt} +0 -0
  91. /package/dist/{TinyLevelUp.js → v1/TinyLevelUp.js} +0 -0
  92. /package/dist/{TinyLevelUp.min.js → v1/TinyLevelUp.min.js} +0 -0
  93. /package/dist/{TinyPromiseQueue.js → v1/TinyPromiseQueue.js} +0 -0
  94. /package/dist/{TinyPromiseQueue.min.js → v1/TinyPromiseQueue.min.js} +0 -0
  95. /package/dist/{TinyRateLimiter.js → v1/TinyRateLimiter.js} +0 -0
  96. /package/dist/{TinyRateLimiter.min.js → v1/TinyRateLimiter.min.js} +0 -0
  97. /package/dist/{aiMarker.css → v1/css/aiMarker.css} +0 -0
  98. /package/dist/{aiMarker.min.css → v1/css/aiMarker.min.css} +0 -0
  99. /package/docs/{basics → v1/basics}/array.md +0 -0
  100. /package/docs/{basics → v1/basics}/asyncReplace.md +0 -0
  101. /package/docs/{basics → v1/basics}/clock.md +0 -0
  102. /package/docs/{basics → v1/basics}/objFilter.md +0 -0
  103. /package/docs/{basics → v1/basics}/simpleMath.md +0 -0
  104. /package/docs/{libs → v1/libs}/ColorSafeStringify.md +0 -0
  105. /package/docs/{libs → v1/libs}/TinyPromiseQueue.md +0 -0
  106. /package/docs/{libs → v1/libs}/TinyRateLimiter.md +0 -0
@@ -1,6 +1,13 @@
1
1
  /******/ (() => { // webpackBootstrap
2
2
  /******/ var __webpack_modules__ = ({
3
3
 
4
+ /***/ 41:
5
+ /***/ (() => {
6
+
7
+ /* (ignored) */
8
+
9
+ /***/ }),
10
+
4
11
  /***/ 251:
5
12
  /***/ ((__unused_webpack_module, exports) => {
6
13
 
@@ -2075,6 +2082,735 @@ function fromByteArray (uint8) {
2075
2082
  }
2076
2083
 
2077
2084
 
2085
+ /***/ }),
2086
+
2087
+ /***/ 606:
2088
+ /***/ ((module) => {
2089
+
2090
+ // shim for using process in browser
2091
+ var process = module.exports = {};
2092
+
2093
+ // cached from whatever global is present so that test runners that stub it
2094
+ // don't break things. But we need to wrap it in a try catch in case it is
2095
+ // wrapped in strict mode code which doesn't define any globals. It's inside a
2096
+ // function because try/catches deoptimize in certain engines.
2097
+
2098
+ var cachedSetTimeout;
2099
+ var cachedClearTimeout;
2100
+
2101
+ function defaultSetTimout() {
2102
+ throw new Error('setTimeout has not been defined');
2103
+ }
2104
+ function defaultClearTimeout () {
2105
+ throw new Error('clearTimeout has not been defined');
2106
+ }
2107
+ (function () {
2108
+ try {
2109
+ if (typeof setTimeout === 'function') {
2110
+ cachedSetTimeout = setTimeout;
2111
+ } else {
2112
+ cachedSetTimeout = defaultSetTimout;
2113
+ }
2114
+ } catch (e) {
2115
+ cachedSetTimeout = defaultSetTimout;
2116
+ }
2117
+ try {
2118
+ if (typeof clearTimeout === 'function') {
2119
+ cachedClearTimeout = clearTimeout;
2120
+ } else {
2121
+ cachedClearTimeout = defaultClearTimeout;
2122
+ }
2123
+ } catch (e) {
2124
+ cachedClearTimeout = defaultClearTimeout;
2125
+ }
2126
+ } ())
2127
+ function runTimeout(fun) {
2128
+ if (cachedSetTimeout === setTimeout) {
2129
+ //normal enviroments in sane situations
2130
+ return setTimeout(fun, 0);
2131
+ }
2132
+ // if setTimeout wasn't available but was latter defined
2133
+ if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) {
2134
+ cachedSetTimeout = setTimeout;
2135
+ return setTimeout(fun, 0);
2136
+ }
2137
+ try {
2138
+ // when when somebody has screwed with setTimeout but no I.E. maddness
2139
+ return cachedSetTimeout(fun, 0);
2140
+ } catch(e){
2141
+ try {
2142
+ // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
2143
+ return cachedSetTimeout.call(null, fun, 0);
2144
+ } catch(e){
2145
+ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error
2146
+ return cachedSetTimeout.call(this, fun, 0);
2147
+ }
2148
+ }
2149
+
2150
+
2151
+ }
2152
+ function runClearTimeout(marker) {
2153
+ if (cachedClearTimeout === clearTimeout) {
2154
+ //normal enviroments in sane situations
2155
+ return clearTimeout(marker);
2156
+ }
2157
+ // if clearTimeout wasn't available but was latter defined
2158
+ if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) {
2159
+ cachedClearTimeout = clearTimeout;
2160
+ return clearTimeout(marker);
2161
+ }
2162
+ try {
2163
+ // when when somebody has screwed with setTimeout but no I.E. maddness
2164
+ return cachedClearTimeout(marker);
2165
+ } catch (e){
2166
+ try {
2167
+ // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
2168
+ return cachedClearTimeout.call(null, marker);
2169
+ } catch (e){
2170
+ // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error.
2171
+ // Some versions of I.E. have different rules for clearTimeout vs setTimeout
2172
+ return cachedClearTimeout.call(this, marker);
2173
+ }
2174
+ }
2175
+
2176
+
2177
+
2178
+ }
2179
+ var queue = [];
2180
+ var draining = false;
2181
+ var currentQueue;
2182
+ var queueIndex = -1;
2183
+
2184
+ function cleanUpNextTick() {
2185
+ if (!draining || !currentQueue) {
2186
+ return;
2187
+ }
2188
+ draining = false;
2189
+ if (currentQueue.length) {
2190
+ queue = currentQueue.concat(queue);
2191
+ } else {
2192
+ queueIndex = -1;
2193
+ }
2194
+ if (queue.length) {
2195
+ drainQueue();
2196
+ }
2197
+ }
2198
+
2199
+ function drainQueue() {
2200
+ if (draining) {
2201
+ return;
2202
+ }
2203
+ var timeout = runTimeout(cleanUpNextTick);
2204
+ draining = true;
2205
+
2206
+ var len = queue.length;
2207
+ while(len) {
2208
+ currentQueue = queue;
2209
+ queue = [];
2210
+ while (++queueIndex < len) {
2211
+ if (currentQueue) {
2212
+ currentQueue[queueIndex].run();
2213
+ }
2214
+ }
2215
+ queueIndex = -1;
2216
+ len = queue.length;
2217
+ }
2218
+ currentQueue = null;
2219
+ draining = false;
2220
+ runClearTimeout(timeout);
2221
+ }
2222
+
2223
+ process.nextTick = function (fun) {
2224
+ var args = new Array(arguments.length - 1);
2225
+ if (arguments.length > 1) {
2226
+ for (var i = 1; i < arguments.length; i++) {
2227
+ args[i - 1] = arguments[i];
2228
+ }
2229
+ }
2230
+ queue.push(new Item(fun, args));
2231
+ if (queue.length === 1 && !draining) {
2232
+ runTimeout(drainQueue);
2233
+ }
2234
+ };
2235
+
2236
+ // v8 likes predictible objects
2237
+ function Item(fun, array) {
2238
+ this.fun = fun;
2239
+ this.array = array;
2240
+ }
2241
+ Item.prototype.run = function () {
2242
+ this.fun.apply(null, this.array);
2243
+ };
2244
+ process.title = 'browser';
2245
+ process.browser = true;
2246
+ process.env = {};
2247
+ process.argv = [];
2248
+ process.version = ''; // empty string to avoid regexp issues
2249
+ process.versions = {};
2250
+
2251
+ function noop() {}
2252
+
2253
+ process.on = noop;
2254
+ process.addListener = noop;
2255
+ process.once = noop;
2256
+ process.off = noop;
2257
+ process.removeListener = noop;
2258
+ process.removeAllListeners = noop;
2259
+ process.emit = noop;
2260
+ process.prependListener = noop;
2261
+ process.prependOnceListener = noop;
2262
+
2263
+ process.listeners = function (name) { return [] }
2264
+
2265
+ process.binding = function (name) {
2266
+ throw new Error('process.binding is not supported');
2267
+ };
2268
+
2269
+ process.cwd = function () { return '/' };
2270
+ process.chdir = function (dir) {
2271
+ throw new Error('process.chdir is not supported');
2272
+ };
2273
+ process.umask = function() { return 0; };
2274
+
2275
+
2276
+ /***/ }),
2277
+
2278
+ /***/ 975:
2279
+ /***/ ((module, __unused_webpack_exports, __webpack_require__) => {
2280
+
2281
+ "use strict";
2282
+ /* provided dependency */ var process = __webpack_require__(606);
2283
+ // 'path' module extracted from Node.js v8.11.1 (only the posix part)
2284
+ // transplited with Babel
2285
+
2286
+ // Copyright Joyent, Inc. and other Node contributors.
2287
+ //
2288
+ // Permission is hereby granted, free of charge, to any person obtaining a
2289
+ // copy of this software and associated documentation files (the
2290
+ // "Software"), to deal in the Software without restriction, including
2291
+ // without limitation the rights to use, copy, modify, merge, publish,
2292
+ // distribute, sublicense, and/or sell copies of the Software, and to permit
2293
+ // persons to whom the Software is furnished to do so, subject to the
2294
+ // following conditions:
2295
+ //
2296
+ // The above copyright notice and this permission notice shall be included
2297
+ // in all copies or substantial portions of the Software.
2298
+ //
2299
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
2300
+ // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
2301
+ // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
2302
+ // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
2303
+ // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
2304
+ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
2305
+ // USE OR OTHER DEALINGS IN THE SOFTWARE.
2306
+
2307
+
2308
+
2309
+ function assertPath(path) {
2310
+ if (typeof path !== 'string') {
2311
+ throw new TypeError('Path must be a string. Received ' + JSON.stringify(path));
2312
+ }
2313
+ }
2314
+
2315
+ // Resolves . and .. elements in a path with directory names
2316
+ function normalizeStringPosix(path, allowAboveRoot) {
2317
+ var res = '';
2318
+ var lastSegmentLength = 0;
2319
+ var lastSlash = -1;
2320
+ var dots = 0;
2321
+ var code;
2322
+ for (var i = 0; i <= path.length; ++i) {
2323
+ if (i < path.length)
2324
+ code = path.charCodeAt(i);
2325
+ else if (code === 47 /*/*/)
2326
+ break;
2327
+ else
2328
+ code = 47 /*/*/;
2329
+ if (code === 47 /*/*/) {
2330
+ if (lastSlash === i - 1 || dots === 1) {
2331
+ // NOOP
2332
+ } else if (lastSlash !== i - 1 && dots === 2) {
2333
+ if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 /*.*/ || res.charCodeAt(res.length - 2) !== 46 /*.*/) {
2334
+ if (res.length > 2) {
2335
+ var lastSlashIndex = res.lastIndexOf('/');
2336
+ if (lastSlashIndex !== res.length - 1) {
2337
+ if (lastSlashIndex === -1) {
2338
+ res = '';
2339
+ lastSegmentLength = 0;
2340
+ } else {
2341
+ res = res.slice(0, lastSlashIndex);
2342
+ lastSegmentLength = res.length - 1 - res.lastIndexOf('/');
2343
+ }
2344
+ lastSlash = i;
2345
+ dots = 0;
2346
+ continue;
2347
+ }
2348
+ } else if (res.length === 2 || res.length === 1) {
2349
+ res = '';
2350
+ lastSegmentLength = 0;
2351
+ lastSlash = i;
2352
+ dots = 0;
2353
+ continue;
2354
+ }
2355
+ }
2356
+ if (allowAboveRoot) {
2357
+ if (res.length > 0)
2358
+ res += '/..';
2359
+ else
2360
+ res = '..';
2361
+ lastSegmentLength = 2;
2362
+ }
2363
+ } else {
2364
+ if (res.length > 0)
2365
+ res += '/' + path.slice(lastSlash + 1, i);
2366
+ else
2367
+ res = path.slice(lastSlash + 1, i);
2368
+ lastSegmentLength = i - lastSlash - 1;
2369
+ }
2370
+ lastSlash = i;
2371
+ dots = 0;
2372
+ } else if (code === 46 /*.*/ && dots !== -1) {
2373
+ ++dots;
2374
+ } else {
2375
+ dots = -1;
2376
+ }
2377
+ }
2378
+ return res;
2379
+ }
2380
+
2381
+ function _format(sep, pathObject) {
2382
+ var dir = pathObject.dir || pathObject.root;
2383
+ var base = pathObject.base || (pathObject.name || '') + (pathObject.ext || '');
2384
+ if (!dir) {
2385
+ return base;
2386
+ }
2387
+ if (dir === pathObject.root) {
2388
+ return dir + base;
2389
+ }
2390
+ return dir + sep + base;
2391
+ }
2392
+
2393
+ var posix = {
2394
+ // path.resolve([from ...], to)
2395
+ resolve: function resolve() {
2396
+ var resolvedPath = '';
2397
+ var resolvedAbsolute = false;
2398
+ var cwd;
2399
+
2400
+ for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
2401
+ var path;
2402
+ if (i >= 0)
2403
+ path = arguments[i];
2404
+ else {
2405
+ if (cwd === undefined)
2406
+ cwd = process.cwd();
2407
+ path = cwd;
2408
+ }
2409
+
2410
+ assertPath(path);
2411
+
2412
+ // Skip empty entries
2413
+ if (path.length === 0) {
2414
+ continue;
2415
+ }
2416
+
2417
+ resolvedPath = path + '/' + resolvedPath;
2418
+ resolvedAbsolute = path.charCodeAt(0) === 47 /*/*/;
2419
+ }
2420
+
2421
+ // At this point the path should be resolved to a full absolute path, but
2422
+ // handle relative paths to be safe (might happen when process.cwd() fails)
2423
+
2424
+ // Normalize the path
2425
+ resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute);
2426
+
2427
+ if (resolvedAbsolute) {
2428
+ if (resolvedPath.length > 0)
2429
+ return '/' + resolvedPath;
2430
+ else
2431
+ return '/';
2432
+ } else if (resolvedPath.length > 0) {
2433
+ return resolvedPath;
2434
+ } else {
2435
+ return '.';
2436
+ }
2437
+ },
2438
+
2439
+ normalize: function normalize(path) {
2440
+ assertPath(path);
2441
+
2442
+ if (path.length === 0) return '.';
2443
+
2444
+ var isAbsolute = path.charCodeAt(0) === 47 /*/*/;
2445
+ var trailingSeparator = path.charCodeAt(path.length - 1) === 47 /*/*/;
2446
+
2447
+ // Normalize the path
2448
+ path = normalizeStringPosix(path, !isAbsolute);
2449
+
2450
+ if (path.length === 0 && !isAbsolute) path = '.';
2451
+ if (path.length > 0 && trailingSeparator) path += '/';
2452
+
2453
+ if (isAbsolute) return '/' + path;
2454
+ return path;
2455
+ },
2456
+
2457
+ isAbsolute: function isAbsolute(path) {
2458
+ assertPath(path);
2459
+ return path.length > 0 && path.charCodeAt(0) === 47 /*/*/;
2460
+ },
2461
+
2462
+ join: function join() {
2463
+ if (arguments.length === 0)
2464
+ return '.';
2465
+ var joined;
2466
+ for (var i = 0; i < arguments.length; ++i) {
2467
+ var arg = arguments[i];
2468
+ assertPath(arg);
2469
+ if (arg.length > 0) {
2470
+ if (joined === undefined)
2471
+ joined = arg;
2472
+ else
2473
+ joined += '/' + arg;
2474
+ }
2475
+ }
2476
+ if (joined === undefined)
2477
+ return '.';
2478
+ return posix.normalize(joined);
2479
+ },
2480
+
2481
+ relative: function relative(from, to) {
2482
+ assertPath(from);
2483
+ assertPath(to);
2484
+
2485
+ if (from === to) return '';
2486
+
2487
+ from = posix.resolve(from);
2488
+ to = posix.resolve(to);
2489
+
2490
+ if (from === to) return '';
2491
+
2492
+ // Trim any leading backslashes
2493
+ var fromStart = 1;
2494
+ for (; fromStart < from.length; ++fromStart) {
2495
+ if (from.charCodeAt(fromStart) !== 47 /*/*/)
2496
+ break;
2497
+ }
2498
+ var fromEnd = from.length;
2499
+ var fromLen = fromEnd - fromStart;
2500
+
2501
+ // Trim any leading backslashes
2502
+ var toStart = 1;
2503
+ for (; toStart < to.length; ++toStart) {
2504
+ if (to.charCodeAt(toStart) !== 47 /*/*/)
2505
+ break;
2506
+ }
2507
+ var toEnd = to.length;
2508
+ var toLen = toEnd - toStart;
2509
+
2510
+ // Compare paths to find the longest common path from root
2511
+ var length = fromLen < toLen ? fromLen : toLen;
2512
+ var lastCommonSep = -1;
2513
+ var i = 0;
2514
+ for (; i <= length; ++i) {
2515
+ if (i === length) {
2516
+ if (toLen > length) {
2517
+ if (to.charCodeAt(toStart + i) === 47 /*/*/) {
2518
+ // We get here if `from` is the exact base path for `to`.
2519
+ // For example: from='/foo/bar'; to='/foo/bar/baz'
2520
+ return to.slice(toStart + i + 1);
2521
+ } else if (i === 0) {
2522
+ // We get here if `from` is the root
2523
+ // For example: from='/'; to='/foo'
2524
+ return to.slice(toStart + i);
2525
+ }
2526
+ } else if (fromLen > length) {
2527
+ if (from.charCodeAt(fromStart + i) === 47 /*/*/) {
2528
+ // We get here if `to` is the exact base path for `from`.
2529
+ // For example: from='/foo/bar/baz'; to='/foo/bar'
2530
+ lastCommonSep = i;
2531
+ } else if (i === 0) {
2532
+ // We get here if `to` is the root.
2533
+ // For example: from='/foo'; to='/'
2534
+ lastCommonSep = 0;
2535
+ }
2536
+ }
2537
+ break;
2538
+ }
2539
+ var fromCode = from.charCodeAt(fromStart + i);
2540
+ var toCode = to.charCodeAt(toStart + i);
2541
+ if (fromCode !== toCode)
2542
+ break;
2543
+ else if (fromCode === 47 /*/*/)
2544
+ lastCommonSep = i;
2545
+ }
2546
+
2547
+ var out = '';
2548
+ // Generate the relative path based on the path difference between `to`
2549
+ // and `from`
2550
+ for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) {
2551
+ if (i === fromEnd || from.charCodeAt(i) === 47 /*/*/) {
2552
+ if (out.length === 0)
2553
+ out += '..';
2554
+ else
2555
+ out += '/..';
2556
+ }
2557
+ }
2558
+
2559
+ // Lastly, append the rest of the destination (`to`) path that comes after
2560
+ // the common path parts
2561
+ if (out.length > 0)
2562
+ return out + to.slice(toStart + lastCommonSep);
2563
+ else {
2564
+ toStart += lastCommonSep;
2565
+ if (to.charCodeAt(toStart) === 47 /*/*/)
2566
+ ++toStart;
2567
+ return to.slice(toStart);
2568
+ }
2569
+ },
2570
+
2571
+ _makeLong: function _makeLong(path) {
2572
+ return path;
2573
+ },
2574
+
2575
+ dirname: function dirname(path) {
2576
+ assertPath(path);
2577
+ if (path.length === 0) return '.';
2578
+ var code = path.charCodeAt(0);
2579
+ var hasRoot = code === 47 /*/*/;
2580
+ var end = -1;
2581
+ var matchedSlash = true;
2582
+ for (var i = path.length - 1; i >= 1; --i) {
2583
+ code = path.charCodeAt(i);
2584
+ if (code === 47 /*/*/) {
2585
+ if (!matchedSlash) {
2586
+ end = i;
2587
+ break;
2588
+ }
2589
+ } else {
2590
+ // We saw the first non-path separator
2591
+ matchedSlash = false;
2592
+ }
2593
+ }
2594
+
2595
+ if (end === -1) return hasRoot ? '/' : '.';
2596
+ if (hasRoot && end === 1) return '//';
2597
+ return path.slice(0, end);
2598
+ },
2599
+
2600
+ basename: function basename(path, ext) {
2601
+ if (ext !== undefined && typeof ext !== 'string') throw new TypeError('"ext" argument must be a string');
2602
+ assertPath(path);
2603
+
2604
+ var start = 0;
2605
+ var end = -1;
2606
+ var matchedSlash = true;
2607
+ var i;
2608
+
2609
+ if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
2610
+ if (ext.length === path.length && ext === path) return '';
2611
+ var extIdx = ext.length - 1;
2612
+ var firstNonSlashEnd = -1;
2613
+ for (i = path.length - 1; i >= 0; --i) {
2614
+ var code = path.charCodeAt(i);
2615
+ if (code === 47 /*/*/) {
2616
+ // If we reached a path separator that was not part of a set of path
2617
+ // separators at the end of the string, stop now
2618
+ if (!matchedSlash) {
2619
+ start = i + 1;
2620
+ break;
2621
+ }
2622
+ } else {
2623
+ if (firstNonSlashEnd === -1) {
2624
+ // We saw the first non-path separator, remember this index in case
2625
+ // we need it if the extension ends up not matching
2626
+ matchedSlash = false;
2627
+ firstNonSlashEnd = i + 1;
2628
+ }
2629
+ if (extIdx >= 0) {
2630
+ // Try to match the explicit extension
2631
+ if (code === ext.charCodeAt(extIdx)) {
2632
+ if (--extIdx === -1) {
2633
+ // We matched the extension, so mark this as the end of our path
2634
+ // component
2635
+ end = i;
2636
+ }
2637
+ } else {
2638
+ // Extension does not match, so our result is the entire path
2639
+ // component
2640
+ extIdx = -1;
2641
+ end = firstNonSlashEnd;
2642
+ }
2643
+ }
2644
+ }
2645
+ }
2646
+
2647
+ if (start === end) end = firstNonSlashEnd;else if (end === -1) end = path.length;
2648
+ return path.slice(start, end);
2649
+ } else {
2650
+ for (i = path.length - 1; i >= 0; --i) {
2651
+ if (path.charCodeAt(i) === 47 /*/*/) {
2652
+ // If we reached a path separator that was not part of a set of path
2653
+ // separators at the end of the string, stop now
2654
+ if (!matchedSlash) {
2655
+ start = i + 1;
2656
+ break;
2657
+ }
2658
+ } else if (end === -1) {
2659
+ // We saw the first non-path separator, mark this as the end of our
2660
+ // path component
2661
+ matchedSlash = false;
2662
+ end = i + 1;
2663
+ }
2664
+ }
2665
+
2666
+ if (end === -1) return '';
2667
+ return path.slice(start, end);
2668
+ }
2669
+ },
2670
+
2671
+ extname: function extname(path) {
2672
+ assertPath(path);
2673
+ var startDot = -1;
2674
+ var startPart = 0;
2675
+ var end = -1;
2676
+ var matchedSlash = true;
2677
+ // Track the state of characters (if any) we see before our first dot and
2678
+ // after any path separator we find
2679
+ var preDotState = 0;
2680
+ for (var i = path.length - 1; i >= 0; --i) {
2681
+ var code = path.charCodeAt(i);
2682
+ if (code === 47 /*/*/) {
2683
+ // If we reached a path separator that was not part of a set of path
2684
+ // separators at the end of the string, stop now
2685
+ if (!matchedSlash) {
2686
+ startPart = i + 1;
2687
+ break;
2688
+ }
2689
+ continue;
2690
+ }
2691
+ if (end === -1) {
2692
+ // We saw the first non-path separator, mark this as the end of our
2693
+ // extension
2694
+ matchedSlash = false;
2695
+ end = i + 1;
2696
+ }
2697
+ if (code === 46 /*.*/) {
2698
+ // If this is our first dot, mark it as the start of our extension
2699
+ if (startDot === -1)
2700
+ startDot = i;
2701
+ else if (preDotState !== 1)
2702
+ preDotState = 1;
2703
+ } else if (startDot !== -1) {
2704
+ // We saw a non-dot and non-path separator before our dot, so we should
2705
+ // have a good chance at having a non-empty extension
2706
+ preDotState = -1;
2707
+ }
2708
+ }
2709
+
2710
+ if (startDot === -1 || end === -1 ||
2711
+ // We saw a non-dot character immediately before the dot
2712
+ preDotState === 0 ||
2713
+ // The (right-most) trimmed path component is exactly '..'
2714
+ preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
2715
+ return '';
2716
+ }
2717
+ return path.slice(startDot, end);
2718
+ },
2719
+
2720
+ format: function format(pathObject) {
2721
+ if (pathObject === null || typeof pathObject !== 'object') {
2722
+ throw new TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
2723
+ }
2724
+ return _format('/', pathObject);
2725
+ },
2726
+
2727
+ parse: function parse(path) {
2728
+ assertPath(path);
2729
+
2730
+ var ret = { root: '', dir: '', base: '', ext: '', name: '' };
2731
+ if (path.length === 0) return ret;
2732
+ var code = path.charCodeAt(0);
2733
+ var isAbsolute = code === 47 /*/*/;
2734
+ var start;
2735
+ if (isAbsolute) {
2736
+ ret.root = '/';
2737
+ start = 1;
2738
+ } else {
2739
+ start = 0;
2740
+ }
2741
+ var startDot = -1;
2742
+ var startPart = 0;
2743
+ var end = -1;
2744
+ var matchedSlash = true;
2745
+ var i = path.length - 1;
2746
+
2747
+ // Track the state of characters (if any) we see before our first dot and
2748
+ // after any path separator we find
2749
+ var preDotState = 0;
2750
+
2751
+ // Get non-dir info
2752
+ for (; i >= start; --i) {
2753
+ code = path.charCodeAt(i);
2754
+ if (code === 47 /*/*/) {
2755
+ // If we reached a path separator that was not part of a set of path
2756
+ // separators at the end of the string, stop now
2757
+ if (!matchedSlash) {
2758
+ startPart = i + 1;
2759
+ break;
2760
+ }
2761
+ continue;
2762
+ }
2763
+ if (end === -1) {
2764
+ // We saw the first non-path separator, mark this as the end of our
2765
+ // extension
2766
+ matchedSlash = false;
2767
+ end = i + 1;
2768
+ }
2769
+ if (code === 46 /*.*/) {
2770
+ // If this is our first dot, mark it as the start of our extension
2771
+ if (startDot === -1) startDot = i;else if (preDotState !== 1) preDotState = 1;
2772
+ } else if (startDot !== -1) {
2773
+ // We saw a non-dot and non-path separator before our dot, so we should
2774
+ // have a good chance at having a non-empty extension
2775
+ preDotState = -1;
2776
+ }
2777
+ }
2778
+
2779
+ if (startDot === -1 || end === -1 ||
2780
+ // We saw a non-dot character immediately before the dot
2781
+ preDotState === 0 ||
2782
+ // The (right-most) trimmed path component is exactly '..'
2783
+ preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
2784
+ if (end !== -1) {
2785
+ if (startPart === 0 && isAbsolute) ret.base = ret.name = path.slice(1, end);else ret.base = ret.name = path.slice(startPart, end);
2786
+ }
2787
+ } else {
2788
+ if (startPart === 0 && isAbsolute) {
2789
+ ret.name = path.slice(1, startDot);
2790
+ ret.base = path.slice(1, end);
2791
+ } else {
2792
+ ret.name = path.slice(startPart, startDot);
2793
+ ret.base = path.slice(startPart, end);
2794
+ }
2795
+ ret.ext = path.slice(startDot, end);
2796
+ }
2797
+
2798
+ if (startPart > 0) ret.dir = path.slice(0, startPart - 1);else if (isAbsolute) ret.dir = '/';
2799
+
2800
+ return ret;
2801
+ },
2802
+
2803
+ sep: '/',
2804
+ delimiter: ':',
2805
+ win32: null,
2806
+ posix: null
2807
+ };
2808
+
2809
+ posix.posix = posix;
2810
+
2811
+ module.exports = posix;
2812
+
2813
+
2078
2814
  /***/ })
2079
2815
 
2080
2816
  /******/ });
@@ -2142,33 +2878,93 @@ __webpack_require__.r(__webpack_exports__);
2142
2878
 
2143
2879
  // EXPORTS
2144
2880
  __webpack_require__.d(__webpack_exports__, {
2145
- ColorSafeStringify: () => (/* reexport */ libs_ColorSafeStringify),
2146
- TinyLevelUp: () => (/* reexport */ userLevel),
2147
- TinyPromiseQueue: () => (/* reexport */ libs_TinyPromiseQueue),
2148
- TinyRateLimiter: () => (/* reexport */ libs_TinyRateLimiter),
2149
2881
  addAiMarkerShortcut: () => (/* reexport */ addAiMarkerShortcut),
2882
+ areHtmlElsColliding: () => (/* reexport */ areHtmlElsColliding),
2150
2883
  arraySortPositions: () => (/* reexport */ arraySortPositions),
2151
2884
  asyncReplace: () => (/* reexport */ asyncReplace),
2152
- checkObj: () => (/* reexport */ checkObj),
2885
+ backupFile: () => (/* reexport */ backupFile),
2886
+ clearDirectory: () => (/* reexport */ clearDirectory),
2153
2887
  cloneObjTypeOrder: () => (/* reexport */ cloneObjTypeOrder),
2154
2888
  countObj: () => (/* reexport */ countObj),
2889
+ dirExists: () => (/* reexport */ dirExists),
2890
+ dirSize: () => (/* reexport */ dirSize),
2891
+ documentIsFullScreen: () => (/* reexport */ documentIsFullScreen),
2892
+ ensureCopyFile: () => (/* reexport */ ensureCopyFile),
2893
+ ensureDirectory: () => (/* reexport */ ensureDirectory),
2894
+ exitFullScreen: () => (/* reexport */ exitFullScreen),
2155
2895
  extendObjType: () => (/* reexport */ extendObjType),
2896
+ fetchJson: () => (/* reexport */ fetchJson),
2897
+ fileExists: () => (/* reexport */ fileExists),
2898
+ fileSize: () => (/* reexport */ fileSize),
2156
2899
  formatBytes: () => (/* reexport */ formatBytes),
2157
2900
  formatCustomTimer: () => (/* reexport */ formatCustomTimer),
2158
2901
  formatDayTimer: () => (/* reexport */ formatDayTimer),
2159
2902
  formatTimer: () => (/* reexport */ formatTimer),
2160
2903
  getAge: () => (/* reexport */ getAge),
2904
+ getHtmlElBorders: () => (/* reexport */ getHtmlElBorders),
2905
+ getHtmlElBordersWidth: () => (/* reexport */ getHtmlElBordersWidth),
2906
+ getHtmlElMargin: () => (/* reexport */ getHtmlElMargin),
2907
+ getHtmlElPadding: () => (/* reexport */ getHtmlElPadding),
2908
+ getLatestBackupPath: () => (/* reexport */ getLatestBackupPath),
2161
2909
  getSimplePerc: () => (/* reexport */ getSimplePerc),
2162
2910
  getTimeDuration: () => (/* reexport */ getTimeDuration),
2911
+ isDirEmpty: () => (/* reexport */ isDirEmpty),
2912
+ isFullScreenMode: () => (/* reexport */ isFullScreenMode),
2163
2913
  isJsonObject: () => (/* reexport */ isJsonObject),
2914
+ isScreenFilled: () => (/* reexport */ isScreenFilled),
2915
+ listDirs: () => (/* reexport */ listDirs),
2916
+ listFiles: () => (/* reexport */ listFiles),
2164
2917
  objType: () => (/* reexport */ objType),
2918
+ offFullScreenChange: () => (/* reexport */ offFullScreenChange),
2919
+ onFullScreenChange: () => (/* reexport */ onFullScreenChange),
2920
+ readJsonBlob: () => (/* reexport */ readJsonBlob),
2921
+ readJsonFile: () => (/* reexport */ readJsonFile),
2922
+ renameFileAddPrefixSuffix: () => (/* reexport */ renameFileAddPrefixSuffix),
2923
+ renameFileBatch: () => (/* reexport */ renameFileBatch),
2924
+ renameFileNormalizeCase: () => (/* reexport */ renameFileNormalizeCase),
2925
+ renameFilePadNumbers: () => (/* reexport */ renameFilePadNumbers),
2926
+ renameFileRegex: () => (/* reexport */ renameFileRegex),
2165
2927
  reorderObjTypeOrder: () => (/* reexport */ reorderObjTypeOrder),
2928
+ requestFullScreen: () => (/* reexport */ requestFullScreen),
2929
+ restoreLatestBackup: () => (/* reexport */ restoreLatestBackup),
2166
2930
  ruleOfThree: () => (/* reexport */ ruleOfThree),
2931
+ saveJsonFile: () => (/* reexport */ saveJsonFile),
2167
2932
  shuffleArray: () => (/* reexport */ shuffleArray),
2168
2933
  toTitleCase: () => (/* reexport */ toTitleCase),
2169
- toTitleCaseLowerFirst: () => (/* reexport */ toTitleCaseLowerFirst)
2934
+ toTitleCaseLowerFirst: () => (/* reexport */ toTitleCaseLowerFirst),
2935
+ tryDeleteFile: () => (/* reexport */ tryDeleteFile),
2936
+ writeJsonFile: () => (/* reexport */ writeJsonFile),
2937
+ writeTextFile: () => (/* reexport */ writeTextFile)
2170
2938
  });
2171
2939
 
2940
+ ;// ./src/legacy/libs/arraySortPositions.mjs
2941
+ /**
2942
+ * Generates a comparator function to sort an array of objects by a given key.
2943
+ *
2944
+ * @param {string} item - The object key to sort by.
2945
+ * @param {boolean} [isReverse=false] - If `true`, the sorting will be in descending order.
2946
+ * @returns {(a: Object<string|number, *>, b: Object<string|number, *>) => number} Comparator function compatible with Array.prototype.sort().
2947
+ *
2948
+ * @example
2949
+ * const arr = [{ pos: 2 }, { pos: 1 }, { pos: 3 }];
2950
+ * arr.sort(arraySortPositions('pos')); // Ascending: [{pos: 1}, {pos: 2}, {pos: 3}]
2951
+ *
2952
+ * @example
2953
+ * const arr = [{ pos: 2 }, { pos: 1 }, { pos: 3 }];
2954
+ * arr.sort(arraySortPositions('pos', true)); // Descending: [{pos: 3}, {pos: 2}, {pos: 1}]
2955
+ */
2956
+ function arraySortPositions(item, isReverse = false) {
2957
+ if (!isReverse) {
2958
+ return function (a, b) {
2959
+ return a[item] < b[item] ? -1 : a[item] > b[item] ? 1 : 0;
2960
+ };
2961
+ } else {
2962
+ return function (a, b) {
2963
+ return a[item] > b[item] ? -1 : a[item] < b[item] ? 1 : 0;
2964
+ };
2965
+ }
2966
+ }
2967
+
2172
2968
  ;// ./src/legacy/libs/replaceAsync.mjs
2173
2969
  /**
2174
2970
  * Asynchronously replaces matches in a string using a regex and an async function.
@@ -2202,348 +2998,68 @@ async function asyncReplace(str, regex, asyncFn) {
2202
2998
  return str.replace(regex, () => data.shift());
2203
2999
  }
2204
3000
 
2205
- ;// ./src/legacy/libs/userLevel.mjs
3001
+ ;// ./src/v1/basics/array.mjs
3002
+ // https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
3003
+
2206
3004
  /**
2207
- * Represents a user object used.
3005
+ * Randomly shuffles the elements of an array in place using the Fisher–Yates algorithm.
2208
3006
  *
2209
- * @typedef {Object} UserEditor
2210
- * @property {number} exp - Current experience points of the user.
2211
- * @property {number} level - Current level of the user.
2212
- * @property {number} totalExp - Total accumulated experience.
3007
+ * This implementation ensures a uniform distribution of permutations.
3008
+ * Original algorithm source: StackOverflow (link above).
3009
+ *
3010
+ * @param {any[]} items - The array to shuffle.
3011
+ * @returns {any[]} The same array instance, now shuffled in place.
2213
3012
  */
3013
+ function shuffleArray(items) {
3014
+ let currentIndex = items.length,
3015
+ randomIndex;
3016
+
3017
+ // While there remain elements to shuffle...
3018
+ while (currentIndex !== 0) {
3019
+ // Pick a remaining element...
3020
+ randomIndex = Math.floor(Math.random() * currentIndex);
3021
+ currentIndex--;
3022
+
3023
+ // And swap it with the current element.
3024
+ [items[currentIndex], items[randomIndex]] = [items[randomIndex], items[currentIndex]];
3025
+ }
3026
+
3027
+ return items;
3028
+ }
2214
3029
 
3030
+ ;// ./src/v1/basics/clock.mjs
2215
3031
  /**
2216
- * Class to manage user level-up logic based on experience points.
3032
+ * Calculates the time duration between the current time and a given time offset.
3033
+ *
3034
+ * @param {Date} timeData - The target time as a Date object.
3035
+ * @param {string} [durationType='asSeconds'] - The type of duration to return. Can be 'asMilliseconds', 'asSeconds', 'asMinutes', 'asHours', 'asDays'.
3036
+ * @param {Date|null} [now=null] - The current time as a Date object. Defaults to the current date and time if not provided.
3037
+ * @returns {number|null} The calculated duration in the specified unit, or `null` if `timeData` is not provided.
2217
3038
  */
2218
- class TinyLevelUp {
2219
- /**
2220
- * Constructor
2221
- * @param {number} giveExp - Base experience value for random experience generation.
2222
- * @param {number} expLevel - Base experience needed to level up (per level).
2223
- */
2224
- constructor(giveExp, expLevel) {
2225
- if (typeof giveExp !== 'number' || Number.isNaN(giveExp))
2226
- throw new Error('giveExp must be a valid number');
2227
- if (typeof expLevel !== 'number' || Number.isNaN(expLevel))
2228
- throw new Error('expLevel must be a valid number');
2229
- this.giveExp = giveExp;
2230
- this.expLevel = expLevel;
2231
- }
3039
+ function getTimeDuration(timeData = new Date(), durationType = 'asSeconds', now = null) {
3040
+ if (timeData instanceof Date) {
3041
+ const currentTime = now instanceof Date ? now : new Date();
3042
+ /** @type {number} */
3043
+ const diffMs = timeData.getTime() - currentTime.getTime();
2232
3044
 
2233
- /**
2234
- * Creates a new user object starting at level 0 with 0 experience.
2235
- * @returns {UserEditor} A fresh user object.
2236
- */
2237
- createUser() {
2238
- return {
2239
- exp: 0,
2240
- level: 1,
2241
- totalExp: 0,
2242
- };
3045
+ switch (durationType) {
3046
+ case 'asMilliseconds':
3047
+ return diffMs;
3048
+ case 'asSeconds':
3049
+ return diffMs / 1000;
3050
+ case 'asMinutes':
3051
+ return diffMs / (1000 * 60);
3052
+ case 'asHours':
3053
+ return diffMs / (1000 * 60 * 60);
3054
+ case 'asDays':
3055
+ return diffMs / (1000 * 60 * 60 * 24);
3056
+ default:
3057
+ return diffMs / 1000; // default to seconds
3058
+ }
2243
3059
  }
2244
3060
 
2245
- /**
2246
- * Validates if the given user object has valid numeric properties.
2247
- * Throws an error if any property is invalid.
2248
- *
2249
- * @param {UserEditor} user - The user object to validate.
2250
- * @throws {Error} If any property (exp, level, totalExp) is not a valid number.
2251
- */
2252
- validateUser(user) {
2253
- if (typeof user.exp !== 'number' || Number.isNaN(user.exp))
2254
- throw new Error('exp must be a valid number');
2255
- if (typeof user.level !== 'number' || Number.isNaN(user.level))
2256
- throw new Error('level must be a valid number');
2257
- if (user.level < 1) throw new Error('level must be at least 1');
2258
- if (typeof user.totalExp !== 'number' || Number.isNaN(user.totalExp))
2259
- throw new Error('totalExp must be a valid number');
2260
- }
2261
-
2262
- /**
2263
- * Checks if the given user object is valid by verifying its numeric properties.
2264
- *
2265
- * @param {UserEditor} user - The user object to check.
2266
- * @returns {boolean} `true` if all properties (exp, level, totalExp) are valid numbers; otherwise `false`.
2267
- */
2268
- isValidUser(user) {
2269
- if (typeof user.exp !== 'number' || Number.isNaN(user.exp)) return false;
2270
- if (typeof user.level !== 'number' || Number.isNaN(user.level)) return false;
2271
- if (user.level < 1) return false;
2272
- if (typeof user.totalExp !== 'number' || Number.isNaN(user.totalExp)) return false;
2273
- return true;
2274
- }
2275
-
2276
- /**
2277
- * Returns the base experience value used for random experience generation.
2278
- * Throws an error if the internal giveExp value is not a valid number.
2279
- *
2280
- * @returns {number} The base experience value.
2281
- * @throws {Error} If giveExp is not a valid number.
2282
- */
2283
- getGiveExpBase() {
2284
- if (typeof this.giveExp !== 'number' || Number.isNaN(this.giveExp))
2285
- throw new Error('giveExp must be a valid number');
2286
- return this.giveExp;
2287
- }
2288
-
2289
- /**
2290
- * Returns the base experience required to level up.
2291
- * Throws an error if the internal expLevel value is not a valid number.
2292
- *
2293
- * @returns {number} The base experience needed per level.
2294
- * @throws {Error} If expLevel is not a valid number.
2295
- */
2296
- getExpLevelBase() {
2297
- if (typeof this.expLevel !== 'number' || Number.isNaN(this.expLevel))
2298
- throw new Error('expLevel must be a valid number');
2299
- return this.expLevel;
2300
- }
2301
-
2302
- /**
2303
- * Validates and adjusts the user's level based on their current experience.
2304
- * @param {UserEditor} user - The user object containing experience and level properties.
2305
- * @returns {UserEditor} The updated user object.
2306
- * @throws {Error} If any property (exp, level, totalExp) is not a valid number.
2307
- */
2308
- expValidator(user) {
2309
- const expLevel = this.getExpLevelBase();
2310
- this.validateUser(user);
2311
-
2312
- let extraValue = 0;
2313
- const nextLevelExp = expLevel * user.level;
2314
-
2315
- // Level Up
2316
- if (user.exp >= nextLevelExp) {
2317
- user.level++;
2318
- extraValue = user.exp - nextLevelExp;
2319
- user.exp = 0;
2320
-
2321
- if (extraValue > 0) return this.give(user, extraValue, 'extra');
2322
- }
2323
-
2324
- // Level Down
2325
- if (user.exp < 1 && user.level > 1) {
2326
- user.level--;
2327
- extraValue = Math.abs(user.exp);
2328
- user.exp = expLevel * user.level;
2329
-
2330
- if (extraValue > 0) return this.remove(user, extraValue, 'extra');
2331
- }
2332
-
2333
- return user;
2334
- }
2335
-
2336
- /**
2337
- * Calculates the total experience based on the user's level.
2338
- * @param {UserEditor} user - The user object containing experience and level properties.
2339
- * @returns {number} The total experience of the user.
2340
- * @throws {Error} If any property (exp, level, totalExp) is not a valid number.
2341
- */
2342
- getTotalExp(user) {
2343
- this.validateUser(user);
2344
- let totalExp = 0;
2345
- for (let p = 1; p <= user.level; p++) totalExp += this.getExpLevelBase() * p;
2346
- totalExp += user.exp;
2347
- return totalExp;
2348
- }
2349
-
2350
- /**
2351
- * Generates random experience points based on the configured multiplier.
2352
- * @param {number} [multi] - A multiplier for the generated experience.
2353
- * @returns {number} The generated experience points.
2354
- */
2355
- expGenerator(multi = 1) {
2356
- if (typeof multi !== 'number' || Number.isNaN(multi))
2357
- throw new Error('multi must be a valid number');
2358
- return Math.floor(Math.random() * this.getGiveExpBase()) * multi;
2359
- }
2360
-
2361
- /**
2362
- * Calculates how much experience is missing to next level.
2363
- * @param {UserEditor} user
2364
- * @returns {number}
2365
- * @throws {Error} If any property (exp, level, totalExp) is not a valid number.
2366
- */
2367
- getMissingExp(user) {
2368
- return this.getProgress(user) - user.exp;
2369
- }
2370
-
2371
- /**
2372
- * Gets the experience points required to reach the next level.
2373
- * @param {UserEditor} user - The user object containing the level.
2374
- * @returns {number} The experience required for the next level.
2375
- * @throws {Error} If any property (exp, level, totalExp) is not a valid number.
2376
- */
2377
- progress(user) {
2378
- return this.getProgress(user);
2379
- }
2380
-
2381
- /**
2382
- * Gets the experience points required to reach the next level.
2383
- * @param {UserEditor} user - The user object containing the level.
2384
- * @returns {number} The experience required for the next level.
2385
- * @throws {Error} If any property (exp, level, totalExp) is not a valid number.
2386
- */
2387
- getProgress(user) {
2388
- this.validateUser(user);
2389
- return this.getExpLevelBase() * user.level;
2390
- }
2391
-
2392
- /**
2393
- * Sets the experience value for the user, adjusting their level if necessary.
2394
- * @param {UserEditor} user - The user object.
2395
- * @param {number} value - The new experience value to set.
2396
- * @returns {UserEditor} The updated user object.
2397
- */
2398
- set(user, value) {
2399
- if (typeof value !== 'number' || Number.isNaN(value))
2400
- throw new Error('value must be a valid number');
2401
-
2402
- user.exp = value;
2403
- this.expValidator(user);
2404
- user.totalExp = this.getTotalExp(user);
2405
- return user;
2406
- }
2407
-
2408
- /**
2409
- * Adds experience to the user, adjusting their level if necessary.
2410
- * @param {UserEditor} user - The user object.
2411
- * @param {number} [extraExp] - Additional experience to be added.
2412
- * @param {'add' | 'extra'} [type] - Type of addition ('add' or 'extra').
2413
- * @param {number} [multi] - Multiplier for experience generation.
2414
- * @returns {UserEditor} The updated user object.
2415
- */
2416
- give(user, extraExp = 0, type = 'add', multi = 1) {
2417
- if (typeof multi !== 'number' || Number.isNaN(multi))
2418
- throw new Error('multi must be a valid number');
2419
- if (typeof extraExp !== 'number' || Number.isNaN(extraExp))
2420
- throw new Error('extraExp must be a valid number');
2421
- if (typeof type !== 'string') throw new Error('type must be a valid string');
2422
-
2423
- if (type === 'add') user.exp += this.expGenerator(multi) + extraExp;
2424
- else if (type === 'extra') user.exp += extraExp;
2425
-
2426
- this.expValidator(user);
2427
- user.totalExp = this.getTotalExp(user);
2428
- return user;
2429
- }
2430
-
2431
- /**
2432
- * Removes experience from the user, adjusting their level if necessary.
2433
- * @param {UserEditor} user - The user object.
2434
- * @param {number} [extraExp] - Additional experience to remove.
2435
- * @param {'add' | 'extra'} [type] - Type of removal ('add' or 'extra').
2436
- * @param {number} [multi] - Multiplier for experience generation.
2437
- * @returns {UserEditor} The updated user object.
2438
- */
2439
- remove(user, extraExp = 0, type = 'add', multi = 1) {
2440
- if (typeof multi !== 'number' || Number.isNaN(multi))
2441
- throw new Error('multi must be a valid number');
2442
- if (typeof extraExp !== 'number' || Number.isNaN(extraExp))
2443
- throw new Error('extraExp must be a valid number');
2444
- if (typeof type !== 'string') throw new Error('type must be a valid string');
2445
-
2446
- if (type === 'add') user.exp -= this.expGenerator(multi) + extraExp;
2447
- else if (type === 'extra') user.exp -= extraExp;
2448
-
2449
- this.expValidator(user);
2450
- user.totalExp = this.getTotalExp(user);
2451
- return user;
2452
- }
2453
- }
2454
-
2455
- /* harmony default export */ const userLevel = (TinyLevelUp);
2456
-
2457
- ;// ./src/legacy/libs/arraySortPositions.mjs
2458
- /**
2459
- * Generates a comparator function to sort an array of objects by a given key.
2460
- *
2461
- * @param {string} item - The object key to sort by.
2462
- * @param {boolean} [isReverse=false] - If `true`, the sorting will be in descending order.
2463
- * @returns {(a: Object<string|number, *>, b: Object<string|number, *>) => number} Comparator function compatible with Array.prototype.sort().
2464
- *
2465
- * @example
2466
- * const arr = [{ pos: 2 }, { pos: 1 }, { pos: 3 }];
2467
- * arr.sort(arraySortPositions('pos')); // Ascending: [{pos: 1}, {pos: 2}, {pos: 3}]
2468
- *
2469
- * @example
2470
- * const arr = [{ pos: 2 }, { pos: 1 }, { pos: 3 }];
2471
- * arr.sort(arraySortPositions('pos', true)); // Descending: [{pos: 3}, {pos: 2}, {pos: 1}]
2472
- */
2473
- function arraySortPositions(item, isReverse = false) {
2474
- if (!isReverse) {
2475
- return function (a, b) {
2476
- return a[item] < b[item] ? -1 : a[item] > b[item] ? 1 : 0;
2477
- };
2478
- } else {
2479
- return function (a, b) {
2480
- return a[item] > b[item] ? -1 : a[item] < b[item] ? 1 : 0;
2481
- };
2482
- }
2483
- }
2484
-
2485
- ;// ./src/v1/basics/array.mjs
2486
- // https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
2487
-
2488
- /**
2489
- * Randomly shuffles the elements of an array in place using the Fisher–Yates algorithm.
2490
- *
2491
- * This implementation ensures a uniform distribution of permutations.
2492
- * Original algorithm source: StackOverflow (link above).
2493
- *
2494
- * @param {any[]} items - The array to shuffle.
2495
- * @returns {any[]} The same array instance, now shuffled in place.
2496
- */
2497
- function shuffleArray(items) {
2498
- let currentIndex = items.length,
2499
- randomIndex;
2500
-
2501
- // While there remain elements to shuffle...
2502
- while (currentIndex !== 0) {
2503
- // Pick a remaining element...
2504
- randomIndex = Math.floor(Math.random() * currentIndex);
2505
- currentIndex--;
2506
-
2507
- // And swap it with the current element.
2508
- [items[currentIndex], items[randomIndex]] = [items[randomIndex], items[currentIndex]];
2509
- }
2510
-
2511
- return items;
2512
- }
2513
-
2514
- ;// ./src/v1/basics/clock.mjs
2515
- /**
2516
- * Calculates the time duration between the current time and a given time offset.
2517
- *
2518
- * @param {Date} timeData - The target time as a Date object.
2519
- * @param {string} [durationType='asSeconds'] - The type of duration to return. Can be 'asMilliseconds', 'asSeconds', 'asMinutes', 'asHours', 'asDays'.
2520
- * @param {Date|null} [now=null] - The current time as a Date object. Defaults to the current date and time if not provided.
2521
- * @returns {number|null} The calculated duration in the specified unit, or `null` if `timeData` is not provided.
2522
- */
2523
- function getTimeDuration(timeData = new Date(), durationType = 'asSeconds', now = null) {
2524
- if (timeData instanceof Date) {
2525
- const currentTime = now instanceof Date ? now : new Date();
2526
- /** @type {number} */
2527
- const diffMs = timeData.getTime() - currentTime.getTime();
2528
-
2529
- switch (durationType) {
2530
- case 'asMilliseconds':
2531
- return diffMs;
2532
- case 'asSeconds':
2533
- return diffMs / 1000;
2534
- case 'asMinutes':
2535
- return diffMs / (1000 * 60);
2536
- case 'asHours':
2537
- return diffMs / (1000 * 60 * 60);
2538
- case 'asDays':
2539
- return diffMs / (1000 * 60 * 60 * 24);
2540
- default:
2541
- return diffMs / 1000; // default to seconds
2542
- }
2543
- }
2544
-
2545
- return null;
2546
- }
3061
+ return null;
3062
+ }
2547
3063
 
2548
3064
  /**
2549
3065
  * Formats a custom timer string based on total seconds and a detail level.
@@ -3088,94 +3604,473 @@ extendObjType([
3088
3604
  ],
3089
3605
  ]);
3090
3606
 
3091
- ;// ./src/v1/basics/simpleMath.mjs
3607
+ ;// ./src/v1/basics/html.mjs
3608
+
3609
+
3092
3610
  /**
3093
- * Executes a Rule of Three calculation.
3094
- *
3095
- * @param {number} val1 - The first reference value (numerator in direct proportion, denominator in inverse).
3096
- * @param {number} val2 - The second reference value (denominator in direct proportion, numerator in inverse).
3097
- * @param {number} val3 - The third value (numerator in direct proportion, denominator in inverse).
3098
- * @param {boolean} [inverse] - Whether the calculation should use inverse proportion (true for inverse, false for direct).
3099
- * @returns {number} The result of the Rule of Three operation.
3100
- *
3101
- * Rule of Three Formula (Direct Proportion):
3102
- * val1 / val2 = val3 / result
3103
- *
3104
- * For Inverse Proportion:
3105
- * val1 / val3 = val2 / result
3106
- *
3107
- * Visual Representation:
3108
- *
3109
- * For Direct Proportion:
3110
- * val1 val2
3111
- * ----- = ------
3112
- * val3 result
3113
- *
3114
- * For Inverse Proportion:
3115
- * val1 val2
3116
- * ----- = ------
3117
- * val3 result
3118
- *
3119
- * @example
3120
- * // Direct proportion:
3121
- * ruleOfThree.execute(2, 6, 3, false); // → 9
3611
+ * Checks if two DOM elements are colliding on the screen.
3122
3612
  *
3123
- * @example
3124
- * // Inverse proportion:
3125
- * ruleOfThree.execute(2, 6, 3, true); // 4
3613
+ * @param {Element} elem1 - First DOM element.
3614
+ * @param {Element} elem2 - Second DOM element.
3615
+ * @returns {boolean} - Returns true if the elements are colliding.
3126
3616
  */
3127
- function ruleOfThree(val1, val2, val3, inverse = false) {
3128
- return inverse ? Number(val1 * val2) / val3 : Number(val3 * val2) / val1;
3617
+ function areHtmlElsColliding(elem1, elem2) {
3618
+ const rect1 = elem1.getBoundingClientRect();
3619
+ const rect2 = elem2.getBoundingClientRect();
3620
+
3621
+ return !(
3622
+ rect1.right < rect2.left ||
3623
+ rect1.left > rect2.right ||
3624
+ rect1.bottom < rect2.top ||
3625
+ rect1.top > rect2.bottom
3626
+ );
3129
3627
  }
3130
3628
 
3131
3629
  /**
3132
- * Calculates a percentage of a given base value.
3133
- * @param {number} price - The base value.
3134
- * @param {number} percentage - The percentage to apply.
3135
- * @returns {number} The result of the percentage calculation.
3136
- *
3137
- * @example
3138
- * getSimplePerc(200, 15); // 30
3630
+ * Reads and parses a JSON data using FileReader.
3631
+ * Throws an error if the content is not valid JSON.
3632
+ * @param {File} file
3633
+ * @returns {Promise<any>}
3139
3634
  */
3140
- function getSimplePerc(price, percentage) {
3141
- return price * (percentage / 100);
3635
+ function readJsonBlob(file) {
3636
+ return new Promise((resolve, reject) => {
3637
+ const reader = new FileReader();
3638
+
3639
+ reader.onload = () => {
3640
+ try {
3641
+ // @ts-ignore
3642
+ const result = JSON.parse(reader.result);
3643
+ resolve(result);
3644
+ } catch (error) {
3645
+ // @ts-ignore
3646
+ reject(new Error(`Invalid JSON in file: ${file.name}\n${error.message}`));
3647
+ }
3648
+ };
3649
+
3650
+ reader.onerror = () => {
3651
+ reject(new Error(`Error reading file: ${file.name}`));
3652
+ };
3653
+
3654
+ reader.readAsText(file);
3655
+ });
3142
3656
  }
3143
3657
 
3144
3658
  /**
3145
- * Calculates the age based on the given date.
3659
+ * Saves a JSON object as a downloadable file.
3660
+ * @param {string} filename
3661
+ * @param {any} data
3662
+ * @param {number} [spaces=2]
3663
+ */
3664
+ function saveJsonFile(filename, data, spaces = 2) {
3665
+ const json = JSON.stringify(data, null, spaces);
3666
+ const blob = new Blob([json], { type: 'application/json' });
3667
+ const url = URL.createObjectURL(blob);
3668
+
3669
+ const link = document.createElement('a');
3670
+ link.href = url;
3671
+ link.download = filename;
3672
+
3673
+ document.body.appendChild(link);
3674
+ link.click();
3675
+ document.body.removeChild(link);
3676
+
3677
+ URL.revokeObjectURL(url);
3678
+ }
3679
+
3680
+ /**
3681
+ * Loads and parses a JSON from a remote URL using Fetch API.
3146
3682
  *
3147
- * @param {number|string|Date} timeData - The birth date (can be a timestamp, ISO string, or Date object).
3148
- * @param {Date|null} [now=null] - The Date object representing the current date. Defaults to the current date and time if not provided.
3149
- * @returns {number|null} The age in years, or null if `timeData` is not provided or invalid.
3683
+ * @param {string} url - The full URL to fetch JSON from.
3684
+ * @param {Object} [options] - Optional settings.
3685
+ * @param {string} [options.method="GET"] - HTTP method to use (GET, POST, etc.).
3686
+ * @param {any} [options.body] - Request body (only for methods like POST, PUT).
3687
+ * @param {number} [options.timeout=0] - Timeout in milliseconds (ignored if signal is provided).
3688
+ * @param {number} [options.retries=0] - Number of retry attempts (ignored if signal is provided).
3689
+ * @param {Headers|Record<string, *>} [options.headers={}] - Additional headers.
3690
+ * @param {AbortSignal|null} [options.signal] - External AbortSignal; disables timeout and retries.
3691
+ * @returns {Promise<*>} Parsed JSON object.
3692
+ * @throws {Error} Throws if fetch fails, times out, or returns invalid JSON.
3150
3693
  */
3151
- function getAge(timeData = 0, now = null) {
3152
- if (typeof timeData !== 'undefined' && timeData !== null && timeData !== 0) {
3153
- const birthDate = new Date(timeData);
3154
- if (Number.isNaN(birthDate.getTime())) return null;
3694
+ async function fetchJson(
3695
+ url,
3696
+ { method = 'GET', body, timeout = 0, retries = 0, headers = {}, signal = null } = {},
3697
+ ) {
3698
+ if (
3699
+ typeof url !== 'string' ||
3700
+ (!url.startsWith('../') &&
3701
+ !url.startsWith('./') &&
3702
+ !url.startsWith('/') &&
3703
+ !url.startsWith('https://') &&
3704
+ !url.startsWith('http://'))
3705
+ )
3706
+ throw new Error('Invalid URL: must be a valid http or https address.');
3155
3707
 
3156
- const currentDate = now instanceof Date ? now : new Date();
3708
+ if (typeof method !== 'string' || !method.trim())
3709
+ throw new Error('Invalid method: must be a non-empty string.');
3157
3710
 
3158
- let age = currentDate.getFullYear() - birthDate.getFullYear();
3711
+ if (!signal) {
3712
+ if (
3713
+ typeof timeout !== 'number' ||
3714
+ !Number.isFinite(timeout) ||
3715
+ Number.isNaN(timeout) ||
3716
+ timeout < 0
3717
+ )
3718
+ throw new Error('Invalid timeout: must be a positive number.');
3159
3719
 
3160
- const currentMonth = currentDate.getMonth();
3161
- const birthMonth = birthDate.getMonth();
3720
+ if (
3721
+ typeof retries !== 'number' ||
3722
+ !Number.isFinite(retries) ||
3723
+ Number.isNaN(retries) ||
3724
+ retries < 0
3725
+ )
3726
+ throw new Error('Invalid retries: must be a positive number.');
3727
+ }
3162
3728
 
3163
- const currentDay = currentDate.getDate();
3164
- const birthDay = birthDate.getDate();
3729
+ const attempts = signal ? 1 : retries + 1;
3730
+ /** @type {Error|null} */
3731
+ let lastError = null;
3165
3732
 
3166
- // Adjust if birthday hasn't occurred yet this year
3167
- if (currentMonth < birthMonth || (currentMonth === birthMonth && currentDay < birthDay)) age--;
3733
+ for (let attempt = 0; attempt < attempts; attempt++) {
3734
+ const controller = signal ? null : new AbortController();
3735
+ const localSignal = signal || (controller?.signal ?? null);
3736
+ const timer =
3737
+ !signal && timeout && controller ? setTimeout(() => controller.abort(), timeout) : null;
3168
3738
 
3169
- return Math.abs(age);
3739
+ try {
3740
+ const response = await fetch(url, {
3741
+ method: method.toUpperCase(),
3742
+ headers: {
3743
+ Accept: 'application/json',
3744
+ ...headers,
3745
+ },
3746
+ body: body !== undefined ? (isJsonObject(body) ? JSON.stringify(body) : body) : undefined,
3747
+ signal: localSignal,
3748
+ });
3749
+
3750
+ if (timer) clearTimeout(timer);
3751
+
3752
+ if (!response.ok) throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
3753
+
3754
+ const contentType = response.headers.get('content-type') || '';
3755
+ if (!contentType.includes('application/json'))
3756
+ throw new Error(`Unexpected content-type: ${contentType}`);
3757
+
3758
+ const data = await response.json();
3759
+
3760
+ if (!isJsonObject(data)) throw new Error('Received invalid data instead of valid JSON.');
3761
+
3762
+ return data;
3763
+ } catch (err) {
3764
+ lastError = /** @type {Error} */ (err);
3765
+ if (signal) break; // if an external signal came, it does not retry
3766
+ if (attempt < retries)
3767
+ await new Promise((resolve) => setTimeout(resolve, 300 * (attempt + 1)));
3768
+ }
3170
3769
  }
3171
3770
 
3172
- return null;
3771
+ throw new Error(
3772
+ `Failed to fetch JSON from "${url}"${lastError ? `: ${lastError.message}` : '.'}`,
3773
+ );
3173
3774
  }
3174
3775
 
3175
3776
  /**
3176
- * @typedef {Object} FormattedByteResult
3177
- * @property {string|null} unit - The resulting unit (e.g., 'MB', 'GB') or null if input is invalid.
3178
- * @property {number|null} value - The numerical value in the chosen unit, or null if input is invalid.
3777
+ * @typedef {Object} HtmlElBoxSides
3778
+ * @property {number} x - Total horizontal size (left + right)
3779
+ * @property {number} y - Total vertical size (top + bottom)
3780
+ * @property {number} left
3781
+ * @property {number} right
3782
+ * @property {number} top
3783
+ * @property {number} bottom
3784
+ */
3785
+
3786
+ /**
3787
+ * Returns the total border width and individual sides from `border{Side}Width` CSS properties.
3788
+ *
3789
+ * @param {Element} el - The target DOM element.
3790
+ * @returns {HtmlElBoxSides} - Total horizontal (x) and vertical (y) border widths, and each side individually.
3791
+ */
3792
+ const getHtmlElBordersWidth = (el) => {
3793
+ const styles = getComputedStyle(el);
3794
+ const left = parseFloat(styles.borderLeftWidth) || 0;
3795
+ const right = parseFloat(styles.borderRightWidth) || 0;
3796
+ const top = parseFloat(styles.borderTopWidth) || 0;
3797
+ const bottom = parseFloat(styles.borderBottomWidth) || 0;
3798
+ const x = left + right;
3799
+ const y = top + bottom;
3800
+
3801
+ return { x, y, left, right, top, bottom };
3802
+ };
3803
+
3804
+ /**
3805
+ * Returns the total border size and individual sides from `border{Side}` CSS properties.
3806
+ *
3807
+ * @param {Element} el - The target DOM element.
3808
+ * @returns {HtmlElBoxSides} - Total horizontal (x) and vertical (y) border sizes, and each side individually.
3809
+ */
3810
+ const getHtmlElBorders = (el) => {
3811
+ const styles = getComputedStyle(el);
3812
+ const left = parseFloat(styles.borderLeft) || 0;
3813
+ const right = parseFloat(styles.borderRight) || 0;
3814
+ const top = parseFloat(styles.borderTop) || 0;
3815
+ const bottom = parseFloat(styles.borderBottom) || 0;
3816
+ const x = left + right;
3817
+ const y = top + bottom;
3818
+
3819
+ return { x, y, left, right, top, bottom };
3820
+ };
3821
+
3822
+ /**
3823
+ * Returns the total margin and individual sides from `margin{Side}` CSS properties.
3824
+ *
3825
+ * @param {Element} el - The target DOM element.
3826
+ * @returns {HtmlElBoxSides} - Total horizontal (x) and vertical (y) margins, and each side individually.
3827
+ */
3828
+ const getHtmlElMargin = (el) => {
3829
+ const styles = getComputedStyle(el);
3830
+ const left = parseFloat(styles.marginLeft) || 0;
3831
+ const right = parseFloat(styles.marginRight) || 0;
3832
+ const top = parseFloat(styles.marginTop) || 0;
3833
+ const bottom = parseFloat(styles.marginBottom) || 0;
3834
+
3835
+ const x = left + right;
3836
+ const y = top + bottom;
3837
+
3838
+ return { x, y, left, right, top, bottom };
3839
+ };
3840
+
3841
+ /**
3842
+ * Returns the total padding and individual sides from `padding{Side}` CSS properties.
3843
+ *
3844
+ * @param {Element} el - The target DOM element.
3845
+ * @returns {HtmlElBoxSides} - Total horizontal (x) and vertical (y) paddings, and each side individually.
3846
+ */
3847
+ const getHtmlElPadding = (el) => {
3848
+ const styles = getComputedStyle(el);
3849
+ const left = parseFloat(styles.paddingLeft) || 0;
3850
+ const right = parseFloat(styles.paddingRight) || 0;
3851
+ const top = parseFloat(styles.paddingTop) || 0;
3852
+ const bottom = parseFloat(styles.paddingBottom) || 0;
3853
+
3854
+ const x = left + right;
3855
+ const y = top + bottom;
3856
+
3857
+ return { x, y, left, right, top, bottom };
3858
+ };
3859
+
3860
+ ;// ./src/v1/basics/fullScreen.mjs
3861
+ /**
3862
+ * Checks if the document is currently in fullscreen mode.
3863
+ *
3864
+ * @returns {boolean}
3865
+ */
3866
+ const documentIsFullScreen = () =>
3867
+ !!(
3868
+ document.fullscreenElement ||
3869
+ // @ts-ignore
3870
+ document.webkitFullscreenElement ||
3871
+ // @ts-ignore
3872
+ document.mozFullScreenElement ||
3873
+ // @ts-ignore
3874
+ document.msFullscreenElement ||
3875
+ // @ts-ignore
3876
+ document.webkitIsFullScreen ||
3877
+ // @ts-ignore
3878
+ document.mozFullScreen
3879
+ );
3880
+
3881
+ /**
3882
+ * Checks if the window occupies the entire screen dimensions.
3883
+ *
3884
+ * @returns {boolean}
3885
+ */
3886
+ const isScreenFilled = () =>
3887
+ window.innerHeight === screen.height && window.innerWidth === screen.width;
3888
+
3889
+ /**
3890
+ * Checks if fullscreen mode is active either via document API or by matching screen dimensions.
3891
+ *
3892
+ * @returns {boolean}
3893
+ */
3894
+ const isFullScreenMode = () => documentIsFullScreen() || isScreenFilled();
3895
+
3896
+ /**
3897
+ * Requests fullscreen mode for the document.
3898
+ *
3899
+ * @param {FullscreenOptions} [ops]
3900
+ * @returns {Promise<void>}
3901
+ */
3902
+ const requestFullScreen = (ops) =>
3903
+ new Promise(async (resolve, reject) => {
3904
+ const docElm = document.documentElement;
3905
+ try {
3906
+ if (docElm.requestFullscreen) {
3907
+ await docElm.requestFullscreen(ops);
3908
+ // @ts-ignore
3909
+ } else if (docElm.mozRequestFullScreen) {
3910
+ // @ts-ignore
3911
+ docElm.mozRequestFullScreen(ops);
3912
+ // @ts-ignore
3913
+ } else if (docElm.webkitRequestFullScreen) {
3914
+ // @ts-ignore
3915
+ docElm.webkitRequestFullScreen(ops);
3916
+ // @ts-ignore
3917
+ } else if (docElm.msRequestFullscreen) {
3918
+ // @ts-ignore
3919
+ docElm.msRequestFullscreen(ops);
3920
+ }
3921
+ resolve();
3922
+ } catch (err) {
3923
+ reject(err);
3924
+ }
3925
+ });
3926
+
3927
+ /**
3928
+ * Exits fullscreen mode.
3929
+ *
3930
+ * @returns {Promise<void>}
3931
+ */
3932
+ const exitFullScreen = () =>
3933
+ new Promise((resolve, reject) => {
3934
+ if (document.exitFullscreen) {
3935
+ document.exitFullscreen().then(resolve).catch(reject);
3936
+ } else {
3937
+ try {
3938
+ // @ts-ignore
3939
+ if (document.mozCancelFullScreen) document.mozCancelFullScreen();
3940
+ // @ts-ignore
3941
+ else if (document.webkitCancelFullScreen) document.webkitCancelFullScreen();
3942
+ // @ts-ignore
3943
+ else if (document.msExitFullscreen) document.msExitFullscreen();
3944
+ else throw new Error('Fullscreen API is not supported');
3945
+ resolve();
3946
+ } catch (err) {
3947
+ reject(err);
3948
+ }
3949
+ }
3950
+ });
3951
+
3952
+ /** @type {readonly string[]} */
3953
+ const fullScreenEvents = [
3954
+ 'fullscreenchange',
3955
+ 'webkitfullscreenchange',
3956
+ 'mozfullscreenchange',
3957
+ 'MSFullscreenChange',
3958
+ ];
3959
+
3960
+ /**
3961
+ * Attaches a listener to fullscreen change events.
3962
+ *
3963
+ * @param {EventListenerOrEventListenerObject} listener
3964
+ * @param {boolean|AddEventListenerOptions} [ops]
3965
+ * @returns {void}
3966
+ */
3967
+ const onFullScreenChange = (listener, ops) => {
3968
+ fullScreenEvents.forEach((event) => {
3969
+ document.addEventListener(event, listener, ops);
3970
+ });
3971
+ };
3972
+
3973
+ /**
3974
+ * Removes a listener from fullscreen change events.
3975
+ *
3976
+ * @param {EventListenerOrEventListenerObject} listener
3977
+ * @param {boolean|AddEventListenerOptions} [ops]
3978
+ * @returns {void}
3979
+ */
3980
+ const offFullScreenChange = (listener, ops) => {
3981
+ fullScreenEvents.forEach((event) => {
3982
+ document.removeEventListener(event, listener, ops);
3983
+ });
3984
+ };
3985
+
3986
+ ;// ./src/v1/basics/simpleMath.mjs
3987
+ /**
3988
+ * Executes a Rule of Three calculation.
3989
+ *
3990
+ * @param {number} val1 - The first reference value (numerator in direct proportion, denominator in inverse).
3991
+ * @param {number} val2 - The second reference value (denominator in direct proportion, numerator in inverse).
3992
+ * @param {number} val3 - The third value (numerator in direct proportion, denominator in inverse).
3993
+ * @param {boolean} [inverse] - Whether the calculation should use inverse proportion (true for inverse, false for direct).
3994
+ * @returns {number} The result of the Rule of Three operation.
3995
+ *
3996
+ * Rule of Three Formula (Direct Proportion):
3997
+ * val1 / val2 = val3 / result
3998
+ *
3999
+ * For Inverse Proportion:
4000
+ * val1 / val3 = val2 / result
4001
+ *
4002
+ * Visual Representation:
4003
+ *
4004
+ * For Direct Proportion:
4005
+ * val1 val2
4006
+ * ----- = ------
4007
+ * val3 result
4008
+ *
4009
+ * For Inverse Proportion:
4010
+ * val1 val2
4011
+ * ----- = ------
4012
+ * val3 result
4013
+ *
4014
+ * @example
4015
+ * // Direct proportion:
4016
+ * ruleOfThree.execute(2, 6, 3, false); // → 9
4017
+ *
4018
+ * @example
4019
+ * // Inverse proportion:
4020
+ * ruleOfThree.execute(2, 6, 3, true); // → 4
4021
+ */
4022
+ function ruleOfThree(val1, val2, val3, inverse = false) {
4023
+ return inverse ? Number(val1 * val2) / val3 : Number(val3 * val2) / val1;
4024
+ }
4025
+
4026
+ /**
4027
+ * Calculates a percentage of a given base value.
4028
+ * @param {number} price - The base value.
4029
+ * @param {number} percentage - The percentage to apply.
4030
+ * @returns {number} The result of the percentage calculation.
4031
+ *
4032
+ * @example
4033
+ * getSimplePerc(200, 15); // 30
4034
+ */
4035
+ function getSimplePerc(price, percentage) {
4036
+ return price * (percentage / 100);
4037
+ }
4038
+
4039
+ /**
4040
+ * Calculates the age based on the given date.
4041
+ *
4042
+ * @param {number|string|Date} timeData - The birth date (can be a timestamp, ISO string, or Date object).
4043
+ * @param {Date|null} [now=null] - The Date object representing the current date. Defaults to the current date and time if not provided.
4044
+ * @returns {number|null} The age in years, or null if `timeData` is not provided or invalid.
4045
+ */
4046
+ function getAge(timeData = 0, now = null) {
4047
+ if (typeof timeData !== 'undefined' && timeData !== null && timeData !== 0) {
4048
+ const birthDate = new Date(timeData);
4049
+ if (Number.isNaN(birthDate.getTime())) return null;
4050
+
4051
+ const currentDate = now instanceof Date ? now : new Date();
4052
+
4053
+ let age = currentDate.getFullYear() - birthDate.getFullYear();
4054
+
4055
+ const currentMonth = currentDate.getMonth();
4056
+ const birthMonth = birthDate.getMonth();
4057
+
4058
+ const currentDay = currentDate.getDate();
4059
+ const birthDay = birthDate.getDate();
4060
+
4061
+ // Adjust if birthday hasn't occurred yet this year
4062
+ if (currentMonth < birthMonth || (currentMonth === birthMonth && currentDay < birthDay)) age--;
4063
+
4064
+ return Math.abs(age);
4065
+ }
4066
+
4067
+ return null;
4068
+ }
4069
+
4070
+ /**
4071
+ * @typedef {Object} FormattedByteResult
4072
+ * @property {string|null} unit - The resulting unit (e.g., 'MB', 'GB') or null if input is invalid.
4073
+ * @property {number|null} value - The numerical value in the chosen unit, or null if input is invalid.
3179
4074
  */
3180
4075
 
3181
4076
  /**
@@ -3304,1035 +4199,504 @@ function KeyPressHandler() {
3304
4199
  export default KeyPressHandler;
3305
4200
  */
3306
4201
 
3307
- ;// ./src/v1/libs/ColorSafeStringify.mjs
3308
- /**
3309
- * @typedef {Record<string, string>} ColorsList
3310
- * Represents a mapping of color keys to ANSI escape codes.
3311
- */
4202
+ // EXTERNAL MODULE: fs (ignored)
4203
+ var fs_ignored_ = __webpack_require__(41);
4204
+ // EXTERNAL MODULE: ./node_modules/path-browserify/index.js
4205
+ var path_browserify = __webpack_require__(975);
4206
+ ;// ./src/v1/basics/fileManager.mjs
3312
4207
 
3313
- class ColorSafeStringify {
3314
- /**
3315
- * Currently active color configuration.
3316
- * @type {ColorsList}
3317
- */
3318
- #colors;
3319
4208
 
3320
- /**
3321
- * Preset collections (internal and user-defined).
3322
- * @type {Record<string, ColorsList>}
3323
- * @static
3324
- */
3325
- static #PRESETS = {
3326
- default: {
3327
- reset: '\x1b[0m',
3328
- key: '\x1b[36m', // Cyan (object keys)
3329
- string: '\x1b[32m', // Green (regular strings)
3330
- string_url: '\x1b[34m', // Blue (URLs)
3331
- string_bool: '\x1b[35m', // Magenta (boolean/null in string form)
3332
- string_number: '\x1b[33m', // Yellow (numbers in string form)
3333
- number: '\x1b[33m', // Yellow (raw numbers)
3334
- boolean: '\x1b[35m', // Magenta (true/false)
3335
- null: '\x1b[1;30m', // Gray (null)
3336
- special: '\x1b[31m', // Red (e.g., [Circular], [undefined])
3337
- func: '\x1b[90m', // Dim (function string representations)
3338
- },
3339
- solarized: {
3340
- reset: '\x1b[0m',
3341
- key: '\x1b[38;5;37m',
3342
- string: '\x1b[38;5;136m',
3343
- string_url: '\x1b[38;5;33m',
3344
- string_bool: '\x1b[38;5;166m',
3345
- string_number: '\x1b[38;5;136m',
3346
- number: '\x1b[38;5;136m',
3347
- boolean: '\x1b[38;5;166m',
3348
- null: '\x1b[38;5;241m',
3349
- special: '\x1b[38;5;160m',
3350
- func: '\x1b[38;5;244m',
3351
- },
3352
- monokai: {
3353
- reset: '\x1b[0m',
3354
- key: '\x1b[38;5;81m',
3355
- string: '\x1b[38;5;114m',
3356
- string_url: '\x1b[38;5;75m',
3357
- string_bool: '\x1b[38;5;204m',
3358
- string_number: '\x1b[38;5;221m',
3359
- number: '\x1b[38;5;221m',
3360
- boolean: '\x1b[38;5;204m',
3361
- null: '\x1b[38;5;241m',
3362
- special: '\x1b[38;5;160m',
3363
- func: '\x1b[38;5;102m',
3364
- },
3365
- };
3366
4209
 
3367
- /**
3368
- * Constructs a new instance with an optional base preset or custom override.
3369
- * @param {ColorsList} [defaultColors] - Optional override for the default color scheme.
3370
- */
3371
- constructor(defaultColors = {}) {
3372
- this.#colors = { ...ColorSafeStringify.#PRESETS.default, ...defaultColors };
3373
- }
3374
4210
 
3375
- /**
3376
- * Internal method to apply ANSI color codes to different parts of a JSON string.
3377
- * @param {string} str - Raw JSON string to be colorized.
3378
- * @param {ColorsList} colors - ANSI color mapping to be applied to each JSON element type.
3379
- * @returns {string} Colorized JSON string.
3380
- */
3381
- #colorizeJSON(str, colors) {
3382
- /** @type {{ marker: string, key: string }[]} */
3383
- const keyMatches = [];
3384
-
3385
- // Colorize numeric values
3386
- str = str.replace(
3387
- /(?<!")\b(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b(?!")/g,
3388
- `${colors.number}$1${colors.reset}`,
3389
- );
4211
+ /*========================*
4212
+ * JSON Operations
4213
+ *========================*/
3390
4214
 
3391
- // Replace keys with temporary markers for later colorization
3392
- str = str.replace(/"([^"]+)":/g, (_, key) => {
3393
- const marker = `___KEY${keyMatches.length}___`;
3394
- keyMatches.push({ marker, key });
3395
- return `${marker}:`; // Keep the colon for valid syntax
3396
- });
4215
+ /**
4216
+ * Reads and parses a JSON file.
4217
+ * Throws an error if the file content is not valid JSON.
4218
+ * @param {string} filePath
4219
+ * @returns {any}
4220
+ */
4221
+ function readJsonFile(filePath) {
4222
+ if (!fs_ignored_.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
4223
+ const content = fs_ignored_.readFileSync(filePath, 'utf-8');
4224
+ return JSON.parse(content);
4225
+ }
3397
4226
 
3398
- // Replace strings and apply specific colors based on their content
3399
- str = str.replace(/"(?:\\.|[^"\\])*?"/g, (match) => {
3400
- const val = match.slice(1, -1); // Remove surrounding quotes
4227
+ /**
4228
+ * Saves an object as JSON to a file.
4229
+ * Automatically creates the directory if it does not exist.
4230
+ * @param {string} filePath
4231
+ * @param {any} data
4232
+ * @param {number} [spaces=2]
4233
+ */
4234
+ function writeJsonFile(filePath, data, spaces = 2) {
4235
+ const json = JSON.stringify(data, null, spaces);
4236
+ fs_ignored_.writeFileSync(filePath, json, 'utf-8');
4237
+ }
3401
4238
 
3402
- if (/^(https?|ftp):\/\/[^\s]+$/i.test(val)) {
3403
- return `${colors.string_url}${match}${colors.reset}`;
3404
- }
4239
+ /**
4240
+ * Reads and parses a JSON file.
4241
+ * Throws an error if the file content is not valid JSON.
4242
+ * @param {string} filePath
4243
+ * @returns {Promise<any>}
4244
+ */
4245
+ async function readJsonFileAsync(filePath) {
4246
+ if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
4247
+ const content = await fs.promises.readFile(filePath, 'utf-8');
4248
+ return JSON.parse(content);
4249
+ }
3405
4250
 
3406
- if (/^(true|false|null)$/.test(val)) {
3407
- return `${colors.string_bool}${match}${colors.reset}`;
3408
- }
4251
+ /**
4252
+ * Saves an object as JSON to a file.
4253
+ * Automatically creates the directory if it does not exist.
4254
+ * @param {string} filePath
4255
+ * @param {any} data
4256
+ * @param {number} [spaces=2]
4257
+ * @returns {Promise<void>}
4258
+ */
4259
+ function writeJsonFileAsync(filePath, data, spaces = 2) {
4260
+ const json = JSON.stringify(data, null, spaces);
4261
+ return fs.promises.writeFile(filePath, json, 'utf-8');
4262
+ }
3409
4263
 
3410
- if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(val)) {
3411
- return `${colors.string_number}${match}${colors.reset}`;
3412
- }
4264
+ /*========================*
4265
+ * Directory Management
4266
+ *========================*/
3413
4267
 
3414
- return `${colors.string}${match}${colors.reset}`;
3415
- });
4268
+ /**
4269
+ * Ensures that the directory exists, creating it recursively if needed.
4270
+ * @param {string} dirPath
4271
+ */
4272
+ function ensureDirectory(dirPath) {
4273
+ if (!fs_ignored_.existsSync(dirPath)) {
4274
+ fs_ignored_.mkdirSync(dirPath, { recursive: true });
4275
+ }
4276
+ }
3416
4277
 
3417
- // Replace markers with colorized keys
3418
- for (const { marker, key } of keyMatches) {
3419
- const regex = new RegExp(marker, 'g');
3420
- str = str.replace(regex, `${colors.key}"${key}"${colors.reset}`);
4278
+ /**
4279
+ * Clears all contents inside a directory but keeps the directory.
4280
+ * @param {string} dirPath
4281
+ */
4282
+ function clearDirectory(dirPath) {
4283
+ if (!fs_ignored_.existsSync(dirPath)) return;
4284
+ const files = fs_ignored_.readdirSync(dirPath);
4285
+ for (const file of files) {
4286
+ const fullPath = path_browserify.join(dirPath, file);
4287
+ const stat = fs_ignored_.lstatSync(fullPath);
4288
+ if (stat.isDirectory()) {
4289
+ fs_ignored_.rmSync(fullPath, { recursive: true, force: true });
4290
+ } else {
4291
+ fs_ignored_.unlinkSync(fullPath);
3421
4292
  }
4293
+ }
4294
+ }
3422
4295
 
3423
- // Colorize boolean values
3424
- str = str.replace(/(?<!")\b(true|false)\b(?!")/g, `${colors.boolean}$1${colors.reset}`);
3425
-
3426
- // Colorize null values
3427
- str = str.replace(/(?<!")\bnull\b(?!")/g, `${colors.null}null${colors.reset}`);
3428
-
3429
- // Highlight special placeholder values
3430
- str = str.replace(/\[Circular\]/g, `${colors.special}[Circular]${colors.reset}`);
3431
- str = str.replace(/\[undefined\]/g, `${colors.special}[undefined]${colors.reset}`);
4296
+ /*========================*
4297
+ * File Checks
4298
+ *========================*/
3432
4299
 
3433
- // Colorize function string representations
3434
- str = str.replace(/"function.*?[^\\]"/gs, `${colors.func}$&${colors.reset}`);
3435
- return str;
3436
- }
4300
+ /**
4301
+ * Checks whether a file exists.
4302
+ * @param {string} filePath
4303
+ * @returns {boolean}
4304
+ */
4305
+ function fileExists(filePath) {
4306
+ return fs_ignored_.existsSync(filePath) && fs_ignored_.lstatSync(filePath).isFile();
4307
+ }
3437
4308
 
3438
- /**
3439
- * Colorizes a JSON string using the active or optionally overridden color set.
3440
- * @param {string} json - The JSON string to format.
3441
- * @param {ColorsList} [customColors] - Optional temporary color override.
3442
- * @returns {string}
3443
- */
3444
- colorize(json, customColors = {}) {
3445
- const colors = { ...this.#colors, ...customColors };
3446
- return this.#colorizeJSON(json, colors);
3447
- }
4309
+ /**
4310
+ * Checks whether a directory exists.
4311
+ * @param {string} dirPath
4312
+ * @returns {boolean}
4313
+ */
4314
+ function dirExists(dirPath) {
4315
+ return fs_ignored_.existsSync(dirPath) && fs_ignored_.lstatSync(dirPath).isDirectory();
4316
+ }
3448
4317
 
3449
- /**
3450
- * Returns the currently active color scheme.
3451
- * @returns {ColorsList}
3452
- */
3453
- getColors() {
3454
- return { ...this.#colors };
3455
- }
4318
+ /**
4319
+ * Checks whether a directory is empty.
4320
+ * @param {string} dirPath
4321
+ * @returns {boolean}
4322
+ */
4323
+ function isDirEmpty(dirPath) {
4324
+ return fs_ignored_.existsSync(dirPath) && fs_ignored_.readdirSync(dirPath).length === 0;
4325
+ }
3456
4326
 
3457
- /**
3458
- * Updates the current color scheme with a partial override.
3459
- * @param {Partial<ColorsList>} newColors
3460
- */
3461
- updateColors(newColors) {
3462
- Object.assign(this.#colors, newColors);
3463
- }
4327
+ /*========================*
4328
+ * File Operations
4329
+ *========================*/
3464
4330
 
3465
- /**
3466
- * Resets the current color scheme to the default preset.
3467
- */
3468
- resetColors() {
3469
- this.#colors = { ...ColorSafeStringify.#PRESETS.default };
3470
- }
4331
+ /**
4332
+ * Copies a file to a destination.
4333
+ * @param {string} src
4334
+ * @param {string} dest
4335
+ * @param {number} [mode]
4336
+ */
4337
+ function ensureCopyFile(src, dest, mode) {
4338
+ ensureDirectory(path_browserify.dirname(dest));
4339
+ fs_ignored_.copyFileSync(src, dest, mode);
4340
+ }
3471
4341
 
3472
- /**
3473
- * Loads a color preset by name.
3474
- * @param {string} presetName - Name of the preset to load.
3475
- * @throws Will throw if the preset doesn't exist.
3476
- */
3477
- loadColorPreset(presetName) {
3478
- const preset = ColorSafeStringify.#PRESETS[presetName];
3479
- if (!preset) throw new Error(`Preset "${presetName}" not found.`);
3480
- this.#colors = { ...preset };
4342
+ /**
4343
+ * Deletes a file (If the file exists).
4344
+ * @param {string} filePath
4345
+ * @returns {boolean}
4346
+ */
4347
+ function tryDeleteFile(filePath) {
4348
+ if (fileExists(filePath)) {
4349
+ fs_ignored_.unlinkSync(filePath);
4350
+ return true;
3481
4351
  }
4352
+ return false;
4353
+ }
3482
4354
 
3483
- /**
3484
- * Saves a new custom color preset.
3485
- * @param {string} name - Name of the new preset.
3486
- * @param {ColorsList} colors - ANSI color map to save.
3487
- */
3488
- saveColorPreset(name, colors) {
3489
- ColorSafeStringify.#PRESETS[name] = { ...colors };
3490
- }
4355
+ /**
4356
+ * Copies a file to a destination.
4357
+ * @param {string} src
4358
+ * @param {string} dest
4359
+ * @param {number} [mode]
4360
+ * @returns {Promise<void>}
4361
+ */
4362
+ function ensureCopyFileAsync(src, dest, mode) {
4363
+ ensureDirectory(path.dirname(dest));
4364
+ return fs.promises.copyFile(src, dest, mode);
4365
+ }
3491
4366
 
3492
- /**
3493
- * Returns a list of all available color preset names.
3494
- * @returns {string[]}
3495
- */
3496
- getAvailablePresets() {
3497
- return Object.keys(ColorSafeStringify.#PRESETS);
4367
+ /**
4368
+ * Deletes a file (If the file exists).
4369
+ * @param {string} filePath
4370
+ * @returns {Promise<boolean>}
4371
+ */
4372
+ async function tryDeleteFileAsync(filePath) {
4373
+ if (fileExists(filePath)) {
4374
+ await fs.promises.unlink(filePath);
4375
+ return true;
3498
4376
  }
4377
+ return false;
3499
4378
  }
3500
4379
 
3501
- /* harmony default export */ const libs_ColorSafeStringify = (ColorSafeStringify);
4380
+ /*========================*
4381
+ * Text Operations
4382
+ *========================*/
3502
4383
 
3503
- ;// ./src/v1/libs/TinyPromiseQueue.mjs
3504
4384
  /**
3505
- * @typedef {Object} QueuedTask
3506
- * @property {(...args: any[]) => Promise<any>|Promise<any>} task - The async task to execute.
3507
- * @property {(value: any) => any} resolve - The resolve function from the Promise.
3508
- * @property {(reason?: any) => any} reject - The reject function from the Promise.
3509
- * @property {string|undefined} [id] - Optional identifier for the task.
3510
- * @property {string|null|undefined} [marker] - Optional marker for the task.
3511
- * @property {number|null|undefined} [delay] - Optional delay (in ms) before the task is executed.
4385
+ * Writes text to a file (Ensures that the directory exists, creating it recursively if needed).
4386
+ * @param {string} filePath
4387
+ * @param {string} content
4388
+ * @param {fs.WriteFileOptions} [ops='utf-8']
3512
4389
  */
4390
+ function writeTextFile(filePath, content, ops = 'utf-8') {
4391
+ const dir = path_browserify.dirname(filePath);
4392
+ ensureDirectory(dir);
4393
+ fs_ignored_.writeFileSync(filePath, content, ops);
4394
+ }
3513
4395
 
3514
4396
  /**
3515
- * A queue system for managing and executing asynchronous tasks sequentially, one at a time.
3516
- *
3517
- * Tasks can be delayed, reordered, canceled, and processed in strict order. The queue ensures that each task
3518
- * is executed after the previous one finishes, and any task can be skipped or canceled if needed.
3519
- *
3520
- * @class
4397
+ * Writes text to a file (Ensures that the directory exists, creating it recursively if needed).
4398
+ * @param {string} filePath
4399
+ * @param {string} content
4400
+ * @param {fs.WriteFileOptions} [ops='utf-8']
4401
+ * @returns {Promise<void>}
3521
4402
  */
3522
- class TinyPromiseQueue {
3523
- /** @type {QueuedTask[]} */
3524
- #queue = [];
3525
- #running = false;
3526
- /** @type {Record<string, ReturnType<typeof setTimeout>>} */
3527
- #timeouts = {};
3528
- /** @type {Set<string>} */
3529
- #blacklist = new Set();
4403
+ function writeTextFileAsync(filePath, content, ops = 'utf-8') {
4404
+ const dir = path.dirname(filePath);
4405
+ ensureDirectory(dir);
4406
+ return fs.promises.writeFile(filePath, content, ops);
4407
+ }
3530
4408
 
3531
- /**
3532
- * Returns whether the queue is currently processing a task.
3533
- *
3534
- * @returns {boolean}
3535
- */
3536
- isRunning() {
3537
- return this.#running;
3538
- }
3539
-
3540
- /**
3541
- * Processes the a normal task.
3542
- *
3543
- * @param {QueuedTask} data
3544
- *
3545
- * @returns {Promise<void>}
3546
- */
3547
- async #normalProcessQueue(data) {
3548
- if (
3549
- data &&
3550
- typeof data.task === 'function' &&
3551
- typeof data.resolve === 'function' &&
3552
- typeof data.reject === 'function'
3553
- ) {
3554
- const { task, resolve, reject, delay, id } = data;
3555
- try {
3556
- if (id && this.#blacklist.has(id)) {
3557
- reject(new Error('The function was canceled on TinyPromiseQueue.'));
3558
- this.#blacklist.delete(id);
3559
- this.#running = false;
3560
- this.#processQueue();
3561
- return;
3562
- }
3563
-
3564
- if (delay && id) {
3565
- await new Promise((resolveDelay) => {
3566
- const timeoutId = setTimeout(() => {
3567
- delete this.#timeouts[id];
3568
- resolveDelay(null);
3569
- }, delay);
3570
- this.#timeouts[id] = timeoutId;
3571
- });
3572
- }
3573
-
3574
- const result = await task();
3575
- resolve(result);
3576
- } catch (error) {
3577
- reject(error);
3578
- } finally {
3579
- this.#running = false;
3580
- this.#processQueue();
3581
- }
3582
- }
3583
- }
3584
-
3585
- /**
3586
- * Processes a group task.
3587
- *
3588
- * @returns {Promise<void>}
3589
- */
3590
- async #groupProcessQueue() {
3591
- /** @type {Array<QueuedTask>} */
3592
- const grouped = [];
3593
- while (this.#queue.length && this.#queue[0]?.marker === 'POINT_MARKER') {
3594
- // @ts-ignore
3595
- grouped.push(this.#queue.shift());
3596
- }
3597
-
3598
- if (grouped.length === 0) {
3599
- this.#running = false;
3600
- this.#processQueue();
3601
- return;
3602
- }
3603
-
3604
- await Promise.all(
3605
- grouped.map(
3606
- ({ task, resolve, reject, id }) =>
3607
- new Promise(async (pResolve) => {
3608
- if (id && this.#blacklist.has(id)) {
3609
- this.#blacklist.delete(id);
3610
- reject(new Error('The function was canceled on TinyPromiseQueue.'));
3611
- pResolve(true);
3612
- return;
3613
- }
3614
- await task().then(resolve).catch(reject);
3615
- pResolve(true);
3616
- }),
3617
- ),
3618
- );
3619
-
3620
- this.#running = false;
3621
- this.#processQueue();
3622
- }
3623
-
3624
- /**
3625
- * Processes the next task in the queue if not already running.
3626
- * Ensures tasks are executed in order, one at a time.
3627
- *
3628
- * @returns {Promise<void>}
3629
- */
3630
- async #processQueue() {
3631
- if (this.#running || this.#queue.length === 0) return;
3632
- this.#running = true;
3633
- if (typeof this.#queue[0]?.marker !== 'string' || this.#queue[0]?.marker !== 'POINT_MARKER') {
3634
- const data = this.#queue.shift();
3635
- // @ts-ignore
3636
- this.#normalProcessQueue(data);
3637
- } else this.#groupProcessQueue();
3638
- }
3639
-
3640
- /**
3641
- * Returns the index of a task by its ID.
3642
- *
3643
- * @param {string} id The ID of the task to locate.
3644
- * @returns {number} The index of the task in the queue, or -1 if not found.
3645
- */
3646
- getIndexById(id) {
3647
- return this.#queue.findIndex((item) => item.id === id);
3648
- }
3649
-
3650
- /**
3651
- * Returns a list of IDs for all tasks currently in the queue.
3652
- *
3653
- * @returns {{ index: number, id: string }[]} An array of task IDs currently queued.
3654
- */
3655
- getQueuedIds() {
3656
- // @ts-ignore
3657
- return this.#queue
3658
- .map((item, index) => ({ index, id: item.id }))
3659
- .filter((entry) => typeof entry.id === 'string');
3660
- }
3661
-
3662
- /**
3663
- * Reorders a task in the queue from one index to another.
3664
- *
3665
- * @param {number} fromIndex The current index of the task to move.
3666
- * @param {number} toIndex The index where the task should be placed.
3667
- */
3668
- reorderQueue(fromIndex, toIndex) {
3669
- if (
3670
- typeof fromIndex !== 'number' ||
3671
- typeof toIndex !== 'number' ||
3672
- fromIndex < 0 ||
3673
- toIndex < 0 ||
3674
- fromIndex >= this.#queue.length ||
3675
- toIndex >= this.#queue.length
3676
- )
3677
- return;
3678
- const [item] = this.#queue.splice(fromIndex, 1);
3679
- this.#queue.splice(toIndex, 0, item);
3680
- }
3681
-
3682
- /**
3683
- * Inserts a point in the queue where subsequent tasks will be grouped and executed together in a Promise.all.
3684
- * If the queue is currently empty, behaves like a regular promise.
3685
- *
3686
- * @param {(...args: any[]) => Promise<any>|Promise<any>} task A function that returns a Promise.
3687
- * @param {string} [id] Optional ID to identify the task in the queue.
3688
- * @returns {Promise<any>} A Promise that resolves or rejects with the result of the task once it's processed.
3689
- * @throws {Error} Throws if param is invalid.
3690
- */
3691
- async enqueuePoint(task, id) {
3692
- if (typeof task !== 'function')
3693
- return Promise.reject(new Error('Task must be a function returning a Promise.'));
3694
- if (typeof id !== 'undefined' && typeof id !== 'string')
3695
- throw new Error('The "id" parameter must be a string.');
3696
- if (!this.#running) return task();
3697
- return new Promise((resolve, reject) => {
3698
- this.#queue.push({ marker: 'POINT_MARKER', task, resolve, reject, id });
3699
- this.#processQueue();
3700
- });
3701
- }
3702
-
3703
- /**
3704
- * Adds a new async task to the queue and ensures it runs in order after previous tasks.
3705
- * Optionally, a delay can be added before the task is executed.
3706
- *
3707
- * If the task is canceled before execution, it will be rejected with the message:
3708
- * "The function was canceled on TinyPromiseQueue."
3709
- *
3710
- * @param {(...args: any[]) => Promise<any>|Promise<any>} task A function that returns a Promise to be executed sequentially.
3711
- * @param {number|null} [delay] Optional delay (in ms) before the task is executed.
3712
- * @param {string} [id] Optional ID to identify the task in the queue.
3713
- * @returns {Promise<any>} A Promise that resolves or rejects with the result of the task once it's processed.
3714
- * @throws {Error} Throws if param is invalid.
3715
- */
3716
- enqueue(task, delay, id) {
3717
- if (typeof task !== 'function')
3718
- return Promise.reject(new Error('Task must be a function returning a Promise.'));
3719
- if (typeof delay !== 'undefined' && (typeof delay !== 'number' || delay < 0))
3720
- return Promise.reject(new Error('Delay must be a positive number or undefined.'));
3721
- if (typeof id !== 'undefined' && typeof id !== 'string')
3722
- throw new Error('The "id" parameter must be a string.');
3723
-
3724
- return new Promise((resolve, reject) => {
3725
- this.#queue.push({ task, resolve, reject, id, delay });
3726
- this.#processQueue();
3727
- });
3728
- }
3729
-
3730
- /**
3731
- * Cancels a scheduled delay and removes the task from the queue.
3732
- * Adds the ID to a blacklist so the task is skipped if already being processed.
3733
- *
3734
- * @param {string} id The ID of the task to cancel.
3735
- * @returns {boolean} True if a delay was cancelled and the task was removed.
3736
- * @throws {Error} Throws if `id` is not a string.
3737
- */
3738
- cancelTask(id) {
3739
- if (typeof id !== 'string') throw new Error('The "id" parameter must be a string.');
3740
- let cancelled = false;
3741
-
3742
- if (id in this.#timeouts) {
3743
- clearTimeout(this.#timeouts[id]);
3744
- delete this.#timeouts[id];
3745
- cancelled = true;
3746
- }
3747
-
3748
- const index = this.getIndexById(id);
3749
- if (index !== -1) {
3750
- const [removed] = this.#queue.splice(index, 1);
3751
- removed?.reject?.(new Error('The function was canceled on TinyPromiseQueue.'));
3752
- cancelled = true;
3753
- }
3754
-
3755
- if (cancelled) this.#blacklist.add(id);
3756
-
3757
- return cancelled;
3758
- }
3759
- }
3760
-
3761
- /* harmony default export */ const libs_TinyPromiseQueue = (TinyPromiseQueue);
3762
-
3763
- ;// ./src/v1/libs/TinyRateLimiter.mjs
3764
- /** @typedef {(groupId: string) => void} OnMemoryExceeded */
3765
-
3766
- /** @typedef {(groupId: string) => void} OnGroupExpired */
4409
+ /*========================*
4410
+ * Directory Listings
4411
+ *========================*/
3767
4412
 
3768
4413
  /**
3769
- * Class representing a flexible rate limiter per user or group.
3770
- *
3771
- * This rate limiter supports limiting per user or per group by mapping
3772
- * userIds to a common groupId. All users within the same group share
3773
- * rate limits.
4414
+ * Lists all files and dirs in a directory (optionally recursive).
4415
+ * @param {string} dirPath
4416
+ * @param {boolean} [recursive=false]
4417
+ * @returns {{ files: string[]; dirs: string[] }}
3774
4418
  */
3775
- class TinyRateLimiter {
3776
- /** @type {number|null} */
3777
- #maxMemory = null;
3778
-
3779
- /** @type {NodeJS.Timeout|null} */
3780
- #cleanupTimer = null;
3781
-
3782
- /** @type {number|null|undefined} */
3783
- #maxHits = null;
3784
-
3785
- /** @type {number|null|undefined} */
3786
- #interval = null;
3787
-
3788
- /** @type {number|null|undefined} */
3789
- #cleanupInterval = null;
3790
-
3791
- /** @type {number|null|undefined} */
3792
- #maxIdle = null;
3793
-
3794
- /** @type {Map<string, number[]>} */
3795
- groupData = new Map(); // groupId -> timestamps[]
3796
-
3797
- /** @type {Map<string, number>} */
3798
- lastSeen = new Map(); // groupId -> timestamp
3799
-
3800
- /** @type {Map<string, string>} */
3801
- userToGroup = new Map(); // userId -> groupId
3802
-
3803
- /** @type {Map<string, boolean>} */
3804
- groupFlags = new Map(); // groupId -> boolean
3805
-
3806
- /**
3807
- * @type {Map<string, number>}
3808
- * Stores TTL (in ms) for each groupId individually
3809
- */
3810
- groupTTL = new Map();
3811
-
3812
- /**
3813
- * @type {null|OnMemoryExceeded}
3814
- */
3815
- #onMemoryExceeded = null;
3816
-
3817
- /**
3818
- * Set the callback to be triggered when a group exceeds its limit
3819
- * @param {OnMemoryExceeded} callback
3820
- */
3821
- setOnMemoryExceeded(callback) {
3822
- if (typeof callback !== 'function') throw new Error('onMemoryExceeded must be a function');
3823
- this.#onMemoryExceeded = callback;
3824
- }
3825
-
3826
- /**
3827
- * Clear the onMemoryExceeded callback
3828
- */
3829
- clearOnMemoryExceeded() {
3830
- this.#onMemoryExceeded = null;
3831
- }
3832
-
3833
- /**
3834
- * @type {null|OnGroupExpired}
3835
- */
3836
- #onGroupExpired = null;
3837
-
3838
- /**
3839
- * Set the callback to be triggered when a group expires and is removed.
3840
- *
3841
- * This callback is called automatically during cleanup when a group
3842
- * becomes inactive for longer than its TTL.
3843
- *
3844
- * @param {OnGroupExpired} callback - A function that receives the expired groupId.
3845
- */
3846
- setOnGroupExpired(callback) {
3847
- if (typeof callback !== 'function') throw new Error('onGroupExpired must be a function');
3848
- this.#onGroupExpired = callback;
3849
- }
3850
-
3851
- /**
3852
- * Clear the onGroupExpired callback
3853
- */
3854
- clearOnGroupExpired() {
3855
- this.#onGroupExpired = null;
3856
- }
3857
-
3858
- /**
3859
- * @param {Object} options
3860
- * @param {number|null} [options.maxMemory] - Max memory allowed
3861
- * @param {number} [options.maxHits] - Max interactions allowed
3862
- * @param {number} [options.interval] - Time window in milliseconds
3863
- * @param {number} [options.cleanupInterval] - Interval for automatic cleanup (ms)
3864
- * @param {number} [options.maxIdle=300000] - Max idle time for a user before being cleaned (ms)
3865
- */
3866
- constructor({ maxHits, interval, cleanupInterval, maxIdle = 300000, maxMemory = 100000 }) {
3867
- /** @param {number|undefined} val */
3868
- const isPositiveInteger = (val) =>
3869
- typeof val === 'number' && Number.isFinite(val) && val >= 1 && Number.isInteger(val);
3870
-
3871
- const isMaxHitsValid = isPositiveInteger(maxHits);
3872
- const isIntervalValid = isPositiveInteger(interval);
3873
- const isCleanupValid = isPositiveInteger(cleanupInterval);
3874
- const isMaxIdleValid = isPositiveInteger(maxIdle);
3875
-
3876
- if (!isMaxHitsValid && !isIntervalValid)
3877
- throw new Error("RateLimiter requires at least one valid option: 'maxHits' or 'interval'.");
3878
- if (maxHits !== undefined && !isMaxHitsValid)
3879
- throw new Error("'maxHits' must be a positive integer if defined.");
3880
- if (interval !== undefined && !isIntervalValid)
3881
- throw new Error("'interval' must be a positive integer in milliseconds if defined.");
3882
- if (cleanupInterval !== undefined && !isCleanupValid)
3883
- throw new Error("'cleanupInterval' must be a positive integer in milliseconds if defined.");
3884
- if (!isMaxIdleValid) throw new Error("'maxIdle' must be a positive integer in milliseconds.");
3885
-
3886
- if (typeof maxMemory === 'number' && Number.isFinite(maxMemory) && maxMemory > 0) {
3887
- this.#maxMemory = Math.floor(maxMemory);
3888
- } else if (maxMemory === null || maxMemory === undefined) {
3889
- this.#maxMemory = null;
3890
- } else {
3891
- throw new Error('maxMemory must be a positive number or null');
3892
- }
3893
-
3894
- this.#maxHits = isMaxHitsValid ? maxHits : null;
3895
- this.#interval = isIntervalValid ? interval : null;
3896
- this.#cleanupInterval = isCleanupValid ? cleanupInterval : null;
3897
- this.#maxIdle = maxIdle;
3898
-
3899
- // Start automatic cleanup only if cleanupInterval is valid
3900
- if (this.#cleanupInterval !== null)
3901
- this.#cleanupTimer = setInterval(() => this._cleanup(), this.#cleanupInterval);
3902
- }
3903
-
3904
- /**
3905
- * Check if a given ID is a groupId (not a userId)
3906
- * @param {string} id
3907
- * @returns {boolean}
3908
- */
3909
- isGroupId(id) {
3910
- const result = this.groupFlags.get(id);
3911
- return typeof result === 'boolean' ? result : false;
3912
- }
3913
-
3914
- /**
3915
- * Get all user IDs that belong to a given group.
3916
- * @param {string} groupId
3917
- * @returns {string[]}
3918
- */
3919
- getUsersInGroup(groupId) {
3920
- const users = [];
3921
- for (const [userId, assignedGroup] of this.userToGroup.entries()) {
3922
- if (assignedGroup === groupId) {
3923
- users.push(userId);
4419
+ function listFiles(dirPath, recursive = false) {
4420
+ /** @type {{ files: string[]; dirs: string[] }} */
4421
+ const results = { files: [], dirs: [] };
4422
+ if (!dirExists(dirPath)) return results;
4423
+
4424
+ const entries = fs_ignored_.readdirSync(dirPath);
4425
+ for (const entry of entries) {
4426
+ const fullPath = path_browserify.join(dirPath, entry);
4427
+ const stat = fs_ignored_.lstatSync(fullPath);
4428
+ if (stat.isDirectory()) {
4429
+ results.dirs.push(fullPath);
4430
+ if (recursive) {
4431
+ const results2 = listFiles(fullPath, true);
4432
+ results.files.push(...results2.files);
4433
+ results.dirs.push(...results2.dirs);
3924
4434
  }
3925
- }
3926
-
3927
- return users;
3928
- }
3929
-
3930
- /**
3931
- * Set TTL (in milliseconds) for a specific group
3932
- * @param {string} groupId
3933
- * @param {number} ttl
3934
- */
3935
- setGroupTTL(groupId, ttl) {
3936
- if (typeof ttl !== 'number' || !Number.isFinite(ttl) || ttl <= 0)
3937
- throw new Error('TTL must be a positive number in milliseconds');
3938
- this.groupTTL.set(groupId, ttl);
3939
- }
3940
-
3941
- /**
3942
- * Get TTL (in ms) for a specific group.
3943
- * @param {string} groupId
3944
- * @returns {number|null}
3945
- */
3946
- getGroupTTL(groupId) {
3947
- return this.groupTTL.get(groupId) ?? null;
3948
- }
3949
-
3950
- /**
3951
- * Delete the TTL setting for a specific group
3952
- * @param {string} groupId
3953
- */
3954
- deleteGroupTTL(groupId) {
3955
- this.groupTTL.delete(groupId);
3956
- }
3957
-
3958
- /**
3959
- * Assign a userId to a groupId, with merge if user has existing data.
3960
- * @param {string} userId
3961
- * @param {string} groupId
3962
- * @throws {Error} If userId is already assigned to a different group
3963
- */
3964
- assignToGroup(userId, groupId) {
3965
- const existingGroup = this.userToGroup.get(userId);
3966
- if (existingGroup && existingGroup !== groupId)
3967
- throw new Error(`User ${userId} is already assigned to group ${existingGroup}`);
3968
-
3969
- // If the user is already in the group, nothing needs to be done
3970
- if (existingGroup === groupId) return;
3971
- const userData = this.groupData.get(userId);
3972
-
3973
- // Associates the user to the group
3974
- if (this.isGroupId(userId)) {
3975
- for (const [uid, gId] of this.userToGroup.entries())
3976
- if (gId === userId) this.userToGroup.set(uid, groupId);
3977
- this.userToGroup.delete(userId);
3978
- } else this.userToGroup.set(userId, groupId);
3979
-
3980
- // If the user has no data, nothing needs to be done
3981
- if (!userData) return;
3982
-
3983
- const groupData = this.groupData.get(groupId);
3984
- if (groupData) {
3985
- for (const item of userData) groupData.push(item);
3986
4435
  } else {
3987
- const newData = [];
3988
- for (const item of userData) newData.push(item);
3989
- this.groupData.set(groupId, newData);
4436
+ results.files.push(fullPath);
3990
4437
  }
3991
-
3992
- this.lastSeen.set(groupId, Date.now());
3993
-
3994
- // Removes individual data as they are now in the group
3995
- this.groupFlags.delete(userId);
3996
- this.groupData.delete(userId);
3997
- this.lastSeen.delete(userId);
3998
- this.groupTTL.delete(userId);
3999
- this.groupFlags.set(groupId, true);
4000
- }
4001
-
4002
- /**
4003
- * Get the groupId for a given userId
4004
- * @param {string} userId
4005
- * @returns {string}
4006
- */
4007
- getGroupId(userId) {
4008
- return this.userToGroup.get(userId) || userId; // fallback: use userId as own group
4009
4438
  }
4439
+ return results;
4440
+ }
4010
4441
 
4011
- /**
4012
- * Register a hit for a specific user
4013
- * @param {string} userId
4014
- */
4015
- hit(userId) {
4016
- const groupId = this.getGroupId(userId);
4017
- const now = Date.now();
4018
-
4019
- if (!this.groupData.has(groupId)) {
4020
- this.groupData.set(groupId, []);
4021
- this.groupFlags.set(groupId, false);
4022
- }
4023
-
4024
- const history = this.groupData.get(groupId);
4025
- if (!history) throw new Error(`No data found for groupId: ${groupId}`);
4026
-
4027
- history.push(now);
4028
- this.lastSeen.set(groupId, now);
4029
-
4030
- // Clean up old entries
4031
- if (this.#interval !== null) {
4032
- const interval = this.getInterval();
4033
- const cutoff = now - interval;
4034
- while (history.length && history[0] < cutoff) {
4035
- history.shift();
4036
- }
4037
- }
4038
-
4039
- // Optional: keep only the last N entries for memory optimization
4040
- if (this.#maxMemory !== null && typeof this.#maxMemory === 'number') {
4041
- if (history.length > this.#maxMemory) {
4042
- history.splice(0, history.length - this.#maxMemory);
4043
- if (typeof this.#onMemoryExceeded === 'function') this.#onMemoryExceeded(groupId);
4442
+ /**
4443
+ * Lists all directories in a directory (optionally recursive).
4444
+ * @param {string} dirPath
4445
+ * @param {boolean} [recursive=false]
4446
+ * @returns {string[]}
4447
+ */
4448
+ function listDirs(dirPath, recursive = false) {
4449
+ /** @type {string[]} */
4450
+ const results = [];
4451
+ if (!dirExists(dirPath)) return results;
4452
+
4453
+ const entries = fs_ignored_.readdirSync(dirPath);
4454
+ for (const entry of entries) {
4455
+ const fullPath = path_browserify.join(dirPath, entry);
4456
+ const stat = fs_ignored_.lstatSync(fullPath);
4457
+ if (stat.isDirectory()) {
4458
+ results.push(fullPath);
4459
+ if (recursive) {
4460
+ results.push(...listDirs(fullPath, true));
4044
4461
  }
4045
4462
  }
4046
4463
  }
4464
+ return results;
4465
+ }
4047
4466
 
4048
- /**
4049
- * Check if the user (via their group) is currently rate limited
4050
- * @param {string} userId
4051
- * @returns {boolean}
4052
- */
4053
- isRateLimited(userId) {
4054
- const groupId = this.getGroupId(userId);
4055
- if (!this.groupData.has(groupId)) return false;
4056
-
4057
- const history = this.groupData.get(groupId);
4058
- if (!history) throw new Error(`No data found for groupId: ${groupId}`);
4059
-
4060
- if (this.#interval !== null) {
4061
- const now = Date.now();
4062
- const interval = this.getInterval();
4063
- const cutoff = now - interval;
4064
- let count = 0;
4065
- for (let i = history.length - 1; i >= 0; i--) {
4066
- if (history[i] > cutoff) count++;
4067
- else break;
4068
- }
4069
- if (this.#maxHits !== null) return count > this.getMaxHits();
4070
- return count > 0;
4071
- }
4467
+ /*========================*
4468
+ * File Info
4469
+ *========================*/
4072
4470
 
4073
- if (this.#maxHits !== null) {
4074
- return history.length > this.getMaxHits();
4075
- }
4471
+ /**
4472
+ * Returns the size of a file in bytes.
4473
+ * @param {string} filePath
4474
+ * @returns {number}
4475
+ */
4476
+ function fileSize(filePath) {
4477
+ if (!fileExists(filePath)) return 0;
4478
+ const stats = fs_ignored_.statSync(filePath);
4479
+ return stats.size;
4480
+ }
4076
4481
 
4077
- return false;
4482
+ /**
4483
+ * Returns the total size of a directory in bytes.
4484
+ * @param {string} dirPath
4485
+ * @returns {number}
4486
+ */
4487
+ function dirSize(dirPath) {
4488
+ let total = 0;
4489
+ const files = listFiles(dirPath, true).files;
4490
+ for (const file of files) {
4491
+ total += fileSize(file);
4078
4492
  }
4493
+ return total;
4494
+ }
4079
4495
 
4080
- /**
4081
- * Manually reset group data
4082
- * @param {string} groupId
4083
- */
4084
- resetGroup(groupId) {
4085
- this.groupFlags.delete(groupId);
4086
- this.groupData.delete(groupId);
4087
- this.lastSeen.delete(groupId);
4088
- this.groupTTL.delete(groupId);
4089
- }
4496
+ /*========================*
4497
+ * Backup Utilities
4498
+ *========================*/
4090
4499
 
4091
- /**
4092
- * Manually reset user data.
4093
- *
4094
- * @deprecated Use `resetUserGroup(userId)` instead. This method will be removed in future versions.
4095
- * @param {string} userId
4096
- * @returns {void}
4097
- */
4098
- reset(userId) {
4099
- if (false)
4100
- {}
4101
- return this.resetUserGroup(userId);
4102
- }
4103
-
4104
- /**
4105
- * Manually reset a user mapping
4106
- * @param {string} userId
4107
- */
4108
- resetUserGroup(userId) {
4109
- this.userToGroup.delete(userId);
4110
- }
4500
+ /**
4501
+ * Creates a backup copy of a file with .bak timestamp suffix.
4502
+ * @param {string} filePath
4503
+ * @param {string} [ext='bak']
4504
+ */
4505
+ function backupFile(filePath, ext = 'bak') {
4506
+ if (!fileExists(filePath)) return;
4507
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
4508
+ const backupPath = `${filePath}.${ext}.${timestamp}`;
4509
+ ensureCopyFile(filePath, backupPath);
4510
+ }
4111
4511
 
4112
- /**
4113
- * Set custom timestamps to a group
4114
- * @param {string} groupId
4115
- * @param {number[]} timestamps
4116
- */
4117
- setData(groupId, timestamps) {
4118
- if (!Array.isArray(timestamps)) throw new Error('timestamps must be an array of numbers.');
4119
- for (const t of timestamps) {
4120
- if (typeof t !== 'number' || !Number.isFinite(t)) {
4121
- throw new Error('All timestamps must be finite numbers.');
4122
- }
4123
- }
4124
- if (!this.groupData.has(groupId)) this.groupFlags.set(groupId, false);
4125
- this.groupData.set(groupId, timestamps);
4126
- this.lastSeen.set(groupId, Date.now());
4127
- }
4512
+ /**
4513
+ * Returns the most recent backup file path for a given file.
4514
+ * @param {string} filePath
4515
+ * @param {string} [ext='bak']
4516
+ * @returns {string} Full path to the most recent backup
4517
+ */
4518
+ function getLatestBackupPath(filePath, ext = 'bak') {
4519
+ const dir = path_browserify.dirname(filePath);
4520
+ const baseName = path_browserify.basename(filePath);
4521
+ const backups = fs_ignored_.readdirSync(dir)
4522
+ .filter((name) => name.startsWith(`${baseName}.${ext}.`))
4523
+ .sort()
4524
+ .reverse();
4128
4525
 
4129
- /**
4130
- * Check if a group has data
4131
- * @param {string} groupId
4132
- * @returns {boolean}
4133
- */
4134
- hasData(groupId) {
4135
- return this.groupData.has(groupId);
4136
- }
4526
+ if (backups.length === 0) throw new Error(`No backups found for ${filePath}`);
4137
4527
 
4138
- /**
4139
- * Get timestamps from a group
4140
- * @param {string} groupId
4141
- * @returns {number[]}
4142
- */
4143
- getData(groupId) {
4144
- return this.groupData.get(groupId) || [];
4145
- }
4528
+ return path_browserify.join(dir, backups[0]);
4529
+ }
4146
4530
 
4147
- /**
4148
- * Get the maximum idle time (in milliseconds) before a group is considered expired.
4149
- * @returns {number}
4150
- */
4151
- getMaxIdle() {
4152
- if (typeof this.#maxIdle !== 'number' || !Number.isFinite(this.#maxIdle) || this.#maxIdle < 0) {
4153
- throw new Error("'maxIdle' must be a non-negative finite number.");
4154
- }
4155
- return this.#maxIdle;
4156
- }
4531
+ /**
4532
+ * Restores the most recent backup of a file.
4533
+ * @param {string} filePath
4534
+ * @param {string} [ext='bak']
4535
+ */
4536
+ function restoreLatestBackup(filePath, ext = 'bak') {
4537
+ const latestBackup = getLatestBackupPath(filePath, ext);
4538
+ ensureCopyFile(latestBackup, filePath);
4539
+ }
4157
4540
 
4158
- /**
4159
- * Set the maximum idle time (in milliseconds) before a group is considered expired.
4160
- * @param {number} ms
4161
- */
4162
- setMaxIdle(ms) {
4163
- if (typeof ms !== 'number' || !Number.isFinite(ms) || ms < 0) {
4164
- throw new Error("'maxIdle' must be a non-negative finite number.");
4165
- }
4166
- this.#maxIdle = ms;
4167
- }
4541
+ /**
4542
+ * Restores the most recent backup of a file.
4543
+ * @param {string} filePath
4544
+ * @param {string} [ext='bak']
4545
+ * @returns {Promise<void>}
4546
+ */
4547
+ function restoreLatestBackupAsync(filePath, ext = 'bak') {
4548
+ const latestBackup = getLatestBackupPath(filePath, ext);
4549
+ return ensureCopyFileAsync(latestBackup, filePath);
4550
+ }
4168
4551
 
4169
- /**
4170
- * Cleanup old/inactive groups with individual TTLs
4171
- * @private
4172
- */
4173
- _cleanup() {
4174
- const now = Date.now();
4175
- for (const [groupId, last] of this.lastSeen.entries()) {
4176
- const ttl = this.getGroupTTL(groupId) ?? this.getMaxIdle();
4177
- if (now - last > ttl) {
4178
- this.groupFlags.delete(groupId);
4179
- this.groupData.delete(groupId);
4180
- this.lastSeen.delete(groupId);
4181
- this.groupTTL.delete(groupId);
4182
-
4183
- // Notify subclass or external binding
4184
- if (typeof this.#onGroupExpired === 'function') {
4185
- this.#onGroupExpired(groupId);
4186
- }
4187
- }
4188
- }
4189
- }
4552
+ /**
4553
+ * Creates a backup copy of a file with .bak timestamp suffix.
4554
+ * @param {string} filePath
4555
+ * @param {string} [ext='bak']
4556
+ * @returns {Promise<void>}
4557
+ */
4558
+ async function backupFileAsync(filePath, ext = 'bak') {
4559
+ if (!fileExists(filePath)) return;
4560
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
4561
+ const backupPath = `${filePath}.${ext}.${timestamp}`;
4562
+ return ensureCopyFileAsync(filePath, backupPath);
4563
+ }
4190
4564
 
4191
- /**
4192
- * Get list of active group IDs
4193
- * @returns {string[]}
4194
- */
4195
- getActiveGroups() {
4196
- return Array.from(this.groupData.keys());
4197
- }
4565
+ /*========================*
4566
+ * Rename Utilities
4567
+ *========================*/
4198
4568
 
4199
- /**
4200
- * Get a shallow copy of all user-to-group mappings as a plain object
4201
- * @returns {Record<string, string>}
4202
- */
4203
- getAllUserMappings() {
4204
- return Object.fromEntries(this.userToGroup);
4569
+ /**
4570
+ * Renames multiple files in a directory using a rename function.
4571
+ * @param {string} dirPath - The target directory.
4572
+ * @param {(original: string, index: number) => string} renameFn - Function that returns new filename.
4573
+ * @param {string[]} [extensions] - Optional: Only rename files with these extensions.
4574
+ *
4575
+ * @throws {TypeError} If any argument has an invalid type.
4576
+ * @throws {Error} If the directory does not exist or contains invalid files.
4577
+ */
4578
+ function renameFileBatch(dirPath, renameFn, extensions = []) {
4579
+ // Validate types
4580
+ if (typeof dirPath !== 'string') throw new TypeError('dirPath must be a string');
4581
+ if (typeof renameFn !== 'function') throw new TypeError('renameFn must be a function');
4582
+ if (!Array.isArray(extensions)) throw new TypeError('extensions must be an array of strings');
4583
+ if (!fs_ignored_.existsSync(dirPath) || !fs_ignored_.statSync(dirPath).isDirectory())
4584
+ throw new Error(`Directory not found or invalid: ${dirPath}`);
4585
+ for (const ext of extensions) {
4586
+ if (typeof ext !== 'string' || !ext.startsWith('.'))
4587
+ throw new TypeError(`Invalid extension: ${ext}`);
4205
4588
  }
4206
4589
 
4207
- /**
4208
- * Get the interval window in milliseconds.
4209
- * @returns {number}
4210
- */
4211
- getInterval() {
4212
- if (typeof this.#interval !== 'number' || !Number.isFinite(this.#interval)) {
4213
- throw new Error("'interval' is not a valid finite number.");
4214
- }
4215
- return this.#interval;
4216
- }
4590
+ const files = listFiles(dirPath).files;
4591
+ let index = 0;
4217
4592
 
4218
- /**
4219
- * Get the maximum number of allowed hits.
4220
- * @returns {number}
4221
- */
4222
- getMaxHits() {
4223
- if (typeof this.#maxHits !== 'number' || !Number.isFinite(this.#maxHits)) {
4224
- throw new Error("'maxHits' is not a valid finite number.");
4225
- }
4226
- return this.#maxHits;
4227
- }
4593
+ for (const file of files) {
4594
+ const ext = path_browserify.extname(file);
4595
+ if (extensions.length && !extensions.includes(ext)) continue;
4228
4596
 
4229
- /**
4230
- * Get the total number of hits recorded for a group.
4231
- * @param {string} groupId
4232
- * @returns {number}
4233
- */
4234
- getTotalHits(groupId) {
4235
- const history = this.groupData.get(groupId);
4236
- return Array.isArray(history) ? history.length : 0;
4237
- }
4597
+ const originalName = path_browserify.basename(file);
4598
+ const newName = renameFn(originalName, index++);
4599
+ const newPath = path_browserify.join(dirPath, newName);
4238
4600
 
4239
- /**
4240
- * Get the timestamp of the last hit for a group.
4241
- * @param {string} groupId
4242
- * @returns {number|null}
4243
- */
4244
- getLastHit(groupId) {
4245
- const history = this.groupData.get(groupId);
4246
- return history?.length ? history[history.length - 1] : null;
4247
- }
4601
+ if (originalName === newName) continue;
4248
4602
 
4249
- /**
4250
- * Get milliseconds since the last hit for a group.
4251
- * @param {string} groupId
4252
- * @returns {number|null}
4253
- */
4254
- getTimeSinceLastHit(groupId) {
4255
- const last = this.getLastHit(groupId);
4256
- return last !== null ? Date.now() - last : null;
4603
+ fs_ignored_.renameSync(file, newPath);
4257
4604
  }
4605
+ }
4258
4606
 
4259
- /**
4260
- * Internal utility to compute average spacing
4261
- * @private
4262
- * @param {number[]|undefined} history
4263
- * @returns {number|null}
4264
- */
4265
- _calculateAverageSpacing(history) {
4266
- if (!Array.isArray(history) || history.length < 2) return null;
4267
- let total = 0;
4268
- for (let i = 1; i < history.length; i++) {
4269
- total += history[i] - history[i - 1];
4270
- }
4271
- return total / (history.length - 1);
4272
- }
4607
+ /**
4608
+ * Renames files using regex replacement.
4609
+ * @param {string} dirPath
4610
+ * @param {RegExp} pattern - Regex to match in the filename.
4611
+ * @param {string} replacement - Replacement string.
4612
+ * @param {string[]} [extensions]
4613
+ */
4614
+ function renameFileRegex(dirPath, pattern, replacement, extensions = []) {
4615
+ renameFileBatch(
4616
+ dirPath,
4617
+ (filename) => {
4618
+ const ext = path_browserify.extname(filename);
4619
+ const name = path_browserify.basename(filename, ext).replace(pattern, replacement);
4620
+ return `${name}${ext}`;
4621
+ },
4622
+ extensions,
4623
+ );
4624
+ }
4273
4625
 
4274
- /**
4275
- * Get average time between hits for a group (ms).
4276
- * @param {string} groupId
4277
- * @returns {number|null}
4278
- */
4279
- getAverageHitSpacing(groupId) {
4280
- return this._calculateAverageSpacing(this.groupData.get(groupId));
4281
- }
4626
+ /**
4627
+ * Adds a prefix or suffix to filenames.
4628
+ * @param {string} dirPath
4629
+ * @param {{ prefix?: string, suffix?: string }} options
4630
+ * @param {string[]} [extensions]
4631
+ */
4632
+ function renameFileAddPrefixSuffix(dirPath, { prefix = '', suffix = '' }, extensions = []) {
4633
+ renameFileBatch(
4634
+ dirPath,
4635
+ (filename) => {
4636
+ const ext = path_browserify.extname(filename);
4637
+ const name = path_browserify.basename(filename, ext);
4638
+ return `${prefix}${name}${suffix}${ext}`;
4639
+ },
4640
+ extensions,
4641
+ );
4642
+ }
4282
4643
 
4283
- /**
4284
- * Get metrics about a group's activity.
4285
- * @param {string} groupId
4286
- * @returns {{
4287
- * totalHits: number,
4288
- * lastHit: number|null,
4289
- * timeSinceLastHit: number|null,
4290
- * averageHitSpacing: number|null
4291
- * }}
4292
- */
4293
- getMetrics(groupId) {
4294
- const history = this.groupData.get(groupId);
4295
-
4296
- if (!Array.isArray(history) || history.length === 0) {
4297
- return {
4298
- totalHits: 0,
4299
- lastHit: null,
4300
- timeSinceLastHit: null,
4301
- averageHitSpacing: null,
4644
+ /**
4645
+ * Normalizes all filenames to lowercase (or uppercase).
4646
+ * @param {string} dirPath
4647
+ * @param {'lower' | 'upper' | 'title'} mode
4648
+ * @param {string[]} [extensions]
4649
+ * @param {boolean} [normalizeExt=false] - Whether to apply case change to file extensions too.
4650
+ * @throws {Error} If mode is invalid.
4651
+ */
4652
+ function renameFileNormalizeCase(
4653
+ dirPath,
4654
+ mode = 'lower',
4655
+ extensions = [],
4656
+ normalizeExt = false,
4657
+ ) {
4658
+ if (typeof mode !== 'string' || !['lower', 'upper', 'title'].includes(mode))
4659
+ throw new Error(`Invalid mode "${mode}". Must be 'lower', 'upper' or 'title'.`);
4660
+ renameFileBatch(
4661
+ dirPath,
4662
+ (filename) => {
4663
+ /**
4664
+ * @param {string} text
4665
+ * @returns {string}
4666
+ */
4667
+ const changeToMode = (text) => {
4668
+ if (mode === 'lower') return text.toLowerCase();
4669
+ else if (mode === 'upper') return text.toUpperCase();
4670
+ else if (mode === 'title') return toTitleCase(text);
4671
+ else return text;
4302
4672
  };
4303
- }
4304
4673
 
4305
- const totalHits = history.length;
4306
- const lastHit = history[totalHits - 1];
4307
- const timeSinceLastHit = Date.now() - lastHit;
4308
- const averageHitSpacing = this._calculateAverageSpacing(history);
4309
-
4310
- return {
4311
- totalHits,
4312
- lastHit,
4313
- timeSinceLastHit,
4314
- averageHitSpacing,
4315
- };
4316
- }
4317
-
4318
- /**
4319
- * Destroy the rate limiter, stopping cleanup and clearing data
4320
- */
4321
- destroy() {
4322
- if (this.#cleanupTimer) clearInterval(this.#cleanupTimer);
4323
- this._cleanup();
4324
- this.groupData.clear();
4325
- this.lastSeen.clear();
4326
- this.userToGroup.clear();
4327
- this.groupTTL.clear();
4328
- this.groupFlags.clear();
4329
- }
4674
+ const rawExt = path_browserify.extname(filename);
4675
+ const ext = normalizeExt ? changeToMode(rawExt) : rawExt;
4676
+ const name = changeToMode(path_browserify.basename(filename, rawExt));
4677
+ return `${name}${ext}`;
4678
+ },
4679
+ extensions,
4680
+ );
4330
4681
  }
4331
4682
 
4332
- /* harmony default export */ const libs_TinyRateLimiter = (TinyRateLimiter);
4333
-
4334
- ;// ./src/v1/index.mjs
4683
+ /**
4684
+ * Pads numbers in filenames (e.g., "img1.jpg" -> "img001.jpg").
4685
+ * @param {string} dirPath
4686
+ * @param {number} totalDigits
4687
+ * @param {string[]} [extensions]
4688
+ */
4689
+ function renameFilePadNumbers(dirPath, totalDigits = 3, extensions = []) {
4690
+ renameFileBatch(
4691
+ dirPath,
4692
+ (filename) => {
4693
+ return filename.replace(/\d+/, (match) => match.padStart(totalDigits, '0'));
4694
+ },
4695
+ extensions,
4696
+ );
4697
+ }
4335
4698
 
4699
+ ;// ./src/v1/basics/index.mjs
4336
4700
 
4337
4701
 
4338
4702
 
@@ -4348,6 +4712,6 @@ class TinyRateLimiter {
4348
4712
 
4349
4713
  })();
4350
4714
 
4351
- window.TinyEssentials = __webpack_exports__;
4715
+ window.TinyBasicsEs = __webpack_exports__;
4352
4716
  /******/ })()
4353
4717
  ;