From 7e12eb43d7f7c9c669484bcfd90488547c1135e2 Mon Sep 17 00:00:00 2001 From: rolux Date: Wed, 27 Apr 2011 21:39:56 +0200 Subject: [PATCH] adding missing file --- source/Ox.js | 3044 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 3044 insertions(+) create mode 100644 source/Ox.js diff --git a/source/Ox.js b/source/Ox.js new file mode 100644 index 00000000..a0383369 --- /dev/null +++ b/source/Ox.js @@ -0,0 +1,3044 @@ +// vim: et:ts=4:sw=4:sts=4:ft=js +// todo: check http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/ + +Ox = function(val) { + return Ox.wrap(val); +}; + +/* +================================================================================ +Constants +================================================================================ +*/ + +Ox.AMPM = ['AM', 'PM']; +//Ox.DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; +Ox.DURATIONS = ['year', 'month', 'day', 'minute', 'second']; +Ox.EARTH_RADIUS = 6378137; +Ox.EARTH_CIRCUMFERENCE = Ox.EARTH_RADIUS * 2 * Math.PI; +Ox.HTML_ENTITIES = { + '"': '"', '&': '&', "'": ''', '<': '<', '>': '>' +}; +Ox.KEYS = { + SECTION: 0, BACKSPACE: 8, TAB: 9, CLEAR: 12, ENTER: 13, + SHIFT: 16, CONTROL: 17, OPTION: 18, PAUSE: 19, CAPSLOCK: 20, + ESCAPE: 27, SPACE: 32, PAGEUP: 33, PAGEDOWN: 34, END: 35, HOME: 36, + LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40, INSERT: 45, DELETE: 46, HELP: 47, + 0: 48, 1: 49, 2: 50, 3: 51, 4: 52, 5: 53, 6: 54, 7: 55, 8: 56, 9: 57, + A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71, H: 72, I: 73, J: 74, + K: 75, L: 76, M: 77, N: 78, O: 79, P: 80, Q: 81, R: 82, S: 83, T: 84, + U: 85, V: 86, W: 87, X: 88, Y: 89, Z: 90, + META_LEFT: 91, META_RIGHT: 92, SELECT: 93, + '0_NUMPAD': 96, '1_NUMPAD': 97, '2_NUMPAD': 98, '3_NUMPAD': 99, + '4_NUMPAD': 100, '5_NUMPAD': 101, '6_NUMPAD': 102, '7_NUMPAD': 103, + '8_NUMPAD': 104, '9_NUMPAD': 105, '*_NUMPAD': 106, '+_NUMPAD': 107, + '\n_NUMPAD': 108, '-_NUMPAD': 109, '._NUMPAD': 110, '/_NUMPAD': 111, + F1: 112, F2: 113, F3: 114, F4: 115, F5: 116, F6: 117, F7: 118, + F8: 110, F9: 120, F10: 121, F11: 122, F12: 123, F13: 124, F14: 125, + F15: 126, F16: 127, NUMLOCK: 144, SCROLLLOCK: 145, + ';': 186, '=': 187, ',': 188, '-': 189, '.': 190, '/': 191, '`': 192, + '(': 219, '\\': 220, ')': 221, '\'': 222 +}; +Ox.MAP_TILE_SIZE = 256; +Ox.MONTHS = [ + 'January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December' +]; +Ox.SHORT_MONTHS = Ox.MONTHS.map(function(val) { + return val.substr(0, 3); +}); +Ox.PREFIXES = ['K', 'M', 'G', 'T', 'P']; +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' +}; +// local timezone offset in milliseconds +Ox.TYPES = [ + 'Arguments', 'Array', 'Boolean', 'Date', 'Element', 'Function', 'Infinity', + 'NaN', 'Null', 'Number', 'Object', 'RegExp', 'String', 'Undefined' +]; +Ox.VERSION = '0.1.2'; +Ox.WEEKDAYS = [ + 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' +]; +Ox.SHORT_WEEKDAYS = Ox.WEEKDAYS.map(function(val) { + return val.substr(0, 3); +}); + +/* +================================================================================ +Core functions +================================================================================ +*/ + +Ox.getset = function(obj, args, callback, context) { + /*** + Generic getter and setter function + + can be implemented like this: + + that.options = function() { + return Ox.getset(options, arguments, setOption(key, val), that); + } + + Ox.getset(obj, []) returns obj + Ox.getset(obj, [key]) returns obj.key + Ox.getset(obj, [key, val], callback, context) + Ox.getset(obj, [{key: val, ...}], callback, context) sets obj.key to val, + calls callback(key, val) + for each changed value, + returns context + (for chaining) + + >>> o = new function() { var o = {}, s = function() {}, t = this; t.o = function() { return Ox['getset'](o, arguments, s, t); }; return t; } + true + >>> Ox.getset({}, []) && o.o('key', 'val').o('key') + 'val' + >>> Ox.getset({}, []) && o.o({key: 'val', foo: 'bar'}).o().foo + 'bar' + >>> Ox.getset({}, []) && typeof o.o({foo: undefined}).o('foo') == 'undefined' + true + >>> delete o + true + ***/ + 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(Ox.isObject(args[0]) ? args[0] : 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.PATH = Array.prototype.slice.apply( + document.getElementsByTagName('script') +).filter(function(element) { + return /Ox\.js$/.test(element.src); +})[0].src.replace('Ox.js', ''); + +Ox.load = function(module, options, callback) { + /*** + loads Ox modules + fixme: no way to load multiple modules + ***/ + 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 = (function() { + /*** + loads stylesheets, scripts and images + ***/ + var cache = {}; + return function (file, callback) { + var element, request, + type = file.split('.').pop(); + if (!cache[file]) { + if (type == 'css' || type == 'js') { + 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 { + element = new Image(); + element.onload = addFileToCache; + element.src = file; + } + } else { + callback(); + } + function addFileToCache() { + type == 'svg' && Ox.print('addToCache', file) + if (type == 'css' || type == 'js') { + cache['file'] = true; + callback(); + } else { + cache['file'] = element; + callback(element); + } + } + 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.loadJSON = function(url, callback) { + var req = new XMLHttpRequest(); + req.open('GET', url, true); + req.onreadystatechange = function() { + if (req.readyState == 4) { + if (req.status == 200) { + callback(JSON.parse(req.responseText)); + } else { + throw new Error('URL ' + url + ', status ' + req.status); + } + } + }; + req.send(); +}; + +Ox.print = function() { + /* + */ + if (window.console) { + var args = Ox.makeArray(arguments), + date = new Date; + args.unshift(Ox.formatDate(date, '%H:%M:%S') + '.' + + (Ox.pad(+date % 1000, 3))); + window.console.log.apply(window.console, args); + } +}; + +Ox.uid = (function() { + /*** + returns a unique id + >>> Ox.uid() != Ox.uid() + true + ***/ + var uid = 0; + return function() { + return uid++; + }; +}()); + +Ox.wrap = function(val, chained) { + /*** + >>> Ox.wrap('foobar').reverse() + 'raboof' + >>> Ox.wrap('foobar').chain().reverse().reverse().value() + 'foobar' + ***/ + 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 = function(obj) { + /*** + returns the average of an array's values, or an object's properties + >>> Ox.avg([-1, 0, 1]) + 0 + >>> Ox.avg({a: 1, b: 2, c: 3}) + 2 + ***/ + 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 = 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.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(arg) { + /* + like $.makeArray() + >>> Ox.makeArray('foo', 'bar') + ['foo', 'bar'] + >>> (function() { return Ox.makeArray(arguments); }('foo', 'bar')) + ['foo', 'bar'] + */ + return Array.prototype.slice.call( + Ox.isArguments(arg) ? arg : arguments + ); +}; + +Ox.makeObject = function() { + /* + >>> Ox.makeObject("foo", "bar").foo + "bar" + >>> Ox.makeObject(["foo", "bar"]).foo + "bar" + >>> Ox.makeObject({foo: "bar"}).foo + "bar" + >>> (function() { return Ox.makeObject(arguments); }("foo", "bar")).foo + "bar" + */ + var obj = {}; + if (arguments.length == 1) { + if (Ox.isObject(arguments[0])) { + // ({foo: 'bar'}) + obj = arguments[0]; + } else { + // (['foo', 'bar']) + obj[arguments[0][0]] = arguments[0][1]; + } + } else { + // ('foo', 'bar') + obj[arguments[0]] = arguments[1]; + } + return obj; +}; + +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] + */ + 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 = {}; + arr.forEach(function(val, i) { + var match = /^\d+/(val); + matches[val] = match ? match[0] : ''; + }); + len = Ox.max(Ox.map(matches, function(val) { + return val.length; + })); + 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) { + /* + >>> 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] + */ + var values = []; + Ox.forEach(obj, function(val) { + values.push(val); + }); + return values; +}; + +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 = function(rgb) { + /* + >>> 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] + */ + 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 = function(hsl) { + /* + >>> 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] + */ + 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 = function(date, weekday, utc) { + /* + >>> 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" + */ + 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 = function(date, utc) { + /* + >>> Ox.getDayOfTheYear(new Date("12/31/2000")) + 366 + >>> Ox.getDayOfTheYear(new Date("12/31/2002")) + 365 + >>> Ox.getDayOfTheYear(new Date("12/31/2004")) + 366 + */ + 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 = 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 = (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') { + //Ox.print('document is ready') + callback(); + } else { + callbacks.push(callback); + //Ox.print('document is not ready', callbacks) + } + } +}()); + +Ox.element = function(str) { + /* + Generic HTML element, mimics jQuery + >>> Ox.element('div').attr({id: 'foo'}).attr('id') + 'foo' + >>> Ox.element('div').css('color', 'red').css('color') + 'red' + >>> Ox.element('div').html('foo').html() + 'foo' + */ + return { + 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: function(str) { + this[0].className += (this[0].className ? ' ' : '') + str; + return this; + }, + append: function(element) { + this[0].appendChild(element[0]); + return this; + }, + appendTo: function(element) { + element[0].appendChild(this[0]); + return this; + }, + 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.apply(null, arguments), function(v, k) { + that[0].setAttribute(k, v); + }); + ret = this; + } + return ret; + }, + 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.apply(null, arguments), function(v, k) { + that[0].style[k] = v; + }); + ret = this; + } + return ret; + }, + html: function(str) { + var ret; + if (Ox.isUndefined(str)) { + ret = this[0].innerHTML; + } else { + this[0].innerHTML = str; + ret = this; + } + return ret; + }, + mousedown: function(fn) { + this[0].onmousedown = fn; + 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; + //return $('
').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); + 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 + */ + return str.indexOf(chr) > -1; +}; + +Ox.endsWith = function(str, sub) { + /* + >>> Ox.endsWith("foobar", "bar") + true + */ + return str.toString().substr(-sub.length) === sub; +}; + +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 + */ + return !!/^[0-9A-Z\.\+\-_]+@(?:[0-9A-Z\-]+\.)+[A-Z]{2,6}$/i(str); +} + +Ox.pad = function(str, len, pad, pos) { + /* + >>> 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(str, num) { + /* + fixme: make this work for arrays, like in python + >>> Ox.repeat(1, 3) + "111" + >>> Ox.repeat("foo", 3) + "foofoofoo" + */ + return num >= 1 ? new Array(num + 1).join(str.toString()) : ''; +}; + +Ox.reverse = function(str) { + /* + Ox.reverse("foo") + oof + */ + return str.toString().split('').reverse().join(''); +}; + +Ox.startsWith = function(str, sub) { + /* + >>> Ox.startsWith("foobar", "foo") + true + */ + return str.toString().substr(0, sub.length) === sub; +}; + +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) + not implemented + >>> 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.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 + /* + 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('He\'s referring to the "ill-conceived" AOL/TimeWarner merger--didn\'t you know?') + ['he\'s', 'referring', 'to' , 'the' , 'ill-conceived' , 'aol', 'timewarner' , 'merger' , 'didn\'t', 'you', 'know'] + */ + var arr = str.toLowerCase().split(/\b/), + chr = "-'", + len = arr.length, + startsWithWord = !!/\w/(arr[0]); + arr.forEach(function(v, i) { + // find single occurrences of chars in chr + // that are not at the beginning or end of str + // 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) { + /* + >>> 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 = function(val) { + // fixme: void 0 is also nice + /* + >>> Ox.isUndefined() + true + */ + return typeof val == 'undefined'; +}; + +Ox.typeOf = function(val) { + /* + >>> (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' + */ + var ret; + Ox.forEach(Ox.TYPES, function(type) { + if (Ox['is' + type](val)) { + ret = type.toLowerCase(); + return false; + } + }); + return ret; +};