@ocavue/utils 0.8.1 → 1.0.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/dist/index.d.ts +27 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +76 -1
- package/dist/index.js.map +1 -1
- package/package.json +10 -8
- package/src/checker.test.ts +35 -1
- package/src/checker.ts +14 -0
- package/src/default-map.test.ts +183 -0
- package/src/default-map.ts +22 -0
- package/src/index.ts +2 -0
- package/src/is-deep-equal.test.ts +219 -0
- package/src/is-deep-equal.ts +95 -0
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
* Checks if the given value is an object.
|
|
4
4
|
*/
|
|
5
5
|
declare function isObject(value: unknown): value is Record<string | symbol | number, unknown>;
|
|
6
|
+
/**
|
|
7
|
+
* Checks if the given value is a Map.
|
|
8
|
+
*/
|
|
9
|
+
declare function isMap(value: unknown): value is Map<unknown, unknown>;
|
|
10
|
+
/**
|
|
11
|
+
* Checks if the given value is a Set.
|
|
12
|
+
*/
|
|
13
|
+
declare function isSet(value: unknown): value is Set<unknown>;
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/default-map.d.ts
|
|
16
|
+
/**
|
|
17
|
+
* A map that automatically creates values for missing keys using a factory function.
|
|
18
|
+
*
|
|
19
|
+
* Similar to Python's [defaultdict](https://docs.python.org/3.13/library/collections.html#collections.defaultdict).
|
|
20
|
+
*/
|
|
21
|
+
declare class DefaultMap<K, V> extends Map<K, V> {
|
|
22
|
+
private readonly defaultFactory;
|
|
23
|
+
constructor(defaultFactory: () => V, iterable?: Iterable<readonly [K, V]>);
|
|
24
|
+
get(key: K): V;
|
|
25
|
+
}
|
|
6
26
|
//#endregion
|
|
7
27
|
//#region src/dom.d.ts
|
|
8
28
|
/**
|
|
@@ -100,5 +120,11 @@ declare function once<T>(fn: () => T): () => T;
|
|
|
100
120
|
*/
|
|
101
121
|
declare function sleep(ms: number): Promise<void>;
|
|
102
122
|
//#endregion
|
|
103
|
-
|
|
123
|
+
//#region src/is-deep-equal.d.ts
|
|
124
|
+
/**
|
|
125
|
+
* Whether two values are deeply equal.
|
|
126
|
+
*/
|
|
127
|
+
declare function isDeepEqual(a: unknown, b: unknown): boolean;
|
|
128
|
+
//#endregion
|
|
129
|
+
export { DefaultMap, formatBytes, getDocument, getDocumentElement, getId, getWindow, isDeepEqual, isDocument, isDocumentFragment, isElement, isElementLike, isHTMLElement, isMap, isMathMLElement, isNodeLike, isObject, isSVGElement, isSet, isShadowRoot, isTextNode, isWindowLike, once, sleep };
|
|
104
130
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/checker.ts","../src/dom.ts","../src/format-bytes.ts","../src/get-id.ts","../src/once.ts","../src/sleep.ts"],"sourcesContent":[],"mappings":";;AAGA
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/checker.ts","../src/default-map.ts","../src/dom.ts","../src/format-bytes.ts","../src/get-id.ts","../src/once.ts","../src/sleep.ts","../src/is-deep-equal.ts"],"sourcesContent":[],"mappings":";;AAGA;AASA;AAOgB,iBAhBA,QAAA,CAgBmC,KAAA,EAAA,OAAA,CAAA,EAAA,KAAA,IAdvC,MAcuC,CAAA,MAAA,GAAA,MAAA,GAAA,MAAA,EAAA,OAAA,CAAA;;;;ACdtC,iBDOG,KAAA,CCPO,KAAA,EAAA,OAAA,CAAA,EAAA,KAAA,IDOyB,GCPzB,CAAA,OAAA,EAAA,OAAA,CAAA;;;;AAG8C,iBDWrD,KAAA,CCXqD,KAAA,EAAA,OAAA,CAAA,EAAA,KAAA,IDWrB,GCXqB,CAAA,OAAA,CAAA;;;;ADLrE;AASA;AAOA;;cCda,yBAAyB,IAAI,GAAG;;EAAhC,WAAA,CAAA,cAAU,EAAA,GAAA,GAGa,CAHb,EAAA,QAAA,CAAA,EAG2B,QAH3B,CAAA,SAAA,CAG8C,CAH9C,EAGiD,CAHjD,CAAA,CAAA;EAAmB,GAAA,CAAA,GAAA,EAQtB,CARsB,CAAA,EAQlB,CARkB;;;;;ADF1C;AASA;AAOgB,iBEbA,SAAA,CFamC,IAAA,EEbnB,IFamB,CAAA,EAAA,IAAA,IEbJ,OFaI;;;;ACdtC,iBCQG,UAAA,CDRO,IAAA,ECQU,IDRV,CAAA,EAAA,IAAA,ICQyB,IDRzB;;;;AAG8C,iBCYrD,aAAA,CDZqD,IAAA,ECYjC,IDZiC,CAAA,EAAA,IAAA,ICYlB,WDZkB;;;;AAK7C,iBCcR,YAAA,CDdQ,IAAA,ECcW,IDdX,CAAA,EAAA,IAAA,ICc0B,UDd1B;;;;iBCqBR,eAAA,OAAsB,eAAe;;AA5BrD;AAOA;AAOgB,iBAwBA,UAAA,CAxBoB,IAAe,EAwBlB,IAxBkB,CAAA,EAAA,IAAW,IAwBd,QAxBc;AAO9D;AAOA;AAUA;AAOgB,iBAAA,kBAAA,CAAwC,IAAA,EAAf,IAAe,CAAA,EAAA,IAAgB,IAAhB,gBAAgB;AAOxE;AAOA;AAOA;AAWgB,iBAzBA,YAAA,CAyBuC,IAAM,EAzB1B,IAyB0B,CAAA,EAAA,IAAA,IAzBX,UAyBW;AAQ7D;;;AAC+B,iBA3Bf,UAAA,CA2Be,KAAA,EAAA,OAAA,CAAA,EAAA,KAAA,IA3BsB,IA2BtB;;;;AAoBf,iBAxCA,aAAA,CAwCW,KAAA,EAAA,OAAA,CAAA,EAAA,KAAA,IAxC6B,OAwC7B;;;;AACU,iBA9BrB,YAAA,CA8BqB,KAAA,EAAA,OAAA,CAAA,EAAA,KAAA,IA9BkB,MA8BlB;;;AAiBrC;;AACqB,iBAxCL,SAAA,CAwCK,MAAA,CAAA,EAvCV,IAuCU,GAvCH,UAuCG,GAvCU,QAuCV,GAAA,IAAA,CAAA,EAtClB,MAsCkB,GAAA,OAtCF,UAsCE;;;;;iBAnBL,WAAA,UACL,UAAU,SAAS,OAAO,kBAClC;;;AC7GH;iBD6HgB,kBAAA,UACL,UAAU,OAAO,SAAS,kBAClC;;;;AFjIH;AASA;AAOA;;iBGdgB,WAAA;;;;AHFhB;AASA;AAOgB,iBIdA,KAAA,CAAA,CJcmC,EAAA,MAAA;;;;AAhBnD;AASA;AAOA;;;;ACdA;;;;;;AAGkD,iBIKlC,IJLkC,CAAA,CAAA,CAAA,CAAA,EAAA,EAAA,GAAA,GIKhB,CJLgB,CAAA,EAAA,GAAA,GIKN,CJLM;;;;ADLlD;AASA;AAOgB,iBMhBA,KAAA,CNgBmC,EAAA,EAAA,MAAA,CAAA,EMhBhB,ONgBgB,CAAA,IAAA,CAAA;;;;AAhBnD;AASA;AAOgB,iBOdA,WAAA,CPcmC,CAAA,EAAA,OAAA,EAAA,CAAA,EAAA,OAAA,CAAA,EAAA,OAAA"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,38 @@
|
|
|
5
5
|
function isObject(value) {
|
|
6
6
|
return value != null && typeof value === "object";
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Checks if the given value is a Map.
|
|
10
|
+
*/
|
|
11
|
+
function isMap(value) {
|
|
12
|
+
return value instanceof Map;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Checks if the given value is a Set.
|
|
16
|
+
*/
|
|
17
|
+
function isSet(value) {
|
|
18
|
+
return value instanceof Set;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/default-map.ts
|
|
23
|
+
/**
|
|
24
|
+
* A map that automatically creates values for missing keys using a factory function.
|
|
25
|
+
*
|
|
26
|
+
* Similar to Python's [defaultdict](https://docs.python.org/3.13/library/collections.html#collections.defaultdict).
|
|
27
|
+
*/
|
|
28
|
+
var DefaultMap = class extends Map {
|
|
29
|
+
constructor(defaultFactory, iterable) {
|
|
30
|
+
super(iterable);
|
|
31
|
+
this.defaultFactory = defaultFactory;
|
|
32
|
+
}
|
|
33
|
+
get(key) {
|
|
34
|
+
if (this.has(key)) return super.get(key);
|
|
35
|
+
const value = this.defaultFactory();
|
|
36
|
+
this.set(key, value);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
8
40
|
|
|
9
41
|
//#endregion
|
|
10
42
|
//#region src/dom-node-type.ts
|
|
@@ -185,5 +217,48 @@ function sleep(ms) {
|
|
|
185
217
|
}
|
|
186
218
|
|
|
187
219
|
//#endregion
|
|
188
|
-
|
|
220
|
+
//#region src/is-deep-equal.ts
|
|
221
|
+
/**
|
|
222
|
+
* Whether two values are deeply equal.
|
|
223
|
+
*/
|
|
224
|
+
function isDeepEqual(a, b) {
|
|
225
|
+
if (a === b) return true;
|
|
226
|
+
if (a == null || b == null) return false;
|
|
227
|
+
const aType = typeof a;
|
|
228
|
+
if (aType !== typeof b) return false;
|
|
229
|
+
if (aType === "number" && Number.isNaN(a) && Number.isNaN(b)) return true;
|
|
230
|
+
if (Array.isArray(a)) {
|
|
231
|
+
if (!Array.isArray(b)) return false;
|
|
232
|
+
if (a.length !== b.length) return false;
|
|
233
|
+
const size = a.length;
|
|
234
|
+
for (let i = 0; i < size; i++) if (!isDeepEqual(a[i], b[i])) return false;
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
if (isSet(a)) {
|
|
238
|
+
if (!isSet(b)) return false;
|
|
239
|
+
if (a.size !== b.size) return false;
|
|
240
|
+
for (const value of a) if (!b.has(value)) return false;
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
if (isMap(a)) {
|
|
244
|
+
if (!isMap(b)) return false;
|
|
245
|
+
if (a.size !== b.size) return false;
|
|
246
|
+
for (const key of a.keys()) {
|
|
247
|
+
if (!b.has(key)) return false;
|
|
248
|
+
if (!isDeepEqual(a.get(key), b.get(key))) return false;
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
253
|
+
const aKeys = Object.keys(a);
|
|
254
|
+
const bKeys = Object.keys(b);
|
|
255
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
256
|
+
for (const key of aKeys) if (!isDeepEqual(a[key], b[key])) return false;
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
//#endregion
|
|
263
|
+
export { DefaultMap, formatBytes, getDocument, getDocumentElement, getId, getWindow, isDeepEqual, isDocument, isDocumentFragment, isElement, isElementLike, isHTMLElement, isMap, isMathMLElement, isNodeLike, isObject, isSVGElement, isSet, isShadowRoot, isTextNode, isWindowLike, once, sleep };
|
|
189
264
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["NodeType.ELEMENT_NODE","NodeType.TEXT_NODE","NodeType.DOCUMENT_NODE","NodeType.DOCUMENT_FRAGMENT_NODE","result: T"],"sources":["../src/checker.ts","../src/dom-node-type.ts","../src/dom.ts","../src/format-bytes.ts","../src/get-id.ts","../src/once.ts","../src/sleep.ts"],"sourcesContent":["/**\n * Checks if the given value is an object.\n */\nexport function isObject(\n value: unknown,\n): value is Record<string | symbol | number, unknown> {\n return value != null && typeof value === 'object'\n}\n","// prettier-ignore\nexport const ELEMENT_NODE = 1 satisfies typeof Node.ELEMENT_NODE;\n// prettier-ignore\nexport const ATTRIBUTE_NODE = 2 satisfies typeof Node.ATTRIBUTE_NODE;\n// prettier-ignore\nexport const TEXT_NODE = 3 satisfies typeof Node.TEXT_NODE;\n// prettier-ignore\nexport const CDATA_SECTION_NODE = 4 satisfies typeof Node.CDATA_SECTION_NODE;\n// prettier-ignore\nexport const ENTITY_REFERENCE_NODE = 5 satisfies typeof Node.ENTITY_REFERENCE_NODE;\n// prettier-ignore\nexport const ENTITY_NODE = 6 satisfies typeof Node.ENTITY_NODE;\n// prettier-ignore\nexport const PROCESSING_INSTRUCTION_NODE = 7 satisfies typeof Node.PROCESSING_INSTRUCTION_NODE;\n// prettier-ignore\nexport const COMMENT_NODE = 8 satisfies typeof Node.COMMENT_NODE;\n// prettier-ignore\nexport const DOCUMENT_NODE = 9 satisfies typeof Node.DOCUMENT_NODE;\n// prettier-ignore\nexport const DOCUMENT_TYPE_NODE = 10 satisfies typeof Node.DOCUMENT_TYPE_NODE;\n// prettier-ignore\nexport const DOCUMENT_FRAGMENT_NODE = 11 satisfies typeof Node.DOCUMENT_FRAGMENT_NODE;\n// prettier-ignore\nexport const NOTATION_NODE = 12 satisfies typeof Node.NOTATION_NODE;\n","import { isObject } from './checker'\nimport * as NodeType from './dom-node-type'\n\n/**\n * Checks if the given DOM node is an Element.\n */\nexport function isElement(node: Node): node is Element {\n return node.nodeType === NodeType.ELEMENT_NODE\n}\n\n/**\n * Checks if the given DOM node is a Text node.\n */\nexport function isTextNode(node: Node): node is Text {\n return node.nodeType === NodeType.TEXT_NODE\n}\n\n/**\n * Checks if the given DOM node is an HTMLElement.\n */\nexport function isHTMLElement(node: Node): node is HTMLElement {\n return isElement(node) && node.namespaceURI === 'http://www.w3.org/1999/xhtml'\n}\n\n/**\n * Checks if the given DOM node is an SVGElement.\n */\nexport function isSVGElement(node: Node): node is SVGElement {\n return isElement(node) && node.namespaceURI === 'http://www.w3.org/2000/svg'\n}\n\n/**\n * Checks if the given DOM node is an MathMLElement.\n */\nexport function isMathMLElement(node: Node): node is MathMLElement {\n return (\n isElement(node) &&\n node.namespaceURI === 'http://www.w3.org/1998/Math/MathML'\n )\n}\n\n/**\n * Checks if the given DOM node is a Document.\n */\nexport function isDocument(node: Node): node is Document {\n return node.nodeType === NodeType.DOCUMENT_NODE\n}\n\n/**\n * Checks if the given DOM node is a DocumentFragment.\n */\nexport function isDocumentFragment(node: Node): node is DocumentFragment {\n return node.nodeType === NodeType.DOCUMENT_FRAGMENT_NODE\n}\n\n/**\n * Checks if the given DOM node is a ShadowRoot.\n */\nexport function isShadowRoot(node: Node): node is ShadowRoot {\n return isDocumentFragment(node) && 'host' in node && isElementLike(node.host)\n}\n\n/**\n * Checks if an unknown value is likely a DOM node.\n */\nexport function isNodeLike(value: unknown): value is Node {\n return isObject(value) && value.nodeType !== undefined\n}\n\n/**\n * Checks if an unknown value is likely a DOM element.\n */\nexport function isElementLike(value: unknown): value is Element {\n return (\n isObject(value) &&\n value.nodeType === NodeType.ELEMENT_NODE &&\n typeof value.nodeName === 'string'\n )\n}\n\n/**\n * Checks if the given value is likely a Window object.\n */\nexport function isWindowLike(value: unknown): value is Window {\n return isObject(value) && value.window === value\n}\n\n/**\n * Gets the window object for the given target or the global window object if no\n * target is provided.\n */\nexport function getWindow(\n target?: Node | ShadowRoot | Document | null,\n): Window & typeof globalThis {\n if (target) {\n if (isShadowRoot(target)) {\n return getWindow(target.host)\n }\n if (isDocument(target)) {\n return target.defaultView || window\n }\n if (isElement(target)) {\n return target.ownerDocument?.defaultView || window\n }\n }\n return window\n}\n\n/**\n * Gets the document for the given target or the global document if no target is\n * provided.\n */\nexport function getDocument(\n target?: Element | Window | Node | Document | null,\n): Document {\n if (target) {\n if (isWindowLike(target)) {\n return target.document\n }\n if (isDocument(target)) {\n return target\n }\n return target.ownerDocument || document\n }\n return document\n}\n\n/**\n * Gets a reference to the root node of the document based on the given target.\n */\nexport function getDocumentElement(\n target?: Element | Node | Window | Document | null,\n): HTMLElement {\n return getDocument(target).documentElement\n}\n","/**\n * Formats a number of bytes into a human-readable string.\n * @param bytes - The number of bytes to format.\n * @returns A string representing the number of bytes in a human-readable format.\n */\nexport function formatBytes(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB']\n let unitIndex = 0\n let num = bytes\n while (Math.abs(num) >= 1024 && unitIndex < units.length - 1) {\n num /= 1024\n unitIndex++\n }\n const fraction = unitIndex === 0 && num % 1 === 0 ? 0 : 1\n return `${num.toFixed(fraction)}${units[unitIndex]}`\n}\n","let id = 0\n\n/**\n * Generates a unique positive integer.\n */\nexport function getId(): number {\n id = (id % Number.MAX_SAFE_INTEGER) + 1\n return id\n}\n","/**\n * Creates a function that will only execute the provided function once.\n * Subsequent calls will return the cached result from the first execution.\n *\n * @param fn The function to execute once\n * @returns A function that will only execute the provided function once\n * @example\n * ```ts\n * const getValue = once(() => expensiveOperation())\n * getValue() // executes expensiveOperation\n * getValue() // returns cached result\n * ```\n */\nexport function once<T>(fn: () => T): () => T {\n let called = false\n let result: T\n return () => {\n if (!called) {\n result = fn()\n called = true\n // @ts-expect-error - micro-optimization for memory management\n fn = undefined\n }\n return result\n }\n}\n","/**\n * Sleep for a given number of milliseconds.\n */\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n"],"mappings":";;;;AAGA,SAAgB,SACd,OACoD;AACpD,QAAO,SAAS,QAAQ,OAAO,UAAU;;;;;ACL3C,MAAa,eAAe;AAI5B,MAAa,YAAY;AAYzB,MAAa,gBAAgB;AAI7B,MAAa,yBAAyB;;;;;;;ACftC,SAAgB,UAAU,MAA6B;AACrD,QAAO,KAAK,aAAaA;;;;;AAM3B,SAAgB,WAAW,MAA0B;AACnD,QAAO,KAAK,aAAaC;;;;;AAM3B,SAAgB,cAAc,MAAiC;AAC7D,QAAO,UAAU,KAAK,IAAI,KAAK,iBAAiB;;;;;AAMlD,SAAgB,aAAa,MAAgC;AAC3D,QAAO,UAAU,KAAK,IAAI,KAAK,iBAAiB;;;;;AAMlD,SAAgB,gBAAgB,MAAmC;AACjE,QACE,UAAU,KAAK,IACf,KAAK,iBAAiB;;;;;AAO1B,SAAgB,WAAW,MAA8B;AACvD,QAAO,KAAK,aAAaC;;;;;AAM3B,SAAgB,mBAAmB,MAAsC;AACvE,QAAO,KAAK,aAAaC;;;;;AAM3B,SAAgB,aAAa,MAAgC;AAC3D,QAAO,mBAAmB,KAAK,IAAI,UAAU,QAAQ,cAAc,KAAK,KAAK;;;;;AAM/E,SAAgB,WAAW,OAA+B;AACxD,QAAO,SAAS,MAAM,IAAI,MAAM,aAAa;;;;;AAM/C,SAAgB,cAAc,OAAkC;AAC9D,QACE,SAAS,MAAM,IACf,MAAM,aAAaH,gBACnB,OAAO,MAAM,aAAa;;;;;AAO9B,SAAgB,aAAa,OAAiC;AAC5D,QAAO,SAAS,MAAM,IAAI,MAAM,WAAW;;;;;;AAO7C,SAAgB,UACd,QAC4B;AAC5B,KAAI,QAAQ;AACV,MAAI,aAAa,OAAO,CACtB,QAAO,UAAU,OAAO,KAAK;AAE/B,MAAI,WAAW,OAAO,CACpB,QAAO,OAAO,eAAe;AAE/B,MAAI,UAAU,OAAO,CACnB,QAAO,OAAO,eAAe,eAAe;;AAGhD,QAAO;;;;;;AAOT,SAAgB,YACd,QACU;AACV,KAAI,QAAQ;AACV,MAAI,aAAa,OAAO,CACtB,QAAO,OAAO;AAEhB,MAAI,WAAW,OAAO,CACpB,QAAO;AAET,SAAO,OAAO,iBAAiB;;AAEjC,QAAO;;;;;AAMT,SAAgB,mBACd,QACa;AACb,QAAO,YAAY,OAAO,CAAC;;;;;;;;;;AChI7B,SAAgB,YAAY,OAAuB;CACjD,MAAM,QAAQ;EAAC;EAAK;EAAM;EAAM;EAAK;CACrC,IAAI,YAAY;CAChB,IAAI,MAAM;AACV,QAAO,KAAK,IAAI,IAAI,IAAI,QAAQ,YAAY,MAAM,SAAS,GAAG;AAC5D,SAAO;AACP;;CAEF,MAAM,WAAW,cAAc,KAAK,MAAM,MAAM,IAAI,IAAI;AACxD,QAAO,GAAG,IAAI,QAAQ,SAAS,GAAG,MAAM;;;;;ACd1C,IAAI,KAAK;;;;AAKT,SAAgB,QAAgB;AAC9B,MAAM,KAAK,OAAO,mBAAoB;AACtC,QAAO;;;;;;;;;;;;;;;;;;ACMT,SAAgB,KAAQ,IAAsB;CAC5C,IAAI,SAAS;CACb,IAAII;AACJ,cAAa;AACX,MAAI,CAAC,QAAQ;AACX,YAAS,IAAI;AACb,YAAS;AAET,QAAK;;AAEP,SAAO;;;;;;;;;ACpBX,SAAgB,MAAM,IAA2B;AAC/C,QAAO,IAAI,SAAS,YAAY,WAAW,SAAS,GAAG,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["NodeType.ELEMENT_NODE","NodeType.TEXT_NODE","NodeType.DOCUMENT_NODE","NodeType.DOCUMENT_FRAGMENT_NODE","result: T"],"sources":["../src/checker.ts","../src/default-map.ts","../src/dom-node-type.ts","../src/dom.ts","../src/format-bytes.ts","../src/get-id.ts","../src/once.ts","../src/sleep.ts","../src/is-deep-equal.ts"],"sourcesContent":["/**\n * Checks if the given value is an object.\n */\nexport function isObject(\n value: unknown,\n): value is Record<string | symbol | number, unknown> {\n return value != null && typeof value === 'object'\n}\n\n/**\n * Checks if the given value is a Map.\n */\nexport function isMap(value: unknown): value is Map<unknown, unknown> {\n return value instanceof Map\n}\n\n/**\n * Checks if the given value is a Set.\n */\nexport function isSet(value: unknown): value is Set<unknown> {\n return value instanceof Set\n}\n","/**\n * A map that automatically creates values for missing keys using a factory function.\n *\n * Similar to Python's [defaultdict](https://docs.python.org/3.13/library/collections.html#collections.defaultdict).\n */\nexport class DefaultMap<K, V> extends Map<K, V> {\n private readonly defaultFactory: () => V\n\n constructor(defaultFactory: () => V, iterable?: Iterable<readonly [K, V]>) {\n super(iterable)\n this.defaultFactory = defaultFactory\n }\n\n override get(key: K): V {\n if (this.has(key)) {\n return super.get(key)!\n }\n const value = this.defaultFactory()\n this.set(key, value)\n return value\n }\n}\n","// prettier-ignore\nexport const ELEMENT_NODE = 1 satisfies typeof Node.ELEMENT_NODE;\n// prettier-ignore\nexport const ATTRIBUTE_NODE = 2 satisfies typeof Node.ATTRIBUTE_NODE;\n// prettier-ignore\nexport const TEXT_NODE = 3 satisfies typeof Node.TEXT_NODE;\n// prettier-ignore\nexport const CDATA_SECTION_NODE = 4 satisfies typeof Node.CDATA_SECTION_NODE;\n// prettier-ignore\nexport const ENTITY_REFERENCE_NODE = 5 satisfies typeof Node.ENTITY_REFERENCE_NODE;\n// prettier-ignore\nexport const ENTITY_NODE = 6 satisfies typeof Node.ENTITY_NODE;\n// prettier-ignore\nexport const PROCESSING_INSTRUCTION_NODE = 7 satisfies typeof Node.PROCESSING_INSTRUCTION_NODE;\n// prettier-ignore\nexport const COMMENT_NODE = 8 satisfies typeof Node.COMMENT_NODE;\n// prettier-ignore\nexport const DOCUMENT_NODE = 9 satisfies typeof Node.DOCUMENT_NODE;\n// prettier-ignore\nexport const DOCUMENT_TYPE_NODE = 10 satisfies typeof Node.DOCUMENT_TYPE_NODE;\n// prettier-ignore\nexport const DOCUMENT_FRAGMENT_NODE = 11 satisfies typeof Node.DOCUMENT_FRAGMENT_NODE;\n// prettier-ignore\nexport const NOTATION_NODE = 12 satisfies typeof Node.NOTATION_NODE;\n","import { isObject } from './checker'\nimport * as NodeType from './dom-node-type'\n\n/**\n * Checks if the given DOM node is an Element.\n */\nexport function isElement(node: Node): node is Element {\n return node.nodeType === NodeType.ELEMENT_NODE\n}\n\n/**\n * Checks if the given DOM node is a Text node.\n */\nexport function isTextNode(node: Node): node is Text {\n return node.nodeType === NodeType.TEXT_NODE\n}\n\n/**\n * Checks if the given DOM node is an HTMLElement.\n */\nexport function isHTMLElement(node: Node): node is HTMLElement {\n return isElement(node) && node.namespaceURI === 'http://www.w3.org/1999/xhtml'\n}\n\n/**\n * Checks if the given DOM node is an SVGElement.\n */\nexport function isSVGElement(node: Node): node is SVGElement {\n return isElement(node) && node.namespaceURI === 'http://www.w3.org/2000/svg'\n}\n\n/**\n * Checks if the given DOM node is an MathMLElement.\n */\nexport function isMathMLElement(node: Node): node is MathMLElement {\n return (\n isElement(node) &&\n node.namespaceURI === 'http://www.w3.org/1998/Math/MathML'\n )\n}\n\n/**\n * Checks if the given DOM node is a Document.\n */\nexport function isDocument(node: Node): node is Document {\n return node.nodeType === NodeType.DOCUMENT_NODE\n}\n\n/**\n * Checks if the given DOM node is a DocumentFragment.\n */\nexport function isDocumentFragment(node: Node): node is DocumentFragment {\n return node.nodeType === NodeType.DOCUMENT_FRAGMENT_NODE\n}\n\n/**\n * Checks if the given DOM node is a ShadowRoot.\n */\nexport function isShadowRoot(node: Node): node is ShadowRoot {\n return isDocumentFragment(node) && 'host' in node && isElementLike(node.host)\n}\n\n/**\n * Checks if an unknown value is likely a DOM node.\n */\nexport function isNodeLike(value: unknown): value is Node {\n return isObject(value) && value.nodeType !== undefined\n}\n\n/**\n * Checks if an unknown value is likely a DOM element.\n */\nexport function isElementLike(value: unknown): value is Element {\n return (\n isObject(value) &&\n value.nodeType === NodeType.ELEMENT_NODE &&\n typeof value.nodeName === 'string'\n )\n}\n\n/**\n * Checks if the given value is likely a Window object.\n */\nexport function isWindowLike(value: unknown): value is Window {\n return isObject(value) && value.window === value\n}\n\n/**\n * Gets the window object for the given target or the global window object if no\n * target is provided.\n */\nexport function getWindow(\n target?: Node | ShadowRoot | Document | null,\n): Window & typeof globalThis {\n if (target) {\n if (isShadowRoot(target)) {\n return getWindow(target.host)\n }\n if (isDocument(target)) {\n return target.defaultView || window\n }\n if (isElement(target)) {\n return target.ownerDocument?.defaultView || window\n }\n }\n return window\n}\n\n/**\n * Gets the document for the given target or the global document if no target is\n * provided.\n */\nexport function getDocument(\n target?: Element | Window | Node | Document | null,\n): Document {\n if (target) {\n if (isWindowLike(target)) {\n return target.document\n }\n if (isDocument(target)) {\n return target\n }\n return target.ownerDocument || document\n }\n return document\n}\n\n/**\n * Gets a reference to the root node of the document based on the given target.\n */\nexport function getDocumentElement(\n target?: Element | Node | Window | Document | null,\n): HTMLElement {\n return getDocument(target).documentElement\n}\n","/**\n * Formats a number of bytes into a human-readable string.\n * @param bytes - The number of bytes to format.\n * @returns A string representing the number of bytes in a human-readable format.\n */\nexport function formatBytes(bytes: number): string {\n const units = ['B', 'KB', 'MB', 'GB']\n let unitIndex = 0\n let num = bytes\n while (Math.abs(num) >= 1024 && unitIndex < units.length - 1) {\n num /= 1024\n unitIndex++\n }\n const fraction = unitIndex === 0 && num % 1 === 0 ? 0 : 1\n return `${num.toFixed(fraction)}${units[unitIndex]}`\n}\n","let id = 0\n\n/**\n * Generates a unique positive integer.\n */\nexport function getId(): number {\n id = (id % Number.MAX_SAFE_INTEGER) + 1\n return id\n}\n","/**\n * Creates a function that will only execute the provided function once.\n * Subsequent calls will return the cached result from the first execution.\n *\n * @param fn The function to execute once\n * @returns A function that will only execute the provided function once\n * @example\n * ```ts\n * const getValue = once(() => expensiveOperation())\n * getValue() // executes expensiveOperation\n * getValue() // returns cached result\n * ```\n */\nexport function once<T>(fn: () => T): () => T {\n let called = false\n let result: T\n return () => {\n if (!called) {\n result = fn()\n called = true\n // @ts-expect-error - micro-optimization for memory management\n fn = undefined\n }\n return result\n }\n}\n","/**\n * Sleep for a given number of milliseconds.\n */\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n","import { isMap, isSet } from './checker'\n\n/**\n * Whether two values are deeply equal.\n */\nexport function isDeepEqual(a: unknown, b: unknown): boolean {\n if (a === b) {\n return true\n }\n\n // Handle null and undefined early\n if (a == null || b == null) {\n return false\n }\n\n const aType = typeof a\n const bType = typeof b\n if (aType !== bType) {\n return false\n }\n\n if (aType === 'number' && Number.isNaN(a) && Number.isNaN(b)) {\n return true\n }\n\n if (Array.isArray(a)) {\n if (!Array.isArray(b)) {\n return false\n }\n if (a.length !== b.length) {\n return false\n }\n const size = a.length\n for (let i = 0; i < size; i++) {\n if (!isDeepEqual(a[i], b[i])) {\n return false\n }\n }\n return true\n }\n\n if (isSet(a)) {\n if (!isSet(b)) {\n return false\n }\n if (a.size !== b.size) {\n return false\n }\n for (const value of a) {\n if (!b.has(value)) {\n return false\n }\n }\n return true\n }\n\n if (isMap(a)) {\n if (!isMap(b)) {\n return false\n }\n if (a.size !== b.size) {\n return false\n }\n for (const key of a.keys()) {\n if (!b.has(key)) {\n return false\n }\n if (!isDeepEqual(a.get(key), b.get(key))) {\n return false\n }\n }\n return true\n }\n\n if (typeof a === 'object' && typeof b === 'object') {\n const aKeys = Object.keys(a)\n const bKeys = Object.keys(b)\n if (aKeys.length !== bKeys.length) {\n return false\n }\n for (const key of aKeys) {\n if (\n !isDeepEqual(\n (a as Record<string, unknown>)[key],\n (b as Record<string, unknown>)[key],\n )\n ) {\n return false\n }\n }\n return true\n }\n\n return false\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ocavue/utils",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"description": "A collection of utility functions for the browser and other environments",
|
|
6
6
|
"author": "ocavue <ocavue@gmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -37,19 +37,20 @@
|
|
|
37
37
|
"dist"
|
|
38
38
|
],
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@ocavue/eslint-config": "^3.6.
|
|
41
|
-
"@ocavue/tsconfig": "^0.
|
|
40
|
+
"@ocavue/eslint-config": "^3.6.2",
|
|
41
|
+
"@ocavue/tsconfig": "^0.6.1",
|
|
42
42
|
"@size-limit/preset-small-lib": "^11.2.0",
|
|
43
43
|
"@types/node": "^24.9.2",
|
|
44
|
-
"@vitest/coverage-v8": "4.0.
|
|
44
|
+
"@vitest/coverage-v8": "4.0.13",
|
|
45
45
|
"eslint": "^9.38.0",
|
|
46
|
-
"jsdom": "^27.0
|
|
46
|
+
"jsdom": "^27.2.0",
|
|
47
|
+
"pkg-pr-new": "^0.0.60",
|
|
47
48
|
"prettier": "^3.6.2",
|
|
48
49
|
"size-limit": "^11.2.0",
|
|
49
|
-
"tsdown": "^0.
|
|
50
|
+
"tsdown": "^0.16.6",
|
|
50
51
|
"typescript": "^5.9.3",
|
|
51
|
-
"vite": "^7.
|
|
52
|
-
"vitest": "^4.0.
|
|
52
|
+
"vite": "^7.2.4",
|
|
53
|
+
"vitest": "^4.0.13"
|
|
53
54
|
},
|
|
54
55
|
"publishConfig": {
|
|
55
56
|
"access": "public"
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
"dev": "tsdown --watch",
|
|
70
71
|
"lint": "eslint .",
|
|
71
72
|
"fix": "eslint --fix . && prettier --write .",
|
|
73
|
+
"ci:publish:snapshot": "pkg-pr-new publish --pnpm",
|
|
72
74
|
"test": "vitest",
|
|
73
75
|
"typecheck": "tsc -b"
|
|
74
76
|
}
|
package/src/checker.test.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it } from 'vitest'
|
|
4
4
|
|
|
5
|
-
import { isObject } from './checker'
|
|
5
|
+
import { isMap, isObject, isSet } from './checker'
|
|
6
6
|
|
|
7
7
|
describe('isObject', () => {
|
|
8
8
|
it('returns true for plain objects', () => {
|
|
@@ -38,3 +38,37 @@ describe('isObject', () => {
|
|
|
38
38
|
expect(isObject(new TestClass())).toBe(true)
|
|
39
39
|
})
|
|
40
40
|
})
|
|
41
|
+
|
|
42
|
+
describe('isMap', () => {
|
|
43
|
+
it('returns true for Map instances', () => {
|
|
44
|
+
expect(isMap(new Map())).toBe(true)
|
|
45
|
+
expect(isMap(new Map([['key', 'value']]))).toBe(true)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('returns false for non-Map values', () => {
|
|
49
|
+
expect(isMap({})).toBe(false)
|
|
50
|
+
expect(isMap([])).toBe(false)
|
|
51
|
+
expect(isMap(new Set())).toBe(false)
|
|
52
|
+
expect(isMap(null)).toBe(false)
|
|
53
|
+
expect(isMap(undefined)).toBe(false)
|
|
54
|
+
expect(isMap('map')).toBe(false)
|
|
55
|
+
expect(isMap(42)).toBe(false)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('isSet', () => {
|
|
60
|
+
it('returns true for Set instances', () => {
|
|
61
|
+
expect(isSet(new Set())).toBe(true)
|
|
62
|
+
expect(isSet(new Set([1, 2, 3]))).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('returns false for non-Set values', () => {
|
|
66
|
+
expect(isSet({})).toBe(false)
|
|
67
|
+
expect(isSet([])).toBe(false)
|
|
68
|
+
expect(isSet(new Map())).toBe(false)
|
|
69
|
+
expect(isSet(null)).toBe(false)
|
|
70
|
+
expect(isSet(undefined)).toBe(false)
|
|
71
|
+
expect(isSet('set')).toBe(false)
|
|
72
|
+
expect(isSet(42)).toBe(false)
|
|
73
|
+
})
|
|
74
|
+
})
|
package/src/checker.ts
CHANGED
|
@@ -6,3 +6,17 @@ export function isObject(
|
|
|
6
6
|
): value is Record<string | symbol | number, unknown> {
|
|
7
7
|
return value != null && typeof value === 'object'
|
|
8
8
|
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checks if the given value is a Map.
|
|
12
|
+
*/
|
|
13
|
+
export function isMap(value: unknown): value is Map<unknown, unknown> {
|
|
14
|
+
return value instanceof Map
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Checks if the given value is a Set.
|
|
19
|
+
*/
|
|
20
|
+
export function isSet(value: unknown): value is Set<unknown> {
|
|
21
|
+
return value instanceof Set
|
|
22
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { DefaultMap } from './default-map'
|
|
6
|
+
|
|
7
|
+
describe('DefaultMap', () => {
|
|
8
|
+
it('creates default values for missing keys', () => {
|
|
9
|
+
const map = new DefaultMap<string, number>(() => 0)
|
|
10
|
+
|
|
11
|
+
expect(map.get('key1')).toBe(0)
|
|
12
|
+
expect(map.get('key2')).toBe(0)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('returns existing values for set keys', () => {
|
|
16
|
+
const map = new DefaultMap<string, number>(() => 0)
|
|
17
|
+
|
|
18
|
+
map.set('key1', 42)
|
|
19
|
+
expect(map.get('key1')).toBe(42)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('stores the default value when accessing missing key', () => {
|
|
23
|
+
const map = new DefaultMap<string, number>(() => 5)
|
|
24
|
+
|
|
25
|
+
const value = map.get('key1')
|
|
26
|
+
expect(value).toBe(5)
|
|
27
|
+
expect(map.has('key1')).toBe(true)
|
|
28
|
+
expect(map.get('key1')).toBe(5)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('works with array factory', () => {
|
|
32
|
+
const map = new DefaultMap<string, string[]>(() => [])
|
|
33
|
+
|
|
34
|
+
map.get('key1').push('item1')
|
|
35
|
+
map.get('key1').push('item2')
|
|
36
|
+
map.get('key2').push('item3')
|
|
37
|
+
|
|
38
|
+
expect(map.get('key1')).toEqual(['item1', 'item2'])
|
|
39
|
+
expect(map.get('key2')).toEqual(['item3'])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('works with object factory', () => {
|
|
43
|
+
const map = new DefaultMap<string, { count: number }>(() => ({
|
|
44
|
+
count: 0,
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
map.get('key1').count += 1
|
|
48
|
+
map.get('key1').count += 1
|
|
49
|
+
map.get('key2').count += 5
|
|
50
|
+
|
|
51
|
+
expect(map.get('key1').count).toBe(2)
|
|
52
|
+
expect(map.get('key2').count).toBe(5)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('accepts initial entries via array', () => {
|
|
56
|
+
const initialEntries: [string, number][] = [
|
|
57
|
+
['a', 1],
|
|
58
|
+
['b', 2],
|
|
59
|
+
['c', 3],
|
|
60
|
+
]
|
|
61
|
+
const map = new DefaultMap<string, number>(() => 0, initialEntries)
|
|
62
|
+
|
|
63
|
+
expect(map.get('a')).toBe(1)
|
|
64
|
+
expect(map.get('b')).toBe(2)
|
|
65
|
+
expect(map.get('c')).toBe(3)
|
|
66
|
+
expect(map.get('d')).toBe(0)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('accepts initial entries via non-array iterable', () => {
|
|
70
|
+
const existingMap = new Map<string, number>([
|
|
71
|
+
['x', 10],
|
|
72
|
+
['y', 20],
|
|
73
|
+
['z', 30],
|
|
74
|
+
])
|
|
75
|
+
const map = new DefaultMap<string, number>(() => 0, existingMap)
|
|
76
|
+
|
|
77
|
+
expect(map.get('x')).toBe(10)
|
|
78
|
+
expect(map.get('y')).toBe(20)
|
|
79
|
+
expect(map.get('z')).toBe(30)
|
|
80
|
+
expect(map.get('w')).toBe(0)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('calls factory function only when key is missing', () => {
|
|
84
|
+
let callCount = 0
|
|
85
|
+
const map = new DefaultMap<string, number>(() => {
|
|
86
|
+
callCount++
|
|
87
|
+
return 42
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
map.set('existing', 100)
|
|
91
|
+
|
|
92
|
+
map.get('existing')
|
|
93
|
+
expect(callCount).toBe(0)
|
|
94
|
+
|
|
95
|
+
map.get('new')
|
|
96
|
+
expect(callCount).toBe(1)
|
|
97
|
+
|
|
98
|
+
map.get('new')
|
|
99
|
+
expect(callCount).toBe(1)
|
|
100
|
+
|
|
101
|
+
map.get('another')
|
|
102
|
+
expect(callCount).toBe(2)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('works with size property', () => {
|
|
106
|
+
const map = new DefaultMap<string, number>(() => 0)
|
|
107
|
+
|
|
108
|
+
expect(map.size).toBe(0)
|
|
109
|
+
|
|
110
|
+
map.get('key1')
|
|
111
|
+
expect(map.size).toBe(1)
|
|
112
|
+
|
|
113
|
+
map.get('key2')
|
|
114
|
+
expect(map.size).toBe(2)
|
|
115
|
+
|
|
116
|
+
map.get('key1')
|
|
117
|
+
expect(map.size).toBe(2)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('works with delete method', () => {
|
|
121
|
+
const map = new DefaultMap<string, number>(() => 10)
|
|
122
|
+
|
|
123
|
+
map.get('key1')
|
|
124
|
+
expect(map.has('key1')).toBe(true)
|
|
125
|
+
|
|
126
|
+
map.delete('key1')
|
|
127
|
+
expect(map.has('key1')).toBe(false)
|
|
128
|
+
|
|
129
|
+
const value = map.get('key1')
|
|
130
|
+
expect(value).toBe(10)
|
|
131
|
+
expect(map.has('key1')).toBe(true)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('works with clear method', () => {
|
|
135
|
+
const map = new DefaultMap<string, number>(() => 0)
|
|
136
|
+
|
|
137
|
+
map.get('key1')
|
|
138
|
+
map.get('key2')
|
|
139
|
+
expect(map.size).toBe(2)
|
|
140
|
+
|
|
141
|
+
map.clear()
|
|
142
|
+
expect(map.size).toBe(0)
|
|
143
|
+
expect(map.has('key1')).toBe(false)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('works with iteration methods', () => {
|
|
147
|
+
const map = new DefaultMap<string, number>(() => 0, [
|
|
148
|
+
['a', 1],
|
|
149
|
+
['b', 2],
|
|
150
|
+
])
|
|
151
|
+
|
|
152
|
+
expect([...map.keys()]).toEqual(['a', 'b'])
|
|
153
|
+
expect([...map.values()]).toEqual([1, 2])
|
|
154
|
+
expect([...map.entries()]).toEqual([
|
|
155
|
+
['a', 1],
|
|
156
|
+
['b', 2],
|
|
157
|
+
])
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('creates independent default values', () => {
|
|
161
|
+
const map = new DefaultMap<string, number[]>(() => [])
|
|
162
|
+
|
|
163
|
+
map.get('key1').push(1)
|
|
164
|
+
map.get('key2').push(2)
|
|
165
|
+
|
|
166
|
+
expect(map.get('key1')).toEqual([1])
|
|
167
|
+
expect(map.get('key2')).toEqual([2])
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('works with nested DefaultMaps', () => {
|
|
171
|
+
const map = new DefaultMap<string, DefaultMap<string, number>>(
|
|
172
|
+
() => new DefaultMap<string, number>(() => 0),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
map.get('group1').get('item1')
|
|
176
|
+
map.get('group1').set('item2', 5)
|
|
177
|
+
map.get('group2').get('item1')
|
|
178
|
+
|
|
179
|
+
expect(map.get('group1').get('item1')).toBe(0)
|
|
180
|
+
expect(map.get('group1').get('item2')).toBe(5)
|
|
181
|
+
expect(map.get('group2').get('item1')).toBe(0)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A map that automatically creates values for missing keys using a factory function.
|
|
3
|
+
*
|
|
4
|
+
* Similar to Python's [defaultdict](https://docs.python.org/3.13/library/collections.html#collections.defaultdict).
|
|
5
|
+
*/
|
|
6
|
+
export class DefaultMap<K, V> extends Map<K, V> {
|
|
7
|
+
private readonly defaultFactory: () => V
|
|
8
|
+
|
|
9
|
+
constructor(defaultFactory: () => V, iterable?: Iterable<readonly [K, V]>) {
|
|
10
|
+
super(iterable)
|
|
11
|
+
this.defaultFactory = defaultFactory
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override get(key: K): V {
|
|
15
|
+
if (this.has(key)) {
|
|
16
|
+
return super.get(key)!
|
|
17
|
+
}
|
|
18
|
+
const value = this.defaultFactory()
|
|
19
|
+
this.set(key, value)
|
|
20
|
+
return value
|
|
21
|
+
}
|
|
22
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from './checker'
|
|
2
|
+
export { DefaultMap } from './default-map'
|
|
2
3
|
export * from './dom'
|
|
3
4
|
export { formatBytes } from './format-bytes'
|
|
4
5
|
export { getId } from './get-id'
|
|
5
6
|
export { once } from './once'
|
|
6
7
|
export { sleep } from './sleep'
|
|
8
|
+
export { isDeepEqual } from './is-deep-equal'
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { isDeepEqual } from './is-deep-equal'
|
|
6
|
+
|
|
7
|
+
describe('isDeepEqual', () => {
|
|
8
|
+
describe('primitives', () => {
|
|
9
|
+
it('returns true for identical primitives', () => {
|
|
10
|
+
expect(isDeepEqual(42, 42)).toBe(true)
|
|
11
|
+
expect(isDeepEqual('hello', 'hello')).toBe(true)
|
|
12
|
+
expect(isDeepEqual(true, true)).toBe(true)
|
|
13
|
+
expect(isDeepEqual(false, false)).toBe(true)
|
|
14
|
+
expect(isDeepEqual(null, null)).toBe(true)
|
|
15
|
+
expect(isDeepEqual(undefined, undefined)).toBe(true)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('returns false for different primitives', () => {
|
|
19
|
+
expect(isDeepEqual(42, 43)).toBe(false)
|
|
20
|
+
expect(isDeepEqual('hello', 'world')).toBe(false)
|
|
21
|
+
expect(isDeepEqual(true, false)).toBe(false)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('returns false for different types', () => {
|
|
25
|
+
expect(isDeepEqual(42, '42')).toBe(false)
|
|
26
|
+
expect(isDeepEqual(0, false)).toBe(false)
|
|
27
|
+
expect(isDeepEqual('', false)).toBe(false)
|
|
28
|
+
expect(isDeepEqual(null, undefined)).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('handles NaN correctly', () => {
|
|
32
|
+
expect(isDeepEqual(Number.NaN, Number.NaN)).toBe(true)
|
|
33
|
+
expect(isDeepEqual(Number.NaN, 0)).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('returns false when comparing null or undefined with other values', () => {
|
|
37
|
+
expect(isDeepEqual(null, 0)).toBe(false)
|
|
38
|
+
expect(isDeepEqual(undefined, 0)).toBe(false)
|
|
39
|
+
expect(isDeepEqual(null, '')).toBe(false)
|
|
40
|
+
expect(isDeepEqual(undefined, '')).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('arrays', () => {
|
|
45
|
+
it('returns true for identical arrays', () => {
|
|
46
|
+
expect(isDeepEqual([], [])).toBe(true)
|
|
47
|
+
expect(isDeepEqual([1, 2, 3], [1, 2, 3])).toBe(true)
|
|
48
|
+
expect(isDeepEqual(['a', 'b'], ['a', 'b'])).toBe(true)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('returns false for arrays with different lengths', () => {
|
|
52
|
+
expect(isDeepEqual([1, 2], [1, 2, 3])).toBe(false)
|
|
53
|
+
expect(isDeepEqual([1], [])).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('returns false for arrays with different values', () => {
|
|
57
|
+
expect(isDeepEqual([1, 2, 3], [1, 2, 4])).toBe(false)
|
|
58
|
+
expect(isDeepEqual(['a', 'b'], ['a', 'c'])).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('handles nested arrays', () => {
|
|
62
|
+
expect(isDeepEqual([[1, 2], [3, 4]], [[1, 2], [3, 4]])).toBe(true)
|
|
63
|
+
expect(isDeepEqual([[1, 2], [3, 4]], [[1, 2], [3, 5]])).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
describe('objects', () => {
|
|
68
|
+
it('returns true for identical objects', () => {
|
|
69
|
+
expect(isDeepEqual({}, {})).toBe(true)
|
|
70
|
+
expect(isDeepEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('returns false for objects with different keys', () => {
|
|
74
|
+
expect(isDeepEqual({ a: 1 }, { b: 1 })).toBe(false)
|
|
75
|
+
expect(isDeepEqual({ a: 1, b: 2 }, { a: 1 })).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('returns false for objects with different values', () => {
|
|
79
|
+
expect(isDeepEqual({ a: 1 }, { a: 2 })).toBe(false)
|
|
80
|
+
expect(isDeepEqual({ a: 'hello' }, { a: 'world' })).toBe(false)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('handles nested objects', () => {
|
|
84
|
+
expect(isDeepEqual({ a: { b: 1 } }, { a: { b: 1 } })).toBe(true)
|
|
85
|
+
expect(isDeepEqual({ a: { b: 1 } }, { a: { b: 2 } })).toBe(false)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('handles objects with arrays', () => {
|
|
89
|
+
expect(isDeepEqual({ arr: [1, 2, 3] }, { arr: [1, 2, 3] })).toBe(true)
|
|
90
|
+
expect(isDeepEqual({ arr: [1, 2, 3] }, { arr: [1, 2, 4] })).toBe(false)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('Sets', () => {
|
|
95
|
+
it('returns true for identical sets', () => {
|
|
96
|
+
expect(isDeepEqual(new Set(), new Set())).toBe(true)
|
|
97
|
+
expect(isDeepEqual(new Set([1, 2, 3]), new Set([1, 2, 3]))).toBe(true)
|
|
98
|
+
expect(isDeepEqual(new Set(['a', 'b']), new Set(['a', 'b']))).toBe(true)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('returns false for sets with different sizes', () => {
|
|
102
|
+
expect(isDeepEqual(new Set([1, 2]), new Set([1, 2, 3]))).toBe(false)
|
|
103
|
+
expect(isDeepEqual(new Set([1]), new Set())).toBe(false)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('returns false for sets with different values', () => {
|
|
107
|
+
expect(isDeepEqual(new Set([1, 2, 3]), new Set([1, 2, 4]))).toBe(false)
|
|
108
|
+
expect(isDeepEqual(new Set(['a', 'b']), new Set(['a', 'c']))).toBe(
|
|
109
|
+
false,
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('handles set order independence', () => {
|
|
114
|
+
expect(isDeepEqual(new Set([1, 2, 3]), new Set([3, 2, 1]))).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('Maps', () => {
|
|
119
|
+
it('returns true for identical maps', () => {
|
|
120
|
+
expect(isDeepEqual(new Map(), new Map())).toBe(true)
|
|
121
|
+
expect(
|
|
122
|
+
isDeepEqual(
|
|
123
|
+
new Map([
|
|
124
|
+
['a', 1],
|
|
125
|
+
['b', 2],
|
|
126
|
+
]),
|
|
127
|
+
new Map([
|
|
128
|
+
['a', 1],
|
|
129
|
+
['b', 2],
|
|
130
|
+
]),
|
|
131
|
+
),
|
|
132
|
+
).toBe(true)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('returns false for maps with different sizes', () => {
|
|
136
|
+
expect(
|
|
137
|
+
isDeepEqual(
|
|
138
|
+
new Map([['a', 1]]),
|
|
139
|
+
new Map([
|
|
140
|
+
['a', 1],
|
|
141
|
+
['b', 2],
|
|
142
|
+
]),
|
|
143
|
+
),
|
|
144
|
+
).toBe(false)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('returns false for maps with different keys', () => {
|
|
148
|
+
expect(
|
|
149
|
+
isDeepEqual(new Map([['a', 1]]), new Map([['b', 1]])),
|
|
150
|
+
).toBe(false)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('returns false for maps with different values', () => {
|
|
154
|
+
expect(
|
|
155
|
+
isDeepEqual(new Map([['a', 1]]), new Map([['a', 2]])),
|
|
156
|
+
).toBe(false)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('handles nested values in maps', () => {
|
|
160
|
+
expect(
|
|
161
|
+
isDeepEqual(
|
|
162
|
+
new Map([['a', { b: 1 }]]),
|
|
163
|
+
new Map([['a', { b: 1 }]]),
|
|
164
|
+
),
|
|
165
|
+
).toBe(true)
|
|
166
|
+
expect(
|
|
167
|
+
isDeepEqual(
|
|
168
|
+
new Map([['a', { b: 1 }]]),
|
|
169
|
+
new Map([['a', { b: 2 }]]),
|
|
170
|
+
),
|
|
171
|
+
).toBe(false)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('handles arrays as map values', () => {
|
|
175
|
+
expect(
|
|
176
|
+
isDeepEqual(
|
|
177
|
+
new Map([['a', [1, 2, 3]]]),
|
|
178
|
+
new Map([['a', [1, 2, 3]]]),
|
|
179
|
+
),
|
|
180
|
+
).toBe(true)
|
|
181
|
+
expect(
|
|
182
|
+
isDeepEqual(
|
|
183
|
+
new Map([['a', [1, 2, 3]]]),
|
|
184
|
+
new Map([['a', [1, 2, 4]]]),
|
|
185
|
+
),
|
|
186
|
+
).toBe(false)
|
|
187
|
+
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
describe('mixed types', () => {
|
|
191
|
+
it('returns false when comparing different collection types', () => {
|
|
192
|
+
expect(isDeepEqual([], {})).toBe(false)
|
|
193
|
+
expect(isDeepEqual(new Set(), new Map())).toBe(false)
|
|
194
|
+
expect(isDeepEqual(new Set([1, 2]), [1, 2])).toBe(false)
|
|
195
|
+
expect(isDeepEqual(new Map(), {})).toBe(false)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('handles complex nested structures', () => {
|
|
199
|
+
const obj1 = {
|
|
200
|
+
arr: [1, 2, { nested: true }],
|
|
201
|
+
map: new Map([['key', [1, 2, 3]]]),
|
|
202
|
+
set: new Set([1, 2, 3]),
|
|
203
|
+
}
|
|
204
|
+
const obj2 = {
|
|
205
|
+
arr: [1, 2, { nested: true }],
|
|
206
|
+
map: new Map([['key', [1, 2, 3]]]),
|
|
207
|
+
set: new Set([1, 2, 3]),
|
|
208
|
+
}
|
|
209
|
+
const obj3 = {
|
|
210
|
+
arr: [1, 2, { nested: false }],
|
|
211
|
+
map: new Map([['key', [1, 2, 3]]]),
|
|
212
|
+
set: new Set([1, 2, 3]),
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
expect(isDeepEqual(obj1, obj2)).toBe(true)
|
|
216
|
+
expect(isDeepEqual(obj1, obj3)).toBe(false)
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { isMap, isSet } from './checker'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether two values are deeply equal.
|
|
5
|
+
*/
|
|
6
|
+
export function isDeepEqual(a: unknown, b: unknown): boolean {
|
|
7
|
+
if (a === b) {
|
|
8
|
+
return true
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Handle null and undefined early
|
|
12
|
+
if (a == null || b == null) {
|
|
13
|
+
return false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const aType = typeof a
|
|
17
|
+
const bType = typeof b
|
|
18
|
+
if (aType !== bType) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (aType === 'number' && Number.isNaN(a) && Number.isNaN(b)) {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (Array.isArray(a)) {
|
|
27
|
+
if (!Array.isArray(b)) {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
if (a.length !== b.length) {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
const size = a.length
|
|
34
|
+
for (let i = 0; i < size; i++) {
|
|
35
|
+
if (!isDeepEqual(a[i], b[i])) {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (isSet(a)) {
|
|
43
|
+
if (!isSet(b)) {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
if (a.size !== b.size) {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
for (const value of a) {
|
|
50
|
+
if (!b.has(value)) {
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (isMap(a)) {
|
|
58
|
+
if (!isMap(b)) {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
if (a.size !== b.size) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
for (const key of a.keys()) {
|
|
65
|
+
if (!b.has(key)) {
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
if (!isDeepEqual(a.get(key), b.get(key))) {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
76
|
+
const aKeys = Object.keys(a)
|
|
77
|
+
const bKeys = Object.keys(b)
|
|
78
|
+
if (aKeys.length !== bKeys.length) {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
for (const key of aKeys) {
|
|
82
|
+
if (
|
|
83
|
+
!isDeepEqual(
|
|
84
|
+
(a as Record<string, unknown>)[key],
|
|
85
|
+
(b as Record<string, unknown>)[key],
|
|
86
|
+
)
|
|
87
|
+
) {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return false
|
|
95
|
+
}
|