@sv443-network/userutils 9.1.0 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @sv443-network/userutils
2
2
 
3
+ ## 9.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 905fea4: Added function `isDomLoaded()` to check if the DOM is queryable, regardless of `@run-at` setting
8
+ - 4264154: Added parameter `withDecimals` to `digitCount()` (true by default)
9
+ - 4264154: Added function `roundFixed()` to round a floating-point number to the given amount of decimals. Can also round to the given power of 10.
10
+ - 7e492cf: Added `probeElementStyle()` to probe the computed style of a temporary element, allowing to resolve CSS variables and default style values, etc.
11
+ - 905fea4: Added function `onDomLoad()` to call a callback and/or resolve a Promise when the DOM is loaded, even retroactively
12
+ - 4264154: Added function `bitSetHas()` to check if a given value is present in a [bitset](https://www.geeksforgeeks.org/cpp-bitset-and-its-application/)
13
+ - bf55335: Replaced a bunch of generic `Error`s with the new custom error class instances
14
+ - bf55335: Added custom error classes `ChecksumMismatchError`, `DataMigrationError` and `PlatformError`, extending from the base class `UUError`
15
+ The base class has the additional property `date` which is the time of the error creation
16
+
17
+ ### Patch Changes
18
+
19
+ - 157dacb: Fixed example code in tsdoc comments of translation functions
20
+ - 4264154: Fixed `digitCount()` not counting decimals by default
21
+
3
22
  ## 9.1.0
4
23
 
5
24
  ### Minor Changes
package/README-summary.md CHANGED
@@ -36,6 +36,8 @@ View the documentation of previous major releases:
36
36
  - **DOM:**
37
37
  - [`SelectorObserver`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#selectorobserver) - class that manages listeners that are called when selectors are found in the DOM
38
38
  - [`getUnsafeWindow()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#getunsafewindow) - get the unsafeWindow object or fall back to the regular window object
39
+ - [`isDomLoaded()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#isdomloaded) - check if the DOM has finished loading and can be queried and modified
40
+ - [`onDomLoad()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#ondomload) - run a function or pause async execution until the DOM has finished loading (or immediately if DOM is already loaded)
39
41
  - [`addParent()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#addparent) - add a parent element around another element
40
42
  - [`addGlobalStyle()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#addglobalstyle) - add a global style to the page
41
43
  - [`preloadImages()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#preloadimages) - preload images into the browser cache for faster loading later on
@@ -46,11 +48,14 @@ View the documentation of previous major releases:
46
48
  - [`observeElementProp()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#observeelementprop) - observe changes to an element's property that can't be observed with MutationObserver
47
49
  - [`getSiblingsFrame()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#getsiblingsframe) - returns a frame of an element's siblings, with a given alignment and size
48
50
  - [`setInnerHtmlUnsafe()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#setinnerhtmlunsafe) - set the innerHTML of an element using a [Trusted Types policy](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API) without sanitizing or escaping it
51
+ - [`probeElementStyle()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#probeelementstyle) - probe the computed style of a temporary element (get default font size, resolve CSS variables, etc.)
49
52
  - **Math:**
50
53
  - [`clamp()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#clamp) - constrain a number between a min and max value
51
54
  - [`mapRange()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#maprange) - map a number from one range to the same spot in another range
52
55
  - [`randRange()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#randrange) - generate a random number between a min and max boundary
53
56
  - [`digitCount()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#digitcount) - calculate the amount of digits in a number
57
+ - [`roundFixed()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#roundfixed) - round a floating-point number at the given amount of decimals, or to the given power of 10
58
+ - [`bitSetHas()`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#bitsethas) - check if a bit is set in a [bitset](https://www.geeksforgeeks.org/cpp-bitset-and-its-application/)
54
59
  - **Misc:**
55
60
  - [`DataStore`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#datastore) - class that manages a hybrid sync & async persistent JSON database, including data migration
56
61
  - [`DataStoreSerializer`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#datastoreserializer) - class for importing & exporting data of multiple DataStore instances, including compression, checksumming and running migrations
@@ -101,6 +106,11 @@ View the documentation of previous major releases:
101
106
  - [`ValueGen`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#valuegen) - a "generator" value that allows for super flexible value typing and declaration
102
107
  - [`StringGen`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#stringgen) - a "generator" string that allows for super flexible string typing and declaration, including enhanced support for unions
103
108
  - [`ListWithLength`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#listwithlength) - represents an array or object with a numeric `length`, `count` or `size` property
109
+ - **Custom Error classes:**
110
+ - [`UUError`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#uuerror) - base class for all custom UserUtils errors - has a custom `date` prop set to the time of creation
111
+ - [`ChecksumMismatchError`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#checksummismatcherror) - thrown when a string of data doesn't match its checksum
112
+ - [`DataMigrationError`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#datamigrationerror) - thrown when a data migration fails
113
+ - [`PlatformError`](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#platformerror) - thrown when a function is called in an unsupported environment
104
114
 
105
115
  <br><br>
106
116
 
package/README.md CHANGED
@@ -39,6 +39,8 @@ View the documentation of previous major releases:
39
39
  - [**DOM:**](./docs.md#dom)
40
40
  - [`SelectorObserver`](./docs.md#selectorobserver) - class that manages listeners that are called when selectors are found in the DOM
41
41
  - [`getUnsafeWindow()`](./docs.md#getunsafewindow) - get the unsafeWindow object or fall back to the regular window object
42
+ - [`isDomLoaded()`](./docs.md#isdomloaded) - check if the DOM has finished loading and can be queried and modified
43
+ - [`onDomLoad()`](./docs.md#ondomload) - run a function or pause async execution until the DOM has finished loading (or immediately if DOM is already loaded)
42
44
  - [`addParent()`](./docs.md#addparent) - add a parent element around another element
43
45
  - [`addGlobalStyle()`](./docs.md#addglobalstyle) - add a global style to the page
44
46
  - [`preloadImages()`](./docs.md#preloadimages) - preload images into the browser cache for faster loading later on
@@ -49,11 +51,14 @@ View the documentation of previous major releases:
49
51
  - [`observeElementProp()`](./docs.md#observeelementprop) - observe changes to an element's property that can't be observed with MutationObserver
50
52
  - [`getSiblingsFrame()`](./docs.md#getsiblingsframe) - returns a frame of an element's siblings, with a given alignment and size
51
53
  - [`setInnerHtmlUnsafe()`](./docs.md#setinnerhtmlunsafe) - set the innerHTML of an element using a [Trusted Types policy](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API) without sanitizing or escaping it
54
+ - [`probeElementStyle()`](./docs.md#probeelementstyle) - probe the computed style of a temporary element (get default font size, resolve CSS variables, etc.)
52
55
  - [**Math:**](./docs.md#math)
53
56
  - [`clamp()`](./docs.md#clamp) - constrain a number between a min and max value
54
57
  - [`mapRange()`](./docs.md#maprange) - map a number from one range to the same spot in another range
55
58
  - [`randRange()`](./docs.md#randrange) - generate a random number between a min and max boundary
56
59
  - [`digitCount()`](./docs.md#digitcount) - calculate the amount of digits in a number
60
+ - [`roundFixed()`](./docs.md#roundfixed) - round a floating-point number at the given amount of decimals, or to the given power of 10
61
+ - [`bitSetHas()`](./docs.md#bitsethas) - check if a bit is set in a [bitset](https://www.geeksforgeeks.org/cpp-bitset-and-its-application/)
57
62
  - [**Misc:**](./docs.md#misc)
58
63
  - [`DataStore`](./docs.md#datastore) - class that manages a hybrid sync & async persistent JSON database, including data migration
59
64
  - [`DataStoreSerializer`](./docs.md#datastoreserializer) - class for importing & exporting data of multiple DataStore instances, including compression, checksumming and running migrations
@@ -104,6 +109,11 @@ View the documentation of previous major releases:
104
109
  - [`ValueGen`](./docs.md#valuegen) - a "generator" value that allows for super flexible value typing and declaration
105
110
  - [`StringGen`](./docs.md#stringgen) - a "generator" string that allows for super flexible string typing and declaration, including enhanced support for unions
106
111
  - [`ListWithLength`](./docs.md#listwithlength) - represents an array or object with a numeric `length`, `count` or `size` property
112
+ - [**Custom Error classes**](./docs.md#error-classes)
113
+ - [`UUError`](./docs.md#uuerror) - base class for all custom UserUtils errors - has a custom `date` prop set to the time of creation
114
+ - [`ChecksumMismatchError`](./docs.md#checksummismatcherror) - thrown when a string of data doesn't match its checksum
115
+ - [`DataMigrationError`](./docs.md#datamigrationerror) - thrown when a data migration fails
116
+ - [`PlatformError`](./docs.md#platformerror) - thrown when a function is called in an unsupported environment
107
117
 
108
118
  <br><br>
109
119
 
package/dist/index.cjs CHANGED
@@ -99,13 +99,21 @@ function randRange(...args) {
99
99
  } else
100
100
  return Math.floor(Math.random() * (max - min + 1)) + min;
101
101
  }
102
- function digitCount(num) {
102
+ function digitCount(num, withDecimals = true) {
103
103
  num = Number(!["string", "number"].includes(typeof num) ? String(num) : num);
104
104
  if (typeof num === "number" && isNaN(num))
105
105
  return NaN;
106
- return num === 0 ? 1 : Math.floor(
107
- Math.log10(Math.abs(Number(num))) + 1
108
- );
106
+ const [intPart, decPart] = num.toString().split(".");
107
+ const intDigits = intPart === "0" ? 1 : Math.floor(Math.log10(Math.abs(Number(intPart))) + 1);
108
+ const decDigits = withDecimals && decPart ? decPart.length : 0;
109
+ return intDigits + decDigits;
110
+ }
111
+ function roundFixed(num, fractionDigits) {
112
+ const scale = 10 ** fractionDigits;
113
+ return Math.round(num * scale) / scale;
114
+ }
115
+ function bitSetHas(bitSet, checkVal) {
116
+ return (bitSet & checkVal) === checkVal;
109
117
  }
110
118
 
111
119
  // lib/array.ts
@@ -130,7 +138,7 @@ function randomizeArray(array) {
130
138
  if (array.length === 0)
131
139
  return retArray;
132
140
  for (let i = retArray.length - 1; i > 0; i--) {
133
- const j = Math.floor(randRange(0, 1e4) / 1e4 * (i + 1));
141
+ const j = Math.floor(Math.random() * (i + 1));
134
142
  [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
135
143
  }
136
144
  return retArray;
@@ -173,10 +181,10 @@ function darkenColor(color, percent, upperCase = false) {
173
181
  else if (color.startsWith("rgb")) {
174
182
  const rgbValues = (_a = color.match(/\d+(\.\d+)?/g)) == null ? undefined : _a.map(Number);
175
183
  if (!rgbValues)
176
- throw new Error("Invalid RGB/RGBA color format");
184
+ throw new TypeError("Invalid RGB/RGBA color format");
177
185
  [r, g, b, a] = rgbValues;
178
186
  } else
179
- throw new Error("Unsupported color format");
187
+ throw new TypeError("Unsupported color format");
180
188
  [r, g, b] = darkenRgb(r, g, b, percent);
181
189
  if (isHexCol)
182
190
  return rgbToHex(r, g, b, a, color.startsWith("#"), upperCase);
@@ -185,10 +193,39 @@ function darkenColor(color, percent, upperCase = false) {
185
193
  else if (color.startsWith("rgb"))
186
194
  return `rgb(${r}, ${g}, ${b})`;
187
195
  else
188
- throw new Error("Unsupported color format");
196
+ throw new TypeError("Unsupported color format");
189
197
  }
190
198
 
199
+ // lib/errors.ts
200
+ var UUError = class extends Error {
201
+ constructor(message, options) {
202
+ super(message, options);
203
+ __publicField(this, "date");
204
+ this.date = /* @__PURE__ */ new Date();
205
+ }
206
+ };
207
+ var ChecksumMismatchError = class extends UUError {
208
+ constructor(message, options) {
209
+ super(message, options);
210
+ this.name = "ChecksumMismatchError";
211
+ }
212
+ };
213
+ var MigrationError = class extends UUError {
214
+ constructor(message, options) {
215
+ super(message, options);
216
+ this.name = "MigrationError";
217
+ }
218
+ };
219
+ var PlatformError = class extends UUError {
220
+ constructor(message, options) {
221
+ super(message, options);
222
+ this.name = "PlatformError";
223
+ }
224
+ };
225
+
191
226
  // lib/dom.ts
227
+ var domReady = false;
228
+ document.addEventListener("DOMContentLoaded", () => domReady = true);
192
229
  function getUnsafeWindow() {
193
230
  try {
194
231
  return unsafeWindow;
@@ -244,8 +281,8 @@ function openInNewTab(href, background, additionalProps) {
244
281
  }
245
282
  function interceptEvent(eventObject, eventName, predicate = () => true) {
246
283
  var _a;
247
- if ((eventObject === window || eventObject === getUnsafeWindow()) && ((_a = GM == null ? undefined : GM.info) == null ? undefined : _a.scriptHandler) && GM.info.scriptHandler === "FireMonkey")
248
- throw new Error("Intercepting window events is not supported on FireMonkey due to the isolated context the userscript runs in.");
284
+ if (((_a = GM == null ? undefined : GM.info) == null ? undefined : _a.scriptHandler) && GM.info.scriptHandler === "FireMonkey" && (eventObject === window || eventObject === getUnsafeWindow()))
285
+ throw new PlatformError("Intercepting window events is not supported on FireMonkey due to the isolated context the userscript runs in.");
249
286
  Error.stackTraceLimit = Math.max(Error.stackTraceLimit, 100);
250
287
  if (isNaN(Error.stackTraceLimit))
251
288
  Error.stackTraceLimit = 100;
@@ -327,6 +364,36 @@ function setInnerHtmlUnsafe(element, html) {
327
364
  element.innerHTML = (_c = (_b = ttPolicy == null ? undefined : ttPolicy.createHTML) == null ? undefined : _b.call(ttPolicy, html)) != null ? _c : html;
328
365
  return element;
329
366
  }
367
+ function probeElementStyle(probeStyle, element, hideOffscreen = true, parentElement = document.body) {
368
+ const el = element ? typeof element === "function" ? element() : element : document.createElement("span");
369
+ if (hideOffscreen) {
370
+ el.style.position = "absolute";
371
+ el.style.left = "-9999px";
372
+ el.style.top = "-9999px";
373
+ el.style.zIndex = "-9999";
374
+ }
375
+ el.classList.add("_uu_probe_element");
376
+ parentElement.appendChild(el);
377
+ const style = window.getComputedStyle(el);
378
+ const result = probeStyle(style, el);
379
+ setTimeout(() => el.remove(), 1);
380
+ return result;
381
+ }
382
+ function isDomLoaded() {
383
+ return domReady;
384
+ }
385
+ function onDomLoad(cb) {
386
+ return new Promise((res) => {
387
+ if (domReady) {
388
+ cb == null ? undefined : cb();
389
+ res();
390
+ } else
391
+ document.addEventListener("DOMContentLoaded", () => {
392
+ cb == null ? undefined : cb();
393
+ res();
394
+ });
395
+ });
396
+ }
330
397
 
331
398
  // lib/crypto.ts
332
399
  function compress(input, compressionFormat, outputType = "string") {
@@ -374,6 +441,8 @@ function computeHash(input, algorithm = "SHA-256") {
374
441
  });
375
442
  }
376
443
  function randomId(length = 16, radix = 16, enhancedEntropy = false, randomCase = true) {
444
+ if (radix < 2 || radix > 36)
445
+ throw new RangeError("The radix argument must be between 2 and 36");
377
446
  let arr = [];
378
447
  const caseArr = randomCase ? [0, 1] : [0];
379
448
  if (enhancedEntropy) {
@@ -547,8 +616,7 @@ var DataStore = class {
547
616
  lastFmtVer = oldFmtVer = ver;
548
617
  } catch (err) {
549
618
  if (!resetOnError)
550
- throw new Error(`Error while running migration function for format version '${fmtVer}'`);
551
- console.error(`Error while running migration function for format version '${fmtVer}' - resetting to the default value.`, err);
619
+ throw new MigrationError(`Error while running migration function for format version '${fmtVer}'`, { cause: err });
552
620
  yield this.saveDefaultData();
553
621
  return this.getData();
554
622
  }
@@ -609,7 +677,6 @@ var DataStore = class {
609
677
  return JSON.parse(decRes != null ? decRes : data);
610
678
  });
611
679
  }
612
- //#region misc
613
680
  /** Copies a JSON-compatible object and loses all its internal references in the process */
614
681
  deepCopy(obj) {
615
682
  return JSON.parse(JSON.stringify(obj));
@@ -661,7 +728,7 @@ var DataStore = class {
661
728
  };
662
729
 
663
730
  // lib/DataStoreSerializer.ts
664
- var DataStoreSerializer = class {
731
+ var DataStoreSerializer = class _DataStoreSerializer {
665
732
  constructor(stores, options = {}) {
666
733
  __publicField(this, "stores");
667
734
  __publicField(this, "options");
@@ -679,27 +746,25 @@ var DataStoreSerializer = class {
679
746
  return computeHash(input, "SHA-256");
680
747
  });
681
748
  }
682
- /** Serializes a DataStore instance */
683
- serializeStore(storeInst) {
684
- return __async(this, null, function* () {
685
- const data = storeInst.encodingEnabled() ? yield storeInst.encodeData(JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData());
686
- const checksum = this.options.addChecksum ? yield this.calcChecksum(data) : undefined;
687
- return {
688
- id: storeInst.id,
689
- data,
690
- formatVersion: storeInst.formatVersion,
691
- encoded: storeInst.encodingEnabled(),
692
- checksum
693
- };
694
- });
695
- }
696
- /** Serializes the data stores into a string */
697
- serialize() {
749
+ /**
750
+ * Serializes the data stores into a string.
751
+ * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
752
+ * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
753
+ */
754
+ serialize(useEncoding = true, stringified = true) {
698
755
  return __async(this, null, function* () {
699
756
  const serData = [];
700
- for (const store of this.stores)
701
- serData.push(yield this.serializeStore(store));
702
- return JSON.stringify(serData);
757
+ for (const storeInst of this.stores) {
758
+ const data = useEncoding && storeInst.encodingEnabled() ? yield storeInst.encodeData(JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData());
759
+ serData.push({
760
+ id: storeInst.id,
761
+ data,
762
+ formatVersion: storeInst.formatVersion,
763
+ encoded: useEncoding && storeInst.encodingEnabled(),
764
+ checksum: this.options.addChecksum ? yield this.calcChecksum(data) : undefined
765
+ });
766
+ }
767
+ return stringified ? JSON.stringify(serData) : serData;
703
768
  });
704
769
  }
705
770
  /**
@@ -708,7 +773,9 @@ var DataStoreSerializer = class {
708
773
  */
709
774
  deserialize(serializedData) {
710
775
  return __async(this, null, function* () {
711
- const deserStores = JSON.parse(serializedData);
776
+ const deserStores = typeof serializedData === "string" ? JSON.parse(serializedData) : serializedData;
777
+ if (!Array.isArray(deserStores) || !deserStores.every(_DataStoreSerializer.isSerializedDataStore))
778
+ throw new TypeError("Invalid serialized data format! Expected an array of SerializedDataStore objects.");
712
779
  for (const storeData of deserStores) {
713
780
  const storeInst = this.stores.find((s) => s.id === storeData.id);
714
781
  if (!storeInst)
@@ -716,7 +783,7 @@ var DataStoreSerializer = class {
716
783
  if (this.options.ensureIntegrity && typeof storeData.checksum === "string") {
717
784
  const checksum = yield this.calcChecksum(storeData.data);
718
785
  if (checksum !== storeData.checksum)
719
- throw new Error(`Checksum mismatch for DataStore with ID "${storeData.id}"!
786
+ throw new ChecksumMismatchError(`Checksum mismatch for DataStore with ID "${storeData.id}"!
720
787
  Expected: ${storeData.checksum}
721
788
  Has: ${checksum}`);
722
789
  }
@@ -760,6 +827,10 @@ Has: ${checksum}`);
760
827
  return Promise.allSettled(this.stores.map((store) => store.deleteData()));
761
828
  });
762
829
  }
830
+ /** Checks if a given value is a SerializedDataStore object */
831
+ static isSerializedDataStore(obj) {
832
+ return typeof obj === "object" && obj !== null && "id" in obj && "data" in obj && "formatVersion" in obj && "encoded" in obj;
833
+ }
763
834
  };
764
835
  var NanoEmitter = class {
765
836
  constructor(options = {}) {
@@ -899,7 +970,7 @@ var Debouncer = class extends NanoEmitter {
899
970
  }, this.timeout);
900
971
  break;
901
972
  default:
902
- throw new Error(`Invalid debouncer type: ${this.type}`);
973
+ throw new TypeError(`Invalid debouncer type: ${this.type}`);
903
974
  }
904
975
  }
905
976
  };
@@ -1403,8 +1474,6 @@ function getListLength(obj, zeroOnInvalid = true) {
1403
1474
  }
1404
1475
 
1405
1476
  // lib/SelectorObserver.ts
1406
- var domLoaded = false;
1407
- document.addEventListener("DOMContentLoaded", () => domLoaded = true);
1408
1477
  var SelectorObserver = class {
1409
1478
  constructor(baseElement, options = {}) {
1410
1479
  __publicField(this, "enabled", false);
@@ -1445,7 +1514,7 @@ var SelectorObserver = class {
1445
1514
  }
1446
1515
  /** Call to check all selectors in the {@linkcode listenerMap} using {@linkcode checkSelector()} */
1447
1516
  checkAllSelectors() {
1448
- if (!this.enabled || !domLoaded)
1517
+ if (!this.enabled || !isDomLoaded())
1449
1518
  return;
1450
1519
  for (const [selector, listeners] of this.listenerMap.entries())
1451
1520
  this.checkSelector(selector, listeners);
@@ -1589,9 +1658,9 @@ function translate(language, key, ...trArgs) {
1589
1658
  if (typeof language !== "string" || language.length === 0 || typeof trObj !== "object" || trObj === null)
1590
1659
  return fallbackLang ? translate(fallbackLang, key, ...trArgs) : key;
1591
1660
  const transformTrVal = (trKey, trValue) => {
1592
- const tfs = valTransforms.filter(({ regex }) => new RegExp(regex).test(trValue));
1661
+ const tfs = valTransforms.filter(({ regex }) => new RegExp(regex).test(String(trValue)));
1593
1662
  if (tfs.length === 0)
1594
- return trValue;
1663
+ return String(trValue);
1595
1664
  let retStr = String(trValue);
1596
1665
  for (const tf of tfs) {
1597
1666
  const re = new RegExp(tf.regex);
@@ -1740,15 +1809,20 @@ var tr = {
1740
1809
  }
1741
1810
  };
1742
1811
 
1812
+ exports.ChecksumMismatchError = ChecksumMismatchError;
1743
1813
  exports.DataStore = DataStore;
1744
1814
  exports.DataStoreSerializer = DataStoreSerializer;
1745
1815
  exports.Debouncer = Debouncer;
1746
1816
  exports.Dialog = Dialog;
1817
+ exports.MigrationError = MigrationError;
1747
1818
  exports.NanoEmitter = NanoEmitter;
1819
+ exports.PlatformError = PlatformError;
1748
1820
  exports.SelectorObserver = SelectorObserver;
1821
+ exports.UUError = UUError;
1749
1822
  exports.addGlobalStyle = addGlobalStyle;
1750
1823
  exports.addParent = addParent;
1751
1824
  exports.autoPlural = autoPlural;
1825
+ exports.bitSetHas = bitSetHas;
1752
1826
  exports.clamp = clamp;
1753
1827
  exports.compress = compress;
1754
1828
  exports.computeHash = computeHash;
@@ -1768,20 +1842,24 @@ exports.hexToRgb = hexToRgb;
1768
1842
  exports.insertValues = insertValues;
1769
1843
  exports.interceptEvent = interceptEvent;
1770
1844
  exports.interceptWindowEvent = interceptWindowEvent;
1845
+ exports.isDomLoaded = isDomLoaded;
1771
1846
  exports.isScrollable = isScrollable;
1772
1847
  exports.lightenColor = lightenColor;
1773
1848
  exports.mapRange = mapRange;
1774
1849
  exports.observeElementProp = observeElementProp;
1850
+ exports.onDomLoad = onDomLoad;
1775
1851
  exports.openDialogs = openDialogs;
1776
1852
  exports.openInNewTab = openInNewTab;
1777
1853
  exports.pauseFor = pauseFor;
1778
1854
  exports.preloadImages = preloadImages;
1855
+ exports.probeElementStyle = probeElementStyle;
1779
1856
  exports.randRange = randRange;
1780
1857
  exports.randomId = randomId;
1781
1858
  exports.randomItem = randomItem;
1782
1859
  exports.randomItemIndex = randomItemIndex;
1783
1860
  exports.randomizeArray = randomizeArray;
1784
1861
  exports.rgbToHex = rgbToHex;
1862
+ exports.roundFixed = roundFixed;
1785
1863
  exports.setInnerHtmlUnsafe = setInnerHtmlUnsafe;
1786
1864
  exports.takeRandomItem = takeRandomItem;
1787
1865
  exports.tr = tr;
@@ -7,8 +7,8 @@
7
7
 
8
8
  // ==UserLibrary==
9
9
  // @name UserUtils
10
- // @description Lightweight library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and much more
11
- // @version 9.1.0
10
+ // @description General purpose DOM/GreaseMonkey library that allows you to register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and much more
11
+ // @version 9.2.0
12
12
  // @license MIT
13
13
  // @copyright Sv443 (https://github.com/Sv443)
14
14
 
@@ -117,13 +117,21 @@ var UserUtils = (function (exports) {
117
117
  } else
118
118
  return Math.floor(Math.random() * (max - min + 1)) + min;
119
119
  }
120
- function digitCount(num) {
120
+ function digitCount(num, withDecimals = true) {
121
121
  num = Number(!["string", "number"].includes(typeof num) ? String(num) : num);
122
122
  if (typeof num === "number" && isNaN(num))
123
123
  return NaN;
124
- return num === 0 ? 1 : Math.floor(
125
- Math.log10(Math.abs(Number(num))) + 1
126
- );
124
+ const [intPart, decPart] = num.toString().split(".");
125
+ const intDigits = intPart === "0" ? 1 : Math.floor(Math.log10(Math.abs(Number(intPart))) + 1);
126
+ const decDigits = withDecimals && decPart ? decPart.length : 0;
127
+ return intDigits + decDigits;
128
+ }
129
+ function roundFixed(num, fractionDigits) {
130
+ const scale = 10 ** fractionDigits;
131
+ return Math.round(num * scale) / scale;
132
+ }
133
+ function bitSetHas(bitSet, checkVal) {
134
+ return (bitSet & checkVal) === checkVal;
127
135
  }
128
136
 
129
137
  // lib/array.ts
@@ -148,7 +156,7 @@ var UserUtils = (function (exports) {
148
156
  if (array.length === 0)
149
157
  return retArray;
150
158
  for (let i = retArray.length - 1; i > 0; i--) {
151
- const j = Math.floor(randRange(0, 1e4) / 1e4 * (i + 1));
159
+ const j = Math.floor(Math.random() * (i + 1));
152
160
  [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
153
161
  }
154
162
  return retArray;
@@ -191,10 +199,10 @@ var UserUtils = (function (exports) {
191
199
  else if (color.startsWith("rgb")) {
192
200
  const rgbValues = (_a = color.match(/\d+(\.\d+)?/g)) == null ? undefined : _a.map(Number);
193
201
  if (!rgbValues)
194
- throw new Error("Invalid RGB/RGBA color format");
202
+ throw new TypeError("Invalid RGB/RGBA color format");
195
203
  [r, g, b, a] = rgbValues;
196
204
  } else
197
- throw new Error("Unsupported color format");
205
+ throw new TypeError("Unsupported color format");
198
206
  [r, g, b] = darkenRgb(r, g, b, percent);
199
207
  if (isHexCol)
200
208
  return rgbToHex(r, g, b, a, color.startsWith("#"), upperCase);
@@ -203,10 +211,39 @@ var UserUtils = (function (exports) {
203
211
  else if (color.startsWith("rgb"))
204
212
  return `rgb(${r}, ${g}, ${b})`;
205
213
  else
206
- throw new Error("Unsupported color format");
214
+ throw new TypeError("Unsupported color format");
207
215
  }
208
216
 
217
+ // lib/errors.ts
218
+ var UUError = class extends Error {
219
+ constructor(message, options) {
220
+ super(message, options);
221
+ __publicField(this, "date");
222
+ this.date = /* @__PURE__ */ new Date();
223
+ }
224
+ };
225
+ var ChecksumMismatchError = class extends UUError {
226
+ constructor(message, options) {
227
+ super(message, options);
228
+ this.name = "ChecksumMismatchError";
229
+ }
230
+ };
231
+ var MigrationError = class extends UUError {
232
+ constructor(message, options) {
233
+ super(message, options);
234
+ this.name = "MigrationError";
235
+ }
236
+ };
237
+ var PlatformError = class extends UUError {
238
+ constructor(message, options) {
239
+ super(message, options);
240
+ this.name = "PlatformError";
241
+ }
242
+ };
243
+
209
244
  // lib/dom.ts
245
+ var domReady = false;
246
+ document.addEventListener("DOMContentLoaded", () => domReady = true);
210
247
  function getUnsafeWindow() {
211
248
  try {
212
249
  return unsafeWindow;
@@ -262,8 +299,8 @@ var UserUtils = (function (exports) {
262
299
  }
263
300
  function interceptEvent(eventObject, eventName, predicate = () => true) {
264
301
  var _a;
265
- if ((eventObject === window || eventObject === getUnsafeWindow()) && ((_a = GM == null ? undefined : GM.info) == null ? undefined : _a.scriptHandler) && GM.info.scriptHandler === "FireMonkey")
266
- throw new Error("Intercepting window events is not supported on FireMonkey due to the isolated context the userscript runs in.");
302
+ if (((_a = GM == null ? undefined : GM.info) == null ? undefined : _a.scriptHandler) && GM.info.scriptHandler === "FireMonkey" && (eventObject === window || eventObject === getUnsafeWindow()))
303
+ throw new PlatformError("Intercepting window events is not supported on FireMonkey due to the isolated context the userscript runs in.");
267
304
  Error.stackTraceLimit = Math.max(Error.stackTraceLimit, 100);
268
305
  if (isNaN(Error.stackTraceLimit))
269
306
  Error.stackTraceLimit = 100;
@@ -345,6 +382,36 @@ var UserUtils = (function (exports) {
345
382
  element.innerHTML = (_c = (_b = ttPolicy == null ? undefined : ttPolicy.createHTML) == null ? undefined : _b.call(ttPolicy, html)) != null ? _c : html;
346
383
  return element;
347
384
  }
385
+ function probeElementStyle(probeStyle, element, hideOffscreen = true, parentElement = document.body) {
386
+ const el = element ? typeof element === "function" ? element() : element : document.createElement("span");
387
+ if (hideOffscreen) {
388
+ el.style.position = "absolute";
389
+ el.style.left = "-9999px";
390
+ el.style.top = "-9999px";
391
+ el.style.zIndex = "-9999";
392
+ }
393
+ el.classList.add("_uu_probe_element");
394
+ parentElement.appendChild(el);
395
+ const style = window.getComputedStyle(el);
396
+ const result = probeStyle(style, el);
397
+ setTimeout(() => el.remove(), 1);
398
+ return result;
399
+ }
400
+ function isDomLoaded() {
401
+ return domReady;
402
+ }
403
+ function onDomLoad(cb) {
404
+ return new Promise((res) => {
405
+ if (domReady) {
406
+ cb == null ? undefined : cb();
407
+ res();
408
+ } else
409
+ document.addEventListener("DOMContentLoaded", () => {
410
+ cb == null ? undefined : cb();
411
+ res();
412
+ });
413
+ });
414
+ }
348
415
 
349
416
  // lib/crypto.ts
350
417
  function compress(input, compressionFormat, outputType = "string") {
@@ -392,6 +459,8 @@ var UserUtils = (function (exports) {
392
459
  });
393
460
  }
394
461
  function randomId(length = 16, radix = 16, enhancedEntropy = false, randomCase = true) {
462
+ if (radix < 2 || radix > 36)
463
+ throw new RangeError("The radix argument must be between 2 and 36");
395
464
  let arr = [];
396
465
  const caseArr = randomCase ? [0, 1] : [0];
397
466
  if (enhancedEntropy) {
@@ -565,8 +634,7 @@ var UserUtils = (function (exports) {
565
634
  lastFmtVer = oldFmtVer = ver;
566
635
  } catch (err) {
567
636
  if (!resetOnError)
568
- throw new Error(`Error while running migration function for format version '${fmtVer}'`);
569
- console.error(`Error while running migration function for format version '${fmtVer}' - resetting to the default value.`, err);
637
+ throw new MigrationError(`Error while running migration function for format version '${fmtVer}'`, { cause: err });
570
638
  yield this.saveDefaultData();
571
639
  return this.getData();
572
640
  }
@@ -627,7 +695,6 @@ var UserUtils = (function (exports) {
627
695
  return JSON.parse(decRes != null ? decRes : data);
628
696
  });
629
697
  }
630
- //#region misc
631
698
  /** Copies a JSON-compatible object and loses all its internal references in the process */
632
699
  deepCopy(obj) {
633
700
  return JSON.parse(JSON.stringify(obj));
@@ -679,7 +746,7 @@ var UserUtils = (function (exports) {
679
746
  };
680
747
 
681
748
  // lib/DataStoreSerializer.ts
682
- var DataStoreSerializer = class {
749
+ var DataStoreSerializer = class _DataStoreSerializer {
683
750
  constructor(stores, options = {}) {
684
751
  __publicField(this, "stores");
685
752
  __publicField(this, "options");
@@ -697,27 +764,25 @@ var UserUtils = (function (exports) {
697
764
  return computeHash(input, "SHA-256");
698
765
  });
699
766
  }
700
- /** Serializes a DataStore instance */
701
- serializeStore(storeInst) {
702
- return __async(this, null, function* () {
703
- const data = storeInst.encodingEnabled() ? yield storeInst.encodeData(JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData());
704
- const checksum = this.options.addChecksum ? yield this.calcChecksum(data) : undefined;
705
- return {
706
- id: storeInst.id,
707
- data,
708
- formatVersion: storeInst.formatVersion,
709
- encoded: storeInst.encodingEnabled(),
710
- checksum
711
- };
712
- });
713
- }
714
- /** Serializes the data stores into a string */
715
- serialize() {
767
+ /**
768
+ * Serializes the data stores into a string.
769
+ * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
770
+ * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
771
+ */
772
+ serialize(useEncoding = true, stringified = true) {
716
773
  return __async(this, null, function* () {
717
774
  const serData = [];
718
- for (const store of this.stores)
719
- serData.push(yield this.serializeStore(store));
720
- return JSON.stringify(serData);
775
+ for (const storeInst of this.stores) {
776
+ const data = useEncoding && storeInst.encodingEnabled() ? yield storeInst.encodeData(JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData());
777
+ serData.push({
778
+ id: storeInst.id,
779
+ data,
780
+ formatVersion: storeInst.formatVersion,
781
+ encoded: useEncoding && storeInst.encodingEnabled(),
782
+ checksum: this.options.addChecksum ? yield this.calcChecksum(data) : undefined
783
+ });
784
+ }
785
+ return stringified ? JSON.stringify(serData) : serData;
721
786
  });
722
787
  }
723
788
  /**
@@ -726,7 +791,9 @@ var UserUtils = (function (exports) {
726
791
  */
727
792
  deserialize(serializedData) {
728
793
  return __async(this, null, function* () {
729
- const deserStores = JSON.parse(serializedData);
794
+ const deserStores = typeof serializedData === "string" ? JSON.parse(serializedData) : serializedData;
795
+ if (!Array.isArray(deserStores) || !deserStores.every(_DataStoreSerializer.isSerializedDataStore))
796
+ throw new TypeError("Invalid serialized data format! Expected an array of SerializedDataStore objects.");
730
797
  for (const storeData of deserStores) {
731
798
  const storeInst = this.stores.find((s) => s.id === storeData.id);
732
799
  if (!storeInst)
@@ -734,7 +801,7 @@ var UserUtils = (function (exports) {
734
801
  if (this.options.ensureIntegrity && typeof storeData.checksum === "string") {
735
802
  const checksum = yield this.calcChecksum(storeData.data);
736
803
  if (checksum !== storeData.checksum)
737
- throw new Error(`Checksum mismatch for DataStore with ID "${storeData.id}"!
804
+ throw new ChecksumMismatchError(`Checksum mismatch for DataStore with ID "${storeData.id}"!
738
805
  Expected: ${storeData.checksum}
739
806
  Has: ${checksum}`);
740
807
  }
@@ -778,6 +845,10 @@ Has: ${checksum}`);
778
845
  return Promise.allSettled(this.stores.map((store) => store.deleteData()));
779
846
  });
780
847
  }
848
+ /** Checks if a given value is a SerializedDataStore object */
849
+ static isSerializedDataStore(obj) {
850
+ return typeof obj === "object" && obj !== null && "id" in obj && "data" in obj && "formatVersion" in obj && "encoded" in obj;
851
+ }
781
852
  };
782
853
 
783
854
  // node_modules/.pnpm/nanoevents@9.1.0/node_modules/nanoevents/index.js
@@ -937,7 +1008,7 @@ Has: ${checksum}`);
937
1008
  }, this.timeout);
938
1009
  break;
939
1010
  default:
940
- throw new Error(`Invalid debouncer type: ${this.type}`);
1011
+ throw new TypeError(`Invalid debouncer type: ${this.type}`);
941
1012
  }
942
1013
  }
943
1014
  };
@@ -1441,8 +1512,6 @@ Has: ${checksum}`);
1441
1512
  }
1442
1513
 
1443
1514
  // lib/SelectorObserver.ts
1444
- var domLoaded = false;
1445
- document.addEventListener("DOMContentLoaded", () => domLoaded = true);
1446
1515
  var SelectorObserver = class {
1447
1516
  constructor(baseElement, options = {}) {
1448
1517
  __publicField(this, "enabled", false);
@@ -1483,7 +1552,7 @@ Has: ${checksum}`);
1483
1552
  }
