@sv443-network/userutils 7.1.0 → 7.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/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  <div style="text-align: center;" align="center">
2
2
 
3
- <!-- #MARKER Description -->
3
+ <!-- #region Description -->
4
4
  ## UserUtils
5
5
  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 more.
6
6
 
7
- Contains builtin TypeScript declarations. Fully web compatible and supports ESM and CJS imports and global declaration.
7
+ Contains builtin TypeScript declarations. Supports ESM and CJS imports via a bundler and UMD / global declaration via `@require`.
8
8
  If you like using this library, please consider [supporting the development ❤️](https://github.com/sponsors/Sv443)
9
9
 
10
10
  <br>
@@ -19,7 +19,7 @@ View the documentation of previous major releases:
19
19
  </div>
20
20
  <br>
21
21
 
22
- <!-- #MARKER Table of Contents -->
22
+ <!-- #region Table of Contents -->
23
23
  ## Table of Contents:
24
24
  - [**Installation**](#installation)
25
25
  - [**Preamble** (info about the documentation)](#preamble)
@@ -37,6 +37,7 @@ View the documentation of previous major releases:
37
37
  - [`isScrollable()`](#isscrollable) - check if an element has a horizontal or vertical scroll bar
38
38
  - [`observeElementProp()`](#observeelementprop) - observe changes to an element's property that can't be observed with MutationObserver
39
39
  - [`getSiblingsFrame()`](#getsiblingsframe) - returns a frame of an element's siblings, with a given alignment and size
40
+ - [`setInnerHtmlUnsafe()`](#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
40
41
  - [**Math:**](#math)
41
42
  - [`clamp()`](#clamp) - constrain a number between a min and max value
42
43
  - [`mapRange()`](#maprange) - map a number from one range to the same spot in another range
@@ -65,6 +66,11 @@ View the documentation of previous major releases:
65
66
  - [`tr.addLanguage()`](#traddlanguage) - add a language and its translations
66
67
  - [`tr.setLanguage()`](#trsetlanguage) - set the currently active language for translations
67
68
  - [`tr.getLanguage()`](#trgetlanguage) - returns the currently active language
69
+ - [**Colors:**](#colors)
70
+ - [`hexToRgb()`](#hextorgb) - convert a hex color string to an RGB number tuple
71
+ - [`rgbToHex()`](#rgbtohex) - convert RGB numbers to a hex color string
72
+ - [`lightenColor()`](#lightencolor) - lighten a CSS color string (hex, rgb or rgba) by a given percentage
73
+ - [`darkenColor()`](#darkencolor) - darken a CSS color string (hex, rgb or rgba) by a given percentage
68
74
  - [**Utility types for TypeScript:**](#utility-types)
69
75
  - [`Stringifiable`](#stringifiable) - any value that is a string or can be converted to one (implicitly or explicitly)
70
76
  - [`NonEmptyArray`](#nonemptyarray) - any array that should have at least one item
@@ -73,12 +79,15 @@ View the documentation of previous major releases:
73
79
 
74
80
  <br><br>
75
81
 
76
- <!-- #MARKER Installation -->
82
+ <!-- #region Installation -->
77
83
  ## Installation:
78
- - If you are using a bundler like webpack, you can install this package using npm:
84
+ Shameless plug: I made a [template for userscripts in TypeScript](https://github.com/Sv443/Userscript.ts) that you can use to get started quickly. It also includes this library by default.
85
+
86
+ - If you are using a bundler (like webpack, rollup, vite, etc.), you can install this package using npm:
79
87
  ```
80
88
  npm i @sv443-network/userutils
81
89
  ```
90
+ <sup>For other package managers, check out the respective install command on the [JavaScript Registry](https://jsr.io/@sv443-network/userutils)</sup>
82
91
  Then, import it in your script as usual:
83
92
  ```ts
84
93
  import { addGlobalStyle } from "@sv443-network/userutils";
@@ -87,18 +96,17 @@ View the documentation of previous major releases:
87
96
 
88
97
  import * as UserUtils from "@sv443-network/userutils";
89
98
  ```
90
- Shameless plug: I made a [template for userscripts in TypeScript](https://github.com/Sv443/Userscript.ts) that you can use to get started quickly. It also includes this library by default.
91
99
 
92
100
  <br>
93
101
 
94
- - If you are not using a bundler, you can include the latest release by adding one of these directives to the userscript header, depending on your preferred CDN:
102
+ - If you are not using a bundler or want to reduce the size of your userscript, you can include the latest release by adding one of these directives to the userscript header, depending on your preferred CDN:
95
103
  ```
96
104
  // @require https://greasyfork.org/scripts/472956-userutils/code/UserUtils.js
97
105
  ```
98
106
  ```
99
107
  // @require https://openuserjs.org/src/libs/Sv443/UserUtils.js
100
108
  ```
101
- (in order for your userscript not to break on a major library update, use the versioned URL at the top of the [GreasyFork page](https://greasyfork.org/scripts/472956-userutils))
109
+ (in order for your userscript not to break on a major library update, instead use the versioned URL at the top of the [GreasyFork page](https://greasyfork.org/scripts/472956-userutils))
102
110
 
103
111
  Then, access the functions on the global variable `UserUtils`:
104
112
  ```ts
@@ -109,10 +117,16 @@ View the documentation of previous major releases:
109
117
  const { clamp } = UserUtils;
110
118
  console.log(clamp(1, 5, 10)); // 5
111
119
  ```
120
+ If you're using TypeScript and it complains about the missing global variable `UserUtils`, install the library using the package manager of your choice and add the following inside a `.d.ts` file somewhere in your project:
121
+ ```ts
122
+ declare global {
123
+ const UserUtils: typeof import("@sv443-network/userutils");
124
+ }
125
+ ```
112
126
 
113
127
  <br><br>
114
128
 
115
- <!-- #MARKER Preamble -->
129
+ <!-- #region Preamble -->
116
130
  ## Preamble:
117
131
  This library is written in TypeScript and contains builtin TypeScript declarations.
118
132
 
@@ -125,19 +139,19 @@ Their documentation will contain a section marked by a warning emoji (⚠️) th
125
139
 
126
140
  <br><br>
127
141
 
128
- <!-- #MARKER License -->
142
+ <!-- #region License -->
129
143
  ## License:
130
144
  This library is licensed under the MIT License.
131
145
  See the [license file](./LICENSE.txt) for details.
132
146
 
133
147
  <br><br>
134
148
 
135
- <!-- #MARKER Features -->
149
+ <!-- #region Features -->
136
150
  ## Features:
137
151
 
138
152
  <br>
139
153
 
140
- <!-- #SECTION DOM -->
154
+ <!-- #region DOM -->
141
155
  ## DOM:
142
156
 
143
157
  ### SelectorObserver
@@ -437,10 +451,8 @@ document.addEventListener("DOMContentLoaded", () => {
437
451
  fooObserver.enable();
438
452
  });
439
453
  ```
440
-
441
454
  </details>
442
455
 
443
-
444
456
  <br>
445
457
 
446
458
  ### getUnsafeWindow()
@@ -468,7 +480,6 @@ const mouseEvent = new MouseEvent("mousemove", {
468
480
 
469
481
  document.body.dispatchEvent(mouseEvent);
470
482
  ```
471
-
472
483
  </details>
473
484
 
474
485
  <br>
@@ -496,7 +507,6 @@ newParent.href = "https://example.org/";
496
507
 
497
508
  addParent(element, newParent);
498
509
  ```
499
-
500
510
  </details>
501
511
 
502
512
  <br>
@@ -524,7 +534,6 @@ document.addEventListener("DOMContentLoaded", () => {
524
534
  `);
525
535
  });
526
536
  ```
527
-
528
537
  </details>
529
538
 
530
539
  <br>
@@ -556,7 +565,6 @@ preloadImages([
556
565
  console.error("Couldn't preload all images. Results:", results);
557
566
  });
558
567
  ```
559
-
560
568
  </details>
561
569
 
562
570
  <br>
@@ -583,7 +591,6 @@ document.querySelector("#my-button").addEventListener("click", () => {
583
591
  openInNewTab("https://example.org/", true);
584
592
  });
585
593
  ```
586
-
587
594
  </details>
588
595
 
589
596
  <br>
@@ -618,7 +625,6 @@ interceptEvent(document.body, "click", (event) => {
618
625
  return false; // allow all other click events through
619
626
  });
620
627
  ```
621
-
622
628
  </details>
623
629
 
624
630
  <br>
@@ -648,7 +654,6 @@ import { interceptWindowEvent } from "@sv443-network/userutils";
648
654
  // as no predicate is specified, all events will be discarded by default
649
655
  interceptWindowEvent("beforeunload");
650
656
  ```
651
-
652
657
  </details>
653
658
 
654
659
  <br>
@@ -673,7 +678,6 @@ const { horizontal, vertical } = isScrollable(element);
673
678
  console.log("Element has a horizontal scroll bar:", horizontal);
674
679
  console.log("Element has a vertical scroll bar:", vertical);
675
680
  ```
676
-
677
681
  </details>
678
682
 
679
683
  <br>
@@ -726,7 +730,6 @@ observeElementProp(myInput, "value", (oldValue, newValue) => {
726
730
  console.log("Value changed from", oldValue, "to", newValue);
727
731
  });
728
732
  ```
729
-
730
733
  </details>
731
734
 
732
735
  <br>
@@ -849,12 +852,42 @@ const allBelowExcl = getSiblingsFrame(refElement, Infinity, "bottom", false);
849
852
  // <div>5</div> │ frame
850
853
  // <div>6</div> ◄──┘
851
854
  ```
855
+ </details>
856
+
857
+ <br>
858
+
859
+ ### setInnerHtmlUnsafe()
860
+ Usage:
861
+ ```ts
862
+ setInnerHtmlUnsafe(element: Element, html: string): Element
863
+ ```
864
+
865
+ Sets the innerHTML property of the provided element without any sanitation or validation.
866
+ Makes use of the [Trusted Types API](https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API) to trick the browser into thinking the HTML is safe.
867
+ Use this function if the page makes use of the CSP directive `require-trusted-types-for 'script'` and throws a "This document requires 'TrustedHTML' assignment" error on Chromium-based browsers.
868
+ If the browser doesn't support Trusted Types, this function will fall back to regular innerHTML assignment.
869
+
870
+ ⚠️ This function does not perform any sanitization, it only tricks the browser into thinking the HTML is safe and should thus be used with utmost caution, as it can easily cause XSS vulnerabilities!
871
+ A much better way of doing this is by using the [DOMPurify](https://github.com/cure53/DOMPurify#what-about-dompurify-and-trusted-types) library to create your own Trusted Types policy that *actually* sanitizes the HTML and prevents (most) XSS attack vectors.
872
+ You can also find more info [here.](https://web.dev/articles/trusted-types#library)
873
+
874
+ <details><summary><b>Example - click to view</b></summary>
875
+
876
+ ```ts
877
+ import { setInnerHtmlUnsafe } from "@sv443-network/userutils";
878
+
879
+ const myElement = document.querySelector("#my-element");
880
+ setInnerHtmlUnsafe(myElement, "<img src='https://picsum.photos/100/100' />"); // hardcoded value, so no XSS risk
852
881
 
882
+ const myXssElement = document.querySelector("#my-xss-element");
883
+ const userModifiableVariable = `<img onerror="alert('XSS!')" src="invalid" />`; // let's pretend this came from user input
884
+ setInnerHtmlUnsafe(myXssElement, userModifiableVariable); // <- uses a user-modifiable variable, so big XSS risk!
885
+ ```
853
886
  </details>
854
887
 
855
888
  <br><br>
856
889
 
857
- <!-- #SECTION Math -->
890
+ <!-- #region Math -->
858
891
  ## Math:
859
892
 
860
893
  ### clamp()
@@ -879,7 +912,6 @@ clamp(99999, 0, 10); // 10
879
912
  clamp(-99999, -Infinity, 0); // -99999
880
913
  clamp(99999, 0, Infinity); // 99999
881
914
  ```
882
-
883
915
  </details>
884
916
 
885
917
  <br>
@@ -910,7 +942,6 @@ mapRange(5, 0, 10, 0, 50); // 25
910
942
  // for example, if 4 files of a total of 13 were downloaded:
911
943
  mapRange(4, 0, 13, 0, 100); // 30.76923076923077
912
944
  ```
913
-
914
945
  </details>
915
946
 
916
947
  <br>
@@ -934,12 +965,11 @@ randRange(0, 10); // 4
934
965
  randRange(10, 20); // 17
935
966
  randRange(10); // 7
936
967
  ```
937
-
938
968
  </details>
939
969
 
940
970
  <br><br>
941
971
 
942
- <!-- #SECTION Misc -->
972
+ <!-- #region Misc -->
943
973
  ## Misc:
944
974
 
945
975
  ### DataStore
@@ -1102,7 +1132,6 @@ async function init() {
1102
1132
 
1103
1133
  init();
1104
1134
  ```
1105
-
1106
1135
  </details>
1107
1136
 
1108
1137
  <br>
@@ -1110,7 +1139,7 @@ init();
1110
1139
  ### DataStoreSerializer
1111
1140
  Usage:
1112
1141
  ```ts
1113
- new DataStoreSerializer(stores: DataStore[], options: DataStoreSerializerOptions)
1142
+ new DataStoreSerializer(stores: DataStore[], options?: DataStoreSerializerOptions)
1114
1143
  ```
1115
1144
 
1116
1145
  A class that manages serializing and deserializing (exporting and importing) one to infinite DataStore instances.
@@ -1203,8 +1232,13 @@ const serializer = new DataStoreSerializer([fooStore, barStore], {
1203
1232
  });
1204
1233
 
1205
1234
  async function exportMyDataPls() {
1235
+ // first, make sure the persistent data of the stores is loaded into their caches:
1236
+ await fooStore.loadData();
1237
+ await barStore.loadData();
1238
+
1239
+ // now serialize the data:
1206
1240
  const serializedData = await serializer.serialize();
1207
- // create file and download it:
1241
+ // create a file and download it:
1208
1242
  const blob = new Blob([serializedData], { type: "application/json" });
1209
1243
  const url = URL.createObjectURL(blob);
1210
1244
  const a = document.createElement("a");
@@ -1213,7 +1247,7 @@ async function exportMyDataPls() {
1213
1247
  a.click();
1214
1248
  a.remove();
1215
1249
 
1216
- // This function exports an object like this:
1250
+ // `serialize()` exports a stringified object that looks similar to this:
1217
1251
  // [
1218
1252
  // {
1219
1253
  // "id": "foo-data",
@@ -1358,7 +1392,6 @@ fooDialog.on("close", () => {
1358
1392
 
1359
1393
  fooDialog.open();
1360
1394
  ```
1361
-
1362
1395
  </details>
1363
1396
 
1364
1397
  <br>
@@ -1372,13 +1405,13 @@ new NanoEmitter<TEventMap = EventsMap>(options?: NanoEmitterOptions): NanoEmitte
1372
1405
  A class that provides a minimalistic event emitter with a tiny footprint powered by [nanoevents.](https://npmjs.com/package/nanoevents)
1373
1406
  The `TEventMap` generic is used to define the events that can be emitted and listened to.
1374
1407
 
1375
- The intention behind this class is to extend it in your own classes to provide a simple event system.
1376
- You can also just create an instance and export it to use it as standalone event emitters throughout your project.
1408
+ The main intention behind this class is to extend it in your own classes to provide a simple event system directly built into the class.
1409
+ However in a functional environment you can also just create instances for use as standalone event emitters throughout your project.
1377
1410
 
1378
1411
  The options object has the following properties:
1379
1412
  | Property | Description |
1380
1413
  | :-- | :-- |
1381
- | `publicEmit?: boolean` | (Optional) If set to true, allows emitting events through the public method `emit()` (`false` by default). |
1414
+ | `publicEmit?: boolean` | (Optional) If set to true, allows emitting events through the public method `emit()` (`false` by default). |
1382
1415
 
1383
1416
  Methods:
1384
1417
  `on<K extends keyof TEventMap>(event: K, listener: TEventMap[K]): void`
@@ -1390,8 +1423,8 @@ Registers a listener function for the given event that will only be called once.
1390
1423
 
1391
1424
  `emit<K extends keyof TEventMap>(event: K, ...args: Parameters<TEventMap[K]>): boolean`
1392
1425
  Emits an event with the given arguments from outside the class instance if `publicEmit` is set to `true`.
1393
- If `publicEmit` is set to `true`, this method is public and can be called from outside the class and will return `true`.
1394
- If not, it is protected and will return `false`.
1426
+ If `publicEmit` is set to `true`, this method will return `true` if the event was emitted.
1427
+ If it is set to `false`, it will always return `false` and you will need to use `this.events.emit()` from inside the class instead.
1395
1428
 
1396
1429
  `unsubscribeAll(): void`
1397
1430
  Removes all listeners from all events.
@@ -1403,6 +1436,7 @@ Removes all listeners from all events.
1403
1436
  ```ts
1404
1437
  import { NanoEmitter } from "@sv443-network/userutils";
1405
1438
 
1439
+ // map of events for strong typing - the functions always return void
1406
1440
  interface MyEventMap {
1407
1441
  foo: (bar: string) => void;
1408
1442
  baz: (qux: number) => void;
@@ -1414,8 +1448,9 @@ class MyClass extends NanoEmitter<MyEventMap> {
1414
1448
  // allow emitting events from outside the class
1415
1449
  publicEmit: true,
1416
1450
  });
1451
+
1417
1452
  this.once("baz", (qux) => {
1418
- console.log("baz event (inside):", qux);
1453
+ console.log("baz event (inside, once):", qux);
1419
1454
  });
1420
1455
  }
1421
1456
 
@@ -1438,7 +1473,6 @@ myInstance.emit("baz", "hello from the outside");
1438
1473
 
1439
1474
  myInstance.unsubscribeAll();
1440
1475
  ```
1441
-
1442
1476
  </details>
1443
1477
 
1444
1478
  <br>
@@ -1448,6 +1482,7 @@ myInstance.unsubscribeAll();
1448
1482
  ```ts
1449
1483
  import { NanoEmitter } from "@sv443-network/userutils";
1450
1484
 
1485
+ // map of events for strong typing - the functions always return void
1451
1486
  interface MyEventMap {
1452
1487
  foo: (bar: string) => void;
1453
1488
  baz: (qux: number) => void;
@@ -1463,7 +1498,7 @@ myEmitter.on("foo", (bar) => {
1463
1498
  });
1464
1499
 
1465
1500
  myEmitter.once("baz", (qux) => {
1466
- console.log("baz event:", qux);
1501
+ console.log("baz event (once):", qux);
1467
1502
  });
1468
1503
 
1469
1504
  function doStuff() {
@@ -1507,7 +1542,6 @@ autoPlural("apple", [1, 2]); // "apples"
1507
1542
  const items = [1, 2, 3, 4, "foo", "bar"];
1508
1543
  console.log(`Found ${items.length} ${autoPlural("item", items)}`); // "Found 6 items"
1509
1544
  ```
1510
-
1511
1545
  </details>
1512
1546
 
1513
1547
  <br>
@@ -1531,7 +1565,6 @@ async function run() {
1531
1565
  console.log("World");
1532
1566
  }
1533
1567
  ```
1534
-
1535
1568
  </details>
1536
1569
 
1537
1570
  <br>
@@ -1577,7 +1610,6 @@ const myFunc = debounce((event) => {
1577
1610
 
1578
1611
  document.body.addEventListener("scroll", myFunc);
1579
1612
  ```
1580
-
1581
1613
  </details>
1582
1614
 
1583
1615
  <br>
@@ -1612,7 +1644,6 @@ fetchAdvanced("https://jokeapi.dev/joke/Any?safe-mode", {
1612
1644
  console.error("Fetch error:", err);
1613
1645
  });
1614
1646
  ```
1615
-
1616
1647
  </details>
1617
1648
 
1618
1649
  <br>
@@ -1640,7 +1671,6 @@ insertValues("Testing %1", { toString: () => "foo" }); // "Testing foo"
1640
1671
  const values = ["foo", "bar", "baz"];
1641
1672
  insertValues("Testing %1, %2, %3 and %4", ...values); // "Testing foo, bar and baz and %4"
1642
1673
  ```
1643
-
1644
1674
  </details>
1645
1675
 
1646
1676
  <br>
@@ -1681,7 +1711,6 @@ const barDeflate = await compress("Hello, World!".repeat(20), "deflate");
1681
1711
  console.log(fooDeflate); // "eJzzSM3JyddRCM8vyklRBAAfngRq"
1682
1712
  console.log(barDeflate); // "eJzzSM3JyddRCM8vyklR9BiZHAAIEVg1"
1683
1713
  ```
1684
-
1685
1714
  </details>
1686
1715
 
1687
1716
  <br>
@@ -1712,7 +1741,6 @@ const decompressed = await decompress(compressed, "gzip");
1712
1741
 
1713
1742
  console.log(decompressed); // "Hello, World!"
1714
1743
  ```
1715
-
1716
1744
  </details>
1717
1745
 
1718
1746
  <br>
@@ -1779,12 +1807,11 @@ randomId(10, 2); // "1010001101" (length 10, radix 2)
1779
1807
  randomId(10, 10); // "0183428506" (length 10, radix 10)
1780
1808
  randomId(10, 36); // "z46jfpa37r" (length 10, radix 36)
1781
1809
  ```
1782
-
1783
1810
  </details>
1784
1811
 
1785
1812
  <br><br>
1786
1813
 
1787
- <!-- #SECTION Arrays -->
1814
+ <!-- #region Arrays -->
1788
1815
  ## Arrays:
1789
1816
 
1790
1817
  ### randomItem()
@@ -1804,7 +1831,6 @@ import { randomItem } from "@sv443-network/userutils";
1804
1831
  randomItem(["foo", "bar", "baz"]); // "bar"
1805
1832
  randomItem([ ]); // undefined
1806
1833
  ```
1807
-
1808
1834
  </details>
1809
1835
 
1810
1836
  <br>
@@ -1831,7 +1857,6 @@ const [item, index] = randomItemIndex(["foo", "bar", "baz"]); // ["bar", 1]
1831
1857
  // or if you only want the index:
1832
1858
  const [, index] = randomItemIndex(["foo", "bar", "baz"]); // 1
1833
1859
  ```
1834
-
1835
1860
  </details>
1836
1861
 
1837
1862
  <br>
@@ -1854,7 +1879,6 @@ const arr = ["foo", "bar", "baz"];
1854
1879
  takeRandomItem(arr); // "bar"
1855
1880
  console.log(arr); // ["foo", "baz"]
1856
1881
  ```
1857
-
1858
1882
  </details>
1859
1883
 
1860
1884
  <br>
@@ -1880,12 +1904,11 @@ console.log(randomizeArray(foo)); // [4, 5, 2, 1, 6, 3]
1880
1904
 
1881
1905
  console.log(foo); // [1, 2, 3, 4, 5, 6] - original array is not mutated
1882
1906
  ```
1883
-
1884
1907
  </details>
1885
1908
 
1886
1909
  <br><br>
1887
1910
 
1888
- <!-- #SECTION Translation -->
1911
+ <!-- #region Translation -->
1889
1912
  ## Translation:
1890
1913
  This is a very lightweight translation function that can be used to translate simple strings.
1891
1914
  Pluralization is not supported but can be achieved manually by adding variations to the translations, identified by a different suffix. See the example section of [`tr.addLanguage()`](#traddlanguage) for an example on how this might be done.
@@ -1933,7 +1956,6 @@ tr.setLanguage("de");
1933
1956
  console.log(tr("welcome")); // "Willkommen"
1934
1957
  console.log(tr("welcome_name", "John")); // "Willkommen, John"
1935
1958
  ```
1936
-
1937
1959
  </details>
1938
1960
 
1939
1961
  <br>
@@ -2005,34 +2027,33 @@ tr.addLanguage("de-AT", {
2005
2027
  // example for custom pluralization:
2006
2028
 
2007
2029
  tr.addLanguage("en", {
2008
- "items_added-0": "Added %1 items to your cart",
2009
- "items_added-1": "Added %1 item to your cart",
2010
- "items_added-n": "Added all %1 items to your cart",
2030
+ "cart_items_added-0": "No items were added to the cart",
2031
+ "cart_items_added-1": "Added %1 item to the cart",
2032
+ "cart_items_added-n": "Added %1 items to the cart",
2011
2033
  });
2012
2034
 
2013
- /** Returns the custom pluralization identifier for the given number of items (or size of Array/NodeList) */
2014
- function pl(num: number | unknown[] | NodeList) {
2035
+ /** Returns the translation key with a custom pluralization identifier added to it for the given number of items (or size of Array/NodeList or anything else with a `length` property) */
2036
+ function pl(key: string, num: number | Array<unknown> | NodeList | { length: number }) {
2015
2037
  if(typeof num !== "number")
2016
2038
  num = num.length;
2017
2039
 
2018
2040
  if(num === 0)
2019
- return "0";
2041
+ return `${key}-0`;
2020
2042
  else if(num === 1)
2021
- return "1";
2043
+ return `${key}-1`;
2022
2044
  else
2023
- return "n";
2045
+ return `${key}-n`;
2024
2046
  };
2025
2047
 
2026
2048
  const items = [];
2027
- tr(`items_added-${pl(items)}`, items.length); // "Added 0 items to your cart"
2049
+ console.log(tr(pl("cart_items_added", items), items.length)); // "No items were added to the cart"
2028
2050
 
2029
2051
  items.push("foo");
2030
- tr(`items_added-${pl(items)}`, items.length); // "Added 1 item to your cart"
2052
+ console.log(tr(pl("cart_items_added", items), items.length)); // "Added 1 item to the cart"
2031
2053
 
2032
2054
  items.push("bar");
2033
- tr(`items_added-${pl(items)}`, items.length); // "Added all 2 items to your cart"
2055
+ console.log(tr(pl("cart_items_added", items), items.length)); // "Added 2 items to the cart"
2034
2056
  ```
2035
-
2036
2057
  </details>
2037
2058
 
2038
2059
  <br>
@@ -2061,7 +2082,101 @@ If no language has been set yet, it will return undefined.
2061
2082
 
2062
2083
  <br><br>
2063
2084
 
2064
- <!-- #SECTION Utility types -->
2085
+ ## Colors:
2086
+ The color functions are used to manipulate and convert colors in various formats.
2087
+
2088
+ ### hexToRgb()
2089
+ Usage:
2090
+ ```ts
2091
+ hexToRgb(hex: string): [red: number, green: number, blue: number]
2092
+ ```
2093
+
2094
+ Converts a hex color string to an RGB color tuple array.
2095
+ Accepts the formats `#RRGGBB` and `#RGB`, with or without the hash symbol.
2096
+
2097
+ <details><summary><b>Example - click to view</b></summary>
2098
+
2099
+ ```ts
2100
+ import { hexToRgb } from "@sv443-network/userutils";
2101
+
2102
+ hexToRgb("#ff0000"); // [255, 0, 0]
2103
+ hexToRgb("0032ef"); // [0, 50, 239]
2104
+ hexToRgb("#0f0"); // [0, 255, 0]
2105
+ ```
2106
+ </details>
2107
+
2108
+ <br>
2109
+
2110
+ ### rgbToHex()
2111
+ Usage:
2112
+ ```ts
2113
+ rgbToHex(red: number, green: number, blue: number, withHash?: boolean, upperCase?: boolean): string
2114
+ ```
2115
+
2116
+ Converts RGB color values to a hex color string.
2117
+ The `withHash` parameter determines if the hash symbol should be included in the output (true by default).
2118
+ The `upperCase` parameter determines if the output should be in uppercase (false by default).
2119
+
2120
+ <details><summary><b>Example - click to view</b></summary>
2121
+
2122
+ ```ts
2123
+ import { rgbToHex } from "@sv443-network/userutils";
2124
+
2125
+ rgbToHex(255, 0, 0); // "#ff0000"
2126
+ rgbToHex(255, 0, 0, false); // "ff0000"
2127
+ rgbToHex(255, 0, 0, true, true); // "#FF0000"
2128
+ ```
2129
+ </details>
2130
+
2131
+ <br>
2132
+
2133
+ ### lightenColor()
2134
+ Usage:
2135
+ ```ts
2136
+ lightenColor(color: string, percent: number): string
2137
+ ```
2138
+
2139
+ Lightens a CSS color value (in hex, RGB or RGBA format) by a given percentage.
2140
+ Will not exceed the maximum range (00-FF or 0-255).
2141
+ Throws an error if the color format is invalid or not supported.
2142
+
2143
+ <details><summary><b>Example - click to view</b></summary>
2144
+
2145
+ ```ts
2146
+ import { lightenColor } from "@sv443-network/userutils";
2147
+
2148
+ lightenColor("#ff0000", 20); // "#ff3333"
2149
+ lightenColor("rgb(0, 255, 0)", 50); // "rgb(128, 255, 128)"
2150
+ lightenColor("rgba(0, 255, 0, 0.5)", 50); // "rgba(128, 255, 128, 0.5)"
2151
+ ```
2152
+ </details>
2153
+
2154
+ <br>
2155
+
2156
+ ### darkenColor()
2157
+ Usage:
2158
+ ```ts
2159
+ darkenColor(color: string, percent: number): string
2160
+ ```
2161
+
2162
+ Darkens a CSS color value (in hex, RGB or RGBA format) by a given percentage.
2163
+ Will not exceed the maximum range (00-FF or 0-255).
2164
+ Throws an error if the color format is invalid or not supported.
2165
+
2166
+ <details><summary><b>Example - click to view</b></summary>
2167
+
2168
+ ```ts
2169
+ import { darkenColor } from "@sv443-network/userutils";
2170
+
2171
+ darkenColor("#ff0000", 20); // "#cc0000"
2172
+ darkenColor("rgb(0, 255, 0)", 50); // "rgb(0, 128, 0)"
2173
+ darkenColor("rgba(0, 255, 0, 0.5)", 50); // "rgba(0, 128, 0, 0.5)"
2174
+ ```
2175
+ </details>
2176
+
2177
+ <br><br>
2178
+
2179
+ <!-- #region Utility types -->
2065
2180
  ## Utility types:
2066
2181
  UserUtils also offers some utility types that can be used in TypeScript projects.
2067
2182
  They don't alter the runtime behavior of the code, but they can be used to make the code more readable and to prevent errors.
@@ -2097,7 +2212,6 @@ logSomething(fooObject); // "Log: hello world"
2097
2212
 
2098
2213
  logSomething(barObject); // Type error
2099
2214
  ```
2100
-
2101
2215
  </details>
2102
2216
 
2103
2217
  <br>
@@ -2128,7 +2242,6 @@ function somethingElse(array: NonEmptyArray) {
2128
2242
 
2129
2243
  logFirstItem(["04abc", "69"]); // 4
2130
2244
  ```
2131
-
2132
2245
  </details>
2133
2246
 
2134
2247
  <br>
@@ -2153,7 +2266,6 @@ function convertToNumber<T extends string>(str: NonEmptyString<T>) {
2153
2266
  convertToNumber("04abc"); // "4"
2154
2267
  convertToNumber(""); // type error: Argument of type 'string' is not assignable to parameter of type 'never'
2155
2268
  ```
2156
-
2157
2269
  </details>
2158
2270
 
2159
2271
  <br>
@@ -2181,12 +2293,11 @@ foo("a"); // included in autocomplete, no type error
2181
2293
  foo(""); // *not* included in autocomplete, still no type error
2182
2294
  foo(1); // type error: Argument of type '1' is not assignable to parameter of type 'LooseUnion<"a" | "b" | "c">'
2183
2295
  ```
2184
-
2185
2296
  </details>
2186
2297
 
2187
2298
  <br><br><br><br>
2188
2299
 
2189
- <!-- #MARKER Footer -->
2300
+ <!-- #region Footer -->
2190
2301
  <div style="text-align: center;" align="center">
2191
2302
 
2192
2303
  Made with ❤️ by [Sv443](https://github.com/Sv443)