You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1033 lines
28 KiB

// a decorator for functions that curry "polymorphically",
// that is, that return a function that can be tested
// against various objects if they're only "partially
// completed", or fewer arguments than needed are used.
//
// this enables the idioms:
// [1, 2, 3].every(lt(4)) eq true
// [1, 2, 3].map(add(1)) eq [2, 3, 4]
// [{}, {}, {}].forEach(set('a', 10))
//
exports.operator = function (name, length, block) {
var operator = function () {
var args = exports.array(arguments);
var completion = function (object) {
if (
typeof object == "object" &&
object !== null && // seriously? typeof null == "object"
name in object && // would throw if object === null
// not interested in literal objects:
!Object.prototype.hasOwnProperty.call(object, name)
)
return object[name].apply(object, args);
return block.apply(
this,
[object].concat(args)
);
};
if (arguments.length < length) {
// polymoprhic curry, delayed completion
return completion;
} else {
// immediate completion
return completion.call(this, args.shift());
}
};
operator.name = name;
operator.displayName = name;
operator.length = length;
operator.operator = block;
return operator;
};
exports.no = function (value) {
return value === null || value === undefined;
};
// object
exports.object = exports.operator('toObject', 1, function (object) {
var items = object;
if (!items.length)
items = exports.items(object);
var copy = {};
for (var i = 0; i < items.length; i++) {
var item = items[i];
var key = item[0];
var value = item[1];
copy[key] = value;
}
return copy;
});
exports.object.copy = function (object) {
var copy = {};
exports.object.keys(object).forEach(function (key) {
copy[key] = object[key];
});
return copy;
};
exports.object.deepCopy = function (object) {
var copy = {};
exports.object.keys(object).forEach(function (key) {
copy[key] = exports.deepCopy(object[key]);
});
return copy;
};
exports.object.eq = function (a, b, stack) {
return (
!exports.no(a) && !exports.no(b) &&
exports.array.eq(
exports.sort(exports.object.keys(a)),
exports.sort(exports.object.keys(b))
) &&
exports.object.keys(a).every(function (key) {
return exports.eq(a[key], b[key], stack);
})
);
};
exports.object.len = function (object) {
return exports.object.keys(object).length;
};
exports.object.has = function (object, key) {
return Object.prototype.hasOwnProperty.call(object, key);
};
exports.object.keys = function (object) {
var keys = [];
for (var key in object) {
if (exports.object.has(object, key))
keys.push(key);
}
return keys;
};
exports.object.values = function (object) {
var values = [];
exports.object.keys(object).forEach(function (key) {
values.push(object[key]);
});
return values;
};
exports.object.items = function (object) {
var items = [];
exports.object.keys(object).forEach(function (key) {
items.push([key, object[key]]);
});
return items;
};
/**
* Updates an object with the properties from another object.
* This function is variadic requiring a minimum of two arguments.
* The first argument is the object to update. Remaining arguments
* are considered the sources for the update. If multiple sources
* contain values for the same property, the last one with that
* property in the arguments list wins.
*
* example usage:
* util.update({}, { hello: "world" }); // -> { hello: "world" }
* util.update({}, { hello: "world" }, { hello: "le monde" }); // -> { hello: "le monde" }
*
* @returns Updated object
* @type Object
*
*/
exports.object.update = function () {
return variadicHelper(arguments, function(target, source) {
var key;
for (key in source) {
if (exports.object.has(source, key)) {
target[key] = source[key];
}
}
});
};
exports.object.complete = function () {
return variadicHelper(arguments, function(target, source) {
var key;
for (key in source) {
if (
exports.object.has(source, key) &&
!exports.object.has(target, key)
) {
target[key] = source[key];
}
}
});
};
exports.object.repr = function (object) {
return "{" +
exports.object.keys(object)
.map(function (key) {
return exports.enquote(key) + ": " +
exports.repr(object[key]);
}).join(", ") +
"}";
};
/**
* @param args Arguments list of the calling function
* First argument should be a callback that takes target and source parameters.
* Second argument should be target.
* Remaining arguments are treated a sources.
*
* @returns Target
* @type Object
*/
var variadicHelper = function (args, callback) {
var sources = Array.prototype.slice.call(args);
var target = sources.shift();
sources.forEach(function(source) {
callback(target, source);
});
return target;
};
// array
exports.array = function (array) {
if (exports.no(array))
return [];
if (!exports.isArrayLike(array)) {
if (
array.toArray &&
!Object.prototype.hasOwnProperty.call(array, 'toArray')
) {
return array.toArray();
} else if (
array.forEach &&
!Object.prototype.hasOwnProperty.call(array, 'forEach')
) {
var results = [];
array.forEach(function (value) {
results.push(value);
});
return results;
} else {
return exports.items(array);
}
}
return Array.prototype.slice.call(array);
};
exports.array.coerce = function (array) {
if (!Array.isArray(array))
return exports.array(array);
return array;
};
exports.isArrayLike = function(object) {
return Array.isArray(object) || exports.isArguments(object);
};
// from http://code.google.com/p/google-caja/wiki/NiceNeighbor
// by "kangax"
//
// Mark Miller posted a solution that will work in ES5 compliant
// implementations, that may provide future insight:
// (http://groups.google.com/group/narwhaljs/msg/116097568bae41c6)
exports.isArguments = function (object) {
// ES5 reliable positive
if (Object.prototype.toString.call(object) == "[object Arguments]")
return true;
// for ES5, we will still need a way to distinguish false negatives
// from the following code (in ES5, it is possible to create
// an object that satisfies all of these constraints but is
// not an Arguments object).
// callee should exist
if (
!typeof object == "object" ||
!Object.prototype.hasOwnProperty.call(object, 'callee') ||
!object.callee ||
// It should be a Function object ([[Class]] === 'Function')
Object.prototype.toString.call(object.callee) !== '[object Function]' ||
typeof object.length != 'number'
)
return false;
for (var name in object) {
// both "callee" and "length" should be { DontEnum }
if (name === 'callee' || name === 'length') return false;
}
return true;
};
exports.array.copy = exports.array;
exports.array.deepCopy = function (array) {
return array.map(exports.deepCopy);
};
exports.array.len = function (array) {
return array.length;
};
exports.array.has = function (array, value) {
return Array.prototype.indexOf.call(array, value) >= 0;
};
exports.array.put = function (array, key, value) {
array.splice(key, 0, value);
return array;
};
exports.array.del = function (array, begin, end) {
array.splice(begin, end === undefined ? 1 : (end - begin));
return array;
};
exports.array.eq = function (a, b, stack) {
return exports.isArrayLike(b) &&
a.length == b.length &&
exports.zip(a, b).every(exports.apply(function (a, b) {
return exports.eq(a, b, stack);
}));
};
exports.array.lt = function (a, b) {
var length = Math.max(a.length, b.length);
for (var i = 0; i < length; i++)
if (!exports.eq(a[i], b[i]))
return exports.lt(a[i], b[i]);
return false;
};
exports.array.repr = function (array) {
return "[" + exports.map(array, exports.repr).join(', ') + "]";
};
exports.array.first = function (array) {
return array[0];
};
exports.array.last = function (array) {
return array[array.length - 1];
};
exports.apply = exports.operator('apply', 2, function (args, block) {
return block.apply(this, args);
});
exports.copy = exports.operator('copy', 1, function (object) {
if (exports.no(object))
return object;
if (exports.isArrayLike(object))
return exports.array.copy(object);
if (typeof object == 'object')
return exports.object.copy(object);
return object;
});
exports.deepCopy = exports.operator('deepCopy', 1, function (object) {
if (exports.no(object))
return object;
if (exports.isArrayLike(object))
return exports.array.deepCopy(object);
if (typeof object == 'object')
return exports.object.deepCopy(object);
return object;
});
exports.repr = exports.operator('repr', 1, function (object) {
if (exports.no(object))
return String(object);
if (exports.isArrayLike(object))
return exports.array.repr(object);
if (typeof object == 'object')
return exports.object.repr(object);
if (typeof object == 'string')
return exports.enquote(object);
return object.toString();
});
exports.keys = exports.operator('keys', 1, function (object) {
if (exports.isArrayLike(object))
return exports.range(object.length);
else if (typeof object == 'object')
return exports.object.keys(object);
return [];
});
exports.values = exports.operator('values', 1, function (object) {
if (exports.isArrayLike(object))
return exports.array(object);
else if (typeof object == 'object')
return exports.object.values(object);
return [];
});
exports.items = exports.operator('items', 1, function (object) {
if (exports.isArrayLike(object) || typeof object == "string")
return exports.enumerate(object);
else if (typeof object == 'object')
return exports.object.items(object);
return [];
});
exports.len = exports.operator('len', 1, function (object) {
if (exports.isArrayLike(object))
return exports.array.len(object);
else if (typeof object == 'object')
return exports.object.len(object);
});
exports.has = exports.operator('has', 2, function (object, value) {
if (exports.isArrayLike(object))
return exports.array.has(object, value);
else if (typeof object == 'object')
return exports.object.has(object, value);
return false;
});
exports.get = exports.operator('get', 2, function (object, key, value) {
if (typeof object == "string") {
if (!typeof key == "number")
throw new Error("TypeError: String keys must be numbers");
if (!exports.has(exports.range(object.length), key)) {
if (arguments.length == 3)
return value;
throw new Error("KeyError: " + exports.repr(key));
}
return object.charAt(key);
}
if (typeof object == "object") {
if (!exports.object.has(object, key)) {
if (arguments.length == 3)
return value;
throw new Error("KeyError: " + exports.repr(key));
}
return object[key];
}
throw new Error("Object does not have keys: " + exports.repr(object));
});
exports.set = exports.operator('set', 3, function (object, key, value) {
object[key] = value;
return object;
});
exports.getset = exports.operator('getset', 3, function (object, key, value) {
if (!exports.has(object, key))
exports.set(object, key, value);
return exports.get(object, key);
});
exports.del = exports.operator('del', 2, function (object, begin, end) {
if (exports.isArrayLike(object))
return exports.array.del(object, begin, end);
delete object[begin];
return object;
});
exports.cut = exports.operator('cut', 2, function (object, key) {
var result = exports.get(object, key);
exports.del(object, key);
return result;
});
exports.put = exports.operator('put', 2, function (object, key, value) {
if (exports.isArrayLike(object))
return exports.array.put(object, key, value);
return exports.set(object, key, value);
});
exports.first = exports.operator('first', 1, function (object) {
return object[0];
});
exports.last = exports.operator('last', 1, function (object) {
return object[object.length - 1];
});
exports.update = exports.operator('update', 2, function () {
var args = Array.prototype.slice.call(arguments);
return exports.object.update.apply(this, args);
});
exports.complete = exports.operator('complete', 2, function (target, source) {
var args = Array.prototype.slice.call(arguments);
return exports.object.complete.apply(this, args);
});
// TODO insert
// TODO remove
// TODO discard
exports.range = function () {
var start = 0, stop = 0, step = 1;
if (arguments.length == 1) {
stop = arguments[0];
} else {
start = arguments[0];
stop = arguments[1];
step = arguments[2] || 1;
}
var range = [];
for (var i = start; i < stop; i += step)
range.push(i);
return range;
};
exports.forEach = function (array, block) {
Array.prototype.forEach.call(
exports.array.coerce(array),
block
);
};
exports.forEachApply = function (array, block) {
Array.prototype.forEach.call(
exports.array.coerce(array),
exports.apply(block)
);
};
exports.map = function (array, block, context) {
return Array.prototype.map.call(
exports.array.coerce(array),
block,
context
);
};
exports.mapApply = function (array, block) {
return Array.prototype.map.call(
exports.array.coerce(array),
exports.apply(block)
);
};
exports.every = exports.operator('every', 2, function (array, block, context) {
return exports.all(exports.map(array, block, context));
});
exports.some = exports.operator('some', 2, function (array, block, context) {
return exports.any(exports.map(array, block, context));
});
exports.all = exports.operator('all', 1, function (array) {
array = exports.array.coerce(array);
for (var i = 0; i < array.length; i++)
if (!array[i])
return false;
return true;
});
exports.any = exports.operator('all', 1, function (array) {
array = exports.array.coerce(array);
for (var i = 0; i < array.length; i++)
if (array[i])
return true;
return false;
});
exports.reduce = exports.operator('reduce', 2, function (array, block, basis) {
array = exports.array.coerce(array);
return array.reduce.apply(array, arguments);
});
exports.reduceRight = exports.operator('reduceRight', 2, function (array, block, basis) {
array = exports.array.coerce(array);
return array.reduceRight.apply(array, arguments);
});
exports.zip = function () {
return exports.transpose(arguments);
};
exports.transpose = function (array) {
array = exports.array.coerce(array);
var transpose = [];
var length = Math.min.apply(this, exports.map(array, function (row) {
return row.length;
}));
for (var i = 0; i < array.length; i++) {
var row = array[i];
for (var j = 0; j < length; j++) {
var cell = row[j];
if (!transpose[j])
transpose[j] = [];
transpose[j][i] = cell;
}
}
return transpose;
};
exports.enumerate = function (array, start) {
array = exports.array.coerce(array);
if (exports.no(start))
start = 0;
return exports.zip(
exports.range(start, start + array.length),
array
);
};
// arithmetic, transitive, and logical operators
exports.is = function (a, b) {
return a === b;
};
exports.eq = exports.operator('eq', 2, function (a, b, stack) {
if (!stack)
stack = [];
if (a === b)
return true;
if (typeof a !== typeof b)
return false;
if (exports.no(a))
return exports.no(b);
if (typeof a == "date")
return a.valueOf() == b.valueOf();
if (typeof a == "regexp")
return a.source == b.source &&
a.global == b.global &&
a.ignoreCase == b.ignoreCase &&
a.multiline == b.multiline;
if (typeof a == "function") {
var caller = stack[stack.length - 1];
return caller !== Object &&
typeof caller != "undefined";
}
if (exports.isArrayLike(a))
return exports.array.eq(
a, b,
stack.concat([a.constructor])
);
if (typeof a == 'object')
return exports.object.eq(
a, b,
stack.concat([a.constructor])
);
return a == b;
});
exports.ne = exports.operator('ne', 2, function (a, b) {
return !exports.eq(a, b);
});
exports.lt = exports.operator('lt', 2, function (a, b) {
if (exports.no(a) != exports.no(b))
return exports.no(a) > exports.no(b);
if (exports.isArrayLike(a) && exports.isArrayLike(b))
return exports.array.lt(a, b);
return a < b;
});
exports.gt = exports.operator('gt', 2, function (a, b) {
return !(exports.lt(a, b) || exports.eq(a, b));
});
exports.le = exports.operator(2, 'le', function (a, b) {
return exports.lt(a, b) || exports.eq(a, b);
});
exports.ge = exports.operator(2, 'ge', function (a, b) {
return !exports.lt(a, b);
});
exports.mul = exports.operator(2, 'mul', function (a, b) {
if (typeof a == "string")
return exports.string.mul(a, b);
return a * b;
});
/*** by
returns a `comparator` that compares
values based on the values resultant from
a given `relation`.
accepts a `relation` and an optional comparator.
To sort a list of objects based on their
"a" key::
objects.sort(by(get("a")))
To get those in descending order::
objects.sort(by(get("a")), desc)
`by` returns a comparison function that also tracks
the arguments you used to construct it. This permits
`sort` and `sorted` to perform a Schwartzian transform
which can increase the performance of the sort
by a factor of 2.
*/
exports.by = function (relation) {
var compare = arguments[1];
if (exports.no(compare))
compare = exports.compare;
var comparator = function (a, b) {
a = relation(a);
b = relation(b);
return compare(a, b);
};
comparator.by = relation;
comparator.compare = compare;
return comparator;
};
exports.compare = exports.operator(2, 'compare', function (a, b) {
if (exports.no(a) != exports.no(b))
return exports.no(b) - exports.no(a);
if (typeof a == "number" && typeof b == "number")
return a - b;
return exports.eq(a, b) ? 0 : exports.lt(a, b) ? -1 : 1;
});
/*** sort
an in-place array sorter that uses a deep comparison
function by default (compare), and improves performance if
you provide a comparator returned by "by", using a
Schwartzian transform.
*/
exports.sort = function (array, compare) {
if (exports.no(compare))
compare = exports.compare;
if (compare.by) {
/* schwartzian transform */
array.splice.apply(
array,
[0, array.length].concat(
array.map(function (value) {
return [compare.by(value), value];
}).sort(function (a, b) {
return compare.compare(a[0], b[0]);
}).map(function (pair) {
return pair[1];
})
)
);
} else {
array.sort(compare);
}
return array;
};
/*** sorted
returns a sorted copy of an array using a deep
comparison function by default (compare), and
improves performance if you provide a comparator
returned by "by", using a Schwartzian transform.
*/
exports.sorted = function (array, compare) {
return exports.sort(exports.array.copy(array), compare);
};
exports.hash = exports.operator(1, 'hash', function (object) {
return '' + object;
});
exports.unique = exports.operator(1, 'unique', function (array, eq, hash) {
var visited = {};
if (!eq) eq = exports.eq;
if (!hash) hash = exports.hash;
return array.filter(function (value) {
var bucket = exports.getset(visited, hash(value), []);
var finds = bucket.filter(function (other) {
return eq(value, other);
});
if (!finds.length)
bucket.push(value);
return !finds.length;
});
});
// string
exports.string = exports.operator(1, 'toString', function (object) {
return '' + object;
});
exports.string.mul = function (string, n) {
return exports.range(n).map(function () {
return string;
}).join('');
};
/*** escape
escapes all characters of a string that are
special to JavaScript and many other languages.
Recognizes all of the relevant
control characters and formats all other
non-printable characters as Hex byte escape
sequences or Unicode escape sequences depending
on their size.
Pass ``true`` as an optional second argument and
``escape`` produces valid contents for escaped
JSON strings, wherein non-printable-characters are
all escaped with the Unicode ``\u`` notation.
*/
/* more Steve Levithan flagrence */
var escapeExpression = /[^ !#-[\]-~]/g;
/* from Doug Crockford's JSON library */
var escapePatterns = {
'\b': '\\b', '\t': '\\t',
'\n': '\\n', '\f': '\\f', '\r': '\\r',
'"' : '\\"', '\\': '\\\\'
};
exports.escape = function (value, strictJson) {
if (typeof value != "string")
throw new Error(
module.path +
"#escape: requires a string. got " +
exports.repr(value)
);
return value.replace(
escapeExpression,
function (match) {
if (escapePatterns[match])
return escapePatterns[match];
match = match.charCodeAt();
if (!strictJson && match < 256)
return "\\x" + exports.padBegin(match.toString(16), 2);
return '\\u' + exports.padBegin(match.toString(16), 4);
}
);
};
/*** enquote
transforms a string into a string literal, escaping
all characters of a string that are special to
JavaScript and and some other languages.
``enquote`` uses double quotes to be JSON compatible.
Pass ``true`` as an optional second argument to
be strictly JSON compliant, wherein all
non-printable-characters are represented with
Unicode escape sequences.
*/
exports.enquote = function (value, strictJson) {
return '"' + exports.escape(value, strictJson) + '"';
};
/*** expand
transforms tabs to an equivalent number of spaces.
*/
// TODO special case for \r if it ever matters
exports.expand = function (str, tabLength) {
str = String(str);
tabLength = tabLength || 4;
var output = [],
tabLf = /[\t\n]/g,
lastLastIndex = 0,
lastLfIndex = 0,
charsAddedThisLine = 0,
tabOffset, match;
while (match = tabLf.exec(str)) {
if (match[0] == "\t") {
tabOffset = (
tabLength - 1 -
(
(match.index - lastLfIndex) +
charsAddedThisLine
) % tabLength
);
charsAddedThisLine += tabOffset;
output.push(
str.slice(lastLastIndex, match.index) +
exports.mul(" ", tabOffset + 1)
);
} else if (match[0] === "\n") {
output.push(str.slice(lastLastIndex, tabLf.lastIndex));
lastLfIndex = tabLf.lastIndex;
charsAddedThisLine = 0;
}
lastLastIndex = tabLf.lastIndex;
}
return output.join("") + str.slice(lastLastIndex);
};
var trimBeginExpression = /^\s\s*/g;
exports.trimBegin = function (value) {
return String(value).replace(trimBeginExpression, "");
};
var trimEndExpression = /\s\s*$/g;
exports.trimEnd = function (value) {
return String(value).replace(trimEndExpression, "");
};
exports.trim = function (value) {
return String(value).replace(trimBeginExpression, "").replace(trimEndExpression, "");
};
/* generates padBegin and padEnd */
var augmentor = function (augment) {
return function (value, length, pad) {
if (exports.no(pad)) pad = '0';
if (exports.no(length)) length = 2;
value = String(value);
while (value.length < length) {
value = augment(value, pad);
}
return value;
};
};
/*** padBegin
accepts:
- a `String` or `Number` value
- a minimum length of the resultant `String`:
by default, 2
- a pad string: by default, ``'0'``.
returns a `String` of the value padded up to at least
the minimum length. adds the padding to the begining
side of the `String`.
*/
exports.padBegin = augmentor(function (value, pad) {
return pad + value;
});
/*** padEnd
accepts:
- a `String` or `Number` value
- a minimum length of the resultant `String`:
by default, 2
- a pad string: by default, ``'0'``.
returns a `String` of the value padded up to at least
the minimum length. adds the padding to the end
side of the `String`.
*/
exports.padEnd = augmentor(function (value, pad) {
return value + pad;
});
/*** splitName
splits a string into an array of words from an original
string.
*/
// thanks go to Steve Levithan for this regular expression
// that, in addition to splitting any normal-form identifier
// in any case convention, splits XMLHttpRequest into
// "XML", "Http", and "Request"
var splitNameExpression = /[a-z]+|[A-Z](?:[a-z]+|[A-Z]*(?![a-z]))|[.\d]+/g;
exports.splitName = function (value) {
return String(value).match(splitNameExpression);
};
/*** joinName
joins a list of words with a given delimiter
between alphanumeric words.
*/
exports.joinName = function (delimiter, parts) {
if (exports.no(delimiter)) delimiter = '_';
parts.unshift([]);
return parts.reduce(function (parts, part) {
if (
part.match(/\d/) &&
exports.len(parts) && parts[parts.length-1].match(/\d/)
) {
return parts.concat([delimiter + part]);
} else {
return parts.concat([part]);
}
}).join('');
};
/*** upper
converts a name to ``UPPER CASE`` using
a given delimiter between numeric words.
see:
- `lower`
- `camel`
- `title`
*/
exports.upper = function (value, delimiter) {
if (exports.no(delimiter))
return value.toUpperCase();
return exports.splitName(value).map(function (part) {
return part.toUpperCase();
}).join(delimiter);
};
/*** lower
converts a name to a ``lower case`` using
a given delimiter between numeric words.
see:
- `upper`
- `camel`
- `title`
*/
exports.lower = function (value, delimiter) {
if (exports.no(delimiter))
return String(value).toLowerCase();
return exports.splitName(value).map(function (part) {
return part.toLowerCase();
}).join(delimiter);
};
/*** camel
converts a name to ``camel Case`` using
a given delimiter between numeric words.
see:
- `lower`
- `upper`
- `title`
*/
exports.camel = function (value, delimiter) {
return exports.joinName(
delimiter,
exports.mapApply(
exports.enumerate(exports.splitName(value)),
function (n, part) {
if (n) {
return (
part.substring(0, 1).toUpperCase() +
part.substring(1).toLowerCase()
);
} else {
return part.toLowerCase();
}
}
)
);
};
/*** title
converts a name to ``Title Case`` using
a given delimiter between numeric words.
see:
- `lower`
- `upper`
- `camel`
*/
exports.title = function (value, delimiter) {
return exports.joinName(
delimiter,
exports.splitName(value).map(function (part) {
return (
part.substring(0, 1).toUpperCase() +
part.substring(1).toLowerCase()
);
})
);
};