1484
1553
  /** Call to check all selectors in the {@linkcode listenerMap} using {@linkcode checkSelector()} */
1485
1554
  checkAllSelectors() {
1486
- if (!this.enabled || !domLoaded)
1555
+ if (!this.enabled || !isDomLoaded())
1487
1556
  return;
1488
1557
  for (const [selector, listeners] of this.listenerMap.entries())
1489
1558
  this.checkSelector(selector, listeners);
@@ -1627,9 +1696,9 @@ Has: ${checksum}`);
1627
1696
  if (typeof language !== "string" || language.length === 0 || typeof trObj !== "object" || trObj === null)
1628
1697
  return fallbackLang ? translate(fallbackLang, key, ...trArgs) : key;
1629
1698
  const transformTrVal = (trKey, trValue) => {
1630
- const tfs = valTransforms.filter(({ regex }) => new RegExp(regex).test(trValue));
1699
+ const tfs = valTransforms.filter(({ regex }) => new RegExp(regex).test(String(trValue)));
1631
1700
  if (tfs.length === 0)
1632
- return trValue;
1701
+ return String(trValue);
1633
1702
  let retStr = String(trValue);
1634
1703
  for (const tf of tfs) {
1635
1704
  const re = new RegExp(tf.regex);
@@ -1778,15 +1847,20 @@ Has: ${checksum}`);
1778
1847
  }
