From 37219bfbe999691aee4536ee9283ff8b1734be14 Mon Sep 17 00:00:00 2001 From: rolux Date: Sun, 8 May 2011 14:14:07 +0200 Subject: [PATCH] better Ox.isEqual(), more tests, more documentation --- demos/test/js/test.js | 9 +- source/Ox.UI/css/Ox.UI.css | 12 + source/Ox.UI/js/Core/Ox.DocPage.js | 13 +- source/Ox.UI/js/Core/Ox.SyntaxHighlighter.js | 22 +- source/Ox.js | 662 ++++++++++++------- 5 files changed, 466 insertions(+), 252 deletions(-) diff --git a/demos/test/js/test.js b/demos/test/js/test.js index 791f52ce..0ba6e3a9 100644 --- a/demos/test/js/test.js +++ b/demos/test/js/test.js @@ -63,8 +63,8 @@ Ox.load('UI', { .html( Ox.repeat(' ', 4) + '> ' + Ox.encodeHTML(test.statement) + ' ==> ' + - Ox.encodeHTML(test.actual) + - (test.success ? '' : ' !=> ' + Ox.encodeHTML(test.expected)) + (test.success ? '' : Ox.encodeHTML(test.actual) + ' !=> ') + + Ox.encodeHTML(test.expected) ) .hide() .appendTo($foo); @@ -78,7 +78,10 @@ Ox.load('UI', { height: '15px', padding: '4px 8px 4px 8px', fontFamily: 'Menlo, Monaco, Courier', - fontSize: '12px' + fontSize: '12px', + whiteSpace: 'nowrap', + MozUserSelect: 'text', + WebkitUserSelect: 'text' }); gradients.forEach(function(gradient) { $div.css({ diff --git a/source/Ox.UI/css/Ox.UI.css b/source/Ox.UI/css/Ox.UI.css index c18c9e50..96d66eb5 100644 --- a/source/Ox.UI/css/Ox.UI.css +++ b/source/Ox.UI/css/Ox.UI.css @@ -254,6 +254,18 @@ Dialog cursor: se-resize; } +/* +================================================================================ +Documentation +================================================================================ +*/ + +.OxDocPage code { + //border: 1px solid rgb(232, 232, 232); + //background: rgb(248, 248, 248); + white-space: nowrap; +} + /* ================================================================================ Drag & Drop diff --git a/source/Ox.UI/js/Core/Ox.DocPage.js b/source/Ox.UI/js/Core/Ox.DocPage.js index 681cd9b8..5bd64f34 100644 --- a/source/Ox.UI/js/Core/Ox.DocPage.js +++ b/source/Ox.UI/js/Core/Ox.DocPage.js @@ -6,7 +6,7 @@ Ox.DocPage = function(options, self) { item: {} }) .options(options || {}) - .addClass('OxText') + .addClass('OxDocPage OxText') .css({ overflow: 'auto' }); @@ -42,8 +42,13 @@ Ox.DocPage = function(options, self) { if (item[section]) { if (section == 'description') { $elements.push($('
') - .css({paddingLeft: (level * 32) + 'px'}) - .html(item.description) + .css({ + paddingTop: (level ? 0 : 8) + 'px', + borderTop: level ? '': '1px solid rgb(192, 192, 192)', + marginTop: (level ? 0 : 8) + 'px', + marginLeft: (level * 32) + 'px', + }) + .html(Ox.parseHTML(item.description)) ); } else { $elements.push($('
') @@ -108,7 +113,7 @@ Ox.DocPage = function(options, self) { .addClass(className) .css({marginLeft: (level * 32 + 16) + 'px'}) .html( - '' + Ox.parseHTML(example.result) + '' + '' + Ox.encodeHTML(example.result) + '' ) ) }); diff --git a/source/Ox.UI/js/Core/Ox.SyntaxHighlighter.js b/source/Ox.UI/js/Core/Ox.SyntaxHighlighter.js index 6722001d..25ce2cf6 100644 --- a/source/Ox.UI/js/Core/Ox.SyntaxHighlighter.js +++ b/source/Ox.UI/js/Core/Ox.SyntaxHighlighter.js @@ -39,7 +39,7 @@ Ox.SyntaxHighlighter = function(options, self) { !(self.options.stripComments && token.type == 'comment') ) { classNames = 'Ox' + Ox.toTitleCase(token.type); - if (token.type == 'whitespace') { + if (self.options.showWhitespace && token.type == 'whitespace') { if (isAfterLinebreak() && hasIrregularSpaces()) { classNames += ' OxLeading' } else if (isBeforeLinebreak()) { @@ -65,27 +65,15 @@ Ox.SyntaxHighlighter = function(options, self) { } }); self.lines = self.source.split('
'); - self.lineNumbersWidth = (self.lines.length + self.options.offset - 1).toString().length * 7; - self.sourceCodeWidth = 80 * 7 + ( - self.lines.length > 40 ? Ox.UI.SCROLLBAR_SIZE : 0 - ); - self.height = 40 * 14 + ( - Math.max.apply(null, self.lines.map(function(line) { - return line.length; - })) > 80 ? Ox.UI.SCROLLBAR_SIZE : 0 - ); - - that.css({ - //width: self.lineNumbersWidth + self.sourceCodeWidth, - //height: self.height - }); + self.lineNumbersWidth = ( + self.lines.length + self.options.offset - 1 + ).toString().length * 7; self.$lineNumbers = new Ox.Element() .addClass('OxLineNumbers') .css({ display: 'table-cell', width: self.lineNumbersWidth + 'px', - //height: (self.lines.length * 14) + 8 + 'px', padding: '4px', }) .html( @@ -98,8 +86,6 @@ Ox.SyntaxHighlighter = function(options, self) { .addClass('OxSourceCode') .css({ display: 'table-cell', - //width: self.sourceCodeWidth + 'px', - //height: (self.lines.length * 14) + 'px', padding: '4px' }) .html(self.source) diff --git a/source/Ox.js b/source/Ox.js index b18d6a86..cbc106ce 100644 --- a/source/Ox.js +++ b/source/Ox.js @@ -2,6 +2,34 @@ // OxJS (c) 2011 Ox2620, dual-licensed GPL/MIT, see http://oxjs.org for details +/* +Some conventions: + Functions + - only one var statement, in the first line of the function + - return only once, from the last line of the function body + Variable names + arg argument + args arguments + arr array + callback callback function + col collection (array, string or object) + date date + fn function + hasFoo boolean + i index (integer key) + isFoo boolean + k key (of a key/value pair) + key key (of a key/value pair) + max maximum value + min minumum value + num number + obj object + re regexp + ret return value + v value (of a key/value pair) + val value (of a key/value pair) +*/ + // 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 @@ -164,6 +192,41 @@ Ox.merge = function(arr) { return arr; }; +/*@ +Ox.sort Sorts an array, handling leading digits and ignoring capitalization + > Ox.sort(['10', '9', 'B', 'a']) + ['9', '10', 'a', 'B'] +@*/ +Ox.sort = function(arr) { + 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.unique Returns an array without duplicate values > Ox.unique([1, 2, 3, 2, 1]) @@ -473,7 +536,7 @@ Ox.getset = function(obj, args, callback, context) { } /*@ -Ox.isEmpty Returns true if an array, object or string is empty +Ox.isEmpty Returns true if a collection is empty > Ox.isEmpty([]) true > Ox.isEmpty({}) @@ -486,7 +549,7 @@ Ox.isEmpty = function(val) { }; /*@ -Ox.keys Returns the keys of an array, object or string +Ox.keys Returns the keys of a collection Unlike Object.keys(), Ox.keys() works for arrays, objects and strings. > Ox.keys([1, 2, 3]) @@ -502,7 +565,6 @@ Ox.keys Returns the keys of an array, object or string @*/ // fixme: is this really needed? arrays... ok... but strings?? - Ox.keys = function(obj) { var keys = []; Ox.forEach(obj, function(v, k) { @@ -727,27 +789,20 @@ Ox.range = function() { 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') +/*@ +Ox.setPropertyOnce Sets a property once + Given a array of objects, each of which has a property with a boolean + value, this sets exactly one of these to true, and returns the index + of the object whose property is true. + > Ox.setPropertyOnce([{selected: false}, {selected: false}], 'selected') 0 - >>> Ox.setPropertyOnce([{selected: false}, {selected: true}], 'selected') + > Ox.setPropertyOnce([{selected: false}, {selected: true}], 'selected') 1 - >>> Ox.setPropertyOnce([{selected: true}, {selected: true}], 'selected') - 0 - */ + > Ox.setPropertyOnce([{selected: true}, {selected: true}], 'selected') + 0 +@*/ +// fixme: strange name, and shouldn't it return the full array? +Ox.setPropertyOnce = function(arr, str) { var pos = -1; Ox.forEach(arr, function(v, i) { if (pos == -1 && arr[i][str]) { @@ -763,74 +818,72 @@ Ox.setPropertyOnce = function(arr, str) { return pos; }; -Ox.shuffle = function(arr) { - /* - >>> Ox.shuffle([1, 2, 3]).length +/*@ +Ox.shuffle Randomizes the order of values within a collection + > Ox.shuffle([1, 2, 3]).length 3 - */ - var shuffle = arr; - return shuffle.sort(function() { + > Ox.len(Ox.shuffle({a: 1, b: 2, c: 3})) + 3 + > Ox.shuffle('123').length + 3 +@*/ + +Ox.shuffle = function(col) { + var keys, ret, type = Ox.typeOf(col), values; + function sort() { return Math.random() - 0.5; - }); + } + if (type == 'array') { + ret = col.sort(sort); + } else if (type == 'object') { + keys = Object.keys(col); + values = Ox.values(col).sort(sort); + ret = {}; + keys.forEach(function(key, i) { + ret[key] = values[i] + }); + } else if (type == 'string') { + ret = col.split('').sort(sort).join(''); + } + return ret; }; +/*@ +Ox.some Tests if one or more elements of a collection satisfy a given condition + Unlike [].some(), Ox.some() works for arrays, + objects and strings. + > Ox.some([2, 1, 0], function(i, v) { return i == v; }) + true + > Ox.some({a: 1, b: 2, c: 3}, function(v) { return v == 1; }) + true + > Ox.some("foo", function(v) { return v == 'f'; }) + true +@*/ Ox.some = function(obj, fn) { - /* - 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}) +/*@ +Ox.sum Returns the sum of the values of a collection + > Ox.sum(1, 2, 3) 6 - */ + > Ox.sum([1, 2, 3]) + 6 + > Ox.sum({a: 1, b: 2, c: 3}) + 6 + > Ox.sum('123') + 6 + > Ox.sum('123foo') + 6 + > Ox.sum('08', -2, 'foo') + 6 +@*/ +Ox.sum = function(col) { var sum = 0; - Ox.forEach(obj, function(val) { - sum += val; + col = arguments.length > 1 ? Ox.makeArray(arguments) : col; + Ox.forEach(col, function(val) { + val = +val; + sum += Ox.isNumber(val) ? val : 0; }); return sum; }; @@ -854,43 +907,39 @@ Ox.toArray = function(obj) { return arr; }; -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]) +/*@ +Ox.values Returns the values of a collection + > Ox.values([1, 2, 3]) [1, 2, 3] - >>> Ox.values({a: 1, b: 2, c: 3}) + > Ox.values({a: 1, b: 2, c: 3}) [1, 2, 3] - >>> Ox.values('abc') + > Ox.values('abc') ['a', 'b', 'c'] - >>> Ox.values([1,]) - [1] - */ - // fixme: why doesn't this use map? + > Ox.values([1,,3]) + [1, 3] +@*/ +Ox.values = function(col) { + // this happens to works for arrays and strings, but still: + // Ox.values(arr) -> arr, Ox.values(str) -> str.split('') var values = []; - Ox.forEach(obj, function(val) { + Ox.forEach(col, 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 +/*@ +Ox.walk Recursively walk a tree-like key/value store + + > Ox.test.number 6 - */ +@*/ +Ox.walk = function(obj, fn) { Ox.forEach(obj, function(val, key) { fn(val, key, obj); Ox.walk(obj[key], fn); @@ -2788,6 +2837,7 @@ Ox.doc Generates documentation for annotated JavaScript @*/ Ox.doc = (function() { + // fixme: dont require the trailing '@' var re = { item: /^(.+?) <(.+?)> (.+)$/, multiline: /^\/\*\@.*?\n([\w\W]+)\n.*?\@\*\/$/, @@ -2886,6 +2936,7 @@ Ox.doc = (function() { }; } function parseTokens(tokens, includeLeading) { + // fixme: do not strip whitespace from the beginning of the first line of the items' source var leading = [], tokens_ = []; tokens.forEach(function(token) { @@ -3079,7 +3130,7 @@ Ox.test = function(file, callback) { var actual = eval(example.statement); if (example.result) { tests.push({ - actual: actual, + actual: JSON.stringify(actual), expected: example.result, name: item.name, section: item.section, @@ -3174,18 +3225,19 @@ Ox.tokenize = (function() { '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', + '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', + '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', @@ -3226,7 +3278,22 @@ Ox.tokenize = (function() { 'match', 'replace', 'search', 'slice', 'split', 'substr', 'substring', - 'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toUpperCase', 'trim' + 'toLocaleLowerCase', 'toLocaleUpperCase', 'toLowerCase', 'toUpperCase', 'trim', + // Window + 'addEventListener', 'alert', 'atob', + 'blur', 'btoa', + 'clearInterval', 'clearTimeout', 'close', 'confirm', + 'dispatchEvent', + 'escape', + 'find', 'focus', + 'getComputedStyle', 'getSelection', + 'moveBy', 'moveTo', + 'open', + 'postMessage', 'print', 'prompt', + 'removeEventListener', 'resizeBy', 'resizeTo', + 'scroll', 'scrollBy', 'scrollTo', + 'setCursor', 'setInterval', 'setTimeout', 'stop', + 'unescape' ], object: [ 'Array', @@ -3250,8 +3317,24 @@ Ox.tokenize = (function() { // Function 'constructor', 'length', 'prototype', // RegExp - 'global', 'ignoreCase', 'lastIndex', 'multiline', 'source' + 'global', 'ignoreCase', 'lastIndex', 'multiline', 'source', + // Window + 'applicationCache', + 'closed', 'content', 'crypto', + 'defaultStatus', 'document', + 'frameElement', 'frames', + 'history', + 'innerHeight', 'innerWidth', + 'length', 'location', 'locationbar', 'localStorage', + 'menubar', + 'name', 'navigator', + 'opener', 'outerHeight', 'outerWidth', + 'pageXOffset', 'pageYOffset', 'parent', 'personalbar', + 'screen', 'screenX', 'screenY', 'scrollbars', 'scrollX', 'scrollY', + 'self', 'sessionStorage', 'status', 'statusbar', + 'toolbar', 'top' ] + // Window stuff? 'atob', 'btoa', 'console', 'document' ... }; return function(source) { @@ -3272,7 +3355,7 @@ Ox.tokenize = (function() { }, identifier: function() { var str; - while (identifier.indexOf(source[++cursor]) > -1) {} + while ((identifier + number).indexOf(source[++cursor]) > -1) {} str = source.substring(start, cursor); Ox.forEach(word, function(value, key) { if (value.indexOf(str) > -1) { @@ -3419,6 +3502,7 @@ Ox.divideInt Divides a number by another and returns an array of integers [16, 16, 17, 17, 17, 17] @*/ Ox.divideInt = function(num, by) { + // fixme: for loops are so C ;) var arr = [], div = parseInt(num / by), mod = num % by, @@ -3550,6 +3634,34 @@ Ox.extend = function() { return obj; }; +/*@ +Ox.serialize Parses an object into query parameters + > Ox.serialize({a: 1, b: 2, c: 3}) + 'a=1&b=2&c=3' +@*/ +Ox.serialize = function(obj) { + var arr = []; + Ox.forEach(obj, function(val, key) { + arr.push(key + '=' + val); + }); + return arr.join('&'); +}; + +/*@ +Ox.unserialize Parses query parameters into an object + > Ox.unserialize('a=1&b=2&c=3') + {a: 1, b: 2, c: 3} +@*/ +Ox.unserialize = function(str) { + var obj = {}; + Ox.forEach(str.split('&'), function(val) { + var arr = val.split('='), + num = +arr[1]; + obj[arr[0]] = Ox.isNumber(num) ? num : arr[1]; + }); + return obj; +}; + /* ================================================================================ RegExp functions @@ -3684,7 +3796,7 @@ Ox.loadFile = (function() { Ox.basename = function(str) { /* - fixme: this should go into Path functions + fixme: deprecate >>> Ox.basename("foo/bar/foo.bar") "foo.bar" >>> Ox.basename("foo.bar") @@ -3693,6 +3805,10 @@ Ox.basename = function(str) { return str.replace(/^.*[\/\\]/g, ''); }; +/*@ +Ox.char Alias for String.fromCharCode +@*/ +// fixme: add some mapping? like Ox.char(9, 13) or Ox.char([9, 13])? Ox.char = String.fromCharCode; Ox.clean = function(str) { @@ -3723,11 +3839,15 @@ Ox.contains = function(str, chr) { return str.indexOf(chr) > -1; }; -Ox.endsWith = function(str, sub) { - /* - >>> Ox.endsWith("foobar", "bar") +/*@ +Ox.endsWith Checks if a string ends with a given substring + While Ox.endsWith('foobar', 'bar') is longer than + /bar$/.test('foobar'), Ox.endsWith('foobar', bar) + is shorter than new RegExp(bar + '$').test('foobar'). + > Ox.endsWith('foobar', 'bar') true - */ +@*/ +Ox.endsWith = function(str, sub) { return new RegExp(sub + '$').test(str); }; @@ -3739,34 +3859,40 @@ Ox.highlight = function(txt, str) { ) : txt; }; +/*@ +Ox.isValidEmail Tests if a string is a valid e-mail address + (str) -> True if the string is a valid e-mail address + str Any string + > Ox.isValidEmail("foo@bar.com") + true + > Ox.isValidEmail("foo.bar@foobar.co.uk") + true + > Ox.isValidEmail("foo@bar") + false + > Ox.isValidEmail("foo@bar..com") + false +@*/ 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 Pad a string to a given length + > 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" +@*/ 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); @@ -3778,6 +3904,30 @@ Ox.pad = function(str, len, pad, pos) { return str; }; +/*@ +Ox.parsePath Returns the components of a path + (str) -> Path + extension File extension + filename Filename + pathname Pathname + > Ox.parsePath('/foo/bar/foo.bar') + {extension: 'bar', filename: 'foo.bar', pathname: '/foo/bar/'} + > Ox.parsePath('foo/') + {extension: '', filename: '', pathname: 'foo/'} + > Ox.parsePath('foo') + {extension: '', filename: 'foo', pathname: ''} + > Ox.parsePath('.foo') + {extension: '', filename: '.foo', pathname: ''} +@*/ +Ox.parsePath = function(str) { + var matches = /^(.+\/)?(.+?(\..+)?)?$/(str); + return { + pathname: matches[1] || '', + filename: matches[2] || '', + extension: matches[3] ? matches[3].substr(1) : '' + }; +} + Ox.repeat = function(obj, num) { /* works for arrays, numbers and strings @@ -3807,6 +3957,14 @@ Ox.reverse = function(str) { return str.toString().split('').reverse().join(''); }; +/*@ +Ox.startsWith Checks if a string starts with a given substring + While Ox.startsWith('foobar', 'foo') is longer than + /^foo/.test('foobar'), Ox.startsWith('foobar', foo) + is shorter than new RegExp('^' + foo).test('foobar'). + > Ox.endsWith('foobar', 'bar') + true +@*/ Ox.startsWith = function(str, sub) { /* >>> Ox.startsWith("foobar", "foo") @@ -3819,12 +3977,13 @@ Ox.startsWith = function(str, sub) { return new RegExp('^' + sub).test(str); }; +/*@ +Ox.stripTags Strips HTML tags from a string + > Ox.stripTags('foo') + 'foo' +@*/ Ox.stripTags = function(str) { - /* - >>> Ox.stripTags("foo") - "foo" - */ - return str.replace(/(<.*?>)/gi, ''); + return str.replace(/(<.*?>)/g, ''); }; Ox.substr = function(str, start, stop) { @@ -3852,47 +4011,54 @@ Ox.substr = function(str, start, stop) { ); }; +/*@ +Ox.toCamelCase Takes a string with '-', '/' or '_', returns a camelCase string + > Ox.toCamelCase('foo-bar-baz') + 'fooBarBaz' + > Ox.toCamelCase('foo/bar/baz') + 'fooBarBaz' + > Ox.toCamelCase('foo_bar_baz') + 'fooBarBaz' +@*/ + Ox.toCamelCase = function(str) { - /* - >>> 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 Takes a camelCase string, returns a string with dashes + > Ox.toDashes('fooBarBaz') + 'foo-bar-baz' +@*/ Ox.toDashes = function(str) { - /* - >>> Ox.toDashes("fooBarBaz") - "foo-bar-baz" - */ return str.replace(/[A-Z]/g, function(str) { return '-' + str.toLowerCase(); }); }; +/*@ +Ox.toSlashes Takes a camelCase string, returns a string with slashes + > Ox.toSlashes('fooBarBaz') + 'foo/bar/baz' +@*/ Ox.toSlashes = function(str) { /* - >>> Ox.toSlashes("fooBarBaz") - "foo/bar/baz" */ return str.replace(/[A-Z]/g, function(str) { return '/' + str.toLowerCase(); }); }; +/*@ +Ox.toTitleCase Returns a string with capitalized words + > Ox.toTitleCase('foo') + 'Foo' + > Ox.toTitleCase('Apple releases iPhone, IBM stock plummets') + 'Apple Releases iPhone, IBM Stock Plummets' +@*/ Ox.toTitleCase = function(str) { - /* - >>> 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(); @@ -3903,11 +4069,12 @@ Ox.toTitleCase = function(str) { }).join(" "); }; +/*@ +Ox.toUnderscores Takes a camelCase string, returns string with underscores + > Ox.toUnderscores('fooBarBaz') + 'foo_bar_baz' +@*/ Ox.toUnderscores = function(str) { - /* - >>> Ox.toUnderscores("fooBarBaz") - "foo_bar_baz" - */ return str.replace(/[A-Z]/g, function(str) { return '_' + str.toLowerCase(); }); @@ -3921,10 +4088,23 @@ Ox.trim = function(str) { // is in jQuery, and in JavaScript itself return str.replace(/^\s+|\s+$/g, ""); }; +/*@ +Ox.truncate Truncate a string to a given length + (string, length) Truncated string + (string, length, position) -> Truncated string + (string, length, placeholder) -> Truncated string + (string, length, position, placeholder) -> Truncated string + > Ox.truncate('anticonstitutionellement', 16) + 'anticonstitut...' + > Ox.truncate('anticonstitutionellement', 16, -1) + '...utionellement' + > Ox.truncate('anticonstitutionellement', 16, '>') + 'anticonstitutio>' + > Ox.truncate("anticonstitutionellement", 16, "...", "center") + 'anticon...lement' +@*/ Ox.truncate = function(str, len, pad, pos) { /* - >>> Ox.truncate("anticonstitutionellement", 16, "...", "center") - "anticon...lement" */ var pad = pad || {}, pos = pos || "right", @@ -3945,11 +4125,14 @@ Ox.truncate = function(str, len, pad, pos) { return str; }; +/*@ +Ox.words Splits a string into words, removing punctuation + (string) -> <[s]> Array of words + string Any string + > Ox.words('Let\'s "walk" a tree-like key/value store--okay?') + ["let's", "walk", "a", "tree-like", "key", "value", "store", "okay"] +@*/ Ox.words = function(str) { - /* - > 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, @@ -3978,23 +4161,21 @@ Ox.words = function(str) { /*@ Ox.wordwrap Wrap a string at word boundaries + > 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' @*/ 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 || "
", + sep = sep || '
', bal = bal || false, spa = Ox.isUndefined(spa) ? true : spa, - words = str.split(" "), + words = str.split(' '), lines; if (bal) { // balance lines: test if same number of lines @@ -4015,15 +4196,15 @@ Ox.wordwrap = function(str, len, sep, bal, spa) { } } } - lines = [""]; + lines = ['']; Ox.forEach(words, function(word) { - if ((lines[lines.length - 1] + word + " ").length <= len + 1) { + if ((lines[lines.length - 1] + word + ' ').length <= len + 1) { // word fits in current line - lines[lines.length - 1] += word + " "; + lines[lines.length - 1] += word + ' '; } else { if (word.length <= len) { // word fits in next line - lines.push(word + " "); + lines.push(word + ' '); } else { // word is longer than line var chr = len - lines[lines.length - 1].length; @@ -4031,7 +4212,7 @@ Ox.wordwrap = function(str, len, sep, bal, spa) { for (var pos = chr; pos < word.length; pos += len) { lines.push(word.substr(pos, len)); } - lines[lines.length - 1] += " "; + lines[lines.length - 1] += ' '; } } }); @@ -4106,48 +4287,75 @@ Ox.isElement = function(val) { /*@ Ox.isEqual Returns true if two values are equal - > Ox.isEqual(false, false) - true - > Ox.isEqual(0, 0) - true - > Ox.isEqual(NaN, NaN) - false - > Ox.isEqual('', '') + > Ox.isEqual((function() { return arguments; }()), (function() { return arguments; }())) 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'}}) + > Ox.isEqual([1, 2, 3], [3, 2, 1]) + false + > Ox.isEqual(false, false) + true + > Ox.isEqual(new Date(0), new Date(0)) + true + > Ox.isEqual(new Date(0), new Date(1)) + false + > Ox.isEqual(document.createElement('a'), document.createElement('a')) + true + > Ox.isEqual(document.createElement('a'), document.createElement('b')) + false + > Ox.isEqual(function(a) { return a; }, function(a) { return a; }) + true + > Ox.isEqual(function(a) { return a; }, function(b) { return b; }) + false + > Ox.isEqual(Infinity, Infinity) + true + > Ox.isEqual(-Infinity, Infinity) + false + > Ox.isEqual(NaN, NaN) + false + > Ox.isEqual(0, 0) true > Ox.isEqual({a: 1, b: 2, c: 3}, {c: 3, b: 2, a: 1}) - true // FIXME: is false - > Ox.isEqual(function(arg) { return arg; }, function(arg) { return arg; }) true - > Ox.isEqual(function(foo) { return foo; }, function(bar) { return bar; }) + > Ox.isEqual({a: 1, b: [2, 3], c: {d: '4'}}, {a: 1, b: [2, 3], c: {d: '4'}}) + true + > Ox.isEqual(/ /, / /) + true + > Ox.isEqual(/ /g, / /i) false + > Ox.isEqual('', '') + true + > Ox.isEqual(void 0, void 0) + true @*/ -Ox.isEqual = function(obj0, obj1) { - 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; +Ox.isEqual = function(a, b) { + var isEqual = false, type = Ox.typeOf(a); + if (a === b) { + isEqual = true; + } else if (type == Ox.typeOf(b)) { + if (a == b) { + isEqual = true; + } else if (type == 'date') { + isEqual = a.getTime() == b.getTime(); + } else if (['element', 'function'].indexOf(type) > -1) { + isEqual = a.toString() == b.toString(); + } else if (type == 'regexp') { + isEqual = a.global == b.global && + a.ignore == b.ignore && + a.multiline == b.multiline && + a.source == b.source; + } else if ( + ['arguments', 'array', 'object'].indexOf(type) > -1 && + Ox.len(a) == Ox.len(b) + ) { + isEqual = true; + Ox.forEach(a, function(v, k) { + isEqual = Ox.isEqual(v, b[k]); + return isEqual; }); - } 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; + return isEqual; }; /*@