/**
* Random set of utilities that didn't fit elsewhere.
* @module Utilities
*/
import Logger from "./logger";
export default class Utilities {
/**
* Stably sorts arrays since `.sort()` has issues.
* @param {Array} list - array to sort
* @param {function} comparator - comparator to sort by
*/
static stableSort(list, comparator) {
const entries = Array(list.length);
// wrap values with initial indices
for (let index = 0; index < list.length; index++) {
entries[index] = [index, list[index]];
}
// sort with fallback based on initial indices
entries.sort(function (a, b) {
const comparison = Number(this(a[1], b[1]));
return comparison || a[0] - b[0];
}.bind(comparator));
// re-map original array to stable sorted values
for (let index = 0; index < list.length; index++) {
list[index] = entries[index][1];
}
}
/**
* Generates an automatically memoizing version of an object.
* @param {Object} object - object to memoize
* @returns {Proxy} the proxy to the object that memoizes properties
*/
static memoizeObject(object) {
const proxy = new Proxy(object, {
get: function(obj, mod) {
if (!obj.hasOwnProperty(mod)) return undefined;
if (Object.getOwnPropertyDescriptor(obj, mod).get) {
const value = obj[mod];
delete obj[mod];
obj[mod] = value;
}
return obj[mod];
},
set: function(obj, mod, value) {
if (obj.hasOwnProperty(mod)) return Logger.err("MemoizedObject", "Trying to overwrite existing property");
obj[mod] = value;
return obj[mod];
}
});
Object.defineProperty(proxy, "hasOwnProperty", {value: function(prop) {
return this[prop] !== undefined;
}});
return proxy;
}
/**
* Wraps the method in a `try..catch` block.
* @param {callable} method - method to wrap
* @param {string} description - description of method
* @returns {callable} wrapped version of method
*/
static suppressErrors(method, description) {
return (...params) => {
try {return method(...params);}
catch (e) {Logger.err("Suppression", "Error occurred in " + description, e);}
};
}
/**
* This only exists because Samo relied on lodash being there... fuck lodash.
* @param {*} anything - whatever you want
*/
static isNil(anything) {
return anything == null;
}
/**
* Format template strings with placeholders (`${placeholder}`) into full strings.
* Quick example: `Utilities.formatString("Hello, ${user}", {user: "Zerebos"})`
* would return "Hello, Zerebos".
* @param {string} string - string to format
* @param {object} values - object literal of placeholders to replacements
* @returns {string} the properly formatted string
*/
static formatTString(string, values) {
for (const val in values) {
let replacement = values[val];
if (Array.isArray(replacement)) replacement = JSON.stringify(replacement);
if (typeof(replacement) === "object" && replacement !== null) replacement = replacement.toString();
string = string.replace(new RegExp(`\\$\\{${val}\\}`, "g"), replacement);
}
return string;
}
/**
* Format strings with placeholders (`{{placeholder}}`) into full strings.
* Quick example: `Utilities.formatString("Hello, {{user}}", {user: "Zerebos"})`
* would return "Hello, Zerebos".
* @param {string} string - string to format
* @param {object} values - object literal of placeholders to replacements
* @returns {string} the properly formatted string
*/
static formatString(string, values) {
for (const val in values) {
let replacement = values[val];
if (Array.isArray(replacement)) replacement = JSON.stringify(replacement);
if (typeof(replacement) === "object" && replacement !== null) replacement = replacement.toString();
string = string.replace(new RegExp(`{{${val}}}`, "g"), replacement);
}
return string;
}
/**
* Finds a value, subobject, or array from a tree that matches a specific filter. Great for patching render functions.
* @param {object} tree React tree to look through. Can be a rendered object or an internal instance.
* @param {callable} searchFilter Filter function to check subobjects against.
*/
static findInReactTree(tree, searchFilter) {
return this.findInTree(tree, searchFilter, {walkable: ["props", "children", "child", "sibling"]});
}
/**
* Finds a value, subobject, or array from a tree that matches a specific filter.
* @param {object} tree Tree that should be walked
* @param {callable} searchFilter Filter to check against each object and subobject
* @param {object} options Additional options to customize the search
* @param {Array<string>|null} [options.walkable=null] Array of strings to use as keys that are allowed to be walked on. Null value indicates all keys are walkable
* @param {Array<string>} [options.ignore=[]] Array of strings to use as keys to exclude from the search, most helpful when `walkable = null`.
*/
static findInTree(tree, searchFilter, {walkable = null, ignore = []} = {}) {
if (typeof searchFilter === "string") {
if (tree.hasOwnProperty(searchFilter)) return tree[searchFilter];
}
else if (searchFilter(tree)) {
return tree;
}
if (typeof tree !== "object" || tree == null) return undefined;
let tempReturn;
if (Array.isArray(tree)) {
for (const value of tree) {
tempReturn = this.findInTree(value, searchFilter, {walkable, ignore});
if (typeof tempReturn != "undefined") return tempReturn;
}
}
else {
const toWalk = walkable == null ? Object.keys(tree) : walkable;
for (const key of toWalk) {
if (!tree.hasOwnProperty(key) || ignore.includes(key)) continue;
tempReturn = this.findInTree(tree[key], searchFilter, {walkable, ignore});
if (typeof tempReturn != "undefined") return tempReturn;
}
}
return tempReturn;
}
/**
* Gets a nested property (if it exists) safely. Path should be something like `prop.prop2.prop3`.
* Numbers can be used for arrays as well like `prop.prop2.array.0.id`.
* @param {Object} obj - object to get nested property of
* @param {string} path - representation of the property to obtain
*/
static getNestedProp(obj, path) {
return path.split(".").reduce(function(ob, prop) {
return ob && ob[prop];
}, obj);
}
/**
* Builds a classname string from any number of arguments. This includes arrays and objects.
* When given an array all values from the array are added to the list.
* When given an object they keys are added as the classnames if the value is truthy.
* Copyright (c) 2018 Jed Watson https://github.com/JedWatson/classnames MIT License
* @param {...Any} argument - anything that should be used to add classnames.
*/
static className() {
const classes = [];
const hasOwn = {}.hasOwnProperty;
for (let i = 0; i < arguments.length; i++) {
const arg = arguments[i];
if (!arg) continue;
const argType = typeof arg;
if (argType === "string" || argType === "number") {
classes.push(arg);
}
else if (Array.isArray(arg) && arg.length) {
const inner = this.classNames.apply(null, arg);
if (inner) {
classes.push(inner);
}
}
else if (argType === "object") {
for (const key in arg) {
if (hasOwn.call(arg, key) && arg[key]) {
classes.push(key);
}
}
}
}
return classes.join(" ");
}
/**
* Safely adds to the prototype of an existing object by checking if the
* property exists on the prototype.
* @param {object} object - Object whose prototype to extend
* @param {string} prop - Name of the prototype property to add
* @param {callable} func - Function to run
*/
static addToPrototype(object, prop, func) {
if (!object.prototype) return;
if (object.prototype[prop]) return;
return object.prototype[prop] = func;
}
/**
* Deep extends an object with a set of other objects. Objects later in the list
* of `extenders` have priority, that is to say if one sets a key to be a primitive,
* it will be overwritten with the next one with the same key. If it is an object,
* and the keys match, the object is extended. This happens recursively.
* @param {object} extendee - Object to be extended
* @param {...object} extenders - Objects to extend with
* @returns {object} - A reference to `extendee`
*/
static extend(extendee, ...extenders) {
for (let i = 0; i < extenders.length; i++) {
for (const key in extenders[i]) {
if (extenders[i].hasOwnProperty(key)) {
if (Array.isArray(extendee[key]) && Array.isArray(extenders[i][key])) this.extend(extendee[key], extenders[i][key]);
else if (typeof extendee[key] === "object" && typeof extenders[i][key] === "object") this.extend(extendee[key], extenders[i][key]);
else if (Array.isArray(extenders[i][key])) extendee[key] = [], this.extend(extendee[key], extenders[i][key]); // eslint-disable-line no-sequences
else if (typeof extenders[i][key] === "object") extendee[key] = {}, this.extend(extendee[key], extenders[i][key]); // eslint-disable-line no-sequences
else extendee[key] = extenders[i][key];
}
}
}
return extendee;
}
/* Code below comes from our work on BDv2:
* https://github.com/JsSucks/BetterDiscordApp/blob/master/common/modules/utils.js
*/
/**
* Clones an object and all it's properties.
* @param {Any} value The value to clone
* @return {Any} The cloned value
*/
static deepclone(value) {
if (typeof value === "object") {
if (Array.isArray(value)) return value.map(i => this.deepclone(i));
const clone = Object.assign({}, value);
for (const key in clone) {
clone[key] = this.deepclone(clone[key]);
}
return clone;
}
return value;
}
/**
* Freezes an object and all it's properties.
* @param {Any} object The object to freeze
* @param {Function} exclude A function to filter object that shouldn't be frozen
*/
static deepfreeze(object, exclude) {
if (exclude && exclude(object)) return;
if (typeof object === "object" && object !== null) {
const properties = Object.getOwnPropertyNames(object);
for (const property of properties) {
this.deepfreeze(object[property], exclude);
}
Object.freeze(object);
}
return object;
}
/**
* Removes an item from an array. This differs from Array.prototype.filter as it mutates the original array instead of creating a new one.
* @param {Array} array The array to filter
* @param {Any} item The item to remove from the array
* @return {Array}
*/
static removeFromArray(array, item, filter) {
let index;
while ((index = filter ? array.findIndex(item) : array.indexOf(item)) > -1) array.splice(index, 1);
return array;
}
/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
* N milliseconds.
*
* Adapted from the version by David Walsh (https://davidwalsh.name/javascript-debounce-function)
*
* @param {function} executor
* @param {number} delay
*/
static debounce(executor, delay) {
let timeout;
return function(...args) {
const callback = () => {
timeout = null;
Reflect.apply(executor, null, args);
};
clearTimeout(timeout);
timeout = setTimeout(callback, delay);
};
}
/**
* Loads data through BetterDiscord's API.
* @param {string} name - name for the file (usually plugin name)
* @param {string} key - which key the data is saved under
* @param {object} defaultData - default data to populate the object with
* @returns {object} the combined saved and default data
*/
static loadData(name, key, defaultData) {
const defaults = this.deepclone(defaultData);
try {
const storedData = BdApi.getData(name, key);
if (typeof(defaults) === "object") return this.extend(defaults, storedData);
return this.isNil(storedData) ? defaults : storedData;
}
catch (err) {
Logger.err(name, "Unable to load data: ", err);
}
return defaults;
}
/**
* Saves data through BetterDiscord's API.
* @param {string} name - name for the file (usually plugin name)
* @param {string} key - which key the data should be saved under
* @param {object} data - data to save
*/
static saveData(name, key, data) {
try {BdApi.setData(name, key, data);}
catch (err) {Logger.err(name, "Unable to save data: ", err);}
}
/**
* Loads settings through BetterDiscord's API.
* @param {string} name - name for the file (usually plugin name)
* @param {object} defaultData - default data to populate the object with
* @returns {object} the combined saved and default settings
*/
static loadSettings(name, defaultSettings) {
return this.loadData(name, "settings", defaultSettings);
}
/**
* Saves settings through BetterDiscord's API.
* @param {string} name - name for the file (usually plugin name)
* @param {object} data - settings to save
*/
static saveSettings(name, data) {
this.saveData(name, "settings", data);
}
}