1779
1848
  };
1780
1849
 
1850
+ exports.ChecksumMismatchError = ChecksumMismatchError;
1781
1851
  exports.DataStore = DataStore;
1782
1852
  exports.DataStoreSerializer = DataStoreSerializer;
1783
1853
  exports.Debouncer = Debouncer;
1784
1854
  exports.Dialog = Dialog;
1855
+ exports.MigrationError = MigrationError;
1785
1856
  exports.NanoEmitter = NanoEmitter;
1857
+ exports.PlatformError = PlatformError;
1786
1858
  exports.SelectorObserver = SelectorObserver;
1859
+ exports.UUError = UUError;
1787
1860
  exports.addGlobalStyle = addGlobalStyle;
1788
1861
  exports.addParent = addParent;
1789
1862
  exports.autoPlural = autoPlural;
1863
+ exports.bitSetHas = bitSetHas;
1790
1864
  exports.clamp = clamp;
1791
1865
  exports.compress = compress;
1792
1866
  exports.computeHash = computeHash;
@@ -1806,20 +1880,24 @@ Has: ${checksum}`);
1806
1880
  exports.insertValues = insertValues;
1807
1881
  exports.interceptEvent = interceptEvent;
1808
1882
  exports.interceptWindowEvent = interceptWindowEvent;
1883
+ exports.isDomLoaded = isDomLoaded;
1809
1884
  exports.isScrollable = isScrollable;
1810
1885
  exports.lightenColor = lightenColor;
1811
1886
  exports.mapRange = mapRange;
1812
1887
  exports.observeElementProp = observeElementProp;
1888
+ exports.onDomLoad = onDomLoad;
1813
1889
  exports.openDialogs = openDialogs;
1814
1890
  exports.openInNewTab = openInNewTab;
1815
1891
  exports.pauseFor = pauseFor;
1816
1892
  exports.preloadImages = preloadImages;
1893
+ exports.probeElementStyle = probeElementStyle;
1817
1894
  exports.randRange = randRange;
1818
1895
  exports.randomId = randomId;
1819
1896
  exports.randomItem = randomItem;
1820
1897
  exports.randomItemIndex = randomItemIndex;
1821
1898
  exports.randomizeArray = randomizeArray;
1822
1899
  exports.rgbToHex = rgbToHex;
1900
+ exports.roundFixed = roundFixed;
1823
1901
  exports.setInnerHtmlUnsafe = setInnerHtmlUnsafe;
1824
1902
  exports.takeRandomItem = takeRandomItem;
1825
1903
  exports.tr = tr;
package/dist/index.js CHANGED
@@ -97,13 +97,21 @@ function randRange(...args) {
97
97
  } else
98
98
  return Math.floor(Math.random() * (max - min + 1)) + min;
99
99
  }
100
- function digitCount(num) {
100
+ function digitCount(num, withDecimals = true) {
101
101
  num = Number(!["string", "number"].includes(typeof num) ? String(num) : num);
102
102
  if (typeof num === "number" && isNaN(num))
103
103
  return NaN;
104
- return num === 0 ? 1 : Math.floor(
105
- Math.log10(Math.abs(Number(num))) + 1
106
- );
104
+ const [intPart, decPart] = num.toString().split(".");
105
+ const intDigits = intPart === "0" ? 1 : Math.floor(Math.log10(Math.abs(Number(intPart))) + 1);
106
+ const decDigits = withDecimals && decPart ? decPart.length : 0;
107
+ return intDigits + decDigits;
108
+ }
109
+ function roundFixed(num, fractionDigits) {
110
+ const scale = 10 ** fractionDigits;
111
+ return Math.round(num * scale) / scale;
112
+ }
113
+ function bitSetHas(bitSet, checkVal) {
114
+ return (bitSet & checkVal) === checkVal;
107
115
  }
108
116
 
109
117
  // lib/array.ts
@@ -128,7 +136,7 @@ function randomizeArray(array) {
128
136
  if (array.length === 0)
129
137
  return retArray;
130
138
  for (let i = retArray.length - 1; i > 0; i--) {
131
- const j = Math.floor(randRange(0, 1e4) / 1e4 * (i + 1));
139
+ const j = Math.floor(Math.random() * (i + 1));
132
140
  [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
133
141
  }
134
142
  return retArray;
@@ -171,10 +179,10 @@ function darkenColor(color, percent, upperCase = false) {
171
179
  else if (color.startsWith("rgb")) {
172
180
  const rgbValues = (_a = color.match(/\d+(\.\d+)?/g)) == null ? undefined : _a.map(Number);
173
181
  if (!rgbValues)
174
- throw new Error("Invalid RGB/RGBA color format");
182
+ throw new TypeError("Invalid RGB/RGBA color format");
175
183
  [r, g, b, a] = rgbValues;
176
184
  } else
177
- throw new Error("Unsupported color format");
185
+ throw new TypeError("Unsupported color format");
178
186
  [r, g, b] = darkenRgb(r, g, b, percent);
179
187
  if (isHexCol)
180
188
  return rgbToHex(r, g, b, a, color.startsWith("#"), upperCase);
@@ -183,10 +191,39 @@ function darkenColor(color, percent, upperCase = false) {
183
191
  else if (color.startsWith("rgb"))
184
192
  return `rgb(${r}, ${g}, ${b})`;
185
193
  else
186
- throw new Error("Unsupported color format");
194
+ throw new TypeError("Unsupported color format");
187
195
  }
188
196
 
197
+ // lib/errors.ts
198
+ var UUError = class extends Error {
199
+ constructor(message, options) {
200
+ super(message, options);
201
+ __publicField(this, "date");
202
+ this.date = /* @__PURE__ */ new Date();
203
+ }
204
+ };
205
+ var ChecksumMismatchError = class extends UUError {
206
+ constructor(message, options) {
207
+ super(message, options);
208
+ this.name = "ChecksumMismatchError";
209
+ }
210
+ };
211
+ var MigrationError = class extends UUError {
212
+ constructor(message, options) {
213
+ super(message, options);
214
+ this.name = "MigrationError";
215
+ }
216
+ };
217
+ var PlatformError = class extends UUError {
218
+ constructor(message, options) {
219
+ super(message, options);
220
+ this.name = "PlatformError";
221
+ }
222
+ };
223
+
189
224
  // lib/dom.ts
225
+ var domReady = false;
226
+ document.addEventListener("DOMContentLoaded", () => domReady = true);
190
227
  function getUnsafeWindow() {
191
228
  try {
192
229
  return unsafeWindow;
@@ -242,8 +279,8 @@ function openInNewTab(href, background, additionalProps) {
242
279
  }
243
280
  function interceptEvent(eventObject, eventName, predicate = () => true) {
244
281
  var _a;
245
- if ((eventObject === window || eventObject === getUnsafeWindow()) && ((_a = GM == null ? undefined : GM.info) == null ? undefined : _a.scriptHandler) && GM.info.scriptHandler === "FireMonkey")
246
- throw new Error("Intercepting window events is not supported on FireMonkey due to the isolated context the userscript runs in.");
282
+ if (((_a = GM == null ? undefined : GM.info) == null ? undefined : _a.scriptHandler) && GM.info.scriptHandler === "FireMonkey" && (eventObject === window || eventObject === getUnsafeWindow()))
283
+ throw new PlatformError("Intercepting window events is not supported on FireMonkey due to the isolated context the userscript runs in.");
247
284
  Error.stackTraceLimit = Math.max(Error.stackTraceLimit, 100);
248
285
  if (isNaN(Error.stackTraceLimit))
249
286
  Error.stackTraceLimit = 100;
@@ -325,6 +362,36 @@ function setInnerHtmlUnsafe(element, html) {
325
362
  element.innerHTML = (_c = (_b = ttPolicy == null ? undefined : ttPolicy.createHTML) == null ? undefined : _b.call(ttPolicy, html)) != null ? _c : html;
326
363
  return element;
327
364
  }
365
+ function probeElementStyle(probeStyle, element, hideOffscreen = true, parentElement = document.body) {
366
+ const el = element ? typeof element === "function" ? element() : element : document.createElement("span");
367
+ if (hideOffscreen) {
368
+ el.style.position = "absolute";
369
+ el.style.left = "-9999px";
370
+ el.style.top = "-9999px";
371
+ el.style.zIndex = "-9999";
372
+ }
373
+ el.classList.add("_uu_probe_element");
374
+ parentElement.appendChild(el);
375
+ const style = window.getComputedStyle(el);
376
+ const result = probeStyle(style, el);
377
+ setTimeout(() => el.remove(), 1);
378
+ return result;
379
+ }
380
+ function isDomLoaded() {
381
+ return domReady;
382
+ }
383
+ function onDomLoad(cb) {
384
+ return new Promise((res) => {
385
+ if (domReady) {
386
+ cb == null ? undefined : cb();
387
+ res();
388
+ } else
389
+ document.addEventListener("DOMContentLoaded", () => {
390
+ cb == null ? undefined : cb();
391
+ res();
392
+ });
393
+ });
394
+ }
328
395
 
329
396
  // lib/crypto.ts
330
397
  function compress(input, compressionFormat, outputType = "string") {
@@ -372,6 +439,8 @@ function computeHash(input, algorithm = "SHA-256") {
372
439
  });
373
440
  }
374
441
  function randomId(length = 16, radix = 16, enhancedEntropy = false, randomCase = true) {
442
+ if (radix < 2 || radix > 36)
443
+ throw new RangeError("The radix argument must be between 2 and 36");
375
444
  let arr = [];
376
445
  const caseArr = randomCase ? [0, 1] : [0];
377
446
  if (enhancedEntropy) {
@@ -545,8 +614,7 @@ var DataStore = class {
545
614
  lastFmtVer = oldFmtVer = ver;
546
615
  } catch (err) {
547
616
  if (!resetOnError)
548
- throw new Error(`Error while running migration function for format version '${fmtVer}'`);
549
- console.error(`Error while running migration function for format version '${fmtVer}' - resetting to the default value.`, err);
617
+ throw new MigrationError(`Error while running migration function for format version '${fmtVer}'`, { cause: err });
550
618
  yield this.saveDefaultData();
551
619
  return this.getData();
552
620
  }
@@ -607,7 +675,6 @@ var DataStore = class {
607
675
  return JSON.parse(decRes != null ? decRes : data);
608
676
  });
609
677
  }
610
- //#region misc
611
678
  /** Copies a JSON-compatible object and loses all its internal references in the process */
612
679
  deepCopy(obj) {
613
680
  return JSON.parse(JSON.stringify(obj));
@@ -659,7 +726,7 @@ var DataStore = class {
659
726
  };
660
727
 
661
728
  // lib/DataStoreSerializer.ts
662
- var DataStoreSerializer = class {
729
+ var DataStoreSerializer = class _DataStoreSerializer {
663
730
  constructor(stores, options = {}) {
664
731
  __publicField(this, "stores");
665
732
  __publicField(this, "options");
@@ -677,27 +744,25 @@ var DataStoreSerializer = class {
677
744
  return computeHash(input, "SHA-256");
678
745
  });
679
746
  }
680
- /** Serializes a DataStore instance */
681
- serializeStore(storeInst) {
682
- return __async(this, null, function* () {
683
- const data = storeInst.encodingEnabled() ? yield storeInst.encodeData(JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData());
684
- const checksum = this.options.addChecksum ? yield this.calcChecksum(data) : undefined;
685
- return {
686
- id: storeInst.id,
687
- data,
688
- formatVersion: storeInst.formatVersion,
689
- encoded: storeInst.encodingEnabled(),
690
- checksum
691
- };
692
- });
693
- }
694
- /** Serializes the data stores into a string */
695
- serialize() {
747
+ /**
748
+ * Serializes the data stores into a string.
749
+ * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
750
+ * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
751
+ */
752
+ serialize(useEncoding = true, stringified = true) {
696
753
  return __async(this, null, function* () {
697
754
  const serData = [];
698
- for (const store of this.stores)
699
- serData.push(yield this.serializeStore(store));
700
- return JSON.stringify(serData);
755
+ for (const storeInst of this.stores) {
756
+ const data = useEncoding && storeInst.encodingEnabled() ? yield storeInst.encodeData(JSON.stringify(storeInst.getData())) : JSON.stringify(storeInst.getData());
757
+ serData.push({
758
+ id: storeInst.id,
759
+ data,
760
+ formatVersion: storeInst.formatVersion,
761
+ encoded: useEncoding && storeInst.encodingEnabled(),
762
+ checksum: this.options.addChecksum ? yield this.calcChecksum(data) : undefined
763
+ });
764
+ }
765
+ return stringified ? JSON.stringify(serData) : serData;
701
766
  });
702
767
  }
703
768
  /**
@@ -706,7 +771,9 @@ var DataStoreSerializer = class {
706
771
  */
707
772
  deserialize(serializedData) {
708
773
  return __async(this, null, function* () {
709
- const deserStores = JSON.parse(serializedData);
774
+ const deserStores = typeof serializedData === "string" ? JSON.parse(serializedData) : serializedData;
775
+ if (!Array.isArray(deserStores) || !deserStores.every(_DataStoreSerializer.isSerializedDataStore))
776
+ throw new TypeError("Invalid serialized data format! Expected an array of SerializedDataStore objects.");
710
777
  for (const storeData of deserStores) {
711
778
  const storeInst = this.stores.find((s) => s.id === storeData.id);
712
779
  if (!storeInst)
@@ -714,7 +781,7 @@ var DataStoreSerializer = class {
714
781
  if (this.options.ensureIntegrity && typeof storeData.checksum === "string") {
715
782
  const checksum = yield this.calcChecksum(storeData.data);
716
783
  if (checksum !== storeData.checksum)
717
- throw new Error(`Checksum mismatch for DataStore with ID "${storeData.id}"!
784
+ throw new ChecksumMismatchError(`Checksum mismatch for DataStore with ID "${storeData.id}"!
718
785
  Expected: ${storeData.checksum}
719
786
  Has: ${checksum}`);
