From 139693dce6865e28a90ce67ff28ed136dd1a0179 Mon Sep 17 00:00:00 2001 From: John-David Dalton Date: Sat, 14 Jul 2012 04:51:50 -0400 Subject: [PATCH] Optimize benchmark setups, cleanup perf/index.html, and add platform.js. Former-commit-id: 228b7b6fd230638f9ec8c79eafa56506fc84b79a --- perf/index.html | 41 +- perf/perf.js | 639 ++++++++++++++--------- vendor/platform.js/LICENSE.txt | 20 + vendor/platform.js/README.md | 99 ++++ vendor/platform.js/platform.js | 922 +++++++++++++++++++++++++++++++++ 5 files changed, 1455 insertions(+), 266 deletions(-) create mode 100644 vendor/platform.js/LICENSE.txt create mode 100644 vendor/platform.js/README.md create mode 100644 vendor/platform.js/platform.js diff --git a/perf/index.html b/perf/index.html index 1fa6110b0..2fc310deb 100644 --- a/perf/index.html +++ b/perf/index.html @@ -21,36 +21,37 @@ var lodash = _.noConflict(); + + diff --git a/perf/perf.js b/perf/perf.js index 4588f9455..5a696456b 100644 --- a/perf/perf.js +++ b/perf/perf.js @@ -25,10 +25,8 @@ _._ || _ ); - /** Used to access the Firebug Lite panel */ - var fbPanel = (fbPanel = window.document && document.getElementById('FirebugUI')) && - (fbPanel = (fbPanel = fbPanel.contentWindow || fbPanel.contentDocument).document || fbPanel) && - fbPanel.getElementById('fbPanel1'); + /** Used to access the Firebug Lite panel (set by `run`) */ + var fbPanel; /** Used to score Lo-Dash and Underscore performance */ var score = { 'lodash': 0, 'underscore': 0 }; @@ -72,20 +70,32 @@ } } + /** + * Runs all benchmark suites. + * + * @private (@public in the browser) + */ + function run() { + fbPanel = (fbPanel = window.document && document.getElementById('FirebugUI')) && + (fbPanel = (fbPanel = fbPanel.contentWindow || fbPanel.contentDocument).document || fbPanel) && + fbPanel.getElementById('fbPanel1'); + + log('\nSit back and relax, this may take a while.'); + suites[0].run(); + } + /*--------------------------------------------------------------------------*/ lodash.extend(Benchmark.options, { 'async': true, - 'maxTime': 1, - 'minSamples': 30, 'setup': function() { var window = Function('return this || global')(), _ = window._, - lodash = window.lodash; + lodash = window.lodash, + belt = this.name == 'Lo-Dash' ? lodash : _; var index, length, - belt = this.name == 'Lo-Dash' ? lodash : _, limit = 20, object = {}, objects = Array(limit), @@ -94,23 +104,30 @@ nestedNumbers = [1, [2], [3, [[4]]]], twoNumbers = [12, 21]; - var object2 = {}, - objects2 = Array(limit), - numbers2 = Array(limit), - nestedNumbers2 = [1, [2], [3, [[4]]]]; - for (index = 0, length = limit; index < length; index++) { numbers[index] = index; - numbers2[index] = index; - object['key' + index] = index; - object2['key' + index] = index; - objects[index] = { 'num': index }; - objects2[index] = { 'num': index }; } - if (typeof isBindAll != 'undefined') { + if (typeof bind != 'undefined') { + var contextObject = { 'name': 'moe' }, + ctor = function() {}; + + var func = function(greeting, punctuation) { + return greeting + ', ' + this.name + (punctuation || '.'); + }; + + var lodashBoundNormal = lodash.bind(func, contextObject), + lodashBoundCtor = lodash.bind(ctor, contextObject), + lodashBoundPartial = lodash.bind(func, contextObject, 'hi'); + + var _boundNormal = _.bind(func, contextObject), + _boundCtor = _.bind(ctor, contextObject), + _boundPartial = _.bind(func, contextObject, 'hi'); + } + + if (typeof bindAll != 'undefined') { var bindAllObjects = Array(this.count), funcNames = belt.functions(lodash); @@ -120,153 +137,154 @@ } } - var ctor = function() {}; + if (typeof groupBy != 'undefined') { + var wordToNumber = { + 'one': 1, + 'two': 2, + 'three': 3, + 'four': 4, + 'five': 5, + 'six': 6, + 'seven': 7, + 'eight': 8, + 'nine': 9, + 'ten': 10, + 'eleven': 11, + 'twelve': 12, + 'thirteen': 13, + 'fourteen': 14, + 'fifteen': 15, + 'sixteen': 16, + 'seventeen': 17, + 'eighteen': 18, + 'nineteen': 19, + 'twenty': 20, + 'twenty-one': 21, + 'twenty-two': 22, + 'twenty-three': 23, + 'twenty-four': 24, + 'twenty-five': 25 + }; - var func = function(greeting, punctuation) { - return greeting + ', ' + this.name + (punctuation || '.'); - }; + var words = belt.keys(wordToNumber).slice(0, limit); + } - var objectOfPrimitives = { - 'boolean': true, - 'number': 1, - 'string': 'a' - }; + if (typeof isEqual != 'undefined') { + var objectOfPrimitives = { + 'boolean': true, + 'number': 1, + 'string': 'a' + }; - var objectOfObjects = { - 'boolean': new Boolean(true), - 'number': new Number(1), - 'string': new String('a') - }; + var objectOfObjects = { + 'boolean': new Boolean(true), + 'number': new Number(1), + 'string': new String('a') + }; - var contextObject = { 'name': 'moe' }; + var object2 = {}, + objects2 = Array(limit), + numbers2 = Array(limit), + nestedNumbers2 = [1, [2], [3, [[4]]]]; - var lodashBoundNormal = lodash.bind(func, contextObject), - lodashBoundCtor = lodash.bind(ctor, contextObject), - lodashBoundPartial = lodash.bind(func, contextObject, 'hi'); + for (index = 0, length = limit; index < length; index++) { + numbers2[index] = index; + object2['key' + index] = index; + objects2[index] = { 'num': index }; + } + } - var _boundNormal = _.bind(func, contextObject), - _boundCtor = _.bind(ctor, contextObject), - _boundPartial = _.bind(func, contextObject, 'hi'); + if (typeof template != 'undefined') { + var tplData = { + 'header1': 'Header1', + 'header2': 'Header2', + 'header3': 'Header3', + 'header4': 'Header4', + 'header5': 'Header5', + 'header6': 'Header6', + 'list': ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] + }; - var tplData = { - 'header1': 'Header1', - 'header2': 'Header2', - 'header3': 'Header3', - 'header4': 'Header4', - 'header5': 'Header5', - 'header6': 'Header6', - 'list': ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] - }; + var tplBase = + '
' + + "

<%= header1 %>

" + + "

<%= header2 %>

" + + "

<%= header3 %>

" + + "

<%= header4 %>

" + + "
<%= header5 %>
" + + "
<%= header6 %>
"; - var tplBase = - '
' + - "

<%= header1 %>

" + - "

<%= header2 %>

" + - "

<%= header3 %>

" + - "

<%= header4 %>

" + - "
<%= header5 %>
" + - "
<%= header6 %>
"; + var tpl = + tplBase + + "
    " + + "
  • <%= list[0] %>
  • " + + "
  • <%= list[1] %>
  • " + + "
  • <%= list[2] %>
  • " + + "
  • <%= list[3] %>
  • " + + "
  • <%= list[4] %>
  • " + + "
  • <%= list[5] %>
  • " + + "
  • <%= list[6] %>
  • " + + "
  • <%= list[7] %>
  • " + + "
  • <%= list[8] %>
  • " + + "
  • <%= list[9] %>
  • " + + '
' + + '
'; - var tpl = - tplBase + - "' + - '
'; + var tplWithEvaluate = + tplBase + + "' + + ''; - var tplWithEvaluate = - tplBase + - "' + - ''; + var tplBaseVerbose = + '
' + + "

<%= data.header1 %>

" + + "

<%= data.header2 %>

" + + "

<%= data.header3 %>

" + + "

<%= data.header4 %>

" + + "
<%= data.header5 %>
" + + "
<%= data.header6 %>
"; - var tplBaseVerbose = - '
' + - "

<%= data.header1 %>

" + - "

<%= data.header2 %>

" + - "

<%= data.header3 %>

" + - "

<%= data.header4 %>

" + - "
<%= data.header5 %>
" + - "
<%= data.header6 %>
"; + var tplVerbose = + tplBaseVerbose + + "
    " + + "
  • <%= data.list[0] %>
  • " + + "
  • <%= data.list[1] %>
  • " + + "
  • <%= data.list[2] %>
  • " + + "
  • <%= data.list[3] %>
  • " + + "
  • <%= data.list[4] %>
  • " + + "
  • <%= data.list[5] %>
  • " + + "
  • <%= data.list[6] %>
  • " + + "
  • <%= data.list[7] %>
  • " + + "
  • <%= data.list[8] %>
  • " + + "
  • <%= data.list[9] %>
  • " + + '
' + + '
'; - var tplVerbose = - tplBaseVerbose + - "' + - '
'; + var tplVerboseWithEvaluate = + tplBaseVerbose + + "' + + ''; - var tplVerboseWithEvaluate = - tplBaseVerbose + - "' + - ''; + var settingsObject = { 'variable': 'data' }; - var settingsObject = { 'variable': 'data' }; + var lodashTpl = lodash.template(tpl), + lodashTplWithEvaluate = lodash.template(tplWithEvaluate), + lodashTplVerbose = lodash.template(tplVerbose, null, settingsObject), + lodashTplVerboseWithEvaluate = lodash.template(tplVerboseWithEvaluate, null, settingsObject); - var lodashTpl = lodash.template(tpl), - lodashTplWithEvaluate = lodash.template(tplWithEvaluate), - lodashTplVerbose = lodash.template(tplVerbose, null, settingsObject), - lodashTplVerboseWithEvaluate = lodash.template(tplVerboseWithEvaluate, null, settingsObject); - - var _tpl = _.template(tpl), - _tplWithEvaluate = _.template(tplWithEvaluate), - _tplVerbose = _.template(tplVerbose, null, settingsObject), - _tplVerboseWithEvaluate = _.template(tplVerboseWithEvaluate, null, settingsObject); - - var wordToNumber = { - 'one': 1, - 'two': 2, - 'three': 3, - 'four': 4, - 'five': 5, - 'six': 6, - 'seven': 7, - 'eight': 8, - 'nine': 9, - 'ten': 10, - 'eleven': 11, - 'twelve': 12, - 'thirteen': 13, - 'fourteen': 14, - 'fifteen': 15, - 'sixteen': 16, - 'seventeen': 17, - 'eighteen': 18, - 'nineteen': 19, - 'twenty': 20, - 'twenty-one': 21, - 'twenty-two': 22, - 'twenty-three': 23, - 'twenty-four': 24, - 'twenty-five': 25 - }; - - var words = belt.keys(wordToNumber).slice(0, limit); + var _tpl = _.template(tpl), + _tplWithEvaluate = _.template(tplWithEvaluate), + _tplVerbose = _.template(tplVerbose, null, settingsObject), + _tplVerboseWithEvaluate = _.template(tplVerboseWithEvaluate, null, settingsObject); + } } }); @@ -331,61 +349,97 @@ suites.push( Benchmark.Suite('`_.bind` (uses native `Function#bind` if available and inferred fast)') - .add('Lo-Dash', function() { - lodash.bind(func, { 'name': 'moe' }, 'hi'); + .add('Lo-Dash', { + 'fn': function() { + lodash.bind(func, { 'name': 'moe' }, 'hi'); + }, + 'teardown': 'function bind(){}' }) - .add('Underscore', function() { - _.bind(func, { 'name': 'moe' }, 'hi'); + .add('Underscore', { + 'fn': function() { + _.bind(func, { 'name': 'moe' }, 'hi'); + }, + 'teardown': 'function bind(){}' }) ); suites.push( Benchmark.Suite('bound call') - .add('Lo-Dash', function() { - lodashBoundNormal(); + .add('Lo-Dash', { + 'fn': function() { + lodashBoundNormal(); + }, + 'teardown': 'function bind(){}' }) - .add('Underscore', function() { - _boundNormal(); + .add('Underscore', { + 'fn': function() { + _boundNormal(); + }, + 'teardown': 'function bind(){}' }) ); suites.push( Benchmark.Suite('bound call with arguments') - .add('Lo-Dash', function() { - lodashBoundNormal('hi', '!'); + .add('Lo-Dash', { + 'fn': function() { + lodashBoundNormal('hi', '!'); + }, + 'teardown': 'function bind(){}' }) - .add('Underscore', function() { - _boundNormal('hi', '!'); + .add('Underscore', { + 'fn': function() { + _boundNormal('hi', '!'); + }, + 'teardown': 'function bind(){}' }) ); suites.push( Benchmark.Suite('bound and partially applied call (uses native `Function#bind` if available)') - .add('Lo-Dash', function() { - lodashBoundPartial(); + .add('Lo-Dash', { + 'fn': function() { + lodashBoundPartial(); + }, + 'teardown': 'function bind(){}' }) - .add('Underscore', function() { - _boundPartial(); + .add('Underscore', { + 'fn': function() { + _boundPartial(); + }, + 'teardown': 'function bind(){}' }) ); suites.push( Benchmark.Suite('bound and partially applied call with arguments (uses native `Function#bind` if available)') - .add('Lo-Dash', function() { - lodashBoundPartial('!'); + .add('Lo-Dash', { + 'fn': function() { + lodashBoundPartial('!'); + }, + 'teardown': 'function bind(){}' }) - .add('Underscore', function() { - _boundPartial('!'); + .add('Underscore', { + 'fn': function() { + _boundPartial('!'); + }, + 'teardown': 'function bind(){}' }) ); suites.push( Benchmark.Suite('bound and called in a `new` expression, i.e. `new bound` (edge case)') - .add('Lo-Dash', function() { - new lodashBoundCtor(); + .add('Lo-Dash', { + 'fn': function() { + new lodashBoundCtor(); + }, + 'teardown': 'function bind(){}' }) - .add('Underscore', function() { - new _boundCtor(); + .add('Underscore', { + 'fn': function() { + new _boundCtor(); + }, + 'teardown': 'function bind(){}' }) ); @@ -397,13 +451,13 @@ 'fn': function() { lodash.bindAll.apply(lodash, [bindAllObjects.pop()].concat(funcNames)); }, - 'teardown': 'function isBindAll(){}' + 'teardown': 'function bindAll(){}' }) .add('Underscore', { 'fn': function() { _.bindAll.apply(_, [bindAllObjects.pop()].concat(funcNames)); }, - 'teardown': 'function isBindAll(){}' + 'teardown': 'function bindAll(){}' }) ); @@ -413,13 +467,13 @@ 'fn': function() { lodash.bindAll(bindAllObjects.pop()); }, - 'teardown': 'function isBindAll(){}' + 'teardown': 'function bindAll(){}' }) .add('Underscore', { 'fn': function() { _.bindAll(bindAllObjects.pop()); }, - 'teardown': 'function isBindAll(){}' + 'teardown': 'function bindAll(){}' }) ); @@ -607,21 +661,33 @@ suites.push( Benchmark.Suite('`_.groupBy` with `property` name iterating an array') - .add('Lo-Dash', function() { - lodash.groupBy(words, 'length'); + .add('Lo-Dash', { + 'fn': function() { + lodash.groupBy(words, 'length'); + }, + 'teardown': 'function groupBy(){}' }) - .add('Underscore', function() { - _.groupBy(words, 'length'); + .add('Underscore', { + 'fn': function() { + _.groupBy(words, 'length'); + }, + 'teardown': 'function groupBy(){}' }) ); suites.push( Benchmark.Suite('`_.groupBy` with `callback` iterating an object') - .add('Lo-Dash', function() { - lodash.groupBy(wordToNumber, function(num) { return num >> 1; }); + .add('Lo-Dash', { + 'fn': function() { + lodash.groupBy(wordToNumber, function(num) { return num >> 1; }); + }, + 'teardown': 'function groupBy(){}' }) - .add('Underscore', function() { - _.groupBy(wordToNumber, function(num) { return num >> 1; }); + .add('Underscore', { + 'fn': function() { + _.groupBy(wordToNumber, function(num) { return num >> 1; }); + }, + 'teardown': 'function groupBy(){}' }) ); @@ -663,51 +729,81 @@ suites.push( Benchmark.Suite('`_.isEqual` comparing primitives and objects (edge case)') - .add('Lo-Dash', function() { - lodash.isEqual(objectOfPrimitives, objectOfObjects); + .add('Lo-Dash', { + 'fn': function() { + lodash.isEqual(objectOfPrimitives, objectOfObjects); + }, + 'teardown': 'function isEqual(){}' }) - .add('Underscore', function() { - _.isEqual(objectOfPrimitives, objectOfObjects); + .add('Underscore', { + 'fn': function() { + _.isEqual(objectOfPrimitives, objectOfObjects); + }, + 'teardown': 'function isEqual(){}' }) ); suites.push( Benchmark.Suite('`_.isEqual` comparing arrays') - .add('Lo-Dash', function() { - lodash.isEqual(numbers, numbers2); + .add('Lo-Dash', { + 'fn': function() { + lodash.isEqual(numbers, numbers2); + }, + 'teardown': 'function isEqual(){}' }) - .add('Underscore', function() { - _.isEqual(numbers, numbers2); + .add('Underscore', { + 'fn': function() { + _.isEqual(numbers, numbers2); + }, + 'teardown': 'function isEqual(){}' }) ); suites.push( Benchmark.Suite('`_.isEqual` comparing nested arrays') - .add('Lo-Dash', function() { - lodash.isEqual(nestedNumbers, nestedNumbers2); + .add('Lo-Dash', { + 'fn': function() { + lodash.isEqual(nestedNumbers, nestedNumbers2); + }, + 'teardown': 'function isEqual(){}' }) - .add('Underscore', function() { - _.isEqual(nestedNumbers, nestedNumbers2); + .add('Underscore', { + 'fn': function() { + _.isEqual(nestedNumbers, nestedNumbers2); + }, + 'teardown': 'function isEqual(){}' }) ); suites.push( Benchmark.Suite('`_.isEqual` comparing arrays of objects') - .add('Lo-Dash', function() { - lodash.isEqual(objects, objects2); + .add('Lo-Dash', { + 'fn': function() { + lodash.isEqual(objects, objects2); + }, + 'teardown': 'function isEqual(){}' }) - .add('Underscore', function() { - _.isEqual(objects, objects2); + .add('Underscore', { + 'fn': function() { + _.isEqual(objects, objects2); + }, + 'teardown': 'function isEqual(){}' }) ); suites.push( Benchmark.Suite('`_.isEqual` comparing objects') - .add('Lo-Dash', function() { - lodash.isEqual(object, object2); + .add('Lo-Dash', { + 'fn': function() { + lodash.isEqual(object, object2); + }, + 'teardown': 'function isEqual(){}' }) - .add('Underscore', function() { - _.isEqual(object, object2); + .add('Underscore', { + 'fn': function() { + _.isEqual(object, object2); + }, + 'teardown': 'function isEqual(){}' }) ); @@ -885,11 +981,17 @@ suites.push( Benchmark.Suite('`_.sortBy` with `property` name') - .add('Lo-Dash', function() { - lodash.sortBy(words, 'length'); + .add('Lo-Dash', { + 'fn': function() { + lodash.sortBy(words, 'length'); + }, + 'teardown': 'function groupBy(){}' }) - .add('Underscore', function() { - _.sortBy(words, 'length'); + .add('Underscore', { + 'fn': function() { + _.sortBy(words, 'length'); + }, + 'teardown': 'function groupBy(){}' }) ); @@ -907,15 +1009,21 @@ suites.push( Benchmark.Suite('`_.sortedIndex` with `callback`') - .add('Lo-Dash', function() { - lodash.sortedIndex(words, 'twenty-five', function(value) { - return wordToNumber[value]; - }); + .add('Lo-Dash', { + 'fn': function() { + lodash.sortedIndex(words, 'twenty-five', function(value) { + return wordToNumber[value]; + }); + }, + 'teardown': 'function groupBy(){}' }) - .add('Underscore', function() { - _.sortedIndex(words, 'twenty-five', function(value) { - return wordToNumber[value]; - }); + .add('Underscore', { + 'fn': function() { + _.sortedIndex(words, 'twenty-five', function(value) { + return wordToNumber[value]; + }); + }, + 'teardown': 'function groupBy(){}' }) ); @@ -923,61 +1031,97 @@ suites.push( Benchmark.Suite('`_.template` without "evaluate" delimiters (slow path)') - .add('Lo-Dash', function() { - lodash.template(tpl, tplData); + .add('Lo-Dash', { + 'fn': function() { + lodash.template(tpl, tplData); + }, + 'teardown': 'function template(){}' }) - .add('Underscore', function() { - _.template(tpl, tplData); + .add('Underscore', { + 'fn': function() { + _.template(tpl, tplData); + }, + 'teardown': 'function template(){}' }) ); suites.push( Benchmark.Suite('`_.template` with "evaluate" delimiters (slow path)') - .add('Lo-Dash', function() { - lodash.template(tplWithEvaluate, tplData); + .add('Lo-Dash', { + 'fn': function() { + lodash.template(tplWithEvaluate, tplData); + }, + 'teardown': 'function template(){}' }) - .add('Underscore', function() { - _.template(tplWithEvaluate, tplData); + .add('Underscore', { + 'fn': function() { + _.template(tplWithEvaluate, tplData); + }, + 'teardown': 'function template(){}' }) ); suites.push( Benchmark.Suite('compiled template without "evaluate" delimiters') - .add('Lo-Dash', function() { - lodashTpl(tplData); + .add('Lo-Dash', { + 'fn': function() { + lodashTpl(tplData); + }, + 'teardown': 'function template(){}' }) - .add('Underscore', function() { - _tpl(tplData); + .add('Underscore', { + 'fn': function() { + _tpl(tplData); + }, + 'teardown': 'function template(){}' }) ); suites.push( Benchmark.Suite('compiled template with "evaluate" delimiters') - .add('Lo-Dash', function() { - lodashTplWithEvaluate(tplData); + .add('Lo-Dash', { + 'fn': function() { + lodashTplWithEvaluate(tplData); + }, + 'teardown': 'function template(){}' }) - .add('Underscore', function() { - _tplWithEvaluate(tplData); + .add('Underscore', { + 'fn': function() { + _tplWithEvaluate(tplData); + }, + 'teardown': 'function template(){}' }) ); suites.push( Benchmark.Suite('compiled template without a with-statement or "evaluate" delimiters') - .add('Lo-Dash', function() { - lodashTplVerbose(tplData); + .add('Lo-Dash', { + 'fn': function() { + lodashTplVerbose(tplData); + }, + 'teardown': 'function template(){}' }) - .add('Underscore', function() { - _tplVerbose(tplData); + .add('Underscore', { + 'fn': function() { + _tplVerbose(tplData); + }, + 'teardown': 'function template(){}' }) ); suites.push( Benchmark.Suite('compiled template without a with-statement using "evaluate" delimiters') - .add('Lo-Dash', function() { - lodashTplVerboseWithEvaluate(tplData); + .add('Lo-Dash', { + 'fn': function() { + lodashTplVerboseWithEvaluate(tplData); + }, + 'teardown': 'function template(){}' }) - .add('Underscore', function() { - _tplVerboseWithEvaluate(tplData); + .add('Underscore', { + 'fn': function() { + _tplVerboseWithEvaluate(tplData); + }, + 'teardown': 'function template(){}' }) ); @@ -1084,8 +1228,11 @@ if (Benchmark.platform + '') { log(Benchmark.platform + ''); } - // start suites - log('\nSit back and relax, this may take a while.'); - suites[0].run(); + // in the browser, expose `run` to be called later + if (window.document) { + window.run = run; + } else { + run(); + } }(typeof global == 'object' && global || this)); diff --git a/vendor/platform.js/LICENSE.txt b/vendor/platform.js/LICENSE.txt new file mode 100644 index 000000000..dadad22fa --- /dev/null +++ b/vendor/platform.js/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright 2011-2012 John-David Dalton + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/vendor/platform.js/README.md b/vendor/platform.js/README.md new file mode 100644 index 000000000..4e5c08a90 --- /dev/null +++ b/vendor/platform.js/README.md @@ -0,0 +1,99 @@ +# Platform.js v1.0.0-pre + +A platform detection library that works on nearly all JavaScript platforms1. + +## Disclaimer + +Platform.js is for informational purposes only and **not** intended as a substitution for [feature detection/inference](http://allyoucanleet.com/post/18087210413/feature-testing-costs#screencast2) checks. + +## BestieJS + +Platform.js is part of the BestieJS *"Best in Class"* module collection. This means we promote solid browser/environment support, ES5 precedents, unit testing, and plenty of documentation. + +## Documentation + +The documentation for Platform.js can be viewed here: [/doc/README.md](https://github.com/bestiejs/platform.js/blob/master/doc/README.md#readme) + +For a list of upcoming features, check out our [roadmap](https://github.com/bestiejs/platform.js/wiki/Roadmap). + +## Installation and usage + +In a browser or Adobe AIR: + +~~~ html + +~~~ + +Via [npm](http://npmjs.org/): + +~~~ bash +npm install platform +~~~ + +In [Narwhal](http://narwhaljs.org/), [Node.js](http://nodejs.org/), and [RingoJS](http://ringojs.org/): + +~~~ js +var platform = require('platform'); +~~~ + +In [Rhino](http://www.mozilla.org/rhino/): + +~~~ js +load('platform.js'); +~~~ + +In an AMD loader like [RequireJS](http://requirejs.org/): + +~~~ js +require({ + 'paths': { + 'platform': 'path/to/platform' + } +}, +['platform'], function(platform) { + console.log(platform.name); +}); +~~~ + +Usage example: + +~~~ js +// on IE10 x86 platform preview running in IE7 compatibility mode on Windows 7 64 bit edition +platform.name; // 'IE' +platform.version; // '10.0' +platform.layout; // 'Trident' +platform.os; // 'Windows Server 2008 R2 / 7 x64' +platform.description; // 'IE 10.0 x86 (platform preview; running in IE 7 mode) on Windows Server 2008 R2 / 7 x64' + +// or on an iPad +platform.name; // 'Safari' +platform.version; // '5.1' +platform.product; // 'iPad' +platform.manufacturer; // 'Apple' +platform.layout; // 'WebKit' +platform.os; // 'iOS 5.0' +platform.description; // 'Safari 5.1 on Apple iPad (iOS 5.0)' + +// or parsing a given UA string +var info = platform.parse('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7.2; en; rv:2.0) Gecko/20100101 Firefox/4.0 Opera 11.52'); +info.name; // 'Opera' +info.version; // '11.52' +info.layout; // 'Presto' +info.os; // 'Mac OS X 10.7.2' +info.description; // 'Opera 11.52 (identifying as Firefox 4.0) on Mac OS X 10.7.2' +~~~ + +## Footnotes + + 1. Platform.js has been tested in at least Adobe AIR 2.6, Chrome 5-15, Firefox 1.5-8, IE 6-10, Opera 9.25-11.52, Safari 2-5.1.1, Node.js 0.4.8-0.6.1, Narwhal 0.3.2, RingoJS 0.7-0.8, and Rhino 1.7RC3. + + +## Author + +* [John-David Dalton](http://allyoucanleet.com/) + [![twitter/jdalton](http://gravatar.com/avatar/299a3d891ff1920b69c364d061007043?s=70)](https://twitter.com/jdalton "Follow @jdalton on Twitter") + +## Contributors + +* [Mathias Bynens](http://mathiasbynens.be/) + [![twitter/mathias](http://gravatar.com/avatar/24e08a9ea84deb17ae121074d0f17125?s=70)](https://twitter.com/mathias "Follow @mathias on Twitter") diff --git a/vendor/platform.js/platform.js b/vendor/platform.js/platform.js new file mode 100644 index 000000000..722fd3f12 --- /dev/null +++ b/vendor/platform.js/platform.js @@ -0,0 +1,922 @@ +/*! + * Platform.js v1.0.0-pre + * Copyright 2010-2012 John-David Dalton + * Available under MIT license + */ +;(function(window) { + 'use strict'; + + /** Backup possible window/global object */ + var oldWin = window; + + /** Detect free variable `exports` */ + var freeExports = typeof exports == 'object' && exports; + + /** Detect free variable `global` */ + var freeGlobal = typeof global == 'object' && global && + (global == global.global ? (window = global) : global); + + /** Opera regexp */ + var reOpera = /Opera/; + + /** Used to resolve a value's internal [[Class]] */ + var toString = {}.toString; + + /** Detect Java environment */ + var java = /Java/.test(getClassOf(window.java)) && window.java; + + /** A character to represent alpha */ + var alpha = java ? 'a' : '\u03b1'; + + /** A character to represent beta */ + var beta = java ? 'b' : '\u03b2'; + + /** Browser document object */ + var doc = window.document || {}; + + /** Used to check for own properties of an object */ + var hasOwnProperty = {}.hasOwnProperty; + + /** Browser navigator object */ + var nav = window.navigator || {}; + + /** + * Detect Opera browser + * http://www.howtocreate.co.uk/operaStuff/operaObject.html + * http://dev.opera.com/articles/view/opera-mini-web-content-authoring-guidelines/#operamini + */ + var opera = window.operamini || window.opera; + + /** Opera [[Class]] */ + var operaClass = reOpera.test(operaClass = getClassOf(opera)) ? operaClass : (opera = null); + + /** Possible global object */ + var thisBinding = this; + + /** Browser user agent string */ + var userAgent = nav.userAgent || ''; + + /*--------------------------------------------------------------------------*/ + + /** + * Capitalizes a string value. + * + * @private + * @param {String} string The string to capitalize. + * @returns {String} The capitalized string. + */ + function capitalize(string) { + string = String(string); + return string.charAt(0).toUpperCase() + string.slice(1); + } + + /** + * An iteration utility for arrays and objects. + * + * @private + * @param {Array|Object} object The object to iterate over. + * @param {Function} callback The function called per iteration. + */ + function each(object, callback) { + var index = -1, + length = object.length; + + if (length == length >>> 0) { + while (++index < length) { + callback(object[index], index, object); + } + } else { + forOwn(object, callback); + } + } + + /** + * Trim and conditionally capitalize string values. + * + * @private + * @param {String} string The string to format. + * @returns {String} The formatted string. + */ + function format(string) { + string = trim(string); + return /^(?:webOS|i(?:OS|P))/.test(string) + ? string + : capitalize(string); + } + + /** + * Iterates over an object's own properties, executing the `callback` for each. + * + * @private + * @param {Object} object The object to iterate over. + * @param {Function} callback The function executed per own property. + */ + function forOwn(object, callback) { + for (var key in object) { + hasKey(object, key) && callback(object[key], key, object); + } + } + + /** + * Gets the internal [[Class]] of a value. + * + * @private + * @param {Mixed} value The value. + * @returns {String} The [[Class]]. + */ + function getClassOf(value) { + return value == null + ? capitalize(value) + : toString.call(value).slice(8, -1); + } + + /** + * Checks if an object has the specified key as a direct property. + * + * @private + * @param {Object} object The object to check. + * @param {String} key The key to check for. + * @returns {Boolean} Returns `true` if key is a direct property, else `false`. + */ + function hasKey() { + // lazy define for others (not as accurate) + hasKey = function(object, key) { + var parent = object != null && (object.constructor || Object).prototype; + return !!parent && key in Object(object) && !(key in parent && object[key] === parent[key]); + }; + // for modern browsers + if (getClassOf(hasOwnProperty) == 'Function') { + hasKey = function(object, key) { + return object != null && hasOwnProperty.call(object, key); + }; + } + // for Safari 2 + else if ({}.__proto__ == Object.prototype) { + hasKey = function(object, key) { + var result = false; + if (object != null) { + object = Object(object); + object.__proto__ = [object.__proto__, object.__proto__ = null, result = key in object][0]; + } + return result; + }; + } + return hasKey.apply(this, arguments); + } + + /** + * Host objects can return type values that are different from their actual + * data type. The objects we are concerned with usually return non-primitive + * types of object, function, or unknown. + * + * @private + * @param {Mixed} object The owner of the property. + * @param {String} property The property to check. + * @returns {Boolean} Returns `true` if the property value is a non-primitive, else `false`. + */ + function isHostType(object, property) { + var type = object != null ? typeof object[property] : 'number'; + return !/^(?:boolean|number|string|undefined)$/.test(type) && + (type == 'object' ? !!object[property] : true); + } + + /** + * Prepares a string for use in a RegExp constructor by making hyphens and + * spaces optional. + * + * @private + * @param {String} string The string to qualify. + * @returns {String} The qualified string. + */ + function qualify(string) { + return String(string).replace(/([ -])(?!$)/g, '$1?'); + } + + /** + * A bare-bones` Array#reduce` like utility function. + * + * @private + * @param {Array} array The array to iterate over. + * @param {Function} callback The function called per iteration. + * @param {Mixed} accumulator Initial value of the accumulator. + * @returns {Mixed} The accumulator. + */ + function reduce(array, callback) { + var accumulator = null; + each(array, function(value, index) { + accumulator = callback(accumulator, value, index, array); + }); + return accumulator; + } + + /** + * Removes leading and trailing whitespace from a string. + * + * @private + * @param {String} string The string to trim. + * @returns {String} The trimmed string. + */ + function trim(string) { + return String(string).replace(/^ +| +$/g, ''); + } + + /*--------------------------------------------------------------------------*/ + + /** + * Creates a new platform object. + * + * @memberOf platform + * @param {String} [ua = navigator.userAgent] The user agent string. + * @returns {Object} A platform object. + */ + function parse(ua) { + + ua || (ua = userAgent); + + /** Temporary variable used over the script's lifetime */ + var data; + + /** The CPU architecture */ + var arch = ua; + + /** Platform description array */ + var description = []; + + /** Platform alpha/beta indicator */ + var prerelease = null; + + /** A flag to indicate that environment features should be used to resolve the platform */ + var useFeatures = ua == userAgent; + + /** The browser/environment version */ + var version = useFeatures && opera && typeof opera.version == 'function' && opera.version(); + + /* Detectable layout engines (order is important) */ + var layout = getLayout([ + { 'label': 'WebKit', 'pattern': 'AppleWebKit' }, + 'iCab', + 'Presto', + 'NetFront', + 'Tasman', + 'Trident', + 'KHTML', + 'Gecko' + ]); + + /* Detectable browser names (order is important) */ + var name = getName([ + 'Adobe AIR', + 'Arora', + 'Avant Browser', + 'Camino', + 'Epiphany', + 'Fennec', + 'Flock', + 'Galeon', + 'GreenBrowser', + 'iCab', + 'Iceweasel', + 'Iron', + 'K-Meleon', + 'Konqueror', + 'Lunascape', + 'Maxthon', + 'Midori', + 'Nook Browser', + 'PhantomJS', + 'Raven', + 'Rekonq', + 'RockMelt', + 'SeaMonkey', + { 'label': 'Silk', 'pattern': '(?:Cloud9|Silk-Accelerated)' }, + 'Sleipnir', + 'SlimBrowser', + 'Sunrise', + 'Swiftfox', + 'WebPositive', + 'Opera Mini', + 'Opera', + 'Chrome', + { 'label': 'Chrome Mobile', 'pattern': 'CrMo' }, + { 'label': 'Firefox', 'pattern': '(?:Firefox|Minefield)' }, + { 'label': 'IE', 'pattern': 'MSIE' }, + 'Safari' + ]); + + /* Detectable products (order is important) */ + var product = getProduct([ + 'BlackBerry', + { 'label': 'Galaxy S', 'pattern': 'GT-I9000' }, + { 'label': 'Galaxy S2', 'pattern': 'GT-I9100' }, + 'Google TV', + 'iPad', + 'iPod', + 'iPhone', + 'Kindle', + { 'label': 'Kindle Fire', 'pattern': '(?:Cloud9|Silk-Accelerated)' }, + 'Nook', + 'PlayBook', + 'PlayStation Vita', + 'TouchPad', + 'Transformer', + 'Xoom' + ]); + + /* Detectable manufacturers */ + var manufacturer = getManufacturer({ + 'Apple': { 'iPad': 1, 'iPhone': 1, 'iPod': 1 }, + 'Amazon': { 'Kindle': 1, 'Kindle Fire': 1 }, + 'Asus': { 'Transformer': 1 }, + 'Barnes & Noble': { 'Nook': 1 }, + 'BlackBerry': { 'PlayBook': 1 }, + 'Google': { 'Google TV': 1 }, + 'HP': { 'TouchPad': 1 }, + 'LG': { }, + 'Motorola': { 'Xoom': 1 }, + 'Nokia': { }, + 'Samsung': { 'Galaxy S': 1, 'Galaxy S2': 1 }, + 'Sony': { 'PlayStation Vita': 1 } + }); + + /* Detectable OSes (order is important) */ + var os = getOS([ + 'Android', + 'CentOS', + 'Debian', + 'Fedora', + 'FreeBSD', + 'Gentoo', + 'Haiku', + 'Kubuntu', + 'Linux Mint', + 'Red Hat', + 'SuSE', + 'Ubuntu', + 'Xubuntu', + 'Cygwin', + 'Symbian OS', + 'hpwOS', + 'webOS ', + 'webOS', + 'Tablet OS', + 'Linux', + 'Mac OS X', + 'Macintosh', + 'Mac', + 'Windows 98;', + 'Windows ' + ]); + + /*------------------------------------------------------------------------*/ + + /** + * Picks the layout engine from an array of guesses. + * + * @private + * @param {Array} guesses An array of guesses. + * @returns {String|Null} The detected layout engine. + */ + function getLayout(guesses) { + return reduce(guesses, function(result, guess) { + return result || RegExp('\\b' + ( + guess.pattern || qualify(guess) + ) + '\\b', 'i').exec(ua) && (guess.label || guess); + }); + } + + /** + * Picks the manufacturer from an array of guesses. + * + * @private + * @param {Array} guesses An array of guesses. + * @returns {String|Null} The detected manufacturer. + */ + function getManufacturer(guesses) { + return reduce(guesses, function(result, value, key) { + // lookup the manufacturer by product or scan the UA for the manufacturer + return result || ( + value[product] || + value[0/*Opera 9.25 fix*/, /^[a-z]+(?: +[a-z]+\b)*/i.exec(product)] || + RegExp('\\b' + (key.pattern || qualify(key)) + '(?:\\b|\\w*\\d)', 'i').exec(ua) + ) && (key.label || key); + }); + } + + /** + * Picks the browser name from an array of guesses. + * + * @private + * @param {Array} guesses An array of guesses. + * @returns {String|Null} The detected browser name. + */ + function getName(guesses) { + return reduce(guesses, function(result, guess) { + return result || RegExp('\\b' + ( + guess.pattern || qualify(guess) + ) + '\\b', 'i').exec(ua) && (guess.label || guess); + }); + } + + /** + * Picks the OS name from an array of guesses. + * + * @private + * @param {Array} guesses An array of guesses. + * @returns {String|Null} The detected OS name. + */ + function getOS(guesses) { + return reduce(guesses, function(result, guess) { + var pattern = guess.pattern || qualify(guess); + if (!result && (result = + RegExp('\\b' + pattern + '(?:/[\\d.]+|[ \\w.]*)', 'i').exec(ua))) { + // platform tokens defined at + // http://msdn.microsoft.com/en-us/library/ms537503(VS.85).aspx + // http://web.archive.org/web/20081122053950/http://msdn.microsoft.com/en-us/library/ms537503(VS.85).aspx + data = { + '6.2': '8', + '6.1': 'Server 2008 R2 / 7', + '6.0': 'Server 2008 / Vista', + '5.2': 'Server 2003 / XP 64-bit', + '5.1': 'XP', + '5.01': '2000 SP1', + '5.0': '2000', + '4.0': 'NT', + '4.90': 'ME' + }; + // detect Windows version from platform tokens + if (/^Win/i.test(result) && + (data = data[0/*Opera 9.25 fix*/, /[\d.]+$/.exec(result)])) { + result = 'Windows ' + data; + } + // correct character case and cleanup + result = format(String(result) + .replace(RegExp(pattern, 'i'), guess.label || guess) + .replace(/ ce$/i, ' CE') + .replace(/hpw/i, 'web') + .replace(/Macintosh/, 'Mac OS') + .replace(/_PowerPC/i, ' OS') + .replace(/(OS X) [^ \d]+/i, '$1') + .replace(/\/(\d)/, ' $1') + .replace(/_/g, '.') + .replace(/(?: BePC|[ .]*fc[ \d.]+)$/i, '') + .replace(/x86\.64/gi, 'x86_64') + .split(' on ')[0]); + } + return result; + }); + } + + /** + * Picks the product name from an array of guesses. + * + * @private + * @param {Array} guesses An array of guesses. + * @returns {String|Null} The detected product name. + */ + function getProduct(guesses) { + return reduce(guesses, function(result, guess) { + var pattern = guess.pattern || qualify(guess); + if (!result && (result = + RegExp('\\b' + pattern + ' *\\d+[.\\w_]*', 'i').exec(ua) || + RegExp('\\b' + pattern + '(?:; *(?:[a-z]+[_-])?[a-z]+\\d+|[^ ();-]*)', 'i').exec(ua) + )) { + // split by forward slash and append product version if needed + if ((result = String(guess.label || result).split('/'))[1] && !/[\d.]+/.test(result[0])) { + result[0] += ' ' + result[1]; + } + // correct character case and cleanup + guess = guess.label || guess; + result = format(result[0] + .replace(RegExp(pattern, 'i'), guess) + .replace(RegExp('; *(?:' + guess + '[_-])?', 'i'), ' ') + .replace(RegExp('(' + guess + ')(\\w)', 'i'), '$1 $2')); + } + return result; + }); + } + + /** + * Resolves the version using an array of UA patterns. + * + * @private + * @param {Array} patterns An array of UA patterns. + * @returns {String|Null} The detected version. + */ + function getVersion(patterns) { + return reduce(patterns, function(result, pattern) { + return result || (RegExp(pattern + + '(?:-[\\d.]+/|(?: for [\\w-]+)?[ /-])([\\d.]+[^ ();/_-]*)', 'i').exec(ua) || 0)[1] || null; + }); + } + + /*------------------------------------------------------------------------*/ + + /** + * Return platform description when the platform object is coerced to a string. + * + * @name toString + * @memberOf platform + * @type Function + * @returns {String} The platform description. + */ + function toStringPlatform() { + return this.description || ''; + } + + /*------------------------------------------------------------------------*/ + + // convert layout to an array so we can add extra details + layout && (layout = [layout]); + + // detect product names that contain their manufacturer's name + if (manufacturer && !product) { + product = getProduct([manufacturer]); + } + // clean up Google TV + if ((data = /Google TV/.exec(product))) { + product = data[0]; + } + // detect simulators + if (/\bSimulator\b/i.test(ua)) { + product = (product ? product + ' ' : '') + 'Simulator'; + } + // detect iOS + if (/^iP/.test(product)) { + name || (name = 'Safari'); + os = 'iOS' + ((data = / OS ([\d_]+)/i.exec(ua)) + ? ' ' + data[1].replace(/_/g, '.') + : ''); + } + // detect Kubuntu + else if (name == 'Konqueror' && !/buntu/i.test(os)) { + os = 'Kubuntu'; + } + // detect Android browsers + else if (manufacturer && manufacturer != 'Google' && + /Chrome|Vita/.test(name + ';' + product)) { + name = 'Android Browser'; + os = /Android/.test(os) ? os : 'Android'; + } + // detect false positives for Firefox/Safari + else if (!name || (data = !/\bMinefield\b/i.test(ua) && /Firefox|Safari/.exec(name))) { + // escape the `/` for Firefox 1 + if (name && !product && /[\/,]|^[^(]+?\)/.test(ua.slice(ua.indexOf(data + '/') + 8))) { + // clear name of false positives + name = null; + } + // reassign a generic name + if ((data = product || manufacturer || os) && + (product || manufacturer || /Android|Symbian OS|Tablet OS|webOS/.test(os))) { + name = /[a-z]+(?: Hat)?/i.exec(/Android/.test(os) ? os : data) + ' Browser'; + } + } + // detect non-Opera versions (order is important) + if (!version) { + version = getVersion([ + '(?:Cloud9|CrMo|Opera ?Mini|Raven|Silk(?!/[\\d.]+$))', + 'Version', + qualify(name), + '(?:Firefox|Minefield|NetFront)' + ]); + } + // detect stubborn layout engines + if (layout == 'iCab' && parseFloat(version) > 3) { + layout = ['WebKit']; + } else if (data = + /Opera/.test(name) && 'Presto' || + /\b(?:Midori|Nook|Safari)\b/i.test(ua) && 'WebKit' || + !layout && /\bMSIE\b/i.test(ua) && (/^Mac/.test(os) ? 'Tasman' : 'Trident')) { + layout = [data]; + } + // leverage environment features + if (useFeatures) { + // detect server-side environments + // Rhino has a global function while others have a global object + if (isHostType(window, 'global')) { + if (java) { + data = java.lang.System; + arch = data.getProperty('os.arch'); + os = os || data.getProperty('os.name') + ' ' + data.getProperty('os.version'); + } + if (typeof exports == 'object' && exports) { + // if `thisBinding` is the [ModuleScope] + if (thisBinding == oldWin && typeof system == 'object' && (data = [system])[0]) { + os || (os = data[0].os || null); + try { + data[1] = require('ringo/engine').version; + version = data[1].join('.'); + name = 'RingoJS'; + } catch(e) { + if (data[0].global == freeGlobal) { + name = 'Narwhal'; + } + } + } else if (typeof process == 'object' && (data = process)) { + name = 'Node.js'; + arch = data.arch; + os = data.platform; + version = /[\d.]+/.exec(data.version)[0]; + } + } else if (getClassOf(window.environment) == 'Environment') { + name = 'Rhino'; + } + } + // detect Adobe AIR + else if (getClassOf(data = window.runtime) == 'ScriptBridgingProxyObject') { + name = 'Adobe AIR'; + os = data.flash.system.Capabilities.os; + } + // detect PhantomJS + else if (getClassOf(data = window.phantom) == 'RuntimeObject') { + name = 'PhantomJS'; + version = (data = data.version || null) && (data.major + '.' + data.minor + '.' + data.patch); + } + // detect IE compatibility modes + else if (typeof doc.documentMode == 'number' && (data = /\bTrident\/(\d+)/i.exec(ua))) { + // we're in compatibility mode when the Trident version + 4 doesn't + // equal the document mode + version = [version, doc.documentMode]; + if ((data = +data[1] + 4) != version[1]) { + description.push('IE ' + version[1] + ' mode'); + layout[1] = ''; + version[1] = data; + } + version = name == 'IE' ? String(version[1].toFixed(1)) : version[0]; + } + os = os && format(os); + } + // detect prerelease phases + if (version && (data = + /(?:[ab]|dp|pre|[ab]\d+pre)(?:\d+\+?)?$/i.exec(version) || + /(?:alpha|beta)(?: ?\d)?/i.exec(ua + ';' + (useFeatures && nav.appMinorVersion)) || + /\bMinefield\b/i.test(ua) && 'a')) { + prerelease = /b/i.test(data) ? 'beta' : 'alpha'; + version = version.replace(RegExp(data + '\\+?$'), '') + + (prerelease == 'beta' ? beta : alpha) + (/\d+\+?/.exec(data) || ''); + } + // rename code name "Fennec" + if (name == 'Fennec') { + name = 'Firefox Mobile'; + } + // obscure Maxthon's unreliable version + else if (name == 'Maxthon' && version) { + version = version.replace(/\.[\d.]+/, '.x'); + } + // detect Silk desktop/accelerated modes + else if (name == 'Silk') { + if (!/Mobi/i.test(ua)) { + os = 'Android'; + description.unshift('desktop mode'); + } + if (/Accelerated *= *true/i.test(ua)) { + description.unshift('accelerated'); + } + } + // detect Windows Phone desktop mode + else if (name == 'IE' && (data = (/; *(?:XBLWP|ZuneWP)(\d+)/i.exec(ua) || 0)[1])) { + name += ' Mobile'; + os = 'Windows Phone OS ' + data + '.x'; + description.unshift('desktop mode'); + } + // add mobile postfix + else if ((name == 'IE' || name && !product && !/Browser|Mobi/.test(name)) && + (os == 'Windows CE' || /Mobi/i.test(ua))) { + name += ' Mobile'; + } + // detect IE platform preview + else if (name == 'IE' && useFeatures && typeof external == 'object' && !external) { + description.unshift('platform preview'); + } + // detect BlackBerry OS version + // http://docs.blackberry.com/en/developers/deliverables/18169/HTTP_headers_sent_by_BB_Browser_1234911_11.jsp + else if (/BlackBerry/.test(product) && (data = + (RegExp(product.replace(/ +/g, ' *') + '/([.\\d]+)', 'i').exec(ua) || 0)[1] || + version)) { + os = 'Device Software ' + data; + version = null; + } + // detect Opera identifying/masking itself as another browser + // http://www.opera.com/support/kb/view/843/ + else if (this != forOwn && ( + (useFeatures && opera) || + (/Opera/.test(name) && /\b(?:MSIE|Firefox)\b/i.test(ua)) || + (name == 'Firefox' && /OS X (?:\d+\.){2,}/.test(os)) || + (name == 'IE' && ( + (os && !/^Win/.test(os) && version > 5.5) || + /Windows XP/.test(os) && version > 8 || + version == 8 && !/Trident/.test(ua) + )) + ) && !reOpera.test(data = parse.call(forOwn, ua.replace(reOpera, '') + ';')) && data.name) { + + // when "indentifying" the UA contains both Opera and the other browser's name + data = 'ing as ' + data.name + ((data = data.version) ? ' ' + data : ''); + if (reOpera.test(name)) { + if (/IE/.test(data) && os == 'Mac OS') { + os = null; + } + data = 'identify' + data; + } + // when "masking" the UA contains only the other browser's name + else { + data = 'mask' + data; + if (operaClass) { + name = format(operaClass.replace(/([a-z])([A-Z])/g, '$1 $2')); + } else { + name = 'Opera'; + } + if (/IE/.test(data)) { + os = null; + } + if (!useFeatures) { + version = null; + } + } + layout = ['Presto']; + description.push(data); + } + // detect WebKit Nightly and approximate Chrome/Safari versions + if ((data = (/\bAppleWebKit\/([\d.]+\+?)/i.exec(ua) || 0)[1])) { + // correct build for numeric comparison + // (e.g. "532.5" becomes "532.05") + data = [parseFloat(data.replace(/\.(\d)$/, '.0$1')), data]; + // nightly builds are postfixed with a `+` + if (name == 'Safari' && data[1].slice(-1) == '+') { + name = 'WebKit Nightly'; + prerelease = 'alpha'; + version = data[1].slice(0, -1); + } + // clear incorrect browser versions + else if (version == data[1] || + version == (/\bSafari\/([\d.]+\+?)/i.exec(ua) || 0)[1]) { + version = null; + } + // use the full Chrome version when available + data = [data[0], (/\bChrome\/([\d.]+)/i.exec(ua) || 0)[1]]; + + // detect JavaScriptCore + // http://stackoverflow.com/questions/6768474/how-can-i-detect-which-javascript-engine-v8-or-jsc-is-used-at-runtime-in-androi + if (!useFeatures || (/internal|\n/i.test(toString.toString()) && !data[1])) { + layout[1] = 'like Safari'; + data = (data = data[0], data < 400 ? 1 : data < 500 ? 2 : data < 526 ? 3 : data < 533 ? 4 : data < 534 ? '4+' : data < 535 ? 5 : '5'); + } else { + layout[1] = 'like Chrome'; + data = data[1] || (data = data[0], data < 530 ? 1 : data < 532 ? 2 : data < 532.05 ? 3 : data < 533 ? 4 : data < 534.03 ? 5 : data < 534.07 ? 6 : data < 534.10 ? 7 : data < 534.13 ? 8 : data < 534.16 ? 9 : data < 534.24 ? 10 : data < 534.30 ? 11 : data < 535.01 ? 12 : data < 535.02 ? '13+' : data < 535.07 ? 15 : data < 535.11 ? 16 : data < 535.19 ? 17 : data < 535.21 ? 18 : '19'); + } + // add the postfix of ".x" or "+" for approximate versions + layout[1] += ' ' + (data += typeof data == 'number' ? '.x' : /[.+]/.test(data) ? '' : '+'); + // obscure version for some Safari 1-2 releases + if (name == 'Safari' && (!version || parseInt(version) > 45)) { + version = data; + } + } + // strip incorrect OS versions + if (version && version.indexOf(data = /[\d.]+$/.exec(os)) == 0 && + ua.indexOf('/' + data + '-') > -1) { + os = trim(os.replace(data, '')); + } + // add layout engine + if (layout && !/Avant|Nook/.test(name) && ( + /Browser|Lunascape|Maxthon/.test(name) || + /^(?:Adobe|Arora|Midori|Phantom|Rekonq|Rock|Sleipnir|Web)/.test(name) && layout[1])) { + // don't add layout details to description if they are falsey + (data = layout[layout.length - 1]) && description.push(data); + } + // combine contextual information + if (description.length) { + description = ['(' + description.join('; ') + ')']; + } + // append manufacturer + if (manufacturer && product && product.indexOf(manufacturer) < 0) { + description.push('on ' + manufacturer); + } + // append product + if (product) { + description.push((/^on /.test(description[description.length -1]) ? '' : 'on ') + product); + } + // add browser/OS architecture + if ((data = /\b(?:AMD|IA|Win|WOW|x86_|x)64\b/i).test(arch) && !/\bi686\b/i.test(arch)) { + os = os && os + (data.test(os) ? '' : ' 64-bit'); + if (name && (/WOW64/i.test(ua) || + (useFeatures && /\w(?:86|32)$/.test(nav.cpuClass || nav.platform)))) { + description.unshift('32-bit'); + } + } + + ua || (ua = null); + + /*------------------------------------------------------------------------*/ + + /** + * The platform object. + * + * @name platform + * @type Object + */ + return { + + /** + * The browser/environment version. + * + * @memberOf platform + * @type String|Null + */ + 'version': name && version && (description.unshift(version), version), + + /** + * The name of the browser/environment. + * + * @memberOf platform + * @type String|Null + */ + 'name': name && (description.unshift(name), name), + + /** + * The name of the operating system. + * + * @memberOf platform + * @type String|Null + */ + 'os': os && (name && + !(os == os.split(' ')[0] && (os == name.split(' ')[0] || product)) && + description.push(product ? '(' + os + ')' : 'on ' + os), os), + + /** + * The platform description. + * + * @memberOf platform + * @type String|Null + */ + 'description': description.length ? description.join(' ') : ua, + + /** + * The name of the browser layout engine. + * + * @memberOf platform + * @type String|Null + */ + 'layout': layout && layout[0], + + /** + * The name of the product's manufacturer. + * + * @memberOf platform + * @type String|Null + */ + 'manufacturer': manufacturer, + + /** + * The alpha/beta release indicator. + * + * @memberOf platform + * @type String|Null + */ + 'prerelease': prerelease, + + /** + * The name of the product hosting the browser. + * + * @memberOf platform + * @type String|Null + */ + 'product': product, + + /** + * The browser's user agent string. + * + * @memberOf platform + * @type String|Null + */ + 'ua': ua, + + // parses a user agent string into a platform object + 'parse': parse, + + // returns the platform description + 'toString': toStringPlatform + }; + } + + /*--------------------------------------------------------------------------*/ + + // expose platform + // some AMD build optimizers, like r.js, check for specific condition patterns like the following: + if (typeof define == 'function' && typeof define.amd == 'object' && define.amd) { + // define as an anonymous module so, through path mapping, it can be aliased + define(function() { + return parse(); + }); + } + // check for `exports` after `define` in case a build optimizer adds an `exports` object + else if (freeExports) { + // in Narwhal, Node.js, or RingoJS + forOwn(parse(), function(value, key) { + freeExports[key] = value; + }); + } + // in a browser or Rhino + else { + // use square bracket notation so Closure Compiler won't munge `platform` + // http://code.google.com/closure/compiler/docs/api-tutorial3.html#export + window['platform'] = parse(); + } +}(this));