@keeex/utils 7.5.0 → 7.6.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/lib/array.d.ts CHANGED
@@ -26,3 +26,5 @@ export declare const arrayEqual: <T1 = unknown, T2 = unknown>(op1: ArrayLike<T1>
26
26
  * @public
27
27
  */
28
28
  export declare const asArray: <T>(data: Array<T> | (T extends Array<unknown> ? never : T)) => Array<T>;
29
+ /** Return a new array with its string sorted */
30
+ export declare const toSortedStringArray: (array: Array<string>) => Array<string>;
package/lib/array.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
14
  *
15
15
  */
16
+ import { alphaSort } from "./string.js";
16
17
  const useEqOp = (op1, op2) => op1 === op2;
17
18
  /**
18
19
  * Compare two array-like structure for equal content
@@ -34,3 +35,5 @@ export const arrayEqual = (op1, op2, eqFunc = useEqOp) => {
34
35
  * @public
35
36
  */
36
37
  export const asArray = (data) => Array.isArray(data) ? data : [data];
38
+ /** Return a new array with its string sorted */
39
+ export const toSortedStringArray = (array) => array.toSorted(alphaSort);
package/lib/dict.d.ts CHANGED
@@ -54,3 +54,35 @@ export type PrimitiveTypeObject = Record<string, unknown> | Array<unknown> | str
54
54
  * @public
55
55
  */
56
56
  export declare const deepCopyPrimitive: (source: unknown) => unknown;
57
+ /**
58
+ * Perform the merge operation between two values.
59
+ *
60
+ * If the new value have no changes from the initial value, this should always return `initial`.
61
+ * If there is any change, this should always return a new reference.
62
+ *
63
+ * TODO: The function returns unknown because I can't work around the requirements for TypeScript to
64
+ * be happy using `DataType | null`.
65
+ */
66
+ type RefMergeDefFn<DataType> = (initial: DataType | null, newValue: DataType | null) => unknown;
67
+ type RefMergeDef<DataType> = DataType extends string ? "string" : DataType extends number ? "number" : DataType extends boolean ? "boolean" : DataType extends Array<string> ? "string[]" : RefMergeDefFn<DataType>;
68
+ type RefMergeProfile<ObjectType> = Required<{
69
+ [key in keyof ObjectType]: RefMergeDef<NonNullable<ObjectType[key]>>;
70
+ }>;
71
+ /**
72
+ * Examine each props on both `initial` and `newValue`, and if there is any change, return a new
73
+ * reference with all values.
74
+ *
75
+ * @param initial - The source object. If no changes are detected, returns it.
76
+ * @param newValue - Values to update in `initial`. `undefined` values are skipped.
77
+ * @param profile - Describe the expected properties and how to merge them.
78
+ *
79
+ * @returns
80
+ * If there is some change from `initial`, will return a new reference with all new values.
81
+ * This property extends to values that are object; all objects in this hierarchy that have an
82
+ * update belows it is replaced with a new reference.
83
+ *
84
+ * This is made to help with libraries like React that depends on state reference actually getting
85
+ * changed on update.
86
+ */
87
+ export declare const refMergeObjects: <ObjectType>(initial: ObjectType, newValue: Partial<ObjectType>, profile: RefMergeProfile<ObjectType>) => ObjectType;
88
+ export {};
package/lib/dict.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
14
  *
15
15
  */
16
+ import { arrayEqual } from "./array.js";
16
17
  /**
17
18
  * Copy all properties from secondary into primary.
18
19
  *
@@ -72,3 +73,65 @@ export const deepCopyPrimitive = (source) => {
72
73
  return res;
73
74
  }
74
75
  };
76
+ const refMergeStringArray = (initial, newValue) => {
77
+ if (newValue === null)
78
+ return null;
79
+ if (initial !== null && arrayEqual(initial, newValue))
80
+ return initial;
81
+ return [...newValue];
82
+ };
83
+ /**
84
+ * Merge a new value and an old value according to the provided merger.
85
+ *
86
+ * If `newValue` is undefined, `initial` is always returned.
87
+ * If `newValue` is defined AND have difference from `initial`, a new object should be returned.
88
+ * If there is no change, `initial` is returned.
89
+ */
90
+ const getMergedRef = (initial, newValue, merger) => {
91
+ if (newValue === undefined)
92
+ return initial;
93
+ switch (merger) {
94
+ case "string":
95
+ case "number":
96
+ case "boolean":
97
+ return newValue;
98
+ case "string[]":
99
+ return refMergeStringArray(initial, newValue);
100
+ default: {
101
+ return merger(initial, newValue);
102
+ }
103
+ }
104
+ };
105
+ /**
106
+ * Examine each props on both `initial` and `newValue`, and if there is any change, return a new
107
+ * reference with all values.
108
+ *
109
+ * @param initial - The source object. If no changes are detected, returns it.
110
+ * @param newValue - Values to update in `initial`. `undefined` values are skipped.
111
+ * @param profile - Describe the expected properties and how to merge them.
112
+ *
113
+ * @returns
114
+ * If there is some change from `initial`, will return a new reference with all new values.
115
+ * This property extends to values that are object; all objects in this hierarchy that have an
116
+ * update belows it is replaced with a new reference.
117
+ *
118
+ * This is made to help with libraries like React that depends on state reference actually getting
119
+ * changed on update.
120
+ */
121
+ export const refMergeObjects = (initial, newValue, profile) => {
122
+ let anyChange = false;
123
+ const newValues = Object.entries(profile).map(([key, merger]) => {
124
+ const initialValue = initial[key];
125
+ const merged = getMergedRef(initialValue, newValue[key], merger);
126
+ anyChange ||= merged !== initialValue;
127
+ return { key, merged };
128
+ });
129
+ // Can't statically detect changes in map() callback
130
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
131
+ if (!anyChange)
132
+ return initial;
133
+ return newValues.reduce((acc, { key, merged }) => {
134
+ acc[key] = merged;
135
+ return acc;
136
+ }, {});
137
+ };
package/lib/string.d.ts CHANGED
@@ -63,3 +63,11 @@ export declare const encodeUTF8: (str: string) => string;
63
63
  export declare const decodeUTF8: (str: string, skipErrors?: boolean) => string;
64
64
  /** Capitalize (or uncapitalize) the first character of a string */
65
65
  export declare const capitalize: (str: string, upperFirstLetter: boolean) => string;
66
+ /** Ensure the string ends with one newline */
67
+ export declare const enforceNewlineEnd: (str: string) => string;
68
+ /** Format the text as a single line or multiline JS comment */
69
+ export declare const jsComment: (text: string, jsDoc?: boolean) => string;
70
+ /** Make sure the string starts with a capital, and all other letters are lowercase */
71
+ export declare const uncapitalize: (str: string) => string;
72
+ /** Helper to use with `Array.sort()` and similar */
73
+ export declare const alphaSort: (a: string, b: string) => number;
package/lib/string.js CHANGED
@@ -106,3 +106,35 @@ export const capitalize = (str, upperFirstLetter) => {
106
106
  const rest = str.slice(1);
107
107
  return `${transformedFirstLetter}${rest}`;
108
108
  };
109
+ /** Return the last index of a character that pass the needle test, or -1 if it never happens */
110
+ const findLastIndexOf = (hay, needle) => {
111
+ for (let index = hay.length - 1; --index; index >= 0) {
112
+ if (needle(hay.at(index)))
113
+ return index;
114
+ }
115
+ return -1;
116
+ };
117
+ /** Ensure the string ends with one newline */
118
+ export const enforceNewlineEnd = (str) => {
119
+ if (str.endsWith("\n")) {
120
+ const firstNonNewline = findLastIndexOf(str, (c) => c !== "\n");
121
+ if (firstNonNewline === -1)
122
+ return "\n";
123
+ return `${str.slice(0, firstNonNewline + 1)}\n`;
124
+ }
125
+ return `${str}\n`;
126
+ };
127
+ /** Format the text as a single line or multiline JS comment */
128
+ export const jsComment = (text, jsDoc = false) => {
129
+ const lines = text.split("\n");
130
+ if (lines.length === 1 && !jsDoc)
131
+ return `// ${lines[0]}`;
132
+ const prefix = jsDoc ? "/**" : "/*";
133
+ if (lines.length === 1)
134
+ return `${prefix} ${lines[0].replaceAll("*/", "* /")} */`;
135
+ return [prefix, ...lines.map((c) => ` * ${c.trimEnd().replaceAll("*/", "* /")}`), " */"].join("\n");
136
+ };
137
+ /** Make sure the string starts with a capital, and all other letters are lowercase */
138
+ export const uncapitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
139
+ /** Helper to use with `Array.sort()` and similar */
140
+ export const alphaSort = (a, b) => a.localeCompare(b);
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@keeex/utils","version":"7.5.0","description":"Various utility functions for pure JavaScript","scripts":{},"author":"KeeeX SAS","contributors":[{"name":"Gabriel Paul \"Cley Faye\" Risterucci","email":"gabriel@keeex.net"}],"homepage":"https://keeex.me/oss","keywords":["utility"],"type":"module","license":"MIT","dependencies":{"@keeex/bubble_babble":"^3.0.1","@keeex/log":"^1.7.2","base64-arraybuffer":"^1.0.2","cron-parser":"^5.4.0","ms":"^2.1.3","text-encoding-shim":"^1.0.5"},"exports":{"./array.js":{"node":"./lib/array.js","browser":"./web/array.js","react-native":"./web/array.js","default":"./lib/array.js"},"./arraybuffer.js":{"node":"./lib/arraybuffer.js","browser":"./web/arraybuffer.js","react-native":"./web/arraybuffer.js","default":"./lib/arraybuffer.js"},"./async/asynctrigger.js":{"node":"./lib/async/asynctrigger.js","browser":"./web/async/asynctrigger.js","react-native":"./web/async/asynctrigger.js","default":"./lib/async/asynctrigger.js"},"./async/deferredpromise.js":{"node":"./lib/async/deferredpromise.js","browser":"./web/async/deferredpromise.js","react-native":"./web/async/deferredpromise.js","default":"./lib/async/deferredpromise.js"},"./async/eventqueue.js":{"node":"./lib/async/eventqueue.js","browser":"./web/async/eventqueue.js","react-native":"./web/async/eventqueue.js","default":"./lib/async/eventqueue.js"},"./async/keycache.js":{"node":"./lib/async/keycache.js","browser":"./web/async/keycache.js","react-native":"./web/async/keycache.js","default":"./lib/async/keycache.js"},"./async/queues.js":{"node":"./lib/async/queues.js","browser":"./web/async/queues.js","react-native":"./web/async/queues.js","default":"./lib/async/queues.js"},"./async/timecache.js":{"node":"./lib/async/timecache.js","browser":"./web/async/timecache.js","react-native":"./web/async/timecache.js","default":"./lib/async/timecache.js"},"./base58.js":{"node":"./lib/base58.js","browser":"./web/base58.js","react-native":"./web/base58.js","default":"./lib/base58.js"},"./base64.js":{"node":"./lib/base64.js","browser":"./web/base64.js","react-native":"./web/base64.js","default":"./lib/base64.js"},"./benchmark.js":{"node":"./lib/benchmark.js","browser":"./web/benchmark.js","react-native":"./web/benchmark.js","default":"./lib/benchmark.js"},"./bytebuffer.js":{"node":"./lib/bytebuffer.js","browser":"./web/bytebuffer.js","react-native":"./web/bytebuffer.js","default":"./lib/bytebuffer.js"},"./consts.js":{"node":"./lib/consts.js","browser":"./web/consts.js","react-native":"./web/consts.js","default":"./lib/consts.js"},"./cron.js":{"node":"./lib/cron.js","browser":"./web/cron.js","react-native":"./web/cron.js","default":"./lib/cron.js"},"./dataview.js":{"node":"./lib/dataview.js","browser":"./web/dataview.js","react-native":"./web/dataview.js","default":"./lib/dataview.js"},"./dict.js":{"node":"./lib/dict.js","browser":"./web/dict.js","react-native":"./web/dict.js","default":"./lib/dict.js"},"./error.js":{"node":"./lib/error.js","browser":"./web/error.js","react-native":"./web/error.js","default":"./lib/error.js"},"./global.js":{"node":"./lib/global.js","browser":"./web/global.js","react-native":"./web/global.js","default":"./lib/global.js"},"./hex.js":{"node":"./lib/hex.js","browser":"./web/hex.js","react-native":"./web/hex.js","default":"./lib/hex.js"},"./idx.js":{"node":"./lib/idx.js","browser":"./web/idx.js","react-native":"./web/idx.js","default":"./lib/idx.js"},"./json.js":{"node":"./lib/json.js","browser":"./web/json.js","react-native":"./web/json.js","default":"./lib/json.js"},"./linebuffer.js":{"node":"./lib/linebuffer.js","browser":"./web/linebuffer.js","react-native":"./web/linebuffer.js","default":"./lib/linebuffer.js"},"./marshalling/marshaller.js":{"node":"./lib/marshalling/marshaller.js","browser":"./web/marshalling/marshaller.js","react-native":"./web/marshalling/marshaller.js","default":"./lib/marshalling/marshaller.js"},"./marshalling/unmarshaller.js":{"node":"./lib/marshalling/unmarshaller.js","browser":"./web/marshalling/unmarshaller.js","react-native":"./web/marshalling/unmarshaller.js","default":"./lib/marshalling/unmarshaller.js"},"./number.js":{"node":"./lib/number.js","browser":"./web/number.js","react-native":"./web/number.js","default":"./lib/number.js"},"./path.js":{"node":"./lib/path.js","browser":"./web/path.js","react-native":"./web/path.js","default":"./lib/path.js"},"./promise.js":{"node":"./lib/promise.js","browser":"./web/promise.js","react-native":"./web/promise.js","default":"./lib/promise.js"},"./starttime.js":{"node":"./lib/starttime.js","browser":"./web/starttime.js","react-native":"./web/starttime.js","default":"./lib/starttime.js"},"./string.js":{"node":"./lib/string.js","browser":"./web/string.js","react-native":"./web/string.js","default":"./lib/string.js"},"./triggers.js":{"node":"./lib/triggers.js","browser":"./web/triggers.js","react-native":"./web/triggers.js","default":"./lib/triggers.js"},"./types/array.js":{"node":"./lib/types/array.js","browser":"./web/types/array.js","react-native":"./web/types/array.js","default":"./lib/types/array.js"},"./types/enum.js":{"node":"./lib/types/enum.js","browser":"./web/types/enum.js","react-native":"./web/types/enum.js","default":"./lib/types/enum.js"},"./types/predicateerror.js":{"node":"./lib/types/predicateerror.js","browser":"./web/types/predicateerror.js","react-native":"./web/types/predicateerror.js","default":"./lib/types/predicateerror.js"},"./types/primitive.js":{"node":"./lib/types/primitive.js","browser":"./web/types/primitive.js","react-native":"./web/types/primitive.js","default":"./lib/types/primitive.js"},"./types/record.js":{"node":"./lib/types/record.js","browser":"./web/types/record.js","react-native":"./web/types/record.js","default":"./lib/types/record.js"},"./types/types.js":{"node":"./lib/types/types.js","browser":"./web/types/types.js","react-native":"./web/types/types.js","default":"./lib/types/types.js"},"./types/utils.js":{"node":"./lib/types/utils.js","browser":"./web/types/utils.js","react-native":"./web/types/utils.js","default":"./lib/types/utils.js"},"./uint8array.js":{"node":"./lib/uint8array.js","browser":"./web/uint8array.js","react-native":"./web/uint8array.js","default":"./lib/uint8array.js"},"./units.js":{"node":"./lib/units.js","browser":"./web/units.js","react-native":"./web/units.js","default":"./lib/units.js"}},"files":["/lib","/web"]}
1
+ {"name":"@keeex/utils","version":"7.6.0","description":"Various utility functions for pure JavaScript","scripts":{},"author":"KeeeX SAS","contributors":[{"name":"Gabriel Paul \"Cley Faye\" Risterucci","email":"gabriel@keeex.net"}],"homepage":"https://keeex.me/oss","keywords":["utility"],"type":"module","license":"MIT","dependencies":{"@keeex/bubble_babble":"^3.0.1","@keeex/log":"^1.7.2","base64-arraybuffer":"^1.0.2","cron-parser":"^5.4.0","ms":"^2.1.3","text-encoding-shim":"^1.0.5"},"exports":{"./array.js":{"node":"./lib/array.js","browser":"./web/array.js","react-native":"./web/array.js","default":"./lib/array.js"},"./arraybuffer.js":{"node":"./lib/arraybuffer.js","browser":"./web/arraybuffer.js","react-native":"./web/arraybuffer.js","default":"./lib/arraybuffer.js"},"./async/asynctrigger.js":{"node":"./lib/async/asynctrigger.js","browser":"./web/async/asynctrigger.js","react-native":"./web/async/asynctrigger.js","default":"./lib/async/asynctrigger.js"},"./async/deferredpromise.js":{"node":"./lib/async/deferredpromise.js","browser":"./web/async/deferredpromise.js","react-native":"./web/async/deferredpromise.js","default":"./lib/async/deferredpromise.js"},"./async/eventqueue.js":{"node":"./lib/async/eventqueue.js","browser":"./web/async/eventqueue.js","react-native":"./web/async/eventqueue.js","default":"./lib/async/eventqueue.js"},"./async/keycache.js":{"node":"./lib/async/keycache.js","browser":"./web/async/keycache.js","react-native":"./web/async/keycache.js","default":"./lib/async/keycache.js"},"./async/queues.js":{"node":"./lib/async/queues.js","browser":"./web/async/queues.js","react-native":"./web/async/queues.js","default":"./lib/async/queues.js"},"./async/timecache.js":{"node":"./lib/async/timecache.js","browser":"./web/async/timecache.js","react-native":"./web/async/timecache.js","default":"./lib/async/timecache.js"},"./base58.js":{"node":"./lib/base58.js","browser":"./web/base58.js","react-native":"./web/base58.js","default":"./lib/base58.js"},"./base64.js":{"node":"./lib/base64.js","browser":"./web/base64.js","react-native":"./web/base64.js","default":"./lib/base64.js"},"./benchmark.js":{"node":"./lib/benchmark.js","browser":"./web/benchmark.js","react-native":"./web/benchmark.js","default":"./lib/benchmark.js"},"./bytebuffer.js":{"node":"./lib/bytebuffer.js","browser":"./web/bytebuffer.js","react-native":"./web/bytebuffer.js","default":"./lib/bytebuffer.js"},"./consts.js":{"node":"./lib/consts.js","browser":"./web/consts.js","react-native":"./web/consts.js","default":"./lib/consts.js"},"./cron.js":{"node":"./lib/cron.js","browser":"./web/cron.js","react-native":"./web/cron.js","default":"./lib/cron.js"},"./dataview.js":{"node":"./lib/dataview.js","browser":"./web/dataview.js","react-native":"./web/dataview.js","default":"./lib/dataview.js"},"./dict.js":{"node":"./lib/dict.js","browser":"./web/dict.js","react-native":"./web/dict.js","default":"./lib/dict.js"},"./error.js":{"node":"./lib/error.js","browser":"./web/error.js","react-native":"./web/error.js","default":"./lib/error.js"},"./global.js":{"node":"./lib/global.js","browser":"./web/global.js","react-native":"./web/global.js","default":"./lib/global.js"},"./hex.js":{"node":"./lib/hex.js","browser":"./web/hex.js","react-native":"./web/hex.js","default":"./lib/hex.js"},"./idx.js":{"node":"./lib/idx.js","browser":"./web/idx.js","react-native":"./web/idx.js","default":"./lib/idx.js"},"./json.js":{"node":"./lib/json.js","browser":"./web/json.js","react-native":"./web/json.js","default":"./lib/json.js"},"./linebuffer.js":{"node":"./lib/linebuffer.js","browser":"./web/linebuffer.js","react-native":"./web/linebuffer.js","default":"./lib/linebuffer.js"},"./marshalling/marshaller.js":{"node":"./lib/marshalling/marshaller.js","browser":"./web/marshalling/marshaller.js","react-native":"./web/marshalling/marshaller.js","default":"./lib/marshalling/marshaller.js"},"./marshalling/unmarshaller.js":{"node":"./lib/marshalling/unmarshaller.js","browser":"./web/marshalling/unmarshaller.js","react-native":"./web/marshalling/unmarshaller.js","default":"./lib/marshalling/unmarshaller.js"},"./number.js":{"node":"./lib/number.js","browser":"./web/number.js","react-native":"./web/number.js","default":"./lib/number.js"},"./path.js":{"node":"./lib/path.js","browser":"./web/path.js","react-native":"./web/path.js","default":"./lib/path.js"},"./promise.js":{"node":"./lib/promise.js","browser":"./web/promise.js","react-native":"./web/promise.js","default":"./lib/promise.js"},"./starttime.js":{"node":"./lib/starttime.js","browser":"./web/starttime.js","react-native":"./web/starttime.js","default":"./lib/starttime.js"},"./string.js":{"node":"./lib/string.js","browser":"./web/string.js","react-native":"./web/string.js","default":"./lib/string.js"},"./triggers.js":{"node":"./lib/triggers.js","browser":"./web/triggers.js","react-native":"./web/triggers.js","default":"./lib/triggers.js"},"./types/array.js":{"node":"./lib/types/array.js","browser":"./web/types/array.js","react-native":"./web/types/array.js","default":"./lib/types/array.js"},"./types/enum.js":{"node":"./lib/types/enum.js","browser":"./web/types/enum.js","react-native":"./web/types/enum.js","default":"./lib/types/enum.js"},"./types/predicateerror.js":{"node":"./lib/types/predicateerror.js","browser":"./web/types/predicateerror.js","react-native":"./web/types/predicateerror.js","default":"./lib/types/predicateerror.js"},"./types/primitive.js":{"node":"./lib/types/primitive.js","browser":"./web/types/primitive.js","react-native":"./web/types/primitive.js","default":"./lib/types/primitive.js"},"./types/record.js":{"node":"./lib/types/record.js","browser":"./web/types/record.js","react-native":"./web/types/record.js","default":"./lib/types/record.js"},"./types/types.js":{"node":"./lib/types/types.js","browser":"./web/types/types.js","react-native":"./web/types/types.js","default":"./lib/types/types.js"},"./types/utils.js":{"node":"./lib/types/utils.js","browser":"./web/types/utils.js","react-native":"./web/types/utils.js","default":"./lib/types/utils.js"},"./uint8array.js":{"node":"./lib/uint8array.js","browser":"./web/uint8array.js","react-native":"./web/uint8array.js","default":"./lib/uint8array.js"},"./units.js":{"node":"./lib/units.js","browser":"./web/units.js","react-native":"./web/units.js","default":"./lib/units.js"}},"files":["/lib","/web"]}
package/web/array.d.ts CHANGED
@@ -26,3 +26,5 @@ export declare const arrayEqual: <T1 = unknown, T2 = unknown>(op1: ArrayLike<T1>
26
26
  * @public
27
27
  */
28
28
  export declare const asArray: <T>(data: Array<T> | (T extends Array<unknown> ? never : T)) => Array<T>;
29
+ /** Return a new array with its string sorted */
30
+ export declare const toSortedStringArray: (array: Array<string>) => Array<string>;
package/web/array.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
14
  *
15
15
  */
16
+ import { alphaSort } from "./string.js";
16
17
  const useEqOp = (op1, op2) => op1 === op2;
17
18
  /**
18
19
  * Compare two array-like structure for equal content
@@ -31,4 +32,6 @@ export const arrayEqual = (op1, op2, eqFunc = useEqOp) => {
31
32
  *
32
33
  * @public
33
34
  */
34
- export const asArray = data => Array.isArray(data) ? data : [data];
35
+ export const asArray = data => Array.isArray(data) ? data : [data];
36
+ /** Return a new array with its string sorted */
37
+ export const toSortedStringArray = array => array.toSorted(alphaSort);
package/web/dict.d.ts CHANGED
@@ -54,3 +54,35 @@ export type PrimitiveTypeObject = Record<string, unknown> | Array<unknown> | str
54
54
  * @public
55
55
  */
56
56
  export declare const deepCopyPrimitive: (source: unknown) => unknown;
57
+ /**
58
+ * Perform the merge operation between two values.
59
+ *
60
+ * If the new value have no changes from the initial value, this should always return `initial`.
61
+ * If there is any change, this should always return a new reference.
62
+ *
63
+ * TODO: The function returns unknown because I can't work around the requirements for TypeScript to
64
+ * be happy using `DataType | null`.
65
+ */
66
+ type RefMergeDefFn<DataType> = (initial: DataType | null, newValue: DataType | null) => unknown;
67
+ type RefMergeDef<DataType> = DataType extends string ? "string" : DataType extends number ? "number" : DataType extends boolean ? "boolean" : DataType extends Array<string> ? "string[]" : RefMergeDefFn<DataType>;
68
+ type RefMergeProfile<ObjectType> = Required<{
69
+ [key in keyof ObjectType]: RefMergeDef<NonNullable<ObjectType[key]>>;
70
+ }>;
71
+ /**
72
+ * Examine each props on both `initial` and `newValue`, and if there is any change, return a new
73
+ * reference with all values.
74
+ *
75
+ * @param initial - The source object. If no changes are detected, returns it.
76
+ * @param newValue - Values to update in `initial`. `undefined` values are skipped.
77
+ * @param profile - Describe the expected properties and how to merge them.
78
+ *
79
+ * @returns
80
+ * If there is some change from `initial`, will return a new reference with all new values.
81
+ * This property extends to values that are object; all objects in this hierarchy that have an
82
+ * update belows it is replaced with a new reference.
83
+ *
84
+ * This is made to help with libraries like React that depends on state reference actually getting
85
+ * changed on update.
86
+ */
87
+ export declare const refMergeObjects: <ObjectType>(initial: ObjectType, newValue: Partial<ObjectType>, profile: RefMergeProfile<ObjectType>) => ObjectType;
88
+ export {};
package/web/dict.js CHANGED
@@ -13,6 +13,7 @@
13
13
  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
14
14
  *
15
15
  */
16
+ import { arrayEqual } from "./array.js";
16
17
  /**
17
18
  * Copy all properties from secondary into primary.
18
19
  *
@@ -64,4 +65,69 @@ export const deepCopyPrimitive = source => {
64
65
  }
65
66
  return res;
66
67
  }
68
+ };
69
+ const refMergeStringArray = (initial, newValue) => {
70
+ if (newValue === null) return null;
71
+ if (initial !== null && arrayEqual(initial, newValue)) return initial;
72
+ return [...newValue];
73
+ };
74
+ /**
75
+ * Merge a new value and an old value according to the provided merger.
76
+ *
77
+ * If `newValue` is undefined, `initial` is always returned.
78
+ * If `newValue` is defined AND have difference from `initial`, a new object should be returned.
79
+ * If there is no change, `initial` is returned.
80
+ */
81
+ const getMergedRef = (initial, newValue, merger) => {
82
+ if (newValue === undefined) return initial;
83
+ switch (merger) {
84
+ case "string":
85
+ case "number":
86
+ case "boolean":
87
+ return newValue;
88
+ case "string[]":
89
+ return refMergeStringArray(initial, newValue);
90
+ default:
91
+ {
92
+ return merger(initial, newValue);
93
+ }
94
+ }
95
+ };
96
+ /**
97
+ * Examine each props on both `initial` and `newValue`, and if there is any change, return a new
98
+ * reference with all values.
99
+ *
100
+ * @param initial - The source object. If no changes are detected, returns it.
101
+ * @param newValue - Values to update in `initial`. `undefined` values are skipped.
102
+ * @param profile - Describe the expected properties and how to merge them.
103
+ *
104
+ * @returns
105
+ * If there is some change from `initial`, will return a new reference with all new values.
106
+ * This property extends to values that are object; all objects in this hierarchy that have an
107
+ * update belows it is replaced with a new reference.
108
+ *
109
+ * This is made to help with libraries like React that depends on state reference actually getting
110
+ * changed on update.
111
+ */
112
+ export const refMergeObjects = (initial, newValue, profile) => {
113
+ let anyChange = false;
114
+ const newValues = Object.entries(profile).map(([key, merger]) => {
115
+ const initialValue = initial[key];
116
+ const merged = getMergedRef(initialValue, newValue[key], merger);
117
+ anyChange ||= merged !== initialValue;
118
+ return {
119
+ key,
120
+ merged
121
+ };
122
+ });
123
+ // Can't statically detect changes in map() callback
124
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
125
+ if (!anyChange) return initial;
126
+ return newValues.reduce((acc, {
127
+ key,
128
+ merged
129
+ }) => {
130
+ acc[key] = merged;
131
+ return acc;
132
+ }, {});
67
133
  };
package/web/string.d.ts CHANGED
@@ -63,3 +63,11 @@ export declare const encodeUTF8: (str: string) => string;
63
63
  export declare const decodeUTF8: (str: string, skipErrors?: boolean) => string;
64
64
  /** Capitalize (or uncapitalize) the first character of a string */
65
65
  export declare const capitalize: (str: string, upperFirstLetter: boolean) => string;
66
+ /** Ensure the string ends with one newline */
67
+ export declare const enforceNewlineEnd: (str: string) => string;
68
+ /** Format the text as a single line or multiline JS comment */
69
+ export declare const jsComment: (text: string, jsDoc?: boolean) => string;
70
+ /** Make sure the string starts with a capital, and all other letters are lowercase */
71
+ export declare const uncapitalize: (str: string) => string;
72
+ /** Helper to use with `Array.sort()` and similar */
73
+ export declare const alphaSort: (a: string, b: string) => number;
package/web/string.js CHANGED
@@ -98,4 +98,32 @@ export const capitalize = (str, upperFirstLetter) => {
98
98
  const transformedFirstLetter = upperFirstLetter ? firstLetter.toUpperCase() : firstLetter.toLowerCase();
99
99
  const rest = str.slice(1);
100
100
  return `${transformedFirstLetter}${rest}`;
101
- };
101
+ };
102
+ /** Return the last index of a character that pass the needle test, or -1 if it never happens */
103
+ const findLastIndexOf = (hay, needle) => {
104
+ for (let index = hay.length - 1; --index; index >= 0) {
105
+ if (needle(hay.at(index))) return index;
106
+ }
107
+ return -1;
108
+ };
109
+ /** Ensure the string ends with one newline */
110
+ export const enforceNewlineEnd = str => {
111
+ if (str.endsWith("\n")) {
112
+ const firstNonNewline = findLastIndexOf(str, c => c !== "\n");
113
+ if (firstNonNewline === -1) return "\n";
114
+ return `${str.slice(0, firstNonNewline + 1)}\n`;
115
+ }
116
+ return `${str}\n`;
117
+ };
118
+ /** Format the text as a single line or multiline JS comment */
119
+ export const jsComment = (text, jsDoc = false) => {
120
+ const lines = text.split("\n");
121
+ if (lines.length === 1 && !jsDoc) return `// ${lines[0]}`;
122
+ const prefix = jsDoc ? "/**" : "/*";
123
+ if (lines.length === 1) return `${prefix} ${lines[0].replaceAll("*/", "* /")} */`;
124
+ return [prefix, ...lines.map(c => ` * ${c.trimEnd().replaceAll("*/", "* /")}`), " */"].join("\n");
125
+ };
126
+ /** Make sure the string starts with a capital, and all other letters are lowercase */
127
+ export const uncapitalize = str => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
128
+ /** Helper to use with `Array.sort()` and similar */
129
+ export const alphaSort = (a, b) => a.localeCompare(b);