720
787
  }
@@ -758,6 +825,10 @@ Has: ${checksum}`);
758
825
  return Promise.allSettled(this.stores.map((store) => store.deleteData()));
759
826
  });
760
827
  }
828
+ /** Checks if a given value is a SerializedDataStore object */
829
+ static isSerializedDataStore(obj) {
830
+ return typeof obj === "object" && obj !== null && "id" in obj && "data" in obj && "formatVersion" in obj && "encoded" in obj;
831
+ }
761
832
  };
762
833
  var NanoEmitter = class {
763
834
  constructor(options = {}) {
@@ -897,7 +968,7 @@ var Debouncer = class extends NanoEmitter {
897
968
  }, this.timeout);
898
969
  break;
899
970
  default:
900
- throw new Error(`Invalid debouncer type: ${this.type}`);
971
+ throw new TypeError(`Invalid debouncer type: ${this.type}`);
901
972
  }
902
973
  }
903
974
  };
@@ -1401,8 +1472,6 @@ function getListLength(obj, zeroOnInvalid = true) {
1401
1472
  }
1402
1473
 
1403
1474
  // lib/SelectorObserver.ts
1404
- var domLoaded = false;
1405
- document.addEventListener("DOMContentLoaded", () => domLoaded = true);
1406
1475
  var SelectorObserver = class {
1407
1476
  constructor(baseElement, options = {}) {
1408
1477
  __publicField(this, "enabled", false);
@@ -1443,7 +1512,7 @@ var SelectorObserver = class {
1443
1512
  }
1444
1513
  /** Call to check all selectors in the {@linkcode listenerMap} using {@linkcode checkSelector()} */
1445
1514
  checkAllSelectors() {
1446
- if (!this.enabled || !domLoaded)
1515
+ if (!this.enabled || !isDomLoaded())
1447
1516
  return;
1448
1517
  for (const [selector, listeners] of this.listenerMap.entries())
1449
1518
  this.checkSelector(selector, listeners);
@@ -1587,9 +1656,9 @@ function translate(language, key, ...trArgs) {
1587
1656
  if (typeof language !== "string" || language.length === 0 || typeof trObj !== "object" || trObj === null)
1588
1657
  return fallbackLang ? translate(fallbackLang, key, ...trArgs) : key;
1589
1658
  const transformTrVal = (trKey, trValue) => {
1590
- const tfs = valTransforms.filter(({ regex }) => new RegExp(regex).test(trValue));
1659
+ const tfs = valTransforms.filter(({ regex }) => new RegExp(regex).test(String(trValue)));
1591
1660
  if (tfs.length === 0)
1592
- return trValue;
1661
+ return String(trValue);
1593
1662
  let retStr = String(trValue);
1594
1663
  for (const tf of tfs) {
1595
1664
  const re = new RegExp(tf.regex);
@@ -1738,4 +1807,4 @@ var tr = {
1738
1807
  }
1739
1808
  };
1740
1809
 
1741
- export { DataStore, DataStoreSerializer, Debouncer, Dialog, NanoEmitter, SelectorObserver, addGlobalStyle, addParent, autoPlural, clamp, compress, computeHash, consumeGen, consumeStringGen, currentDialogId, darkenColor, debounce, decompress, defaultDialogCss, defaultStrings, digitCount, fetchAdvanced, getListLength, getSiblingsFrame, getUnsafeWindow, hexToRgb, insertValues, interceptEvent, interceptWindowEvent, isScrollable, lightenColor, mapRange, observeElementProp, openDialogs, openInNewTab, pauseFor, preloadImages, randRange, randomId, randomItem, randomItemIndex, randomizeArray, rgbToHex, setInnerHtmlUnsafe, takeRandomItem, tr };
1810
+ export { ChecksumMismatchError, DataStore, DataStoreSerializer, Debouncer, Dialog, MigrationError, NanoEmitter, PlatformError, SelectorObserver, UUError, addGlobalStyle, addParent, autoPlural, bitSetHas, clamp, compress, computeHash, consumeGen, consumeStringGen, currentDialogId, darkenColor, debounce, decompress, defaultDialogCss, defaultStrings, digitCount, fetchAdvanced, getListLength, getSiblingsFrame, getUnsafeWindow, hexToRgb, insertValues, interceptEvent, interceptWindowEvent, isDomLoaded, isScrollable, lightenColor, mapRange, observeElementProp, onDomLoad, openDialogs, openInNewTab, pauseFor, preloadImages, probeElementStyle, randRange, randomId, randomItem, randomItemIndex, randomizeArray, rgbToHex, roundFixed, setInnerHtmlUnsafe, takeRandomItem, tr };
@@ -2,7 +2,7 @@
2
2
  * @module lib/DataStoreSerializer
3
3
  * This module contains the DataStoreSerializer class, which allows you to import and export serialized DataStore data - [see the documentation for more info](https://github.com/Sv443-Network/UserUtils/blob/main/docs.md#datastoreserializer)
4
4
  */
5
- import { type DataStore } from "./index.js";
5
+ import type { DataStore } from "./DataStore.js";
6
6
  export type DataStoreSerializerOptions = {
7
7
  /** Whether to add a checksum to the exported data */
8
8
  addChecksum?: boolean;
@@ -43,15 +43,23 @@ export declare class DataStoreSerializer {
43
43
  constructor(stores: DataStore[], options?: DataStoreSerializerOptions);
44
44
  /** Calculates the checksum of a string */
45
45
  protected calcChecksum(input: string): Promise<string>;
46
- /** Serializes a DataStore instance */
47
- protected serializeStore(storeInst: DataStore): Promise<SerializedDataStore>;
48
- /** Serializes the data stores into a string */
49
- serialize(): Promise<string>;
46
+ /**
47
+ * Serializes the data stores into a string.
48
+ * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
49
+ * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
50
+ */
51
+ serialize(useEncoding: boolean, stringified: true): Promise<string>;
52
+ /**
53
+ * Serializes the data stores into a string.
54
+ * @param useEncoding Whether to encode the data using each DataStore's `encodeData()` method
55
+ * @param stringified Whether to return the result as a string or as an array of `SerializedDataStore` objects
56
+ */
57
+ serialize(useEncoding: boolean, stringified: false): Promise<SerializedDataStore[]>;
50
58
  /**
51
59
  * Deserializes the data exported via {@linkcode serialize()} and imports it into the DataStore instances.
52
60
  * Also triggers the migration process if the data format has changed.
53
61
  */
54
- deserialize(serializedData: string): Promise<void>;
62
+ deserialize(serializedData: string | SerializedDataStore[]): Promise<void>;
55
63
  /**
56
64
  * Loads the persistent data of the DataStore instances into the in-memory cache.
57
65
  * Also triggers the migration process if the data format has changed.
@@ -65,4 +73,6 @@ export declare class DataStoreSerializer {
65
73
  * Leaves the in-memory data untouched.
66
74
  */
67
75
  deleteStoresData(): Promise<PromiseSettledResult<void>[]>;
76
+ /** Checks if a given value is a SerializedDataStore object */
77
+ static isSerializedDataStore(obj: unknown): obj is SerializedDataStore;
68
78
  }
package/dist/lib/dom.d.ts CHANGED
@@ -76,3 +76,22 @@ export declare function getSiblingsFrame<TSibling extends Element = HTMLElement>
76
76
  * - ⚠️ This function does not perform any sanitization and should thus be used with utmost caution, as it can easily lead to XSS vulnerabilities!
77
77
  */
78
78
  export declare function setInnerHtmlUnsafe<TElement extends Element = HTMLElement>(element: TElement, html: string): TElement;
79
+ /**
80
+ * Creates an invisible temporary element to probe its rendered style.
81
+ * Has to be run after the `DOMContentLoaded` event has fired on the document object.
82
+ * @param probeStyle Function to probe the element's style. First argument is the element's style object from [`window.getComputedStyle()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/getComputedStyle), second argument is the element itself
83
+ * @param element The element to probe, or a function that creates and returns the element - should not be added to the DOM prior to calling this function! - all probe elements will have the class `_uu_probe_element` added to them
84
+ * @param hideOffscreen Whether to hide the element offscreen, enabled by default - disable if you want to probe the position style properties of the element
85
+ * @param parentElement The parent element to append the probe element to, defaults to `document.body`
86
+ * @returns The value returned by the `probeElement` function
87
+ */
88
+ export declare function probeElementStyle<TValue, TElem extends HTMLElement = HTMLSpanElement>(probeStyle: (style: CSSStyleDeclaration, element: TElem) => TValue, element?: TElem | (() => TElem), hideOffscreen?: boolean, parentElement?: HTMLElement): TValue;
89
+ /** Returns whether or not the DOM has finished loading */
90
+ export declare function isDomLoaded(): boolean;
91
+ /**
92
+ * Executes a callback and/or resolves the returned Promise when the DOM has finished loading.
93
+ * Immediately executes/resolves if the DOM is already loaded.
94
+ * @param cb Callback to execute when the DOM has finished loading
95
+ * @returns Returns a Promise that resolves when the DOM has finished loading
96
+ */
97
+ export declare function onDomLoad(cb?: () => void): Promise<void>;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @module lib/errors
3
+ * Contains custom error classes
4
+ */
5
+ /** Base class for all UserUtils errors - adds a `date` prop set to the error throw time */
6
+ export declare class UUError extends Error {
7
+ readonly date: Date;
8
+ constructor(message: string, options?: ErrorOptions);
9
+ }
10
+ /** Error while validating checksum */
11
+ export declare class ChecksumMismatchError extends UUError {
12
+ constructor(message: string, options?: ErrorOptions);
13
+ }
14
+ /** Error while migrating data */
15
+ export declare class MigrationError extends UUError {
16
+ constructor(message: string, options?: ErrorOptions);
17
+ }
18
+ /** Error due to the platform, like using a feature that's not supported by the browser */
19
+ export declare class PlatformError extends UUError {
20
+ constructor(message: string, options?: ErrorOptions);
21
+ }
@@ -10,6 +10,7 @@ export * from "./DataStoreSerializer.js";
10
10
  export * from "./Debouncer.js";
