// vim: et:ts=4:sw=4:sts=4:ft=js // OxJS (c) 2007-2011 Ox2620, dual-licensed (GPL/MIT), see oxjs.org for details // 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 /*@ Ox The Ox object See Ox.wrap for details. (value) -> wrapped value value <*> any value @*/ Ox = function(val) { return Ox.wrap(val); }; /* ================================================================================ Constants ================================================================================ */ //@ Ox.AMPM <[str]> ['AM', 'PM'] Ox.AMPM = ['AM', 'PM']; //@ Ox.DURATIONS <[str]> ['year', 'month', 'day', 'minute', 'second'] Ox.DURATIONS = ['year', 'month', 'day', 'minute', 'second']; //@ Ox.EARTH_RADIUS Radius of the earth in meters 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 'key0.numpad' too 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 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 = Array.prototype.slice.apply( 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.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); }); /* ================================================================================ Core functions ================================================================================ */ /*@ Ox.doc Generates documentation for annotated JavaScript (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() { var re = { item: /^(.+?) <(.+?)> (.+)$/, multiline: /^\/\*\@.*?\n([\w\W]+)\n.*?\@\*\/$/, script: /\n(\s* > object.options("key", "val").options("key") "val" > 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.load Loads a module (module, callback) -> undefined (module, options, callback) -> undefined module Module name options Module options callback Callback function success If true, the module has been loaded successfully @*/ Ox.load = function(module, options, callback) { // fixme: no way to load multiple modules // problem: where do multiple options go? // [{name: "", options: {}}, {name: "", options: {}}] isn't pretty callback = arguments[arguments.length - 1]; options = arguments.length == 3 ? arguments[1] : {}; Ox.loadFile(Ox.PATH + 'Ox.' + module + '/Ox.' + module + '.js', function() { Ox.load[module](options, callback); }); }; /*@ Ox.loadFile Loads a file (image, script or stylesheet) (file="script.js", callback) -> undefined (file="stylesheet.css", callback) -> undefined (file="image.png", callback) -> DOM element file Local path or remote URL callback Callback function @*/ Ox.loadFile = (function() { // fixme: this doesn't handle errors yet 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; element.type = type == 'css' ? 'text/css' : 'text/javascript'; if (type == 'css') { element.rel = 'stylesheet'; waitForCSS(); } else { element.onload = addFileToCache; } document.head.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 Array.prototype.slice.apply( 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(); } }; }()); /*@ 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").substr(-3) "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.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 Additionally, chain() allows for chaining, and value() returns the original value. See examples for details. (value) -> wrapped value chain wrap the return value to allow chaining value unwrap the value wrapped by 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) { 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 and Object functions ================================================================================ */ /*@ Ox.avg returns the average of an array's values, or an object's properties (collection) -> average collection <[n]|o> An array or object with numerical values > Ox.avg([-1, 0, 1]) 0 > Ox.avg({a: 1, b: 2, c: 3}) 2 @*/ Ox.avg = function(obj) { return Ox.sum(obj) / Ox.len(obj); }; Ox.clone = function(obj) { /*** returns a copy of an array or object >>> (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' ***/ return Ox.isArray(obj) ? obj.slice() : Ox.extend({}, obj); }; Ox.compact = function(arr) { /*** returns an array without null or undefined values >>> Ox.compact([null,,1,,2,,3]) [1, 2, 3] ***/ return Ox.map(arr, function(val) { return Ox.isUndefined(val) ? null : val; }); }; Ox.count = function(arr) { /*** >>> Ox.count(['foo', 'bar', 'foo']).foo 2 ***/ var obj = {}; arr.forEach(function(v) { obj[v] = (obj[v] || 0) + 1; }); return obj; }; Ox.each = function(obj, fn) { // fixme: deprecate! /* Ox.each() works for arrays, objects and strings, like $.each(), unlike [].forEach() >>> Ox.each([0, 1, 2], function(i, v) {}) [0, 1, 2] >>> Ox.each({a: 1, b: 2, c: 3}, function(k, v) {}).a 1 >>> Ox.each('foo', function(i, v) {}) 'foo' */ var i, isArray = Ox.isArray(obj); for (i in obj) { i = isArray ? parseInt(i) : i; // fixme: should be (v, k), like [].forEach() if (fn(i, obj[i]) === false) { break; } } return obj; }; Ox.every = function(obj, fn) { /* Ox.every() works for arrays, objects and strings, unlike [].every() >>> 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 */ return Ox.filter(Ox.values(obj), fn || function(v) { return v; }).length == Ox.len(obj); }; Ox.extend = function() { /* >>> Ox.extend({a: 1, b: 1, c: 1}, {b: 2, c: 2}, {c: 3}).c 3 */ 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.filter = function(obj, fn) { /*** Ox.filter works for arrays, objects and strings, unlike [].filter() >>> 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(' hello world ', function(v) { return v != ' '; }) 'helloworld' ***/ var type = Ox.typeOf(obj), ret = type == 'array' ? [] : type == 'object' ? {} : ''; Ox.forEach(obj, 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 = function(arr, str) { /* returns an array with two arrays as elements: an array of elements of arr that begin with str, and an array of elements of arr that contain, but do not begin with str >>> Ox.find(["foo", "bar", "foobar", "barfoo"], "foo") [["foo", "foobar"], ["barfoo"]] */ var arrLowerCase = arr.map(function(v) { return v.toLowerCase(); }), ret = [[], []]; str && arrLowerCase.forEach(function(v, i) { var index = v.indexOf(str.toLowerCase()); index > -1 && ret[index == 0 ? 0 : 1].push(arr[i]); }); return ret; }; Ox.flatten = function(arr) { /* >>> Ox.flatten([1, [2, [3], 4], 5]) [1, 2, 3, 4, 5] */ var ret = []; arr.forEach(function(v) { if (Ox.isArray(v)) { Ox.flatten(v).forEach(function(v) { ret.push(v); }); } else { ret.push(v); } }); 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 An array, object or string callback Callback function value <*> Value key Key @*/ Ox.forEach = function(obj, fn) { /* Ox.forEach() works for arrays, objects and strings, like $.each(), unlike [].forEach() The arguments of the iterator function are (value, key), like [].forEach(), unlike $.each() >>> Ox.forEach('foo', function(v, i) {}) 'foo' >>> Ox.forEach([0, 1, 2], function(v, i) {}) [0, 1, 2] >>> Ox.forEach({a: 1, b: 2, c: 3}, function(v, k) {}).a 1 */ var isObject = Ox.isObject(obj), key; for (key in obj) { key = isObject ? key : parseInt(key); if (/*hasOwnProperty.call(obj, key) && */fn(obj[key], key) === false) { break; } } return obj; }; Ox.getObjectById = function(arr, id) { /*** >>> Ox.getObjectById([{id: "foo", title: "Foo"}, {id: "bar", title: "Bar"}], "foo").title "Foo" ***/ var ret = null; Ox.forEach(arr, function(v) { if (v.id == id) { ret = v; return false; } }); return ret; }; Ox.getPositionById = function(arr, id) { /*** >>> Ox.getPositionById([{id: "foo", title: "Foo"}, {id: "bar", title: "Bar"}], "bar") 1 ***/ var ret = -1; Ox.forEach(arr, function(v, i) { if (v.id == id) { ret = i; return false; } }); return ret; }; Ox.isEmpty = function(val) { return Ox.len(val) == 0; }; Ox.isEqual = function(obj0, obj1) { /* >>> Ox.isEqual(false, false) true >>> Ox.isEqual(0, 0) true >>> Ox.isEqual(NaN, NaN) false >>> Ox.isEqual('', '') true >>> Ox.isEqual([1, 2, 3], [1, 2, 3]) true >>> Ox.isEqual({a: 1, b: [2, 3], c: {d: '4'}}, {a: 1, b: [2, 3], c: {d: '4'}}) true >>> Ox.isEqual(function(arg) { return arg; }, function(arg) { return arg; }); true */ var ret = false; if (obj0 === obj1) { ret = true; } else if (typeof(obj0) == typeof(obj1)) { if (obj0 == obj1) { ret = true; } else if (Ox.isArray(obj0) && obj0.length == obj1.length) { ret = true; Ox.forEach(obj0, function(v, i) { ret = Ox.isEqual(v, obj1[i]); return ret; }); } else if (Ox.isDate(obj0)) { ret = obj0.getTime() == obj1.getTime(); } else if (Ox.isObject(obj0)) { ret = Ox.isEqual(Ox.keys(obj0), Ox.keys(obj1)) && Ox.isEqual(Ox.values(obj0), Ox.values(obj1)); } else if (Ox.isFunction(obj0)) { ret = obj0.toString() == obj1.toString(); } } return ret; }; Ox.keys = function(obj) { /* works for arrays, objects and strings, unlike Object.keys() >>> Ox.keys([1, 2, 3]) [0, 1, 2] >>> Ox.keys({a: 1, b: 2, c: 3}) ["a", "b", "c"] >>> Ox.keys('abc') [0, 1, 2] >>> Ox.keys([,]) [0] */ var keys = []; Ox.forEach(obj, function(v, k) { keys.push(k); }); return keys.sort(); }; Ox.last = function(arr, val) { /*** >>> Ox.last([1, 2, 3]) 3 >>> Ox.last([1, 2, 3], 4) [1, 2, 4] ***/ var ret; if (arguments.length == 1) { ret = arr[arr.length - 1]; } else { arr[arr.length - 1] = val; ret = arr; } return ret; }; Ox.len = function(obj) { /* >>> Ox.len([1, 2, 3]) 3 >>> Ox.len({a: 1, b: 2, c: 3}) 3 >>> Ox.len('abc') 3 >>> Ox.len(function(x, y) { return x + y; }) 2 >>> Ox.len([,]) 1 */ return Ox.isObject(obj) ? Ox.values(obj).length : obj.length; }; Ox.loop = function() { var length = arguments.length, fn = arguments[length - 1], step = length == 4 ? arguments[2] : 1, stop = arguments[length > 2 ? 1 : 0], start = length > 2 ? arguments[0] : 0, i; for (i = start; i < stop; i += step) { fn(i); } }; Ox.makeArray = function(obj) { /* >>> (function() { return Ox.makeArray(arguments); }('foo', 'bar')) ['foo', 'bar'] */ return Array.prototype.slice.call(obj); }; Ox.makeObject = function(obj) { /* >>> (function() { return Ox.makeObject(arguments); }({foo: 'bar'})).foo 'bar' >>> (function() { return Ox.makeObject(arguments); }('foo', 'bar')).foo 'bar' >>> (function() { return Ox.makeObject(arguments); }('foo')).foo undefined >>> (function() { return Ox.makeObject(arguments); }()).foo undefined */ 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 = function(obj, fn) { /* Ox.map() works for arrays, objects and strings, unlike [].map() >>> Ox.map([1, 1, 1], function(v, i) { return v == i; }) [false, true, false] >>> Ox.map({a: 'a', b: 'a', c: 'a'}, function(v, k) { return v == k; }).a true >>> Ox.map("111", function(v, i) { return v == i; }) [false, true, false] >>> Ox.map([,], function(v, i) { return i; }) [0] */ // fixme: return null to filter out is a bit esoteric var isObject = Ox.isObject(obj), ret = isObject ? {} : []; Ox.forEach(obj, function(val, key) { if ((v = fn(val, key)) !== null) { ret[isObject ? key : ret.length] = v; } }); return ret; }; Ox.max = function(obj) { /* >>> Ox.max([-1, 0, 1]) 1 >>> Ox.max({a: 1, b: 2, c: 3}) 3 */ return Math.max.apply(Math, Ox.values(obj)); }; Ox.min = function(obj) { /* >>> Ox.min([-1, 0, 1]) -1 >>> Ox.min({a: 1, b: 2, c: 3}) 1 */ return Math.min.apply(Math, Ox.values(obj)); }; Ox.merge = function(arr) { /* >>> Ox.merge(['foo'], ['bar'], ['baz']) ['foo', 'bar', 'baz'] */ Ox.forEach(Array.prototype.slice.call(arguments, 1), function(arg) { Ox.forEach(arg, function(val) { arr.push(val); }); }); return arr; }; Ox.range = function(start, stop, step) { /* >>> Ox.range(3) [0, 1, 2] >>> Ox.range(3, 0) [3, 2, 1] >>> Ox.range(1, 2, 0.5) [1, 1.5] */ stop = arguments.length > 1 ? stop : arguments[0]; start = arguments.length > 1 ? start : 0; step = step || (start <= stop ? 1 : -1); var arr = [], i; for (i = start; step > 0 ? i < stop : i > stop; i += step) { arr.push(i); } return arr; }; Ox.serialize = function(obj) { /* >>> Ox.serialize({a: 1, b: 2, c: 3}) 'a=1&b=2&c=3' */ var arr = []; Ox.forEach(obj, function(val, key) { val !== '' && arr.push(key + '=' + val); }); return arr.join('&'); }; Ox.setPropertyOnce = function(arr, str) { /* >>> Ox.setPropertyOnce([{selected: false}, {selected: false}], 'selected') 0 >>> Ox.setPropertyOnce([{selected: false}, {selected: true}], 'selected') 1 >>> Ox.setPropertyOnce([{selected: true}, {selected: true}], 'selected') 0 */ 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 = function(arr) { /* >>> Ox.shuffle([1, 2, 3]).length 3 */ var shuffle = arr; return shuffle.sort(function() { return Math.random() - 0.5; }); }; Ox.some = function(obj, fn) { /* Ox.some() works for arrays, objects and strings, unlike [].some() >>> 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 */ return Ox.filter(Ox.values(obj), fn).length > 0; }; Ox.sort = function(arr) { /* >>> Ox.sort(['10', '9', 'B', 'a']) ['9', '10', 'a', 'B'] */ var len, matches = {}, sort = {}; // find leading numbers arr.forEach(function(val, i) { var match = /^\d+/(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 arr.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) { var ret = 0; if (sort[a] < sort[b]) { ret = -1; } else if (sort[a] > sort[b]) { ret = 1; } return ret; }); }; Ox.sum = function(obj) { /* >>> Ox.sum([-1, 0, 1]) 0 >>> Ox.sum({a: 1, b: 2, c: 3}) 6 */ var sum = 0; Ox.forEach(obj, function(val) { sum += val; }); 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.unique = function(arr) { /* >>> Ox.unique([1, 2, 3, 1]) [1, 2, 3] */ var unique = []; Ox.forEach(arr, function(val) { unique.indexOf(val) == -1 && unique.push(val); }); return unique; }; Ox.unserialize = function(str) { /* >>> Ox.unserialize('a=1&b=2&c=3').c '3' */ var arr, obj = {}; Ox.forEach(str.split('&'), function(val) { arr = val.split('='); obj[arr[0]] = arr[1]; }); return obj; }; Ox.values = function(obj) { /* >>> 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,]) [1] */ // fixme: why doesn't this use map? var values = []; Ox.forEach(obj, function(val) { values.push(val); }); return values; }; Ox.walk = function(obj, fn) { /* >>> a = 0; Ox.walk({a: 1, b: {c: 2, d: 3}}, function(v, k) { a += Ox.isNumber(v) ? v : 0}); a 6 */ Ox.forEach(obj, function(val, key) { fn(val, key, obj); Ox.walk(obj[key], fn); }); }; Ox.zip = function() { /* >>> 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]] */ var args = arguments.length == 1 ? arguments[0] : Ox.makeArray(arguments), arr = []; args[0].forEach(function(v, i) { arr[i] = []; args.forEach(function(v_, i_) { arr[i].push(v_[i]); }); }); return arr; }; /* ================================================================================ Color functions ================================================================================ */ /*@ Ox.hsl Takes RGB values and returns HSL values (rgb) <[n]> HSL values rgb <[n]> RGB values > 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) { rgb = rgb.map(function(v) { return v / 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 hsl <[n]> HSL values > 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) { 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; }); }; /* ================================================================================ Date functions ================================================================================ */ /*@ 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" @*/ 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]; Ox.print(date, Ox.getDate(date, utc), sourceWeekday, targetWeekday) return Ox.setDate(date, Ox.getDate(date, utc) - sourceWeekday + targetWeekday, utc); } /*@ 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 = function(year, month, utc) { /* >>> Ox.getDaysInMonth(2000, 2) 29 >>> Ox.getDaysInMonth("2002", "Feb") 28 >>> Ox.getDaysInMonth(new Date('01/01/2004'), "February") 29 */ 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 = function(year, utc) { /* >>> Ox.getDaysInYear(1900) 365 >>> Ox.getDaysInYear('2000') 366 >>> Ox.getDaysInYear(new Date('01/01/2004')) 366 */ return 365 + Ox.isLeapYear(Ox.makeYear(year, utc)); }; Ox.getFirstDayOfTheYear = function(date, utc) { /* Decimal weekday of January 1 (0-6, Sunday as first day) >>> Ox.getFirstDayOfTheYear(new Date("01/01/2000")) 6 */ date = Ox.makeDate(date); date = Ox.setMonth(date, 0, utc); date = Ox.setDate(date, 1, utc); return Ox.getDay(date, utc) }; Ox.getISODate = function(date, utc) { /* >>> Ox.getISODate(new Date("01/01/2000")) "2000-01-01T00:00:00Z" */ return Ox.formatDate(Ox.makeDate(date), '%FT%TZ', utc); }; Ox.getISODay = function(date, utc) { /* 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 */ return Ox.getDay(Ox.makeDate(date), utc) || 7; }; Ox.getISOWeek = function(date, utc) { /* see http://en.wikipedia.org/wiki/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 */ 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 = function(date, utc) { /* see http://en.wikipedia.org/wiki/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 */ 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.getTime = function() { // fixme: needed? return +new Date(); } Ox.getTimezoneOffset = function() { return new Date().getTimezoneOffset() * 60000; } Ox.getTimezoneOffsetString = function(date) { /* Time zone offset string ('-1200' - '+1200') >>> Ox.getTimezoneOffsetString(new Date('01/01/2000')).length 5 */ 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 = function(date, utc) { /* 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 */ date = Ox.makeDate(date); return Math.floor((Ox.getDayOfTheYear(date, utc) + Ox.getFirstDayOfTheYear(date, utc) - 1) / 7); }; Ox.isLeapYear = function(year, utc) { /* >>> Ox.isLeapYear(1900) false >>> Ox.isLeapYear('2000') true >>> Ox.isLeapYear(new Date('01/01/2004')) true */ year = Ox.makeYear(year, utc); return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); }; Ox.makeDate = function(date) { return Ox.isDate(date) ? date : Ox.isUndefined(date) ? new Date() : new Date(date); }; Ox.makeYear = function(date, utc) { return Ox.isDate(date) ? Ox.getFullYear(date, utc) : parseInt(date); }; [ 'FullYear', 'Month', 'Date', 'Day', 'Hours', 'Minutes', 'Seconds', 'Milliseconds' ].forEach(function(noun) { Ox['get' + noun] = function(date, utc) { return Ox.makeDate(date)['get' + (utc ? 'UTC' : '') + noun]() } Ox['set' + noun] = function(date, num, utc) { // new Date(date) makes a clone, so that // setSomething() doesn't have side effects return new Date( Ox.makeDate(date) )['set' + (utc ? 'UTC' : '') + noun](num); // fixme: maybe we _want_ set to have side effects? } }); /* ================================================================================ DOM functions ================================================================================ */ /*@ 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.canvas(img) or Ox.canvas(width, height) 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') { //Ox.print('document has become ready', callbacks); 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").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)) : 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(' ') ) : 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; }, /*@ 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; }, /*@ 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 @*/ mousedown: function(callback) { this[0].onmousedown = callback; 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 functions ================================================================================ */ (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 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 = function(num) { // see http://www.crockford.com/wrmg/base32.html /* >>> Ox.encodeBase32(15360) 'F00' >>> Ox.encodeBase32(33819) '110V' */ return Ox.map(num.toString(32), function(char) { return digits[parseInt(char, 32)]; }).join(''); } Ox.decodeBase32 = function(str) { /* >>> Ox.decodeBase32('foo') 15360 >>> Ox.decodeBase32('ilou') 33819 >>> Ox.decodeBase32('?').toString() 'NaN' */ 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 = function(num) { /* >>> Ox.encodeBase64(32394) 'foo' */ return btoa(Ox.encodeBase256(num)).replace(/=/g, ''); } Ox.decodeBase64 = function(str) { /* >>> Ox.decodeBase64('foo') 32394 */ return Ox.decodeBase256(atob(str)); } Ox.encodeBase128 = function(num) { /* >>> Ox.encodeBase128(1685487) 'foo' */ var str = ''; while (num) { str = Ox.char(num & 127) + str; num >>= 7; } return str; } Ox.decodeBase128 = function(str) { /* >>> Ox.decodeBase128('foo') 1685487 */ var num = 0, len = str.length; Ox.forEach(str, function(char, i) { num += char.charCodeAt(0) << (len - i - 1) * 7; }); return num; } Ox.encodeBase256 = function(num) { /* >>> Ox.encodeBase256(6713199) 'foo' */ var str = ''; while (num) { str = Ox.char(num & 255) + str; num >>= 8; } return str; } Ox.decodeBase256 = function(str) { /* >>> Ox.decodeBase256('foo') 6713199 */ var num = 0, len = str.length; Ox.forEach(str, function(char, i) { num += char.charCodeAt(0) << (len - i - 1) * 8; }); return num; } Ox.encodeDeflate = function(str) { // encodes string, using deflate /* in fact, the string is written to the rgb channels of a canvas element, then the dataURL is decoded from base64, and some head and tail cut off */ str = Ox.encodeUTF8(str); var len = str.length, c = Ox.canvas(Math.ceil((4 + len) / 3), 1), data; str = Ox.pad(Ox.encodeBase256(len), 4, Ox.char(0)) + str + Ox.repeat('\u00FF', (4 - len % 4) % 4); // simpler? Ox.pad()? /* fixme: why does map not work here? c.data = $.map(c.data, function(v, i) { return i % 4 < 3 ? str.charCodeAt(i - parseInt(i / 4)) : 255; }); */ for (i = 0; i < c.data.length; i += 1) { c.data[i] = i % 4 < 3 ? str.charCodeAt(i - parseInt(i / 4)) : 255; } c.context.putImageData(c.imageData, 0, 0); Ox.print(c.canvas.toDataURL()) data = atob(c.canvas.toDataURL().split(',')[1]); Ox.print('data', data); return data.substr(8, data.length - 20); } Ox.decodeDeflate = function(str) { var image = new Image(); image.src = 'data:image/png;base64,' + btoa('\u0089PNG\r\n\u001A\n' + str + Ox.repeat('\u0000', 4) + 'IEND\u00AEB`\u0082'); Ox.print(image.src); while (!image.width) {} // block until image data is available str = Ox.map(Ox.canvas(image).data, function(v, i) { return i % 4 < 3 ? Ox.char(v) : ''; }).join(''); return Ox.decodeUTF8(str.substr(4, Ox.decodeBase256(str.substr(0, 4)))); } Ox.encodeHTML = function(str) { /* >>> Ox.encodeHTML('\'<"&">\'') ''<"&">'' >>> Ox.encodeHTML('äbçdê') 'äbçdê' */ return Ox.map(str, 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 = function(str) { /* >>> Ox.decodeHTML(''<"&">'') '\'<"&">\'' >>> Ox.decodeHTML(''<"&">'') '\'<"&">\'' >>> Ox.decodeHTML('äbçdê') 'äbçdê' >>> Ox.decodeHTML('äbçdê') 'äbçdê' */ // 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 = function(img, str) { // encodes string into image, returns new image url /* the message is compressed with deflate (by proxy of canvas), then the string (four bytes length) + (length bytes message) is encoded bitwise into the r/g/b bytes of all opaque pixels by flipping, if necessary, the least significant bit, so that (number of "1"-bits of the byte) % 2 is the bit of the string wishlist: - only use deflate if it actually shortens the message - in deflate, strip and later re-insert the chunk types - 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 */ //str = Ox.encodeDeflate(str); currently broken str = Ox.encodeUTF8(str); var c = Ox.canvas(img), len = str.length, px = 0; if (len == 0 || len > cap(img.width, img.height)) { throwPNGError('en') } len = Ox.pad(Ox.encodeBase256(len), 4, Ox.char(0)); Ox.forEach(Ox.map(len + str, function(byte) { return Ox.map(Ox.range(8), function(i) { return byte.charCodeAt(0) >> 7 - i & 1; }).join(''); }).join(''), function(bit, i) { var index = parseInt((px = seek(c.data, px)) * 4 + i % 3), byte = c.data[index]; c.data[index] = bit == xor(byte) ? byte : byte & 254 | !(byte & 1); px += i % 3 == 2; }); c.context.putImageData(c.imageData, 0, 0); return c.canvas.toDataURL(); } Ox.decodePNG = function(img) { // decodes image, returns string var bits = '', data = Ox.canvas(img).data, flag = false, i = 0, len = 4, max = cap(img.width, img.height), px = 0, str = ''; do { bits += xor(data[parseInt((px = seek(data, px)) * 4 + i % 3)]); px += i % 3 == 2; if (++i % 8 == 0) { str += Ox.char(parseInt(bits, 2)); bits = ''; len--; if (len == 0 && !flag) { len = Ox.decodeBase256(str); if (len <= 0 || len > max) { Ox.print(len); throwPNGError('de'); } str = ''; flag = true; } } } while (len); try { //return Ox.decodeDeflate(str); currently broken return Ox.decodeUTF8(str); } catch(e) { Ox.print(e.toString()); throwPNGError('de'); } } Ox.encodeUTF8 = function(str) { /* see http://en.wikipedia.org/wiki/UTF-8 >>> Ox.encodeUTF8('foo') 'foo' >>> Ox.encodeUTF8('¥€$') '\u00C2\u00A5\u00E2\u0082\u00AC\u0024' */ 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 = function(str) { /* >>> Ox.decodeUTF8('foo') 'foo' >>> Ox.decodeUTF8('\u00C2\u00A5\u00E2\u0082\u00AC\u0024') '¥€$' */ 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 functions ================================================================================ */ Ox.formatArea = function(num, dec) { return Ox.formatNumber(Ox.round(num / 1000000, dec)) + ' km\u00B2'; } Ox.formatColor = function() { }; Ox.formatCurrency = function(num, str, dec) { /* >>> Ox.formatCurrency(1000, '$', 2) '$1,000.00' */ return str + Ox.formatNumber(num, dec); }; Ox.formatDate = function(date, str, utc) { // fixme: date and utc are optional, date can be date, number or string /* See http://developer.apple.com/documentation/Darwin/Reference/ManPages/man3/strftime.3.html and http://en.wikipedia.org/wiki/ISO_8601 >>> _date = new Date("2005-01-02 00:03:04") "Sun Jan 02 2005 00:03:04 GMT+0100 (CET)" >>> Ox.formatDate(_date, "%A") // Full weekday "Sunday" >>> Ox.formatDate(_date, "%a") // Abbreviated weekday "Sun" >>> Ox.formatDate(_date, "%B") // Full month "January" >>> Ox.formatDate(_date, "%b") // Abbreviated month "Jan" >>> Ox.formatDate(_date, "%C") // Century "20" >>> Ox.formatDate(_date, "%c") // US time and date "01/02/05 12:03:04 AM" >>> Ox.formatDate(_date, "%D") // US date "01/02/05" >>> Ox.formatDate(_date, "%d") // Zero-padded day of the month "02" >>> Ox.formatDate(_date, "%e") // Space-padded day of the month " 2" >>> Ox.formatDate(_date, "%F") // Date "2005-01-02" >>> Ox.formatDate(_date, "%G") // Full ISO-8601 year "2004" >>> Ox.formatDate(_date, "%g") // Abbreviated ISO-8601 year "04" >>> Ox.formatDate(_date, "%H") // Zero-padded hour (24-hour clock) "00" >>> Ox.formatDate(_date, "%h") // Abbreviated month "Jan" >>> Ox.formatDate(_date, "%I") // Zero-padded hour (12-hour clock) "12" >>> Ox.formatDate(_date, "%j") // Zero-padded day of the year "002" >>> Ox.formatDate(_date, "%k") // Space-padded hour (24-hour clock) " 0" >>> Ox.formatDate(_date, "%l") // Space-padded hour (12-hour clock) "12" >>> Ox.formatDate(_date, "%M") // Zero-padded minute "03" >>> Ox.formatDate(_date, "%m") // Zero-padded month "01" >>> Ox.formatDate(_date, "%n") // Newline "\n" >>> Ox.formatDate(_date, "%p") // AM or PM "AM" >>> Ox.formatDate(_date, "%Q") // Quarter of the year "1" >>> Ox.formatDate(_date, "%R") // Zero-padded hour and minute "00:03" >>> Ox.formatDate(_date, "%r") // US time "12:03:04 AM" >>> Ox.formatDate(_date, "%S") // Zero-padded second "04" >/> Ox.formatDate(_date, "%s") // Number of seconds since the Epoch "1104620584" >>> Ox.formatDate(_date, "%T") // Time "00:03:04" >>> Ox.formatDate(_date, "%t") // Tab "\t" >>> Ox.formatDate(_date, "%U") // Zero-padded week of the year (00-53, Sunday as first day) "01" >>> Ox.formatDate(_date, "%u") // Decimal weekday (1-7, Monday as first day) "7" >>> Ox.formatDate(_date, "%V") // Zero-padded ISO-8601 week of the year "53" >>> Ox.formatDate(_date, "%v") // Formatted date " 2-Jan-2005" >>> Ox.formatDate(_date, "%W") // Zero-padded week of the year (00-53, Monday as first day) "00" >>> Ox.formatDate(_date, "%w") // Decimal weekday (0-6, Sunday as first day) "0" >>> Ox.formatDate(_date, "%X") // US time "12:03:04 AM" >>> Ox.formatDate(_date, "%x") // US date "01/02/05" >>> Ox.formatDate(_date, "%Y") // Full year "2005" >>> Ox.formatDate(_date, "%y") // Abbreviated year "05" >/> Ox.formatDate(_date, "%Z") // Time zone name "CET" >/> Ox.formatDate(_date, "%z") // Time zone offset "+0100" >/> Ox.formatDate(_date, "%+") // Formatted date and time "Sun Jan 2 00:03:04 CET 2005" >>> Ox.formatDate(_date, "%%") "%" >>> delete _date true >>> Ox.formatDate(new Date("01/01/2000"), "%W") "00" >>> Ox.formatDate(new Date("01/02/2000"), "%W") "00" >>> Ox.formatDate(new Date("01/03/2000"), "%W") "01" */ date = Ox.makeDate(date); var format = [ ['%', function() {return '%{%}';}], ['c', function() {return '%x %X';}], ['X', function() {return '%r';}], ['x', function() {return '%D';}], ['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);}], ['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.formatDuration = function(sec, dec, format) { /* >>> 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" */ 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 = function(num, dec) { /* >>> Ox.formatNumber(123456789, 3) "123,456,789.000" >>> Ox.formatNumber(-2000000 / 3, 3) "-666,666.667" >>> Ox.formatNumber(666666.666) "666,667" */ var str = Math.abs(num).toFixed(dec || 0), spl = str.split('.'), arr = []; 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 = function(num) { /* >>> 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" */ 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 = function(num, total, dec) { /* >>> Ox.formatPercent(1, 1000, 2) "0.10%" */ return Ox.formatNumber(num / total * 100, dec) + '%' }; Ox.formatResolution = function(arr, str) { /* >>> Ox.formatResolution([1920, 1080], 'px') "1920 x 1080 px" */ return arr[0] + ' x ' + arr[1] + (str ? ' ' + str : ''); } Ox.formatString = function (str, obj) { /* >>> Ox.formatString('{0}{1}', ['foo', 'bar']) "foobar" >>> Ox.formatString('{a}{b}', {a: 'foo', b: 'bar'}) "foobar" */ return str.replace(/\{([^}]+)\}/g, function(str, match) { return obj[match]; }); } Ox.formatValue = function(num, str, bin) { /* >>> Ox.formatValue(0, "B") "0 KB" >>> Ox.formatValue(123456789, "B") "123.5 MB" >>> Ox.formatValue(1234567890, "B", true) "1.15 GiB" */ 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 = function(num, str) { return num + ' ' + str; }; /* ================================================================================ Geo functions ================================================================================ */ (function() { function rad(point) { return { lat: Ox.rad(point.lat), lng: Ox.rad(point.lng) }; } Ox.crossesDateline = function(point0, point1) { return point0.lng > point1.lng; } 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 = function(point0, point1) { /* >>> Ox.getBearing({lat: -45, lng: 0}, {lat: 45, lng: 0}) 0 */ 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 = function(point0, point1) { /* >>> Ox.values(Ox.getCenter({lat: -45, lng: -90}, {lat: 45, lng: 90})) [0, 0] */ 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 = function(lat) { /*** return degrees per meter at a given latitude >>> Ox.EARTH_CIRCUMFERENCE == 360 / Ox.getDegreesPerMeter(0) true ***/ return 360 / Ox.EARTH_CIRCUMFERENCE / Math.cos(lat * Math.PI / 180); }; Ox.getDistance = function(point0, point1) { /* >>> Ox.EARTH_CIRCUMFERENCE == Ox.getDistance({lat: -45, lng: -90}, {lat: 45, lng: 90}) * 2 true */ 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 = function(xy) { /* >>> Ox.values(Ox.getLatLngByXY({x: 0.5, y: 0.5})) [0, 0] */ 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 = function(lat) { /*** returns meters per degree at a given latitude >>> Ox.EARTH_CIRCUMFERENCE == Ox.getMetersPerDegree(0) * 360 true ***/ return Math.cos(lat * Math.PI / 180) * Ox.EARTH_CIRCUMFERENCE / 360; }; Ox.getXYByLatLng = function(latlng) { /* >>> Ox.values(Ox.getXYByLatLng({lat: 0, lng: 0})) [0.5, 0.5] */ 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 = 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 = 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 = 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 functions ================================================================================ */ Ox.parseEmailAddresses = function(html) { return html.replace( /\b([0-9A-Z\.\+\-_]+@(?:[0-9A-Z\-]+\.)+[A-Z]{2,6})\b/gi, '$1' ); }; Ox.parseHTML = (function() { /* >>> 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</a>' >>> Ox.parseHTML('[http://foo.com foo]') 'foo' >>> Ox.parseHTML('foo') '
foo
' >>> Ox.parseHTML('') '<script>alert()</script>' */ var defaultTags = [ '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); 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 // return $('
').html(html).html(); // fixme: this converts '"' to '"' return Ox.element('
').html(html).html(); } }()); 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 = 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 } ); } ); }; /* ================================================================================ Math functions ================================================================================ */ Ox.asinh = function(x) { /* fixme: no test */ return Math.log(x + Math.sqrt(x * x + 1)); }; Ox.deg = function(rad) { /* >>> Ox.deg(2 * Math.PI) 360 */ return rad * 180 / Math.PI; }; Ox.divideInt = function(num, by) { /* >>> Ox.divideInt(100, 3) [33, 33, 34] >>> Ox.divideInt(100, 6) [16, 16, 17, 17, 17, 17] */ var arr = [], div = parseInt(num / by), mod = num % by, i; for (i = 0; i < by; i++) { arr[i] = div + (i > by - 1 - mod); } return arr; } Ox.limit = function(num, min, max) { /* >>> Ox.limit(1, 2, 3) 2 >>> Ox.limit(2, 1) 1 */ var len = arguments.length; max = arguments[len - 1]; min = len == 3 ? min : 0; return Math.min(Math.max(num, min), max); }; Ox.log = function(num, base) { /* >>> Ox.log(100, 10) 2 >>> Ox.log(Math.E) 1 */ return Math.log(num) / Math.log(base || Math.E); }; Ox.mod = function(num, by) { /* >>> Ox.mod(11, 10) 1 >>> Ox.mod(-11, 10) 9 */ var mod = num % by; return mod >= 0 ? mod : mod + by; }; Ox.rad = function(deg) { /* >>> Ox.rad(360) 2 * Math.PI */ return deg * Math.PI / 180; }; Ox.random = function() { /* >>> Ox.random(3) in {0: 0, 1: 0, 2: 0, 3: 0} true >>> Ox.random(1, 2) in {1: 0, 2: 0} true */ var len = arguments.length, min = len == 1 ? 0 : arguments[0], max = arguments[len - 1]; return min + parseInt(Math.random() * (max - min + 1)); }; Ox.round = function(num, dec) { /* >>> Ox.round(2 / 3, 6) 0.666667 >>> Ox.round(1 / 2, 3) 0.5 */ var pow = Math.pow(10, dec || 0); return Math.round(num * pow) / pow; }; Ox.sinh = function(x) { /* fixme: no test */ return (Math.exp(x) - Math.exp(-x)) / 2; }; Ox.MAX_LATITUDE = Ox.deg(Math.atan(Ox.sinh(Math.PI))); Ox.MIN_LATITUDE = -Ox.MAX_LATITUDE; /* ================================================================================ RegExp functions ================================================================================ */ Ox.regexp = { 'accents': '¨´`ˆ˜', 'letters': 'a-z¨´`ˆ˜äåáàâãæçëéèèñïíìîöóòôõøœßúùûÿ' }; /* ================================================================================ String functions ================================================================================ */ Ox.basename = function(str) { /* fixme: this should go into Path functions >>> Ox.basename("foo/bar/foo.bar") "foo.bar" >>> Ox.basename("foo.bar") "foo.bar" */ return str.replace(/^.*[\/\\]/g, ''); }; Ox.char = String.fromCharCode; Ox.clean = function(str) { /* >>> Ox.clean("foo bar") "foo bar" >>> Ox.clean(" foo bar ") "foo bar" >>> Ox.clean(" foo \n bar ") "foo\nbar" */ return Ox.map(str.split('\n'), function(str) { return Ox.trim(str.replace(/\s+/g, ' ')); }).join('\n'); }; Ox.contains = function(str, chr) { /* >>> Ox.contains("foo", "bar") false >>> Ox.contains("foobar", "bar") true >>> Ox.contains(['foo', 'bar'], 'foo') true // fixme: rename to Ox.has or Ox.isIn? // then it'd become convenient for arrays */ return str.indexOf(chr) > -1; }; Ox.endsWith = function(str, sub) { /* >>> Ox.endsWith("foobar", "bar") true */ return new RegExp(sub + '$').test(str); }; Ox.highlight = function(txt, str) { // fixme: move to ox.ui return str ? txt.replace( new RegExp('(' + str + ')', 'ig'), '$1' ) : txt; }; Ox.isValidEmail = function(str) { /* >>> 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 */ return !!/^[0-9A-Z\.\+\-_]+@(?:[0-9A-Z\-]+\.)+[A-Z]{2,6}$/i(str); } Ox.pad = function(str, len, pad, pos) { // fixme: slighly obscure signature // fixme: weird for negative numbers /* >>> Ox.pad(1, 2) "01" >>> Ox.pad("abc", 6, ".", "right") "abc..." >>> Ox.pad("foobar", 3, ".", "right") "foo" >>> Ox.pad("abc", 6, "123456", "right") "abc123" >>> Ox.pad("abc", 6, "123456", "left") "456abc" */ str = str.toString().substr(0, len); pad = Ox.repeat(pad || '0', len - str.length); pos = pos || 'left'; str = pos == 'left' ? pad + str : str + pad; str = pos == 'left' ? str.substr(str.length - len, str.length) : str.substr(0, len); return str; }; Ox.repeat = function(obj, num) { /* 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] */ var ret; if (Ox.isArray(obj)) { ret = num >= 1 ? Ox.map(Ox.range(obj.length * num), function(v, i) { return obj[i % obj.length] }) : []; } else { ret = num >= 1 ? new Array(num + 1).join(obj.toString()) : ''; } return ret; }; Ox.reverse = function(str) { /* Ox.reverse("foo") oof */ return str.toString().split('').reverse().join(''); }; Ox.startsWith = function(str, sub) { /* >>> Ox.startsWith("foobar", "foo") true // fixme: // !!(/^sub/(str)) is shorter than // Ox.startsWith(str, sub) anyway // new RegExp('^' + sub).test(str) is longer though... */ return new RegExp('^' + sub).test(str); }; Ox.stripTags = function(str) { /* >>> Ox.stripTags("foo") "foo" */ return str.replace(/(<.*?>)/gi, ''); }; Ox.substr = function(str, start, stop) { /*** Ox.substr behaves like str[start:stop] in Python (or like str.substring() with negative values for stop) // fixme: needed? >>> Ox.substr('foobar', 1) "oobar" >>> Ox.substr('foobar', -1) "r" >>> Ox.substr('foobar', 1, 3) "oo" >>> Ox.substr('foobar', -3, 5) "ba" >>> Ox.substr('foobar', 1, -2) "oob" >>> Ox.substr('foobar', -4, -1) "oba" ***/ stop = Ox.isUndefined(stop) ? str.length : stop; return str.substring( start < 0 ? str.length + start : start, stop < 0 ? str.length + stop : stop ); }; Ox.toCamelCase = function(str) { /* >>> Ox.toCamelCase("foo-bar-baz") "fooBarBaz" >>> Ox.toCamelCase("foo/bar/baz") "fooBarBaz" >>> Ox.toCamelCase("foo_bar_baz") "fooBarBaz" */ return str.replace(/[\-_\/][a-z]/g, function(str) { return str[1].toUpperCase(); }); }; Ox.toDashes = function(str) { /* >>> Ox.toDashes("fooBarBaz") "foo-bar-baz" */ return str.replace(/[A-Z]/g, function(str) { return '-' + str.toLowerCase(); }); }; 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' ], 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' ] }; 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.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; }; }()); Ox.toSlashes = function(str) { /* >>> Ox.toSlashes("fooBarBaz") "foo/bar/baz" */ return str.replace(/[A-Z]/g, function(str) { return '/' + str.toLowerCase(); }); }; Ox.toTitleCase = function(str) { /* >>> Ox.toTitleCase("foo") "Foo" >>> Ox.toTitleCase("Apple releases iPhone, IBM stock plummets") "Apple Releases iPhone, IBM Stock Plummets" */ return Ox.map(str.split(' '), function(v) { var sub = v.substr(1), low = sub.toLowerCase(); if (sub == low) { v = v.substr(0, 1).toUpperCase() + low; } return v; }).join(" "); }; Ox.toUnderscores = function(str) { /* >>> Ox.toUnderscores("fooBarBaz") "foo_bar_baz" */ 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 = function(str, len, pad, pos) { /* >>> Ox.truncate("anticonstitutionellement", 16, "...", "center") "anticon...lement" */ 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 = function(str) { /* > Ox.words("The key/value pairs are read-only--aren't they?") ["the", "key", "value", "pairs", "are", "read-only", "aren't", "they"] */ var arr = str.toLowerCase().split(/\b/), chr = "-'", len = arr.length, startsWithWord = !!/\w/(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 = function(str, len, sep, bal, spa) { // fixme: bad API, sep/bal/spa should be in options object /* >>> Ox.wordwrap("Anticonstitutionellement, Paris s'eveille", 25, "
") "Anticonstitutionellement,
Paris s'eveille" >>> Ox.wordwrap("Anticonstitutionellement, Paris s'eveille", 16, "
") "Anticonstitution
ellement, Paris
s'eveille" >>> Ox.wordwrap("These are short words", 16, "
", true) "These are
short words" */ var str = str === null ? '' : str.toString(), len = len || 80, sep = sep || "
", 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 functions ================================================================================ */ Ox.isArguments = function(val) { /* >>> Ox.isArguments((function() { return arguments; }())) true */ return !!(val && val.toString() == '[object Arguments]'); } Ox.isArray = function(val) { // is in jQuery /* >>> Ox.isArray([]) true */ return val instanceof Array; } Ox.isBoolean = function(val) { /* >>> Ox.isBoolean(false) true */ return typeof val == 'boolean'; }; Ox.isDate = function(val) { /* >>> Ox.isDate(new Date()) true */ return val instanceof Date; }; Ox.isElement = function(val) { /* >>> Ox.isElement(document.createElement()) true */ return !!(val && val.nodeType == 1); }; Ox.isFunction = function(val) { // is in jQuery /* >>> Ox.isFunction(function() {}) true >>> Ox.isFunction(/ /) false */ return typeof val == 'function' && !Ox.isRegExp(val); }; Ox.isInfinity = function(val) { /* >>> Ox.isInfinity(Infinity) true >>> Ox.isInfinity(-Infinity) true >>> Ox.isInfinity(NaN) false */ return typeof val == 'number' && !isFinite(val) && !Ox.isNaN(val); }; Ox.isNaN = function(val) { /* >>> Ox.isNaN(NaN) true */ return val !== val; } Ox.isNull = function(val) { /* >>> Ox.isNull(null) true */ return val === null; }; Ox.isNumber = function(val) { /* >>> Ox.isNumber(0) true >>> Ox.isNumber(Infinity) false >>> Ox.isNumber(-Infinity) false >>> Ox.isNumber(NaN) false */ return typeof val == 'number' && isFinite(val); }; Ox.isObject = function(val) { /* >>> Ox.isObject({}) true >>> Ox.isObject([]) false >>> Ox.isObject(new Date()) false >>> Ox.isObject(null) false */ return typeof val == 'object' && !Ox.isArguments(val) && !Ox.isArray(val) && !Ox.isDate(val) && !Ox.isNull(val); }; Ox.isRegExp = function(val) { /* >>> Ox.isRegExp(/ /) true */ return val instanceof RegExp; }; Ox.isString = function(val) { /* >>> Ox.isString('') true */ return typeof val == 'string'; }; /*@ Ox.isUndefined Tests if a value is undefined (value) -> If true, the value is undefined value <*> any value > Ox.isUndefined() true @*/ Ox.isUndefined = function(val) { // fixme: void 0 is also nice return typeof val == 'undefined'; }; /*@ Ox.typeOf Returns the type of a value (value) -> 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; };