lively.lang
Version:
JavaScript utils providing useful abstractions for working with collections, functions, objects.
625 lines (544 loc) • 19.5 kB
JavaScript
/*
* Utility functions that help to inspect, enumerate, and create JS objects
*/
import { fromString as functionFromString, asScriptOf, argumentNames } from "./function.js";
import { deepEquals as arrayDeepEquals, isSubset, flatten } from "./array.js";
// -=-=-=-=-=-=-=-=-
// internal helper
// -=-=-=-=-=-=-=-=-
// serveral methods in lib/object.js are inspired or derived from
// Prototype JavaScript framework, version 1.6.0_rc1
// (c) 2005-2007 Sam Stephenson
// Prototype is freely distributable under the terms of an MIT-style license.
// For details, see the Prototype web site: http://www.prototypejs.org/
function print(object) {
if (object && Array.isArray(object)) { return '[' + object.map(print) + ']'; }
if (typeof object !== "string") { return String(object); }
var result = String(object);
result = result.replace(/\n/g, '\\n\\\n');
result = result.replace(/(")/g, '\\$1');
result = '\"' + result + '\"';
return result;
}
function indent(str, indentString, depth) {
if (!depth || depth <= 0) return str;
while (depth > 0) { depth--; str = indentString + str; }
return str;
}
var getOwnPropertyDescriptors = (typeof Object.getOwnPropertyDescriptors === "function") ?
Object.getOwnPropertyDescriptors :
function getOwnPropertyDescriptors(object) {
let descriptors = {};
for (let name in object) {
if (!Object.prototype.hasOwnProperty.call(object, name)) continue;
Object.defineProperty(descriptors, name, {
configurable: true,
enumerable: true,
writable: true,
value: Object.getOwnPropertyDescriptor(object, name)
});
}
return descriptors;
}
// show-in-doc
// -=-=-=-=-
// testing
// -=-=-=-=-
function isArray(obj) { /*show-in-doc*/ return Array.isArray(obj); }
function isElement(object) { /*show-in-doc*/ return object && object.nodeType == 1; }
function isFunction(object) { /*show-in-doc*/ return object instanceof Function; }
function isBoolean(object) { /*show-in-doc*/ return typeof object == "boolean"; }
function isString(object) { /*show-in-doc*/ return typeof object == "string"; }
function isNumber(object) { /*show-in-doc*/ return typeof object == "number"; }
function isUndefined(object) { /*show-in-doc*/ return typeof object == "undefined"; }
function isRegExp(object) { /*show-in-doc*/ return object instanceof RegExp; }
function isObject(object) { /*show-in-doc*/ return typeof object == "object"; }
function isPrimitive(obj) {
// show-in-doc
if (!obj) return true;
switch (typeof obj) {
case "string":
case "number":
case "boolean": return true;
}
return false;
}
function isEmpty(object) {
/*show-in-doc*/
for (var key in object)
if (object.hasOwnProperty(key)) return false;
return true;
}
function equals(a, b) {
// Is object `a` structurally equivalent to object `b`? Deep comparison.
if (a === b) return true;
if (!a || !b) return a == b;
if (Array.isArray(a)) return arrayDeepEquals(a, b);
switch (a.constructor) {
case String:
case Date:
case Boolean:
case Number: return a == b;
}
if (typeof a.isEqualNode === "function") return a.isEqualNode(b);
if (typeof a.equals === "function") return a.equals(b);
var seenInA = [];
for (var name in a) {
seenInA.push(name);
if (typeof a[name] === "function") continue;
if (!equals(a[name], b[name])) return false;
}
for (var name in b) {
if (seenInA.indexOf(name) !== -1) continue;
if (typeof b[name] === "function") continue;
if (!equals(b[name], a[name])) return false;
}
return true;
}
// -=-=-=-=-=-
// accessing
// -=-=-=-=-=-
var keys = Object.keys;
function values(object) {
// Example:
// var obj1 = {x: 22}, obj2 = {x: 23, y: {z: 3}};
// obj2.__proto__ = obj1;
// obj.values(obj1) // => [22]
// obj.values(obj2) // => [23,{z: 3}]
return object ? Object.keys(object).map(function(k) { return object[k]; }) : [];
}
function select(obj, keys) {
// return a new object that copies all properties with `keys` from `obj`
var selected = {};
for (var i = 0; i < keys.length; i++) selected[keys[i]] = obj[keys[i]];
return selected;
}
function dissoc(object, keys) {
object = object || {};
var descriptors = getOwnPropertyDescriptors(object);
for (var i = 0; i < keys.length; i++) {
if (keys[i] in descriptors) delete descriptors[keys[i]];
}
return Object.defineProperties({}, descriptors);
}
function addScript(object, funcOrString, optName, optMapping) {
var func = functionFromString(funcOrString);
return asScriptOf(func, object, optName, optMapping);
}
// -=-=-=-=-
// mutation
// -=-=-=-=-
function extend(destination, source) {
// Add all properties of `source` to `destination`.
// Example:
// var dest = {x: 22}, src = {x: 23, y: 24}
// obj.extend(dest, src);
// dest // => {x: 23,y: 24}
var currentCategoryNames = null;
for (var i = 1; i < arguments.length; i++) {
if (typeof arguments[i] == "string") {
var catName = arguments[i];
if (!destination.categories) destination.categories = {};
if (!destination.categories[catName]) destination.categories[catName] = [];
currentCategoryNames = destination.categories[catName];
continue;
}
var source = arguments[i];
for (var property in source) {
var getter = source.__lookupGetter__(property),
setter = source.__lookupSetter__(property);
if (getter)
destination.__defineGetter__(property, getter);
if (setter)
destination.__defineSetter__(property, setter);
if (getter || setter)
continue;
var sourceObj = source[property];
destination[property] = sourceObj;
if (currentCategoryNames)
currentCategoryNames.push(property);
if (typeof sourceObj === "function") {
if (!sourceObj.displayName)
sourceObj.displayName = property;
// remember the module that contains the definition
if (typeof lively !== "undefined"
&& lively.Module && lively.Module.current)
sourceObj.sourceModule = lively.Module.current();
}
}
}
return destination;
}
// -=-=-=-=-
// clone
// -=-=-=-=-
function clone(object) {
// Shallow copy
if (isPrimitive(object)) return object;
if (Array.isArray(object)) return Array.prototype.slice.call(object);
var clone = {};
for (var key in object) {
if (object.hasOwnProperty(key))
clone[key] = object[key];
}
return clone;
}
function extract(object, properties, mapFunc) {
// Takes a list of properties and returns a new object with those
// properties shallow-copied from object
var copied = {};
for (var i = 0; i < properties.length; i++) {
if (properties[i] in object)
copied[properties[i]] = mapFunc ?
mapFunc(properties[i], object[properties[i]]) : object[properties[i]];
}
return copied;
}
// -=-=-=-=-=-
// inspection
// -=-=-=-=-=-
function inspect(object, options, depth) {
// Prints a human-readable representation of `obj`. The printed
// representation will be syntactically correct JavaScript but will not
// necessarily evaluate to a structurally identical object. `inspect` is
// meant to be used while interactivively exploring JavaScript programs and
// state.
//
// `options` can be {
// printFunctionSource: BOOLEAN,
// escapeKeys: BOOLEAN,
// maxDepth: NUMBER,
// customPrinter: FUNCTION,
// maxNumberOfKeys: NUMBER
// }
options = options || {};
depth = depth || 0;
if (options.customPrinter) {
let ignoreSignal = options._ignoreSignal || (options._ignoreSignal = {}),
continueInspectFn = (obj) => inspect(obj, options, depth+1),
customInspected = options.customPrinter(object, ignoreSignal, continueInspectFn);
if (customInspected !== ignoreSignal) return customInspected
}
if (!object) return print(object);
// print function
if (typeof object === 'function') {
return options.printFunctionSource ? String(object) :
'function' + (object.name ? ' ' + object.name : '')
+ '(' + argumentNames(object).join(',') + ') {/*...*/}';
}
// print "primitive"
switch (object.constructor) {
case String:
case Boolean:
case RegExp:
case Number: return print(object);
};
if (typeof object.serializeExpr === 'function')
return object.serializeExpr();
var isArray = object && Array.isArray(object),
openBr = isArray ? '[' : '{', closeBr = isArray ? ']' : '}';
if (options.maxDepth && depth >= options.maxDepth)
return openBr + '/*...*/' + closeBr;
var printedProps = [];
if (isArray) {
printedProps = object.map(function(ea) { return inspect(ea, options, depth + 1); });
} else {
const propsToPrint = Object.keys(object)
.sort(function(a, b) {
var aIsFunc = typeof object[a] === 'function',
bIsFunc = typeof object[b] === 'function';
if (aIsFunc === bIsFunc) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
}
return aIsFunc ? 1 : -1;
});
for (let i = 0; i<propsToPrint.length; i++) {
if (i > (options.maxNumberOfKeys || Infinity)) {
const hiddenEntryCount = propsToPrint.length - i;
printedProps.push(`...${hiddenEntryCount} hidden ${hiddenEntryCount > 1 ? 'entries' : 'entry'}...`)
break;
}
const key = propsToPrint[i];
if (isArray) inspect(object[key], options, depth + 1);
var printedVal = inspect(object[key], options, depth + 1);
printedProps.push((options.escapeKeys ?
JSON.stringify(key) : key) + ": " + printedVal);
}
}
if (printedProps.length === 0) { return openBr + closeBr; }
var printedPropsJoined = printedProps.join(', '),
useNewLines = (!isArray || options.newLineInArrays)
&& (!options.minLengthForNewLine
|| printedPropsJoined.length >= options.minLengthForNewLine),
ind = indent('', options.indent || ' ', depth),
propIndent = indent('', options.indent || ' ', depth + 1),
startBreak = useNewLines && !isArray ? '\n' + propIndent : '',
eachBreak = useNewLines ? '\n' + propIndent : '',
endBreak = useNewLines && !isArray ? '\n' + ind : '';
if (useNewLines) printedPropsJoined = printedProps.join(',' + eachBreak);
return openBr + startBreak + printedPropsJoined + endBreak + closeBr;
}
// -=-=-=-=-
// merging
// -=-=-=-=-
function merge(objs) {
// `objs` can be a list of objects. The return value will be a new object,
// containing all properties of all objects. If the same property exist in
// multiple objects, the right-most property takes precedence.
//
// Like `extend` but will not mutate objects in `objs`.
// if objs are arrays just concat them
// if objs are real objs then merge propertdies
if (arguments.length > 1) {
return merge(Array.prototype.slice.call(arguments));
}
if (Array.isArray(objs[0])) { // test for all?
return Array.prototype.concat.apply([], objs);
}
return objs.reduce(function(merged, ea) {
for (var name in ea)
if (ea.hasOwnProperty(name))
merged[name] = ea[name];
return merged;
}, {});
}
function deepMerge(objA, objB) {
// `objs` can be a list of objects. The return value will be a new object,
// containing all properties of all objects. If the same property exist in
// multiple objects, the right-most property takes precedence.
//
// Like `extend` but will not mutate objects in `objs`.
// if objs are arrays just concat them
// if objs are real objs then merge propertdies
if (!objA) return objB;
if (!objB) return objA;
if (Array.isArray(objA)) {
if (!Array.isArray(objB)) return objB;
var merged = objA.map(function(ea, i) { return deepMerge(ea, objB[i]); });
if (objB.length > objA.length) merged = merged.concat(objB.slice(objA.length));
return merged;
}
if (typeof objA !== "object" || typeof objB !== "object") return objB;
return Object.keys(objA).concat(Object.keys(objB)).reduce(function(merged, name) {
if (!objA[name]) merged[name] = objB[name];
else if (!objB[name]) merged[name] = objA[name];
else if (typeof objA[name] !== "object" || typeof objB[name] !== "object") merged[name] = objB[name];
else merged[name] = deepMerge(objA[name], objB[name]);
return merged;
}, {});
}
function sortKeysWithBeforeAndAfterConstraints(properties, throwErrorOnMissing = false) {
// Expects `properties` to be a map of keys to objects having optional
// before/after attributes that, if present, should be lists of other property
// keys. `sortProperties` will return an ordered list of property keys so
// that the before / after requirements are fullfilled. If a cyclic
// dependency is encountered an error will be thrown.
// Example:
// ```
// sortProperties({foo: {}, bar: {after: ["foo"], before: ["baz"]}, "baz": {after: ["foo"]}})
// // => ["foo","bar","baz"]
// ```
// ignore-in-doc
// 1. convert "before" requirement into "after" and check if all properties
// mentioned in after/before are actually there
var keys = [], props = [], remaining = [];
for (var key in properties) {
var prop = properties[key],
before = prop.hasOwnProperty("before") ? prop.before : (prop.before = []),
after = prop.hasOwnProperty("after") ? prop.after : (prop.after = []);
keys.push(key);
props.push(prop);
for (let i = before.length; i--; ) {
var beforePropName = before[i];
var beforeProp = properties[beforePropName];
if (!beforeProp) {
console.warn(`[initializeProperties] ${this} sortProperties: `
+ `Property ${key} requires to be initialized before ${beforePropName} `
+ `but that property cannot be found.`);
before.splice(i, 1)
continue;
}
if (!beforeProp.hasOwnProperty("after")) beforeProp.after = [];
beforeProp.after.push(key);
}
for (let i = after.length; i--; ) {
var afterPropName = after[i];
var afterProp = properties[afterPropName];
if (!afterProp) {
console.warn(`[initializeProperties] ${this} sortProperties: `
+ `Property ${key} requires to be initialized after ${afterPropName} `
+ `but that property cannot be found.`);
after.splice(i, 1);
}
}
remaining.push(key);
}
// ignore-in-doc
// compute order
var resolvedGroups = [],
resolvedKeys = [],
lastLength = remaining.length + 1;
while (remaining.length) {
if (lastLength === remaining.length)
throw new Error("Circular dependencies in handler order, could not resolve properties "
+ remaining.map(key => {
var before = properties[key].before, after = properties[key].after;
if ((!before || !before.length) && (!after || !after.length)) return "";
var report = `${key}\n`;
if (before && before.length) report += ` - before ${before.join(",")}\n`;
if (after && after.length) report += ` - after ${after.join(",")}\n`;
return report;
}).join(""));
lastLength = remaining.length;
var resolvedGroup = [];
for (let i = remaining.length; i--; ) {
let key = remaining[i];
if (isSubset(properties[key].after, resolvedKeys)) {
remaining.splice(i, 1);
resolvedKeys.push(key);
resolvedGroup.push(key);
}
}
resolvedGroups.push(resolvedGroup);
}
return flatten(resolvedGroups, 1);
}
// -=-=-=-=-=-=-
// inheritance
// -=-=-=-=-=-=-
function inherit(obj) { return Object.create(obj); }
function valuesInPropertyHierarchy(obj, name) {
// Lookup all properties named name in the proto hierarchy of obj.
// Example:
// var a = {foo: 3}, b = Object.create(a), c = Object.create(b);
// c.foo = 4;
// obj.valuesInPropertyHierarchy(c, "foo") // => [3,4]
var result = [], lookupObj = obj;
while (lookupObj) {
if (lookupObj.hasOwnProperty(name)) result.unshift(lookupObj[name])
lookupObj = Object.getPrototypeOf(lookupObj);
}
return result;
}
function mergePropertyInHierarchy(obj, propName) {
// like `merge` but automatically gets all definitions of the value in the
// prototype chain and merges those.
// Example:
// var o1 = {x: {foo: 23}}, o2 = {x: {foo: 24, bar: 15}}, o3 = {x: {baz: "zork"}};
// o2.__proto__ = o1; o3.__proto__ = o2;
// obj.mergePropertyInHierarchy(o3, "x");
// // => {bar: 15, baz: "zork",foo: 24}
return merge(valuesInPropertyHierarchy(obj, propName));
}
function deepCopy (object) {
// Recursively traverses `object` and its properties to create a copy.
if (!object || typeof object !== "object" || object instanceof RegExp) return object;
var result = Array.isArray(object) ? Array(object.length) : {};
for (var key in object) {
if (object.hasOwnProperty(key))
result[key] = deepCopy(object[key]);
}
return result;
}
// -=-=-=-=-=-=-=-=-
// stringification
// -=-=-=-=-=-=-=-=-
function typeStringOf(obj) {
// ignore-in-doc
if (obj === null) return "null";
if (typeof obj === "undefined") return "undefined";
return obj.constructor.name;
}
function shortPrintStringOf(obj) {
// ignore-in-doc
// primitive values
if (!isMutableType(obj)) return safeToString(obj);
// constructed objects
if (obj.constructor.name !== 'Object' && !Array.isArray(obj)) {
if(obj.constructor.name)
return obj.constructor.name ?
obj.constructor.name :
Object.prototype.toString.call(obj).split(" ")[1].split("]")[0];
}
// arrays or plain objects
var typeString = "";
function displayTypeAndLength(obj, collectionType, firstBracket, secondBracket) {
if (obj.constructor.name === collectionType) {
typeString += firstBracket;
if (obj.length || Object.keys(obj).length) typeString += "...";
typeString += secondBracket;
}
}
displayTypeAndLength(obj, "Object", "{", "}");
displayTypeAndLength(obj, "Array", "[", "]");
return typeString;
}
function isMutableType(obj) {
// Is `obj` a value or mutable type?
var immutableTypes = ["null", "undefined", "Boolean", "Number", "String"];
return immutableTypes.indexOf(typeStringOf(obj)) === -1;
}
function safeToString(obj) {
// Like `toString` but catches errors.
try {
return (obj ? obj.toString() : String(obj)).replace('\n','');
} catch (e) { return '<error printing object>'; }
}
function asObject(obj) {
switch (typeof obj) {
case 'string':
return new String(obj);
case 'boolean':
return new Boolean(obj);
case 'number':
return new Number(obj);
default:
return obj;
}
}
function newKeyIn(obj, base = "_") {
var i = 1, key;
do {
key = base + "-" + i++;
} while (key in obj);
return key;
}
export {
isArray,
isElement,
isFunction,
isBoolean,
isString,
isNumber,
isUndefined,
isRegExp,
isObject,
isPrimitive,
isEmpty,
equals,
keys,
values,
select,
dissoc,
addScript,
extend,
clone,
extract,
inspect,
merge,
deepMerge,
inherit,
valuesInPropertyHierarchy,
mergePropertyInHierarchy,
sortKeysWithBeforeAndAfterConstraints,
deepCopy,
typeStringOf,
shortPrintStringOf,
isMutableType,
safeToString,
asObject,
newKeyIn,
getOwnPropertyDescriptors
}