11
11
  export * from "./Dialog.js";
12
12
  export * from "./dom.js";
13
+ export * from "./errors.js";
13
14
  export * from "./math.js";
14
15
  export * from "./misc.js";
15
16
  export * from "./NanoEmitter.js";
@@ -27,5 +27,55 @@ export declare function randRange(min: number, max: number, enhancedEntropy?: bo
27
27
  * Set {@linkcode enhancedEntropy} to true to use `crypto.getRandomValues()` for better cryptographic randomness (this also makes it take longer to generate)
28
28
  */
29
29
  export declare function randRange(max: number, enhancedEntropy?: boolean): number;
30
- /** Calculates the amount of digits in the given number - the given number or string will be passed to the `Number()` constructor. Returns NaN if the number is invalid. */
31
- export declare function digitCount(num: number | Stringifiable): number;
30
+ /**
31
+ * Calculates the amount of digits in the given number - the given number or string will be passed to the `Number()` constructor.
32
+ * Returns NaN if the number is invalid.
33
+ * @param num The number to count the digits of
34
+ * @param withDecimals Whether to count the decimal places as well (defaults to true)
35
+ * @example ```ts
36
+ * digitCount(); // NaN
37
+ * digitCount(0); // 1
38
+ * digitCount(123); // 3
39
+ * digitCount(123.456); // 6
40
+ * digitCount(Infinity); // Infinity
41
+ * ```
42
+ */
43
+ export declare function digitCount(num: number | Stringifiable, withDecimals?: boolean): number;
44
+ /**
45
+ * Rounds {@linkcode num} to a fixed amount of decimal places, specified by {@linkcode fractionDigits} (supports negative values to round to the nearest power of 10).
46
+ * @example ```ts
47
+ * roundFixed(234.567, -2); // 200
48
+ * roundFixed(234.567, -1); // 230
49
+ * roundFixed(234.567, 0); // 235
50
+ * roundFixed(234.567, 1); // 234.6
51
+ * roundFixed(234.567, 2); // 234.57
52
+ * roundFixed(234.567, 3); // 234.567
53
+ * ```
54
+ */
55
+ export declare function roundFixed(num: number, fractionDigits: number): number;
56
+ /**
57
+ * Checks if the {@linkcode bitSet} has the {@linkcode checkVal} set
58
+ * @example ```ts
59
+ * // the two vertically adjacent bits are tested for:
60
+ * bitSetHas(
61
+ * 0b1110,
62
+ * 0b0010,
63
+ * ); // true
64
+ *
65
+ * bitSetHas(
66
+ * 0b1110,
67
+ * 0b0001,
68
+ * ); // false
69
+ *
70
+ * // with TS enums (or JS maps):
71
+ * enum MyEnum {
72
+ * A = 1, B = 2, C = 4,
73
+ * D = 8, E = 16, F = 32,
74
+ * }
75
+ *
76
+ * const myBitSet = MyEnum.A | MyEnum.B;
77
+ * bitSetHas(myBitSet, MyEnum.B); // true
78
+ * bitSetHas(myBitSet, MyEnum.F); // false
79
+ * ```
80
+ */
81
+ export declare function bitSetHas<TType extends number | bigint>(bitSet: TType, checkVal: TType): boolean;
@@ -120,35 +120,42 @@ declare function getFallbackLanguage(): string | undefined;
120
120
  * After all %n-formatted values have been injected, the transform functions will be called sequentially in the order they were added.
121
121
  * @example
122
122
  * ```ts
123
- * tr.addTranslations("en", {
124
- * "greeting": {
125
- * "with_username": "Hello, ${USERNAME}",
126
- * "headline_html": "Hello, ${USERNAME}<br><c=red>You have ${UNREAD_NOTIFS} unread notifications.</c>"
123
+ * import { tr, type TrKeys } from "@sv443-network/userutils";
124
+ *
125
+ * const transEn = {
126
+ * "headline": {
127
+ * "basic": "Hello, ${USERNAME}",
128
+ * "html": "Hello, ${USERNAME}<br><c=red>You have ${UNREAD_NOTIFS} unread notifications.</c>"
127
129
  * }
128
- * });
130
+ * } as const;
131
+ *
132
+ * tr.addTranslations("en", transEn);
129
133
  *
130
- * // replace ${PATTERN}
131
- * tr.addTransform(/<\$([A-Z_]+)>/g, ({ matches }) => {
134
+ * // replace ${PATTERN} with predefined values
135
+ * tr.addTransform(/\$\{([A-Z_]+)\}/g, ({ matches }) => {
132
136
  * switch(matches?.[1]) {
133
- * default: return "<UNKNOWN_PATTERN>";
134
- * // these would be grabbed from elsewhere in the application:
135
- * case "USERNAME": return "JohnDoe45";
136
- * case "UNREAD_NOTIFS": return 5;
137
+ * default:
138
+ * return `[UNKNOWN: ${matches?.[1]}]`;
139
+ * // these would be grabbed from elsewhere in the application, like a DataStore, global state or variable:
140
+ * case "USERNAME":
141
+ * return "JohnDoe45";
142
+ * case "UNREAD_NOTIFS":
143
+ * return 5;
137
144
  * }
138
145
  * });
139
146
  *
140
- * // replace <c=red>...</c> with <span class="color red">...</span>
147
+ * // replace <c=red>...</c> with <span style="color: red;">...</span>
141
148
  * tr.addTransform(/<c=([a-z]+)>(.*?)<\/c>/g, ({ matches }) => {
142
149
  * const color = matches?.[1];
143
150
  * const content = matches?.[2];
144
151
  *
145
- * return "<span class=\"color " + color + "\">" + content + "</span>";
152
+ * return `<span style="color: ${color};">${content}</span>`;
146
153
  * });
147
154
  *
148
- * tr.setLanguage("en");
155
+ * const t = tr.use<TrKeys<typeof transEn>>("en");
149
156
  *
150
- * tr("greeting.with_username"); // "Hello, JohnDoe45"
151
- * tr("greeting.headline"); // "<b>Hello, JohnDoe45</b>\nYou have 5 unread notifications."
157
+ * t("headline.basic"); // "Hello, JohnDoe45"
158
+ * t("headline.html"); // "Hello, JohnDoe45<br><span style="color: red;">You have 5 unread notifications.</span>"
152
159
  * ```
153
160
  * @param args A tuple containing the regular expression to match and the transform function to call if the pattern is found in a translation string
154
161
  */
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@sv443-network/userutils",
3
3
  "libName": "UserUtils",
4
- "version": "9.1.0",
5
- "description": "Lightweight library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and much more",
4
+ "version": "9.2.0",
5
+ "description": "General purpose DOM/GreaseMonkey library that allows you to register listeners for when CSS selectors exist, intercept events, create persistent & synchronous data stores, modify the DOM more easily and much more",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.js",
8
8
  "types": "dist/lib/index.d.ts",