cmpstr
Version:
CmpStr is a lightweight, fast and well performing package for calculating string similarity
145 lines (142 loc) • 5.14 kB
JavaScript
// CmpStr v3.0.1 dev-052fa0c-250614 by Paul Köhler @komed3 / MIT License
;
/**
* Deep Merge Utility
* src/utils/DeepMerge.ts
*
* This module provides utility functions for deep merging objects, getting values by path,
* and setting values by path in a deeply nested object structure.
*
* It supports dot and bracket notation (e.g. `a.b[0].c`) as well as escaped keys.
*
* Included functions:
* - `get`: Retrieve a deeply nested value by path
* - `set`: Assign a value to a nested path
* - `merge`: Deeply merge two objects
* - `has`: Check whether a path exists
* - `rmv`: Delete a value at a path
*
* @module Utils/DeepMerge
* @author Paul Köhler
* @license MIT
*/
/**
* Parse a path string into an array of keys.
*
* @param {string} p - The path string, e.g. `a.b.c` or `a[0].b`
* @returns {(string|number)[]} - An array of keys, e.g. `['a', 'b', 'c']` or `['a', 0, 'b']`
*/
const parse = (p) =>
p
.replace(/\[(\d+)]/g, '.$1')
.split('.')
.map((s) => (/^\d+$/.test(s) ? +s : s));
/**
* Deeply get a value from an object by a path string.
*
* @template T - The type of the object to get the value from
* @param {T} t - The object to get the value from
* @param {string} path - The path string, e.g. `a.b.c`
* @param {any} fallback - The default value to return if the path does not exist
* @returns {T|R|undefined} - The value at the specified path, otherwise the default value
*/
function get(t, path, fallback) {
return parse(path).reduce((o, k) => o?.[k] ?? fallback, t);
}
/**
* Deeply set a value in an object by a path string.
*
* @template T - The type of the object to get the value from
* @param {T} t - The object to set the value in
* @param {string} path - The path string, e.g. `a.b.c`
* @param {any} value - The value to set at the specified path
* @returns {T} - The modified object with the value set at the specified path
* @throws {Error} - Throws an error if the key is not a valid identifier
*/
function set(t, path, value) {
// If the path is empty, return the value
if (path === '') return value;
// Split the path into the first key and the rest of the path
const [k, ...r] = parse(path);
// Throw an error if the key is not a valid identifier
if (t !== undefined && (typeof t !== 'object' || t === null))
throw Error(`cannot set property <${k}> of <${JSON.stringify(t)}>`);
// Assign the value to the specified key in the object
return Object.assign(
t ?? (typeof k === 'number' ? [] : Object.create(null)),
{ [k]: set(t?.[k], r.join('.'), value) }
);
}
/**
* Deeply merge two objects, where the second object overrides the first.
*
* @template T - The type of the object to get the value from
* @param {T} t - The target object to merge into
* @param {T} o - The source object to merge from
* @param {boolean} [mergeUndefined=false] - Whether to merge undefined values
* @returns {T} - The merged object
*/
function merge(
t = Object.create(null),
o = Object.create(null),
mergeUndefined = false
) {
// Iterate over the keys of the source object and merge them into the target object
return (
Object.keys(o).forEach((k) => {
const val = o[k];
// If the value is undefined and mergeUndefined is false, skip it
if (!mergeUndefined && val === undefined) return;
// Skip dangerous property names to prevent prototype pollution
if (k === '__proto__' || k === 'constructor') return;
// If the value is an object and not an array, recursively merge it
t[k] =
typeof val === 'object' && !Array.isArray(val)
? merge(
typeof t[k] === 'object' && !Array.isArray(t[k])
? t[k]
: Object.create(null),
val
)
: val;
}),
t
);
}
/**
* Delete a value at a specified path in an object.
*
* @template T - The type of the object to get the value from
* @param {T} t - The object to delete the value from
* @param {string} path - The path string, e.g. `a.b.c`
* @param {boolean} [preserveEmpty=false] - Whether to preserve empty objects/arrays
* @returns {T} - The modified object with the value deleted at the specified path
*/
function rmv(t, path, preserveEmpty = false) {
const r = (o, k, i = 0) => {
const key = k[i];
// Delete the key if it is not an object or if it is the last key in the path
if (!o || typeof o !== 'object') return false;
if (i === k.length - 1) return delete o[key];
if (!r(o[key], k, i + 1)) return false;
// If preserveEmpty is false, check if the object or array is empty
if (!preserveEmpty) {
const val = o[key];
// If the value is an empty array or object, delete the key
if (
typeof val === 'object' &&
((Array.isArray(val) && val.every((v) => v == null)) ||
(!Array.isArray(val) && Object.keys(val).length === 0))
)
delete o[key];
}
return true;
};
r(t, parse(path));
return t;
}
exports.get = get;
exports.merge = merge;
exports.rmv = rmv;
exports.set = set;
//# sourceMappingURL=DeepMerge.cjs.map