oxjs/source/Ox.js

5137 lines
160 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// vim: et:ts=4:sw=4:sts=4:ft=javascript
// OxJS (c) 2011 0x2620, dual-licensed GPL/MIT, see http://oxjs.org for details
/*
Some conventions:
Functions
- only one var statement, in the first line of the function
- return only once, from the last line of the function body
Variable names
arg argument
args arguments
arr array
callback callback function
col collection (array, string or object)
date date
fn function
hasFoo boolean
i index (integer key)
isFoo boolean
k key (of a key/value pair)
key key (of a key/value pair)
max maximum value
min minumum value
num number
obj object
re regexp
ret return value
v value (of a key/value pair)
val value (of a key/value pair)
Indentation
var a = 1,
b = 2,
c = 3;
Obj.fn1()
.fn2()
.fn3();
*/
// todo: check http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/
// also see https://github.com/tlrobinson/narwhal/blob/master/lib/util.js
// see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/filter
if (!Array.prototype.filter) {
Array.prototype.filter = function(fn, that) {
if (this === void 0 || this === null || typeof fn !== 'function') {
throw new TypeError();
}
var arr = Object(this),
i,
len = arr.length >>> 0,
ret = [],
val;
for (i = 0; i < len; i++) {
// save val in case fn mutates it
if (i in arr && fn.call(that, val = arr[i], i, arr)) {
ret.push(val);
}
}
return ret;
};
}
// see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(fn, that) {
if (this === void 0 || this === null || typeof fn !== 'function') {
throw new TypeError();
}
var arr = Object(this),
i,
len = arr.length >>> 0;
for (i = 0; i < len; i++) {
if (i in arr) {
fn.call(that, arr[i], i, arr);
}
}
}
}
// see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/indexOf
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(val) {
if (this === void 0 || this === null) {
throw new TypeError();
}
var arr = Object(this),
i,
len = arr.length >>> 0;
ret = -1;
for (i = 0; i < len; i++) {
if (i in arr && arr[i] === val) {
ret = val;
break
}
}
return ret;
}
}
// see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/map
if (!Array.prototype.map) {
Array.prototype.map = function(fn, that) {
if (this === void 0 || this === null || typeof fn !== 'function') {
throw new TypeError();
}
var arr = Object(this),
i,
len = arr.length >>> 0,
ret = new Array(len);
for (i = 0; i < len; i++) {
if (i in arr) {
ret[i] == fn.call(that, arr[i], i, arr);
}
}
return ret;
}
}
// see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/reduce
if (!Array.prototype.reduce) {
Array.prototype.reduce = function reduce(fn, ret) {
if (this === void 0 || this === null || typeof fn !== 'function') {
throw new TypeError();
}
var arr = Object(this),
i,
len = this.length;
if (!len && ret === void 0) {
throw new TypeError();
}
if (ret === void 0) {
ret = arr[0];
i = 1;
}
for (i = i || 0; i < len ; ++i) {
if (i in arr) {
ret = fn.call(void 0, ret, arr[i], i, arr);
}
}
return ret;
};
}
// see https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys
if (!Object.keys) {
Object.keys = function(obj) {
if (obj !== Object(obj)) {
throw new TypeError();
}
var key,
ret = [];
for (key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
ret.push(key);
}
}
return ret;
}
}
//@ Core -----------------------------------------------------------------------
/*@
Ox <f> The <code>Ox</code> object
See <code>Ox.wrap</code> for details.
(value) -> <o> wrapped value
value <*> Any value
@*/
Ox = function(val) {
return Ox.wrap(val);
};
/*@
Ox.load <f> Loads a module
(module, callback) -> <u> undefined
(module, options, callback) -> <u> undefined
(modules, callback) -> <u> undefined
module <s> Module name
modules <o> Multiple modules {name: options}
options <o> Module options
callback <f> Callback function
success <b> If true, the module has been loaded successfully
@*/
Ox.load = function() {
var callback = arguments[arguments.length - 1],
counter = 0,
isObject = Ox.isObject(arguments[0]),
length,
modules = isObject ? arguments[0] : {},
success = 0;
if (!isObject) {
modules[arguments[0]] = Ox.isObject(arguments[1]) ? arguments[1] : {};
}
length = Ox.len(modules)
Ox.forEach(modules, function(options, module) {
Ox.loadFile(Ox.PATH + 'Ox.' + module + '/Ox.' + module + '.js', function() {
Ox.load[module](options, function(s) {
success += s;
++counter == length && callback(success == counter);
});
});
});
};
/*@
Ox.print <f> Prints its arguments to the console
(arg, ...) -> <s> String
The string contains the timestamp, the name of the caller function, and
any arguments, separated by spaces
arg <*> any value
> Ox.print('foo').split(' ').pop()
"foo"
@*/
Ox.print = function() {
var args = Ox.makeArray(arguments),
date = new Date();
args.unshift(
Ox.formatDate(date, '%H:%M:%S.') + (+date).toString().substr(-3),
(arguments.callee.caller && arguments.callee.caller.name)
|| '(anonymous)'
);
window.console && window.console.log.apply(window.console, args);
return args.join(' ');
};
/*@
Ox.uid <f> Returns a unique id
() -> <n> Unique id
> Ox.uid() != Ox.uid()
true
@*/
Ox.uid = (function() {
var uid = 0;
return function() {
return uid++;
};
}());
/*@
Ox.wrap <f> Wraps a value so that one can directly call any Ox function on it
(value) -> <o> wrapped value
chain <f> wrap the return value to allow chaining
value <f> unwrap the value wrapped by <code>chain()</chain>
value <*> Any value
> Ox.wrap("foobar").repeat(2)
"foobarfoobar"
> Ox.wrap("foobar").chain().reverse().toTitleCase().value()
"Raboof"
> Ox.wrap("foobar").value()
"foobar"
@*/
Ox.wrap = function(val, chained) {
// somewhat inspired by underscore.js
var wrapper = {
chain: function() {
wrapper.chained = true;
return wrapper;
},
chained: chained || false,
value: function() {
return val;
}
};
Object.getOwnPropertyNames(Ox).forEach(function(name) {
if (name[0] == name[0].toLowerCase() && Ox.isFunction(Ox[name])) {
wrapper[name] = function() {
var args = Array.prototype.slice.call(arguments), ret;
args.unshift(val);
ret = Ox[name].apply(Ox, args);
return wrapper.chained ? Ox.wrap(ret, true) : ret;
};
}
});
return wrapper;
};
//@ Array ----------------------------------------------------------------------
/*@
Ox.compact <f> Returns an array w/o <code>null</code> or <code>undefined</code>
> Ox.compact([null,,1,,2,,3])
[1, 2, 3]
@*/
Ox.compact = function(arr) {
return Ox.map(arr, function(val) {
return Ox.isUndefined(val) ? null : val;
});
};
/*@
Ox.flatten <f> Flattens an array
> Ox.flatten([1, [2, [3], 2], 1])
[1, 2, 3, 2, 1]
@*/
Ox.flatten = function(arr) {
// fixme: can this work for objects too?
var ret = [];
arr.forEach(function(val) {
if (Ox.isArray(val)) {
Ox.flatten(val).forEach(function(val) {
ret.push(val);
});
} else {
ret.push(val);
}
});
return ret;
};
/*@
Ox.merge <f> Merges an array with one or more other arrays
> Ox.merge([1], [2, 3, 2], [1])
[1, 2, 3, 2, 1]
> Ox.merge(1, [2, 3, 2], 1)
[1, 2, 3, 2, 1]
@*/
Ox.merge = function(arr) {
arr = Ox.isArray(arr) ? arr : [arr];
Ox.forEach(Array.prototype.slice.call(arguments, 1), function(arg) {
Ox.isArray(arg) ? Ox.forEach(arg, function(val) {
arr.push(val);
}) : arr.push(arg);
});
return arr;
};
/*@
Ox.sort <f> Sorts an array, handling leading digits and ignoring capitalization
arr <a> Array
fn <f|u> Optional map function that returns the value for the array element
> Ox.sort(['10', '9', 'B', 'a'])
['9', '10', 'a', 'B']
> Ox.sort([{id: 0, name: '80 Days'}, {id: 1, name: '8 Women'}], function(v) {return v.name});
[{id: 1, name: '8 Women'}, {id: 0, name: '80 Days'}]
@*/
Ox.sort = function(arr, fn) {
var len, matches = {}, sort = {},
values = fn ? arr.map(fn) : arr;
// find leading numbers
values.forEach(function(val, i) {
var match = /^\d+/.exec(val);
matches[val] = match ? match[0] : '';
});
// get length of longest leading number
len = Ox.max(Ox.map(matches, function(val) {
return val.length;
}));
// pad leading numbers, and make lower case
values.forEach(function(val) {
sort[val] = (
matches[val] ? Ox.pad(
matches[val], len
) + val.toString().substr(matches[val].length) : val
).toLowerCase();
});
return arr.sort(function(a, b) {
a = fn ? fn(a) : a;
b = fn ? fn(b) : b;
var ret = 0;
if (sort[a] < sort[b]) {
ret = -1;
} else if (sort[a] > sort[b]) {
ret = 1;
}
return ret;
});
};
/*@
Ox.unique <f> Returns an array without duplicate values
> Ox.unique([1, 2, 3, 2, 1])
[1, 2, 3]
> Ox.unique([NaN, NaN])
[]
@*/
Ox.unique = function(arr) {
return Ox.map(arr, function(val, i) {
return arr.indexOf(val) == i ? val : null;
});
};
/*@
Ox.zip <f> Zips an array of arrays
> Ox.zip([[0, 1], [2, 3], [4, 5]])
[[0, 2, 4], [1, 3, 5]]
> Ox.zip([0, 1, 2], [3, 4, 5])
[[0, 3], [1, 4], [2, 5]]
@*/
Ox.zip = function() {
var args = arguments.length == 1 ? arguments[0] : Ox.makeArray(arguments),
arr = [];
args[0].forEach(function(v, i) {
arr[i] = [];
args.forEach(function(v) {
arr[i].push(v[i]);
});
});
return arr;
};
//@ Collections ----------------------------------------------------------------
/*@
Ox.avg <f> Returns the average of an array's values, or an object's properties
(collection) -> <n> Average value
collection <[n]|o> Array or object with numerical values
> Ox.avg([-1, 0, 1])
0
> Ox.avg({a: 1, b: 2, c: 3})
2
> Ox.avg('avg is 0.1')
0.1
@*/
Ox.avg = function(obj) {
return Ox.sum(obj) / Ox.len(obj);
};
/*@
Ox.clone <f> Returns a (shallow or deep) copy of an object or array
> (function() { a = ['val']; b = Ox.clone(a); a[0] = null; return b[0]; }())
'val'
> (function() { a = {key: 'val'}; b = Ox.clone(a); a.key = null; return b.key; }())
'val'
@*/
Ox.clone = function(col, deep) {
var ret;
if (deep) {
ret = Ox.isArray(col) ? [] : {};
Ox.forEach(col, function(val, key) {
ret[key] = ['array', 'object'].indexOf(Ox.typeOf(val)) > -1
? Ox.clone(val) : val;
});
} else {
ret = Ox.isArray(col) ? col.slice() : Ox.extend({}, col);
}
return ret;
};
/*@
Ox.contains <f> Tests if a collection contains a value
> Ox.contains(['foo', 'bar'], 'foo')
true
> Ox.contains({foo: 'bar'}, 'bar')
true
> Ox.contains({foo: 'bar'}, 'foo')
false
> Ox.contains("foobar", "bar")
true
@*/
Ox.contains = function(col, val) {
/*
// fixme: rename to Ox.has or Ox.isIn?
// then it'd become convenient for arrays
*/
return (Ox.isObject(col) ? Ox.values(col) : col).indexOf(val) > -1;
};
/*@
Ox.count <f> Counts the occurences of values in a collection
> Ox.count(['f', 'o', 'o'])
{f: 1, o: 2}
> Ox.count({a: 'f', b: 'o', c: 'o'})
{f: 1, o: 2}
> Ox.count('foo')
{f: 1, o: 2}
@*/
Ox.count = function(arr) {
var obj = {};
Ox.forEach(arr, function(v) {
obj[v] = (obj[v] || 0) + 1;
});
return obj;
};
/*@
Ox.every <f> Tests if every element of a collection satisfies a given condition
Unlike <code>[].every()</code>, <code>Ox.every()</code> works for arrays,
objects and strings.
> Ox.every([0, 1, 2], function(v, i) { return i == v; })
true
> Ox.every({a: 1, b: 2, c: 3}, function(v) { return v == 1; })
false
> Ox.every("foo", function(v) { return v == 'f'; })
false
> Ox.every([true, true, true])
true
@*/
Ox.every = function(col, fn) {
return Ox.filter(Ox.values(col), fn || function(v) {
return v;
}).length == Ox.len(col);
};
/*@
Ox.filter <f> Filters a collection by a given condition
Unlike <code>[].filter()</code>, <code>Ox.filter()</code> works for arrays,
objects and strings.
> Ox.filter([2, 1, 0], function(v, i) { return v == i; })
[1]
> Ox.keys(Ox.filter({a: 'c', b: 'b', c: 'a'}, function(v, k) { return v == k; }))
['b']
> Ox.filter(' foo bar ', function(v) { return v != ' '; })
'foobar'
@*/
Ox.filter = function(col, fn) {
var type = Ox.typeOf(col),
ret = type == 'array' ? [] : type == 'object' ? {} : '';
Ox.forEach(col, function(v, k) {
if (fn(v, k)) {
if (type == 'array') {
ret.push(v);
} else if (type == 'object') {
ret[k] = v;
} else {
ret += v;
}
}
});
return ret;
};
/*@
Ox.find <f> Returns array elements that match a string
Returns an array of two arrays, the first containing leading matches
(exact match first), the second containing non-leading matches
> Ox.find(['foo', 'bar', 'foobar', 'barfoo'], 'foo')
[['foo', 'foobar'], ['barfoo']]
@*/
// fixme: wouldn't it make more sense to return just one array?
Ox.find = function(arr, str) {
var ret = [[], []];
str = str.toLowerCase();
arr.map(function(v) {
return v.toLowerCase();
}).forEach(function(v, i) {
var index = v.indexOf(str);
index > -1 && ret[index == 0 ? 0 : 1][v == str ? 'unshift' : 'push'](arr[i]);
});
return ret;
};
/*@
Ox.forEach <f> forEach loop
<code>Ox.forEach()</code> loops over arrays, objects and strings.
Returning <code>false</code> from the iterator function acts like a
<code>break</code> statement (unlike <code>[].forEach()</code>, like
<code>$.each()</code>). The arguments of the iterator function are
<code>(value, key)</code> (like <code>[].forEach()</code>, unlike
<code>$.each()</code>).
(collection, callback) <a|o|s> The collection
(collection, callback, includePrototype) <a|o|s> The collection
collection <a|o|s> An array, object or string
callback <f> Callback function
value <*> Value
key <n|s> Key
includePrototype <b|false> If true, include prototype properties
<script>
Ox.test.string = "";
Ox.forEach(["f", "o", "o"], function(v, i) { Ox.test.string += i; });
Ox.forEach({a: "f", b: "o", c: "o"}, function(v, k) { Ox.test.string += k; });
Ox.forEach("foo", function(v) { Ox.test.string += v; });
</script>
> Ox.test.string
"012abcfoo"
@*/
Ox.forEach = function(col, fn, includePrototype) {
var isObject = Ox.isObject(col), key;
// Safari will not loop through an arguments array
col = Ox.isArguments(col) ? Ox.makeArray(col) : col;
for (key in col) {
key = isObject ? key : parseInt(key);
// fixme: fn.call(context, obj[key], key, obj) may be more standard...
if ((
includePrototype || Object.hasOwnProperty.call(col, key)
) && fn(col[key], key) === false) {
break;
}
}
return col;
};
/*@
Ox.getObjectById <f> Returns an array element with a given id
> Ox.getObjectById([{id: "foo", title: "Foo"}, {id: "bar", title: "Bar"}], "foo")
{id: "foo", title: "Foo"}
@*/
// fixme: shouldn't this be getElementById() ?
Ox.getObjectById = function(arr, id) {
var ret = null;
Ox.forEach(arr, function(v) {
if (v.id == id) {
ret = v;
return false;
}
});
return ret;
};
/*@
Ox.getPositionById <f> Returns the index of an array element with a given id
> Ox.getPositionById([{id: "foo", title: "Foo"}, {id: "bar", title: "Bar"}], "foo")
0
@*/
// fixme: shouldn't this be getIndexById() ?
Ox.getPositionById = function(arr, id) {
var ret = -1;
Ox.forEach(arr, function(v, i) {
if (v.id == id) {
ret = i;
return false;
}
});
return ret;
};
// fixme: and what about getElementBy() and getIndexBy() ?
/*@
Ox.getset <f> Generic getter and setter function
See examples for details.
# Usage --------------------------------------------------------------------
Ox.getset(options, args=[]) -> <o> all options
Ox.getset(options, args=[key]) -> <*> options[key]
Ox.getset(options, args=[key, value], callback, context) -> <f|o> context
sets options[key] to value and calls fn(key, value)
if the key/value pair was added or modified
Ox.getset(options, args=[{key: value}], callback, context) -> <f|o> context
sets multiple options and calls fn(key, value)
for every key/value pair that was added or modified
# Arguments ----------------------------------------------------------------
options <obj> Options object (key/value pairs)
args <arr> The arguments "array" of the caller function
callback <fun> Callback function
The callback is called for every key/value pair that was added or
modified.
key <s> Key
value <*> Value
context <obj> The parent object of the caller function (for chaining)
# Examples -----------------------------------------------------------------
<script>
Ox.test.object = new function() {
var options = {},
setOption = function(key, value) {},
that = this;
that.options = function() {
return Ox.getset(options, arguments, setOption, that);
};
return that;
};
</script>
> Ox.test.object.options("key", "val").options("key")
"val"
> Ox.test.object.options({foo: "foo", bar: "bar"}).options()
{"key": "val", "foo": "foo", "bar": "bar"}
@*/
Ox.getset = function(obj, args, callback, context) {
var obj_ = Ox.clone(obj), ret;
if (args.length == 0) {
// getset([])
ret = obj;
} else if (args.length == 1 && !Ox.isObject(args[0])) {
// getset([key])
ret = obj[args[0]];
} else {
// getset([key, val]) or getset([{key: val, ...}])
args = Ox.makeObject(args);
obj = Ox.extend(obj, args);
Ox.forEach(args, function(val, key) {
if (!obj_ || !Ox.isEqual(obj_[key], val)) {
callback && callback(key, val);
}
});
ret = context;
}
return ret;
}
/*@
Ox.isEmpty <f> Returns true if a collection is empty
> Ox.isEmpty([])
true
> Ox.isEmpty({})
true
> Ox.isEmpty('')
true
> Ox.isEmpty(function() {})
true
@*/
Ox.isEmpty = function(val) {
return Ox.len(val) == 0;
};
/*@
Ox.keys <f> Returns the keys of a collection
Unlike <code>Object.keys()</code>, <code>Ox.keys()</code> works for arrays,
objects and strings.
> Ox.keys([1, 2, 3])
[0, 1, 2]
> Ox.keys([1,,3])
[0, 2]
# fixme?
# > Ox.keys([,])
# [0]
> Ox.keys({a: 1, b: 2, c: 3})
['a', 'b', 'c']
> Ox.keys('abc')
[0, 1, 2]
@*/
// fixme: is this really needed? arrays... ok... but strings??
Ox.keys = function(obj) {
var keys = [];
Ox.forEach(obj, function(v, k) {
keys.push(k);
});
return keys.sort();
};
/*@
Ox.last <f> Gets or sets the last element of an array
Unlike <code>foobarbaz[foobarbaz.length - 1]</code>,
<code>Ox.last(foobarbaz)</code> is short.
<script>
Ox.test.array = [1, 2, 3];
</script>
> Ox.last(Ox.test.array)
3
> Ox.last(Ox.test.array, 4)
[1, 2, 4]
> Ox.test.array
[1, 2, 4]
@*/
Ox.last = function(arr, val) {
var ret;
if (arguments.length == 1) {
ret = arr[arr.length - 1];
} else {
arr[arr.length - 1] = val;
ret = arr;
}
return ret;
};
/*@
Ox.len <f> Returns the length of an array, function, object or string
Not to be confused with <code>Ox.length</code>, which is the
<code>length</code> property of the <code>Ox</code> function
(<code>1</code>).
> Ox.len([1, 2, 3])
3
> Ox.len([,])
1
> Ox.len({a: 1, b: 2, c: 3})
3
> Ox.len(function(a, b, c) {})
3
> Ox.len('abc')
3
@*/
Ox.len = function(obj) {
return (Ox.isObject(obj) ? Ox.values(obj) : obj).length;
};
/*@
Ox.loop <f> For-loop, functional-style
Returning <code>false</code> from the iterator function acts like a
<code>break</code> statement. Unlike a <code>for</code> loop,
<code>Ox.loop</code> doesn't leak its counter variable to the outer scope,
but returns it.
(stop, callback) -> <n> Next value
equivalent to <code>for (var i = 0; i < stop; i++)</code>
(start, stop, callback) -> <n> Next value
equivalent to <code>for (var i = start; i < stop; i++)</code> or,
if <code>start</code> is larger than <code>stop</code>,
<code>for (var i = start; i > stop; i--)</code>
(start, stop, step, callback) -> <n> Next value
equivalent to <code>for (var i = start; i < stop; i += step)</code> or,
if <code>step</code> is negative,
<code>for (var i = start; i > stop; i += step)</code>
start <n> Start value
stop <n> Stop value (exclusive)
step <n> Step value
callback <f> Iterator function
i <n> Counter value
> Ox.loop(0, 3, 2, function() {})
4
> Ox.loop(10, function(i) { return i != 4; })
4
@*/
Ox.loop = function() {
var len = arguments.length,
start = len > 2 ? arguments[0] : 0,
stop = arguments[len > 2 ? 1 : 0],
step = len == 4 ? arguments[2] : (start <= stop ? 1 : -1),
callback = arguments[len - 1],
i;
for (i = start; step > 0 ? i < stop : i > stop; i += step) {
if (callback(i) === false) {
break;
};
}
return i;
};
/*@
Ox.makeArray <f> Takes an array-like object and returns a true array
Alias for <code>Array.prototype.slice.call</code>
(value) -> <a> True array
value <*> Array-like object
> (function() { return Ox.makeArray(arguments); }("foo", "bar"))
["foo", "bar"]
> Ox.makeArray("foo")
["f", "o", "o"]
> Ox.makeArray({0: "f", 1: "o", 2: "o", length: 3})
["f", "o", "o"]
@*/
Ox.makeArray = /MSIE/.test(navigator.userAgent) ? function(col) {
var i, len, ret = [];
try {
ret = Array.prototype.slice.call(col);
} catch(e) {
// handle MSIE NodeLists
len = col.length;
for (i = 0; i < len; i++) {
ret[i] = col[i];
}
}
return ret;
} : function(col) {
return Array.prototype.slice.call(col);
}
/*@
Ox.makeObject <f> Takes an array and returns an object
<code>Ox.makeObject</code> is a helper for functions with two alternative
signatures like <code>('key', 'val')</code> and <code>({key: 'val'})</code>.
> (function() { return Ox.makeObject(arguments); }({foo: 1, bar: 2}))
{foo: 1, bar: 2}
> (function() { return Ox.makeObject(arguments); }('foo', 1))
{foo: 1}
> (function() { return Ox.makeObject(arguments); }('foo'))
{}
> (function() { return Ox.makeObject(arguments); }())
{}
@*/
Ox.makeObject = function(obj) {
var ret = {};
if (Ox.isObject(obj[0])) {
// ({foo: 'bar'})
ret = obj[0];
} else if (obj.length) {
// ('foo', 'bar')
ret[obj[0]] = obj[1]
}
return ret;
};
/*@
Ox.map <f> Transforms the values of an array, object or string
Unlike <code>[].map()</code>, <code>Ox.map()</code> works for arrays,
objects and strings. Returning <code>null</code> from the iterator
function will remove the element from the collection.
> Ox.map([0, 0, 0], function(v, i) { return v == i; })
[true, false, false]
> Ox.map({a: 'a', b: 'a', c: 'a'}, function(v, k) { return v == k; })
{a: true, b: false, c: false}
> Ox.map("000", function(v, i) { return v == i; })
[true, false, false]
> Ox.map([0, 1, 2, 4], function(v, i) { return v ? i == v : null; })
[true, true, false]
# fixme?
# > Ox.map([,], function(v, i) { return i; })
# [0]
@*/
Ox.map = function(obj, fn) {
// fixme: return null to filter out is a bit esoteric
var isObject = Ox.isObject(obj),
ret = isObject ? {} : [];
Ox.forEach(obj, function(val, key) {
var map;
if ((map = fn(val, key)) !== null) {
ret[isObject ? key : ret.length] = map;
}
});
return ret;
};
/*@
Ox.max <f> Returns the maximum value of a collection
> Ox.max([1, 2, 3])
3
> Ox.max({a: 1, b: 2, c: 3})
3
> Ox.max('123')
3
@*/
Ox.max = function(col) {
return Math.max.apply(Math, Ox.values(col));
};
/*@
Ox.min <f> Returns the minimum value of a collection
> Ox.min([1, 2, 3])
1
> Ox.min({a: 1, b: 2, c: 3})
1
> Ox.min('123')
1
@*/
Ox.min = function(col) {
return Math.min.apply(Math, Ox.values(col));
};
/*@
Ox.range <f> Python-style range
(stop) -> <[n]> range
Returns an array of integers from <code>0</code> (inclusive) to
<code>stop</code> (exclusive).
(start, stop) -> <[n]> range
Returns an array of integers from <code>start</code> (inclusive) to
<code>stop</code> (exclusive).
(start, stop, step) -> <[n]> range
Returns an array of numbers from <code>start</code> (inclusive) to
<code>stop</code> (exclusive), incrementing by <code>step</step>.
start <n> Start value
stop <n> Stop value
step <n> Step value
> Ox.range(3)
[0, 1, 2]
> Ox.range(1, 4)
[1, 2, 3]
> Ox.range(3, 0)
[3, 2, 1]
> Ox.range(1, 2, 0.5)
[1, 1.5]
> Ox.range(-1, -2, -0.5)
[-1, -1.5]
@*/
Ox.range = function() {
var args = Ox.makeArray(arguments),
arr = [];
args.push(function(i) {
arr.push(i);
});
Ox.loop.apply(null, args);
return arr;
};
/*@
Ox.setPropertyOnce <f> Sets a property once
Given a array of objects, each of which has a property with a boolean
value, this sets exactly one of these to true, and returns the index
of the object whose property is true.
> Ox.setPropertyOnce([{selected: false}, {selected: false}], 'selected')
0
> Ox.setPropertyOnce([{selected: false}, {selected: true}], 'selected')
1
> Ox.setPropertyOnce([{selected: true}, {selected: true}], 'selected')
0
@*/
// fixme: strange name, and shouldn't it return the full array?
Ox.setPropertyOnce = function(arr, str) {
var pos = -1;
Ox.forEach(arr, function(v, i) {
if (pos == -1 && arr[i][str]) {
pos = i;
} else if (pos > -1 && arr[i][str]) {
delete arr[i][str];
}
});
if (pos == -1) {
arr[0][str] = true;
pos = 0;
}
return pos;
};
/*@
Ox.shuffle <f> Randomizes the order of values within a collection
> Ox.shuffle([1, 2, 3]).length
3
> Ox.len(Ox.shuffle({a: 1, b: 2, c: 3}))
3
> Ox.shuffle('123').length
3
@*/
Ox.shuffle = function(col) {
var keys, ret, type = Ox.typeOf(col), values;
function sort() {
return Math.random() - 0.5;
}
if (type == 'array') {
ret = col.sort(sort);
} else if (type == 'object') {
keys = Object.keys(col);
values = Ox.values(col).sort(sort);
ret = {};
keys.forEach(function(key, i) {
ret[key] = values[i]
});
} else if (type == 'string') {
ret = col.split('').sort(sort).join('');
}
return ret;
};
/*@
Ox.some <f> Tests if one or more elements of a collection satisfy a given condition
Unlike <code>[].some()</code>, <code>Ox.some()</code> works for arrays,
objects and strings.
> Ox.some([2, 1, 0], function(i, v) { return i == v; })
true
> Ox.some({a: 1, b: 2, c: 3}, function(v) { return v == 1; })
true
> Ox.some("foo", function(v) { return v == 'f'; })
true
@*/
Ox.some = function(obj, fn) {
return Ox.filter(Ox.values(obj), fn).length > 0;
};
/*@
Ox.sub <f> Returns a substring or sub-array
Ox.sub behaves like collection[start:stop] in Python
(or, for strings, like str.substring() with negative values for stop)
> Ox.sub([1, 2, 3], 1, -1)
[2]
> Ox.sub('foobar', 1)
"oobar"
> Ox.sub('foobar', -1)
"r"
> Ox.sub('foobar', 1, 5)
"ooba"
> Ox.sub('foobar', 1, -1)
"ooba"
> Ox.sub('foobar', -5, 5)
"ooba"
> Ox.sub('foobar', -5, -1)
"ooba"
@*/
Ox.sub = function(col, start, stop) {
stop = Ox.isUndefined(stop) ? col.length : stop;
start = start < 0 ? col.length + start : start;
stop = stop < 0 ? col.length + stop : stop;
return Ox.isArray(col) ? Ox.filter(col, function(val, key) {
return key >= start && key < stop;
}) : col.substring(start, stop);
}
/*@
Ox.sum <f> Returns the sum of the values of a collection
> Ox.sum(1, 2, 3)
6
> Ox.sum([1, 2, 3])
6
> Ox.sum({a: 1, b: 2, c: 3})
6
> Ox.sum('123')
6
> Ox.sum('123foo')
6
> Ox.sum('08', -2, 'foo')
6
@*/
Ox.sum = function(col) {
var sum = 0;
col = arguments.length > 1 ? Ox.makeArray(arguments) : col;
Ox.forEach(col, function(val) {
val = +val;
sum += Ox.isNumber(val) ? val : 0;
});
return sum;
};
Ox.toArray = function(obj) {
// fixme: can this be thrown out?
/*
>>> Ox.toArray('foo')
['foo']
>>> Ox.toArray(['foo'])
['foo']
*/
var arr;
if (Ox.isArray(obj)) {
arr = obj;
} else if (Ox.isArguments(obj)) {
arr = Ox.makeArray(obj);
} else {
arr = [obj];
}
return arr;
};
/*@
Ox.values <f> Returns the values of a collection
> Ox.values([1, 2, 3])
[1, 2, 3]
> Ox.values({a: 1, b: 2, c: 3})
[1, 2, 3]
> Ox.values('abc')
['a', 'b', 'c']
> Ox.values([1,,3])
[1, 3]
@*/
Ox.values = function(col) {
// Ox.values(str) is identical to str.split('')
var values = [];
Ox.forEach(col, function(val) {
values.push(val);
});
return values;
};
/*@
Ox.walk <f> Recursively walk a tree-like key/value store
<script>
Ox.test.number = 0;
Ox.walk({a: 1, b: {c: 2, d: 3}}, function (v) {
Ox.test.number += Ox.isNumber(v) ? v : 0;
});
</script>
> Ox.test.number
6
@*/
Ox.walk = function(obj, fn) {
Ox.forEach(obj, function(val, key) {
fn(val, key, obj);
Ox.walk(obj[key], fn);
});
};
//@ Color ----------------------------------------------------------------------
/*@
Ox.hsl <f> Takes RGB values and returns HSL values
(rgb) <[n]> HSL values
(r, g, b) <[n]> HSL values
rgb <[n]> RGB values
r <n> red
g <n> green
b <n> blue
> Ox.hsl([0, 0, 0])
[0, 0, 0]
> Ox.hsl([255, 255, 255])
[0, 0, 1]
> Ox.hsl(0, 255, 0)
[120, 1, 0.5]
@*/
Ox.hsl = function(rgb) {
if (arguments.length == 3) {
rgb = Ox.makeArray(arguments);
}
rgb = rgb.map(function(val) {
return val / 255;
});
var max = Ox.max(rgb),
min = Ox.min(rgb),
hsl = [0, 0, 0];
hsl[2] = 0.5 * (max + min);
if (max == min) {
hsl[0] = 0;
hsl[1] = 0;
} else {
if (max == rgb[0]) {
hsl[0] = (60 * (rgb[1] - rgb[2]) / (max - min) + 360) % 360;
} else if (max == rgb[1]) {
hsl[0] = 60 * (rgb[2] - rgb[0]) / (max - min) + 120;
} else if (max == rgb[2]) {
hsl[0] = 60 * (rgb[0] - rgb[1]) / (max - min) + 240;
}
if (hsl[2] <= 0.5) {
hsl[1] = (max - min) / (2 * hsl[2]);
} else {
hsl[1] = (max - min) / (2 - 2 * hsl[2]);
}
}
return hsl;
};
/*@
Ox.rgb <f> Takes HSL values and returns RGB values
(hsl) <[n]> RGB values
(h, s, l) <[n]> RGB values
hsl <[n]> HSL values
h <n> hue
s <n> saturation
l <n> lightness
> Ox.rgb([0, 0, 0])
[0, 0, 0]
> Ox.rgb([0, 0, 1])
[255, 255, 255]
> Ox.rgb(120, 1, 0.5)
[0, 255, 0]
@*/
Ox.rgb = function(hsl) {
if (arguments.length == 3) {
hsl = Ox.makeArray(arguments);
}
hsl[0] /= 360;
var rgb = [0, 0, 0],
v1, v2, v3;
if (hsl[1] == 0) {
rgb = [hsl[2], hsl[2], hsl[2]];
} else {
if (hsl[2] < 0.5) {
v2 = hsl[2] * (1 + hsl[1]);
} else {
v2 = hsl[1] + hsl[2] - (hsl[1] * hsl[2]);
}
v1 = 2 * hsl[2] - v2;
rgb.forEach(function(v, i) {
v3 = hsl[0] + (1 - i) * 1/3;
if (v3 < 0) {
v3++;
} else if (v3 > 1) {
v3--;
}
if (v3 < 1/6) {
rgb[i] = v1 + ((v2 - v1) * 6 * v3);
} else if (v3 < 0.5) {
rgb[i] = v2;
} else if (v3 < 2/3) {
rgb[i] = v1 + ((v2 - v1) * 6 * (2/3 - v3));
} else {
rgb[i] = v1;
}
});
}
return rgb.map(function(v) {
return v * 255;
});
};
//@ Constants ------------------------------------------------------------------
//@ Ox.AMPM <[str]> ['AM', 'PM']
Ox.AMPM = ['AM', 'PM'];
//@ Ox.BCAD <[str]> ['BC', 'AD']
Ox.BCAD = ['BC', 'AD'];
// fixme: this is unused, and probably unneeded
//@ Ox.DURATIONS <[str]> ['year', 'month', 'day', 'hour', 'minute', 'second']
Ox.DURATIONS = ['year', 'month', 'day', 'hour', 'minute', 'second'];
//@ Ox.EARTH_RADIUS <number> Radius of the earth in meters
// see http://en.wikipedia.org/wiki/WGS-84
Ox.EARTH_RADIUS = 6378137;
//@ Ox.EARTH_CIRCUMFERENCE <num> Circumference of the earth in meters
Ox.EARTH_CIRCUMFERENCE = Ox.EARTH_RADIUS * 2 * Math.PI;
//@ Ox.HTML_ENTITIES <object> HTML entities for ... (FIXME)
Ox.HTML_ENTITIES = {
'"': '&quot;', '&': '&amp;', "'": '&apos;', '<': '&lt;', '>': '&gt;'
};
//@ Ox.KEYS <object> Names for key codes
// The dot notation ('0.numpad') allows for namespaced events ('key_0.numpad'),
// so that binding to 'key_0' will catch both 'key_0' and 'key_0.numpad'.
Ox.KEYS = {
0: 'section', 8: 'backspace', 9: 'tab', 12: 'clear', 13: 'enter',
16: 'shift', 17: 'control', 18: 'alt', 20: 'capslock', 27: 'escape',
32: 'space', 33: 'pageup', 34: 'pagedown', 35: 'end', 36: 'home',
37: 'left', 38: 'up', 39: 'right', 40: 'down',
45: 'insert', 46: 'delete', 47: 'help',
48: '0', 49: '1', 50: '2', 51: '3', 52: '4',
53: '5', 54: '6', 55: '7', 56: '8', 57: '9',
65: 'a', 66: 'b', 67: 'c', 68: 'd', 69: 'e',
70: 'f', 71: 'g', 72: 'h', 73: 'i', 74: 'j',
75: 'k', 76: 'l', 77: 'm', 78: 'n', 79: 'o',
80: 'p', 81: 'q', 82: 'r', 83: 's', 84: 't',
85: 'u', 86: 'v', 87: 'w', 88: 'x', 89: 'y', 90: 'z',
// fixme: this is usually 91: window.left, 92: window.right, 93: select
91: 'meta.left', 92: 'meta.right', 93: 'meta.right',
96: '0.numpad', 97: '1.numpad', 98: '2.numpad',
99: '3.numpad', 100: '4.numpad', 101: '5.numpad',
102: '6.numpad', 103: '7.numpad', 104: '8.numpad', 105: '9.numpad',
106: 'asterisk.numpad', 107: 'plus.numpad', 109: 'minus.numpad',
108: 'enter.numpad', 110: 'dot.numpad', 111: 'slash.numpad',
112: 'f1', 113: 'f2', 114: 'f3', 115: 'f4', 116: 'f5',
117: 'f6', 118: 'f7', 119: 'f8', 120: 'f9', 121: 'f10',
122: 'f11', 123: 'f12', 124: 'f13', 125: 'f14', 126: 'f15', 127: 'f16',
144: 'numlock', 145: 'scrolllock',
186: 'semicolon', 187: 'equal', 188: 'comma', 189: 'minus',
190: 'dot', 191: 'slash', 192: 'backtick', 219: 'openbracket',
220: 'backslash', 221: 'closebracket', 222: 'quote', 224: 'meta'
// see dojo, for ex.
},
Ox.MAP_TILE_SIZE = 256; // fixme: definitely not needed here
//@ Ox.MODIFIER_KEYS <obj> Names for modifier keys
// meta comes last so that one can differentiate between
// alt_control_shift_meta.left and alt_control_shift_meta.right
Ox.MODIFIER_KEYS = {
altKey: 'alt', // Mac: option
ctrlKey: 'control',
shiftKey: 'shift',
metaKey: 'meta', // Mac: command
}
//@ Ox.MONTHS <[str]> Names of months
Ox.MONTHS = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
//@ Ox.SHORT_MONTHS <[str]> Short names of months
Ox.SHORT_MONTHS = Ox.MONTHS.map(function(val) {
return val.substr(0, 3);
});
//@ Ox.PATH <str> Path of Ox.js
Ox.PATH = Ox.makeArray(
document.getElementsByTagName('script')
).filter(function(element) {
return /Ox\.js$/.test(element.src);
})[0].src.replace('Ox.js', '');
//@ Ox.PREFIXES <[str]> <code>['K', 'M', 'G', 'T', 'P']</code>
Ox.PREFIXES = ['K', 'M', 'G', 'T', 'P'];
//@ Ox.SEASONS <[str]> Names of the seasons of the year
Ox.SEASONS = ['Winter', 'Spring', 'Summer', 'Fall'];
//@ Ox.SYMBOLS <obj> Unicode characters for symbols
Ox.SYMBOLS = {
DOLLAR: '\u0024',
CENT: '\u00A2', POUND: '\u00A3', CURRENCY: '\u00A4', YEN: '\u00A5',
BULLET: '\u2022', ELLIPSIS: '\u2026', PERMILLE: '\u2030',
COLON: '\u20A1', CRUZEIRO: '\u20A2', FRANC: '\u20A3', LIRA: '\u20A4',
NAIRA: '\u20A6', PESETA: '\u20A7', WON: '\u20A9', SHEQEL: '\u20AA',
DONG: '\u20AB', EURO: '\u20AC', KIP: '\u20AD', TUGRIK: '\u20AE',
DRACHMA: '\u20AF', PESO: '\u20B1', GUARANI: '\u20B2', AUSTRAL: '\u20B3',
HRYVNIA: '\u20B4', CEDI: '\u20B5', TENGE: '\u20B8', RUPEE: '\u20B9',
CELSIUS: '\u2103', FAHRENHEIT: '\u2109', POUNDS: '\u2114', OUNCE: '\u2125',
OHM: '\u2126', KELVIN: '\u212A', ANGSTROM: '\u212B', INFO: '\u2139',
LEFT: '\u2190', UP: '\u2191', RIGHT: '\u2192', DOWN: '\u2193',
HOME: '\u2196', END: '\u2198', RETURN: '\u21A9',
REDO: '\u21BA', UNDO: '\u21BB', PAGEUP: '\u21DE', PAGEDOWN: '\u21DF',
CAPSLOCK: '\u21EA', TAB: '\u21E5', SHIFT: '\u21E7', INFINITY: '\u221E',
CONTROL: '\u2303', COMMAND: '\u2318', ENTER: '\u2324', ALT: '\u2325',
DELETE: '\u2326', CLEAR:'\u2327',BACKSPACE: '\u232B', OPTION: '\u2387',
NAVIGATE: '\u2388', ESCAPE: '\u238B', EJECT: '\u23CF',
SPACE: '\u2423', DIAMOND: '\u25C6',
STAR: '\u2605', SOUND: '\u266B', TRASH: '\u267A', FLAG: '\u2691',
ANCHOR: '\u2693', GEAR: '\u2699', ATOM: '\u269B', WARNING: '\u26A0',
CUT: '\u2702', BACKUP: '\u2707', FLY: '\u2708', CHECK: '\u2713',
CLOSE: '\u2715', BALLOT: '\u2717', WINDOWS: '\u2756',
EDIT: '\uF802', CLICK: '\uF803', APPLE: '\uF8FF'
};
//@ Ox.TYPES <[str]> list of types, as returned by <code>Ox.type()</code>
Ox.TYPES = [
'Arguments', 'Array', 'Boolean', 'Date', 'Element', 'Function', 'Infinity',
'NaN', 'Null', 'Number', 'Object', 'RegExp', 'String', 'Undefined'
];
//@ Ox.VERSION <str> OxJS version number
Ox.VERSION = '0.1.2';
//@ Ox.WEEKDAYS <[str]> Names of weekdays
Ox.WEEKDAYS = [
'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'
];
//@ Ox.SHORT_WEEKDAYS <[str]> Short names of weekdays
Ox.SHORT_WEEKDAYS = Ox.WEEKDAYS.map(function(val) {
return val.substr(0, 3);
});
//@ Date -----------------------------------------------------------------------
//@ Ox.getDate <f> Get the day of a date, optionally UTC
// see Ox.setSeconds for source code
/*@
Ox.getDateInWeek <f> Get the date that falls on a given weekday in the same week
# Usage
(date, weekday) -> <dat> Date
(date, weekday, utc) -> <dat> Date
# Arguments
date <d> Date
weekday <n|s> 1-7 (Monday-Sunday) or name, full ("Monday") or short ("Sun")
utc <b> If true, all dates are UTC
# Examples
> Ox.formatDate(Ox.getDateInWeek(new Date("January 1 2000"), "Sunday"), "%A, %B %e, %Y")
"Sunday, January 2, 2000"
> Ox.formatDate(Ox.getDateInWeek(new Date("Jan 1 2000"), "Fri"), "%A, %B %e, %Y")
"Friday, December 31, 1999"
> Ox.formatDate(Ox.getDateInWeek(new Date("1/1/2000"), 1), "%A, %B %e, %Y")
"Monday, December 27, 1999"
@*/
// fixme: why is this Monday first? shouldn't it then be "getDateInISOWeek"??
Ox.getDateInWeek = function(date, weekday, utc) {
date = Ox.makeDate(date);
Ox.print(date, Ox.getDate(date, utc), Ox.formatDate(date, '%u', utc), date)
var sourceWeekday = Ox.getISODay(date, utc),
targetWeekday = Ox.isNumber(weekday) ? weekday :
Ox.map(Ox.WEEKDAYS, function(v, i) {
return v.substr(0, 3) == weekday.substr(0, 3) ? i + 1 : null;
})[0];
return Ox.setDate(date, Ox.getDate(date, utc) - sourceWeekday + targetWeekday, utc);
}
//@ Ox.getDay <f> Get the weekday of a date, optionally UTC
// see Ox.setSeconds for source code
/*@
Ox.getDayOfTheYear <f> Get the day of the year for a given date
# Usage
(date) -> <d> Date
(date, utc) -> <d> Date
# Arguments
date <d> Date
utc <b> If true, all dates are UTC
# Examples
> Ox.getDayOfTheYear(new Date("12/31/2000"))
366
> Ox.getDayOfTheYear(new Date("12/31/2002"))
365
> Ox.getDayOfTheYear(new Date("12/31/2004"))
366
@*/
Ox.getDayOfTheYear = function(date, utc) {
date = Ox.makeDate(date);
var month = Ox.getMonth(date, utc),
year = Ox.getFullYear(date, utc);
return Ox.sum(Ox.map(Ox.range(month), function(i) {
return Ox.getDaysInMonth(year, i + 1);
})) + Ox.getDate(date, utc);
};
/*@
Ox.getDaysInMonth <f> Get the number of days in a given month
> Ox.getDaysInMonth(2000, 2)
29
> Ox.getDaysInMonth("2002", "Feb")
28
> Ox.getDaysInMonth(new Date('01/01/2004'), "February")
29
@*/
Ox.getDaysInMonth = function(year, month, utc) {
year = Ox.makeYear(year);
month = Ox.isNumber(month) ? month :
Ox.map(Ox.MONTHS, function(v, i) {
return v.substr(0, 3) == month.substr(0, 3) ? i + 1 : null;
})[0];
return new Date(year, month, 0).getDate();
}
/*@
Ox.getDaysInYear <f> Get the number of days in a given year
> Ox.getDaysInYear(1900)
365
> Ox.getDaysInYear('2000')
366
> Ox.getDaysInYear(new Date('01/01/2004'))
366
@*/
Ox.getDaysInYear = function(year, utc) {
return 365 + Ox.isLeapYear(Ox.makeYear(year, utc));
};
/*@
Ox.getFirstDayOfTheYear <f> Get the weekday of the first day of a given year
Returns the decimal weekday of January 1 (0-6, Sunday as first day)
> Ox.getFirstDayOfTheYear(new Date('01/01/2000'))
6
@*/
Ox.getFirstDayOfTheYear = function(date, utc) {
date = Ox.makeDate(date);
date = Ox.setMonth(date, 0, utc);
date = Ox.setDate(date, 1, utc);
return Ox.getDay(date, utc)
};
//@ Ox.getFullYear <f> Get the year of a date, optionally UTC
// see Ox.setSeconds for source code
//@ Ox.getHours <f> Get the hours of a date, optionally UTC
// see Ox.setSeconds for source code
/*@
Ox.getISODate <f> Get the ISO date string for a given date
> Ox.getISODate(new Date('01/01/2000'))
'2000-01-01T00:00:00Z'
@*/
Ox.getISODate = function(date, utc) {
return Ox.formatDate(Ox.makeDate(date), '%FT%TZ', utc);
};
/*@
Ox.getISODay <f> Get the ISO weekday of a given date
Returns the decimal weekday (1-7, Monday as first day)
> Ox.getISODay(new Date('01/01/2000'))
6
> Ox.getISODay(new Date('01/02/2000'))
7
> Ox.getISODay(new Date('01/03/2000'))
1
@*/
Ox.getISODay = function(date, utc) {
return Ox.getDay(Ox.makeDate(date), utc) || 7;
};
/*@
Ox.getISOWeek <f> Get the ISO week of a given date
See <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>
> Ox.getISOWeek(new Date('01/01/2000'))
52
> Ox.getISOWeek(new Date('01/02/2000'))
52
> Ox.getISOWeek(new Date('01/03/2000'))
1
@*/
Ox.getISOWeek = function(date, utc) {
date = Ox.makeDate(date);
// set date to Thursday of the same week
return Math.floor((Ox.getDayOfTheYear(Ox.setDate(
date, Ox.getDate(date, utc) - Ox.getISODay(date, utc) + 4, utc
), utc) - 1) / 7) + 1;
};
/*@
Ox.getISOYear <f> Get the ISO year of a given date
See <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>
> Ox.getISOYear(new Date("01/01/2000"))
1999
> Ox.getISOYear(new Date("01/02/2000"))
1999
> Ox.getISOYear(new Date("01/03/2000"))
2000
@*/
Ox.getISOYear = function(date, utc) {
date = Ox.makeDate(date);
// set date to Thursday of the same week
return Ox.getFullYear(Ox.setDate(
date, Ox.getDate(date, utc) - Ox.getISODay(date, utc) + 4, utc
));
};
//@ Ox.getMilliseconds <f> Get the milliseconds of a date
// see Ox.setSeconds for source code
//@ Ox.getMinutes <f> Get the minutes of a date, optionally UTC
// see Ox.setSeconds for source code
//@ Ox.getMonth <f> Get the month of a date, optionally UTC
// see Ox.setSeconds for source code
//@ Ox.getSeconds <f> Get the seconds of a date
// see Ox.setSeconds for source code
//@ Ox.getTime <f> Alias for <code>+new Date()</code> (deprecated)
Ox.getTime = function() {
// fixme: needed?
return +new Date();
};
/*@
Ox.getTimezoneOffset <f> Get the local time zone offset in milliseconds
@*/
Ox.getTimezoneOffset = function() {
return new Date().getTimezoneOffset() * 60000;
};
/*@
Ox.getTimezoneOffsetString <f> Get the local time zone offset as a string
Returns a time zone offset string (from around '-1200' to around '+1200').
> Ox.getTimezoneOffsetString(new Date('01/01/2000')).length
5
@*/
Ox.getTimezoneOffsetString = function(date) {
var offset = (Ox.makeDate(date)).getTimezoneOffset();
return (offset < 0 ? '+' : '-') +
Ox.pad(Math.floor(Math.abs(offset) / 60), 2) +
Ox.pad(Math.abs(offset) % 60, 2);
};
/*@
Ox.getWeek <f> Get the week of a given day
Returns the week of the year (0-53, Sunday as first day)
> Ox.getWeek(new Date('01/01/2000'))
0
> Ox.getWeek(new Date('01/02/2000'))
1
> Ox.getWeek(new Date('01/03/2000'))
1
@*/
Ox.getWeek = function(date, utc) {
date = Ox.makeDate(date);
return Math.floor((Ox.getDayOfTheYear(date, utc) +
Ox.getFirstDayOfTheYear(date, utc) - 1) / 7);
};
/*@
Ox.isLeapYear <f> Returns true if a given year is a leap year
> Ox.isLeapYear(1900)
false
> Ox.isLeapYear('2000')
true
> Ox.isLeapYear(new Date('01/01/2004'))
true
@*/
Ox.isLeapYear = function(year, utc) {
year = Ox.makeYear(year, utc);
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
};
/*@
Ox.makeDate <f> Takes a date, number or string, returns a date
> Ox.formatDate(Ox.makeDate(new Date('01/01/1970')), '%m/%d/%Y')
'01/01/1970'
> Ox.formatDate(Ox.makeDate(0), '%m/%d/%Y')
'01/01/1970'
> Ox.formatDate(Ox.makeDate('01/01/1970'), '%m/%d/%Y')
'01/01/1970'
@*/
Ox.makeDate = function(date) {
// if date is a date, new Date(date) makes a clone
return Ox.isUndefined(date) ? new Date() : new Date(date);
};
/*@
Ox.makeYear <f> Takes a date, number or string, returns a year
> Ox.makeYear(new Date('01/01/1970'))
1970
> Ox.makeYear(1970)
1970
> Ox.makeYear('1970')
1970
@*/
Ox.makeYear = function(date, utc) {
return Ox.isDate(date) ? Ox.getFullYear(date, utc) : parseInt(date);
};
/*@
Ox.parseDate <f> Takes a string ('YYYY-MM-DD HH:MM:SS') and returns a date
str <s> string
utc <b|false> If true, Date is UTC
> +Ox.parseDate('1970-01-01 01:01:01', true)
3661000
> +Ox.parseDate('1970', true)
0
> Ox.parseDate('50', true).getUTCFullYear()
50
@*/
Ox.parseDate = function(str, utc) {
var date = new Date(0),
defaults = [, 1, 1, 0, 0, 0],
values = /(-?\d+)-?(\d+)?-?(\d+)? ?(\d+)?:?(\d+)?:?(\d+)?/.exec(str);
values.shift();
values = values.map(function(v, i) {
return v || defaults[i];
});
values[1]--;
[
'FullYear', 'Month', 'Date', 'Hours', 'Minutes', 'Seconds'
].forEach(function(part, i) {
Ox['set' + part](date, values[i], utc);
});
return date;
};
/*
Ox.parseDateRange = function(start, end, utc) {
var dates = [
Ox.parseDate(start, utc),
Ox.parseDate(end, utc)
],
part = [
'FullYear', 'Month', 'Date', 'Hours', 'Minutes', 'Seconds'
][
Ox.compact(
/(-?\d+)-?(\d+)?-?(\d+)? ?(\d+)?:?(\d+)?:?(\d+)?/.exec(end)
).length - 2
];
Ox['set' + part](dates[1], Ox['get' + part](dates[1], utc) + 1, utc);
return dates;
};
*/
//@ Ox.setDate <f> Set the day of a date, optionally UTC
// see Ox.setSeconds for source code
//@ Ox.setDay <f> Set the weekday of a date, optionally UTC
// see Ox.setSeconds for source code
//@ Ox.setFullYear <f> Set the year of a date, optionally UTC
// see Ox.setSeconds for source code
//@ Ox.setHours <f> Set the hours of a date, optionally UTC
// see Ox.setSeconds for source code
//@ Ox.setMilliseconds <f> Set the milliseconds of a date
// see Ox.setSeconds for source code
//@ Ox.setMinutes <f> Set the minutes of a date, optionally UTC
// see Ox.setSeconds for source code
//@ Ox.setMonth <f> Set the month of a date, optionally UTC
// see Ox.setSeconds for source code
//@ Ox.setSeconds <f> Set the seconds of a date
[
'FullYear', 'Month', 'Date', 'Day',
'Hours', 'Minutes', 'Seconds', 'Milliseconds'
].forEach(function(part) {
Ox['get' + part] = function(date, utc) {
return Ox.makeDate(date)['get' + (utc ? 'UTC' : '') + part]()
}
// Ox.setPart(date) modifies date
Ox['set' + part] = function(date, num, utc) {
return (
Ox.isDate(date) ? date : new Date(date)
)['set' + (utc ? 'UTC' : '') + part](num);
}
});
//@ DOM ------------------------------------------------------------------------
/*@
Ox.canvas <function> Generic canvas object
Returns an object with the properties: <code>canvas</code>,
<code>context</code>, <code>data</code> and <code>imageData</code>.
# Usage --------------------------------------------------------------------
Ox.canvas(width, height) -> <object> canvas
Ox.canvas(image) -> <object> canvas
# Arguments ----------------------------------------------------------------
width <n> Width in px
height <n> Height in px
image <e> Image object
@*/
Ox.canvas = function() {
// Ox.print("CANVAS", arguments)
var c = {}, isImage = arguments.length == 1,
image = isImage ? arguments[0] : {
width: arguments[0], height: arguments[1]
};
c.context = (c.canvas = Ox.element('<canvas>').attr({
width: image.width, height: image.height
})[0]).getContext('2d');
isImage && c.context.drawImage(image, 0, 0);
c.data = (c.imageData = c.context.getImageData(
0, 0, image.width, image.height
)).data;
return c;
};
/*@
Ox.documentReady <function> Calls a callback function once the DOM is ready
(callback) -> <boolean> If true, the document was ready
callback <function> Callback function
@*/
Ox.documentReady = (function() {
var callbacks = [];
document.onreadystatechange = function() {
if (document.readyState == 'complete') {
callbacks.forEach(function(callback) {
callback();
});
delete callbacks;
}
};
return function(callback) {
if (document.readyState == 'complete') {
callback();
return true;
} else {
callbacks.push(callback);
return false;
}
}
}());
/*@
Ox.element <f> Generic HTML element, mimics jQuery
(str) -> <o> Element object
str <s> Tagname ('<tagname>') or selector ('tagname', '.classname', '#id')
> Ox.element("<div>").addClass("red").hasClass("red")
true
> Ox.element("<div>").addClass("red").removeClass("red").hasClass("red")
false
> Ox.element("<div>").addClass("red").addClass("red")[0].className
"red"
> Ox.element("<div>").attr({id: "red"}).attr("id")
"red"
> Ox.element("<div>").css("color", "red").css("color")
"red"
> Ox.element("<div>").html("red").html()
"red"
@*/
Ox.element = function(str) {
return {
//@ 0 <e> The DOM element itself
0: str[0] == '<' ? document.createElement(str.substr(1, str.length - 2)) :
// fixme: why only take the first match?
str[0] == '.' ? document.getElementsByClassName(str.substr(1))[0] :
str[0] == '#' ? document.getElementById(str.substr(1)) :
document.getElementsByTagName(str)[0],
/*@
addClass <f> Adds a class name
(className) -> <o> This element
className <s> Class name
@*/
addClass: function(str) {
this[0].className = this[0].className ? Ox.unique(
(this[0].className + ' ' + str).split(' ')
).join(' ') : str;
return this;
},
/*@
append <f> Appends another element to this element
(element) -> <o> This element
element <o> Another element
@*/
append: function(element) {
this[0].appendChild(element[0]);
return this;
},
/*@
appendTo <f> appends this element object to another element object
(element) -> <o> This element
element <o> Another element
@*/
appendTo: function(element) {
element[0].appendChild(this[0]);
return this;
},
/*@
attr <f> Gets or sets an attribute
(key) -> <s> Value
(key, value) -> <o> This element
({key, value}) -> <o> This element
key <str> Attribute name
value <str> Attribute value
@*/
attr: function() {
var ret, that = this;
if (arguments.length == 1 && Ox.isString(arguments[0])) {
ret = this[0].getAttribute(arguments[0]);
} else {
Ox.forEach(Ox.makeObject(arguments), function(v, k) {
that[0].setAttribute(k, v);
});
ret = this;
}
return ret;
},
/*@
click <f> Binds a function to the click event
(callback) -> <o> This element
callback <f> Callback function
event <o> The DOM event
@*/
// fixme: why not a generic bind?
click: function(callback) {
this[0].onclick = callback;
return this;
},
/*@
css <f> Gets or sets a CSS attribute
(key) -> <s> Value
(key, value) -> <o> This element
({key, value}) -> <o> This element
key <str> Attribute name
value <str> Attribute value
@*/
css: function() {
var ret, that = this;
if (arguments.length == 1 && Ox.isString(arguments[0])) {
ret = this[0].style[arguments[0]];
} else {
Ox.forEach(Ox.makeObject(arguments), function(v, k) {
that[0].style[k] = v;
});
ret = this;
}
return ret;
},
/*@
hasClass <f> Returns true if the element has a given class
(className) -> <b> True if the element has the class
className <s> Class name
@*/
hasClass: function(str) {
return this[0].className.split(' ').indexOf(str) > -1;
},
/*@
html <f> Gets or sets the inner HTML
() -> <s> The inner HTML
(html) -> <o> This element
html <s> The inner HTML
@*/
html: function(str) {
var ret;
if (Ox.isUndefined(str)) {
ret = this[0].innerHTML;
} else {
this[0].innerHTML = str;
ret = this;
}
return ret;
},
/*@
mousedown <f> Binds a function to the mousedown event
(callback) -> <o> This element
callback <f> Callback function
event <o> The DOM event
@*/
// fixme: why not a generic bind?
mousedown: function(callback) {
this[0].onmousedown = callback;
return this;
},
/*@
removeAttr <f> Removes an attribute
(key) -> <o> This element
key <s> The attribute
@*/
removeAttr: function(key) {
this[0].removeAttribute(key);
return this;
},
/*@
removeClass <f> Removes a class name
(className) -> <o> This element
className <s> Class name
@*/
removeClass: function(str) {
this[0].className = Ox.filter(
this[0].className.split(' '), function(className) {
return className != str;
}
).join(' ');
return this;
}
}
};
//@ Encoding -------------------------------------------------------------------
(function() {
var aliases = {'I': '1', 'L': '1', 'O': '0', 'U': 'V'},
digits = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
function cap(width, height) {
// returns maximum encoding capacity of an image
return parseInt(width * height * 3/8) - 4;
}
function seek(data, px) {
// returns this, or the next, opaque pixel
while (data[px * 4 + 3] < 255) {
if (++px * 4 == data.length) {
throwPNGError('de');
}
}
return px;
}
function xor(byte) {
// returns "1"-bits-in-byte % 2
// use: num.toString(2).replace(/0/g, '').length % 2
var xor = 0;
Ox.range(8).forEach(function(i) {
xor ^= byte >> i & 1;
});
return xor;
}
function throwPNGError(str) {
throw new RangeError(
'PNG codec can\'t ' +
(str == 'en' ? 'encode data' : 'decode image')
);
}
function throwUTF8Error(byte, pos) {
throw new RangeError(
'UTF-8 codec can\'t decode byte 0x' +
byte.toString(16).toUpperCase() + ' at position ' + pos
);
}
/*@
Ox.encodeBase32 <b> Encode a number as base32
See <a href="http://www.crockford.com/wrmg/base32.html">Base 32</a>.
> Ox.encodeBase32(15360)
'F00'
> Ox.encodeBase32(33819)
'110V'
@*/
Ox.encodeBase32 = function(num) {
return Ox.map(num.toString(32), function(char) {
return digits[parseInt(char, 32)];
}).join('');
}
/*@
Ox.decodeBase32 <f> Decodes a base32-encoded number
See <a href="http://www.crockford.com/wrmg/base32.html">Base 32</a>.
> Ox.decodeBase32('foo')
15360
> Ox.decodeBase32('ILOU')
33819
> Ox.decodeBase32('?').toString()
'NaN'
@*/
Ox.decodeBase32 = function(str) {
return parseInt(Ox.map(str.toUpperCase(), function(char) {
var index = digits.indexOf(aliases[char] || char);
return (index == -1 ? ' ' : index).toString(32);
}).join(''), 32);
}
/*@
Ox.encodeBase64 <f> Encode a number as base64
> Ox.encodeBase64(32394)
'foo'
@*/
Ox.encodeBase64 = function(num) {
return btoa(Ox.encodeBase256(num)).replace(/=/g, '');
}
/*@
Ox.decodeBase64 <f> Decodes a base64-encoded number
> Ox.decodeBase64('foo')
32394
@*/
Ox.decodeBase64 = function(str) {
return Ox.decodeBase256(atob(str));
}
/*@
Ox.encodeBase128 <f> Encode a number as base128
> Ox.encodeBase128(1685487)
'foo'
@*/
Ox.encodeBase128 = function(num) {
var str = '';
while (num) {
str = Ox.char(num & 127) + str;
num >>= 7;
}
return str;
}
/*@
Ox.decodeBase128 <f> Decode a base128-encoded number
> Ox.decodeBase128('foo')
1685487
@*/
Ox.decodeBase128 = function(str) {
var num = 0, len = str.length;
Ox.forEach(str, function(char, i) {
num += char.charCodeAt(0) << (len - i - 1) * 7;
});
return num;
}
/*@
Ox.encodeBase256 <f> Encode a number as base256
> Ox.encodeBase256(6713199)
'foo'
@*/
Ox.encodeBase256 = function(num) {
var str = '';
while (num) {
str = Ox.char(num & 255) + str;
num >>= 8;
}
return str;
}
/*@
Ox.decodeBase256 <f> Decode a base256-encoded number
> Ox.decodeBase256('foo')
6713199
@*/
Ox.decodeBase256 = function(str) {
var num = 0, len = str.length;
Ox.forEach(str, function(char, i) {
num += char.charCodeAt(0) << (len - i - 1) * 8;
});
return num;
}
/*@
Ox.encodeDeflate <f> Encodes a string, using deflate
Since PNGs are deflate-encoded, the <code>canvas</code> object's
<code>toDataURL</code> method provides an efficient implementation.
The string is encoded as UTF-8 and written to the RGB channels of a
canvas element, then the PNG dataURL is decoded from base64, and some
head, tail and chunk names are removed.
(str) -> <s> The encoded string
str <s> The string to be encoded
# Test with: Ox.decodeDeflate(Ox.encodeDeflate('foo'), alert)
@*/
Ox.encodeDeflate = function(str, callback) {
// Make sure we can encode the full unicode range of characters.
str = Ox.encodeUTF8(str);
// We can only safely write to RGB, so we need 1 pixel for 3 bytes.
// The string length may not be a multiple of 3, so we need to encode
// the number of padding bytes (1 byte), the string, and non-0-bytes
// as padding, so that the combined length becomes a multiple of 3.
var len = 1 + str.length, c = Ox.canvas(Math.ceil(len / 3), 1),
data, idat, pad = (3 - len % 3) % 3;
str = Ox.char(pad) + str + Ox.repeat('\u00FF', pad);
Ox.loop(c.data.length, function(i) {
// Write character codes into RGB, and 255 into ALPHA
c.data[i] = i % 4 < 3 ? str.charCodeAt(i - parseInt(i / 4)) : 255;
});
c.context.putImageData(c.imageData, 0, 0);
// Get the PNG data from the data URL and decode it from base64.
str = atob(c.canvas.toDataURL().split(',')[1]);
// Discard bytes 0 to 15 (8 bytes PNG signature, 4 bytes IHDR length, 4
// bytes IHDR name), keep bytes 16 to 19 (width), discard bytes 20 to 29
// (4 bytes height, 5 bytes flags), keep bytes 29 to 32 (IHDR checksum),
// keep the rest (IDAT chunks), discard the last 12 bytes (IEND chunk).
data = str.substr(16, 4) + str.substr(29, 4);
idat = str.substr(33, str.length - 45);
while (idat) {
// Each IDAT chunk is 4 bytes length, 4 bytes name, length bytes
// data and 4 bytes checksum. We can discard the name parts.
len = idat.substr(0, 4);
data += len + idat.substr(8, 4 + (len = Ox.decodeBase256(len)));
idat = idat.substr(12 + len);
}
callback && callback(data);
return data;
}
/*@
Ox.decodeDeflate <f> Decodes an deflate-encoded string
Since PNGs are deflate-encoded, the <code>canvas</code> object's
<code>drawImage</code> method provides an efficient implementation. The
string will be wrapped as a PNG dataURL, encoded as base64, and drawn
onto a canvas element, then the RGB channels will be read, and the
result will be decoded from UTF8.
(str) -> <u> undefined
str <s> The string to be decoded
callback <f> Callback function
str <s> The decoded string
@*/
Ox.decodeDeflate = function(str, callback) {
var image = new Image(),
// PNG file signature and IHDR chunk
data = '\u0089PNG\r\n\u001A\n\u0000\u0000\u0000\u000DIHDR'
+ str.substr(0, 4) + '\u0000\u0000\u0000\u0001'
+ '\u0008\u0006\u0000\u0000\u0000' + str.substr(4, 4),
// IDAT chunks
idat = str.substr(8), len;
function error() {
throw new RangeError('Deflate codec can\'t decode data.');
}
while (idat) {
// Reinsert the IDAT chunk names
len = idat.substr(0, 4);
data += len + 'IDAT' + idat.substr(4, 4 + (len = Ox.decodeBase256(len)));
idat = idat.substr(8 + len);
}
// IEND chunk
data += '\u0000\u0000\u0000\u0000IEND\u00AE\u0042\u0060\u0082';
// Unfortunately, we can't synchronously set the source of an image,
// draw it onto a canvas, and read its data.
image.onload = function() {
str = Ox.makeArray(Ox.canvas(image).data).map(function(v, i) {
// Read one character per RGB byte, ignore ALPHA.
return i % 4 < 3 ? Ox.char(v) : '';
}).join('');
try {
// Parse the first byte as number of bytes to chop at the end,
// and the rest, without these bytes, as an UTF8-encoded string.
str = Ox.decodeUTF8(str.substr(1, str.length - 1 - str.charCodeAt(0)))
} catch (e) {
error();
}
callback(str);
}
image.onerror = error;
image.src = 'data:image/png;base64,' + btoa(data);
}
/*@
Ox.encodeHTML <f> HTML-encodes a string
> Ox.encodeHTML('\'<"&">\'')
'&apos;&lt;&quot;&amp;&quot;&gt;&apos;'
> Ox.encodeHTML('äbçdê')
'&#x00E4;b&#x00E7;d&#x00EA;'
@*/
Ox.encodeHTML = function(str) {
return Ox.map(str.toString(), function(v) {
var code = v.charCodeAt(0);
return code < 128 ? (v in Ox.HTML_ENTITIES ? Ox.HTML_ENTITIES[v] : v) :
'&#x' + Ox.pad(code.toString(16).toUpperCase(), 4) + ';';
}).join('');
};
/*@
Ox.decodeHTML <f> Decodes an HTML-encoded string
> Ox.decodeHTML('&apos;&lt;&quot;&amp;&quot;&gt;&apos;')
'\'<"&">\''
> Ox.decodeHTML('&#x0027;&#x003C;&#x0022;&#x0026;&#x0022;&#x003E;&#x0027;')
'\'<"&">\''
> Ox.decodeHTML('&auml;b&ccedil;d&ecirc;')
'äbçdê'
> Ox.decodeHTML('&#x00E4;b&#x00E7;d&#x00EA;')
'äbçdê'
@*/
Ox.decodeHTML = function(str) {
// relies on dom, but shorter than using this:
// http://www.w3.org/TR/html5/named-character-references.html
return Ox.element('<div>').html(str)[0].childNodes[0].nodeValue;
};
/*@
Ox.encodePNG <f> Encodes a string into an image, returns a new image
The string is compressed with deflate (by proxy of canvas), prefixed
with its length (four bytes), and encoded bitwise into the red, green
and blue bytes of all fully opaque pixels of the image, by flipping, if
necessary, the least significant bit, so that for every byte, the total
number of bits set to to 1, modulo 2, is the bit that we are encoding.
(img, src) -> <e> An image into which the string has been encoded
img <e> Any JavaScript PNG image object
str <s> The string to be encoded
@*/
Ox.encodePNG = function(img, str) {
var c = Ox.canvas(img), i = 0;
// Compress the string
str = Ox.encodeDeflate(str);
// Prefix the string with its length, as a four-byte value
str = Ox.pad(Ox.encodeBase256(str.length), 4, Ox.char(0)) + str;
// Create an array of bit values
Ox.forEach(Ox.flatten(Ox.map(str, function(chr) {
return Ox.map(Ox.range(8), function(i) {
return chr.charCodeAt(0) >> 7 - i & 1;
});
})), function(bit) {
// Skip all pixels that are not fully opaque
while (i < c.data.length && i % 4 == 0 && c.data[i + 3] < 255) {
i += 4;
}
if (i == c.data.length) {
throw new RangeError('PNG codec can\'t encode data');
}
// If the number of bits set to one, modulo 2 is equal to the bit,
// do nothing, otherwise, flip the least significant bit.
c.data[i] += c.data[i].toString(2).replace(/0/g, '').length % 2
== bit ? 0 : c.data[i] % 2 ? -1 : 1;
i++;
});
c.context.putImageData(c.imageData, 0, 0);
img = new Image();
img.src = c.canvas.toDataURL();
return img;
/*
wishlist:
- only use deflate if it actually shortens the message
- encode a decoy message into the least significant bit
(and flip the second least significant bit, if at all)
- write an extra png chunk containing some key
*/
}
/*@
Ox.decodePNG <f> Decodes an image, returns a string
For every red, green and blue byte of every fully opaque pixel of the
image, one bit, namely the number of bits of the byte set to one, modulo
2, is being read, the result being the string, prefixed with its length
(four bytes), which is decompressed with deflate (by proxy of canvas).
(img, callback) -> <u> undefined
img <e> The image into which the string has been encoded
callback <f> Callback function
str <s> The decoded string
@*/
Ox.decodePNG = function(img, callback) {
var bits = '', data = Ox.canvas(img).data, flag = false, i = 0,
len = 4, str = '';
while (len) {
// Skip all pixels that are not fully opaque
while (i < data.length && i % 4 == 0 && data[i + 3] < 255) {
i += 4;
}
if (i == data.length) {
break;
}
// Read the number of bits set to one, modulo 2
bits += data[i].toString(2).replace(/0/g, '').length % 2;
if (++i % 8 == 0) {
// Every 8 bits, add one byte
str += Ox.char(parseInt(bits, 2));
bits = '';
// When length reaches 0 for the first time,
// decode the string and treat it as the new length
if (--len == 0 && !flag) {
flag = true;
len = Ox.decodeBase256(str);
str = '';
}
}
}
try {
Ox.decodeDeflate(str, callback);
} catch (e) {
throw new RangeError('PNG codec can\'t decode image');
}
}
/*@
Ox.encodeUTF8 <f> Encodes a string as UTF-8
see http://en.wikipedia.org/wiki/UTF-8
(string) -> <s> UTF-8 encoded string
string <s> Any string
> Ox.encodeUTF8("YES")
"YES"
> Ox.encodeUTF8("¥€$")
"\u00C2\u00A5\u00E2\u0082\u00AC\u0024"
@*/
Ox.encodeUTF8 = function(str) {
return Ox.map(str, function(chr) {
var code = chr.charCodeAt(0),
str = '';
if (code < 128) {
str = chr;
} else if (code < 2048) {
str = String.fromCharCode(code >> 6 | 192) +
String.fromCharCode(code & 63 | 128);
} else {
str = String.fromCharCode(code >> 12 | 224) +
String.fromCharCode(code >> 6 & 63 | 128) +
String.fromCharCode(code & 63 | 128);
}
return str;
}).join('');
}
/*@
Ox.decodeUTF8 <f> Decodes an UTF-8-encoded string
see http://en.wikipedia.org/wiki/UTF-8
(utf8) -> <s> string
utf8 <s> Any UTF-8-encoded string
> Ox.decodeUTF8('YES')
'YES'
> Ox.decodeUTF8('\u00C2\u00A5\u00E2\u0082\u00AC\u0024')
'¥€$'
@*/
Ox.decodeUTF8 = function(str) {
var bytes = Ox.map(str, function(v) {
return v.charCodeAt(0);
}),
i = 0,
len = str.length,
str = '';
while (i < len) {
if (bytes[i] <= 128) {
str += String.fromCharCode(bytes[i]);
i++;
} else if (
bytes[i] >= 192 && bytes[i] < 240 &&
i < len - (bytes[i] < 224 ? 1 : 2)
) {
if (bytes[i + 1] >= 128 && bytes[i + 1] < 192) {
if (bytes[i] < 224) {
str += String.fromCharCode((bytes[i] & 31) << 6 |
bytes[i + 1] & 63);
i += 2;
} else if (bytes[i + 2] >= 128 && bytes[i + 2] < 192) {
str += String.fromCharCode((bytes[i] & 15) << 12 |
(bytes[i + 1] & 63) << 6 | bytes[i + 2] & 63);
i += 3;
} else {
throwUTF8Error(bytes[i + 2], i + 2);
}
} else {
throwUTF8Error(bytes[i + 1], i + 1);
}
} else {
throwUTF8Error(bytes[i], i);
}
}
return str;
};
})();
//@ Format ---------------------------------------------------------------------
/*@
Ox.formatArea <f> Formats a number of meters as square meters or kilometers
> Ox.formatArea(1000)
'1,000 m\u00B2'
> Ox.formatArea(1000000)
'1 km\u00B2'
@*/
Ox.formatArea = function(num, dec) {
var km = num >= 1000000;
return Ox.formatNumber(
(km ? num / 1000000 : num).toPrecision(8)
) + ' ' + (km ? 'k' : '') + 'm\u00B2';
}
/*@
Ox.formatColor <f> (strange one)
@*/
Ox.formatColor = function(val, type) {
var background, color;
if (type == 'hue') {
background = Ox.rgb(val, 1, 0.25).map(function(val) {
return Math.round(val);
});
color = Ox.rgb(val, 1, 0.75).map(function(val) {
return Math.round(val);
});
} else if (type == 'saturation') {
background = Ox.range(7).map(function(i) {
return Ox.rgb(i * 60, val, 0.25).map(function(val) {
return Math.round(val);
});
});
color = Ox.range(3).map(function() {
return Math.round(128 + val * 127);
});
} else if (type == 'lightness') {
background = Ox.range(3).map(function() {
return Math.round(val * 255);
});
color = Ox.range(3).map(function() {
var v = val * 255;
return val < 0.5 ? 128 + v : 255 - v;
});
}
return Ox.element('<div>')
.css({
borderRadius: '4px',
padding: '0 3px 1px 3px',
background: Ox.isNumber(background[0])
? 'rgb(' + background.join(', ') + ')'
: Ox.print([/*'moz', 'o', */'webkit'].map(function(browser) {
return '-' + browser + '-linear-gradient(left, '
+ background.map(function(rgb, i) {
return 'rgb(' + rgb.join(', ') + ') '
+ Math.round(i * 100 / 6) + '%';
}).join(', ')
+ ')';
}).join(', ')) &&
[/*'moz', 'o', */'webkit'].map(function(browser) {
return '-' + browser + '-linear-gradient(left, '
+ background.map(function(rgb, i) {
return 'rgb(' + rgb.join(', ') + ') '
+ Math.round(i * 100 / 6) + '%';
}).join(', ')
+ ')';
}).join(', '),
color: 'rgb(' + color.join(', ') + ')',
overflow: 'hidden',
textOverflow: 'ellipsis',
//textShadow: 'black 1px 1px 1px'
})
.html(Ox.formatNumber(val, 3));
};
/*@
Ox.formatCurrency <f> Formats a number with a currency symbol
> Ox.formatCurrency(1000, '$', 2)
'$1,000.00'
@*/
Ox.formatCurrency = function(num, str, dec) {
return str + Ox.formatNumber(num, dec);
};
/*@
Ox.formatDate <f> Formats a date according to a format string
See
<a href="http://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man3/strftime.3.html">strftime</a>
and <a href="http://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>.
'%Q' (quarter) and '%X'/'%x' (year with 'BC'/'AD') are non-standard
<script>
Ox.test.date = new Date('2005-01-02 00:03:04');
</script>
> Ox.formatDate(Ox.test.date, '%A') // Full weekday
'Sunday'
> Ox.formatDate(Ox.test.date, '%a') // Abbreviated weekday
'Sun'
> Ox.formatDate(Ox.test.date, '%B') // Full month
'January'
> Ox.formatDate(Ox.test.date, '%b') // Abbreviated month
'Jan'
> Ox.formatDate(Ox.test.date, '%C') // Century
'20'
> Ox.formatDate(Ox.test.date, '%c') // US time and date
'01/02/05 12:03:04 AM'
> Ox.formatDate(Ox.test.date, '%D') // US date
'01/02/05'
> Ox.formatDate(Ox.test.date, '%d') // Zero-padded day of the month
'02'
> Ox.formatDate(Ox.test.date, '%e') // Space-padded day of the month
' 2'
> Ox.formatDate(Ox.test.date, '%F') // Date
'2005-01-02'
> Ox.formatDate(Ox.test.date, '%G') // Full ISO-8601 year
'2004'
> Ox.formatDate(Ox.test.date, '%g') // Abbreviated ISO-8601 year
'04'
> Ox.formatDate(Ox.test.date, '%H') // Zero-padded hour (24-hour clock)
'00'
> Ox.formatDate(Ox.test.date, '%h') // Abbreviated month
'Jan'
> Ox.formatDate(Ox.test.date, '%I') // Zero-padded hour (12-hour clock)
'12'
> Ox.formatDate(Ox.test.date, '%j') // Zero-padded day of the year
'002'
> Ox.formatDate(Ox.test.date, '%k') // Space-padded hour (24-hour clock)
' 0'
> Ox.formatDate(Ox.test.date, '%l') // Space-padded hour (12-hour clock)
'12'
> Ox.formatDate(Ox.test.date, '%M') // Zero-padded minute
'03'
> Ox.formatDate(Ox.test.date, '%m') // Zero-padded month
'01'
> Ox.formatDate(Ox.test.date, '%n') // Newline
'\n'
> Ox.formatDate(Ox.test.date, '%p') // AM or PM
'AM'
> Ox.formatDate(Ox.test.date, '%Q') // Quarter of the year
'1'
> Ox.formatDate(Ox.test.date, '%R') // Zero-padded hour and minute
'00:03'
> Ox.formatDate(Ox.test.date, '%r') // US time
'12:03:04 AM'
> Ox.formatDate(Ox.test.date, '%S') // Zero-padded second
'04'
> Ox.formatDate(Ox.test.date, '%s', true) // Number of seconds since the Epoch
'1104620584'
> Ox.formatDate(Ox.test.date, '%T') // Time
'00:03:04'
> Ox.formatDate(Ox.test.date, '%t') // Tab
'\t'
> Ox.formatDate(Ox.test.date, '%U') // Zero-padded week of the year (00-53, Sunday as first day)
'01'
> Ox.formatDate(Ox.test.date, '%u') // Decimal weekday (1-7, Monday as first day)
'7'
> Ox.formatDate(Ox.test.date, '%V') // Zero-padded ISO-8601 week of the year
'53'
> Ox.formatDate(Ox.test.date, '%v') // Formatted date
' 2-Jan-2005'
> Ox.formatDate(Ox.test.date, '%W') // Zero-padded week of the year (00-53, Monday as first day)
'00'
> Ox.formatDate(Ox.test.date, '%w') // Decimal weekday (0-6, Sunday as first day)
'0'
> Ox.formatDate(Ox.test.date, '%X') // Full year with BC or AD
'2005 AD'
> Ox.formatDate(Ox.test.date, '%x') // Full year with BC or AD if year < 1000
'2005'
> Ox.formatDate(Ox.test.date, '%Y') // Full year
'2005'
> Ox.formatDate(Ox.test.date, '%y') // Abbreviated year
'05'
> Ox.formatDate(Ox.test.date, '%Z', true) // Time zone name
'UTC'
> Ox.formatDate(Ox.test.date, '%z', true) // Time zone offset
'+0000'
> Ox.formatDate(Ox.test.date, '%+', true) // Formatted date and time
'Sun Jan 2 00:03:04 CET 2005'
> Ox.formatDate(Ox.test.date, '%%')
'%'
@*/
Ox.formatDate = function(date, str, utc) {
// fixme: date and utc are optional, date can be date, number or string
date = Ox.makeDate(date);
var format = [
['%', function() {return '%{%}';}],
['c', function() {return '%D %r';}],
['D', function() {return '%m/%d/%y';}],
['F', function() {return '%Y-%m-%d';}],
['h', function() {return '%b';}],
['R', function() {return '%H:%M';}],
['r', function() {return '%I:%M:%S %p';}],
['T', function() {return '%H:%M:%S';}],
['v', function() {return '%e-%b-%Y';}],
['\\+', function() {return '%a %b %e %H:%M:%S %Z %Y';}],
['A', function(d) {return Ox.WEEKDAYS[(Ox.getDay(d, utc) + 6) % 7];}],
['a', function(d) {return Ox.SHORT_WEEKDAYS[(Ox.getDay(d, utc) + 6) % 7];}],
['B', function(d) {return Ox.MONTHS[Ox.getMonth(d, utc)];}],
['b', function(d) {return Ox.SHORT_MONTHS[Ox.getMonth(d, utc)];}],
['C', function(d) {return Math.floor(Ox.getFullYear(d, utc) / 100).toString();}],
['d', function(d) {return Ox.pad(Ox.getDate(d, utc), 2);}],
['e', function(d) {return Ox.pad(Ox.getDate(d, utc), 2, ' ');}],
['G', function(d) {return Ox.getISOYear(d, utc);}],
['g', function(d) {return Ox.getISOYear(d, utc).toString().substr(-2);}],
['H', function(d) {return Ox.pad(Ox.getHours(d, utc), 2);}],
['I', function(d) {return Ox.pad((Ox.getHours(d, utc) + 11) % 12 + 1, 2);}],
['j', function(d) {return Ox.pad(Ox.getDayOfTheYear(d, utc), 3);}],
['k', function(d) {return Ox.pad(Ox.getHours(d, utc), 2, ' ');}],
['l', function(d) {return Ox.pad(((Ox.getHours(d, utc) + 11) % 12 + 1), 2, ' ');}],
['M', function(d) {return Ox.pad(Ox.getMinutes(d, utc), 2);}],
['m', function(d) {return Ox.pad((Ox.getMonth(d, utc) + 1), 2);}],
['p', function(d) {return Ox.AMPM[Math.floor(Ox.getHours(d, utc) / 12)];}],
['Q', function(d) {return Math.floor(Ox.getMonth(d, utc) / 4) + 1;}],
['S', function(d) {return Ox.pad(Ox.getSeconds(d, utc), 2);}],
['s', function(d) {return Math.floor(d.getTime() / 1000);}],
['U', function(d) {return Ox.pad(Ox.getWeek(d, utc), 2);}],
['u', function(d) {return Ox.getISODay(d, utc);}],
['V', function(d) {return Ox.pad(Ox.getISOWeek(d, utc), 2);}],
['W', function(d) {
return Ox.pad(Math.floor((Ox.getDayOfTheYear(d, utc) +
(Ox.getFirstDayOfTheYear(d, utc) || 7) - 2) / 7), 2);
}],
['w', function(d) {return Ox.getDay(d, utc);}],
['X', function(d) {
var y = Ox.getFullYear(d, utc);
return Math.abs(y) + ' ' + Ox.BCAD[y < 0 ? 0 : 1];
}],
['x', function(d) {
var y = Ox.getFullYear(d, utc);
return Math.abs(y) + (y < 1000 ? ' ' + Ox.BCAD[y < 0 ? 0 : 1] : '');
}],
['Y', function(d) {return Ox.getFullYear(d, utc);}],
['y', function(d) {return Ox.getFullYear(d, utc).toString().substr(-2);}],
['Z', function(d) {return d.toString().split('(')[1].replace(')', '');}],
['z', function(d) {return Ox.getTimezoneOffsetString(d);}],
['n', function() {return '\n';}],
['t', function() {return '\t';}],
['\\{%\\}', function() {return '%';}]
];
format.forEach(function(v) {
var regexp = new RegExp('%' + v[0], 'g');
if (regexp.test(str)) {
str = str.replace(regexp, v[1](date));
}
});
return str;
};
/*@
Ox.formatDateRange <f> Formats a date range as a string
A date range is a pair of arbitrary-presicion date strings
> Ox.formatDateRange('2000', '2001')
'2000'
> Ox.formatDateRange('2000', '2002')
'2000 - 2002'
> Ox.formatDateRange('2000-01', '2000-02')
'January 2000'
> Ox.formatDateRange('2000-01', '2000-03')
'January - March 2000'
> Ox.formatDateRange('2000-01-01', '2000-01-02')
'Sat, Jan 1, 2000'
> Ox.formatDateRange('2000-01-01', '2000-01-03')
'Sat, Jan 1 - Mon, Jan 3, 2000'
> Ox.formatDateRange('2000-01-01 00', '2000-01-01 01')
'Sat, Jan 1, 2000, 00:00'
> Ox.formatDateRange('2000-01-01 00', '2000-01-01 02')
'Sat, Jan 1, 2000, 00:00 - 02:00'
> Ox.formatDateRange('2000-01-01 00:00', '2000-01-01 00:01')
'Sat, Jan 1, 2000, 00:00'
> Ox.formatDateRange('2000-01-01 00:00', '2000-01-01 00:02')
'Sat, Jan 1, 2000, 00:00 - 00:02'
> Ox.formatDateRange('2000-01-01 00:00:00', '2000-01-01 00:00:01')
'Sat, Jan 1, 2000, 00:00:00'
> Ox.formatDateRange('2000-01-01 00:00:00', '2000-01-01 00:00:02')
'Sat, Jan 1, 2000, 00:00:00 - 00:00:02'
> Ox.formatDateRange('-50', '50')
'50 BC - 50 AD'
> Ox.formatDateRange('-50-01-01', '-50-12-31')
'Sun, Jan 1 - Sun, Dec 31, 50 BC'
> Ox.formatDateRange('-50-01-01 00:00:00', '-50-01-01 23:59:59')
'Sun, Jan 1, 50 BC, 00:00:00 - 23:59:59'
@*/
Ox.formatDateRange = function(start, end, utc) {
var isOneUnit = false,
range = [start, end],
strings,
dates = range.map(function(str){
return Ox.parseDate(str, utc);
}),
parts = range.map(function(str) {
var parts = Ox.compact(
/(-?\d+)-?(\d+)?-?(\d+)? ?(\d+)?:?(\d+)?:?(\d+)?/.exec(str)
);
parts.shift();
return parts.map(function(part) {
return parseInt(part);
});
}),
precision = parts.map(function(parts) {
return parts.length;
}),
y = parts[0][0] < 0 ? '%X' : '%Y',
formats = [
y,
'%B ' + y,
'%a, %b %e, ' + y,
'%a, %b %e, ' + y + ', %H:%M',
'%a, %b %e, ' + y + ', %H:%M',
'%a, %b %e, ' + y + ', %H:%M:%S',
];
if (precision[0] == precision[1]) {
isOneUnit = true;
Ox.loop(precision[0], function(i) {
if (i < precision[0] - 1 && parts[0][i] != parts[1][i]) {
isOneUnit = false;
}
if (i == precision[0] - 1 && parts[0][i] != parts[1][i] - 1) {
isOneUnit = false;
}
return isOneUnit;
});
}
if (isOneUnit) {
strings = [Ox.formatDate(dates[0], formats[precision[0] - 1], utc)];
} else {
format = formats[precision[0] - 1];
strings = [
Ox.formatDate(dates[0], formats[precision[0] - 1], utc),
Ox.formatDate(dates[1], formats[precision[1] - 1], utc)
];
// if same year, and neither date is more precise than day, then omit first year
if (
parts[0][0] == parts[1][0]
&& precision[0] <= 3
&& precision[1] <= 3
) {
strings[0] = Ox.formatDate(
dates[0], formats[precision[0] - 1].replace(
new RegExp(',? ' + y), ''
), utc
);
}
// if same day then omit second day
if (
parts[0][0] == parts[1][0]
&& parts[0][1] == parts[1][1]
&& parts[0][2] == parts[1][2]
) {
strings[1] = strings[1].split(', ').pop();
}
}
return strings.map(function(string) {
// %e is a space-padded day
return string.replace(' ', ' ');
}).join(' - ');
};
/*@
Ox.formatDateRangeDuration <f> Formats the duration of a date range as a string
A date range is a pair of arbitrary-presicion date strings
> Ox.formatDateRangeDuration('2000-01-01 00:00:00', '2001-03-04 04:05:06')
'1 year 2 months 3 days 4 hours 5 minutes 6 seconds'
> Ox.formatDateRangeDuration('1999', '2000', true)
'1 year'
> Ox.formatDateRangeDuration('2000', '2001', true)
'1 year'
> Ox.formatDateRangeDuration('1999-02', '1999-03', true)
'1 month'
> Ox.formatDateRangeDuration('2000-02', '2000-03', true)
'1 month'
@*/
Ox.formatDateRangeDuration = function(start, end, utc) {
var date = Ox.parseDate(start, utc),
dates = [start, end].map(function(str) {
return Ox.parseDate(str, utc);
}),
keys = ['year', 'month', 'day', 'hour', 'minute', 'second'],
parts = ['FullYear', 'Month', 'Date', 'Hours', 'Minutes', 'Seconds'],
values = [];
Ox.forEach(keys, function(key, i) {
while (true) {
if (key == 'month') {
Ox.setDate(date, Math.min(
Ox.getDate(date, utc),
Ox.getDaysInMonth(
Ox.getFullYear(date, utc),
Ox.getMonth(date, utc) + 2
)
), utc);
}
Ox['set' + parts[i]](date, Ox['get' + parts[i]](date, utc) + 1, utc);
if (date <= dates[1]) {
values[i] = (values[i] || 0) + 1;
} else {
Ox['set' + parts[i]](date, Ox['get' + parts[i]](date, utc) - 1, utc);
break;
}
}
});
return Ox.map(values, function(value, i) {
return value ? value + ' ' + keys[i] + (value > 1 ? 's' : '') : null;
}).join(' ');
};
/*@
Ox.formatDuration <f> Formats a duration as a string
> Ox.formatDuration(123456.789, 3)
"1:10:17:36.789"
> Ox.formatDuration(12345.6789)
"03:25:46"
> Ox.formatDuration(12345.6789, true)
"0:03:25:46"
> Ox.formatDuration(3599.999, 3)
"00:59:59.999"
> Ox.formatDuration(3599.999)
"01:00:00"
@*/
Ox.formatDuration = function(sec, dec, format) {
var format = arguments.length == 3 ? format : (Ox.isString(dec) ? dec : "short"),
dec = (arguments.length == 3 || Ox.isNumber(dec)) ? dec : 0,
sec = dec ? sec : Math.round(sec),
val = [
Math.floor(sec / 31536000),
Math.floor(sec % 31536000 / 86400),
Math.floor(sec % 86400 / 3600),
Math.floor(sec % 3600 / 60),
format == "short" ? Ox.formatNumber(sec % 60, dec) : sec % 60
],
str = {
medium: ["y", "d", "h", "m", "s"],
long: ["year", "day", "hour", "minute", "second"]
},
pad = [0, 3, 2, 2, dec ? dec + 3 : 2];
while (!val[0] && val.length > (format == "short" ? 3 : 1)) {
val.shift();
str.medium.shift();
str.long.shift();
pad.shift();
}
while (format != "short" && !val[val.length - 1] && val.length > 1) {
val.pop();
str.medium.pop();
str.long.pop();
}
return Ox.map(val, function(v, i) {
return format == "short" ? Ox.pad(v, pad[i]) :
v + (format == "long" ? " " : "") + str[format][i] +
(format == "long" && v != 1 ? "s" : "");
}).join(format == "short" ? ":" : " ");
};
/*@
Ox.formatNumber <f> Formats a number with thousands separators
> Ox.formatNumber(123456789, 3)
"123,456,789.000"
> Ox.formatNumber(-2000000 / 3, 3)
"-666,666.667"
> Ox.formatNumber(666666.666, 0)
"666,667"
@*/
Ox.formatNumber = function(num, dec) {
// fixme: specify decimal and thousands separators
var arr = [],
abs = Math.abs(num),
str = Ox.isUndefined(dec) ? abs.toString() : abs.toFixed(dec),
spl = str.split('.');
while (spl[0]) {
arr.unshift(spl[0].substr(-3));
spl[0] = spl[0].substr(0, spl[0].length - 3);
}
spl[0] = arr.join(',');
return (num < 0 ? '-' : '') + spl.join('.');
};
/*@
Ox.formatOrdinal <f> Formats a number as an ordinal
> Ox.formatOrdinal(1)
"1st"
> Ox.formatOrdinal(2)
"2nd"
> Ox.formatOrdinal(3)
"3rd"
> Ox.formatOrdinal(4)
"4th"
> Ox.formatOrdinal(11)
"11th"
> Ox.formatOrdinal(12)
"12th"
> Ox.formatOrdinal(13)
"13th"
@*/
Ox.formatOrdinal = function(num) {
var str = num.toString(),
end = str[str.length - 1],
ten = str.length > 1 && str[str.length - 2] == '1';
if (end == '1' && !ten) {
str += 'st';
} else if (end == '2' && !ten) {
str += 'nd';
} else if (end == '3' && !ten) {
str += 'rd';
} else {
str += 'th';
}
return str;
};
/*@
Ox.formatPercent <f> Formats the relation of two numbers as a percentage
> Ox.formatPercent(1, 1000, 2)
"0.10%"
@*/
Ox.formatPercent = function(num, total, dec) {
return Ox.formatNumber(num / total * 100, dec) + '%'
};
/*@
Ox.formatResolution <f> Formats two values as a resolution
> Ox.formatResolution([1920, 1080], 'px')
"1920 x 1080 px"
@*/
// fixme: should be formatDimensions
Ox.formatResolution = function(arr, str) {
return arr[0] + ' x ' + arr[1] + (str ? ' ' + str : '');
}
/*@
Ox.formatString <f> Basic string formatting
> Ox.formatString('{0}{1}', ['foo', 'bar'])
'foobar'
> Ox.formatString('{a}{b}', {a: 'foo', b: 'bar'})
'foobar'
@*/
Ox.formatString = function (str, obj) {
return str.replace(/\{([^}]+)\}/g, function(str, match) {
return obj[match];
});
}
/*@
Ox.formatValue <f> Formats a numerical value
> Ox.formatValue(0, "B")
"0 KB"
> Ox.formatValue(123456789, "B")
"123.5 MB"
> Ox.formatValue(1234567890, "B", true)
"1.15 GiB"
@*/
// fixme: is this the best name?
Ox.formatValue = function(num, str, bin) {
var base = bin ? 1024 : 1000,
len = Ox.PREFIXES.length,
val;
Ox.forEach(Ox.PREFIXES, function(chr, i) {
if (num < Math.pow(base, i + 2) || i == len - 1) {
val = Ox.formatNumber(num / Math.pow(base, i + 1), i) +
' ' + chr + (bin ? 'i' : '') + str;
return false;
}
});
return val;
};
/*@
Ox.formatUnit <f> Formats a number with a unit
@*/
Ox.formatUnit = function(num, str) {
return num + ' ' + str;
};
//@ Geo ------------------------------------------------------------------------
(function() {
// fixme: make all this work with different types of "points"
// i.e. {lat, lng}, [lat, lng]
function rad(point) {
return {
lat: Ox.rad(point.lat),
lng: Ox.rad(point.lng)
};
}
/*@
Ox.crossesDateline <f> Returns true if a given rectangle crosses the dateline
@*/
Ox.crossesDateline = function(point0, point1) {
return point0.lng > point1.lng;
}
/*@
Ox.getArea <f> Returns the area in square meters of a given rectancle
@*/
Ox.getArea = function(point0, point1) {
/*
area of a ring between two latitudes:
2 * PI * r^2 * abs(sin(lat0) - sin(lat1))
see http://mathforum.org/library/drmath/view/63767.html
*/
/*
2 * Math.PI *
Math.pow(Ox.EARTH_RADIUS, 2) *
Math.abs(Math.sin(Ox.rad(0)) - Math.sin(Ox.rad(1))) *
Math.abs(Ox.rad(0) - Ox.rad(1)) /
(2 * Math.PI)
*/
if (Ox.crossesDateline(point0, point1)) {
point1.lng += 360;
}
var point0 = rad(point0),
point1 = rad(point1);
return Math.pow(Ox.EARTH_RADIUS, 2) *
Math.abs(Math.sin(point0.lat) - Math.sin(point1.lat)) *
Math.abs(point0.lng - point1.lng);
};
/*@
Ox.getBearing <f> Returns the bearing from one point to another
> Ox.getBearing({lat: -45, lng: 0}, {lat: 45, lng: 0})
0
@*/
Ox.getBearing = function(point0, point1) {
var point0 = rad(point0),
point1 = rad(point1),
x = Math.cos(point0.lat) * Math.sin(point1.lat) -
Math.sin(point0.lat) * Math.cos(point1.lat) *
Math.cos(point1.lng - point0.lng),
y = Math.sin(point1.lng - point0.lng) *
Math.cos(point1.lat);
return (Ox.deg(Math.atan2(y, x)) + 360) % 360;
};
/*@
Ox.getCenter <f> Returns the center of a recangle on a spehre
> Ox.getCenter({lat: -45, lng: -90}, {lat: 45, lng: 90})
{lat: 0, lng: 0}
@*/
Ox.getCenter = function(point0, point1) {
var point0 = rad(point0),
point1 = rad(point1),
x = Math.cos(point1.lat) *
Math.cos(point1.lng - point0.lng),
y = Math.cos(point1.lat) *
Math.sin(point1.lng - point0.lng),
d = Math.sqrt(
Math.pow(Math.cos(point0.lat) + x, 2) + Math.pow(y, 2)
),
lat = Ox.deg(
Math.atan2(Math.sin(point0.lat) + Math.sin(point1.lat), d)
),
lng = Ox.deg(
point0.lng + Math.atan2(y, Math.cos(point0.lat) + x)
);
return {lat: lat, lng: lng};
};
/*@
Ox.getDegreesPerMeter <f> Returns degrees per meter at a given latitude
> 360 / Ox.getDegreesPerMeter(0)
Ox.EARTH_CIRCUMFERENCE
@*/
Ox.getDegreesPerMeter = function(lat) {
return 360 / Ox.EARTH_CIRCUMFERENCE / Math.cos(lat * Math.PI / 180);
};
/*@
Ox.getDistance <f> Returns the distance in meters between two points
> Ox.getDistance({lat: -45, lng: -90}, {lat: 45, lng: 90}) * 2
Ox.EARTH_CIRCUMFERENCE
@*/
Ox.getDistance = function(point0, point1) {
var point0 = rad(point0),
point1 = rad(point1);
return Math.acos(
Math.sin(point0.lat) * Math.sin(point1.lat) +
Math.cos(point0.lat) * Math.cos(point1.lat) *
Math.cos(point1.lng - point0.lng)
) * Ox.EARTH_RADIUS;
};
/*@
Ox.getLatLngByXY <f> Returns lat/lng for a given x/y on a 1x1 mercator projection
> Ox.getLatLngByXY({x: 0.5, y: 0.5})
{lat: 0, lng: 0}
@*/
Ox.getLatLngByXY = function(xy) {
function getVal(val) {
return (val - 0.5) * 2 * Math.PI;
}
return {
lat: -Ox.deg(Math.atan(Ox.sinh(getVal(xy.y)))),
lng: Ox.deg(getVal(xy.x))
}
};
/*@
Ox.getMetersPerDegree <f> Returns meters per degree at a given latitude
> Ox.getMetersPerDegree(0) * 360
Ox.EARTH_CIRCUMFERENCE
@*/
Ox.getMetersPerDegree = function(lat) {
return Math.cos(lat * Math.PI / 180) * Ox.EARTH_CIRCUMFERENCE / 360;
};
/*@
Ox.getXYByLatLng <f> Returns x/y on a 1x1 mercator projection for a given lat/lng
> Ox.getXYByLatLng({lat: 0, lng: 0})
{x: 0.5, y: 0.5}
@*/
Ox.getXYByLatLng = function(latlng) {
function getVal(val) {
return (val / (2 * Math.PI) + 0.5)
}
return {
x: getVal(Ox.rad(latlng.lng)),
y: getVal(Ox.asinh(Math.tan(Ox.rad(-latlng.lat))))
};
};
}());
//@ Ox.Line <f> (undocumented)
Ox.Line = function(point0, point1) {
var self = {
points: [point0, point1]
},
that = this;
function rad() {
return self.points.map(function(point) {
return {
lat: Ox.rad(point.lat()),
lng: Ox.rad(point.lng())
};
});
}
that.getArea = function() {
};
that.getBearing = function() {
};
that.getDistance = function() {
var points = rad();
return Math.acos(
Math.sin(point[0].lat) * Math.sin(point[1].lat) +
Math.cos(point[0].lat) * Math.cos(point[1].lat) *
Math.cos(point[1].lng - point[0].lng)
) * Ox.EARTH_RADIUS;
};
that.getMidpoint = function() {
var points = rad(),
x = Math.cos(point[1].lat) *
Math.cos(point[1].lng - point[0].lng),
y = Math.cos(point[1].lat) *
Math.sin(point[1].lng - point[0].lng),
d = Math.sqrt(
Math.pow(Math.cos(point[0].lat) + x, 2) + Math.pow(y, 2)
),
lat = Ox.deg(
Math.atan2(Math.sin(points[0].lat) + Math.sin(points[1].lat), d)
),
lng = Ox.deg(
points[0].lng + Math.atan2(y, math.cos(points[0].lat) + x)
);
return new Point(lat, lng);
};
that.points = function() {
};
return that;
};
//@ Ox.Point <f> (undocumented)
Ox.Point = function(lat, lng) {
var self = {lat: lat, lng: lng},
that = this;
that.lat = function() {
};
that.latlng = function() {
};
that.lng = function() {
};
that.getMetersPerDegree = function() {
return Math.cos(self.lat * Math.PI / 180) *
Ox.EARTH_CIRCUMFERENCE / 360;
}
that.getXY = function() {
return [
getXY(Ox.rad(self.lng)),
getXY(Ox.asinh(Math.tan(Ox.rad(-self.lat))))
];
};
return that;
};
//@ Ox.Rectangle <f> (undocumented)
Ox.Rectangle = function(point0, point1) {
var self = {
points: [
new Point(
Math.min(point0.lat(), point1.lat()),
point0.lng()
),
new Point(
Math.max(point0.lat(), point1.lat()),
point1.lng()
)
]
},
that = this;
that.contains = function(rectangle) {
}
that.crossesDateline = function() {
return self.points[0].lng > self.points[1].lng;
}
that.getCenter = function() {
return new Ox.Line(self.points[0], self.points[1]).getMidpoint();
};
that.intersects = function(rectangle) {
};
return that;
};
//@ HTML -----------------------------------------------------------------------
/*@
Ox.parseEmailAddresses <f> Takes HTML and turns e-mail addresses into links
@*/
// fixme: no tests
Ox.parseEmailAddresses = function(html) {
return html.replace(
/\b([0-9A-Z\.\+\-_]+@(?:[0-9A-Z\-]+\.)+[A-Z]{2,6})\b/gi,
'<a href="mailto:$1" title="mailto:$1">$1</a>'
);
};
/*@
Ox.parseHTML <f> Takes HTML from an untrusted source and returns something sane
> Ox.parseHTML('http://foo.com, bar')
'<a href="http://foo.com" title="http://foo.com">foo.com</a>, bar'
> Ox.parseHTML('(see: www.foo.com)')
'(see: <a href="http://www.foo.com" title="http://www.foo.com">www.foo.com</a>)'
> Ox.parseHTML('foo@bar.com')
'<a href="mailto:foo@bar.com" title="mailto:foo@bar.com">foo@bar.com</a>'
> Ox.parseHTML('<a href="http://foo.com" onmouseover="alert()">foo</a>')
'<a href="http://foo.com" title="http://foo.com">foo</a>'
> Ox.parseHTML('<a href="javascript:alert()">foo</a>')
'&lt;a href=&quot;javascript:alert()&quot;&gt;foo&lt;/a&gt;'
> Ox.parseHTML('[http://foo.com foo]')
'<a href="http://foo.com" title="http://foo.com">foo</a>'
> Ox.parseHTML('<rtl>foo</rtl>')
'<div style="direction: rtl">foo</div>'
> Ox.parseHTML('<script>alert()</script>')
'&lt;script&gt;alert()&lt;/script&gt;'
@*/
Ox.parseHTML = (function() {
var defaultTags = [
'a', 'b', 'blockquote', 'cite', 'code',
'del', 'em', 'i', 'img', 'ins',
'li', 'ol', 'q', 'rtl', 's',
'strong', 'sub', 'sup', 'ul', '[]'
],
parse = {
a: {
'<a [^<>]*?href="(https?:\/\/.+?)".*?>': '<a href="{1}" title="{1}">',
'<\/a>': '</a>'
},
img: {
'<img [^<>]*?src="(https?:\/\/.+?)".*?>': '<img src="{1}">'
},
rtl: {
'<rtl>': '<div style="direction: rtl">',
'<\/rtl>': '</div>'
},
'*': function(tag) {
var ret = {};
ret['<(/?' + tag + ')>'] = '<{1}>';
return ret;
}
},
tab = '\t';
return function(html, tags, wikilinks) {
var matches = [],
tags = tags || defaultTags;
// html = Ox.clean(html); fixme: can this be a parameter?
if (tags.indexOf('[]') > -1) {
html = html.replace(/\[(https?:\/\/.+?) (.+?)\]/gi, '<a href="$1">$2</a>');
tags = tags.filter(function(tag) {
return tag != '[]';
});
}
tags.forEach(function(tag) {
var p = parse[tag] || parse['*'](tag);
Ox.forEach(p, function(replace, regexp) {
html = html.replace(new RegExp(regexp, 'gi'), function() {
matches.push(Ox.formatString(replace, arguments));
return tab + (matches.length - 1) + tab;
});
});
});
html = Ox.encodeHTML(html);
html = Ox.parseURLs(html);
html = Ox.parseEmailAddresses(html);
matches.forEach(function(match, i) {
html = html.replace(new RegExp(tab + i + tab, 'gi'), match);
});
html = html.replace(/\n/g, '<br/>\n')
// close extra opening (and remove extra closing) tags
// return $('<div>').html(html).html();
// fixme: this converts '&quot;' to '"'
return Ox.element('<div>').html(html).html();
}
}());
/*@
Ox.parseURL <f> Takes a URL, returns its components
(url) -> <o> URL components
url <s> URL
<script>
Ox.test.object = Ox.parseURL('http://www.foo.com:8080/bar/index.html?a=0&b=1#c');
</script>
> Ox.test.object.hash
'#c'
> Ox.test.object.host
'www.foo.com:8080'
> Ox.test.object.hostname
'www.foo.com'
> Ox.test.object.origin
'http://www.foo.com:8080'
> Ox.test.object.pathname
'/bar/index.html'
> Ox.test.object.port
'8080'
> Ox.test.object.protocol
'http:'
> Ox.test.object.search
'?a=0&b=1'
@*/
Ox.parseURL = (function() {
// fixme: leak memory, like now, or create every time? ... benchmark??
var a = document.createElement('a'),
keys = ['hash', 'host', 'hostname', 'origin',
'pathname', 'port', 'protocol', 'search'];
return function(str) {
var ret = {};
a.href = str;
keys.forEach(function(key) {
ret[key] = a[key];
});
return ret;
};
}());
/*@
Ox.parseURLs <f> Takes HTML and turns URLs into links
@*/
// fixme: is parseURLs the right name?
// fixme: no tests
Ox.parseURLs = function(html) {
return html.replace(
/\b((https?:\/\/|www\.).+?)([\.,:;!\?\)\]]*?(\s|$))/gi,
function(str, url, pre, end) {
url = (pre == 'www.' ? 'http://' : '' ) + url;
return Ox.formatString(
'<a href="{url}" title="{url}">{host}</a>{end}',
{
end: end,
host: Ox.parseURL(url).hostname,
url: url
}
);
}
);
};
//@ JavaScript -----------------------------------------------------------------
/*@
Ox.doc <f> Generates documentation for annotated JavaScript
(file, callback) -> <u> undefined
file <s> JavaScript file
callback <f> Callback function
doc <[o]> Array of doc objects
arguments <[o]|u> Arguments (array of doc objects)
Present if the <code>type</code> of the item is
<code>"function"</code>.
description <s|u> Multi-line description with optional markup
See Ox.parseHTML for details
events <[o]|u> Events (array of doc objects)
Present if the item fires any events
file <s> File name
line <n> Line number
name <s> Name of the item
properties <[o]|u> Properties (array of doc objects)
Present if the <code>type</code> of the item is
<code>"event"</code>, <code>"function"</code>
or <code>"object"</code>.
section <s|u> Section in the file
source <[o]> Source code (array of tokens)
length <n> Length of the token
offset <n> Offset of the token
type <s> Type of the token
See Ox.tokenize for list of types
summary <s> One-line summary
usage <[o]> Usage (array of doc objects)
Present if the <code>type</code> of the item is
<code>"function"</code>.
type <s> Type of the item
(source) <a> Array of documentation objects
source <s> JavaScript source code
# > Ox.doc("//@ Ox.foo <string> just some string")
# [{"name": "Ox.foo", "summary": "just some string", "type": "string"}]
@*/
Ox.doc = (function() {
// fixme: dont require the trailing '@'
var re = {
item: /^(.+?) <(.+?)> (.+)$/,
multiline: /^\/\*\@.*?\n([\w\W]+)\n.*?\@\*\/$/,
script: /\n(\s*<script>s*\n[\w\W]+\n\s*<\/script>s*)/g,
singleline: /^\/\/@\s*(.*?)\s*$/,
test: /\n(\s*> .+\n.+?)/g,
usage: /\(.*?\)/
},
types = {
a: 'array', b: 'boolean', d: 'date',
e: 'element', f: 'function', n: 'number',
o: 'object', r: 'regexp', s: 'string',
u: 'undefined', '*': 'any', '!': 'event'
};
function decodeLinebreaks(match, submatch) {
return (submatch || match).replace(/\u21A9/g, '\n');
}
function encodeLinebreaks(match, submatch) {
return '\n' + (submatch || match).replace(/\n/g, '\u21A9');
}
function getIndent(str) {
var indent = -1;
while (str[++indent] == ' ') {}
return indent;
}
function parseItem(str) {
var matches = re.item.exec(str);
// to tell a variable with default value, like
// name <string|'<a href="...">foo</a>'> summary
// from a line of description with tags, like
// some <a href="...">description</a> text
// we need to check if there is either no forward slash
// or if the second last char is a single or double quote
return matches && (
matches[2].indexOf('/') == -1 ||
'\'"'.indexOf(matches[2].substr(-2, 1)) > -1
) ? Ox.extend({
name: parseName(matches[1].trim()),
summary: matches[3].trim()
}, parseType(matches[2])) : null;
}
function parseName(str) {
var matches = re.usage.exec(str);
return matches ? matches[0] : str;
}
function parseNode(node) {
var item = parseItem(node.line), subitem;
node.nodes && node.nodes.forEach(function(node) {
var key, line = node.line, subitem;
if (!/^#/.test(node.line)) {
if (/^<script>/.test(line)) {
item.examples = [parseScript(line)];
} else if (/^>/.test(line)) {
item.examples = item.examples || [];
item.examples.push(parseTest(line));
} else if ((subitem = parseItem(line))) {
if (/^\(/.test(subitem.name)) {
item.usage = item.usage || [];
item.usage.push(parseNode(node));
} else if (subitem.types[0] == 'event') {
item.events = item.events || [];
item.events.push(parseNode(node));
} else {
key = item.types[0] == 'function' ?
'arguments' : 'properties'
item[key] = item[key] || [];
item[key].push(parseNode(node));
}
} else {
item.description = item.description ?
item.description + ' ' + line : line
}
}
});
return item;
}
function parseScript(str) {
// remove script tags and extra indentation
var lines = decodeLinebreaks(str).split('\n'),
indent = getIndent(lines[1]);
return {
statement: Ox.map(lines, function(line, i) {
return i && i < lines.length - 1 ?
line.substr(indent) : null;
}).join('\n')
};
}
function parseTest(str) {
var lines = decodeLinebreaks(str).split('\n');
return {
statement: lines[0].substr(2),
result: lines[1].trim()
};
}
function parseTokens(tokens, includeLeading) {
// fixme: do not strip whitespace from the beginning of the first line of the items' source
var leading = [],
tokens_ = [];
tokens.forEach(function(token) {
if (['linebreak', 'whitespace'].indexOf(token.type) > -1) {
includeLeading && leading.push(token);
} else {
tokens_ = Ox.merge(tokens_, leading, [token]);
leading = [];
includeLeading = true;
}
});
return tokens_;
}
function parseTree(lines) {
// parses indented lines into a tree structure, like
// {line: "...", nodes: [{line: "...", nodes: [...]}]}
var branches = [],
indent,
node = {
// chop the root line
line: lines.shift().trim()
};
if (lines.length) {
indent = getIndent(lines[0]);
lines.forEach(function(line) {
if (getIndent(line) == indent) {
// line is a child,
// make it the root line of a new branch
branches.push([line]);
} else {
// line is a descendant of the last child,
// add it to the last branch
branches[branches.length - 1].push(line);
}
});
node.nodes = branches.map(function(lines) {
return parseTree(lines);
});
}
return node;
}
function parseType(str) {
// returns {types: [""]}
// or {types: [""], default: ""}
// or {types: [""], parent: ""}
var isArray,
ret = {types: []},
split,
type;
// only split by ':' if there is no default string value
if ('\'"'.indexOf(str.substr(-2, 1)) == -1) {
split = str.split(':');
str = split[0];
if (split.length == 2) {
ret.parent = split[1];
}
}
str.split('|').forEach(function(str) {
var unwrapped = unwrap(str);
if (unwrapped in types) {
ret.types.push(wrap(types[unwrapped]))
} else if (
(type = Ox.filter(Ox.values(types), function(type) {
return Ox.startsWith(type, unwrapped);
})).length
) {
ret.types.push(wrap(type[0]));
} else {
ret['default'] = str;
}
});
function unwrap(str) {
return (isArray = /^\[.+\]$/.test(str)) ?
str = str.substr(1, str.length - 2) : str;
}
function wrap(str) {
return isArray ?
'array[' + str + (str != 'any' ? 's' : '') + ']' : str;
}
return ret;
}
return function(file, callback) {
Ox.get(file, function(source) {
var blocks = [],
items = [],
section = '',
tokens = [];
Ox.tokenize(source).forEach(function(token) {
var match;
token.source = source.substr(token.offset, token.length);
if (token.type == 'comment' && (match =
re.multiline.exec(token.source)|| re.singleline.exec(token.source)
)) {
blocks.push(match[1]);
tokens.push([]);
} else if (tokens.length) {
tokens[tokens.length - 1].push(token);
}
});
/*
var blocks = Ox.map(Ox.tokenize(source), function(token) {
// filter out tokens that are not comments
// or don't match the doc comment pattern
var match;
token.source = source.substr(token.offset, token.length);
return token.type == 'comment' && (match =
re.multiline(token.source) || re.singleline(token.source)
) ? match[1] : null;
}),
items = [];
*/
blocks.forEach(function(block, i) {
var item, lastItem,
lines = block
.replace(re.script, encodeLinebreaks)
.replace(re.test, encodeLinebreaks)
.split('\n'),
tree = parseTree(lines);
if (re.item.test(tree.line)) {
// parse the tree's root node
item = parseNode(tree);
item.file = file;
if (section) {
item.section = section;
}
if (/^[A-Z]/.test(item.name)) {
// main item
item.source = parseTokens(tokens[i]);
item.line = source.substr(0, item.source[0].offset)
.split('\n').length;
items.push(item);
} else {
// property of a function item
lastItem = items[items.length - 1];
lastItem.properties = lastItem.properties || [];
lastItem.properties.push(item);
// include leading linebreaks and whitespace
lastItem.source = Ox.merge(
lastItem.source, parseTokens(tokens[i], true)
);
}
} else {
section = tree.line.split(' ')[0]
}
});
callback(items);
});
}
}());
/*@
Ox.minify <f> Minifies JavaScript (experimental!)
@*/
Ox.minify = function(source) {
var tokens = Ox.tokenize(source)
return Ox.map(tokens, function(token, i) {
var ret = source.substr(token.offset, token.type == 'whitespace' ? 1 : token.length);
if (token.type == 'comment') {
ret = null;
} else if (
token.type == 'linebreak' && (
i == 0 || i == tokens.length - 1 ||
tokens[i - 1].type == 'linebreak' ||
tokens[i + 1].type == 'linebreak'
)
) {
ret = null;
} else if (
token.type == 'whitespace' && (
i == 0 || i == tokens.length - 1 ||
tokens[i - 1].type != 'keyword' || (
tokens[i + 1].type == 'operator'
)
)
) {
ret = null;
}
return ret;
}).join('');
};
/*@
Ox.test <f> Takes JavaScript, runs inline tests, returns results
@*/
Ox.test = function(file, callback) {
Ox.doc(file, function(items) {
var tests = [];
items.forEach(function(item) {
item.examples && item.examples.forEach(function(example) {
var actual = eval(example.statement);
if (example.result) {
tests.push({
actual: JSON.stringify(actual),
expected: example.result,
name: item.name,
section: item.section,
statement: example.statement,
passed: Ox.isEqual(eval(
'Ox.test.result = ' + example.result
), actual)
});
}
});
});
callback(tests);
});
};
/*@
Ox.tokenize <f> Tokenizes JavaScript
(source) -> <[o]> Array of tokens
length <n> Length of the token
offset <n> Offset of the token
type <s> Type of the token
Type can be <code>"comment"</code>, <code>"constant"</code>,
<code>"identifier"</code>, <code>"keyword"</code>,
<code>"linebreak"</code>, <code>"method"</code>,
<code>"number"</code>, <code>"object"</code>,
<code>"operator"</code>, <code>"property"</code>,
<code>"regexp"</code>, <code>"string"</code>
or <code>"whitespace"</code>
source <s> JavaScript source code
@*/
Ox.tokenize = (function() {
// see https://github.com/mozilla/narcissus/blob/master/lib/jslex.js
// and https://developer.mozilla.org/en/JavaScript/Reference
var identifier = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_',
linebreak = '\n\r',
number = '0123456789',
operator = [
// arithmetic
'+', '-', '*', '/', '%', '++', '--',
// assignment
'=', '+=', '-=', '*=', '/=', '%=',
'&=', '|=', '^=', '<<=', '>>=', '>>>=',
// bitwise
'&', '|', '^', '~', '<<', '>>', '>>>',
// comparison
'==', '!=', '===', '!==', '>', '>=', '<', '<=',
// conditional
'?', ':',
// grouping
'(', ')', '[', ']', '{', '}',
// logical
'&&', '||', '!',
// other
'.', ',', ';'
],
whitespace = ' \t',
word = {
constant: [
// Math
'E', 'LN2', 'LN10', 'LOG2E', 'LOG10E', 'PI', 'SQRT1_2', 'SQRT2',
// Number
'MAX_VALUE', 'MIN_VALUE', 'NEGATIVE_INFINITY', 'POSITIVE_INFINITY'
],
keyword: [
'break',
'case', 'catch', 'class', 'const', 'continue',
'debugger', 'default', 'delete', 'do',
'else', 'enum', 'export', 'extends',
'false', 'finally', 'for', 'function',
'if', 'implements', 'import', 'in', 'instanceof', 'interface',
'let', 'module',
'new', 'null',
'package', 'private', 'protected', 'public',
'return',
'super', 'switch', 'static',
'this', 'throw', 'true', 'try', 'typeof',
'var', 'void',
'yield',
'while', 'with',
],
method: [
// Array
'concat',
'every',
'filter', 'forEach',
'join',
'lastIndexOf',
'indexOf', 'isArray',
'map',
'pop', 'push',
'reduce', 'reduceRight', 'reverse',
'shift', 'slice', 'some', 'sort', 'splice',
'unshift',
// Date
'getDate', 'getDay', 'getFullYear', 'getHours',
'getMilliseconds', 'getMinutes', 'getMonth', 'getSeconds',
'getTime', 'getTimezoneOffset',
'getUTCDate', 'getUTCDay', 'getUTCFullYear', 'getUTCHours',
'getUTCMilliseconds', 'getUTCMinutes', 'getUTCMonth', 'getUTCSeconds',
'now',
'parse',
'setDate', 'setFullYear', 'setHours', 'setMilliseconds',
'setMinutes', 'setMonth', 'setSeconds', 'setTime',
'setUTCDate', 'setUTCFullYear', 'setUTCHours', 'setUTCMilliseconds',
'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds',
'toDateString', 'toJSON', 'toLocaleDateString', 'toLocaleString',
'toLocaleTimeString', 'toTimeString', 'toUTCString',
'UTC',
// Function
'apply', 'bind', 'call', 'isGenerator',
// JSON
'parse', 'stringify',
// Math
'abs', 'acos', 'asin', 'atan', 'atan2',
'ceil', 'cos',
'exp',
'floor',
'log',
'max', 'min',
'pow',
'random', 'round',
'sin', 'sqrt',
'tan',
// Number
'toExponential', 'toFixed', 'toLocaleString', 'toPrecision',
// Object
'create',
'defineProperty', 'defineProperties',
'freeze',
'getOwnPropertyDescriptor', 'getOwnPropertyNames', 'getPrototypeOf',
'hasOwnProperty',
'isExtensible', 'isFrozen', 'isPrototypeOf', 'isSealed',
'keys',
'preventExtensions', 'propertyIsEnumerable',
'seal',
'toLocaleString', 'toString',
'valueOf',
// RegExp
'exec', 'test',
// String
'charAt', 'charCodeAt', 'concat',
'fromCharCode',
'indexOf',
'lastIndexOf', 'localeCompare',
'match',
'replace',
'search', 'slice', 'split', 'substr', 'substring',
'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toUpperCase', 'trim',
// Window
'addEventListener', 'alert', 'atob',
'blur', 'btoa',
'clearInterval', 'clearTimeout', 'close', 'confirm',
'dispatchEvent',
'escape',
'find', 'focus',
'getComputedStyle', 'getSelection',
'moveBy', 'moveTo',
'open',
'postMessage', 'print', 'prompt',
'removeEventListener', 'resizeBy', 'resizeTo',
'scroll', 'scrollBy', 'scrollTo',
'setCursor', 'setInterval', 'setTimeout', 'stop',
'unescape'
],
object: [
'Array',
'Boolean',
'Date', 'decodeURI', 'decodeURIComponent',
'encodeURI', 'encodeURIComponent', 'Error', 'eval', 'EvalError',
'Function',
'Infinity', 'isFinite', 'isNaN',
'JSON',
'Math',
'NaN', 'Number',
'Object',
'parseFloat', 'parseInt',
'RangeError', 'ReferenceError', 'RegExp',
'String', 'SyntaxError',
'TypeError',
'undefined', 'URIError',
'window'
],
property: [
// Function
'constructor', 'length', 'prototype',
// RegExp
'global', 'ignoreCase', 'lastIndex', 'multiline', 'source',
// Window
'applicationCache',
'closed', 'content', 'crypto',
'defaultStatus', 'document',
'frameElement', 'frames',
'history',
'innerHeight', 'innerWidth',
'length', 'location', 'locationbar', 'localStorage',
'menubar',
'name', 'navigator',
'opener', 'outerHeight', 'outerWidth',
'pageXOffset', 'pageYOffset', 'parent', 'personalbar',
'screen', 'screenX', 'screenY', 'scrollbars', 'scrollX', 'scrollY',
'self', 'sessionStorage', 'status', 'statusbar',
'toolbar', 'top'
]
// Window stuff? 'atob', 'btoa', 'console', 'document' ...
};
return function(source) {
//source = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
var cursor = 0,
tokenize = {
comment: function() {
while (char = source[++cursor]) {
if (next == '/' && char == '\n') {
break;
} else if (next == '*' && char == '*' && source[cursor + 1] == '/') {
cursor += 2;
break;
}
}
},
identifier: function() {
var str;
while ((identifier + number).indexOf(source[++cursor]) > -1) {}
str = source.substring(start, cursor);
Ox.forEach(word, function(value, key) {
if (value.indexOf(str) > -1) {
type = key;
return false;
}
});
},
linebreak: function() {
while (linebreak.indexOf(source[++cursor]) > -1) {}
},
number: function() {
while ((number + '.').indexOf(source[++cursor]) > -1) {}
},
operator: function() {
if (operator.indexOf(char + source[++cursor]) > -1) {
if (operator.indexOf(char + next + source[++cursor]) > 1) {
++cursor;
}
}
},
regexp: function() {
while ((char = source[++cursor]) != '/') {
char == '\\' && ++cursor;
if (cursor == source.length) {
break;
}
}
while (identifier.indexOf(source[++cursor]) > -1) {}
},
string: function() {
var delimiter = char;
while ((char = source[++cursor]) != delimiter) {
char == '\\' && ++cursor;
if (cursor == source.length) {
break;
}
}
++cursor;
},
whitespace: function() {
while (whitespace.indexOf(source[++cursor]) > -1) {}
}
},
tokens = [],
type;
while (cursor < source.length) {
var char = source[cursor],
next = source[cursor + 1],
start = cursor;
if (char == '/' && (next == '/' || next == '*')) {
type = 'comment';
} else if (identifier.indexOf(char) > -1) {
type = 'identifier';
} else if (linebreak.indexOf(char) > -1) {
type = 'linebreak';
} else if (number.indexOf(char) > -1) {
type = 'number';
} else if (char == "'" || char == '"') {
type = 'string';
} else if (whitespace.indexOf(char) > -1) {
type = 'whitespace';
} else if (char == '/') {
type = isRegExp() ? 'regexp' : 'operator';
} else if (operator.indexOf(char) > -1) {
type = 'operator';
}
tokenize[type]();
tokens.push({
length: cursor - start,
offset: start,
type: type,
});
}
function isRegExp() {
// checks if a forward slash is the beginning of a regexp,
// as opposed to the beginning of an operator
// see http://www.mozilla.org/js/language/js20-2000-07/rationale/syntax.html#regular-expressions
var index = tokens.length,
isRegExp = false
offset = 0;
// scan back to the previous significant token,
// or the beginning of the source
while (
typeof tokens[--index] != 'undefined' &&
['comment', 'linebreak', 'whitespace'].indexOf(tokens[index].type) > -1
) {
offset += tokens[index].length;
}
if (typeof tokens[index] == 'undefined') {
// source begins with forward slash
isRegExp = true;
} else {
prevToken = tokens[index];
prevString = source.substr(cursor - prevToken.length - offset, prevToken.length);
isRegExp = (
prevToken.type == 'keyword' &&
['false', 'null', 'true'].indexOf(prevString) == -1
) || (
prevToken.type == 'operator' &&
['++', '--', ')', ']', '}'].indexOf(prevString) == -1
);
}
return isRegExp;
}
return tokens;
};
}());
//@ Math -----------------------------------------------------------------------
/*@
Ox.asinh <f> Inverse hyperbolic sine
Strangely missing from <code>Math</code>.
@*/
Ox.asinh = function(x) {
// fixme: no test
return Math.log(x + Math.sqrt(x * x + 1));
};
/*@
Ox.deg <f> Takes radians, returns degrees
Strangely missing from <code>Math</code>.
> Ox.deg(2 * Math.PI)
360
@*/
Ox.deg = function(rad) {
return rad * 180 / Math.PI;
};
/*@
Ox.divideInt <f> Divides a number by another and returns an array of integers
<code>Ox.divideInt(num, by)</code> returns a sorted array of "as equal as
possible" integers that has a sum of <code>num</code> and a length of
<code>by</code>.
> Ox.divideInt(100, 3)
[33, 33, 34]
> Ox.divideInt(100, 6)
[16, 16, 17, 17, 17, 17]
@*/
Ox.divideInt = function(num, by) {
var arr = [],
div = parseInt(num / by),
mod = num % by;
Ox.loop(by, function(i) {
arr[i] = div + (i > by - 1 - mod);
});
return arr;
}
/*@
Ox.limit <f> Limits a number by a given mininum and maximum
<code>Ox.limit(num, min, max)</code> is a shorthand for
<code>Math.min(Math.max(num, min), max)</code>
(num) -> <n> <code>num</code>
(num, max) -> <n> <code>Math.max(num, max)</code>
(num, min, max) -> <n> <code>Math.min(Math.max(num, min), max)</code>
num <n> number
min <n> minimum
max <n> maximum
> Ox.limit(1, 2, 3)
2
> Ox.limit(4, 2, 3)
3
> Ox.limit(2, 1)
1
@*/
Ox.limit = function(/*num[[, min], max]*/) {
var len = arguments.length,
num = arguments[0],
min = len == 3 ? arguments[1] : 0, // fixme: should be -Infinity
max = arguments[len - 1];
return Math.min(Math.max(num, min), max);
};
/*@
Ox.log <f> Returns the logarithm of a given number to a given base
Strangely missing from <code>Math</code>.
> Ox.log(100, 10)
2
> Ox.log(Math.E)
1
@*/
Ox.log = function(num, base) {
return Math.log(num) / Math.log(base || Math.E);
};
/*@
Ox.mod <f> Modulo function
Unlike <code>-1 % 10</code>, which returns <code>-1</code>,
<code>Ox.mod(-1, 10)</code> returns <code>9</code>.
> Ox.mod(11, 10)
1
> Ox.mod(-11, 10)
9
@*/
Ox.mod = function(num, by) {
var mod = num % by;
return mod >= 0 ? mod : mod + by;
};
/*@
Ox.rad <f> Takes degrees, returns radians
Strangely missing from <code>Math</code>.
> Ox.rad(360)
2 * Math.PI
@*/
Ox.rad = function(deg) {
return deg * Math.PI / 180;
};
/*@
Ox.random <f> Returns a random integer
> [0, 1, 2].indexOf(Ox.random(3)) > -1
true
> Ox.random(1, 2) == 1
true
@*/
Ox.random = function() {
var len = arguments.length,
min = len == 1 ? 0 : arguments[0],
max = arguments[len - 1];
return min + parseInt(Math.random() * (max - min));
};
/*@
Ox.round <f> Rounds a number with a given number of decimals
> Ox.round(2 / 3, 6)
0.666667
> Ox.round(1 / 2, 3)
0.5
> Ox.round(1 / 2)
1
@*/
Ox.round = function(num, dec) {
var pow = Math.pow(10, dec || 0);
return Math.round(num * pow) / pow;
};
/*@
Ox.sinh <f> Hyperbolic sine
Strangely missing from <code>Math</code>.
@*/
Ox.sinh = function(x) {
// fixme: no test
return (Math.exp(x) - Math.exp(-x)) / 2;
};
//@ Constants ------------------------------------------------------------------
//@ Ox.MAX_LATITUDE <n> Maximum latitude of a Mercator projection
Ox.MAX_LATITUDE = Ox.deg(Math.atan(Ox.sinh(Math.PI)));
//@ Ox.MIN_LATITUDE <n> Minimum latitude of a Mercator projection
Ox.MIN_LATITUDE = -Ox.MAX_LATITUDE;
//@ Object ---------------------------------------------------------------------
/*@
Ox.extend <function> Extends an object with one or more other objects
> Ox.extend({a: 1, b: 1, c: 1}, {b: 2, c: 2}, {c: 3})
{a: 1, b: 2, c: 3}
@*/
Ox.extend = function() {
var obj = arguments[0];
Ox.forEach(Array.prototype.slice.call(arguments, 1), function(arg, i) {
Ox.forEach(arg, function(val, key) {
obj[key] = val;
});
});
return obj;
};
/*@
Ox.serialize <f> Parses an object into query parameters
> Ox.serialize({a: 1, b: 2, c: 3})
'a=1&b=2&c=3'
> Ox.serialize({a: 1, b: 2.3, c: [4, 5]})
'a=1&b=2.3&c=4,5'
@*/
Ox.serialize = function(obj) {
var arr = [];
Ox.forEach(obj, function(val, key) {
arr.push(key + '=' + val);
});
return arr.join('&');
};
/*@
Ox.unserialize <f> Parses query parameters into an object
> Ox.unserialize('a=1&b=2&c=3')
{a: '1', b: '2', c: '3'}
> Ox.unserialize('a=1&b=2.3&c=4,5', true)
{a: 1, b: 2.3, c: [4, 5]}
@*/
Ox.unserialize = function(str, toNumber) {
var obj = {};
Ox.forEach(str.split('&'), function(val) {
var arr = val.split('=');
obj[arr[0]] = !toNumber ? arr[1]
: arr[1].indexOf(',') == -1 ? +arr[1]
: arr[1].split(',').map(function(val) {
return +val;
});
});
return obj;
};
/*
================================================================================
RegExp functions
================================================================================
*/
Ox.regexp = {
'accents': '¨´`ˆ˜',
'letters': 'a-z¨´`ˆ˜äåáàâãæçëéèèñïíìîöóòôõøœßúùûÿ'
};
//@ Requests -------------------------------------------------------------------
/*@
Ox.get <f> Get a remote file
# fixme: remote? same-origin-policy? jsonp?
(url, callback) -> <u> undefined
url <s> Remote URL
callback <f> Callback function
data <s> The contents of the remote resource
@*/
Ox.get = function(url, callback) {
var req = new XMLHttpRequest();
req.open('GET', url, true);
req.onreadystatechange = function() {
if (req.readyState == 4) {
if (req.status == 200) {
callback(req.responseText);
} else {
throw new Error('URL ' + url + ', status ' + req.status);
}
}
};
req.send();
};
/*@
Ox.getJSON <f> Get and parse a remote JSON file
# fixme: remote? same-origin-policy? jsonp?
(url, callback) -> <u> undefined
url <s> Remote URL
callback <f> Callback function
data <s> The contents of the remote resource
@*/
Ox.getJSON = function(url, callback) {
Ox.get(url, function(data) {
callback(JSON.parse(data));
});
}
/*@
Ox.loadFile <f> Loads a file (image, script or stylesheet)
(file="script.js", callback) -> <u> undefined
(file="stylesheet.css", callback) -> <u> undefined
(file="image.png", callback) -> <e> DOM element
file <s> Local path or remote URL
callback <f> Callback function
@*/
Ox.loadFile = (function() {
// fixme: this doesn't handle errors yet
// fixme: rename to getFile?
// fixme: what about array of files?
var cache = {};
return function (file, callback) {
var element,
request,
type = file.split('.').pop();
isImage = type != 'css' && type != 'js';
if (!cache[file]) {
if (isImage) {
element = new Image();
element.onload = addFileToCache;
element.src = file;
} else {
if (!findFileInHead()) {
element = document.createElement(
type == 'css' ? 'link' : 'script'
);
element[type == 'css' ? 'href' : 'src'] = file + '?' + Ox.random(1000000);
element.type = type == 'css' ? 'text/css' : 'text/javascript';
if (/MSIE/.test(navigator.userAgent)) {
// fixme: find a way to check if css/js have loaded in msie
setTimeout(addFileToCache, 2500);
} else {
if (type == 'css') {
element.rel = 'stylesheet';
waitForCSS();
} else {
element.onload = addFileToCache;
}
}
document.getElementsByTagName('head')[0].appendChild(element);
} else {
addFileToCache();
}
}
} else {
callback();
}
function addFileToCache() {
if (isImage) {
// for an image, save the element itself,
// so that it remains in the browser cache
cache['file'] = element;
callback(element);
} else {
cache['file'] = true;
callback();
}
}
function findFileInHead() {
return Ox.makeArray(
document.getElementsByTagName(type == 'css' ? 'link' : 'script')
).map(function(element) {
return element[type == 'css' ? 'href' : 'src'] == file;
}).reduce(function(prev, curr) {
return prev || curr;
}, false);
}
function waitForCSS() {
var error = false;
try {
element.sheet.cssRule;
} catch(e) {
error = true;
setTimeout(function() {
waitForCSS();
}, 25);
}
!error && addFileToCache();
}
};
}());
//@ String ---------------------------------------------------------------------
Ox.basename = function(str) {
/*
fixme: deprecate
>>> Ox.basename("foo/bar/foo.bar")
"foo.bar"
>>> Ox.basename("foo.bar")
"foo.bar"
*/
return str.replace(/^.*[\/\\]/g, '');
};
/*@
Ox.char <f> Alias for String.fromCharCode
@*/
// fixme: add some mapping? like Ox.char(9, 13) or Ox.char([9, 13])?
Ox.char = String.fromCharCode;
/*@
Ox.clean <f> Remove leading, trailing and double whitespace from a string
> Ox.clean("foo bar")
"foo bar"
> Ox.clean(" foo bar ")
"foo bar"
> Ox.clean(" foo \n bar ")
"foo\nbar"
@*/
Ox.clean = function(str) {
return Ox.map(str.split('\n'), function(str) {
return Ox.trim(str.replace(/\s+/g, ' '));
}).join('\n');
};
/*@
Ox.endsWith <f> Checks if a string ends with a given substring
If the substring is a string literal (and not a variable),
<code>/sub$/.test(str)</code> or <code>!!/sub$/(str)</code>
is shorter than <code>Ox.ends(str, sub)</code>.
> Ox.endsWith('foobar', 'bar')
true
@*/
Ox.endsWith = function(str, sub) {
// fixme: rename to ends
return str.substr(str.length - sub.length) == sub;
};
/*@
Ox.highlight <f> Highlight matches in a string
> Ox.highlight('foobar', 'foo', 'match')
'<span class="match">foo</span>bar'
@*/
// fixme: with regexp, special chars would have to be escaped
Ox.highlight = function(txt, str, classname) {
return str && str.length ? txt.replace(
new RegExp('(' + str + ')', 'ig'),
'<span class="' + classname + '">$1</span>'
) : txt;
};
/*@
Ox.isValidEmail <f> Tests if a string is a valid e-mail address
(str) -> <b> True if the string is a valid e-mail address
str <s> Any string
> Ox.isValidEmail("foo@bar.com")
true
> Ox.isValidEmail("foo.bar@foobar.co.uk")
true
> Ox.isValidEmail("foo@bar")
false
> Ox.isValidEmail("foo@bar..com")
false
@*/
// fixme: rename to isEmail
Ox.isValidEmail = function(str) {
return !!/^[0-9A-Z\.\+\-_]+@(?:[0-9A-Z\-]+\.)+[A-Z]{2,6}$/i(str);
}
/*@
Ox.pad <f> Pad a string to a given length
> Ox.pad(1, 2)
"01"
> Ox.pad("abc", -6, ".")
"abc..."
> Ox.pad("foobar", -3, ".")
"foo"
> Ox.pad("abc", -6, "123456")
"abc123"
> Ox.pad("abc", 6, "123456")
"456abc"
@*/
Ox.pad = function(str, len, pad) {
// fixme: slighly obscure signature
// fixme: weird for negative numbers
var pos = len / (len = Math.abs(len));
str = str.toString().substr(0, len);
pad = Ox.repeat(pad || '0', len - str.length);
str = pos == 1 ? pad + str : str + pad;
str = pos == 1 ?
str.substr(str.length - len, str.length) :
str.substr(0, len);
return str;
};
/*@
Ox.parsePath <f> Returns the components of a path
(str) -> <o> Path
extension <s> File extension
filename <s> Filename
pathname <s> Pathname
> Ox.parsePath('/foo/bar/foo.bar')
{extension: 'bar', filename: 'foo.bar', pathname: '/foo/bar/'}
> Ox.parsePath('foo/')
{extension: '', filename: '', pathname: 'foo/'}
> Ox.parsePath('foo')
{extension: '', filename: 'foo', pathname: ''}
> Ox.parsePath('.foo')
{extension: '', filename: '.foo', pathname: ''}
@*/
Ox.parsePath = function(str) {
var matches = /^(.+\/)?(.+?(\..+)?)?$/.exec(str);
return {
pathname: matches[1] || '',
filename: matches[2] || '',
extension: matches[3] ? matches[3].substr(1) : ''
};
}
/*@
Ox.parseSRT <f> Parses an srt subtitle file
(str) -> <o> Parsed subtitles
in <n> In point (sec)
out <n> Out point (sec)
text <s> Text
str <s> Contents of an srt subtitle file
> Ox.parseSRT('1\n01:02:00,000 --> 01:02:03,400\nHello World')
[{'in': 3720, out: 3723.4, text: 'Hello World'}]
@*/
Ox.parseSRT = function(str, fps) {
return str.replace(/\r\n/g, '\n').replace(/\n+$/, '').split('\n\n')
.map(function(block) {
var lines = block.split('\n'), points;
lines.shift();
points = lines.shift().split(' --> ').map(function(point) {
return point.replace(',', ':').split(':')
.reduce(function(prev, curr, i) {
return prev + parseInt(curr, 10) *
[3600, 60, 1, 0.001][i];
}, 0);
});
if (fps) {
points = points.map(function(point) {
return Math.round(point * fps) / fps;
});
}
return {
'in': points[0],
out: points[1],
text: lines.join('\n')
};
});
};
/*@
Ox.repeat <f> Repeat a value multiple times
Works for arrays, numbers and strings
> Ox.repeat(1, 3)
"111"
> Ox.repeat("foo", 3)
"foofoofoo"
> Ox.repeat([1, 2], 3)
[1, 2, 1, 2, 1, 2]
@*/
Ox.repeat = function(val, num) {
var ret;
if (Ox.isArray(val)) {
ret = [];
num >= 1 && Ox.loop(num, function() {
ret = Ox.merge(ret, val);
});
} else {
ret = num >= 1 ? new Array(num + 1).join(val.toString()) : '';
}
return ret;
};
Ox.reverse = function(str) {
/*
Ox.reverse("foo")
oof
*/
return str.toString().split('').reverse().join('');
};
/*@
Ox.startsWith <f> Checks if a string starts with a given substring
If the substring is a string literal (and not a variable),
<code>/^sub/.test(str)</code> or <code>!!/^sub/(str)</code>
is shorter than <code>Ox.starts(str, sub)</code>.
> Ox.startsWith('foobar', 'foo')
true
@*/
Ox.startsWith = function(str, sub) {
// fixme: rename to starts
return str.substr(0, sub.length) == sub;
};
/*@
Ox.stripTags <f> Strips HTML tags from a string
> Ox.stripTags('f<span>o</span>o')
'foo'
@*/
Ox.stripTags = function(str) {
return str.replace(/<.*?>/g, '');
};
/*@
Ox.toCamelCase <f> Takes a string with '-', '/' or '_', returns a camelCase string
> Ox.toCamelCase('foo-bar-baz')
'fooBarBaz'
> Ox.toCamelCase('foo/bar/baz')
'fooBarBaz'
> Ox.toCamelCase('foo_bar_baz')
'fooBarBaz'
@*/
Ox.toCamelCase = function(str) {
return str.replace(/[\-\/_][a-z]/g, function(str) {
return str[1].toUpperCase();
});
};
/*@
Ox.toDashes <f> Takes a camelCase string, returns a string with dashes
> Ox.toDashes('fooBarBaz')
'foo-bar-baz'
@*/
Ox.toDashes = function(str) {
return str.replace(/[A-Z]/g, function(str) {
return '-' + str.toLowerCase();
});
};
/*@
Ox.toSlashes <f> Takes a camelCase string, returns a string with slashes
> Ox.toSlashes('fooBarBaz')
'foo/bar/baz'
@*/
Ox.toSlashes = function(str) {
/*
*/
return str.replace(/[A-Z]/g, function(str) {
return '/' + str.toLowerCase();
});
};
/*@
Ox.toTitleCase <f> Returns a string with capitalized words
> Ox.toTitleCase('foo')
'Foo'
> Ox.toTitleCase('Apple releases iPhone, IBM stock plummets')
'Apple Releases iPhone, IBM Stock Plummets'
@*/
Ox.toTitleCase = function(str) {
return Ox.map(str.split(' '), function(val) {
var sub = val.substr(1),
low = sub.toLowerCase();
if (sub == low) {
val = val.substr(0, 1).toUpperCase() + low;
}
return val;
}).join(' ');
};
/*@
Ox.toUnderscores <f> Takes a camelCase string, returns string with underscores
> Ox.toUnderscores('fooBarBaz')
'foo_bar_baz'
@*/
Ox.toUnderscores = function(str) {
return str.replace(/[A-Z]/g, function(str) {
return '_' + str.toLowerCase();
});
};
Ox.trim = function(str) { // is in jQuery, and in JavaScript itself
/*
Ox.trim(" foo ")
"foo"
*/
return str.replace(/^\s+|\s+$/g, '');
};
/*@
Ox.truncate <f> Truncate a string to a given length
(string, length) <s> Truncated string
(string, length, position) -> <s> Truncated string
(string, length, placeholder) -> <s> Truncated string
(string, length, position, placeholder) -> <s> Truncated string
> Ox.truncate('anticonstitutionellement', 16)
'anticonstitut...'
> Ox.truncate('anticonstitutionellement', 16, '...', 'left')
'...utionellement'
> Ox.truncate('anticonstitutionellement', 16, '>')
'anticonstitutio>'
> Ox.truncate('anticonstitutionellement', 16, '...', 'center')
'anticon...lement'
@*/
Ox.truncate = function(str, len, pad, pos) {
var pad = pad || '...',
pos = pos || 'right',
strlen = str.length,
padlen = pad.length,
left, right;
if (strlen > len) {
if (pos == 'left') {
str = pad + str.substr(padlen + strlen - len);
} else if (pos == 'center') {
left = Math.ceil((len - padlen) / 2);
right = Math.floor((len - padlen) / 2);
str = str.substr(0, left) + pad + str.substr(-right);
} else if (pos == 'right') {
str = str.substr(0, len - padlen) + pad;
}
}
return str;
};
/*@
Ox.words <f> Splits a string into words, removing punctuation
(string) -> <[s]> Array of words
string <s> Any string
> Ox.words('Let\'s "walk" a tree-like key/value store--okay?')
["let's", "walk", "a", "tree-like", "key", "value", "store", "okay"]
@*/
Ox.words = function(str) {
var arr = str.toLowerCase().split(/\b/),
chr = "-'",
len = arr.length,
startsWithWord = /\w/.test(arr[0]);
arr.forEach(function(v, i) {
// find single occurrences of "-" or "-"
// that are not at the beginning or end of the string
// and join the surrounding words with them
if (
i > 0 && i < len - 1 &&
v.length == 1 && chr.indexOf(v) > -1
) {
arr[i + 1] = arr[i - 1] + arr[i] + arr[i + 1];
arr[i - 1] = arr[i] = '';
}
});
// remove elements that have been emptied above
arr = arr.filter(function(v) {
return v.length;
});
// return words, not spaces or punctuation
return arr.filter(function(v, i) {
return i % 2 == !startsWithWord;
});
}
/*@
Ox.wordwrap <f> Wrap a string at word boundaries
> Ox.wordwrap("Anticonstitutionellement, Paris s'eveille", 25, '<br/>')
"Anticonstitutionellement, <br/>Paris s'eveille"
> Ox.wordwrap("Anticonstitutionellement, Paris s'eveille", 16, '<br/>')
"Anticonstitution<br/>ellement, Paris <br/>s'eveille"
> Ox.wordwrap('These are short words', 16, '<br/>', true)
'These are <br/>short words'
@*/
Ox.wordwrap = function(str, len, sep, bal, spa) {
// fixme: bad API, sep/bal/spa should be in options object
var str = str === null ? '' : str.toString(),
len = len || 80,
sep = sep || '<br/>',
bal = bal || false,
spa = Ox.isUndefined(spa) ? true : spa,
words = str.split(' '),
lines;
if (bal) {
// balance lines: test if same number of lines
// can be achieved with a shorter line length
lines = Ox.wordwrap(str, len, sep, false).split(sep);
if (lines.length > 1) {
// test shorter line, unless
// that means cutting a word
var max = Ox.max(Ox.map(words, function(word) {
return word.length;
}));
while (len > max) {
len--;
if (Ox.wordwrap(str, len, sep, false).split(sep).length > lines.length) {
len++;
break;
}
}
}
}
lines = [''];
Ox.forEach(words, function(word) {
if ((lines[lines.length - 1] + word + ' ').length <= len + 1) {
// word fits in current line
lines[lines.length - 1] += word + ' ';
} else {
if (word.length <= len) {
// word fits in next line
lines.push(word + ' ');
} else {
// word is longer than line
var chr = len - lines[lines.length - 1].length;
lines[lines.length - 1] += word.substr(0, chr);
for (var pos = chr; pos < word.length; pos += len) {
lines.push(word.substr(pos, len));
}
lines[lines.length - 1] += ' ';
}
}
});
if (!spa) {
lines = Ox.map(lines, function(line) {
return Ox.trim(line);
});
}
return Ox.trim(lines.join(sep));
};
//@ Type -----------------------------------------------------------------------
/*@
Ox.isArguments <f> Tests if a value is an arguments "array"
(value) -> <b> True if the value is an arguments "array"
value <*> Any value
> Ox.isArguments((function() { return arguments; }()))
true
@*/
Ox.isArguments = /MSIE/.test(navigator.userAgent) ? function(val) {
return !!(val && Object.hasOwnProperty.call(val, 'callee'));
} : function(val) {
return !!(val && val.toString() == '[object Arguments]');
}
/*@
Ox.isArray <f> Tests if a value is an array
(value) -> <b> True if the value is a date
value <*> Any value
> Ox.isArray([])
true
> Ox.isArray((function() { return arguments; }()))
false
> Ox.isArray({0: 0, length: 1})
false
@*/
Ox.isArray = function(val) {
return val instanceof Array;
}
/*@
Ox.isBoolean <f> Tests if a value is boolean
(value) -> <b> True if the value is boolean
value <*> Any value
> Ox.isBoolean(false)
true
@*/
Ox.isBoolean = function(val) {
return typeof val == 'boolean';
};
/*@
Ox.isDate <f> Tests if a value is a date
(value) -> <b> True if the value is a date
value <*> Any value
> Ox.isDate(new Date())
true
@*/
Ox.isDate = function(val) {
return val instanceof Date;
};
/*@
Ox.isElement <f> Tests if a value is a DOM element
(value) -> <b> True if the value is a DOM element
value <*> Any value
> Ox.isElement(document.createElement())
true
@*/
Ox.isElement = function(val) {
return !!(val && val.nodeType == 1);
};
/*@
Ox.isEqual <function> Returns true if two values are equal
> Ox.isEqual((function() { return arguments; }()), (function() { return arguments; }()))
true
> Ox.isEqual([1, 2, 3], [1, 2, 3])
true
> Ox.isEqual([1, 2, 3], [3, 2, 1])
false
> Ox.isEqual(false, false)
true
> Ox.isEqual(new Date(0), new Date(0))
true
> Ox.isEqual(new Date(0), new Date(1))
false
> Ox.isEqual(document.createElement('a'), document.createElement('a'))
true
> Ox.isEqual(document.createElement('a'), document.createElement('b'))
false
> Ox.isEqual(function(a) { return a; }, function(a) { return a; })
true
> Ox.isEqual(function(a) { return a; }, function(b) { return b; })
false
> Ox.isEqual(Infinity, Infinity)
true
> Ox.isEqual(-Infinity, Infinity)
false
> Ox.isEqual(NaN, NaN)
false
> Ox.isEqual(0, 0)
true
> Ox.isEqual({}, {})
true
> Ox.isEqual({a: 1, b: 2, c: 3}, {c: 3, b: 2, a: 1})
true
> Ox.isEqual({a: 1, b: [2, 3], c: {d: '4'}}, {a: 1, b: [2, 3], c: {d: '4'}})
true
> Ox.isEqual(/ /, / /)
true
> Ox.isEqual(/ /g, / /i)
false
> Ox.isEqual('', '')
true
> Ox.isEqual(void 0, void 0)
true
@*/
Ox.isEqual = function(a, b) {
var isEqual = false, type = Ox.typeOf(a);
if (a === b) {
isEqual = true;
} else if (type == Ox.typeOf(b)) {
if (a == b) {
isEqual = true;
} else if (type == 'date') {
isEqual = a.getTime() == b.getTime();
/* toString doesn't do it
} else if (['element', 'function'].indexOf(type) > -1) {
isEqual = a.toString() == b.toString();
*/
} else if (type == 'regexp') {
isEqual = a.global == b.global &&
a.ignore == b.ignore &&
a.multiline == b.multiline &&
a.source == b.source;
} else if (
['arguments', 'array', 'object'].indexOf(type) > -1 &&
Ox.len(a) == Ox.len(b)
) {
isEqual = true;
Ox.forEach(a, function(v, k) {
isEqual = Ox.isEqual(v, b[k]);
return isEqual;
});
}
}
return isEqual;
};
/*@
Ox.isFunction <f> Tests if a value is a function
(value) -> <b> True if the value is a function
value <*> Any value
> Ox.isFunction(function() {})
true
> Ox.isFunction(/ /)
false
@*/
Ox.isFunction = function(val) { // is in jQuery
return typeof val == 'function' && !Ox.isRegExp(val);
};
/*@
Ox.isInfinity <f> Tests if a value is infinite
(value) -> <b> True if the value is infinite
value <*> Any value
> Ox.isInfinity(Infinity)
true
> Ox.isInfinity(-Infinity)
true
> Ox.isInfinity(NaN)
false
@*/
Ox.isInfinity = function(val) {
return typeof val == 'number' && !isFinite(val) && !Ox.isNaN(val);
};
/*@
Ox.isNaN <f> Tests if a value is "Not a Number"
(value) -> <b> True if the value is "Not a Number"
value <*> Any value
> Ox.isNaN(NaN)
true
@*/
Ox.isNaN = function(val) {
return val !== val;
}
/*@
Ox.isNull <f> Tests if a value is <code>null</code>
(value) -> <b> True if the value is <code>null</null>
value <*> Any value
> Ox.isNull(null)
true
@*/
Ox.isNull = function(val) {
return val === null;
};
/*@
Ox.isNumber <f> Tests if a value is a number
(value) -> <b> True if the value is a number
value <*> Any value
> Ox.isNumber(0)
true
> Ox.isNumber(Infinity)
false
> Ox.isNumber(-Infinity)
false
> Ox.isNumber(NaN)
false
@*/
Ox.isNumber = function(val) {
return typeof val == 'number' && isFinite(val);
};
/*@
Ox.isObject <f> Tests if a value is a an object
(value) -> <b> True if the value is an object
value <*> Any value
> Ox.isObject({})
true
> Ox.isObject([])
false
> Ox.isObject(new Date())
false
> Ox.isObject(null)
false
@*/
Ox.isObject = function(val) {
return typeof val == 'object' && !Ox.isArguments(val)
&& !Ox.isArray(val) && !Ox.isDate(val) && !Ox.isNull(val);
};
/*@
Ox.isRegExp <f> Tests if a value is a regular expression
(value) -> <b> True if the value is a regular expression
value <*> Any value
> Ox.isRegExp(/ /)
true
@*/
Ox.isRegExp = function(val) {
return val instanceof RegExp;
};
/*@
Ox.isString <f> Tests if a value is a string
(value) -> <b> True if the value is a string
value <*> Any value
> Ox.isString('')
true
@*/
Ox.isString = function(val) {
return typeof val == 'string';
};
/*@
Ox.isUndefined <f> Tests if a value is undefined
(value) -> <b> True if the value is undefined
value <*> Any value
> Ox.isUndefined()
true
@*/
Ox.isUndefined = function(val) {
// fixme: val === void 0 is also nice
return typeof val == 'undefined';
};
/*@
Ox.typeOf <f> Returns the type of a value
(value) -> <s> type
value <*> Any value
# Examples
> (function() { return Ox.typeOf(arguments); }())
"arguments"
> Ox.typeOf([])
"array"
> Ox.typeOf(false)
"boolean"
> Ox.typeOf(new Date())
"date"
> Ox.typeOf(document.createElement())
"element"
> Ox.typeOf(function() {})
"function"
> Ox.typeOf(Infinity)
"infinity"
> Ox.typeOf(NaN)
"nan"
> Ox.typeOf(null)
"null"
> Ox.typeOf(0)
"number"
> Ox.typeOf({})
"object"
> Ox.typeOf(/ /)
"regexp"
> Ox.typeOf('')
"string"
> Ox.typeOf()
"undefined"
@*/
Ox.typeOf = function(val) {
var ret;
Ox.forEach(Ox.TYPES, function(type) {
if (Ox['is' + type](val)) {
ret = type.toLowerCase();
return false;
}
});
return ret;
};