From 7df5a2c0a68030d51170aca2c3d3195487d2d1ab Mon Sep 17 00:00:00 2001 From: yiminghe Date: Wed, 3 Sep 2014 14:22:43 +0800 Subject: [PATCH] init --- .gitignore | 20 +++ .jscs.json | 25 +++ .jscsrc | 27 +++ .jshintrc | 28 +++ .npmignore | 21 +++ .travis.yml | 15 ++ README.md | 13 +- bower.json | 16 ++ build/ua-debug.js | 415 +++++++++++++++++++++++++++++++++++++++++++ build/ua.js | 1 + gulpfile.js | 51 ++++++ index.js | 2 + lib/ua.js | 411 ++++++++++++++++++++++++++++++++++++++++++ package.json | 62 +++++++ server.js | 45 +++++ tests/runner.html | 60 +++++++ tests/specs/index.js | 21 +++ 17 files changed, 1231 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 .jscs.json create mode 100644 .jscsrc create mode 100644 .jshintrc create mode 100644 .npmignore create mode 100644 .travis.yml create mode 100644 bower.json create mode 100644 build/ua-debug.js create mode 100644 build/ua.js create mode 100644 gulpfile.js create mode 100644 index.js create mode 100644 lib/ua.js create mode 100644 package.json create mode 100644 server.js create mode 100644 tests/runner.html create mode 100644 tests/specs/index.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dea54e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +bower_components/ +nohup.out +*.iml +.idea/ +.ipr +.iws +*~ +~* +*.diff +*.log +*.patch +*.bak +.DS_Store +Thumbs.db +.project +.*proj +.svn/ +*.swp +out/ +node_modules/ \ No newline at end of file diff --git a/.jscs.json b/.jscs.json new file mode 100644 index 0000000..deb818e --- /dev/null +++ b/.jscs.json @@ -0,0 +1,25 @@ +{ + "requireCurlyBraces": ["if", "else", "for", "while", "do", "try", "catch"], + "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], + "requireSpacesInFunctionExpression": { + "beforeOpeningCurlyBrace": true + }, + "disallowSpacesInsideArrayBrackets": true, + "disallowSpacesInsideObjectBrackets": true, + "disallowSpacesInsideParentheses": true, + "disallowQuotedKeysInObjects": "allButReserved", + "disallowSpaceAfterObjectKeys": true, + "disallowLeftStickedOperators": [ "?", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<=" ], + "disallowRightStickedOperators": [ "?", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], + "requireRightStickedOperators": [ "!" ], + "requireLeftStickedOperators": [ "," ], + "disallowKeywords": [ "with" ], + "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~"], + "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], + "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + "requireSpaceAfterBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + "disallowMultipleLineBreaks": true, + "disallowKeywordsOnNewLine": ["else"], + "safeContextKeyword": "self", + "excludeFiles": ["**/*-tpl.js", "**/*-xtpl.js", "**/parser.js", "**/parser.js","**/modules.js"] +} \ No newline at end of file diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..75c7395 --- /dev/null +++ b/.jscsrc @@ -0,0 +1,27 @@ +{ + "requireCurlyBraces": ["if", "else", "for", "while", "do", "try", "catch"], + "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], + "requireSpacesInFunctionExpression": { + "beforeOpeningCurlyBrace": true + }, + "disallowSpacesInsideArrayBrackets": true, + "disallowSpacesInsideObjectBrackets": true, + "disallowSpacesInsideParentheses": true, + "disallowQuotedKeysInObjects": "allButReserved", + "disallowSpaceAfterObjectKeys": true, + "requireSpaceBeforeBinaryOperators": ["-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<=" ], + "requireSpacesInConditionalExpression": { + "afterTest": true, + "beforeConsequent": true, + "afterConsequent": true, + "beforeAlternate": true + }, + "requireSpaceAfterBinaryOperators": ["/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], + "disallowKeywords": [ "with" ], + "disallowSpaceAfterPrefixUnaryOperators": [ "!" , "++", "--", "+", "-", "~"], + "disallowSpaceBeforePostfixUnaryOperators": ["++", "--", ","], + "disallowMultipleLineBreaks": true, + "disallowKeywordsOnNewLine": ["else"], + "safeContextKeyword": "self", + "excludeFiles": ["**/*-tpl.js", "**/*-xtpl.js", "**/parser.js", "**/parser.js", "**/modules.js"] +} \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..621cb75 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,28 @@ +{ + "camelcase": true, + "curly": true, + "eqeqeq": true, + "freeze": true, + "indent": 4, + "latedef": "nofunc", + "quotmark": "single", + "nonew": true, + "newcap": true, + "immed": true, + "noarg": true, + "eqnull": true, + "trailing": true, + "undef": true, + "unused": true, + "browser": true, + "node": true, + "es3": true, + "globals": { + "describe": false, + "expect": false, + "beforeEach": false, + "afterEach": false, + "modulex": false, + "it": false + } +} \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..c36b7f3 --- /dev/null +++ b/.npmignore @@ -0,0 +1,21 @@ +bower_components/ +tests/ +node_modules/ +nohup.out +*.iml +.idea/ +.ipr +.iws +*~ +~* +*.diff +*.log +*.patch +*.bak +.DS_Store +Thumbs.db +.project +.*proj +.svn/ +*.swp +out/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5f21b9d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,15 @@ +language: node_js +notifications: + email: + - yiminghe@gmail.com +node_js: +- 0.1 +before_script: +- node server & +- npm install gulp bower mocha-phantomjs -g +- bower install +- phantomjs --version +script: +- npm run-script browser-test +- npm run-script browser-test-build +- npm run-script browser-test-cover \ No newline at end of file diff --git a/README.md b/README.md index 8bba080..726280e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,13 @@ ua -== +=========== -userAgent detector +ua detector + +[![modulex-ua](https://nodei.co/npm/modulex-ua.png)](https://npmjs.org/package/modulex-ua) +[![NPM downloads](http://img.shields.io/npm/dm/modulex-ua.svg)](https://npmjs.org/package/modulex-ua) +[![Build Status](https://secure.travis-ci.org/kissyteam/ua.png?branch=master)](https://travis-ci.org/kissyteam/ua) +[![Coverage Status](https://img.shields.io/coveralls/kissyteam/ua.svg)](https://coveralls.io/r/kissyteam/ua?branch=master) +[![Dependency Status](https://gemnasium.com/kissyteam/ua.png)](https://gemnasium.com/kissyteam/modulex-ua) +[![Bower version](https://badge.fury.io/bo/modulex-ua.svg)](http://badge.fury.io/bo/modulex-ua) + +[![browser support](https://ci.testling.com/kissyteam/ua.png)](https://ci.testling.com/kissyteam/ua) \ No newline at end of file diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..4e9d858 --- /dev/null +++ b/bower.json @@ -0,0 +1,16 @@ +{ + "name": "modulex-ua", + "author": "yiminghe ", + "license": "MIT", + "repository": { + "type": "git", + "url": "http://github.com/kissyteam/ua.git" + }, + "dependencies": { + "modulex": "modulex#master" + }, + "ignore": [ + "**/*", + "!build/**/*" + ] +} diff --git a/build/ua-debug.js b/build/ua-debug.js new file mode 100644 index 0000000..318159a --- /dev/null +++ b/build/ua-debug.js @@ -0,0 +1,415 @@ +/* +combined modules: +ua +*/ +modulex.add("ua", [], function(require, exports, module) {/** + * @ignore + * ua + */ + +/*global process*/ +var win = typeof window !== 'undefined' ? window : {}; +var undef; +var doc = win.document; +var ua = win.navigator && win.navigator.userAgent || ''; + +function numberify(s) { + var c = 0; + // convert '1.2.3.4' to 1.234 + return parseFloat(s.replace(/\./g, function () { + return (c++ === 0) ? '.' : ''; + })); +} + +function setTridentVersion(ua, UA) { + var core, m; + UA[core = 'trident'] = 0.1; // Trident detected, look for revision + + // Get the Trident's accurate version + if ((m = ua.match(/Trident\/([\d.]*)/)) && m[1]) { + UA[core] = numberify(m[1]); + } + + UA.core = core; +} + +function getIEVersion(ua) { + var m, v; + if ((m = ua.match(/MSIE ([^;]*)|Trident.*; rv(?:\s|:)?([0-9.]+)/)) && + (v = (m[1] || m[2]))) { + return numberify(v); + } + return 0; +} + +function getDescriptorFromUserAgent(ua) { + var EMPTY = '', + os, + core = EMPTY, + shell = EMPTY, + m, + IE_DETECT_RANGE = [6, 9], + ieVersion, + v, + end, + VERSION_PLACEHOLDER = '{{version}}', + IE_DETECT_TPL = '', + div = doc && doc.createElement('div'), + s = []; + + /** + * KISSY UA + * @class KISSY.UA + * @singleton + */ + var UA = { + /** + * webkit version + * @type undef|Number + * @member KISSY.UA + */ + webkit: undef, + /** + * trident version + * @type undef|Number + * @member KISSY.UA + */ + trident: undef, + /** + * gecko version + * @type undef|Number + * @member KISSY.UA + */ + gecko: undef, + /** + * presto version + * @type undef|Number + * @member KISSY.UA + */ + presto: undef, + /** + * chrome version + * @type undef|Number + * @member KISSY.UA + */ + chrome: undef, + /** + * safari version + * @type undef|Number + * @member KISSY.UA + */ + safari: undef, + /** + * firefox version + * @type undef|Number + * @member KISSY.UA + */ + firefox: undef, + /** + * ie version + * @type undef|Number + * @member KISSY.UA + */ + ie: undef, + /** + * ie document mode + * @type undef|Number + * @member KISSY.UA + */ + ieMode: undef, + /** + * opera version + * @type undef|Number + * @member KISSY.UA + */ + opera: undef, + /** + * mobile browser. apple, android. + * @type String + * @member KISSY.UA + */ + mobile: undef, + /** + * browser render engine name. webkit, trident + * @type String + * @member KISSY.UA + */ + core: undef, + /** + * browser shell name. ie, chrome, firefox + * @type String + * @member KISSY.UA + */ + shell: undef, + + /** + * PhantomJS version number + * @type undef|Number + * @member KISSY.UA + */ + phantomjs: undef, + + /** + * operating system. android, ios, linux, windows + * @type string + * @member KISSY.UA + */ + os: undef, + + /** + * ipad ios version + * @type Number + * @member KISSY.UA + */ + ipad: undef, + /** + * iphone ios version + * @type Number + * @member KISSY.UA + */ + iphone: undef, + /** + * ipod ios + * @type Number + * @member KISSY.UA + */ + ipod: undef, + /** + * ios version + * @type Number + * @member KISSY.UA + */ + ios: undef, + + /** + * android version + * @type Number + * @member KISSY.UA + */ + android: undef, + + /** + * nodejs version + * @type Number + * @member KISSY.UA + */ + nodejs: undef + }; + + // ejecta + if (div && div.getElementsByTagName) { + // try to use IE-Conditional-Comment detect IE more accurately + // IE10 doesn't support this method, @ref: http://blogs.msdn.com/b/ie/archive/2011/07/06/html5-parsing-in-ie10.aspx + div.innerHTML = IE_DETECT_TPL.replace(VERSION_PLACEHOLDER, ''); + s = div.getElementsByTagName('s'); + } + + if (s.length > 0) { + setTridentVersion(ua, UA); + + // Detect the accurate version + // 注意: + // UA.shell = ie, 表示外壳是 ie + // 但 UA.ie = 7, 并不代表外壳是 ie7, 还有可能是 ie8 的兼容模式 + // 对于 ie8 的兼容模式,还要通过 documentMode 去判断。但此处不能让 UA.ie = 8, 否则 + // 很多脚本判断会失误。因为 ie8 的兼容模式表现行为和 ie7 相同,而不是和 ie8 相同 + for (v = IE_DETECT_RANGE[0], end = IE_DETECT_RANGE[1]; v <= end; v++) { + div.innerHTML = IE_DETECT_TPL.replace(VERSION_PLACEHOLDER, v); + if (s.length > 0) { + UA[shell = 'ie'] = v; + break; + } + } + + // https://github.com/kissyteam/kissy/issues/321 + // win8 embed app + if (!UA.ie && (ieVersion = getIEVersion(ua))) { + UA[shell = 'ie'] = ieVersion; + } + } else { + // WebKit + // https://github.com/kissyteam/kissy/issues/545 + if (((m = ua.match(/AppleWebKit\/([\d.]*)/)) || (m = ua.match(/Safari\/([\d.]*)/))) && m[1]) { + UA[core = 'webkit'] = numberify(m[1]); + + if ((m = ua.match(/OPR\/(\d+\.\d+)/)) && m[1]) { + UA[shell = 'opera'] = numberify(m[1]); + } else if ((m = ua.match(/Chrome\/([\d.]*)/)) && m[1]) { + UA[shell = 'chrome'] = numberify(m[1]); + } else if ((m = ua.match(/\/([\d.]*) Safari/)) && m[1]) { + UA[shell = 'safari'] = numberify(m[1]); + } else { + // default to mobile safari + UA.safari = UA.webkit; + } + + // Apple Mobile + if (/ Mobile\//.test(ua) && ua.match(/iPad|iPod|iPhone/)) { + UA.mobile = 'apple'; // iPad, iPhone or iPod Touch + + m = ua.match(/OS ([^\s]*)/); + if (m && m[1]) { + UA.ios = numberify(m[1].replace('_', '.')); + } + os = 'ios'; + m = ua.match(/iPad|iPod|iPhone/); + if (m && m[0]) { + UA[m[0].toLowerCase()] = UA.ios; + } + } else if (/ Android/i.test(ua)) { + if (/Mobile/.test(ua)) { + os = UA.mobile = 'android'; + } + m = ua.match(/Android ([^\s]*);/); + if (m && m[1]) { + UA.android = numberify(m[1]); + } + } else if ((m = ua.match(/NokiaN[^\/]*|Android \d\.\d|webOS\/\d\.\d/))) { + UA.mobile = m[0].toLowerCase(); // Nokia N-series, Android, webOS, ex: NokiaN95 + } + + if ((m = ua.match(/PhantomJS\/([^\s]*)/)) && m[1]) { + UA.phantomjs = numberify(m[1]); + } + } else { + // Presto + // ref: http://www.useragentstring.com/pages/useragentstring.php + if ((m = ua.match(/Presto\/([\d.]*)/)) && m[1]) { + UA[core = 'presto'] = numberify(m[1]); + + // Opera + if ((m = ua.match(/Opera\/([\d.]*)/)) && m[1]) { + UA[shell = 'opera'] = numberify(m[1]); // Opera detected, look for revision + + if ((m = ua.match(/Opera\/.* Version\/([\d.]*)/)) && m[1]) { + UA[shell] = numberify(m[1]); + } + + // Opera Mini + if ((m = ua.match(/Opera Mini[^;]*/)) && m) { + UA.mobile = m[0].toLowerCase(); // ex: Opera Mini/2.0.4509/1316 + } else if ((m = ua.match(/Opera Mobi[^;]*/)) && m) { + // Opera Mobile + // ex: Opera/9.80 (Windows NT 6.1; Opera Mobi/49; U; en) Presto/2.4.18 Version/10.00 + // issue: 由于 Opera Mobile 有 Version/ 字段,可能会与 Opera 混淆,同时对于 Opera Mobile 的版本号也比较混乱 + UA.mobile = m[0]; + } + } + // NOT WebKit or Presto + } else { + // MSIE + // 由于最开始已经使用了 IE 条件注释判断,因此落到这里的唯一可能性只有 IE10+ + // and analysis tools in nodejs + if ((ieVersion = getIEVersion(ua))) { + UA[shell = 'ie'] = ieVersion; + setTridentVersion(ua, UA); + // NOT WebKit, Presto or IE + } else { + // Gecko + if ((m = ua.match(/Gecko/))) { + UA[core = 'gecko'] = 0.1; // Gecko detected, look for revision + if ((m = ua.match(/rv:([\d.]*)/)) && m[1]) { + UA[core] = numberify(m[1]); + if (/Mobile|Tablet/.test(ua)) { + UA.mobile = 'firefox'; + } + } + // Firefox + if ((m = ua.match(/Firefox\/([\d.]*)/)) && m[1]) { + UA[shell = 'firefox'] = numberify(m[1]); + } + } + } + } + } + } + + if (!os) { + if ((/windows|win32/i).test(ua)) { + os = 'windows'; + } else if ((/macintosh|mac_powerpc/i).test(ua)) { + os = 'macintosh'; + } else if ((/linux/i).test(ua)) { + os = 'linux'; + } else if ((/rhino/i).test(ua)) { + os = 'rhino'; + } + } + + UA.os = os; + UA.core = UA.core || core; + UA.shell = shell; + UA.ieMode = UA.ie && doc.documentMode || UA.ie; + + return UA; +} + +var UA = module.exports = getDescriptorFromUserAgent(ua); + +// nodejs +if (typeof process === 'object') { + var versions, nodeVersion; + if ((versions = process.versions) && (nodeVersion = versions.node)) { + UA.os = process.platform; + UA.nodejs = numberify(nodeVersion); + } +} + +// use by analysis tools in nodejs +UA.getDescriptorFromUserAgent = getDescriptorFromUserAgent; + +var browsers = [ + // browser core type + 'webkit', + 'trident', + 'gecko', + 'presto', + // browser type + 'chrome', + 'safari', + 'firefox', + 'ie', + 'opera' + ], + documentElement = doc && doc.documentElement, + className = ''; +if (documentElement) { + for (var i = 0; i < browsers.length; i++) { + var key = browsers[i]; + var v = UA[key]; + if (v) { + className += ' ks-' + key + (parseInt(v, 10) + ''); + className += ' ks-' + key; + } + } + if (className) { + documentElement.className = (documentElement.className + className) + .replace(/^[\s\xa0]+|[\s\xa0]+$/g, ''); + } +} + +/* + NOTES: + 2013.07.08 yiminghe@gmail.com + - support ie11 and opera(using blink) + + 2013.01.17 yiminghe@gmail.com + - expose getDescriptorFromUserAgent for analysis tool in nodejs + + 2012.11.27 yiminghe@gmail.com + - moved to seed for conditional loading and better code share + + 2012.11.21 yiminghe@gmail.com + - touch and os support + + 2011.11.08 gonghaocn@gmail.com + - ie < 10 使用条件注释判断内核,更精确 + + 2010.03 + - jQuery, YUI 等类库都推荐用特性探测替代浏览器嗅探。特性探测的好处是能自动适应未来设备和未知设备,比如 + if(document.addEventListener) 假设 IE9 支持标准事件,则代码不用修改,就自适应了“未来浏览器”。 + 对于未知浏览器也是如此。但是,这并不意味着浏览器嗅探就得彻底抛弃。当代码很明确就是针对已知特定浏览器的, + 同时并非是某个特性探测可以解决时,用浏览器嗅探反而能带来代码的简洁,同时也也不会有什么后患。总之,一切 + 皆权衡。 + - UA.ie && UA.ie < 8 并不意味着浏览器就不是 IE8, 有可能是 IE8 的兼容模式。进一步的判断需要使用 documentMode. + */}); \ No newline at end of file diff --git a/build/ua.js b/build/ua.js new file mode 100644 index 0000000..062aeb2 --- /dev/null +++ b/build/ua.js @@ -0,0 +1 @@ +modulex.add("ua",[],function(e,o,i){function t(e){var o=0;return parseFloat(e.replace(/\./g,function(){return 0===o++?".":""}))}function a(e,o){var i,a;o[i="trident"]=.1,(a=e.match(/Trident\/([\d.]*)/))&&a[1]&&(o[i]=t(a[1])),o.core=i}function r(e){var o,i;return(o=e.match(/MSIE ([^;]*)|Trident.*; rv(?:\s|:)?([0-9.]+)/))&&(i=o[1]||o[2])?t(i):0}function n(e){var o,i,n,d,m,h="",l=h,p=h,f=[6,9],b="{{version}}",u="",w=c&&c.createElement("div"),g=[],v={webkit:s,trident:s,gecko:s,presto:s,chrome:s,safari:s,firefox:s,ie:s,ieMode:s,opera:s,mobile:s,core:s,shell:s,phantomjs:s,os:s,ipad:s,iphone:s,ipod:s,ios:s,android:s,nodejs:s};if(w&&w.getElementsByTagName&&(w.innerHTML=u.replace(b,""),g=w.getElementsByTagName("s")),g.length>0){for(a(e,v),d=f[0],m=f[1];m>=d;d++)if(w.innerHTML=u.replace(b,d),g.length>0){v[p="ie"]=d;break}!v.ie&&(n=r(e))&&(v[p="ie"]=n)}else((i=e.match(/AppleWebKit\/([\d.]*)/))||(i=e.match(/Safari\/([\d.]*)/)))&&i[1]?(v[l="webkit"]=t(i[1]),(i=e.match(/OPR\/(\d+\.\d+)/))&&i[1]?v[p="opera"]=t(i[1]):(i=e.match(/Chrome\/([\d.]*)/))&&i[1]?v[p="chrome"]=t(i[1]):(i=e.match(/\/([\d.]*) Safari/))&&i[1]?v[p="safari"]=t(i[1]):v.safari=v.webkit,/ Mobile\//.test(e)&&e.match(/iPad|iPod|iPhone/)?(v.mobile="apple",i=e.match(/OS ([^\s]*)/),i&&i[1]&&(v.ios=t(i[1].replace("_","."))),o="ios",i=e.match(/iPad|iPod|iPhone/),i&&i[0]&&(v[i[0].toLowerCase()]=v.ios)):/ Android/i.test(e)?(/Mobile/.test(e)&&(o=v.mobile="android"),i=e.match(/Android ([^\s]*);/),i&&i[1]&&(v.android=t(i[1]))):(i=e.match(/NokiaN[^\/]*|Android \d\.\d|webOS\/\d\.\d/))&&(v.mobile=i[0].toLowerCase()),(i=e.match(/PhantomJS\/([^\s]*)/))&&i[1]&&(v.phantomjs=t(i[1]))):(i=e.match(/Presto\/([\d.]*)/))&&i[1]?(v[l="presto"]=t(i[1]),(i=e.match(/Opera\/([\d.]*)/))&&i[1]&&(v[p="opera"]=t(i[1]),(i=e.match(/Opera\/.* Version\/([\d.]*)/))&&i[1]&&(v[p]=t(i[1])),(i=e.match(/Opera Mini[^;]*/))&&i?v.mobile=i[0].toLowerCase():(i=e.match(/Opera Mobi[^;]*/))&&i&&(v.mobile=i[0]))):(n=r(e))?(v[p="ie"]=n,a(e,v)):(i=e.match(/Gecko/))&&(v[l="gecko"]=.1,(i=e.match(/rv:([\d.]*)/))&&i[1]&&(v[l]=t(i[1]),/Mobile|Tablet/.test(e)&&(v.mobile="firefox")),(i=e.match(/Firefox\/([\d.]*)/))&&i[1]&&(v[p="firefox"]=t(i[1])));return o||(/windows|win32/i.test(e)?o="windows":/macintosh|mac_powerpc/i.test(e)?o="macintosh":/linux/i.test(e)?o="linux":/rhino/i.test(e)&&(o="rhino")),v.os=o,v.core=v.core||l,v.shell=p,v.ieMode=v.ie&&c.documentMode||v.ie,v}var s,d="undefined"!=typeof window?window:{},c=d.document,m=d.navigator&&d.navigator.userAgent||"",h=i.exports=n(m);if("object"==typeof process){var l,p;(l=process.versions)&&(p=l.node)&&(h.os=process.platform,h.nodejs=t(p))}h.getDescriptorFromUserAgent=n;var f=["webkit","trident","gecko","presto","chrome","safari","firefox","ie","opera"],b=c&&c.documentElement,u="";if(b){for(var w=0;w<' + 's>', + div = doc && doc.createElement('div'), + s = []; + + /** + * KISSY UA + * @class KISSY.UA + * @singleton + */ + var UA = { + /** + * webkit version + * @type undef|Number + * @member KISSY.UA + */ + webkit: undef, + /** + * trident version + * @type undef|Number + * @member KISSY.UA + */ + trident: undef, + /** + * gecko version + * @type undef|Number + * @member KISSY.UA + */ + gecko: undef, + /** + * presto version + * @type undef|Number + * @member KISSY.UA + */ + presto: undef, + /** + * chrome version + * @type undef|Number + * @member KISSY.UA + */ + chrome: undef, + /** + * safari version + * @type undef|Number + * @member KISSY.UA + */ + safari: undef, + /** + * firefox version + * @type undef|Number + * @member KISSY.UA + */ + firefox: undef, + /** + * ie version + * @type undef|Number + * @member KISSY.UA + */ + ie: undef, + /** + * ie document mode + * @type undef|Number + * @member KISSY.UA + */ + ieMode: undef, + /** + * opera version + * @type undef|Number + * @member KISSY.UA + */ + opera: undef, + /** + * mobile browser. apple, android. + * @type String + * @member KISSY.UA + */ + mobile: undef, + /** + * browser render engine name. webkit, trident + * @type String + * @member KISSY.UA + */ + core: undef, + /** + * browser shell name. ie, chrome, firefox + * @type String + * @member KISSY.UA + */ + shell: undef, + + /** + * PhantomJS version number + * @type undef|Number + * @member KISSY.UA + */ + phantomjs: undef, + + /** + * operating system. android, ios, linux, windows + * @type string + * @member KISSY.UA + */ + os: undef, + + /** + * ipad ios version + * @type Number + * @member KISSY.UA + */ + ipad: undef, + /** + * iphone ios version + * @type Number + * @member KISSY.UA + */ + iphone: undef, + /** + * ipod ios + * @type Number + * @member KISSY.UA + */ + ipod: undef, + /** + * ios version + * @type Number + * @member KISSY.UA + */ + ios: undef, + + /** + * android version + * @type Number + * @member KISSY.UA + */ + android: undef, + + /** + * nodejs version + * @type Number + * @member KISSY.UA + */ + nodejs: undef + }; + + // ejecta + if (div && div.getElementsByTagName) { + // try to use IE-Conditional-Comment detect IE more accurately + // IE10 doesn't support this method, @ref: http://blogs.msdn.com/b/ie/archive/2011/07/06/html5-parsing-in-ie10.aspx + div.innerHTML = IE_DETECT_TPL.replace(VERSION_PLACEHOLDER, ''); + s = div.getElementsByTagName('s'); + } + + if (s.length > 0) { + setTridentVersion(ua, UA); + + // Detect the accurate version + // 注意: + // UA.shell = ie, 表示外壳是 ie + // 但 UA.ie = 7, 并不代表外壳是 ie7, 还有可能是 ie8 的兼容模式 + // 对于 ie8 的兼容模式,还要通过 documentMode 去判断。但此处不能让 UA.ie = 8, 否则 + // 很多脚本判断会失误。因为 ie8 的兼容模式表现行为和 ie7 相同,而不是和 ie8 相同 + for (v = IE_DETECT_RANGE[0], end = IE_DETECT_RANGE[1]; v <= end; v++) { + div.innerHTML = IE_DETECT_TPL.replace(VERSION_PLACEHOLDER, v); + if (s.length > 0) { + UA[shell = 'ie'] = v; + break; + } + } + + // https://github.com/kissyteam/kissy/issues/321 + // win8 embed app + if (!UA.ie && (ieVersion = getIEVersion(ua))) { + UA[shell = 'ie'] = ieVersion; + } + } else { + // WebKit + // https://github.com/kissyteam/kissy/issues/545 + if (((m = ua.match(/AppleWebKit\/([\d.]*)/)) || (m = ua.match(/Safari\/([\d.]*)/))) && m[1]) { + UA[core = 'webkit'] = numberify(m[1]); + + if ((m = ua.match(/OPR\/(\d+\.\d+)/)) && m[1]) { + UA[shell = 'opera'] = numberify(m[1]); + } else if ((m = ua.match(/Chrome\/([\d.]*)/)) && m[1]) { + UA[shell = 'chrome'] = numberify(m[1]); + } else if ((m = ua.match(/\/([\d.]*) Safari/)) && m[1]) { + UA[shell = 'safari'] = numberify(m[1]); + } else { + // default to mobile safari + UA.safari = UA.webkit; + } + + // Apple Mobile + if (/ Mobile\//.test(ua) && ua.match(/iPad|iPod|iPhone/)) { + UA.mobile = 'apple'; // iPad, iPhone or iPod Touch + + m = ua.match(/OS ([^\s]*)/); + if (m && m[1]) { + UA.ios = numberify(m[1].replace('_', '.')); + } + os = 'ios'; + m = ua.match(/iPad|iPod|iPhone/); + if (m && m[0]) { + UA[m[0].toLowerCase()] = UA.ios; + } + } else if (/ Android/i.test(ua)) { + if (/Mobile/.test(ua)) { + os = UA.mobile = 'android'; + } + m = ua.match(/Android ([^\s]*);/); + if (m && m[1]) { + UA.android = numberify(m[1]); + } + } else if ((m = ua.match(/NokiaN[^\/]*|Android \d\.\d|webOS\/\d\.\d/))) { + UA.mobile = m[0].toLowerCase(); // Nokia N-series, Android, webOS, ex: NokiaN95 + } + + if ((m = ua.match(/PhantomJS\/([^\s]*)/)) && m[1]) { + UA.phantomjs = numberify(m[1]); + } + } else { + // Presto + // ref: http://www.useragentstring.com/pages/useragentstring.php + if ((m = ua.match(/Presto\/([\d.]*)/)) && m[1]) { + UA[core = 'presto'] = numberify(m[1]); + + // Opera + if ((m = ua.match(/Opera\/([\d.]*)/)) && m[1]) { + UA[shell = 'opera'] = numberify(m[1]); // Opera detected, look for revision + + if ((m = ua.match(/Opera\/.* Version\/([\d.]*)/)) && m[1]) { + UA[shell] = numberify(m[1]); + } + + // Opera Mini + if ((m = ua.match(/Opera Mini[^;]*/)) && m) { + UA.mobile = m[0].toLowerCase(); // ex: Opera Mini/2.0.4509/1316 + } else if ((m = ua.match(/Opera Mobi[^;]*/)) && m) { + // Opera Mobile + // ex: Opera/9.80 (Windows NT 6.1; Opera Mobi/49; U; en) Presto/2.4.18 Version/10.00 + // issue: 由于 Opera Mobile 有 Version/ 字段,可能会与 Opera 混淆,同时对于 Opera Mobile 的版本号也比较混乱 + UA.mobile = m[0]; + } + } + // NOT WebKit or Presto + } else { + // MSIE + // 由于最开始已经使用了 IE 条件注释判断,因此落到这里的唯一可能性只有 IE10+ + // and analysis tools in nodejs + if ((ieVersion = getIEVersion(ua))) { + UA[shell = 'ie'] = ieVersion; + setTridentVersion(ua, UA); + // NOT WebKit, Presto or IE + } else { + // Gecko + if ((m = ua.match(/Gecko/))) { + UA[core = 'gecko'] = 0.1; // Gecko detected, look for revision + if ((m = ua.match(/rv:([\d.]*)/)) && m[1]) { + UA[core] = numberify(m[1]); + if (/Mobile|Tablet/.test(ua)) { + UA.mobile = 'firefox'; + } + } + // Firefox + if ((m = ua.match(/Firefox\/([\d.]*)/)) && m[1]) { + UA[shell = 'firefox'] = numberify(m[1]); + } + } + } + } + } + } + + if (!os) { + if ((/windows|win32/i).test(ua)) { + os = 'windows'; + } else if ((/macintosh|mac_powerpc/i).test(ua)) { + os = 'macintosh'; + } else if ((/linux/i).test(ua)) { + os = 'linux'; + } else if ((/rhino/i).test(ua)) { + os = 'rhino'; + } + } + + UA.os = os; + UA.core = UA.core || core; + UA.shell = shell; + UA.ieMode = UA.ie && doc.documentMode || UA.ie; + + return UA; +} + +var UA = module.exports = getDescriptorFromUserAgent(ua); + +// nodejs +if (typeof process === 'object') { + var versions, nodeVersion; + if ((versions = process.versions) && (nodeVersion = versions.node)) { + UA.os = process.platform; + UA.nodejs = numberify(nodeVersion); + } +} + +// use by analysis tools in nodejs +UA.getDescriptorFromUserAgent = getDescriptorFromUserAgent; + +var browsers = [ + // browser core type + 'webkit', + 'trident', + 'gecko', + 'presto', + // browser type + 'chrome', + 'safari', + 'firefox', + 'ie', + 'opera' + ], + documentElement = doc && doc.documentElement, + className = ''; +if (documentElement) { + for (var i = 0; i < browsers.length; i++) { + var key = browsers[i]; + var v = UA[key]; + if (v) { + className += ' ks-' + key + (parseInt(v, 10) + ''); + className += ' ks-' + key; + } + } + if (className) { + documentElement.className = (documentElement.className + className) + .replace(/^[\s\xa0]+|[\s\xa0]+$/g, ''); + } +} + +/* + NOTES: + 2013.07.08 yiminghe@gmail.com + - support ie11 and opera(using blink) + + 2013.01.17 yiminghe@gmail.com + - expose getDescriptorFromUserAgent for analysis tool in nodejs + + 2012.11.27 yiminghe@gmail.com + - moved to seed for conditional loading and better code share + + 2012.11.21 yiminghe@gmail.com + - touch and os support + + 2011.11.08 gonghaocn@gmail.com + - ie < 10 使用条件注释判断内核,更精确 + + 2010.03 + - jQuery, YUI 等类库都推荐用特性探测替代浏览器嗅探。特性探测的好处是能自动适应未来设备和未知设备,比如 + if(document.addEventListener) 假设 IE9 支持标准事件,则代码不用修改,就自适应了“未来浏览器”。 + 对于未知浏览器也是如此。但是,这并不意味着浏览器嗅探就得彻底抛弃。当代码很明确就是针对已知特定浏览器的, + 同时并非是某个特性探测可以解决时,用浏览器嗅探反而能带来代码的简洁,同时也也不会有什么后患。总之,一切 + 皆权衡。 + - UA.ie && UA.ie < 8 并不意味着浏览器就不是 IE8, 有可能是 IE8 的兼容模式。进一步的判断需要使用 documentMode. + */ \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..2b4ae95 --- /dev/null +++ b/package.json @@ -0,0 +1,62 @@ +{ + "name": "modulex-ua", + "version": "1.0.0", + "author": "yiminghe ", + "engines": { + "node": "~0.10" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "http://github.com/kissyteam/path.git" + }, + "testling": { + "server": "server.js", + "html": "tests/runner.html", + "browsers": [ + "ie/6..latest", + "chrome/latest", + "firefox/latest", + "safari/latest", + "iphone/6..lastest", + "ipad/6..latest", + "android-browser/latest" + ] + }, + "devDependencies": { + "body-parser": "^1.6.5", + "chai": "^1.9.1", + "express": "^4.8.5", + "gulp": "^3.8.7", + "gulp-clean": "^0.3.1", + "gulp-filter": "^1.0.0", + "gulp-footer": "^1.0.5", + "gulp-header": "^1.0.5", + "gulp-jscs": "^1.1.0", + "gulp-jshint": "^1.8.4", + "gulp-kclean": "^0.0.13", + "gulp-modulex": "^1.0.0", + "gulp-rename": "^1.2.0", + "gulp-replace": "^0.4.0", + "gulp-uglify": "^0.3.1", + "gulp-util": "^3.0.0", + "jshint": "^2.5.5", + "jshint-stylish": "^0.4.0", + "mocha": "^1.21.4", + "node-jscover": "^0.6.8", + "node-jscover-coveralls": "^1.0.10", + "node-jscover-handler": "^1.0.3", + "precommit-hook": "^1.0.7", + "serve-index": "^1.1.6", + "serve-static": "^1.5.3" + }, + "precommit": [ + "lint" + ], + "scripts": { + "lint": "gulp lint", + "browser-test": "mocha-phantomjs http://localhost:8006/tests/runner.html", + "browser-test-build": "mocha-phantomjs http://localhost:8006/tests/runner.html?build", + "browser-test-cover": "mocha-phantomjs -R node_modules/node-jscover/lib/reporters/mocha/console http://localhost:8006/tests/runner.html?coverage" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..4aebbbc --- /dev/null +++ b/server.js @@ -0,0 +1,45 @@ +var gutil = require('gulp-util'); +var express = require('express'); +var path = require('path'); +var jscoverHandler = require('node-jscover-handler'); +var jscoverCoveralls = require('node-jscover-coveralls'); +var serveStatic = require('serve-static'); +var serveIndex = require('serve-index'); +var fs = require('fs'); +var app = express(); +var cwd = process.cwd(); +var bodyParser = require('body-parser'); + +function modularize(req, res, next) { + var filePath = path.resolve(cwd, req.originalUrl.substring(1)).replace(/-coverage\.js/, '.js'); + var stats = fs.statSync(filePath); + if (!stats.isFile()) { + next(); + return; + } + var code = fs.readFileSync(filePath, 'utf-8'); + code = 'modulex.add(function(require,exports,module){' + code + '});'; + if (req.path.indexOf('-coverage.js') !== -1) { + req.nodeJsCoverCode = code; + next(); + return; + } + res.set('content-type', 'application/javascript;charset=utf-8'); + res.end(code); +} + +// parse application/x-www-form-urlencoded +app.use(bodyParser.urlencoded({ extended: false })); +app.use('/lib/', modularize); +app.use('/tests/specs/', modularize); + +app.use(jscoverCoveralls()); +app.use(jscoverHandler()); +app.use(serveIndex(cwd, { + hidden: true, + view: 'details' +})); +app.use(serveStatic(cwd)); +var port = process.env.PORT || 8006; +app.listen(port); +gutil.log('server start at ' + port); \ No newline at end of file diff --git a/tests/runner.html b/tests/runner.html new file mode 100644 index 0000000..5394ef3 --- /dev/null +++ b/tests/runner.html @@ -0,0 +1,60 @@ + + + + ua tests + + + +

ua tests

+ +
+ + + + + + + + + + + \ No newline at end of file diff --git a/tests/specs/index.js b/tests/specs/index.js new file mode 100644 index 0000000..25a4935 --- /dev/null +++ b/tests/specs/index.js @@ -0,0 +1,21 @@ +var UA = require('ua'); +describe('ua', function () { + if (!UA.ie) { + it('recoginize webkit', function () { + var userAgent = 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.41 Safari/537.36'; + var ua = UA.getDescriptorFromUserAgent(userAgent); + expect(ua.webkit).to.equal(537.36); + expect(ua.safari).to.equal(undefined); + expect(ua.chrome).to.equal(32.0170041); + }); + + // https://github.com/kissyteam/kissy/issues/545 + it('recoginize xiaomi', function () { + var userAgent = 'Xiaomi_2013061_TD/V1 Linux/3.4.5 Android/4.2.1 Release/09.18.2013 Browser/AppleWebKit534.30 ' + + 'Mobile Safari/534.30 MBBMS/2.2 System/Android 4.2.1 XiaoMi/MiuiBrowser/1.0'; + var ua = UA.getDescriptorFromUserAgent(userAgent); + expect(ua.webkit).to.equal(534.30); + expect(ua.safari).to.equal(534.30); + }); + } +});