/*! * Platform.js v1.2.0 * Copyright 2010-2014 John-David Dalton * Available under MIT license */ ;(function() { 'use strict'; /** Used to determine if values are of the language type `Object` */ var objectTypes = { 'function': true, 'object': true }; /** Used as a reference to the global object */ var root = (objectTypes[typeof window] && window) || this; /** Backup possible global object */ var oldRoot = root; /** Detect free variable `exports` */ var freeExports = objectTypes[typeof exports] && exports; /** Detect free variable `module` */ var freeModule = objectTypes[typeof module] && module && !module.nodeType && module; /** Detect free variable `global` from Node.js or Browserified code and use it as `root` */ var freeGlobal = freeExports && freeModule && typeof global == 'object' && global; if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal || freeGlobal.self === freeGlobal)) { root = freeGlobal; } /** * Used as the maximum length of an array-like object. * See the [ES6 spec](http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength) * for more details. */ var maxSafeInteger = Math.pow(2, 53) - 1; /** Opera regexp */ var reOpera = /Opera/; /** Possible global object */ var thisBinding = this; /** Used for native method references */ var objectProto = Object.prototype; /** Used to check for own properties of an object */ var hasOwnProperty = objectProto.hasOwnProperty; /** Used to resolve the internal `[[Class]]` of values */ var toString = objectProto.toString; /*--------------------------------------------------------------------------*/ /** * 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); } /** * A utility function to clean up the OS name. * * @private * @param {string} os The OS name to clean up. * @param {string} [pattern] A `RegExp` pattern matching the OS name. * @param {string} [label] A label for the OS. */ function cleanupOS(os, pattern, label) { // 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 var data = { '6.3': '8.1', '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 (pattern && label && /^Win/i.test(os) && (data = data[0/*Opera 9.25 fix*/, /[\d.]+$/.exec(os)])) { os = 'Windows ' + data; } // correct character case and cleanup os = String(os); if (pattern && label) { os = os.replace(RegExp(pattern, 'i'), label); } os = format( os.replace(/ ce$/i, ' CE') .replace(/hpw/i, 'web') .replace(/Macintosh/, 'Mac OS') .replace(/_PowerPC/i, ' OS') .replace(/(OS X) [^ \d]+/i, '$1') .replace(/Mac (OS X)/, '$1') .replace(/\/(\d)/, ' $1') .replace(/_/g, '.') .replace(/(?: BePC|[ .]*fc[ \d.]+)$/i, '') .replace(/x86\.64/gi, 'x86_64') .replace(/(Windows Phone)(?! OS)/, '$1 OS') .split(' on ')[0] ); return os; } /** * 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 ? object.length : 0; if (typeof length == 'number' && length > -1 && length <= maxSafeInteger) { 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) { if (hasOwnProperty.call(object, key)) { callback(object[key], key, object); } } } /** * Gets the internal `[[Class]]` of a value. * * @private * @param {*} value The value. * @returns {string} The `[[Class]]`. */ function getClassOf(value) { return value == null ? capitalize(value) : toString.call(value).slice(8, -1); } /** * 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 {*} 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` 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. * @returns {*} The accumulated result. */ 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 {Object|string} [ua=navigator.userAgent] The user agent string or * context object. * @returns {Object} A platform object. */ function parse(ua) { /** The environment context object */ var context = root; /** Used to flag when a custom context is provided */ var isCustomContext = ua && typeof ua == 'object' && getClassOf(ua) != 'String'; // juggle arguments if (isCustomContext) { context = ua; ua = null; } /** Browser navigator object */ var nav = context.navigator || {}; /** Browser user agent string */ var userAgent = nav.userAgent || ''; ua || (ua = userAgent); /** Used to flag when `thisBinding` is the [ModuleScope] */ var isModuleScope = isCustomContext || thisBinding == oldRoot; /** Used to detect if browser is like Chrome */ var likeChrome = isCustomContext ? !!nav.likeChrome : /\bChrome\b/.test(ua) && !/internal|\n/i.test(toString.toString()); /** Internal `[[Class]]` value shortcuts */ var objectClass = 'Object', airRuntimeClass = isCustomContext ? objectClass : 'ScriptBridgingProxyObject', enviroClass = isCustomContext ? objectClass : 'Environment', javaClass = (isCustomContext && context.java) ? 'JavaPackage' : getClassOf(context.java), phantomClass = isCustomContext ? objectClass : 'RuntimeObject'; /** Detect Java environment */ var java = /Java/.test(javaClass) && context.java; /** Detect Rhino */ var rhino = java && getClassOf(context.environment) == enviroClass; /** 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 = context.document || {}; /** * Detect Opera browser (Presto-based) * http://www.howtocreate.co.uk/operaStuff/operaObject.html * http://dev.opera.com/articles/view/opera-mini-web-content-authoring-guidelines/#operamini */ var opera = context.operamini || context.opera; /** Opera `[[Class]]` */ var operaClass = reOpera.test(operaClass = (isCustomContext && opera) ? opera['[[Class]]'] : getClassOf(opera)) ? operaClass : (opera = null); /*------------------------------------------------------------------------*/ /** 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(); /** A flag to indicate if the OS ends with "/ Version" */ var isSpecialCasedOS; /* 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', 'Breach', 'Camino', 'Epiphany', 'Fennec', 'Flock', 'Galeon', 'GreenBrowser', 'iCab', 'Iceweasel', { 'label': 'SRWare Iron', 'pattern': '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', { 'label': 'Opera Mini', 'pattern': 'OPiOS' }, 'Opera', { 'label': 'Opera', 'pattern': 'OPR' }, 'Chrome', { 'label': 'Chrome Mobile', 'pattern': '(?:CriOS|CrMo)' }, { 'label': 'Firefox', 'pattern': '(?:Firefox|Minefield)' }, { 'label': 'IE', 'pattern': 'MSIE' }, 'Safari' ]); /* Detectable products (order is important) */ var product = getProduct([ { 'label': 'BlackBerry', 'pattern': 'BB10' }, 'BlackBerry', { 'label': 'Galaxy S', 'pattern': 'GT-I9000' }, { 'label': 'Galaxy S2', 'pattern': 'GT-I9100' }, { 'label': 'Galaxy S3', 'pattern': 'GT-I9300' }, { 'label': 'Galaxy S4', 'pattern': 'GT-I9500' }, 'Google TV', 'iPad', 'iPod', 'iPhone', 'Kindle', { 'label': 'Kindle Fire', 'pattern': '(?:Cloud9|Silk-Accelerated)' }, 'Nook', 'PlayBook', 'PlayStation 4', 'PlayStation 3', 'PlayStation Vita', 'TouchPad', 'Transformer', { 'label': 'Wii U', 'pattern': 'WiiU' }, 'Wii', 'Xbox One', { 'label': 'Xbox 360', 'pattern': 'Xbox' }, '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 }, 'HTC': {}, 'LG': {}, 'Microsoft': { 'Xbox': 1, 'Xbox One': 1 }, 'Motorola': { 'Xoom': 1 }, 'Nintendo': { 'Wii U': 1, 'Wii': 1 }, 'Nokia': {}, 'Samsung': { 'Galaxy S': 1, 'Galaxy S2': 1, 'Galaxy S3': 1, 'Galaxy S4': 1 }, 'Sony': { 'PlayStation 4': 1, 'PlayStation 3': 1, '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 {null|string} 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 object of guesses. * @returns {null|string} 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' + qualify(key) + '(?:\\b|\\w*\\d)', 'i').exec(ua) ) && key; }); } /** * Picks the browser name from an array of guesses. * * @private * @param {Array} guesses An array of guesses. * @returns {null|string} 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 {null|string} 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) )) { result = cleanupOS(result, pattern, guess.label || guess); } return result; }); } /** * Picks the product name from an array of guesses. * * @private * @param {Array} guesses An array of guesses. * @returns {null|string} 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 && !RegExp(pattern, 'i').test(guess.label)) ? 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 {null|string} 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; }); } /** * Returns `platform.description` when the platform object is coerced to a string. * * @name toString * @memberOf platform * @returns {string} Returns `platform.description` if available, else an empty string. */ 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 Opera Mini 8+ running in Turbo / Uncompressed mode on iOS if (name == 'Opera Mini' && /OPiOS/.test(ua)) { description.push('running in Turbo / Uncompressed mode'); } // 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/.test(name) && !/Mobile Safari/.test(ua)) || /Vita/.test(product))) { name = 'Android Browser'; os = /Android/.test(os) ? os : 'Android'; } // detect false positives for Firefox/Safari else if (!name || (data = !/\bMinefield\b|\(Android;/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 Firefox OS if ((data = /\((Mobile|Tablet).*?Firefox/i.exec(ua)) && data[1]) { os = 'Firefox OS'; if (!product) { product = data[1]; } } // detect non-Opera versions (order is important) if (!version) { version = getVersion([ '(?:Cloud9|CriOS|CrMo|Iron|Opera ?Mini|OPiOS|OPR|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) && (/OPR/.test(ua) ? 'Blink' : 'Presto') || /\b(?:Midori|Nook|Safari)\b/i.test(ua) && 'WebKit' || !layout && /\bMSIE\b/i.test(ua) && (os == 'Mac OS' ? 'Tasman' : 'Trident') )) { layout = [data]; } // detect NetFront on PlayStation else if (/PlayStation(?! Vita)/i.test(name) && layout == 'WebKit') { layout = ['NetFront']; } // detect IE 11 and above if (name != 'IE' && layout == 'Trident' && (data = /\brv:([\d.]+)/.exec(ua))) { if (name) { description.push('identifying as ' + name + (version ? ' ' + version : '')); } name = 'IE'; version = data[1]; } // leverage environment features if (useFeatures) { // detect server-side environments // Rhino has a global function while others have a global object if (isHostType(context, 'global')) { if (java) { data = java.lang.System; arch = data.getProperty('os.arch'); os = os || data.getProperty('os.name') + ' ' + data.getProperty('os.version'); } if (isModuleScope && isHostType(context, 'system') && (data = [context.system])[0]) { os || (os = data[0].os || null); try { data[1] = context.require('ringo/engine').version; version = data[1].join('.'); name = 'RingoJS'; } catch(e) { if (data[0].global.system == context.system) { name = 'Narwhal'; } } } else if (typeof context.process == 'object' && (data = context.process)) { name = 'Node.js'; arch = data.arch; os = data.platform; version = /[\d.]+/.exec(data.version)[0]; } else if (rhino) { name = 'Rhino'; } } // detect Adobe AIR else if (getClassOf((data = context.runtime)) == airRuntimeClass) { name = 'Adobe AIR'; os = data.flash.system.Capabilities.os; } // detect PhantomJS else if (getClassOf((data = context.phantom)) == phantomClass) { 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 && (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) || ''); } // detect Firefox Mobile if (name == 'Fennec' || name == 'Firefox' && /Android|Firefox OS/.test(os)) { 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'); } // detect Xbox 360 and Xbox One else if (/Xbox/i.test(product)) { os = null; if (product == 'Xbox 360' && /IEMobile/.test(ua)) { description.unshift('mobile mode'); } } // add mobile postfix else if ((name == 'Chrome' || 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 && context.external === null) { 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) || /BB10/.test(ua)) && (data = (RegExp(product.replace(/ +/g, ' *') + '/([.\\d]+)', 'i').exec(ua) || 0)[1] || version )) { data = [data, /BB10/.test(ua)]; os = (data[1] ? (product = null, manufacturer = 'BlackBerry') : 'Device Software') + ' ' + data[0]; version = null; } // detect Opera identifying/masking itself as another browser // http://www.opera.com/support/kb/view/843/ else if (this != forOwn && ( product != 'Wii' && ( (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 == (data[2] = (/\bSafari\/([\d.]+\+?)/i.exec(ua) || 0)[1])) { version = null; } // use the full Chrome version when available data[1] = (/\bChrome\/([\d.]+)/i.exec(ua) || 0)[1]; // detect Blink layout engine if (data[0] == 537.36 && data[2] == 537.36 && parseFloat(data[1]) >= 28) { layout = ['Blink']; } // 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 || (!likeChrome && !data[1])) { layout && (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 : data < 537 ? 6 : data < 538 ? 7 : '7'); } else { layout && (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 < 536.05 ? 18 : data < 536.10 ? 19 : data < 537.01 ? 20 : data < 537.11 ? '21+' : data < 537.13 ? 23 : data < 537.18 ? 24 : data < 537.24 ? 25 : data < 537.36 ? 26 : layout != 'Blink' ? '27' : '28'); } // add the postfix of ".x" or "+" for approximate versions layout && (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; } } // detect Opera desktop modes if (name == 'Opera' && (data = /(?:zbov|zvav)$/.exec(os))) { name += ' '; description.unshift('desktop mode'); if (data == 'zvav') { name += 'Mini'; version = null; } else { name += 'Mobile'; } } // detect Chrome desktop mode else if (name == 'Safari' && /Chrome/.exec(layout && layout[1])) { description.unshift('desktop mode'); name = 'Chrome Mobile'; version = null; if (/OS X/.test(os)) { manufacturer = 'Apple'; os = 'iOS 4.3+'; } else { os = null; } } // 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|Breach|Midori|Opera|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); } // parse OS into an object if (os) { data = / ([\d.+]+)$/.exec(os); isSpecialCasedOS = data && os.charAt(os.length - data[0].length - 1) == '/'; os = { 'architecture': 32, 'family': (data && !isSpecialCasedOS) ? os.replace(data[0], '') : os, 'version': data ? data[1] : null, 'toString': function() { var version = this.version; return this.family + ((version && !isSpecialCasedOS) ? ' ' + version : '') + (this.architecture == 64 ? ' 64-bit' : ''); } }; } // add browser/OS architecture if ((data = /\b(?:AMD|IA|Win|WOW|x86_|x)64\b/i.exec(arch)) && !/\bi686\b/i.test(arch)) { if (os) { os.architecture = 64; os.family = os.family.replace(RegExp(' *' + data), ''); } if (name && (/WOW64/i.test(ua) || (useFeatures && /\w(?:86|32)$/.test(nav.cpuClass || nav.platform) && !/^win32$/i.test(nav.platform)))) { description.unshift('32-bit'); } } ua || (ua = null); /*------------------------------------------------------------------------*/ /** * The platform object. * * @name platform * @type Object */ var platform = {}; /** * The platform description. * * @memberOf platform * @type string|null */ platform.description = ua; /** * The name of the browser's layout engine. * * @memberOf platform * @type string|null */ platform.layout = layout && layout[0]; /** * The name of the product's manufacturer. * * @memberOf platform * @type string|null */ platform.manufacturer = manufacturer; /** * The name of the browser/environment. * * @memberOf platform * @type string|null */ platform.name = name; /** * The alpha/beta release indicator. * * @memberOf platform * @type string|null */ platform.prerelease = prerelease; /** * The name of the product hosting the browser. * * @memberOf platform * @type string|null */ platform.product = product; /** * The browser's user agent string. * * @memberOf platform * @type string|null */ platform.ua = ua; /** * The browser/environment version. * * @memberOf platform * @type string|null */ platform.version = name && version; /** * The name of the operating system. * * @memberOf platform * @type Object */ platform.os = os || { /** * The CPU architecture the OS is built for. * * @memberOf platform.os * @type number|null */ 'architecture': null, /** * The family of the OS. * * Common values include: * "Windows", "Windows Server 2008 R2 / 7", "Windows Server 2008 / Vista", * "Windows XP", "OS X", "Ubuntu", "Debian", "Fedora", "Red Hat", "SuSE", * "Android", "iOS" and "Windows Phone OS" * * @memberOf platform.os * @type string|null */ 'family': null, /** * The version of the OS. * * @memberOf platform.os * @type string|null */ 'version': null, /** * Returns the OS string. * * @memberOf platform.os * @returns {string} The OS string. */ 'toString': function() { return 'null'; } }; platform.parse = parse; platform.toString = toStringPlatform; if (platform.version) { description.unshift(version); } if (platform.name) { description.unshift(name); } if (os && name && !(os == String(os).split(' ')[0] && (os == name.split(' ')[0] || product))) { description.push(product ? '(' + os + ')' : 'on ' + os); } if (description.length) { platform.description = description.join(' '); } return platform; } /*--------------------------------------------------------------------------*/ // export platform // some AMD build optimizers, like r.js, check for 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 && freeModule) { // in Narwhal, Node.js, Rhino -require, or RingoJS forOwn(parse(), function(value, key) { freeExports[key] = value; }); } // in a browser or Rhino else { root.platform = parse(); } }.call(this));