// 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 Variable names arg argument args arguments arr array canFoo boolean 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 Variable definitions var a = { key: value, key: value, key: value }, b = {key: value}, c = {key: value}; Method chaining Obj.fnA({ key: value, key: value, key: value }) .fnB({key: val}) .fnC({key: val}); Simple conditionals condition && expression; Conditionals if (condition) { expression; } Conditionals with long conditions if ( condition && condition && condition ) { expression; } Ternary operator condition ? expression : expression; Ternary operator with long conditions or expressions condition ? expression : condition ? expression : expression; */ // 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 // Fallbacks ------------------------------------------------------------------- // 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 The Ox object See Ox.wrap for details. (value) -> wrapped value value <*> Any value @*/ Ox = function(val) { return Ox.wrap(val); }; /*@ Ox.load Loads a module A module named "Test" provides Ox.Test/Ox.Test.js, in which it defines one method, Ox.load.Test, that takes two arguments, options and callback, and calls callback with one argument, true for success or false if an error occurred. Generally, the module should define Ox.Test and attach its own methods there. (module, callback) -> undefined (module, options, callback) -> undefined (modules, callback) -> undefined module Module name modules Multiple modules {name: options, ...} options Module options callback Callback function success 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 Prints its arguments to the console (arg, ...) -> 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 Returns a unique id () -> Unique id > Ox.uid() != Ox.uid() true @*/ Ox.uid = (function() { var uid = 0; return function() { return uid++; }; }()); /*@ Ox.wrap Wraps a value so that one can directly call any Ox function on it Ox(value) is a shorthand for Ox.wrap(value). (value) -> wrapped value chain Wrap return values to allow chaining value Unwrap the value wrapped by chain() value <*> Any value > Ox("foobar").repeat(2) "foobarfoobar" > Ox("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 Returns an array w/o null or undefined > 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 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 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 Sorts an array, handling leading digits and ignoring capitalization arr Array fn 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 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 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 Returns the average of an array's values, or an object's properties (collection) -> 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.contains 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.copy Returns a (shallow or deep) copy of an object or array > (function() { a = ['v']; b = Ox.copy(a); a[0] = null; return b[0]; }()) 'v' > (function() { a = {k: 'v'}; b = Ox.copy(a); a.k = null; return b.k; }()) 'v' > Ox.clone(0) 0 @*/ Ox.copy = Ox.clone = function(col, deep) { // fixme: remove references to Ox.clone // fixme: is there any use case for shallow copy? var ret = Ox.isArray(col) ? [] : {}; if (deep) { Ox.forEach(col, function(val, key) { ret[key] = ['array', 'object'].indexOf(Ox.typeOf(val)) > -1 ? Ox.clone(val, true) : val; }); } else { ret = Ox.isArray(col) ? col.slice() : Ox.isObject(col) ? Ox.extend({}, col) : col; } return ret; }; /*@ Ox.count 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 Tests if every element of a collection satisfies a given condition Unlike [].every(), Ox.every() 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 Filters a collection by a given condition Unlike [].filter(), Ox.filter() 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 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 forEach loop Ox.forEach() loops over arrays, objects and strings. Returning false from the iterator function acts like a break statement (unlike [].forEach(), like $.each()). The arguments of the iterator function are (value, key) (like [].forEach(), unlike $.each()). (collection, callback) The collection (collection, callback, includePrototype) The collection collection An array, object or string callback Callback function value <*> Value key Key includePrototype If true, include prototype properties > 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 Returns an array element with a given id > Ox.getObjectById([{id: "foo", title: "Foo"}, {id: "bar", title: "Bar"}], "foo") {id: "foo", title: "Foo"} @*/ // fixme: should 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 Returns the index of an array element with a given id > Ox.getPositionById([{id: "foo", title: "Foo"}, {id: "bar", title: "Bar"}], "foo") 0 @*/ // fixme: this should 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 Generic getter and setter function See examples for details. # Usage -------------------------------------------------------------------- Ox.getset(options, args=[]) -> all options Ox.getset(options, args=[key]) -> <*> options[key] Ox.getset(options, args=[key, value], callback, context) -> 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) -> context sets multiple options and calls fn(key, value) for every key/value pair that was added or modified # Arguments ---------------------------------------------------------------- options Options object (key/value pairs) args The arguments "array" of the caller function callback Callback function The callback is called for every key/value pair that was added or modified. key Key value <*> Value context The parent object of the caller function (for chaining) # Examples ----------------------------------------------------------------- > 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) { // [] ret = obj_; } else if (args.length == 1 && !Ox.isObject(args[0])) { // [key] ret = Ox.clone(obj[args[0]]); } else { // [key, val] or [{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 Returns true if a collection is empty > Ox.isEmpty([]) true > Ox.isEmpty({}) true > Ox.isEmpty('') true > Ox.isEmpty(function() {}) true > Ox.isEmpty(function(a) {}) false > Ox.isEmpty(null) false > Ox.isEmpty() false @*/ Ox.isEmpty = function(val) { return Ox.len(val) == 0; }; /*@ Ox.keys Returns the keys of a collection Unlike Object.keys(), Ox.keys() 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 Gets or sets the last element of an array Unlike foobarbaz[foobarbaz.length - 1], Ox.last(foobarbaz) is short. > 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 Returns the length of an array, function, object or string Not to be confused with Ox.length, which is the length property of the Ox function (1). > 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(col) { var type = Ox.typeOf(col); return ['array', 'function', 'string'].indexOf(type) > -1 ? col.length : type == 'object' ? Ox.values(col).length : void 0; }; /*@ Ox.loop For-loop, functional-style Returning false from the iterator function acts like a break statement. Unlike a for loop, Ox.loop doesn't leak its counter variable to the outer scope, but returns it. (stop, callback) -> Next value equivalent to for (var i = 0; i < stop; i++) (start, stop, callback) -> Next value equivalent to for (var i = start; i < stop; i++) or, if start is larger than stop, for (var i = start; i > stop; i--) (start, stop, step, callback) -> Next value equivalent to for (var i = start; i < stop; i += step) or, if step is negative, for (var i = start; i > stop; i += step) start Start value stop Stop value (exclusive) step Step value callback Iterator function i Counter value > Ox.loop(10, function(i) { return i != 4; }) 4 > Ox.loop(0, 3, 2, function() {}) 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 Takes an array-like object and returns a true array Alias for Array.prototype.slice.call (value) -> 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 Takes an array and returns an object Ox.makeObject is a helper for functions with two alternative signatures like ('key', 'val') and ({key: 'val'}). > (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')) {foo: void 0} > (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 Transforms the values of an array, object or string Unlike [].map(), Ox.map() works for arrays, objects and strings. Returning null 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 may be 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 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 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 Python-style range (stop) -> <[n]> range Returns an array of integers from 0 (inclusive) to stop (exclusive). (start, stop) -> <[n]> range Returns an array of integers from start (inclusive) to stop (exclusive). (start, stop, step) -> <[n]> range Returns an array of numbers from start (inclusive) to stop (exclusive), incrementing by step. start Start value stop Stop value step 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 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 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 Tests if one or more elements of a collection satisfy a given condition Unlike [].some(), Ox.some() 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 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 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 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 Recursively walk a tree-like key/value store > 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 Takes RGB values and returns HSL values (rgb) <[n]> HSL values (r, g, b) <[n]> HSL values rgb <[n]> RGB values r red g green b 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 Takes HSL values and returns RGB values (hsl) <[n]> RGB values (h, s, l) <[n]> RGB values hsl <[n]> HSL values h hue s saturation l 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.BASE_32_ALIASES Base 32 aliases Ox.BASE_32_ALIASES = {'I': '1', 'L': '1', 'O': '0', 'U': 'V'}, //@ Ox.BASE_32_DIGITS Base 32 digits Ox.BASE_32_DIGITS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'; //@ 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 Radius of the earth in meters // see http://en.wikipedia.org/wiki/WGS-84 Ox.EARTH_RADIUS = 6378137; //@ Ox.EARTH_CIRCUMFERENCE Circumference of the earth in meters Ox.EARTH_CIRCUMFERENCE = Ox.EARTH_RADIUS * 2 * Math.PI; //@ Ox.HTML_ENTITIES HTML entities for ... (FIXME) Ox.HTML_ENTITIES = { '"': '"', '&': '&', "'": ''', '<': '<', '>': '>' }; //@ Ox.KEYS 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', 128: 'f17', 129: 'f18', 130: 'f19', 131: 'f20', 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 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 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]> ['K', 'M', 'G', 'T', 'P'] 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 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 Ox.type() Ox.TYPES = [ 'Arguments', 'Array', 'Boolean', 'Date', 'Element', 'Function', 'Infinity', 'NaN', 'Null', 'Number', 'Object', 'RegExp', 'String', 'Undefined' ]; //@ Ox.VERSION 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 Get the day of a date, optionally UTC // see Ox.setSeconds for source code /*@ Ox.getDateInWeek Get the date that falls on a given weekday in the same week # Usage (date, weekday) -> Date (date, weekday, utc) -> Date # Arguments date Date weekday 1-7 (Monday-Sunday) or name, full ("Monday") or short ("Sun") utc 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 Get the weekday of a date, optionally UTC // see Ox.setSeconds for source code /*@ Ox.getDayOfTheYear Get the day of the year for a given date # Usage (date) -> Date (date, utc) -> Date # Arguments date Date utc 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 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 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 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 Get the year of a date, optionally UTC // see Ox.setSeconds for source code //@ Ox.getHours Get the hours of a date, optionally UTC // see Ox.setSeconds for source code /*@ Ox.getISODate 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 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 Get the ISO week of a given date See ISO 8601 > 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 Get the ISO year of a given date See ISO 8601 > 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 Get the milliseconds of a date // see Ox.setSeconds for source code //@ Ox.getMinutes Get the minutes of a date, optionally UTC // see Ox.setSeconds for source code //@ Ox.getMonth Get the month of a date, optionally UTC // see Ox.setSeconds for source code //@ Ox.getSeconds Get the seconds of a date // see Ox.setSeconds for source code //@ Ox.getTime Alias for +new Date() (deprecated) Ox.getTime = function() { // fixme: needed? return +new Date(); }; /*@ Ox.getTimezoneOffset Get the local time zone offset in milliseconds @*/ Ox.getTimezoneOffset = function() { return new Date().getTimezoneOffset() * 60000; }; /*@ Ox.getTimezoneOffsetString 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 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 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 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 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 Takes a string ('YYYY-MM-DD HH:MM:SS') and returns a date str string utc 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 Set the day of a date, optionally UTC // see Ox.setSeconds for source code //@ Ox.setDay Set the weekday of a date, optionally UTC // see Ox.setSeconds for source code //@ Ox.setFullYear Set the year of a date, optionally UTC // see Ox.setSeconds for source code //@ Ox.setHours Set the hours of a date, optionally UTC // see Ox.setSeconds for source code //@ Ox.setMilliseconds Set the milliseconds of a date // see Ox.setSeconds for source code //@ Ox.setMinutes Set the minutes of a date, optionally UTC // see Ox.setSeconds for source code //@ Ox.setMonth Set the month of a date, optionally UTC // see Ox.setSeconds for source code //@ Ox.setSeconds 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 Generic canvas object Returns an object with the properties: canvas, context, data and imageData. # Usage -------------------------------------------------------------------- Ox.canvas(width, height) -> canvas Ox.canvas(image) -> canvas # Arguments ---------------------------------------------------------------- width Width in px height Height in px image 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('').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 Calls a callback function once the DOM is ready (callback) -> If true, the document was ready callback 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 Generic HTML element, mimics jQuery (str) -> Element object str Tagname ('') or selector ('tagname', '.classname', '#id') > Ox.element("
").addClass("red").hasClass("red") true > Ox.element("
").addClass("red").removeClass("red").hasClass("red") false > Ox.element("
").addClass("red").addClass("red")[0].className "red" > Ox.element("
").attr({id: "red"}).attr("id") "red" > Ox.element("
").css("color", "red").css("color") "red" > Ox.element("
").html("red").html() "red" @*/ Ox.element = function(str) { return { //@ 0 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 Adds a class name (className) -> This element className Class name @*/ addClass: function(str) { this[0].className = this[0].className ? Ox.unique( (this[0].className + ' ' + str).split(' ') ).join(' ') : str; return this; }, /*@ append Appends another element to this element (element) -> This element element Another element @*/ append: function(element) { this[0].appendChild(element[0]); return this; }, /*@ appendTo appends this element object to another element object (element) -> This element element Another element @*/ appendTo: function(element) { element[0].appendChild(this[0]); return this; }, /*@ attr Gets or sets an attribute (key) -> Value (key, value) -> This element ({key, value}) -> This element key Attribute name value 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 Binds a function to the click event (callback) -> This element callback Callback function event The DOM event @*/ // fixme: why not a generic bind? click: function(callback) { this[0].onclick = callback; return this; }, /*@ css Gets or sets a CSS attribute (key) -> Value (key, value) -> This element ({key, value}) -> This element key Attribute name value 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 Returns true if the element has a given class (className) -> True if the element has the class className Class name @*/ hasClass: function(str) { return this[0].className.split(' ').indexOf(str) > -1; }, /*@ html Gets or sets the inner HTML () -> The inner HTML (html) -> This element html 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 Binds a function to the mousedown event (callback) -> This element callback Callback function event The DOM event @*/ // fixme: why not a generic bind? mousedown: function(callback) { this[0].onmousedown = callback; return this; }, /*@ removeAttr Removes an attribute (key) -> This element key The attribute @*/ removeAttr: function(key) { this[0].removeAttribute(key); return this; }, /*@ removeClass Removes a class name (className) -> This element className Class name @*/ removeClass: function(str) { this[0].className = Ox.filter( this[0].className.split(' '), function(className) { return className != str; } ).join(' '); return this; } } }; //@ Encoding ------------------------------------------------------------------- (function() { 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.encodeBase26 Encode a number as base26 > Ox.encodeBase26(3758) 'FOO' @*/ Ox.encodeBase26 = function(num) { return Ox.map(num.toString(26), function(char) { return Ox.char(65 + parseInt(char, 26)); }).join(''); }; /*@ Ox.decodeBase26 Decodes a base26-encoded number See Base 32. > Ox.decodeBase26('foo') 3758 @*/ Ox.decodeBase26 = function(str) { return parseInt(Ox.map(str.toUpperCase(), function(char) { return (char.charCodeAt(0) - 65).toString(26); }).join(''), 26); }; /*@ Ox.encodeBase32 Encode a number as base32 See Base 32. > Ox.encodeBase32(15360) 'F00' > Ox.encodeBase32(33819) '110V' @*/ Ox.encodeBase32 = function(num) { return Ox.map(num.toString(32), function(char) { return Ox.BASE_32_DIGITS[parseInt(char, 32)]; }).join(''); }; /*@ Ox.decodeBase32 Decodes a base32-encoded number See Base 32. > 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 = Ox.BASE_32_DIGITS.indexOf(Ox.BASE_32_ALIASES[char] || char); return (index == -1 ? ' ' : index).toString(32); }).join(''), 32); }; /*@ Ox.encodeBase64 Encode a number as base64 > Ox.encodeBase64(32394) 'foo' @*/ Ox.encodeBase64 = function(num) { return btoa(Ox.encodeBase256(num)).replace(/=/g, ''); }; /*@ Ox.decodeBase64 Decodes a base64-encoded number > Ox.decodeBase64('foo') 32394 @*/ Ox.decodeBase64 = function(str) { return Ox.decodeBase256(atob(str)); }; /*@ Ox.encodeBase128 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 || '0'; }; /*@ Ox.decodeBase128 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 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 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 Encodes a string, using deflate Since PNGs are deflate-encoded, the canvas object's toDataURL 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) -> The encoded string str 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 Decodes an deflate-encoded string Since PNGs are deflate-encoded, the canvas object's drawImage 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) -> undefined str The string to be decoded callback Callback function str 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 HTML-encodes a string > Ox.encodeHTML('\'<"&">\'') ''<"&">'' > Ox.encodeHTML('äbçdê') 'äbçdê' @*/ 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 Decodes an HTML-encoded string > Ox.decodeHTML(''<"&">'') '\'<"&">\'' > Ox.decodeHTML(''<"&">'') '\'<"&">\'' > Ox.decodeHTML('äbçdê') 'äbçdê' > Ox.decodeHTML('äbçdê') 'ä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('
').html(str)[0].childNodes[0].nodeValue; }; /*@ Ox.encodePNG 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) -> An image into which the string has been encoded img Any JavaScript PNG image object str 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 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) -> undefined img The image into which the string has been encoded callback Callback function str 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 Encodes a string as UTF-8 see http://en.wikipedia.org/wiki/UTF-8 (string) -> UTF-8 encoded string string 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 Decodes an UTF-8-encoded string see http://en.wikipedia.org/wiki/UTF-8 (utf8) -> string utf8 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 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 (strange one) @*/ Ox.formatColor = function(val, type) { var background, color, element; 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 = Math.round(val * 255); return val < 0.5 ? 128 + v : v - 128; }); } element = Ox.element('
') .css({ borderRadius: '4px', padding: '0 3px 1px 3px', color: 'rgb(' + color.join(', ') + ')', overflow: 'hidden', textOverflow: 'ellipsis', //textShadow: 'black 1px 1px 1px' }) .html(Ox.formatNumber(val, 3)); if (Ox.isNumber(background[0])) { element.css({background: 'rgb(' + background.join(', ') + ')'}); } else { ['moz', 'o', 'webkit'].forEach(function(browser) { element.css({ background: '-' + browser + '-linear-gradient(left, ' + background.map(function(rgb, i) { return 'rgb(' + rgb.join(', ') + ') ' + Math.round(i * 100 / 6) + '%'; }).join(', ') + ')' }); }); } return element }; /*@ Ox.formatCurrency 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 Formats a date according to a format string See strftime and ISO 8601. '%Q' (quarter) and '%X'/'%x' (year with 'BC'/'AD') are non-standard > 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 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 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 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 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 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 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 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 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 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 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 Returns true if a given rectangle crosses the dateline @*/ Ox.crossesDateline = function(point0, point1) { return point0.lng > point1.lng; } /*@ Ox.getArea 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 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 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 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 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 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 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 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 (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 (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 (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 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, '$1' ); }; /*@ Ox.parseHTML Takes HTML from an untrusted source and returns something sane > Ox.parseHTML('http://foo.com, bar') 'foo.com, bar' > Ox.parseHTML('(see: www.foo.com)') '(see: www.foo.com)' > Ox.parseHTML('foo@bar.com') 'foo@bar.com' > Ox.parseHTML('foo') 'foo' > Ox.parseHTML('foo') '<a href="javascript:alert()">foo' > Ox.parseHTML('[http://foo.com foo]') 'foo' > Ox.parseHTML('foo') '
foo
' > Ox.parseHTML('') '<script>alert()</script>' > Ox.parseHTML('\'foo\' < \'bar\' && "foo" > "bar"') '\'foo\' < \'bar\' && "foo" > "bar"' > Ox.parseHTML('foo') 'foo' > Ox.parseHTML('foo') 'foo' @*/ Ox.parseHTML = (function() { var defaultTags = [ // fixme: why not p? 'a', 'b', 'blockquote', 'cite', 'code', 'del', 'em', 'i', 'img', 'ins', 'li', 'ol', 'q', 'rtl', 's', 'strong', 'sub', 'sup', 'ul', '[]' ], parse = { a: { ']*?href="(https?:\/\/.+?)".*?>': '', '<\/a>': '' }, img: { ']*?src="(https?:\/\/.+?)".*?>': '' }, rtl: { '': '
', '<\/rtl>': '
' }, '*': 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, '$2'); 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); Ox.print(html, 'matches', matches); matches.forEach(function(match, i) { html = html.replace(new RegExp(tab + i + tab, 'gi'), match); }); html = html.replace(/\n/g, '
\n') // close extra opening (and remove extra closing) tags // note: this converts '"' to '"' return Ox.element('
').html(html).html(); } }()); /*@ Ox.parseURL Takes a URL, returns its components (url) -> URL components url URL > 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 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( '{host}{end}', { end: end, host: Ox.parseURL(url).hostname, url: url } ); } ); }; //@ JavaScript ----------------------------------------------------------------- /*@ Ox.doc Generates documentation for annotated JavaScript (file, callback) -> undefined file JavaScript file callback Callback function doc <[o]> Array of doc objects arguments <[o]|u> Arguments (array of doc objects) Present if the type of the item is "function". description 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 File name line Line number name Name of the item properties <[o]|u> Properties (array of doc objects) Present if the type of the item is "event", "function" or "object". section Section in the file source <[o]> Source code (array of tokens) length Length of the token offset Offset of the token type Type of the token See Ox.tokenize for list of types summary One-line summary usage <[o]> Usage (array of doc objects) Present if the type of the item is "function". type Type of the item (source) Array of documentation objects source JavaScript source code # > Ox.doc("//@ Ox.foo 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*