├── test ├── qunit_setup.js ├── lang │ ├── test2-ja.json │ ├── test-en.json │ ├── test-ast.json │ ├── test-en-US.json │ ├── test-ja-XX.json │ ├── custom │ │ └── test-fo.json │ ├── test-ja.foo │ ├── test-zh-CN.json │ ├── footer-en.json │ ├── header-en.json │ └── test-ja.json ├── .jshintrc ├── localize.html ├── localize_test.coffee └── localize_test.js ├── .gitignore ├── examples ├── ruby_round.gif ├── ruby_square.gif ├── language_and_country.html ├── absolute_prefix_path.html ├── multiple_language_packs.html ├── custom_callback_usage.html ├── basic_usage.html └── skip_language.html ├── .travis.yml ├── libs ├── jquery-loader.js └── qunit │ ├── qunit.css │ └── qunit.js ├── localize.jquery.json ├── package.json ├── CONTRIBUTING.md ├── Gruntfile.coffee ├── dist ├── jquery.localize.min.js └── jquery.localize.js ├── src └── jquery.localize.coffee └── README.md /test/qunit_setup.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /test/lang/test2-ja.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /test/lang/test-en.json: -------------------------------------------------------------------------------- 1 | { "en_message": "en loaded" } 2 | -------------------------------------------------------------------------------- /test/lang/test-ast.json: -------------------------------------------------------------------------------- 1 | { 2 | "basic": "basic success" 3 | } 4 | -------------------------------------------------------------------------------- /test/lang/test-en-US.json: -------------------------------------------------------------------------------- 1 | { "en_us_message": "en-US loaded" } 2 | -------------------------------------------------------------------------------- /test/lang/test-ja-XX.json: -------------------------------------------------------------------------------- 1 | { "message": "country code success" } 2 | -------------------------------------------------------------------------------- /test/lang/custom/test-fo.json: -------------------------------------------------------------------------------- 1 | { "path_prefix": "pathPrefix success" } 2 | -------------------------------------------------------------------------------- /test/lang/test-ja.foo: -------------------------------------------------------------------------------- 1 | { 2 | "basic": "basic success foo" 3 | } 4 | 5 | -------------------------------------------------------------------------------- /test/lang/test-zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "basic": "basic success zh-CN" 3 | } 4 | -------------------------------------------------------------------------------- /test/lang/footer-en.json: -------------------------------------------------------------------------------- 1 | { "footer_message": "Footer message loaded" } 2 | 3 | -------------------------------------------------------------------------------- /test/lang/header-en.json: -------------------------------------------------------------------------------- 1 | { "header_message": "Header message loaded" } 2 | 3 | -------------------------------------------------------------------------------- /examples/ruby_round.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderifous/jquery-localize/HEAD/examples/ruby_round.gif -------------------------------------------------------------------------------- /examples/ruby_square.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coderifous/jquery-localize/HEAD/examples/ruby_square.gif -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "4" 5 | before_script: 6 | - npm install -g grunt-cli 7 | -------------------------------------------------------------------------------- /libs/jquery-loader.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // Default to the local version. 3 | var path = '../libs/jquery/jquery.js'; 4 | // Get any jquery=___ param from the query string. 5 | var jqversion = location.search.match(/[?&]jquery=(.*?)(?=&|$)/); 6 | // If a version was specified, use that version from code.jquery.com. 7 | if (jqversion) { 8 | path = 'http://code.jquery.com/jquery-' + jqversion[1] + '.js'; 9 | } 10 | // This is the only time I'll ever use document.write, I promise! 11 | document.write(''); 12 | }()); 13 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "browser": true, 14 | "predef": [ 15 | "jQuery", 16 | "QUnit", 17 | "module", 18 | "test", 19 | "asyncTest", 20 | "expect", 21 | "start", 22 | "stop", 23 | "ok", 24 | "equal", 25 | "notEqual", 26 | "deepEqual", 27 | "notDeepEqual", 28 | "strictEqual", 29 | "notStrictEqual", 30 | "throws" 31 | ] 32 | } -------------------------------------------------------------------------------- /test/lang/test-ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "nested": "nested success", 4 | "input": "input success", 5 | "input_as_obj": { 6 | "value": "input_as_obj value success", 7 | "title": "input_as_obj title success" 8 | }, 9 | "optgroup": "optgroup success", 10 | "option": "option success", 11 | "ruby_image": { 12 | "src": "ruby_round.gif", 13 | "alt": "a round ruby", 14 | "title": "A Round Ruby" 15 | }, 16 | "link": { 17 | "text": "success", 18 | "href": "http://success" 19 | } 20 | }, 21 | "basic": "basic success", 22 | "with_title": { 23 | "text": "with_title text success", 24 | "title": "with_title title success" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /localize.jquery.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery.localize", 3 | "title": "Localize", 4 | "description": "A jQuery plugin that makes it easy to i18n your static web site.", 5 | "version": "0.2.0", 6 | "homepage": "https://github.com/coderifous/jquery-localize", 7 | "author": { 8 | "name": "coderifous", 9 | "email": "jim@thegarvin.com" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/coderifous/jquery-localize.git" 14 | }, 15 | "bugs": "https://github.com/coderifous/jquery-localize/issues", 16 | "licenses": [ 17 | { 18 | "type": "MIT", 19 | "url": "https://github.com/coderifous/jquery-localize/blob/master/LICENSE-MIT" 20 | } 21 | ], 22 | "dependencies": { 23 | "jquery": "*" 24 | }, 25 | "keywords": [] 26 | } 27 | -------------------------------------------------------------------------------- /examples/language_and_country.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Localize Test 9 | 10 | 11 | 12 | 13 | 14 |

Test localization...

15 |

It failed :(

16 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-localize", 3 | "description": "A jQuery plugin that makes it easy to i18n your static web site.", 4 | "version": "0.2.0", 5 | "homepage": "https://github.com/coderifous/jquery-localize", 6 | "license": "MIT", 7 | "main": "dist/jquery.localize.js", 8 | "author": { 9 | "name": "coderifous", 10 | "email": "jim@thegarvin.com" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/coderifous/jquery-localize.git" 15 | }, 16 | "bugs": "https://github.com/coderifous/jquery-localize/issues", 17 | "engines": { 18 | "node": ">= 0.8.0" 19 | }, 20 | "scripts": { 21 | "prepublish": "grunt", 22 | "test": "grunt test" 23 | }, 24 | "devDependencies": { 25 | "grunt": "~0.4.2", 26 | "grunt-cli": "^0.1.13", 27 | "grunt-contrib-clean": "~0.4.0", 28 | "grunt-contrib-coffee": "~0.10.0", 29 | "grunt-contrib-connect": "~0.6.0", 30 | "grunt-contrib-qunit": "~0.2.0", 31 | "grunt-contrib-uglify": "~0.2.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/absolute_prefix_path.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Localize Test 9 | 10 | 11 | 12 | 13 | 14 |

Test localization...

15 |

puts 2 + 2

16 |

It failed :(

17 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/localize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Localize Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 |
21 |
22 | lame test markup 23 | normal test markup 24 | awesome test markup 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/multiple_language_packs.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Localize Test 9 | 10 | 11 | 12 | 13 | 14 |

Test localization...

15 |

Header failed to load.

16 |

Footer failed to load.

17 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/custom_callback_usage.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Localize Test 9 | 10 | 11 | 12 | 13 | 14 |

Test localization...

15 |

puts 2 + 2

16 |

It failed :(

17 |

Optional callback never happened.

18 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/basic_usage.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Localize Test 9 | 10 | 11 | 12 | 13 | 14 |

Test localization...

15 |

puts 2 + 2

16 | 17 | 22 |

23 | a square ruby 24 | Ruby image should be round. 25 |

26 |

It failed :(

27 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /examples/skip_language.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Localize Test 9 | 10 | 11 | 12 | 13 | 14 |

Test localization...

15 |

Should not load en lang file.

16 |

Should not load en-US lang file.

17 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Important notes 4 | Please don't edit files in the `dist` subdirectory as they are generated via Grunt. You'll find source code in the `src` subdirectory! This plugin is written in [CoffeeScript](http://jashkenas.github.com/coffee-script/) 5 | 6 | ### Code style 7 | Regarding code style like indentation and whitespace, **follow the conventions you see used in the source already.** 8 | 9 | ### PhantomJS 10 | While Grunt can run the included unit tests via [PhantomJS](http://phantomjs.org/), this shouldn't be considered a substitute for the real thing. Please be sure to test the `test/*.html` unit test file(s) in _actual_ browsers. 11 | 12 | ## Modifying the code 13 | First, ensure that you have the latest [Node.js](http://nodejs.org/) and [npm](http://npmjs.org/) installed. 14 | 15 | Test that Grunt's CLI is installed by running `grunt --version`. If the command isn't found, run `npm install -g grunt-cli`. For more information about installing Grunt, see the [getting started guide](http://gruntjs.com/getting-started). 16 | 17 | 1. Fork and clone the repo. 18 | 1. Run `npm install` to install all dependencies (including Grunt). 19 | 1. Run `grunt` to grunt this project. 20 | 1. To run the tests only, `grunt test`. 21 | 22 | Assuming that you don't see any red, you're ready to go. Just be sure to run `grunt` after making any changes, to ensure that nothing is broken. 23 | 24 | ## Submitting pull requests 25 | 26 | 1. Create a new branch, please don't work in your `master` branch directly. 27 | 1. Add failing tests for the change you want to make. Run `grunt` to see the tests fail. 28 | 1. Fix stuff. 29 | 1. Run `grunt` to see if the tests pass. Repeat steps 2-4 until done. 30 | 1. Open `test/*.html` unit test file(s) in actual browser to ensure tests pass everywhere. 31 | 1. Update the documentation to reflect any changes. 32 | 1. Push to your fork and submit a pull request. -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | "use strict" 2 | module.exports = (grunt) -> 3 | 4 | # Project configuration. 5 | grunt.initConfig 6 | 7 | # Metadata. 8 | pkg: grunt.file.readJSON("localize.jquery.json") 9 | banner: """ 10 | /*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today(\"yyyy-mm-dd\") %> 11 | <%= pkg.homepage ? "* " + pkg.homepage : "" %> 12 | * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>; Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */ 13 | 14 | """ 15 | 16 | # Task configuration. 17 | clean: 18 | files: ["dist"] 19 | 20 | coffee: 21 | options: 22 | bare: true 23 | compile: 24 | files: 25 | "dist/<%= pkg.name %>.js": "src/*.coffee" 26 | "test/localize_test.js": "test/localize_test.coffee" 27 | 28 | uglify: 29 | options: 30 | banner: "<%= banner %>" 31 | 32 | dist: 33 | src: "dist/<%= pkg.name %>.js" 34 | dest: "dist/<%= pkg.name %>.min.js" 35 | 36 | qunit: 37 | all: 38 | options: 39 | urls: [ 40 | "1.7.2" 41 | "1.8.3" 42 | "1.9.1" 43 | "1.10.2" 44 | "1.11.3" 45 | "2.0.3" 46 | "2.1.4" 47 | ].map((version) -> 48 | "http://localhost:<%= connect.server.options.port %>/test/localize.html?jquery=" + version 49 | ) 50 | 51 | connect: 52 | server: 53 | options: 54 | port: 8085 # This is a random port, feel free to change it. 55 | 56 | # These plugins provide necessary tasks. 57 | grunt.loadNpmTasks "grunt-contrib-clean" 58 | grunt.loadNpmTasks "grunt-contrib-uglify" 59 | grunt.loadNpmTasks "grunt-contrib-qunit" 60 | grunt.loadNpmTasks "grunt-contrib-coffee" 61 | grunt.loadNpmTasks "grunt-contrib-connect" 62 | 63 | # Default task. 64 | grunt.registerTask "default", [ 65 | "connect" 66 | "clean" 67 | "coffee" 68 | "uglify" 69 | "qunit" 70 | ] 71 | 72 | grunt.registerTask "test", [ 73 | "connect" 74 | "qunit" 75 | ] 76 | return 77 | -------------------------------------------------------------------------------- /dist/jquery.localize.min.js: -------------------------------------------------------------------------------- 1 | /*! Localize - v0.2.0 - 2016-10-13 2 | * https://github.com/coderifous/jquery-localize 3 | * Copyright (c) 2016 coderifous; Licensed MIT */ 4 | !function(a){var b;return b=function(a){return a=a.replace(/_/,"-").toLowerCase(),a.length>3&&(a=a.substring(0,3)+a.substring(3).toUpperCase()),a},a.defaultLanguage=b(navigator.languages&&navigator.languages.length>0?navigator.languages[0]:navigator.language||navigator.userLanguage),a.localize=function(c,d){var e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v;return null==d&&(d={}),v=this,h={},g=d.fileExtension||"json",f=a.Deferred(),k=function(a,b,c){var e;switch(null==c&&(c=1),c){case 1:return h={},d.loadBase?(e=a+("."+g),i(e,a,b,c)):k(a,b,2);case 2:return e=""+a+"-"+b.split("-")[0]+"."+g,i(e,a,b,c);case 3:return e=""+a+"-"+b.split("-").slice(0,2).join("-")+"."+g,i(e,a,b,c);default:return f.resolve()}},i=function(b,c,e,f){var g,i,j;return null!=d.pathPrefix&&(b=""+d.pathPrefix+"/"+b),j=function(b){return a.extend(h,b),q(h),k(c,e,f+1)},i=function(){return 2===f&&e.indexOf("-")>-1?k(c,e,f+1):d.fallback&&d.fallback!==e?k(c,d.fallback):void 0},g={url:b,dataType:"json",async:!0,timeout:null!=d.timeout?d.timeout:500,success:j,error:i},"file:"===window.location.protocol&&(g.error=function(b){return j(a.parseJSON(b.responseText))}),a.ajax(g)},q=function(a){return null!=d.callback?d.callback(a,e):e(a)},e=function(b){return a.localize.data[c]=b,v.each(function(){var c,d,e;return c=a(this),d=c.data("localize"),d||(d=c.attr("rel").match(/localize\[(.*?)\]/)[1]),e=u(d,b),null!=e?l(c,d,e):void 0})},l=function(b,c,d){return b.is("input")?o(b,c,d):b.is("textarea")?o(b,c,d):b.is("img")?n(b,c,d):b.is("optgroup")?p(b,c,d):a.isPlainObject(d)||b.html(d),a.isPlainObject(d)?m(b,d):void 0},o=function(b,c,d){var e;return e=a.isPlainObject(d)?d.value:d,b.is("[placeholder]")?b.attr("placeholder",e):b.val(e)},m=function(a,b){return s(a,"title",b),s(a,"href",b),t(a,"text",b)},p=function(a,b,c){return a.attr("label",c)},n=function(a,b,c){return s(a,"alt",c),s(a,"src",c)},u=function(a,b){var c,d,e,f;for(c=a.split(/\./),d=b,e=0,f=c.length;f>e;e++)a=c[e],d=null!=d?d[a]:null;return d},s=function(a,b,c){return c=u(b,c),null!=c?a.attr(b,c):void 0},t=function(a,b,c){return c=u(b,c),null!=c?a.text(c):void 0},r=function(a){var b;return"string"==typeof a?"^"+a+"$":null!=a.length?function(){var c,d,e;for(e=[],c=0,d=a.length;d>c;c++)b=a[c],e.push(r(b));return e}().join("|"):a},j=b(d.language?d.language:a.defaultLanguage),d.skipLanguage&&j.match(r(d.skipLanguage))?f.resolve():k(c,j,1),v.localizePromise=f,v},a.fn.localize=a.localize,a.localize.data={}}(jQuery); -------------------------------------------------------------------------------- /src/jquery.localize.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | Copyright (c) Jim Garvin (http://github.com/coderifous), 2008. 3 | Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. 4 | Written by Jim Garvin (@coderifous) for use on LMGTFY.com. 5 | http://github.com/coderifous/jquery-localize 6 | Based off of Keith Wood's Localisation jQuery plugin. 7 | http://keith-wood.name/localisation.html 8 | ### 9 | 10 | do ($ = jQuery) -> 11 | 12 | # Ensures language code is in the format aa-AA. 13 | normaliseLang = (lang) -> 14 | lang = lang.replace(/_/, '-').toLowerCase() 15 | if lang.length > 3 16 | lang = lang.substring(0, 3) + lang.substring(3).toUpperCase() 17 | lang 18 | 19 | # Mozilla uses .language, IE uses .userLanguage 20 | $.defaultLanguage = normaliseLang(if navigator.languages and navigator.languages.length > 0 then navigator.languages[0] else navigator.language or navigator.userLanguage) 21 | 22 | $.localize = (pkg, options = {}) -> 23 | wrappedSet = this 24 | intermediateLangData = {} 25 | fileExtension = options.fileExtension || "json" 26 | deferred = $.Deferred() 27 | 28 | loadLanguage = (pkg, lang, level = 1) -> 29 | switch level 30 | when 1 31 | intermediateLangData = {} 32 | if options.loadBase 33 | file = pkg + ".#{fileExtension}" 34 | jsonCall(file, pkg, lang, level) 35 | else 36 | loadLanguage(pkg, lang, 2) 37 | when 2 38 | file = "#{pkg}-#{lang.split('-')[0]}.#{fileExtension}" 39 | jsonCall(file, pkg, lang, level) 40 | when 3 41 | file = "#{pkg}-#{lang.split('-').slice(0,2).join('-')}.#{fileExtension}" 42 | jsonCall(file, pkg, lang, level) 43 | else 44 | # ensure deferred is resolved 45 | deferred.resolve() 46 | 47 | jsonCall = (file, pkg, lang, level) -> 48 | file = "#{options.pathPrefix}/#{file}" if options.pathPrefix? 49 | successFunc = (d) -> 50 | $.extend(intermediateLangData, d) 51 | notifyDelegateLanguageLoaded(intermediateLangData) 52 | loadLanguage(pkg, lang, level + 1) 53 | errorFunc = -> 54 | if level == 2 && lang.indexOf('-') > -1 55 | # the language-only file may not exist, try the language-country file next 56 | # (ref: https://github.com/coderifous/jquery-localize/issues/47) 57 | loadLanguage(pkg, lang, level + 1) 58 | else if options.fallback && options.fallback != lang 59 | loadLanguage(pkg, options.fallback) 60 | ajaxOptions = 61 | url: file 62 | dataType: "json" 63 | async: true 64 | timeout: if options.timeout? then options.timeout else 500 65 | success: successFunc 66 | error: errorFunc 67 | # hack to work with serving from local file system. 68 | # local file:// urls won't work in chrome: 69 | # http://code.google.com/p/chromium/issues/detail?id=40787 70 | if window.location.protocol == "file:" 71 | ajaxOptions.error = (xhr) -> successFunc($.parseJSON(xhr.responseText)) 72 | $.ajax(ajaxOptions) 73 | 74 | notifyDelegateLanguageLoaded = (data) -> 75 | if options.callback? 76 | options.callback(data, defaultCallback) 77 | else 78 | defaultCallback(data) 79 | 80 | defaultCallback = (data) -> 81 | $.localize.data[pkg] = data 82 | wrappedSet.each -> 83 | elem = $(this) 84 | key = elem.data("localize") 85 | key ||= elem.attr("rel").match(/localize\[(.*?)\]/)[1] 86 | value = valueForKey(key, data) 87 | localizeElement(elem, key, value) if value? 88 | 89 | localizeElement = (elem, key, value) -> 90 | if elem.is('input') then localizeInputElement(elem, key, value) 91 | else if elem.is('textarea') then localizeInputElement(elem, key, value) 92 | else if elem.is('img') then localizeImageElement(elem, key, value) 93 | else if elem.is('optgroup') then localizeOptgroupElement(elem, key, value) 94 | else unless $.isPlainObject(value) then elem.html(value) 95 | localizeForSpecialKeys(elem, value) if $.isPlainObject(value) 96 | 97 | localizeInputElement = (elem, key, value) -> 98 | val = if $.isPlainObject(value) then value.value else value 99 | if elem.is("[placeholder]") 100 | elem.attr("placeholder", val) 101 | else 102 | elem.val(val) 103 | 104 | localizeForSpecialKeys = (elem, value) -> 105 | setAttrFromValueForKey(elem, "title", value) 106 | setAttrFromValueForKey(elem, "href", value) 107 | setTextFromValueForKey(elem, "text", value) 108 | 109 | localizeOptgroupElement = (elem, key, value) -> 110 | elem.attr("label", value) 111 | 112 | localizeImageElement = (elem, key, value) -> 113 | setAttrFromValueForKey(elem, "alt", value) 114 | setAttrFromValueForKey(elem, "src", value) 115 | 116 | valueForKey = (key, data) -> 117 | keys = key.split(/\./) 118 | value = data 119 | for key in keys 120 | value = if value? then value[key] else null 121 | value 122 | 123 | setAttrFromValueForKey = (elem, key, value) -> 124 | value = valueForKey(key, value) 125 | elem.attr(key, value) if value? 126 | 127 | setTextFromValueForKey = (elem, key, value) -> 128 | value = valueForKey(key, value) 129 | elem.text(value) if value? 130 | 131 | regexify = (string_or_regex_or_array) -> 132 | if typeof(string_or_regex_or_array) == "string" 133 | "^" + string_or_regex_or_array + "$" 134 | else if string_or_regex_or_array.length? 135 | (regexify(thing) for thing in string_or_regex_or_array).join("|") 136 | else 137 | string_or_regex_or_array 138 | 139 | lang = normaliseLang(if options.language then options.language else $.defaultLanguage) 140 | if (options.skipLanguage && lang.match(regexify(options.skipLanguage))) 141 | deferred.resolve() 142 | else 143 | loadLanguage(pkg, lang, 1) 144 | 145 | wrappedSet.localizePromise = deferred 146 | 147 | wrappedSet 148 | 149 | $.fn.localize = $.localize 150 | $.localize.data = {} 151 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jquery.localize.js 2 | 3 | [![Build Status](https://travis-ci.org/coderifous/jquery-localize.png?branch=master)](https://travis-ci.org/coderifous/jquery-localize) 4 | 5 | A jQuery plugin that makes it easy to i18n your static web site. 6 | 7 | ## Synopsis 8 | * Lazily loads JSON translation files based on a simple naming convention. 9 | * By default, applies the translations to your document based on simple attribute convention. 10 | * Tested with jQuery versions 1.7.2, 1.8.3, 1.9.1, 1.10.2, 1.11.3, 2.0.3, 2.1.4 11 | 12 | ## Getting Started 13 | Download the [production version][min] or the [development version][max]. 14 | 15 | [min]: https://raw.github.com/coderifous/jquery-localize/master/dist/jquery.localize.min.js 16 | [max]: https://raw.github.com/coderifous/jquery-localize/master/dist/jquery.localize.js 17 | 18 | ### Load the jquery-localize plugin on your page. 19 | 20 | It's the file located at `dist/jquery.localize.js` 21 | 22 | ### Mark up tags whose content you want to be translated 23 | 24 | Somewhere in your html: 25 | 26 | ```html 27 |

Hello!

28 | ``` 29 | 30 | ### Provide a JSON language file that has translations: 31 | 32 | example-fr.json: 33 | 34 | { 35 | "greeting": "Bonjour!" 36 | } 37 | 38 | ### Use the localize plugin. 39 | 40 | ```html 41 | 48 | ``` 49 | 50 | ## Gory Details 51 | 52 | ### Language file loading 53 | 54 | The first argument of the localize method is the name of the language pack. You might have a different language pack for different parts of your website. 55 | 56 | Here's an example of loading several language packs: 57 | 58 | ```html 59 | 65 | ``` 66 | 67 | If the language of the browser were set to "fr", then the plugin would try to load: 68 | 69 | * header-fr.json 70 | * sidebar-fr.json 71 | * footer-fr.json 72 | 73 | if the language of the browser also had a country code, like "fr-FR", then the plugin would ALSO try to load: 74 | 75 | * header-fr-FR.json 76 | * sidebar-fr-FR.json 77 | * footer-fr-FR.json 78 | 79 | This let's you define partial language refinements for different regions. For instance, you can have the base language translation file for a language that translates 100 different phrases, and for countries were maybe a some of those phrases would be out of place, you can just provide a country-specific file with _just those special phrases_ defined. 80 | 81 | ### Skipping Languages (aka Optimizing for My Language) 82 | 83 | This is useful if you've got a default language. For example, if all of your content is served in english, then you probably don't want the overhead of loading up unecessary (and probably non-existant) english langauge packs (foo-en.json) 84 | 85 | You can tell the localize plugin to always skip certain languages using the skipLanguage option: 86 | 87 | ```html 88 | 100 | ``` 101 | 102 | ### Applying the language file 103 | 104 | If you rely on the default callback and use the "data-localize" attribute then the changes will be applied for you. 105 | 106 | ### Examples: 107 | 108 | **HTML:** 109 | 110 | ```html 111 |

Tracker Pro XT Deluxe

112 |

Search...

113 |

Go!

114 |

Use at your own risk.

115 |

Dashboard

116 |

Bug List

117 |

Logout

118 | ``` 119 | 120 | **application-es.json (fake spanish)** 121 | 122 | { 123 | "title": "Tracker Pro XT Deluxo", 124 | "search": { 125 | "placeholder": "Searcho...", 126 | "button": "Vamos!" 127 | }, 128 | "footer": { 129 | "disclaimer": "Bewaro." 130 | }, 131 | "menu": { 132 | "dashboard": "Dashboardo", 133 | "list": "Bug Listo", 134 | "logout": "Exito" 135 | } 136 | } 137 | 138 | **Localize it!** 139 | 140 | ```html 141 | 144 | ``` 145 | 146 | ### Callbacks 147 | 148 | You can provide a callback if you want to augment or replace the default callback provided by the plugin. Your callback should take at least 1 argument: the language data (contents of your json file). It can optionally accept a second argument, which is a reference to the default callback function. This is handy if you still want the default behavior, but also need to do something else with the language data. 149 | 150 | ```html 151 | 160 | ``` 161 | 162 | See the test/samples for working examples. 163 | 164 | # Contributing 165 | 166 | To contribute to this plugin, please read the [contributing guidelines](CONTRIBUTING.md). 167 | 168 | # Credits & Licensing 169 | 170 | Copyright (c) Jim Garvin (http://github.com/coderifous), 2008. 171 | 172 | Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. 173 | 174 | Written by Jim Garvin (@coderifous) for use on LMGTFY.com. 175 | Please use it, and contribute changes. 176 | 177 | http://github.com/coderifous/jquery-localize 178 | 179 | Based off of Keith Wood's Localisation jQuery plugin. 180 | http://keith-wood.name/localisation.html 181 | -------------------------------------------------------------------------------- /dist/jquery.localize.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Copyright (c) Jim Garvin (http://github.com/coderifous), 2008. 4 | Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses. 5 | Written by Jim Garvin (@coderifous) for use on LMGTFY.com. 6 | http://github.com/coderifous/jquery-localize 7 | Based off of Keith Wood's Localisation jQuery plugin. 8 | http://keith-wood.name/localisation.html 9 | */ 10 | (function($) { 11 | var normaliseLang; 12 | normaliseLang = function(lang) { 13 | lang = lang.replace(/_/, '-').toLowerCase(); 14 | if (lang.length > 3) { 15 | lang = lang.substring(0, 3) + lang.substring(3).toUpperCase(); 16 | } 17 | return lang; 18 | }; 19 | $.defaultLanguage = normaliseLang(navigator.languages && navigator.languages.length > 0 ? navigator.languages[0] : navigator.language || navigator.userLanguage); 20 | $.localize = function(pkg, options) { 21 | var defaultCallback, deferred, fileExtension, intermediateLangData, jsonCall, lang, loadLanguage, localizeElement, localizeForSpecialKeys, localizeImageElement, localizeInputElement, localizeOptgroupElement, notifyDelegateLanguageLoaded, regexify, setAttrFromValueForKey, setTextFromValueForKey, valueForKey, wrappedSet; 22 | if (options == null) { 23 | options = {}; 24 | } 25 | wrappedSet = this; 26 | intermediateLangData = {}; 27 | fileExtension = options.fileExtension || "json"; 28 | deferred = $.Deferred(); 29 | loadLanguage = function(pkg, lang, level) { 30 | var file; 31 | if (level == null) { 32 | level = 1; 33 | } 34 | switch (level) { 35 | case 1: 36 | intermediateLangData = {}; 37 | if (options.loadBase) { 38 | file = pkg + ("." + fileExtension); 39 | return jsonCall(file, pkg, lang, level); 40 | } else { 41 | return loadLanguage(pkg, lang, 2); 42 | } 43 | break; 44 | case 2: 45 | file = "" + pkg + "-" + (lang.split('-')[0]) + "." + fileExtension; 46 | return jsonCall(file, pkg, lang, level); 47 | case 3: 48 | file = "" + pkg + "-" + (lang.split('-').slice(0, 2).join('-')) + "." + fileExtension; 49 | return jsonCall(file, pkg, lang, level); 50 | default: 51 | return deferred.resolve(); 52 | } 53 | }; 54 | jsonCall = function(file, pkg, lang, level) { 55 | var ajaxOptions, errorFunc, successFunc; 56 | if (options.pathPrefix != null) { 57 | file = "" + options.pathPrefix + "/" + file; 58 | } 59 | successFunc = function(d) { 60 | $.extend(intermediateLangData, d); 61 | notifyDelegateLanguageLoaded(intermediateLangData); 62 | return loadLanguage(pkg, lang, level + 1); 63 | }; 64 | errorFunc = function() { 65 | if (level === 2 && lang.indexOf('-') > -1) { 66 | return loadLanguage(pkg, lang, level + 1); 67 | } else if (options.fallback && options.fallback !== lang) { 68 | return loadLanguage(pkg, options.fallback); 69 | } 70 | }; 71 | ajaxOptions = { 72 | url: file, 73 | dataType: "json", 74 | async: true, 75 | timeout: options.timeout != null ? options.timeout : 500, 76 | success: successFunc, 77 | error: errorFunc 78 | }; 79 | if (window.location.protocol === "file:") { 80 | ajaxOptions.error = function(xhr) { 81 | return successFunc($.parseJSON(xhr.responseText)); 82 | }; 83 | } 84 | return $.ajax(ajaxOptions); 85 | }; 86 | notifyDelegateLanguageLoaded = function(data) { 87 | if (options.callback != null) { 88 | return options.callback(data, defaultCallback); 89 | } else { 90 | return defaultCallback(data); 91 | } 92 | }; 93 | defaultCallback = function(data) { 94 | $.localize.data[pkg] = data; 95 | return wrappedSet.each(function() { 96 | var elem, key, value; 97 | elem = $(this); 98 | key = elem.data("localize"); 99 | key || (key = elem.attr("rel").match(/localize\[(.*?)\]/)[1]); 100 | value = valueForKey(key, data); 101 | if (value != null) { 102 | return localizeElement(elem, key, value); 103 | } 104 | }); 105 | }; 106 | localizeElement = function(elem, key, value) { 107 | if (elem.is('input')) { 108 | localizeInputElement(elem, key, value); 109 | } else if (elem.is('textarea')) { 110 | localizeInputElement(elem, key, value); 111 | } else if (elem.is('img')) { 112 | localizeImageElement(elem, key, value); 113 | } else if (elem.is('optgroup')) { 114 | localizeOptgroupElement(elem, key, value); 115 | } else if (!$.isPlainObject(value)) { 116 | elem.html(value); 117 | } 118 | if ($.isPlainObject(value)) { 119 | return localizeForSpecialKeys(elem, value); 120 | } 121 | }; 122 | localizeInputElement = function(elem, key, value) { 123 | var val; 124 | val = $.isPlainObject(value) ? value.value : value; 125 | if (elem.is("[placeholder]")) { 126 | return elem.attr("placeholder", val); 127 | } else { 128 | return elem.val(val); 129 | } 130 | }; 131 | localizeForSpecialKeys = function(elem, value) { 132 | setAttrFromValueForKey(elem, "title", value); 133 | setAttrFromValueForKey(elem, "href", value); 134 | return setTextFromValueForKey(elem, "text", value); 135 | }; 136 | localizeOptgroupElement = function(elem, key, value) { 137 | return elem.attr("label", value); 138 | }; 139 | localizeImageElement = function(elem, key, value) { 140 | setAttrFromValueForKey(elem, "alt", value); 141 | return setAttrFromValueForKey(elem, "src", value); 142 | }; 143 | valueForKey = function(key, data) { 144 | var keys, value, _i, _len; 145 | keys = key.split(/\./); 146 | value = data; 147 | for (_i = 0, _len = keys.length; _i < _len; _i++) { 148 | key = keys[_i]; 149 | value = value != null ? value[key] : null; 150 | } 151 | return value; 152 | }; 153 | setAttrFromValueForKey = function(elem, key, value) { 154 | value = valueForKey(key, value); 155 | if (value != null) { 156 | return elem.attr(key, value); 157 | } 158 | }; 159 | setTextFromValueForKey = function(elem, key, value) { 160 | value = valueForKey(key, value); 161 | if (value != null) { 162 | return elem.text(value); 163 | } 164 | }; 165 | regexify = function(string_or_regex_or_array) { 166 | var thing; 167 | if (typeof string_or_regex_or_array === "string") { 168 | return "^" + string_or_regex_or_array + "$"; 169 | } else if (string_or_regex_or_array.length != null) { 170 | return ((function() { 171 | var _i, _len, _results; 172 | _results = []; 173 | for (_i = 0, _len = string_or_regex_or_array.length; _i < _len; _i++) { 174 | thing = string_or_regex_or_array[_i]; 175 | _results.push(regexify(thing)); 176 | } 177 | return _results; 178 | })()).join("|"); 179 | } else { 180 | return string_or_regex_or_array; 181 | } 182 | }; 183 | lang = normaliseLang(options.language ? options.language : $.defaultLanguage); 184 | if (options.skipLanguage && lang.match(regexify(options.skipLanguage))) { 185 | deferred.resolve(); 186 | } else { 187 | loadLanguage(pkg, lang, 1); 188 | } 189 | wrappedSet.localizePromise = deferred; 190 | return wrappedSet; 191 | }; 192 | $.fn.localize = $.localize; 193 | return $.localize.data = {}; 194 | })(jQuery); 195 | -------------------------------------------------------------------------------- /libs/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 2.0.1 3 | * https://qunitjs.com/ 4 | * 5 | * Copyright jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * https://jquery.org/license 8 | * 9 | * Date: 2016-07-23T19:39Z 10 | */ 11 | 12 | /** Font Family and Sizes */ 13 | 14 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult { 15 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 16 | } 17 | 18 | #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 19 | #qunit-tests { font-size: smaller; } 20 | 21 | 22 | /** Resets */ 23 | 24 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | 30 | /** Header (excluding toolbar) */ 31 | 32 | #qunit-header { 33 | padding: 0.5em 0 0.5em 1em; 34 | 35 | color: #8699A4; 36 | background-color: #0D3349; 37 | 38 | font-size: 1.5em; 39 | line-height: 1em; 40 | font-weight: 400; 41 | 42 | border-radius: 5px 5px 0 0; 43 | } 44 | 45 | #qunit-header a { 46 | text-decoration: none; 47 | color: #C2CCD1; 48 | } 49 | 50 | #qunit-header a:hover, 51 | #qunit-header a:focus { 52 | color: #FFF; 53 | } 54 | 55 | #qunit-banner { 56 | height: 5px; 57 | } 58 | 59 | #qunit-filteredTest { 60 | padding: 0.5em 1em 0.5em 1em; 61 | color: #366097; 62 | background-color: #F4FF77; 63 | } 64 | 65 | #qunit-userAgent { 66 | padding: 0.5em 1em 0.5em 1em; 67 | color: #FFF; 68 | background-color: #2B81AF; 69 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 70 | } 71 | 72 | 73 | /** Toolbar */ 74 | 75 | #qunit-testrunner-toolbar { 76 | padding: 0.5em 1em 0.5em 1em; 77 | color: #5E740B; 78 | background-color: #EEE; 79 | } 80 | 81 | #qunit-testrunner-toolbar .clearfix { 82 | height: 0; 83 | clear: both; 84 | } 85 | 86 | #qunit-testrunner-toolbar label { 87 | display: inline-block; 88 | } 89 | 90 | #qunit-testrunner-toolbar input[type=checkbox], 91 | #qunit-testrunner-toolbar input[type=radio] { 92 | margin: 3px; 93 | vertical-align: -2px; 94 | } 95 | 96 | #qunit-testrunner-toolbar input[type=text] { 97 | box-sizing: border-box; 98 | height: 1.6em; 99 | } 100 | 101 | .qunit-url-config, 102 | .qunit-filter, 103 | #qunit-modulefilter { 104 | display: inline-block; 105 | line-height: 2.1em; 106 | } 107 | 108 | .qunit-filter, 109 | #qunit-modulefilter { 110 | float: right; 111 | position: relative; 112 | margin-left: 1em; 113 | } 114 | 115 | .qunit-url-config label { 116 | margin-right: 0.5em; 117 | } 118 | 119 | #qunit-modulefilter-search { 120 | box-sizing: border-box; 121 | width: 400px; 122 | } 123 | 124 | #qunit-modulefilter-search-container:after { 125 | position: absolute; 126 | right: 0.3em; 127 | content: "\25bc"; 128 | color: black; 129 | } 130 | 131 | #qunit-modulefilter-dropdown { 132 | /* align with #qunit-modulefilter-search */ 133 | box-sizing: border-box; 134 | width: 400px; 135 | position: absolute; 136 | right: 0; 137 | top: 50%; 138 | margin-top: 0.8em; 139 | 140 | border: 1px solid #D3D3D3; 141 | border-top: none; 142 | border-radius: 0 0 .25em .25em; 143 | color: #000; 144 | background-color: #F5F5F5; 145 | z-index: 99; 146 | } 147 | 148 | #qunit-modulefilter-dropdown a { 149 | color: inherit; 150 | text-decoration: none; 151 | } 152 | 153 | #qunit-modulefilter-dropdown .clickable.checked { 154 | font-weight: bold; 155 | color: #000; 156 | background-color: #D2E0E6; 157 | } 158 | 159 | #qunit-modulefilter-dropdown .clickable:hover { 160 | color: #FFF; 161 | background-color: #0D3349; 162 | } 163 | 164 | #qunit-modulefilter-actions { 165 | display: block; 166 | overflow: auto; 167 | 168 | /* align with #qunit-modulefilter-dropdown-list */ 169 | font: smaller/1.5em sans-serif; 170 | } 171 | 172 | #qunit-modulefilter-dropdown #qunit-modulefilter-actions > * { 173 | box-sizing: border-box; 174 | max-height: 2.8em; 175 | display: block; 176 | padding: 0.4em; 177 | } 178 | 179 | #qunit-modulefilter-dropdown #qunit-modulefilter-actions > button { 180 | float: right; 181 | font: inherit; 182 | } 183 | 184 | #qunit-modulefilter-dropdown #qunit-modulefilter-actions > :last-child { 185 | /* insert padding to align with checkbox margins */ 186 | padding-left: 3px; 187 | } 188 | 189 | #qunit-modulefilter-dropdown-list { 190 | max-height: 200px; 191 | overflow-y: auto; 192 | margin: 0; 193 | border-top: 2px groove threedhighlight; 194 | padding: 0.4em 0 0; 195 | font: smaller/1.5em sans-serif; 196 | } 197 | 198 | #qunit-modulefilter-dropdown-list li { 199 | white-space: nowrap; 200 | overflow: hidden; 201 | text-overflow: ellipsis; 202 | } 203 | 204 | #qunit-modulefilter-dropdown-list .clickable { 205 | display: block; 206 | padding-left: 0.15em; 207 | } 208 | 209 | 210 | /** Tests: Pass/Fail */ 211 | 212 | #qunit-tests { 213 | list-style-position: inside; 214 | } 215 | 216 | #qunit-tests li { 217 | padding: 0.4em 1em 0.4em 1em; 218 | border-bottom: 1px solid #FFF; 219 | list-style-position: inside; 220 | } 221 | 222 | #qunit-tests > li { 223 | display: none; 224 | } 225 | 226 | #qunit-tests li.running, 227 | #qunit-tests li.pass, 228 | #qunit-tests li.fail, 229 | #qunit-tests li.skipped { 230 | display: list-item; 231 | } 232 | 233 | #qunit-tests.hidepass { 234 | position: relative; 235 | } 236 | 237 | #qunit-tests.hidepass li.running, 238 | #qunit-tests.hidepass li.pass { 239 | visibility: hidden; 240 | position: absolute; 241 | width: 0; 242 | height: 0; 243 | padding: 0; 244 | border: 0; 245 | margin: 0; 246 | } 247 | 248 | #qunit-tests li strong { 249 | cursor: pointer; 250 | } 251 | 252 | #qunit-tests li.skipped strong { 253 | cursor: default; 254 | } 255 | 256 | #qunit-tests li a { 257 | padding: 0.5em; 258 | color: #C2CCD1; 259 | text-decoration: none; 260 | } 261 | 262 | #qunit-tests li p a { 263 | padding: 0.25em; 264 | color: #6B6464; 265 | } 266 | #qunit-tests li a:hover, 267 | #qunit-tests li a:focus { 268 | color: #000; 269 | } 270 | 271 | #qunit-tests li .runtime { 272 | float: right; 273 | font-size: smaller; 274 | } 275 | 276 | .qunit-assert-list { 277 | margin-top: 0.5em; 278 | padding: 0.5em; 279 | 280 | background-color: #FFF; 281 | 282 | border-radius: 5px; 283 | } 284 | 285 | .qunit-source { 286 | margin: 0.6em 0 0.3em; 287 | } 288 | 289 | .qunit-collapsed { 290 | display: none; 291 | } 292 | 293 | #qunit-tests table { 294 | border-collapse: collapse; 295 | margin-top: 0.2em; 296 | } 297 | 298 | #qunit-tests th { 299 | text-align: right; 300 | vertical-align: top; 301 | padding: 0 0.5em 0 0; 302 | } 303 | 304 | #qunit-tests td { 305 | vertical-align: top; 306 | } 307 | 308 | #qunit-tests pre { 309 | margin: 0; 310 | white-space: pre-wrap; 311 | word-wrap: break-word; 312 | } 313 | 314 | #qunit-tests del { 315 | color: #374E0C; 316 | background-color: #E0F2BE; 317 | text-decoration: none; 318 | } 319 | 320 | #qunit-tests ins { 321 | color: #500; 322 | background-color: #FFCACA; 323 | text-decoration: none; 324 | } 325 | 326 | /*** Test Counts */ 327 | 328 | #qunit-tests b.counts { color: #000; } 329 | #qunit-tests b.passed { color: #5E740B; } 330 | #qunit-tests b.failed { color: #710909; } 331 | 332 | #qunit-tests li li { 333 | padding: 5px; 334 | background-color: #FFF; 335 | border-bottom: none; 336 | list-style-position: inside; 337 | } 338 | 339 | /*** Passing Styles */ 340 | 341 | #qunit-tests li li.pass { 342 | color: #3C510C; 343 | background-color: #FFF; 344 | border-left: 10px solid #C6E746; 345 | } 346 | 347 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 348 | #qunit-tests .pass .test-name { color: #366097; } 349 | 350 | #qunit-tests .pass .test-actual, 351 | #qunit-tests .pass .test-expected { color: #999; } 352 | 353 | #qunit-banner.qunit-pass { background-color: #C6E746; } 354 | 355 | /*** Failing Styles */ 356 | 357 | #qunit-tests li li.fail { 358 | color: #710909; 359 | background-color: #FFF; 360 | border-left: 10px solid #EE5757; 361 | white-space: pre; 362 | } 363 | 364 | #qunit-tests > li:last-child { 365 | border-radius: 0 0 5px 5px; 366 | } 367 | 368 | #qunit-tests .fail { color: #000; background-color: #EE5757; } 369 | #qunit-tests .fail .test-name, 370 | #qunit-tests .fail .module-name { color: #000; } 371 | 372 | #qunit-tests .fail .test-actual { color: #EE5757; } 373 | #qunit-tests .fail .test-expected { color: #008000; } 374 | 375 | #qunit-banner.qunit-fail { background-color: #EE5757; } 376 | 377 | /*** Skipped tests */ 378 | 379 | #qunit-tests .skipped { 380 | background-color: #EBECE9; 381 | } 382 | 383 | #qunit-tests .qunit-skipped-label { 384 | background-color: #F4FF77; 385 | display: inline-block; 386 | font-style: normal; 387 | color: #366097; 388 | line-height: 1.8em; 389 | padding: 0 0.5em; 390 | margin: -0.4em 0.4em -0.4em 0; 391 | } 392 | 393 | /** Result */ 394 | 395 | #qunit-testresult { 396 | padding: 0.5em 1em 0.5em 1em; 397 | 398 | color: #2B81AF; 399 | background-color: #D2E0E6; 400 | 401 | border-bottom: 1px solid #FFF; 402 | } 403 | #qunit-testresult .module-name { 404 | font-weight: 700; 405 | } 406 | 407 | /** Fixture */ 408 | 409 | #qunit-fixture { 410 | position: absolute; 411 | top: -10000px; 412 | left: -10000px; 413 | width: 1000px; 414 | height: 1000px; 415 | } 416 | -------------------------------------------------------------------------------- /test/localize_test.coffee: -------------------------------------------------------------------------------- 1 | do ($ = jQuery) -> 2 | 3 | module = QUnit.module 4 | test = QUnit.test 5 | testOpts = {} 6 | 7 | asyncTest = (desc, testFn) -> 8 | test desc, (assert) -> 9 | done = assert.async() 10 | deferredAssertions = testFn(assert) 11 | deferredAssertions.then -> done() 12 | 13 | localizableTagWithRel = (tag, localizeKey, attributes) -> 14 | t = $("<#{tag}>").attr("rel", "localize[#{localizeKey}]") 15 | applyTagAttributes(t, attributes) 16 | 17 | localizableTagWithDataLocalize = (tag, localizeKey, attributes) -> 18 | t = $("<#{tag}>").attr("data-localize", localizeKey) 19 | applyTagAttributes(t, attributes) 20 | 21 | applyTagAttributes = (tag, attributes) -> 22 | if attributes.text? 23 | tag.text(attributes.text) 24 | delete attributes.text 25 | if attributes.val? 26 | tag.val(attributes.val) 27 | delete attributes.val 28 | tag.attr(k,v) for k, v of attributes 29 | tag 30 | 31 | module "Basic Usage", 32 | beforeEach: -> 33 | testOpts = language: "ja", pathPrefix: "lang" 34 | 35 | asyncTest "basic tag text substitution", (assert) -> 36 | t = localizableTagWithRel("p", "basic", text: "basic fail") 37 | t.localize("test", testOpts).localizePromise.then -> 38 | assert.equal t.text(), "basic success" 39 | 40 | asyncTest "basic tag text substitution using data-localize instead of rel", (assert) -> 41 | t = localizableTagWithDataLocalize("p", "basic", text: "basic fail") 42 | t.localize("test", testOpts).localizePromise.then -> 43 | assert.equal t.text(), "basic success" 44 | 45 | asyncTest "basic tag text substitution with nested key", (assert) -> 46 | t = localizableTagWithRel("p", "test.nested", text: "nested fail") 47 | t.localize("test", testOpts).localizePromise.then -> 48 | assert.equal t.text(), "nested success" 49 | 50 | asyncTest "basic tag text substitution for special title key", (assert) -> 51 | t = localizableTagWithDataLocalize("p", "with_title", text: "with_title element fail", title: "with_title title fail") 52 | t.localize("test", testOpts).localizePromise.then -> 53 | assert.equal t.text(), "with_title text success" 54 | assert.equal t.attr("title"), "with_title title success" 55 | 56 | asyncTest "input tag value substitution", (assert) -> 57 | t = localizableTagWithRel("input", "test.input", val: "input fail") 58 | t.localize("test", testOpts).localizePromise.then -> 59 | assert.equal t.val(), "input success" 60 | 61 | asyncTest "input test value after second localization without key", (assert) -> 62 | t = localizableTagWithRel("input", "test.input", val: "input fail") 63 | d = $.Deferred() 64 | t.localize("test", testOpts).localizePromise.then -> 65 | t.localize("test2", testOpts).localizePromise.then -> 66 | assert.equal t.val(), "input success" 67 | d.resolve() 68 | d 69 | 70 | asyncTest "input tag placeholder substitution", (assert) -> 71 | t = localizableTagWithRel("input", "test.input", placeholder: "placeholder fail") 72 | t.localize("test", testOpts).localizePromise.then -> 73 | assert.equal t.attr("placeholder"), "input success" 74 | 75 | asyncTest "textarea tag placeholder substitution", (assert) -> 76 | t = localizableTagWithRel("textarea", "test.input", placeholder: "placeholder fail") 77 | t.localize("test", testOpts).localizePromise.then -> 78 | assert.equal t.attr("placeholder"), "input success" 79 | 80 | asyncTest "titled input tag value substitution", (assert) -> 81 | t = localizableTagWithRel("input", "test.input_as_obj", val: "input_as_obj fail") 82 | t.localize("test", testOpts).localizePromise.then -> 83 | assert.equal t.val(), "input_as_obj value success" 84 | 85 | asyncTest "titled input tag title substitution", (assert) -> 86 | t = localizableTagWithRel("input", "test.input_as_obj", val: "input_as_obj fail") 87 | t.localize("test", testOpts).localizePromise.then -> 88 | assert.equal t.attr("title"), "input_as_obj title success" 89 | 90 | asyncTest "titled input tag placeholder substitution", (assert) -> 91 | t = localizableTagWithRel("input", "test.input_as_obj", placeholder: "placeholder fail") 92 | t.localize("test", testOpts).localizePromise.then -> 93 | assert.equal t.attr("placeholder"), "input_as_obj value success" 94 | 95 | asyncTest "image tag src, alt, and title substitution", (assert) -> 96 | t = localizableTagWithRel("img", "test.ruby_image", src: "ruby_square.gif", alt: "a square ruby", title: "A Square Ruby") 97 | t.localize("test", testOpts).localizePromise.then -> 98 | assert.equal t.attr("src"), "ruby_round.gif" 99 | assert.equal t.attr("alt"), "a round ruby" 100 | assert.equal t.attr("title"), "A Round Ruby" 101 | 102 | asyncTest "link tag href substitution", (assert) -> 103 | t = localizableTagWithRel("a", "test.link", href: "http://fail", text: "fail") 104 | t.localize("test", testOpts).localizePromise.then -> 105 | assert.equal t.attr("href"), "http://success" 106 | assert.equal t.text(), "success" 107 | 108 | asyncTest "chained call", (assert) -> 109 | t = localizableTagWithRel("p", "basic", text: "basic fail") 110 | t.localize("test", testOpts).localize("test", testOpts).localizePromise.then -> 111 | assert.equal t.text(), "basic success" 112 | 113 | asyncTest "alternative file extension", (assert) -> 114 | t = localizableTagWithRel("p", "basic", text: "basic fail") 115 | t.localize("test", $.extend({ fileExtension: "foo" }, testOpts)).localizePromise.then -> 116 | assert.equal t.text(), "basic success foo" 117 | 118 | selectTag = null 119 | module "Basic Usage for 121 | testOpts = language: "ja", pathPrefix: "lang" 122 | selectTag = $('') 127 | 128 | asyncTest "optgroup tag label substitution", (assert) -> 129 | t = selectTag.find("optgroup") 130 | t.localize("test", testOpts).localizePromise.then -> 131 | assert.equal t.attr("label"), "optgroup success" 132 | 133 | asyncTest "option tag text substitution", (assert) -> 134 | t = selectTag.find("option") 135 | t.localize("test", testOpts).localizePromise.then -> 136 | assert.equal t.text(), "option success" 137 | 138 | module "Options" 139 | 140 | asyncTest "fallback language loads", (assert) -> 141 | opts = language: "fo", fallback: "ja", pathPrefix: "lang" 142 | t = localizableTagWithRel("p", "basic", text: "basic fail") 143 | t.localize("test", opts).localizePromise.then -> 144 | assert.equal t.text(), "basic success" 145 | 146 | asyncTest "pathPrefix loads lang files from custom path", (assert) -> 147 | opts = language: "fo", pathPrefix: "/test/lang/custom" 148 | t = localizableTagWithRel("p", "path_prefix", text: "pathPrefix fail") 149 | t.localize("test", opts).localizePromise.then -> 150 | assert.equal t.text(), "pathPrefix success" 151 | 152 | asyncTest "custom callback is fired", (assert) -> 153 | opts = language: "ja", pathPrefix: "lang" 154 | opts.callback = (data, defaultCallback) -> 155 | data.custom_callback = "custom callback success" 156 | defaultCallback(data) 157 | t = localizableTagWithRel("p", "custom_callback", text: "custom callback fail") 158 | t.localize("test", opts).localizePromise.then -> 159 | assert.equal t.text(), "custom callback success" 160 | 161 | asyncTest "language with country code", (assert) -> 162 | opts = language: "ja-XX", pathPrefix: "lang" 163 | t = localizableTagWithRel("p", "message", text: "country code fail") 164 | t.localize("test", opts).localizePromise.then -> 165 | assert.equal t.text(), "country code success" 166 | 167 | # Ref: https://github.com/coderifous/jquery-localize/issues/50 168 | asyncTest "three-letter language code", (assert) -> 169 | opts = language: "ast", pathPrefix: "lang" 170 | t = localizableTagWithRel("p", "basic", text: "basic fail") 171 | t.localize("test", opts).localizePromise.then -> 172 | assert.equal t.text(), "basic success" 173 | 174 | # Ref: https://github.com/coderifous/jquery-localize/issues/47 175 | asyncTest "language-country code with no language-only file", (assert) -> 176 | opts = language: "zh-CN", pathPrefix: "lang" 177 | t = localizableTagWithRel("p", "basic", text: "basic fail") 178 | t.localize("test", opts).localizePromise.then -> 179 | assert.equal t.text(), "basic success zh-CN" 180 | 181 | module "Language optimization" 182 | 183 | asyncTest "skipping language using string match", (assert) -> 184 | opts = language: "en", pathPrefix: "lang", skipLanguage: "en" 185 | t = localizableTagWithRel("p", "en_message", text: "en not loaded") 186 | t.localize("test", opts).localizePromise.then -> 187 | assert.equal t.text(), "en not loaded" 188 | 189 | asyncTest "skipping language using regex match", (assert) -> 190 | opts = language: "en-US", pathPrefix: "lang", skipLanguage: /^en/ 191 | t = localizableTagWithRel("p", "en_us_message", text: "en-US not loaded") 192 | t.localize("test", opts).localizePromise.then -> 193 | assert.equal t.text(), "en-US not loaded" 194 | 195 | asyncTest "skipping language using array match", (assert) -> 196 | opts = language: "en", pathPrefix: "lang", skipLanguage: ["en", "en-US"] 197 | t = localizableTagWithRel("p", "en_message", text: "en not loaded") 198 | t.localize("test", opts).localizePromise.then -> 199 | assert.equal t.text(), "en not loaded" 200 | 201 | asyncTest "skipping region language using array match", (assert) -> 202 | opts = language: "en-US", pathPrefix: "lang", skipLanguage: ["en", "en-US"] 203 | t = localizableTagWithRel("p", "en_us_message", text: "en-US not loaded") 204 | t.localize("test", opts).localizePromise.then -> 205 | assert.equal t.text(), "en-US not loaded" 206 | -------------------------------------------------------------------------------- /test/localize_test.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | var applyTagAttributes, asyncTest, localizableTagWithDataLocalize, localizableTagWithRel, module, selectTag, test, testOpts; 3 | module = QUnit.module; 4 | test = QUnit.test; 5 | testOpts = {}; 6 | asyncTest = function(desc, testFn) { 7 | return test(desc, function(assert) { 8 | var deferredAssertions, done; 9 | done = assert.async(); 10 | deferredAssertions = testFn(assert); 11 | return deferredAssertions.then(function() { 12 | return done(); 13 | }); 14 | }); 15 | }; 16 | localizableTagWithRel = function(tag, localizeKey, attributes) { 17 | var t; 18 | t = $("<" + tag + ">").attr("rel", "localize[" + localizeKey + "]"); 19 | return applyTagAttributes(t, attributes); 20 | }; 21 | localizableTagWithDataLocalize = function(tag, localizeKey, attributes) { 22 | var t; 23 | t = $("<" + tag + ">").attr("data-localize", localizeKey); 24 | return applyTagAttributes(t, attributes); 25 | }; 26 | applyTagAttributes = function(tag, attributes) { 27 | var k, v; 28 | if (attributes.text != null) { 29 | tag.text(attributes.text); 30 | delete attributes.text; 31 | } 32 | if (attributes.val != null) { 33 | tag.val(attributes.val); 34 | delete attributes.val; 35 | } 36 | for (k in attributes) { 37 | v = attributes[k]; 38 | tag.attr(k, v); 39 | } 40 | return tag; 41 | }; 42 | module("Basic Usage", { 43 | beforeEach: function() { 44 | return testOpts = { 45 | language: "ja", 46 | pathPrefix: "lang" 47 | }; 48 | } 49 | }); 50 | asyncTest("basic tag text substitution", function(assert) { 51 | var t; 52 | t = localizableTagWithRel("p", "basic", { 53 | text: "basic fail" 54 | }); 55 | return t.localize("test", testOpts).localizePromise.then(function() { 56 | return assert.equal(t.text(), "basic success"); 57 | }); 58 | }); 59 | asyncTest("basic tag text substitution using data-localize instead of rel", function(assert) { 60 | var t; 61 | t = localizableTagWithDataLocalize("p", "basic", { 62 | text: "basic fail" 63 | }); 64 | return t.localize("test", testOpts).localizePromise.then(function() { 65 | return assert.equal(t.text(), "basic success"); 66 | }); 67 | }); 68 | asyncTest("basic tag text substitution with nested key", function(assert) { 69 | var t; 70 | t = localizableTagWithRel("p", "test.nested", { 71 | text: "nested fail" 72 | }); 73 | return t.localize("test", testOpts).localizePromise.then(function() { 74 | return assert.equal(t.text(), "nested success"); 75 | }); 76 | }); 77 | asyncTest("basic tag text substitution for special title key", function(assert) { 78 | var t; 79 | t = localizableTagWithDataLocalize("p", "with_title", { 80 | text: "with_title element fail", 81 | title: "with_title title fail" 82 | }); 83 | return t.localize("test", testOpts).localizePromise.then(function() { 84 | assert.equal(t.text(), "with_title text success"); 85 | return assert.equal(t.attr("title"), "with_title title success"); 86 | }); 87 | }); 88 | asyncTest("input tag value substitution", function(assert) { 89 | var t; 90 | t = localizableTagWithRel("input", "test.input", { 91 | val: "input fail" 92 | }); 93 | return t.localize("test", testOpts).localizePromise.then(function() { 94 | return assert.equal(t.val(), "input success"); 95 | }); 96 | }); 97 | asyncTest("input test value after second localization without key", function(assert) { 98 | var d, t; 99 | t = localizableTagWithRel("input", "test.input", { 100 | val: "input fail" 101 | }); 102 | d = $.Deferred(); 103 | t.localize("test", testOpts).localizePromise.then(function() { 104 | return t.localize("test2", testOpts).localizePromise.then(function() { 105 | assert.equal(t.val(), "input success"); 106 | return d.resolve(); 107 | }); 108 | }); 109 | return d; 110 | }); 111 | asyncTest("input tag placeholder substitution", function(assert) { 112 | var t; 113 | t = localizableTagWithRel("input", "test.input", { 114 | placeholder: "placeholder fail" 115 | }); 116 | return t.localize("test", testOpts).localizePromise.then(function() { 117 | return assert.equal(t.attr("placeholder"), "input success"); 118 | }); 119 | }); 120 | asyncTest("textarea tag placeholder substitution", function(assert) { 121 | var t; 122 | t = localizableTagWithRel("textarea", "test.input", { 123 | placeholder: "placeholder fail" 124 | }); 125 | return t.localize("test", testOpts).localizePromise.then(function() { 126 | return assert.equal(t.attr("placeholder"), "input success"); 127 | }); 128 | }); 129 | asyncTest("titled input tag value substitution", function(assert) { 130 | var t; 131 | t = localizableTagWithRel("input", "test.input_as_obj", { 132 | val: "input_as_obj fail" 133 | }); 134 | return t.localize("test", testOpts).localizePromise.then(function() { 135 | return assert.equal(t.val(), "input_as_obj value success"); 136 | }); 137 | }); 138 | asyncTest("titled input tag title substitution", function(assert) { 139 | var t; 140 | t = localizableTagWithRel("input", "test.input_as_obj", { 141 | val: "input_as_obj fail" 142 | }); 143 | return t.localize("test", testOpts).localizePromise.then(function() { 144 | return assert.equal(t.attr("title"), "input_as_obj title success"); 145 | }); 146 | }); 147 | asyncTest("titled input tag placeholder substitution", function(assert) { 148 | var t; 149 | t = localizableTagWithRel("input", "test.input_as_obj", { 150 | placeholder: "placeholder fail" 151 | }); 152 | return t.localize("test", testOpts).localizePromise.then(function() { 153 | return assert.equal(t.attr("placeholder"), "input_as_obj value success"); 154 | }); 155 | }); 156 | asyncTest("image tag src, alt, and title substitution", function(assert) { 157 | var t; 158 | t = localizableTagWithRel("img", "test.ruby_image", { 159 | src: "ruby_square.gif", 160 | alt: "a square ruby", 161 | title: "A Square Ruby" 162 | }); 163 | return t.localize("test", testOpts).localizePromise.then(function() { 164 | assert.equal(t.attr("src"), "ruby_round.gif"); 165 | assert.equal(t.attr("alt"), "a round ruby"); 166 | return assert.equal(t.attr("title"), "A Round Ruby"); 167 | }); 168 | }); 169 | asyncTest("link tag href substitution", function(assert) { 170 | var t; 171 | t = localizableTagWithRel("a", "test.link", { 172 | href: "http://fail", 173 | text: "fail" 174 | }); 175 | return t.localize("test", testOpts).localizePromise.then(function() { 176 | assert.equal(t.attr("href"), "http://success"); 177 | return assert.equal(t.text(), "success"); 178 | }); 179 | }); 180 | asyncTest("chained call", function(assert) { 181 | var t; 182 | t = localizableTagWithRel("p", "basic", { 183 | text: "basic fail" 184 | }); 185 | return t.localize("test", testOpts).localize("test", testOpts).localizePromise.then(function() { 186 | return assert.equal(t.text(), "basic success"); 187 | }); 188 | }); 189 | asyncTest("alternative file extension", function(assert) { 190 | var t; 191 | t = localizableTagWithRel("p", "basic", { 192 | text: "basic fail" 193 | }); 194 | return t.localize("test", $.extend({ 195 | fileExtension: "foo" 196 | }, testOpts)).localizePromise.then(function() { 197 | return assert.equal(t.text(), "basic success foo"); 198 | }); 199 | }); 200 | selectTag = null; 201 | module("Basic Usage for '); 208 | } 209 | }); 210 | asyncTest("optgroup tag label substitution", function(assert) { 211 | var t; 212 | t = selectTag.find("optgroup"); 213 | return t.localize("test", testOpts).localizePromise.then(function() { 214 | return assert.equal(t.attr("label"), "optgroup success"); 215 | }); 216 | }); 217 | asyncTest("option tag text substitution", function(assert) { 218 | var t; 219 | t = selectTag.find("option"); 220 | return t.localize("test", testOpts).localizePromise.then(function() { 221 | return assert.equal(t.text(), "option success"); 222 | }); 223 | }); 224 | module("Options"); 225 | asyncTest("fallback language loads", function(assert) { 226 | var opts, t; 227 | opts = { 228 | language: "fo", 229 | fallback: "ja", 230 | pathPrefix: "lang" 231 | }; 232 | t = localizableTagWithRel("p", "basic", { 233 | text: "basic fail" 234 | }); 235 | return t.localize("test", opts).localizePromise.then(function() { 236 | return assert.equal(t.text(), "basic success"); 237 | }); 238 | }); 239 | asyncTest("pathPrefix loads lang files from custom path", function(assert) { 240 | var opts, t; 241 | opts = { 242 | language: "fo", 243 | pathPrefix: "/test/lang/custom" 244 | }; 245 | t = localizableTagWithRel("p", "path_prefix", { 246 | text: "pathPrefix fail" 247 | }); 248 | return t.localize("test", opts).localizePromise.then(function() { 249 | return assert.equal(t.text(), "pathPrefix success"); 250 | }); 251 | }); 252 | asyncTest("custom callback is fired", function(assert) { 253 | var opts, t; 254 | opts = { 255 | language: "ja", 256 | pathPrefix: "lang" 257 | }; 258 | opts.callback = function(data, defaultCallback) { 259 | data.custom_callback = "custom callback success"; 260 | return defaultCallback(data); 261 | }; 262 | t = localizableTagWithRel("p", "custom_callback", { 263 | text: "custom callback fail" 264 | }); 265 | return t.localize("test", opts).localizePromise.then(function() { 266 | return assert.equal(t.text(), "custom callback success"); 267 | }); 268 | }); 269 | asyncTest("language with country code", function(assert) { 270 | var opts, t; 271 | opts = { 272 | language: "ja-XX", 273 | pathPrefix: "lang" 274 | }; 275 | t = localizableTagWithRel("p", "message", { 276 | text: "country code fail" 277 | }); 278 | return t.localize("test", opts).localizePromise.then(function() { 279 | return assert.equal(t.text(), "country code success"); 280 | }); 281 | }); 282 | asyncTest("three-letter language code", function(assert) { 283 | var opts, t; 284 | opts = { 285 | language: "ast", 286 | pathPrefix: "lang" 287 | }; 288 | t = localizableTagWithRel("p", "basic", { 289 | text: "basic fail" 290 | }); 291 | return t.localize("test", opts).localizePromise.then(function() { 292 | return assert.equal(t.text(), "basic success"); 293 | }); 294 | }); 295 | asyncTest("language-country code with no language-only file", function(assert) { 296 | var opts, t; 297 | opts = { 298 | language: "zh-CN", 299 | pathPrefix: "lang" 300 | }; 301 | t = localizableTagWithRel("p", "basic", { 302 | text: "basic fail" 303 | }); 304 | return t.localize("test", opts).localizePromise.then(function() { 305 | return assert.equal(t.text(), "basic success zh-CN"); 306 | }); 307 | }); 308 | module("Language optimization"); 309 | asyncTest("skipping language using string match", function(assert) { 310 | var opts, t; 311 | opts = { 312 | language: "en", 313 | pathPrefix: "lang", 314 | skipLanguage: "en" 315 | }; 316 | t = localizableTagWithRel("p", "en_message", { 317 | text: "en not loaded" 318 | }); 319 | return t.localize("test", opts).localizePromise.then(function() { 320 | return assert.equal(t.text(), "en not loaded"); 321 | }); 322 | }); 323 | asyncTest("skipping language using regex match", function(assert) { 324 | var opts, t; 325 | opts = { 326 | language: "en-US", 327 | pathPrefix: "lang", 328 | skipLanguage: /^en/ 329 | }; 330 | t = localizableTagWithRel("p", "en_us_message", { 331 | text: "en-US not loaded" 332 | }); 333 | return t.localize("test", opts).localizePromise.then(function() { 334 | return assert.equal(t.text(), "en-US not loaded"); 335 | }); 336 | }); 337 | asyncTest("skipping language using array match", function(assert) { 338 | var opts, t; 339 | opts = { 340 | language: "en", 341 | pathPrefix: "lang", 342 | skipLanguage: ["en", "en-US"] 343 | }; 344 | t = localizableTagWithRel("p", "en_message", { 345 | text: "en not loaded" 346 | }); 347 | return t.localize("test", opts).localizePromise.then(function() { 348 | return assert.equal(t.text(), "en not loaded"); 349 | }); 350 | }); 351 | return asyncTest("skipping region language using array match", function(assert) { 352 | var opts, t; 353 | opts = { 354 | language: "en-US", 355 | pathPrefix: "lang", 356 | skipLanguage: ["en", "en-US"] 357 | }; 358 | t = localizableTagWithRel("p", "en_us_message", { 359 | text: "en-US not loaded" 360 | }); 361 | return t.localize("test", opts).localizePromise.then(function() { 362 | return assert.equal(t.text(), "en-US not loaded"); 363 | }); 364 | }); 365 | })(jQuery); 366 | -------------------------------------------------------------------------------- /libs/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * QUnit 2.0.1 3 | * https://qunitjs.com/ 4 | * 5 | * Copyright jQuery Foundation and other contributors 6 | * Released under the MIT license 7 | * https://jquery.org/license 8 | * 9 | * Date: 2016-07-23T19:39Z 10 | */ 11 | 12 | ( function( global ) { 13 | 14 | var QUnit = {}; 15 | 16 | var Date = global.Date; 17 | var now = Date.now || function() { 18 | return new Date().getTime(); 19 | }; 20 | 21 | var setTimeout = global.setTimeout; 22 | var clearTimeout = global.clearTimeout; 23 | 24 | // Store a local window from the global to allow direct references. 25 | var window = global.window; 26 | 27 | var defined = { 28 | document: window && window.document !== undefined, 29 | setTimeout: setTimeout !== undefined, 30 | sessionStorage: ( function() { 31 | var x = "qunit-test-string"; 32 | try { 33 | sessionStorage.setItem( x, x ); 34 | sessionStorage.removeItem( x ); 35 | return true; 36 | } catch ( e ) { 37 | return false; 38 | } 39 | }() ) 40 | }; 41 | 42 | var fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ); 43 | var globalStartCalled = false; 44 | var runStarted = false; 45 | 46 | var autorun = false; 47 | 48 | var toString = Object.prototype.toString, 49 | hasOwn = Object.prototype.hasOwnProperty; 50 | 51 | // Returns a new Array with the elements that are in a but not in b 52 | function diff( a, b ) { 53 | var i, j, 54 | result = a.slice(); 55 | 56 | for ( i = 0; i < result.length; i++ ) { 57 | for ( j = 0; j < b.length; j++ ) { 58 | if ( result[ i ] === b[ j ] ) { 59 | result.splice( i, 1 ); 60 | i--; 61 | break; 62 | } 63 | } 64 | } 65 | return result; 66 | } 67 | 68 | // From jquery.js 69 | function inArray( elem, array ) { 70 | if ( array.indexOf ) { 71 | return array.indexOf( elem ); 72 | } 73 | 74 | for ( var i = 0, length = array.length; i < length; i++ ) { 75 | if ( array[ i ] === elem ) { 76 | return i; 77 | } 78 | } 79 | 80 | return -1; 81 | } 82 | 83 | /** 84 | * Makes a clone of an object using only Array or Object as base, 85 | * and copies over the own enumerable properties. 86 | * 87 | * @param {Object} obj 88 | * @return {Object} New object with only the own properties (recursively). 89 | */ 90 | function objectValues ( obj ) { 91 | var key, val, 92 | vals = QUnit.is( "array", obj ) ? [] : {}; 93 | for ( key in obj ) { 94 | if ( hasOwn.call( obj, key ) ) { 95 | val = obj[ key ]; 96 | vals[ key ] = val === Object( val ) ? objectValues( val ) : val; 97 | } 98 | } 99 | return vals; 100 | } 101 | 102 | function extend( a, b, undefOnly ) { 103 | for ( var prop in b ) { 104 | if ( hasOwn.call( b, prop ) ) { 105 | if ( b[ prop ] === undefined ) { 106 | delete a[ prop ]; 107 | } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { 108 | a[ prop ] = b[ prop ]; 109 | } 110 | } 111 | } 112 | 113 | return a; 114 | } 115 | 116 | function objectType( obj ) { 117 | if ( typeof obj === "undefined" ) { 118 | return "undefined"; 119 | } 120 | 121 | // Consider: typeof null === object 122 | if ( obj === null ) { 123 | return "null"; 124 | } 125 | 126 | var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ), 127 | type = match && match[ 1 ]; 128 | 129 | switch ( type ) { 130 | case "Number": 131 | if ( isNaN( obj ) ) { 132 | return "nan"; 133 | } 134 | return "number"; 135 | case "String": 136 | case "Boolean": 137 | case "Array": 138 | case "Set": 139 | case "Map": 140 | case "Date": 141 | case "RegExp": 142 | case "Function": 143 | case "Symbol": 144 | return type.toLowerCase(); 145 | } 146 | if ( typeof obj === "object" ) { 147 | return "object"; 148 | } 149 | } 150 | 151 | // Safe object type checking 152 | function is( type, obj ) { 153 | return QUnit.objectType( obj ) === type; 154 | } 155 | 156 | // Doesn't support IE9, it will return undefined on these browsers 157 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack 158 | function extractStacktrace( e, offset ) { 159 | offset = offset === undefined ? 4 : offset; 160 | 161 | var stack, include, i; 162 | 163 | if ( e.stack ) { 164 | stack = e.stack.split( "\n" ); 165 | if ( /^error$/i.test( stack[ 0 ] ) ) { 166 | stack.shift(); 167 | } 168 | if ( fileName ) { 169 | include = []; 170 | for ( i = offset; i < stack.length; i++ ) { 171 | if ( stack[ i ].indexOf( fileName ) !== -1 ) { 172 | break; 173 | } 174 | include.push( stack[ i ] ); 175 | } 176 | if ( include.length ) { 177 | return include.join( "\n" ); 178 | } 179 | } 180 | return stack[ offset ]; 181 | } 182 | } 183 | 184 | function sourceFromStacktrace( offset ) { 185 | var error = new Error(); 186 | 187 | // Support: Safari <=7 only, IE <=10 - 11 only 188 | // Not all browsers generate the `stack` property for `new Error()`, see also #636 189 | if ( !error.stack ) { 190 | try { 191 | throw error; 192 | } catch ( err ) { 193 | error = err; 194 | } 195 | } 196 | 197 | return extractStacktrace( error, offset ); 198 | } 199 | 200 | /** 201 | * Config object: Maintain internal state 202 | * Later exposed as QUnit.config 203 | * `config` initialized at top of scope 204 | */ 205 | var config = { 206 | 207 | // The queue of tests to run 208 | queue: [], 209 | 210 | // Block until document ready 211 | blocking: true, 212 | 213 | // By default, run previously failed tests first 214 | // very useful in combination with "Hide passed tests" checked 215 | reorder: true, 216 | 217 | // By default, modify document.title when suite is done 218 | altertitle: true, 219 | 220 | // HTML Reporter: collapse every test except the first failing test 221 | // If false, all failing tests will be expanded 222 | collapse: true, 223 | 224 | // By default, scroll to top of the page when suite is done 225 | scrolltop: true, 226 | 227 | // Depth up-to which object will be dumped 228 | maxDepth: 5, 229 | 230 | // When enabled, all tests must call expect() 231 | requireExpects: false, 232 | 233 | // Placeholder for user-configurable form-exposed URL parameters 234 | urlConfig: [], 235 | 236 | // Set of all modules. 237 | modules: [], 238 | 239 | // Stack of nested modules 240 | moduleStack: [], 241 | 242 | // The first unnamed module 243 | currentModule: { 244 | name: "", 245 | tests: [] 246 | }, 247 | 248 | callbacks: {} 249 | }; 250 | 251 | // Push a loose unnamed module to the modules collection 252 | config.modules.push( config.currentModule ); 253 | 254 | // Register logging callbacks 255 | function registerLoggingCallbacks( obj ) { 256 | var i, l, key, 257 | callbackNames = [ "begin", "done", "log", "testStart", "testDone", 258 | "moduleStart", "moduleDone" ]; 259 | 260 | function registerLoggingCallback( key ) { 261 | var loggingCallback = function( callback ) { 262 | if ( objectType( callback ) !== "function" ) { 263 | throw new Error( 264 | "QUnit logging methods require a callback function as their first parameters." 265 | ); 266 | } 267 | 268 | config.callbacks[ key ].push( callback ); 269 | }; 270 | 271 | return loggingCallback; 272 | } 273 | 274 | for ( i = 0, l = callbackNames.length; i < l; i++ ) { 275 | key = callbackNames[ i ]; 276 | 277 | // Initialize key collection of logging callback 278 | if ( objectType( config.callbacks[ key ] ) === "undefined" ) { 279 | config.callbacks[ key ] = []; 280 | } 281 | 282 | obj[ key ] = registerLoggingCallback( key ); 283 | } 284 | } 285 | 286 | function runLoggingCallbacks( key, args ) { 287 | var i, l, callbacks; 288 | 289 | callbacks = config.callbacks[ key ]; 290 | for ( i = 0, l = callbacks.length; i < l; i++ ) { 291 | callbacks[ i ]( args ); 292 | } 293 | } 294 | 295 | ( function() { 296 | if ( !defined.document ) { 297 | return; 298 | } 299 | 300 | // `onErrorFnPrev` initialized at top of scope 301 | // Preserve other handlers 302 | var onErrorFnPrev = window.onerror; 303 | 304 | // Cover uncaught exceptions 305 | // Returning true will suppress the default browser handler, 306 | // returning false will let it run. 307 | window.onerror = function( error, filePath, linerNr ) { 308 | var ret = false; 309 | if ( onErrorFnPrev ) { 310 | ret = onErrorFnPrev( error, filePath, linerNr ); 311 | } 312 | 313 | // Treat return value as window.onerror itself does, 314 | // Only do our handling if not suppressed. 315 | if ( ret !== true ) { 316 | if ( QUnit.config.current ) { 317 | if ( QUnit.config.current.ignoreGlobalErrors ) { 318 | return true; 319 | } 320 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 321 | } else { 322 | QUnit.test( "global failure", extend( function() { 323 | QUnit.pushFailure( error, filePath + ":" + linerNr ); 324 | }, { validTest: true } ) ); 325 | } 326 | return false; 327 | } 328 | 329 | return ret; 330 | }; 331 | }() ); 332 | 333 | // Figure out if we're running the tests from a server or not 334 | QUnit.isLocal = !( defined.document && window.location.protocol !== "file:" ); 335 | 336 | // Expose the current QUnit version 337 | QUnit.version = "2.0.1"; 338 | 339 | extend( QUnit, { 340 | 341 | // Call on start of module test to prepend name to all tests 342 | module: function( name, testEnvironment, executeNow ) { 343 | var module, moduleFns; 344 | var currentModule = config.currentModule; 345 | 346 | if ( arguments.length === 2 ) { 347 | if ( objectType( testEnvironment ) === "function" ) { 348 | executeNow = testEnvironment; 349 | testEnvironment = undefined; 350 | } 351 | } 352 | 353 | module = createModule(); 354 | 355 | if ( testEnvironment && ( testEnvironment.setup || testEnvironment.teardown ) ) { 356 | console.warn( 357 | "Module's `setup` and `teardown` are not hooks anymore on QUnit 2.0, use " + 358 | "`beforeEach` and `afterEach` instead\n" + 359 | "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/" 360 | ); 361 | } 362 | 363 | moduleFns = { 364 | before: setHook( module, "before" ), 365 | beforeEach: setHook( module, "beforeEach" ), 366 | afterEach: setHook( module, "afterEach" ), 367 | after: setHook( module, "after" ) 368 | }; 369 | 370 | if ( objectType( executeNow ) === "function" ) { 371 | config.moduleStack.push( module ); 372 | setCurrentModule( module ); 373 | executeNow.call( module.testEnvironment, moduleFns ); 374 | config.moduleStack.pop(); 375 | module = module.parentModule || currentModule; 376 | } 377 | 378 | setCurrentModule( module ); 379 | 380 | function createModule() { 381 | var parentModule = config.moduleStack.length ? 382 | config.moduleStack.slice( -1 )[ 0 ] : null; 383 | var moduleName = parentModule !== null ? 384 | [ parentModule.name, name ].join( " > " ) : name; 385 | var module = { 386 | name: moduleName, 387 | parentModule: parentModule, 388 | tests: [], 389 | moduleId: generateHash( moduleName ), 390 | testsRun: 0 391 | }; 392 | 393 | var env = {}; 394 | if ( parentModule ) { 395 | parentModule.childModule = module; 396 | extend( env, parentModule.testEnvironment ); 397 | delete env.beforeEach; 398 | delete env.afterEach; 399 | } 400 | extend( env, testEnvironment ); 401 | module.testEnvironment = env; 402 | 403 | config.modules.push( module ); 404 | return module; 405 | } 406 | 407 | function setCurrentModule( module ) { 408 | config.currentModule = module; 409 | } 410 | 411 | }, 412 | 413 | test: test, 414 | 415 | skip: skip, 416 | 417 | only: only, 418 | 419 | start: function( count ) { 420 | var globalStartAlreadyCalled = globalStartCalled; 421 | 422 | if ( !config.current ) { 423 | globalStartCalled = true; 424 | 425 | if ( runStarted ) { 426 | throw new Error( "Called start() while test already started running" ); 427 | } else if ( globalStartAlreadyCalled || count > 1 ) { 428 | throw new Error( "Called start() outside of a test context too many times" ); 429 | } else if ( config.autostart ) { 430 | throw new Error( "Called start() outside of a test context when " + 431 | "QUnit.config.autostart was true" ); 432 | } else if ( !config.pageLoaded ) { 433 | 434 | // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it 435 | config.autostart = true; 436 | return; 437 | } 438 | } else { 439 | throw new Error( 440 | "QUnit.start cannot be called inside a test context. This feature is removed in " + 441 | "QUnit 2.0. For async tests, use QUnit.test() with assert.async() instead.\n" + 442 | "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/" 443 | ); 444 | } 445 | 446 | scheduleBegin(); 447 | }, 448 | 449 | config: config, 450 | 451 | is: is, 452 | 453 | objectType: objectType, 454 | 455 | extend: extend, 456 | 457 | load: function() { 458 | config.pageLoaded = true; 459 | 460 | // Initialize the configuration options 461 | extend( config, { 462 | stats: { all: 0, bad: 0 }, 463 | moduleStats: { all: 0, bad: 0 }, 464 | started: 0, 465 | updateRate: 1000, 466 | autostart: true, 467 | filter: "" 468 | }, true ); 469 | 470 | if ( !runStarted ) { 471 | config.blocking = false; 472 | 473 | if ( config.autostart ) { 474 | scheduleBegin(); 475 | } 476 | } 477 | }, 478 | 479 | stack: function( offset ) { 480 | offset = ( offset || 0 ) + 2; 481 | return sourceFromStacktrace( offset ); 482 | } 483 | } ); 484 | 485 | registerLoggingCallbacks( QUnit ); 486 | 487 | function scheduleBegin() { 488 | 489 | runStarted = true; 490 | 491 | // Add a slight delay to allow definition of more modules and tests. 492 | if ( defined.setTimeout ) { 493 | setTimeout( function() { 494 | begin(); 495 | }, 13 ); 496 | } else { 497 | begin(); 498 | } 499 | } 500 | 501 | function begin() { 502 | var i, l, 503 | modulesLog = []; 504 | 505 | // If the test run hasn't officially begun yet 506 | if ( !config.started ) { 507 | 508 | // Record the time of the test run's beginning 509 | config.started = now(); 510 | 511 | // Delete the loose unnamed module if unused. 512 | if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) { 513 | config.modules.shift(); 514 | } 515 | 516 | // Avoid unnecessary information by not logging modules' test environments 517 | for ( i = 0, l = config.modules.length; i < l; i++ ) { 518 | modulesLog.push( { 519 | name: config.modules[ i ].name, 520 | tests: config.modules[ i ].tests 521 | } ); 522 | } 523 | 524 | // The test run is officially beginning now 525 | runLoggingCallbacks( "begin", { 526 | totalTests: Test.count, 527 | modules: modulesLog 528 | } ); 529 | } 530 | 531 | config.blocking = false; 532 | process( true ); 533 | } 534 | 535 | function process( last ) { 536 | function next() { 537 | process( last ); 538 | } 539 | var start = now(); 540 | config.depth = ( config.depth || 0 ) + 1; 541 | 542 | while ( config.queue.length && !config.blocking ) { 543 | if ( !defined.setTimeout || config.updateRate <= 0 || 544 | ( ( now() - start ) < config.updateRate ) ) { 545 | if ( config.current ) { 546 | 547 | // Reset async tracking for each phase of the Test lifecycle 548 | config.current.usedAsync = false; 549 | } 550 | config.queue.shift()(); 551 | } else { 552 | setTimeout( next, 13 ); 553 | break; 554 | } 555 | } 556 | config.depth--; 557 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { 558 | done(); 559 | } 560 | } 561 | 562 | function done() { 563 | var runtime, passed; 564 | 565 | autorun = true; 566 | 567 | // Log the last module results 568 | if ( config.previousModule ) { 569 | runLoggingCallbacks( "moduleDone", { 570 | name: config.previousModule.name, 571 | tests: config.previousModule.tests, 572 | failed: config.moduleStats.bad, 573 | passed: config.moduleStats.all - config.moduleStats.bad, 574 | total: config.moduleStats.all, 575 | runtime: now() - config.moduleStats.started 576 | } ); 577 | } 578 | delete config.previousModule; 579 | 580 | runtime = now() - config.started; 581 | passed = config.stats.all - config.stats.bad; 582 | 583 | runLoggingCallbacks( "done", { 584 | failed: config.stats.bad, 585 | passed: passed, 586 | total: config.stats.all, 587 | runtime: runtime 588 | } ); 589 | } 590 | 591 | function setHook( module, hookName ) { 592 | if ( module.testEnvironment === undefined ) { 593 | module.testEnvironment = {}; 594 | } 595 | 596 | return function( callback ) { 597 | module.testEnvironment[ hookName ] = callback; 598 | }; 599 | } 600 | 601 | var unitSampler, 602 | focused = false, 603 | priorityCount = 0; 604 | 605 | function Test( settings ) { 606 | var i, l; 607 | 608 | ++Test.count; 609 | 610 | this.expected = null; 611 | extend( this, settings ); 612 | this.assertions = []; 613 | this.semaphore = 0; 614 | this.usedAsync = false; 615 | this.module = config.currentModule; 616 | this.stack = sourceFromStacktrace( 3 ); 617 | 618 | // Register unique strings 619 | for ( i = 0, l = this.module.tests; i < l.length; i++ ) { 620 | if ( this.module.tests[ i ].name === this.testName ) { 621 | this.testName += " "; 622 | } 623 | } 624 | 625 | this.testId = generateHash( this.module.name, this.testName ); 626 | 627 | this.module.tests.push( { 628 | name: this.testName, 629 | testId: this.testId 630 | } ); 631 | 632 | if ( settings.skip ) { 633 | 634 | // Skipped tests will fully ignore any sent callback 635 | this.callback = function() {}; 636 | this.async = false; 637 | this.expected = 0; 638 | } else { 639 | this.assert = new Assert( this ); 640 | } 641 | } 642 | 643 | Test.count = 0; 644 | 645 | Test.prototype = { 646 | before: function() { 647 | if ( 648 | 649 | // Emit moduleStart when we're switching from one module to another 650 | this.module !== config.previousModule || 651 | 652 | // They could be equal (both undefined) but if the previousModule property doesn't 653 | // yet exist it means this is the first test in a suite that isn't wrapped in a 654 | // module, in which case we'll just emit a moduleStart event for 'undefined'. 655 | // Without this, reporters can get testStart before moduleStart which is a problem. 656 | !hasOwn.call( config, "previousModule" ) 657 | ) { 658 | if ( hasOwn.call( config, "previousModule" ) ) { 659 | runLoggingCallbacks( "moduleDone", { 660 | name: config.previousModule.name, 661 | tests: config.previousModule.tests, 662 | failed: config.moduleStats.bad, 663 | passed: config.moduleStats.all - config.moduleStats.bad, 664 | total: config.moduleStats.all, 665 | runtime: now() - config.moduleStats.started 666 | } ); 667 | } 668 | config.previousModule = this.module; 669 | config.moduleStats = { all: 0, bad: 0, started: now() }; 670 | runLoggingCallbacks( "moduleStart", { 671 | name: this.module.name, 672 | tests: this.module.tests 673 | } ); 674 | } 675 | 676 | config.current = this; 677 | 678 | if ( this.module.testEnvironment ) { 679 | delete this.module.testEnvironment.before; 680 | delete this.module.testEnvironment.beforeEach; 681 | delete this.module.testEnvironment.afterEach; 682 | delete this.module.testEnvironment.after; 683 | } 684 | this.testEnvironment = extend( {}, this.module.testEnvironment ); 685 | 686 | this.started = now(); 687 | runLoggingCallbacks( "testStart", { 688 | name: this.testName, 689 | module: this.module.name, 690 | testId: this.testId 691 | } ); 692 | 693 | if ( !config.pollution ) { 694 | saveGlobal(); 695 | } 696 | }, 697 | 698 | run: function() { 699 | var promise; 700 | 701 | config.current = this; 702 | 703 | this.callbackStarted = now(); 704 | 705 | if ( config.notrycatch ) { 706 | runTest( this ); 707 | return; 708 | } 709 | 710 | try { 711 | runTest( this ); 712 | } catch ( e ) { 713 | this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + 714 | this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); 715 | 716 | // Else next test will carry the responsibility 717 | saveGlobal(); 718 | 719 | // Restart the tests if they're blocking 720 | if ( config.blocking ) { 721 | internalRecover( this ); 722 | } 723 | } 724 | 725 | function runTest( test ) { 726 | promise = test.callback.call( test.testEnvironment, test.assert ); 727 | test.resolvePromise( promise ); 728 | } 729 | }, 730 | 731 | after: function() { 732 | checkPollution(); 733 | }, 734 | 735 | queueHook: function( hook, hookName, hookOwner ) { 736 | var promise, 737 | test = this; 738 | return function runHook() { 739 | if ( hookName === "before" ) { 740 | if ( hookOwner.testsRun !== 0 ) { 741 | return; 742 | } 743 | 744 | test.preserveEnvironment = true; 745 | } 746 | 747 | if ( hookName === "after" && hookOwner.testsRun !== numberOfTests( hookOwner ) - 1 ) { 748 | return; 749 | } 750 | 751 | config.current = test; 752 | if ( config.notrycatch ) { 753 | callHook(); 754 | return; 755 | } 756 | try { 757 | callHook(); 758 | } catch ( error ) { 759 | test.pushFailure( hookName + " failed on " + test.testName + ": " + 760 | ( error.message || error ), extractStacktrace( error, 0 ) ); 761 | } 762 | 763 | function callHook() { 764 | promise = hook.call( test.testEnvironment, test.assert ); 765 | test.resolvePromise( promise, hookName ); 766 | } 767 | }; 768 | }, 769 | 770 | // Currently only used for module level hooks, can be used to add global level ones 771 | hooks: function( handler ) { 772 | var hooks = []; 773 | 774 | function processHooks( test, module ) { 775 | if ( module.parentModule ) { 776 | processHooks( test, module.parentModule ); 777 | } 778 | if ( module.testEnvironment && 779 | QUnit.objectType( module.testEnvironment[ handler ] ) === "function" ) { 780 | hooks.push( test.queueHook( module.testEnvironment[ handler ], handler, module ) ); 781 | } 782 | } 783 | 784 | // Hooks are ignored on skipped tests 785 | if ( !this.skip ) { 786 | processHooks( this, this.module ); 787 | } 788 | return hooks; 789 | }, 790 | 791 | finish: function() { 792 | config.current = this; 793 | if ( config.requireExpects && this.expected === null ) { 794 | this.pushFailure( "Expected number of assertions to be defined, but expect() was " + 795 | "not called.", this.stack ); 796 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) { 797 | this.pushFailure( "Expected " + this.expected + " assertions, but " + 798 | this.assertions.length + " were run", this.stack ); 799 | } else if ( this.expected === null && !this.assertions.length ) { 800 | this.pushFailure( "Expected at least one assertion, but none were run - call " + 801 | "expect(0) to accept zero assertions.", this.stack ); 802 | } 803 | 804 | var i, 805 | skipped = !!this.skip, 806 | bad = 0; 807 | 808 | this.runtime = now() - this.started; 809 | 810 | config.stats.all += this.assertions.length; 811 | config.moduleStats.all += this.assertions.length; 812 | 813 | for ( i = 0; i < this.assertions.length; i++ ) { 814 | if ( !this.assertions[ i ].result ) { 815 | bad++; 816 | config.stats.bad++; 817 | config.moduleStats.bad++; 818 | } 819 | } 820 | 821 | notifyTestsRan( this.module ); 822 | runLoggingCallbacks( "testDone", { 823 | name: this.testName, 824 | module: this.module.name, 825 | skipped: skipped, 826 | failed: bad, 827 | passed: this.assertions.length - bad, 828 | total: this.assertions.length, 829 | runtime: skipped ? 0 : this.runtime, 830 | 831 | // HTML Reporter use 832 | assertions: this.assertions, 833 | testId: this.testId, 834 | 835 | // Source of Test 836 | source: this.stack 837 | } ); 838 | 839 | config.current = undefined; 840 | }, 841 | 842 | preserveTestEnvironment: function() { 843 | if ( this.preserveEnvironment ) { 844 | this.module.testEnvironment = this.testEnvironment; 845 | this.testEnvironment = extend( {}, this.module.testEnvironment ); 846 | } 847 | }, 848 | 849 | queue: function() { 850 | var priority, 851 | test = this; 852 | 853 | if ( !this.valid() ) { 854 | return; 855 | } 856 | 857 | function run() { 858 | 859 | // Each of these can by async 860 | synchronize( [ 861 | function() { 862 | test.before(); 863 | }, 864 | 865 | test.hooks( "before" ), 866 | 867 | function() { 868 | test.preserveTestEnvironment(); 869 | }, 870 | 871 | test.hooks( "beforeEach" ), 872 | 873 | function() { 874 | test.run(); 875 | }, 876 | 877 | test.hooks( "afterEach" ).reverse(), 878 | test.hooks( "after" ).reverse(), 879 | 880 | function() { 881 | test.after(); 882 | }, 883 | 884 | function() { 885 | test.finish(); 886 | } 887 | ] ); 888 | } 889 | 890 | // Prioritize previously failed tests, detected from sessionStorage 891 | priority = QUnit.config.reorder && defined.sessionStorage && 892 | +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName ); 893 | 894 | return synchronize( run, priority, config.seed ); 895 | }, 896 | 897 | pushResult: function( resultInfo ) { 898 | 899 | // Destructure of resultInfo = { result, actual, expected, message, negative } 900 | var source, 901 | details = { 902 | module: this.module.name, 903 | name: this.testName, 904 | result: resultInfo.result, 905 | message: resultInfo.message, 906 | actual: resultInfo.actual, 907 | expected: resultInfo.expected, 908 | testId: this.testId, 909 | negative: resultInfo.negative || false, 910 | runtime: now() - this.started 911 | }; 912 | 913 | if ( !resultInfo.result ) { 914 | source = sourceFromStacktrace(); 915 | 916 | if ( source ) { 917 | details.source = source; 918 | } 919 | } 920 | 921 | runLoggingCallbacks( "log", details ); 922 | 923 | this.assertions.push( { 924 | result: !!resultInfo.result, 925 | message: resultInfo.message 926 | } ); 927 | }, 928 | 929 | pushFailure: function( message, source, actual ) { 930 | if ( !( this instanceof Test ) ) { 931 | throw new Error( "pushFailure() assertion outside test context, was " + 932 | sourceFromStacktrace( 2 ) ); 933 | } 934 | 935 | var details = { 936 | module: this.module.name, 937 | name: this.testName, 938 | result: false, 939 | message: message || "error", 940 | actual: actual || null, 941 | testId: this.testId, 942 | runtime: now() - this.started 943 | }; 944 | 945 | if ( source ) { 946 | details.source = source; 947 | } 948 | 949 | runLoggingCallbacks( "log", details ); 950 | 951 | this.assertions.push( { 952 | result: false, 953 | message: message 954 | } ); 955 | }, 956 | 957 | resolvePromise: function( promise, phase ) { 958 | var then, resume, message, 959 | test = this; 960 | if ( promise != null ) { 961 | then = promise.then; 962 | if ( QUnit.objectType( then ) === "function" ) { 963 | resume = internalStop( test ); 964 | then.call( 965 | promise, 966 | function() { resume(); }, 967 | function( error ) { 968 | message = "Promise rejected " + 969 | ( !phase ? "during" : phase.replace( /Each$/, "" ) ) + 970 | " " + test.testName + ": " + ( error.message || error ); 971 | test.pushFailure( message, extractStacktrace( error, 0 ) ); 972 | 973 | // Else next test will carry the responsibility 974 | saveGlobal(); 975 | 976 | // Unblock 977 | resume(); 978 | } 979 | ); 980 | } 981 | } 982 | }, 983 | 984 | valid: function() { 985 | var filter = config.filter, 986 | regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec( filter ), 987 | module = config.module && config.module.toLowerCase(), 988 | fullName = ( this.module.name + ": " + this.testName ); 989 | 990 | function moduleChainNameMatch( testModule ) { 991 | var testModuleName = testModule.name ? testModule.name.toLowerCase() : null; 992 | if ( testModuleName === module ) { 993 | return true; 994 | } else if ( testModule.parentModule ) { 995 | return moduleChainNameMatch( testModule.parentModule ); 996 | } else { 997 | return false; 998 | } 999 | } 1000 | 1001 | function moduleChainIdMatch( testModule ) { 1002 | return inArray( testModule.moduleId, config.moduleId ) > -1 || 1003 | testModule.parentModule && moduleChainIdMatch( testModule.parentModule ); 1004 | } 1005 | 1006 | // Internally-generated tests are always valid 1007 | if ( this.callback && this.callback.validTest ) { 1008 | return true; 1009 | } 1010 | 1011 | if ( config.moduleId && config.moduleId.length > 0 && 1012 | !moduleChainIdMatch( this.module ) ) { 1013 | 1014 | return false; 1015 | } 1016 | 1017 | if ( config.testId && config.testId.length > 0 && 1018 | inArray( this.testId, config.testId ) < 0 ) { 1019 | 1020 | return false; 1021 | } 1022 | 1023 | if ( module && !moduleChainNameMatch( this.module ) ) { 1024 | return false; 1025 | } 1026 | 1027 | if ( !filter ) { 1028 | return true; 1029 | } 1030 | 1031 | return regexFilter ? 1032 | this.regexFilter( !!regexFilter[ 1 ], regexFilter[ 2 ], regexFilter[ 3 ], fullName ) : 1033 | this.stringFilter( filter, fullName ); 1034 | }, 1035 | 1036 | regexFilter: function( exclude, pattern, flags, fullName ) { 1037 | var regex = new RegExp( pattern, flags ); 1038 | var match = regex.test( fullName ); 1039 | 1040 | return match !== exclude; 1041 | }, 1042 | 1043 | stringFilter: function( filter, fullName ) { 1044 | filter = filter.toLowerCase(); 1045 | fullName = fullName.toLowerCase(); 1046 | 1047 | var include = filter.charAt( 0 ) !== "!"; 1048 | if ( !include ) { 1049 | filter = filter.slice( 1 ); 1050 | } 1051 | 1052 | // If the filter matches, we need to honour include 1053 | if ( fullName.indexOf( filter ) !== -1 ) { 1054 | return include; 1055 | } 1056 | 1057 | // Otherwise, do the opposite 1058 | return !include; 1059 | } 1060 | }; 1061 | 1062 | QUnit.pushFailure = function() { 1063 | if ( !QUnit.config.current ) { 1064 | throw new Error( "pushFailure() assertion outside test context, in " + 1065 | sourceFromStacktrace( 2 ) ); 1066 | } 1067 | 1068 | // Gets current test obj 1069 | var currentTest = QUnit.config.current; 1070 | 1071 | return currentTest.pushFailure.apply( currentTest, arguments ); 1072 | }; 1073 | 1074 | // Based on Java's String.hashCode, a simple but not 1075 | // rigorously collision resistant hashing function 1076 | function generateHash( module, testName ) { 1077 | var hex, 1078 | i = 0, 1079 | hash = 0, 1080 | str = module + "\x1C" + testName, 1081 | len = str.length; 1082 | 1083 | for ( ; i < len; i++ ) { 1084 | hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i ); 1085 | hash |= 0; 1086 | } 1087 | 1088 | // Convert the possibly negative integer hash code into an 8 character hex string, which isn't 1089 | // strictly necessary but increases user understanding that the id is a SHA-like hash 1090 | hex = ( 0x100000000 + hash ).toString( 16 ); 1091 | if ( hex.length < 8 ) { 1092 | hex = "0000000" + hex; 1093 | } 1094 | 1095 | return hex.slice( -8 ); 1096 | } 1097 | 1098 | function synchronize( callback, priority, seed ) { 1099 | var last = !priority, 1100 | index; 1101 | 1102 | if ( QUnit.objectType( callback ) === "array" ) { 1103 | while ( callback.length ) { 1104 | synchronize( callback.shift() ); 1105 | } 1106 | return; 1107 | } 1108 | 1109 | if ( priority ) { 1110 | config.queue.splice( priorityCount++, 0, callback ); 1111 | } else if ( seed ) { 1112 | if ( !unitSampler ) { 1113 | unitSampler = unitSamplerGenerator( seed ); 1114 | } 1115 | 1116 | // Insert into a random position after all priority items 1117 | index = Math.floor( unitSampler() * ( config.queue.length - priorityCount + 1 ) ); 1118 | config.queue.splice( priorityCount + index, 0, callback ); 1119 | } else { 1120 | config.queue.push( callback ); 1121 | } 1122 | 1123 | if ( autorun && !config.blocking ) { 1124 | process( last ); 1125 | } 1126 | } 1127 | 1128 | function unitSamplerGenerator( seed ) { 1129 | 1130 | // 32-bit xorshift, requires only a nonzero seed 1131 | // http://excamera.com/sphinx/article-xorshift.html 1132 | var sample = parseInt( generateHash( seed ), 16 ) || -1; 1133 | return function() { 1134 | sample ^= sample << 13; 1135 | sample ^= sample >>> 17; 1136 | sample ^= sample << 5; 1137 | 1138 | // ECMAScript has no unsigned number type 1139 | if ( sample < 0 ) { 1140 | sample += 0x100000000; 1141 | } 1142 | 1143 | return sample / 0x100000000; 1144 | }; 1145 | } 1146 | 1147 | function saveGlobal() { 1148 | config.pollution = []; 1149 | 1150 | if ( config.noglobals ) { 1151 | for ( var key in global ) { 1152 | if ( hasOwn.call( global, key ) ) { 1153 | 1154 | // In Opera sometimes DOM element ids show up here, ignore them 1155 | if ( /^qunit-test-output/.test( key ) ) { 1156 | continue; 1157 | } 1158 | config.pollution.push( key ); 1159 | } 1160 | } 1161 | } 1162 | } 1163 | 1164 | function checkPollution() { 1165 | var newGlobals, 1166 | deletedGlobals, 1167 | old = config.pollution; 1168 | 1169 | saveGlobal(); 1170 | 1171 | newGlobals = diff( config.pollution, old ); 1172 | if ( newGlobals.length > 0 ) { 1173 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) ); 1174 | } 1175 | 1176 | deletedGlobals = diff( old, config.pollution ); 1177 | if ( deletedGlobals.length > 0 ) { 1178 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) ); 1179 | } 1180 | } 1181 | 1182 | // Will be exposed as QUnit.test 1183 | function test( testName, callback ) { 1184 | if ( focused ) { return; } 1185 | 1186 | var newTest; 1187 | 1188 | newTest = new Test( { 1189 | testName: testName, 1190 | callback: callback 1191 | } ); 1192 | 1193 | newTest.queue(); 1194 | } 1195 | 1196 | // Will be exposed as QUnit.skip 1197 | function skip( testName ) { 1198 | if ( focused ) { return; } 1199 | 1200 | var test = new Test( { 1201 | testName: testName, 1202 | skip: true 1203 | } ); 1204 | 1205 | test.queue(); 1206 | } 1207 | 1208 | // Will be exposed as QUnit.only 1209 | function only( testName, callback ) { 1210 | var newTest; 1211 | 1212 | if ( focused ) { return; } 1213 | 1214 | QUnit.config.queue.length = 0; 1215 | focused = true; 1216 | 1217 | newTest = new Test( { 1218 | testName: testName, 1219 | callback: callback 1220 | } ); 1221 | 1222 | newTest.queue(); 1223 | } 1224 | 1225 | // Put a hold on processing and return a function that will release it. 1226 | function internalStop( test ) { 1227 | var released = false; 1228 | 1229 | test.semaphore += 1; 1230 | config.blocking = true; 1231 | 1232 | // Set a recovery timeout, if so configured. 1233 | if ( config.testTimeout && defined.setTimeout ) { 1234 | clearTimeout( config.timeout ); 1235 | config.timeout = setTimeout( function() { 1236 | QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) ); 1237 | internalRecover( test ); 1238 | }, config.testTimeout ); 1239 | } 1240 | 1241 | return function resume() { 1242 | if ( released ) { 1243 | return; 1244 | } 1245 | 1246 | released = true; 1247 | test.semaphore -= 1; 1248 | internalStart( test ); 1249 | }; 1250 | } 1251 | 1252 | // Forcefully release all processing holds. 1253 | function internalRecover( test ) { 1254 | test.semaphore = 0; 1255 | internalStart( test ); 1256 | } 1257 | 1258 | // Release a processing hold, scheduling a resumption attempt if no holds remain. 1259 | function internalStart( test ) { 1260 | 1261 | // If semaphore is non-numeric, throw error 1262 | if ( isNaN( test.semaphore ) ) { 1263 | test.semaphore = 0; 1264 | 1265 | QUnit.pushFailure( 1266 | "Invalid value on test.semaphore", 1267 | sourceFromStacktrace( 2 ) 1268 | ); 1269 | return; 1270 | } 1271 | 1272 | // Don't start until equal number of stop-calls 1273 | if ( test.semaphore > 0 ) { 1274 | return; 1275 | } 1276 | 1277 | // Throw an Error if start is called more often than stop 1278 | if ( test.semaphore < 0 ) { 1279 | test.semaphore = 0; 1280 | 1281 | QUnit.pushFailure( 1282 | "Tried to restart test while already started (test's semaphore was 0 already)", 1283 | sourceFromStacktrace( 2 ) 1284 | ); 1285 | return; 1286 | } 1287 | 1288 | // Add a slight delay to allow more assertions etc. 1289 | if ( defined.setTimeout ) { 1290 | if ( config.timeout ) { 1291 | clearTimeout( config.timeout ); 1292 | } 1293 | config.timeout = setTimeout( function() { 1294 | if ( test.semaphore > 0 ) { 1295 | return; 1296 | } 1297 | 1298 | if ( config.timeout ) { 1299 | clearTimeout( config.timeout ); 1300 | } 1301 | 1302 | begin(); 1303 | }, 13 ); 1304 | } else { 1305 | begin(); 1306 | } 1307 | } 1308 | 1309 | function numberOfTests( module ) { 1310 | var count = module.tests.length; 1311 | while ( module = module.childModule ) { 1312 | count += module.tests.length; 1313 | } 1314 | return count; 1315 | } 1316 | 1317 | function notifyTestsRan( module ) { 1318 | module.testsRun++; 1319 | while ( module = module.parentModule ) { 1320 | module.testsRun++; 1321 | } 1322 | } 1323 | 1324 | function Assert( testContext ) { 1325 | this.test = testContext; 1326 | } 1327 | 1328 | // Assert helpers 1329 | QUnit.assert = Assert.prototype = { 1330 | 1331 | // Specify the number of expected assertions to guarantee that failed test 1332 | // (no assertions are run at all) don't slip through. 1333 | expect: function( asserts ) { 1334 | if ( arguments.length === 1 ) { 1335 | this.test.expected = asserts; 1336 | } else { 1337 | return this.test.expected; 1338 | } 1339 | }, 1340 | 1341 | // Put a hold on processing and return a function that will release it a maximum of once. 1342 | async: function( count ) { 1343 | var resume, 1344 | test = this.test, 1345 | popped = false, 1346 | acceptCallCount = count; 1347 | 1348 | if ( typeof acceptCallCount === "undefined" ) { 1349 | acceptCallCount = 1; 1350 | } 1351 | 1352 | test.usedAsync = true; 1353 | resume = internalStop( test ); 1354 | 1355 | return function done() { 1356 | 1357 | if ( popped ) { 1358 | test.pushFailure( "Too many calls to the `assert.async` callback", 1359 | sourceFromStacktrace( 2 ) ); 1360 | return; 1361 | } 1362 | acceptCallCount -= 1; 1363 | if ( acceptCallCount > 0 ) { 1364 | return; 1365 | } 1366 | 1367 | popped = true; 1368 | resume(); 1369 | }; 1370 | }, 1371 | 1372 | // Exports test.push() to the user API 1373 | // Alias of pushResult. 1374 | push: function( result, actual, expected, message, negative ) { 1375 | var currentAssert = this instanceof Assert ? this : QUnit.config.current.assert; 1376 | return currentAssert.pushResult( { 1377 | result: result, 1378 | actual: actual, 1379 | expected: expected, 1380 | message: message, 1381 | negative: negative 1382 | } ); 1383 | }, 1384 | 1385 | pushResult: function( resultInfo ) { 1386 | 1387 | // Destructure of resultInfo = { result, actual, expected, message, negative } 1388 | var assert = this, 1389 | currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current; 1390 | 1391 | // Backwards compatibility fix. 1392 | // Allows the direct use of global exported assertions and QUnit.assert.* 1393 | // Although, it's use is not recommended as it can leak assertions 1394 | // to other tests from async tests, because we only get a reference to the current test, 1395 | // not exactly the test where assertion were intended to be called. 1396 | if ( !currentTest ) { 1397 | throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) ); 1398 | } 1399 | 1400 | if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) { 1401 | currentTest.pushFailure( "Assertion after the final `assert.async` was resolved", 1402 | sourceFromStacktrace( 2 ) ); 1403 | 1404 | // Allow this assertion to continue running anyway... 1405 | } 1406 | 1407 | if ( !( assert instanceof Assert ) ) { 1408 | assert = currentTest.assert; 1409 | } 1410 | 1411 | return assert.test.pushResult( resultInfo ); 1412 | }, 1413 | 1414 | ok: function( result, message ) { 1415 | message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " + 1416 | QUnit.dump.parse( result ) ); 1417 | this.pushResult( { 1418 | result: !!result, 1419 | actual: result, 1420 | expected: true, 1421 | message: message 1422 | } ); 1423 | }, 1424 | 1425 | notOk: function( result, message ) { 1426 | message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " + 1427 | QUnit.dump.parse( result ) ); 1428 | this.pushResult( { 1429 | result: !result, 1430 | actual: result, 1431 | expected: false, 1432 | message: message 1433 | } ); 1434 | }, 1435 | 1436 | equal: function( actual, expected, message ) { 1437 | /*jshint eqeqeq:false */ 1438 | this.pushResult( { 1439 | result: expected == actual, 1440 | actual: actual, 1441 | expected: expected, 1442 | message: message 1443 | } ); 1444 | }, 1445 | 1446 | notEqual: function( actual, expected, message ) { 1447 | /*jshint eqeqeq:false */ 1448 | this.pushResult( { 1449 | result: expected != actual, 1450 | actual: actual, 1451 | expected: expected, 1452 | message: message, 1453 | negative: true 1454 | } ); 1455 | }, 1456 | 1457 | propEqual: function( actual, expected, message ) { 1458 | actual = objectValues( actual ); 1459 | expected = objectValues( expected ); 1460 | this.pushResult( { 1461 | result: QUnit.equiv( actual, expected ), 1462 | actual: actual, 1463 | expected: expected, 1464 | message: message 1465 | } ); 1466 | }, 1467 | 1468 | notPropEqual: function( actual, expected, message ) { 1469 | actual = objectValues( actual ); 1470 | expected = objectValues( expected ); 1471 | this.pushResult( { 1472 | result: !QUnit.equiv( actual, expected ), 1473 | actual: actual, 1474 | expected: expected, 1475 | message: message, 1476 | negative: true 1477 | } ); 1478 | }, 1479 | 1480 | deepEqual: function( actual, expected, message ) { 1481 | this.pushResult( { 1482 | result: QUnit.equiv( actual, expected ), 1483 | actual: actual, 1484 | expected: expected, 1485 | message: message 1486 | } ); 1487 | }, 1488 | 1489 | notDeepEqual: function( actual, expected, message ) { 1490 | this.pushResult( { 1491 | result: !QUnit.equiv( actual, expected ), 1492 | actual: actual, 1493 | expected: expected, 1494 | message: message, 1495 | negative: true 1496 | } ); 1497 | }, 1498 | 1499 | strictEqual: function( actual, expected, message ) { 1500 | this.pushResult( { 1501 | result: expected === actual, 1502 | actual: actual, 1503 | expected: expected, 1504 | message: message 1505 | } ); 1506 | }, 1507 | 1508 | notStrictEqual: function( actual, expected, message ) { 1509 | this.pushResult( { 1510 | result: expected !== actual, 1511 | actual: actual, 1512 | expected: expected, 1513 | message: message, 1514 | negative: true 1515 | } ); 1516 | }, 1517 | 1518 | "throws": function( block, expected, message ) { 1519 | var actual, expectedType, 1520 | expectedOutput = expected, 1521 | ok = false, 1522 | currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current; 1523 | 1524 | // 'expected' is optional unless doing string comparison 1525 | if ( QUnit.objectType( expected ) === "string" ) { 1526 | if ( message == null ) { 1527 | message = expected; 1528 | expected = null; 1529 | } else { 1530 | throw new Error( 1531 | "throws/raises does not accept a string value for the expected argument.\n" + 1532 | "Use a non-string object value (e.g. regExp) instead if it's necessary." + 1533 | "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/" 1534 | ); 1535 | } 1536 | } 1537 | 1538 | currentTest.ignoreGlobalErrors = true; 1539 | try { 1540 | block.call( currentTest.testEnvironment ); 1541 | } catch ( e ) { 1542 | actual = e; 1543 | } 1544 | currentTest.ignoreGlobalErrors = false; 1545 | 1546 | if ( actual ) { 1547 | expectedType = QUnit.objectType( expected ); 1548 | 1549 | // We don't want to validate thrown error 1550 | if ( !expected ) { 1551 | ok = true; 1552 | expectedOutput = null; 1553 | 1554 | // Expected is a regexp 1555 | } else if ( expectedType === "regexp" ) { 1556 | ok = expected.test( errorString( actual ) ); 1557 | 1558 | // Expected is a constructor, maybe an Error constructor 1559 | } else if ( expectedType === "function" && actual instanceof expected ) { 1560 | ok = true; 1561 | 1562 | // Expected is an Error object 1563 | } else if ( expectedType === "object" ) { 1564 | ok = actual instanceof expected.constructor && 1565 | actual.name === expected.name && 1566 | actual.message === expected.message; 1567 | 1568 | // Expected is a validation function which returns true if validation passed 1569 | } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) { 1570 | expectedOutput = null; 1571 | ok = true; 1572 | } 1573 | } 1574 | 1575 | currentTest.assert.pushResult( { 1576 | result: ok, 1577 | actual: actual, 1578 | expected: expectedOutput, 1579 | message: message 1580 | } ); 1581 | } 1582 | }; 1583 | 1584 | // Provide an alternative to assert.throws(), for environments that consider throws a reserved word 1585 | // Known to us are: Closure Compiler, Narwhal 1586 | ( function() { 1587 | /*jshint sub:true */ 1588 | Assert.prototype.raises = Assert.prototype [ "throws" ]; //jscs:ignore requireDotNotation 1589 | }() ); 1590 | 1591 | function errorString( error ) { 1592 | var name, message, 1593 | resultErrorString = error.toString(); 1594 | if ( resultErrorString.substring( 0, 7 ) === "[object" ) { 1595 | name = error.name ? error.name.toString() : "Error"; 1596 | message = error.message ? error.message.toString() : ""; 1597 | if ( name && message ) { 1598 | return name + ": " + message; 1599 | } else if ( name ) { 1600 | return name; 1601 | } else if ( message ) { 1602 | return message; 1603 | } else { 1604 | return "Error"; 1605 | } 1606 | } else { 1607 | return resultErrorString; 1608 | } 1609 | } 1610 | 1611 | // Test for equality any JavaScript type. 1612 | // Author: Philippe Rathé 1613 | QUnit.equiv = ( function() { 1614 | 1615 | // Stack to decide between skip/abort functions 1616 | var callers = []; 1617 | 1618 | // Stack to avoiding loops from circular referencing 1619 | var parents = []; 1620 | var parentsB = []; 1621 | 1622 | var getProto = Object.getPrototypeOf || function( obj ) { 1623 | 1624 | /*jshint proto: true */ 1625 | return obj.__proto__; 1626 | }; 1627 | 1628 | function useStrictEquality( b, a ) { 1629 | 1630 | // To catch short annotation VS 'new' annotation of a declaration. e.g.: 1631 | // `var i = 1;` 1632 | // `var j = new Number(1);` 1633 | if ( typeof a === "object" ) { 1634 | a = a.valueOf(); 1635 | } 1636 | if ( typeof b === "object" ) { 1637 | b = b.valueOf(); 1638 | } 1639 | 1640 | return a === b; 1641 | } 1642 | 1643 | function compareConstructors( a, b ) { 1644 | var protoA = getProto( a ); 1645 | var protoB = getProto( b ); 1646 | 1647 | // Comparing constructors is more strict than using `instanceof` 1648 | if ( a.constructor === b.constructor ) { 1649 | return true; 1650 | } 1651 | 1652 | // Ref #851 1653 | // If the obj prototype descends from a null constructor, treat it 1654 | // as a null prototype. 1655 | if ( protoA && protoA.constructor === null ) { 1656 | protoA = null; 1657 | } 1658 | if ( protoB && protoB.constructor === null ) { 1659 | protoB = null; 1660 | } 1661 | 1662 | // Allow objects with no prototype to be equivalent to 1663 | // objects with Object as their constructor. 1664 | if ( ( protoA === null && protoB === Object.prototype ) || 1665 | ( protoB === null && protoA === Object.prototype ) ) { 1666 | return true; 1667 | } 1668 | 1669 | return false; 1670 | } 1671 | 1672 | function getRegExpFlags( regexp ) { 1673 | return "flags" in regexp ? regexp.flags : regexp.toString().match( /[gimuy]*$/ )[ 0 ]; 1674 | } 1675 | 1676 | var callbacks = { 1677 | "string": useStrictEquality, 1678 | "boolean": useStrictEquality, 1679 | "number": useStrictEquality, 1680 | "null": useStrictEquality, 1681 | "undefined": useStrictEquality, 1682 | "symbol": useStrictEquality, 1683 | "date": useStrictEquality, 1684 | 1685 | "nan": function() { 1686 | return true; 1687 | }, 1688 | 1689 | "regexp": function( b, a ) { 1690 | return a.source === b.source && 1691 | 1692 | // Include flags in the comparison 1693 | getRegExpFlags( a ) === getRegExpFlags( b ); 1694 | }, 1695 | 1696 | // - skip when the property is a method of an instance (OOP) 1697 | // - abort otherwise, 1698 | // initial === would have catch identical references anyway 1699 | "function": function() { 1700 | var caller = callers[ callers.length - 1 ]; 1701 | return caller !== Object && typeof caller !== "undefined"; 1702 | }, 1703 | 1704 | "array": function( b, a ) { 1705 | var i, j, len, loop, aCircular, bCircular; 1706 | 1707 | len = a.length; 1708 | if ( len !== b.length ) { 1709 | 1710 | // Safe and faster 1711 | return false; 1712 | } 1713 | 1714 | // Track reference to avoid circular references 1715 | parents.push( a ); 1716 | parentsB.push( b ); 1717 | for ( i = 0; i < len; i++ ) { 1718 | loop = false; 1719 | for ( j = 0; j < parents.length; j++ ) { 1720 | aCircular = parents[ j ] === a[ i ]; 1721 | bCircular = parentsB[ j ] === b[ i ]; 1722 | if ( aCircular || bCircular ) { 1723 | if ( a[ i ] === b[ i ] || aCircular && bCircular ) { 1724 | loop = true; 1725 | } else { 1726 | parents.pop(); 1727 | parentsB.pop(); 1728 | return false; 1729 | } 1730 | } 1731 | } 1732 | if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { 1733 | parents.pop(); 1734 | parentsB.pop(); 1735 | return false; 1736 | } 1737 | } 1738 | parents.pop(); 1739 | parentsB.pop(); 1740 | return true; 1741 | }, 1742 | 1743 | "set": function( b, a ) { 1744 | var innerEq, 1745 | outerEq = true; 1746 | 1747 | if ( a.size !== b.size ) { 1748 | return false; 1749 | } 1750 | 1751 | a.forEach( function( aVal ) { 1752 | innerEq = false; 1753 | 1754 | b.forEach( function( bVal ) { 1755 | if ( innerEquiv( bVal, aVal ) ) { 1756 | innerEq = true; 1757 | } 1758 | } ); 1759 | 1760 | if ( !innerEq ) { 1761 | outerEq = false; 1762 | } 1763 | } ); 1764 | 1765 | return outerEq; 1766 | }, 1767 | 1768 | "map": function( b, a ) { 1769 | var innerEq, 1770 | outerEq = true; 1771 | 1772 | if ( a.size !== b.size ) { 1773 | return false; 1774 | } 1775 | 1776 | a.forEach( function( aVal, aKey ) { 1777 | innerEq = false; 1778 | 1779 | b.forEach( function( bVal, bKey ) { 1780 | if ( innerEquiv( [ bVal, bKey ], [ aVal, aKey ] ) ) { 1781 | innerEq = true; 1782 | } 1783 | } ); 1784 | 1785 | if ( !innerEq ) { 1786 | outerEq = false; 1787 | } 1788 | } ); 1789 | 1790 | return outerEq; 1791 | }, 1792 | 1793 | "object": function( b, a ) { 1794 | var i, j, loop, aCircular, bCircular; 1795 | 1796 | // Default to true 1797 | var eq = true; 1798 | var aProperties = []; 1799 | var bProperties = []; 1800 | 1801 | if ( compareConstructors( a, b ) === false ) { 1802 | return false; 1803 | } 1804 | 1805 | // Stack constructor before traversing properties 1806 | callers.push( a.constructor ); 1807 | 1808 | // Track reference to avoid circular references 1809 | parents.push( a ); 1810 | parentsB.push( b ); 1811 | 1812 | // Be strict: don't ensure hasOwnProperty and go deep 1813 | for ( i in a ) { 1814 | loop = false; 1815 | for ( j = 0; j < parents.length; j++ ) { 1816 | aCircular = parents[ j ] === a[ i ]; 1817 | bCircular = parentsB[ j ] === b[ i ]; 1818 | if ( aCircular || bCircular ) { 1819 | if ( a[ i ] === b[ i ] || aCircular && bCircular ) { 1820 | loop = true; 1821 | } else { 1822 | eq = false; 1823 | break; 1824 | } 1825 | } 1826 | } 1827 | aProperties.push( i ); 1828 | if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { 1829 | eq = false; 1830 | break; 1831 | } 1832 | } 1833 | 1834 | parents.pop(); 1835 | parentsB.pop(); 1836 | 1837 | // Unstack, we are done 1838 | callers.pop(); 1839 | 1840 | for ( i in b ) { 1841 | 1842 | // Collect b's properties 1843 | bProperties.push( i ); 1844 | } 1845 | 1846 | // Ensures identical properties name 1847 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); 1848 | } 1849 | }; 1850 | 1851 | function typeEquiv( a, b ) { 1852 | var type = QUnit.objectType( a ); 1853 | return QUnit.objectType( b ) === type && callbacks[ type ]( b, a ); 1854 | } 1855 | 1856 | // The real equiv function 1857 | function innerEquiv( a, b ) { 1858 | 1859 | // We're done when there's nothing more to compare 1860 | if ( arguments.length < 2 ) { 1861 | return true; 1862 | } 1863 | 1864 | // Require type-specific equality 1865 | return ( a === b || typeEquiv( a, b ) ) && 1866 | 1867 | // ...across all consecutive argument pairs 1868 | ( arguments.length === 2 || innerEquiv.apply( this, [].slice.call( arguments, 1 ) ) ); 1869 | } 1870 | 1871 | return innerEquiv; 1872 | }() ); 1873 | 1874 | // Based on jsDump by Ariel Flesler 1875 | // http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html 1876 | QUnit.dump = ( function() { 1877 | function quote( str ) { 1878 | return "\"" + str.toString().replace( /\\/g, "\\\\" ).replace( /"/g, "\\\"" ) + "\""; 1879 | } 1880 | function literal( o ) { 1881 | return o + ""; 1882 | } 1883 | function join( pre, arr, post ) { 1884 | var s = dump.separator(), 1885 | base = dump.indent(), 1886 | inner = dump.indent( 1 ); 1887 | if ( arr.join ) { 1888 | arr = arr.join( "," + s + inner ); 1889 | } 1890 | if ( !arr ) { 1891 | return pre + post; 1892 | } 1893 | return [ pre, inner + arr, base + post ].join( s ); 1894 | } 1895 | function array( arr, stack ) { 1896 | var i = arr.length, 1897 | ret = new Array( i ); 1898 | 1899 | if ( dump.maxDepth && dump.depth > dump.maxDepth ) { 1900 | return "[object Array]"; 1901 | } 1902 | 1903 | this.up(); 1904 | while ( i-- ) { 1905 | ret[ i ] = this.parse( arr[ i ], undefined, stack ); 1906 | } 1907 | this.down(); 1908 | return join( "[", ret, "]" ); 1909 | } 1910 | 1911 | function isArray( obj ) { 1912 | return ( 1913 | 1914 | //Native Arrays 1915 | toString.call( obj ) === "[object Array]" || 1916 | 1917 | // NodeList objects 1918 | ( typeof obj.length === "number" && obj.item !== undefined ) && 1919 | ( obj.length ? 1920 | obj.item( 0 ) === obj[ 0 ] : 1921 | ( obj.item( 0 ) === null && obj[ 0 ] === undefined ) 1922 | ) 1923 | ); 1924 | } 1925 | 1926 | var reName = /^function (\w+)/, 1927 | dump = { 1928 | 1929 | // The objType is used mostly internally, you can fix a (custom) type in advance 1930 | parse: function( obj, objType, stack ) { 1931 | stack = stack || []; 1932 | var res, parser, parserType, 1933 | inStack = inArray( obj, stack ); 1934 | 1935 | if ( inStack !== -1 ) { 1936 | return "recursion(" + ( inStack - stack.length ) + ")"; 1937 | } 1938 | 1939 | objType = objType || this.typeOf( obj ); 1940 | parser = this.parsers[ objType ]; 1941 | parserType = typeof parser; 1942 | 1943 | if ( parserType === "function" ) { 1944 | stack.push( obj ); 1945 | res = parser.call( this, obj, stack ); 1946 | stack.pop(); 1947 | return res; 1948 | } 1949 | return ( parserType === "string" ) ? parser : this.parsers.error; 1950 | }, 1951 | typeOf: function( obj ) { 1952 | var type; 1953 | 1954 | if ( obj === null ) { 1955 | type = "null"; 1956 | } else if ( typeof obj === "undefined" ) { 1957 | type = "undefined"; 1958 | } else if ( QUnit.is( "regexp", obj ) ) { 1959 | type = "regexp"; 1960 | } else if ( QUnit.is( "date", obj ) ) { 1961 | type = "date"; 1962 | } else if ( QUnit.is( "function", obj ) ) { 1963 | type = "function"; 1964 | } else if ( obj.setInterval !== undefined && 1965 | obj.document !== undefined && 1966 | obj.nodeType === undefined ) { 1967 | type = "window"; 1968 | } else if ( obj.nodeType === 9 ) { 1969 | type = "document"; 1970 | } else if ( obj.nodeType ) { 1971 | type = "node"; 1972 | } else if ( isArray( obj ) ) { 1973 | type = "array"; 1974 | } else if ( obj.constructor === Error.prototype.constructor ) { 1975 | type = "error"; 1976 | } else { 1977 | type = typeof obj; 1978 | } 1979 | return type; 1980 | }, 1981 | 1982 | separator: function() { 1983 | return this.multiline ? this.HTML ? "
" : "\n" : this.HTML ? " " : " "; 1984 | }, 1985 | 1986 | // Extra can be a number, shortcut for increasing-calling-decreasing 1987 | indent: function( extra ) { 1988 | if ( !this.multiline ) { 1989 | return ""; 1990 | } 1991 | var chr = this.indentChar; 1992 | if ( this.HTML ) { 1993 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " ); 1994 | } 1995 | return new Array( this.depth + ( extra || 0 ) ).join( chr ); 1996 | }, 1997 | up: function( a ) { 1998 | this.depth += a || 1; 1999 | }, 2000 | down: function( a ) { 2001 | this.depth -= a || 1; 2002 | }, 2003 | setParser: function( name, parser ) { 2004 | this.parsers[ name ] = parser; 2005 | }, 2006 | 2007 | // The next 3 are exposed so you can use them 2008 | quote: quote, 2009 | literal: literal, 2010 | join: join, 2011 | depth: 1, 2012 | maxDepth: QUnit.config.maxDepth, 2013 | 2014 | // This is the list of parsers, to modify them, use dump.setParser 2015 | parsers: { 2016 | window: "[Window]", 2017 | document: "[Document]", 2018 | error: function( error ) { 2019 | return "Error(\"" + error.message + "\")"; 2020 | }, 2021 | unknown: "[Unknown]", 2022 | "null": "null", 2023 | "undefined": "undefined", 2024 | "function": function( fn ) { 2025 | var ret = "function", 2026 | 2027 | // Functions never have name in IE 2028 | name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ]; 2029 | 2030 | if ( name ) { 2031 | ret += " " + name; 2032 | } 2033 | ret += "("; 2034 | 2035 | ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" ); 2036 | return join( ret, dump.parse( fn, "functionCode" ), "}" ); 2037 | }, 2038 | array: array, 2039 | nodelist: array, 2040 | "arguments": array, 2041 | object: function( map, stack ) { 2042 | var keys, key, val, i, nonEnumerableProperties, 2043 | ret = []; 2044 | 2045 | if ( dump.maxDepth && dump.depth > dump.maxDepth ) { 2046 | return "[object Object]"; 2047 | } 2048 | 2049 | dump.up(); 2050 | keys = []; 2051 | for ( key in map ) { 2052 | keys.push( key ); 2053 | } 2054 | 2055 | // Some properties are not always enumerable on Error objects. 2056 | nonEnumerableProperties = [ "message", "name" ]; 2057 | for ( i in nonEnumerableProperties ) { 2058 | key = nonEnumerableProperties[ i ]; 2059 | if ( key in map && inArray( key, keys ) < 0 ) { 2060 | keys.push( key ); 2061 | } 2062 | } 2063 | keys.sort(); 2064 | for ( i = 0; i < keys.length; i++ ) { 2065 | key = keys[ i ]; 2066 | val = map[ key ]; 2067 | ret.push( dump.parse( key, "key" ) + ": " + 2068 | dump.parse( val, undefined, stack ) ); 2069 | } 2070 | dump.down(); 2071 | return join( "{", ret, "}" ); 2072 | }, 2073 | node: function( node ) { 2074 | var len, i, val, 2075 | open = dump.HTML ? "<" : "<", 2076 | close = dump.HTML ? ">" : ">", 2077 | tag = node.nodeName.toLowerCase(), 2078 | ret = open + tag, 2079 | attrs = node.attributes; 2080 | 2081 | if ( attrs ) { 2082 | for ( i = 0, len = attrs.length; i < len; i++ ) { 2083 | val = attrs[ i ].nodeValue; 2084 | 2085 | // IE6 includes all attributes in .attributes, even ones not explicitly 2086 | // set. Those have values like undefined, null, 0, false, "" or 2087 | // "inherit". 2088 | if ( val && val !== "inherit" ) { 2089 | ret += " " + attrs[ i ].nodeName + "=" + 2090 | dump.parse( val, "attribute" ); 2091 | } 2092 | } 2093 | } 2094 | ret += close; 2095 | 2096 | // Show content of TextNode or CDATASection 2097 | if ( node.nodeType === 3 || node.nodeType === 4 ) { 2098 | ret += node.nodeValue; 2099 | } 2100 | 2101 | return ret + open + "/" + tag + close; 2102 | }, 2103 | 2104 | // Function calls it internally, it's the arguments part of the function 2105 | functionArgs: function( fn ) { 2106 | var args, 2107 | l = fn.length; 2108 | 2109 | if ( !l ) { 2110 | return ""; 2111 | } 2112 | 2113 | args = new Array( l ); 2114 | while ( l-- ) { 2115 | 2116 | // 97 is 'a' 2117 | args[ l ] = String.fromCharCode( 97 + l ); 2118 | } 2119 | return " " + args.join( ", " ) + " "; 2120 | }, 2121 | 2122 | // Object calls it internally, the key part of an item in a map 2123 | key: quote, 2124 | 2125 | // Function calls it internally, it's the content of the function 2126 | functionCode: "[code]", 2127 | 2128 | // Node calls it internally, it's a html attribute value 2129 | attribute: quote, 2130 | string: quote, 2131 | date: quote, 2132 | regexp: literal, 2133 | number: literal, 2134 | "boolean": literal, 2135 | symbol: function( sym ) { 2136 | return sym.toString(); 2137 | } 2138 | }, 2139 | 2140 | // If true, entities are escaped ( <, >, \t, space and \n ) 2141 | HTML: false, 2142 | 2143 | // Indentation unit 2144 | indentChar: " ", 2145 | 2146 | // If true, items in a collection, are separated by a \n, else just a space. 2147 | multiline: true 2148 | }; 2149 | 2150 | return dump; 2151 | }() ); 2152 | 2153 | // Back compat 2154 | QUnit.jsDump = QUnit.dump; 2155 | 2156 | function applyDeprecated( name ) { 2157 | return function() { 2158 | throw new Error( 2159 | name + " is removed in QUnit 2.0.\n" + 2160 | "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/" 2161 | ); 2162 | }; 2163 | } 2164 | 2165 | Object.keys( Assert.prototype ).forEach( function( key ) { 2166 | QUnit[ key ] = applyDeprecated( "`QUnit." + key + "`" ); 2167 | } ); 2168 | 2169 | QUnit.asyncTest = function() { 2170 | throw new Error( 2171 | "asyncTest is removed in QUnit 2.0, use QUnit.test() with assert.async() instead.\n" + 2172 | "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/" 2173 | ); 2174 | }; 2175 | 2176 | QUnit.stop = function() { 2177 | throw new Error( 2178 | "QUnit.stop is removed in QUnit 2.0, use QUnit.test() with assert.async() instead.\n" + 2179 | "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/" 2180 | ); 2181 | }; 2182 | 2183 | function resetThrower() { 2184 | throw new Error( 2185 | "QUnit.reset is removed in QUnit 2.0 without replacement.\n" + 2186 | "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/" 2187 | ); 2188 | } 2189 | 2190 | Object.defineProperty( QUnit, "reset", { 2191 | get: function() { 2192 | return resetThrower; 2193 | }, 2194 | set: resetThrower 2195 | } ); 2196 | 2197 | if ( defined.document ) { 2198 | if ( window.QUnit ) { 2199 | throw new Error( "QUnit has already been defined." ); 2200 | } 2201 | 2202 | [ 2203 | "test", 2204 | "module", 2205 | "expect", 2206 | "start", 2207 | "ok", 2208 | "notOk", 2209 | "equal", 2210 | "notEqual", 2211 | "propEqual", 2212 | "notPropEqual", 2213 | "deepEqual", 2214 | "notDeepEqual", 2215 | "strictEqual", 2216 | "notStrictEqual", 2217 | "throws", 2218 | "raises" 2219 | ].forEach( function( key ) { 2220 | window[ key ] = applyDeprecated( "The global `" + key + "`" ); 2221 | } ); 2222 | 2223 | window.QUnit = QUnit; 2224 | } 2225 | 2226 | // For nodejs 2227 | if ( typeof module !== "undefined" && module && module.exports ) { 2228 | module.exports = QUnit; 2229 | 2230 | // For consistency with CommonJS environments' exports 2231 | module.exports.QUnit = QUnit; 2232 | } 2233 | 2234 | // For CommonJS with exports, but without module.exports, like Rhino 2235 | if ( typeof exports !== "undefined" && exports ) { 2236 | exports.QUnit = QUnit; 2237 | } 2238 | 2239 | if ( typeof define === "function" && define.amd ) { 2240 | define( function() { 2241 | return QUnit; 2242 | } ); 2243 | QUnit.config.autostart = false; 2244 | } 2245 | 2246 | // Get a reference to the global object, like window in browsers 2247 | }( ( function() { 2248 | return this; 2249 | }() ) ) ); 2250 | 2251 | ( function() { 2252 | 2253 | if ( typeof window === "undefined" || !window.document ) { 2254 | return; 2255 | } 2256 | 2257 | var config = QUnit.config, 2258 | hasOwn = Object.prototype.hasOwnProperty; 2259 | 2260 | // Stores fixture HTML for resetting later 2261 | function storeFixture() { 2262 | 2263 | // Avoid overwriting user-defined values 2264 | if ( hasOwn.call( config, "fixture" ) ) { 2265 | return; 2266 | } 2267 | 2268 | var fixture = document.getElementById( "qunit-fixture" ); 2269 | if ( fixture ) { 2270 | config.fixture = fixture.innerHTML; 2271 | } 2272 | } 2273 | 2274 | QUnit.begin( storeFixture ); 2275 | 2276 | // Resets the fixture DOM element if available. 2277 | function resetFixture() { 2278 | if ( config.fixture == null ) { 2279 | return; 2280 | } 2281 | 2282 | var fixture = document.getElementById( "qunit-fixture" ); 2283 | if ( fixture ) { 2284 | fixture.innerHTML = config.fixture; 2285 | } 2286 | } 2287 | 2288 | QUnit.testStart( resetFixture ); 2289 | 2290 | }() ); 2291 | 2292 | ( function() { 2293 | 2294 | // Only interact with URLs via window.location 2295 | var location = typeof window !== "undefined" && window.location; 2296 | if ( !location ) { 2297 | return; 2298 | } 2299 | 2300 | var urlParams = getUrlParams(); 2301 | 2302 | QUnit.urlParams = urlParams; 2303 | 2304 | // Match module/test by inclusion in an array 2305 | QUnit.config.moduleId = [].concat( urlParams.moduleId || [] ); 2306 | QUnit.config.testId = [].concat( urlParams.testId || [] ); 2307 | 2308 | // Exact case-insensitive match of the module name 2309 | QUnit.config.module = urlParams.module; 2310 | 2311 | // Regular expression or case-insenstive substring match against "moduleName: testName" 2312 | QUnit.config.filter = urlParams.filter; 2313 | 2314 | // Test order randomization 2315 | if ( urlParams.seed === true ) { 2316 | 2317 | // Generate a random seed if the option is specified without a value 2318 | QUnit.config.seed = Math.random().toString( 36 ).slice( 2 ); 2319 | } else if ( urlParams.seed ) { 2320 | QUnit.config.seed = urlParams.seed; 2321 | } 2322 | 2323 | // Add URL-parameter-mapped config values with UI form rendering data 2324 | QUnit.config.urlConfig.push( 2325 | { 2326 | id: "hidepassed", 2327 | label: "Hide passed tests", 2328 | tooltip: "Only show tests and assertions that fail. Stored as query-strings." 2329 | }, 2330 | { 2331 | id: "noglobals", 2332 | label: "Check for Globals", 2333 | tooltip: "Enabling this will test if any test introduces new properties on the " + 2334 | "global object (`window` in Browsers). Stored as query-strings." 2335 | }, 2336 | { 2337 | id: "notrycatch", 2338 | label: "No try-catch", 2339 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " + 2340 | "exceptions in IE reasonable. Stored as query-strings." 2341 | } 2342 | ); 2343 | 2344 | QUnit.begin( function() { 2345 | var i, option, 2346 | urlConfig = QUnit.config.urlConfig; 2347 | 2348 | for ( i = 0; i < urlConfig.length; i++ ) { 2349 | 2350 | // Options can be either strings or objects with nonempty "id" properties 2351 | option = QUnit.config.urlConfig[ i ]; 2352 | if ( typeof option !== "string" ) { 2353 | option = option.id; 2354 | } 2355 | 2356 | if ( QUnit.config[ option ] === undefined ) { 2357 | QUnit.config[ option ] = urlParams[ option ]; 2358 | } 2359 | } 2360 | } ); 2361 | 2362 | function getUrlParams() { 2363 | var i, param, name, value; 2364 | var urlParams = {}; 2365 | var params = location.search.slice( 1 ).split( "&" ); 2366 | var length = params.length; 2367 | 2368 | for ( i = 0; i < length; i++ ) { 2369 | if ( params[ i ] ) { 2370 | param = params[ i ].split( "=" ); 2371 | name = decodeQueryParam( param[ 0 ] ); 2372 | 2373 | // Allow just a key to turn on a flag, e.g., test.html?noglobals 2374 | value = param.length === 1 || 2375 | decodeQueryParam( param.slice( 1 ).join( "=" ) ) ; 2376 | if ( urlParams[ name ] ) { 2377 | urlParams[ name ] = [].concat( urlParams[ name ], value ); 2378 | } else { 2379 | urlParams[ name ] = value; 2380 | } 2381 | } 2382 | } 2383 | 2384 | return urlParams; 2385 | } 2386 | 2387 | function decodeQueryParam( param ) { 2388 | return decodeURIComponent( param.replace( /\+/g, "%20" ) ); 2389 | } 2390 | 2391 | // Don't load the HTML Reporter on non-browser environments 2392 | if ( typeof window === "undefined" || !window.document ) { 2393 | return; 2394 | } 2395 | 2396 | QUnit.init = function() { 2397 | throw new Error( 2398 | "QUnit.init is removed in QUnit 2.0, use QUnit.test() with assert.async() instead.\n" + 2399 | "Details in our upgrade guide at https://qunitjs.com/upgrade-guide-2.x/" 2400 | ); 2401 | }; 2402 | 2403 | var config = QUnit.config, 2404 | document = window.document, 2405 | collapseNext = false, 2406 | hasOwn = Object.prototype.hasOwnProperty, 2407 | unfilteredUrl = setUrl( { filter: undefined, module: undefined, 2408 | moduleId: undefined, testId: undefined } ), 2409 | defined = { 2410 | sessionStorage: ( function() { 2411 | var x = "qunit-test-string"; 2412 | try { 2413 | sessionStorage.setItem( x, x ); 2414 | sessionStorage.removeItem( x ); 2415 | return true; 2416 | } catch ( e ) { 2417 | return false; 2418 | } 2419 | }() ) 2420 | }, 2421 | modulesList = []; 2422 | 2423 | // Escape text for attribute or text content. 2424 | function escapeText( s ) { 2425 | if ( !s ) { 2426 | return ""; 2427 | } 2428 | s = s + ""; 2429 | 2430 | // Both single quotes and double quotes (for attributes) 2431 | return s.replace( /['"<>&]/g, function( s ) { 2432 | switch ( s ) { 2433 | case "'": 2434 | return "'"; 2435 | case "\"": 2436 | return """; 2437 | case "<": 2438 | return "<"; 2439 | case ">": 2440 | return ">"; 2441 | case "&": 2442 | return "&"; 2443 | } 2444 | } ); 2445 | } 2446 | 2447 | function addEvent( elem, type, fn ) { 2448 | elem.addEventListener( type, fn, false ); 2449 | } 2450 | 2451 | function removeEvent( elem, type, fn ) { 2452 | elem.removeEventListener( type, fn, false ); 2453 | } 2454 | 2455 | function addEvents( elems, type, fn ) { 2456 | var i = elems.length; 2457 | while ( i-- ) { 2458 | addEvent( elems[ i ], type, fn ); 2459 | } 2460 | } 2461 | 2462 | function hasClass( elem, name ) { 2463 | return ( " " + elem.className + " " ).indexOf( " " + name + " " ) >= 0; 2464 | } 2465 | 2466 | function addClass( elem, name ) { 2467 | if ( !hasClass( elem, name ) ) { 2468 | elem.className += ( elem.className ? " " : "" ) + name; 2469 | } 2470 | } 2471 | 2472 | function toggleClass( elem, name, force ) { 2473 | if ( force || typeof force === "undefined" && !hasClass( elem, name ) ) { 2474 | addClass( elem, name ); 2475 | } else { 2476 | removeClass( elem, name ); 2477 | } 2478 | } 2479 | 2480 | function removeClass( elem, name ) { 2481 | var set = " " + elem.className + " "; 2482 | 2483 | // Class name may appear multiple times 2484 | while ( set.indexOf( " " + name + " " ) >= 0 ) { 2485 | set = set.replace( " " + name + " ", " " ); 2486 | } 2487 | 2488 | // Trim for prettiness 2489 | elem.className = typeof set.trim === "function" ? set.trim() : set.replace( /^\s+|\s+$/g, "" ); 2490 | } 2491 | 2492 | function id( name ) { 2493 | return document.getElementById && document.getElementById( name ); 2494 | } 2495 | 2496 | function interceptNavigation( ev ) { 2497 | applyUrlParams(); 2498 | 2499 | if ( ev && ev.preventDefault ) { 2500 | ev.preventDefault(); 2501 | } 2502 | 2503 | return false; 2504 | } 2505 | 2506 | function getUrlConfigHtml() { 2507 | var i, j, val, 2508 | escaped, escapedTooltip, 2509 | selection = false, 2510 | urlConfig = config.urlConfig, 2511 | urlConfigHtml = ""; 2512 | 2513 | for ( i = 0; i < urlConfig.length; i++ ) { 2514 | 2515 | // Options can be either strings or objects with nonempty "id" properties 2516 | val = config.urlConfig[ i ]; 2517 | if ( typeof val === "string" ) { 2518 | val = { 2519 | id: val, 2520 | label: val 2521 | }; 2522 | } 2523 | 2524 | escaped = escapeText( val.id ); 2525 | escapedTooltip = escapeText( val.tooltip ); 2526 | 2527 | if ( !val.value || typeof val.value === "string" ) { 2528 | urlConfigHtml += ""; 2534 | } else { 2535 | urlConfigHtml += ""; 2564 | } 2565 | } 2566 | 2567 | return urlConfigHtml; 2568 | } 2569 | 2570 | // Handle "click" events on toolbar checkboxes and "change" for select menus. 2571 | // Updates the URL with the new state of `config.urlConfig` values. 2572 | function toolbarChanged() { 2573 | var updatedUrl, value, tests, 2574 | field = this, 2575 | params = {}; 2576 | 2577 | // Detect if field is a select menu or a checkbox 2578 | if ( "selectedIndex" in field ) { 2579 | value = field.options[ field.selectedIndex ].value || undefined; 2580 | } else { 2581 | value = field.checked ? ( field.defaultValue || true ) : undefined; 2582 | } 2583 | 2584 | params[ field.name ] = value; 2585 | updatedUrl = setUrl( params ); 2586 | 2587 | // Check if we can apply the change without a page refresh 2588 | if ( "hidepassed" === field.name && "replaceState" in window.history ) { 2589 | QUnit.urlParams[ field.name ] = value; 2590 | config[ field.name ] = value || false; 2591 | tests = id( "qunit-tests" ); 2592 | if ( tests ) { 2593 | toggleClass( tests, "hidepass", value || false ); 2594 | } 2595 | window.history.replaceState( null, "", updatedUrl ); 2596 | } else { 2597 | window.location = updatedUrl; 2598 | } 2599 | } 2600 | 2601 | function setUrl( params ) { 2602 | var key, arrValue, i, 2603 | querystring = "?", 2604 | location = window.location; 2605 | 2606 | params = QUnit.extend( QUnit.extend( {}, QUnit.urlParams ), params ); 2607 | 2608 | for ( key in params ) { 2609 | 2610 | // Skip inherited or undefined properties 2611 | if ( hasOwn.call( params, key ) && params[ key ] !== undefined ) { 2612 | 2613 | // Output a parameter for each value of this key (but usually just one) 2614 | arrValue = [].concat( params[ key ] ); 2615 | for ( i = 0; i < arrValue.length; i++ ) { 2616 | querystring += encodeURIComponent( key ); 2617 | if ( arrValue[ i ] !== true ) { 2618 | querystring += "=" + encodeURIComponent( arrValue[ i ] ); 2619 | } 2620 | querystring += "&"; 2621 | } 2622 | } 2623 | } 2624 | return location.protocol + "//" + location.host + 2625 | location.pathname + querystring.slice( 0, -1 ); 2626 | } 2627 | 2628 | function applyUrlParams() { 2629 | var i, 2630 | selectedModules = [], 2631 | modulesList = id( "qunit-modulefilter-dropdown-list" ).getElementsByTagName( "input" ), 2632 | filter = id( "qunit-filter-input" ).value; 2633 | 2634 | for ( i = 0; i < modulesList.length; i++ ) { 2635 | if ( modulesList[ i ].checked ) { 2636 | selectedModules.push( modulesList[ i ].value ); 2637 | } 2638 | } 2639 | 2640 | window.location = setUrl( { 2641 | filter: ( filter === "" ) ? undefined : filter, 2642 | moduleId: ( selectedModules.length === 0 ) ? undefined : selectedModules, 2643 | 2644 | // Remove module and testId filter 2645 | module: undefined, 2646 | testId: undefined 2647 | } ); 2648 | } 2649 | 2650 | function toolbarUrlConfigContainer() { 2651 | var urlConfigContainer = document.createElement( "span" ); 2652 | 2653 | urlConfigContainer.innerHTML = getUrlConfigHtml(); 2654 | addClass( urlConfigContainer, "qunit-url-config" ); 2655 | 2656 | addEvents( urlConfigContainer.getElementsByTagName( "input" ), "change", toolbarChanged ); 2657 | addEvents( urlConfigContainer.getElementsByTagName( "select" ), "change", toolbarChanged ); 2658 | 2659 | return urlConfigContainer; 2660 | } 2661 | 2662 | function toolbarLooseFilter() { 2663 | var filter = document.createElement( "form" ), 2664 | label = document.createElement( "label" ), 2665 | input = document.createElement( "input" ), 2666 | button = document.createElement( "button" ); 2667 | 2668 | addClass( filter, "qunit-filter" ); 2669 | 2670 | label.innerHTML = "Filter: "; 2671 | 2672 | input.type = "text"; 2673 | input.value = config.filter || ""; 2674 | input.name = "filter"; 2675 | input.id = "qunit-filter-input"; 2676 | 2677 | button.innerHTML = "Go"; 2678 | 2679 | label.appendChild( input ); 2680 | 2681 | filter.appendChild( label ); 2682 | filter.appendChild( document.createTextNode( " " ) ); 2683 | filter.appendChild( button ); 2684 | addEvent( filter, "submit", interceptNavigation ); 2685 | 2686 | return filter; 2687 | } 2688 | 2689 | function moduleListHtml () { 2690 | var i, checked, 2691 | html = ""; 2692 | 2693 | for ( i = 0; i < config.modules.length; i++ ) { 2694 | if ( config.modules[ i ].name !== "" ) { 2695 | checked = config.moduleId.indexOf( config.modules[ i ].moduleId ) > -1; 2696 | html += "
  • "; 2700 | } 2701 | } 2702 | 2703 | return html; 2704 | } 2705 | 2706 | function toolbarModuleFilter () { 2707 | var allCheckbox, commit, reset, 2708 | moduleFilter = document.createElement( "form" ), 2709 | label = document.createElement( "label" ), 2710 | moduleSearch = document.createElement( "input" ), 2711 | dropDown = document.createElement( "div" ), 2712 | actions = document.createElement( "span" ), 2713 | dropDownList = document.createElement( "ul" ), 2714 | dirty = false; 2715 | 2716 | moduleSearch.id = "qunit-modulefilter-search"; 2717 | addEvent( moduleSearch, "input", searchInput ); 2718 | addEvent( moduleSearch, "input", searchFocus ); 2719 | addEvent( moduleSearch, "focus", searchFocus ); 2720 | addEvent( moduleSearch, "click", searchFocus ); 2721 | 2722 | label.id = "qunit-modulefilter-search-container"; 2723 | label.innerHTML = "Module: "; 2724 | label.appendChild( moduleSearch ); 2725 | 2726 | actions.id = "qunit-modulefilter-actions"; 2727 | actions.innerHTML = 2728 | "" + 2729 | "" + 2730 | ""; 2734 | allCheckbox = actions.lastChild.firstChild; 2735 | commit = actions.firstChild; 2736 | reset = commit.nextSibling; 2737 | addEvent( commit, "click", applyUrlParams ); 2738 | 2739 | dropDownList.id = "qunit-modulefilter-dropdown-list"; 2740 | dropDownList.innerHTML = moduleListHtml(); 2741 | 2742 | dropDown.id = "qunit-modulefilter-dropdown"; 2743 | dropDown.style.display = "none"; 2744 | dropDown.appendChild( actions ); 2745 | dropDown.appendChild( dropDownList ); 2746 | addEvent( dropDown, "change", selectionChange ); 2747 | selectionChange(); 2748 | 2749 | moduleFilter.id = "qunit-modulefilter"; 2750 | moduleFilter.appendChild( label ); 2751 | moduleFilter.appendChild( dropDown ) ; 2752 | addEvent( moduleFilter, "submit", interceptNavigation ); 2753 | addEvent( moduleFilter, "reset", function() { 2754 | 2755 | // Let the reset happen, then update styles 2756 | window.setTimeout( selectionChange ); 2757 | } ); 2758 | 2759 | // Enables show/hide for the dropdown 2760 | function searchFocus() { 2761 | if ( dropDown.style.display !== "none" ) { 2762 | return; 2763 | } 2764 | 2765 | dropDown.style.display = "block"; 2766 | addEvent( document, "click", hideHandler ); 2767 | addEvent( document, "keydown", hideHandler ); 2768 | 2769 | // Hide on Escape keydown or outside-container click 2770 | function hideHandler( e ) { 2771 | var inContainer = moduleFilter.contains( e.target ); 2772 | 2773 | if ( e.keyCode === 27 || !inContainer ) { 2774 | if ( e.keyCode === 27 && inContainer ) { 2775 | moduleSearch.focus(); 2776 | } 2777 | dropDown.style.display = "none"; 2778 | removeEvent( document, "click", hideHandler ); 2779 | removeEvent( document, "keydown", hideHandler ); 2780 | moduleSearch.value = ""; 2781 | searchInput(); 2782 | } 2783 | } 2784 | } 2785 | 2786 | // Processes module search box input 2787 | function searchInput() { 2788 | var i, item, 2789 | searchText = moduleSearch.value.toLowerCase(), 2790 | listItems = dropDownList.children; 2791 | 2792 | for ( i = 0; i < listItems.length; i++ ) { 2793 | item = listItems[ i ]; 2794 | if ( !searchText || item.textContent.toLowerCase().indexOf( searchText ) > -1 ) { 2795 | item.style.display = ""; 2796 | } else { 2797 | item.style.display = "none"; 2798 | } 2799 | } 2800 | } 2801 | 2802 | // Processes selection changes 2803 | function selectionChange( evt ) { 2804 | var i, item, 2805 | checkbox = evt && evt.target || allCheckbox, 2806 | modulesList = dropDownList.getElementsByTagName( "input" ), 2807 | selectedNames = []; 2808 | 2809 | toggleClass( checkbox.parentNode, "checked", checkbox.checked ); 2810 | 2811 | dirty = false; 2812 | if ( checkbox.checked && checkbox !== allCheckbox ) { 2813 | allCheckbox.checked = false; 2814 | removeClass( allCheckbox.parentNode, "checked" ); 2815 | } 2816 | for ( i = 0; i < modulesList.length; i++ ) { 2817 | item = modulesList[ i ]; 2818 | if ( !evt ) { 2819 | toggleClass( item.parentNode, "checked", item.checked ); 2820 | } else if ( checkbox === allCheckbox && checkbox.checked ) { 2821 | item.checked = false; 2822 | removeClass( item.parentNode, "checked" ); 2823 | } 2824 | dirty = dirty || ( item.checked !== item.defaultChecked ); 2825 | if ( item.checked ) { 2826 | selectedNames.push( item.parentNode.textContent ); 2827 | } 2828 | } 2829 | 2830 | commit.style.display = reset.style.display = dirty ? "" : "none"; 2831 | moduleSearch.placeholder = selectedNames.join( ", " ) || allCheckbox.parentNode.textContent; 2832 | moduleSearch.title = "Type to filter list. Current selection:\n" + 2833 | ( selectedNames.join( "\n" ) || allCheckbox.parentNode.textContent ); 2834 | } 2835 | 2836 | return moduleFilter; 2837 | } 2838 | 2839 | function appendToolbar() { 2840 | var toolbar = id( "qunit-testrunner-toolbar" ); 2841 | 2842 | if ( toolbar ) { 2843 | toolbar.appendChild( toolbarUrlConfigContainer() ); 2844 | toolbar.appendChild( toolbarModuleFilter() ); 2845 | toolbar.appendChild( toolbarLooseFilter() ); 2846 | toolbar.appendChild( document.createElement( "div" ) ).className = "clearfix"; 2847 | } 2848 | } 2849 | 2850 | function appendHeader() { 2851 | var header = id( "qunit-header" ); 2852 | 2853 | if ( header ) { 2854 | header.innerHTML = "" + header.innerHTML + 2855 | " "; 2856 | } 2857 | } 2858 | 2859 | function appendBanner() { 2860 | var banner = id( "qunit-banner" ); 2861 | 2862 | if ( banner ) { 2863 | banner.className = ""; 2864 | } 2865 | } 2866 | 2867 | function appendTestResults() { 2868 | var tests = id( "qunit-tests" ), 2869 | result = id( "qunit-testresult" ); 2870 | 2871 | if ( result ) { 2872 | result.parentNode.removeChild( result ); 2873 | } 2874 | 2875 | if ( tests ) { 2876 | tests.innerHTML = ""; 2877 | result = document.createElement( "p" ); 2878 | result.id = "qunit-testresult"; 2879 | result.className = "result"; 2880 | tests.parentNode.insertBefore( result, tests ); 2881 | result.innerHTML = "Running...
     "; 2882 | } 2883 | } 2884 | 2885 | function appendFilteredTest() { 2886 | var testId = QUnit.config.testId; 2887 | if ( !testId || testId.length <= 0 ) { 2888 | return ""; 2889 | } 2890 | return "
    Rerunning selected tests: " + 2891 | escapeText( testId.join( ", " ) ) + 2892 | " Run all tests
    "; 2895 | } 2896 | 2897 | function appendUserAgent() { 2898 | var userAgent = id( "qunit-userAgent" ); 2899 | 2900 | if ( userAgent ) { 2901 | userAgent.innerHTML = ""; 2902 | userAgent.appendChild( 2903 | document.createTextNode( 2904 | "QUnit " + QUnit.version + "; " + navigator.userAgent 2905 | ) 2906 | ); 2907 | } 2908 | } 2909 | 2910 | function appendInterface() { 2911 | var qunit = id( "qunit" ); 2912 | 2913 | if ( qunit ) { 2914 | qunit.innerHTML = 2915 | "

    " + escapeText( document.title ) + "

    " + 2916 | "

    " + 2917 | "
    " + 2918 | appendFilteredTest() + 2919 | "

    " + 2920 | "
      "; 2921 | } 2922 | 2923 | appendHeader(); 2924 | appendBanner(); 2925 | appendTestResults(); 2926 | appendUserAgent(); 2927 | appendToolbar(); 2928 | } 2929 | 2930 | function appendTestsList( modules ) { 2931 | var i, l, x, z, test, moduleObj; 2932 | 2933 | for ( i = 0, l = modules.length; i < l; i++ ) { 2934 | moduleObj = modules[ i ]; 2935 | 2936 | for ( x = 0, z = moduleObj.tests.length; x < z; x++ ) { 2937 | test = moduleObj.tests[ x ]; 2938 | 2939 | appendTest( test.name, test.testId, moduleObj.name ); 2940 | } 2941 | } 2942 | } 2943 | 2944 | function appendTest( name, testId, moduleName ) { 2945 | var title, rerunTrigger, testBlock, assertList, 2946 | tests = id( "qunit-tests" ); 2947 | 2948 | if ( !tests ) { 2949 | return; 2950 | } 2951 | 2952 | title = document.createElement( "strong" ); 2953 | title.innerHTML = getNameHtml( name, moduleName ); 2954 | 2955 | rerunTrigger = document.createElement( "a" ); 2956 | rerunTrigger.innerHTML = "Rerun"; 2957 | rerunTrigger.href = setUrl( { testId: testId } ); 2958 | 2959 | testBlock = document.createElement( "li" ); 2960 | testBlock.appendChild( title ); 2961 | testBlock.appendChild( rerunTrigger ); 2962 | testBlock.id = "qunit-test-output-" + testId; 2963 | 2964 | assertList = document.createElement( "ol" ); 2965 | assertList.className = "qunit-assert-list"; 2966 | 2967 | testBlock.appendChild( assertList ); 2968 | 2969 | tests.appendChild( testBlock ); 2970 | } 2971 | 2972 | // HTML Reporter initialization and load 2973 | QUnit.begin( function( details ) { 2974 | var i, moduleObj, tests; 2975 | 2976 | // Sort modules by name for the picker 2977 | for ( i = 0; i < details.modules.length; i++ ) { 2978 | moduleObj = details.modules[ i ]; 2979 | if ( moduleObj.name ) { 2980 | modulesList.push( moduleObj.name ); 2981 | } 2982 | } 2983 | modulesList.sort( function( a, b ) { 2984 | return a.localeCompare( b ); 2985 | } ); 2986 | 2987 | // Initialize QUnit elements 2988 | appendInterface(); 2989 | appendTestsList( details.modules ); 2990 | tests = id( "qunit-tests" ); 2991 | if ( tests && config.hidepassed ) { 2992 | addClass( tests, "hidepass" ); 2993 | } 2994 | } ); 2995 | 2996 | QUnit.done( function( details ) { 2997 | var i, key, 2998 | banner = id( "qunit-banner" ), 2999 | tests = id( "qunit-tests" ), 3000 | html = [ 3001 | "Tests completed in ", 3002 | details.runtime, 3003 | " milliseconds.
      ", 3004 | "", 3005 | details.passed, 3006 | " assertions of ", 3007 | details.total, 3008 | " passed, ", 3009 | details.failed, 3010 | " failed." 3011 | ].join( "" ); 3012 | 3013 | if ( banner ) { 3014 | banner.className = details.failed ? "qunit-fail" : "qunit-pass"; 3015 | } 3016 | 3017 | if ( tests ) { 3018 | id( "qunit-testresult" ).innerHTML = html; 3019 | } 3020 | 3021 | if ( config.altertitle && document.title ) { 3022 | 3023 | // Show ✖ for good, ✔ for bad suite result in title 3024 | // use escape sequences in case file gets loaded with non-utf-8-charset 3025 | document.title = [ 3026 | ( details.failed ? "\u2716" : "\u2714" ), 3027 | document.title.replace( /^[\u2714\u2716] /i, "" ) 3028 | ].join( " " ); 3029 | } 3030 | 3031 | // Clear own sessionStorage items if all tests passed 3032 | if ( config.reorder && defined.sessionStorage && details.failed === 0 ) { 3033 | for ( i = 0; i < sessionStorage.length; i++ ) { 3034 | key = sessionStorage.key( i++ ); 3035 | if ( key.indexOf( "qunit-test-" ) === 0 ) { 3036 | sessionStorage.removeItem( key ); 3037 | } 3038 | } 3039 | } 3040 | 3041 | // Scroll back to top to show results 3042 | if ( config.scrolltop && window.scrollTo ) { 3043 | window.scrollTo( 0, 0 ); 3044 | } 3045 | } ); 3046 | 3047 | function getNameHtml( name, module ) { 3048 | var nameHtml = ""; 3049 | 3050 | if ( module ) { 3051 | nameHtml = "" + escapeText( module ) + ": "; 3052 | } 3053 | 3054 | nameHtml += "" + escapeText( name ) + ""; 3055 | 3056 | return nameHtml; 3057 | } 3058 | 3059 | QUnit.testStart( function( details ) { 3060 | var running, testBlock, bad; 3061 | 3062 | testBlock = id( "qunit-test-output-" + details.testId ); 3063 | if ( testBlock ) { 3064 | testBlock.className = "running"; 3065 | } else { 3066 | 3067 | // Report later registered tests 3068 | appendTest( details.name, details.testId, details.module ); 3069 | } 3070 | 3071 | running = id( "qunit-testresult" ); 3072 | if ( running ) { 3073 | bad = QUnit.config.reorder && defined.sessionStorage && 3074 | +sessionStorage.getItem( "qunit-test-" + details.module + "-" + details.name ); 3075 | 3076 | running.innerHTML = ( bad ? 3077 | "Rerunning previously failed test:
      " : 3078 | "Running:
      " ) + 3079 | getNameHtml( details.name, details.module ); 3080 | } 3081 | 3082 | } ); 3083 | 3084 | function stripHtml( string ) { 3085 | 3086 | // Strip tags, html entity and whitespaces 3087 | return string.replace( /<\/?[^>]+(>|$)/g, "" ).replace( /\"/g, "" ).replace( /\s+/g, "" ); 3088 | } 3089 | 3090 | QUnit.log( function( details ) { 3091 | var assertList, assertLi, 3092 | message, expected, actual, diff, 3093 | showDiff = false, 3094 | testItem = id( "qunit-test-output-" + details.testId ); 3095 | 3096 | if ( !testItem ) { 3097 | return; 3098 | } 3099 | 3100 | message = escapeText( details.message ) || ( details.result ? "okay" : "failed" ); 3101 | message = "" + message + ""; 3102 | message += "@ " + details.runtime + " ms"; 3103 | 3104 | // The pushFailure doesn't provide details.expected 3105 | // when it calls, it's implicit to also not show expected and diff stuff 3106 | // Also, we need to check details.expected existence, as it can exist and be undefined 3107 | if ( !details.result && hasOwn.call( details, "expected" ) ) { 3108 | if ( details.negative ) { 3109 | expected = "NOT " + QUnit.dump.parse( details.expected ); 3110 | } else { 3111 | expected = QUnit.dump.parse( details.expected ); 3112 | } 3113 | 3114 | actual = QUnit.dump.parse( details.actual ); 3115 | message += ""; 3118 | 3119 | if ( actual !== expected ) { 3120 | 3121 | message += ""; 3123 | 3124 | // Don't show diff if actual or expected are booleans 3125 | if ( !( /^(true|false)$/.test( actual ) ) && 3126 | !( /^(true|false)$/.test( expected ) ) ) { 3127 | diff = QUnit.diff( expected, actual ); 3128 | showDiff = stripHtml( diff ).length !== 3129 | stripHtml( expected ).length + 3130 | stripHtml( actual ).length; 3131 | } 3132 | 3133 | // Don't show diff if expected and actual are totally different 3134 | if ( showDiff ) { 3135 | message += ""; 3137 | } 3138 | } else if ( expected.indexOf( "[object Array]" ) !== -1 || 3139 | expected.indexOf( "[object Object]" ) !== -1 ) { 3140 | message += ""; 3146 | } else { 3147 | message += ""; 3150 | } 3151 | 3152 | if ( details.source ) { 3153 | message += ""; 3155 | } 3156 | 3157 | message += "
      Expected:
      " +
      3116 | 			escapeText( expected ) +
      3117 | 			"
      Result:
      " +
      3122 | 				escapeText( actual ) + "
      Diff:
      " +
      3136 | 					diff + "
      Message: " + 3141 | "Diff suppressed as the depth of object is more than current max depth (" + 3142 | QUnit.config.maxDepth + ").

      Hint: Use QUnit.dump.maxDepth to " + 3143 | " run with a higher max depth or " + 3145 | "Rerun without max depth.

      Message: " + 3148 | "Diff suppressed as the expected and actual results have an equivalent" + 3149 | " serialization
      Source:
      " +
      3154 | 				escapeText( details.source ) + "
      "; 3158 | 3159 | // This occurs when pushFailure is set and we have an extracted stack trace 3160 | } else if ( !details.result && details.source ) { 3161 | message += "" + 3162 | "" + 3164 | "
      Source:
      " +
      3163 | 			escapeText( details.source ) + "
      "; 3165 | } 3166 | 3167 | assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; 3168 | 3169 | assertLi = document.createElement( "li" ); 3170 | assertLi.className = details.result ? "pass" : "fail"; 3171 | assertLi.innerHTML = message; 3172 | assertList.appendChild( assertLi ); 3173 | } ); 3174 | 3175 | QUnit.testDone( function( details ) { 3176 | var testTitle, time, testItem, assertList, 3177 | good, bad, testCounts, skipped, sourceName, 3178 | tests = id( "qunit-tests" ); 3179 | 3180 | if ( !tests ) { 3181 | return; 3182 | } 3183 | 3184 | testItem = id( "qunit-test-output-" + details.testId ); 3185 | 3186 | assertList = testItem.getElementsByTagName( "ol" )[ 0 ]; 3187 | 3188 | good = details.passed; 3189 | bad = details.failed; 3190 | 3191 | // Store result when possible 3192 | if ( config.reorder && defined.sessionStorage ) { 3193 | if ( bad ) { 3194 | sessionStorage.setItem( "qunit-test-" + details.module + "-" + details.name, bad ); 3195 | } else { 3196 | sessionStorage.removeItem( "qunit-test-" + details.module + "-" + details.name ); 3197 | } 3198 | } 3199 | 3200 | if ( bad === 0 ) { 3201 | 3202 | // Collapse the passing tests 3203 | addClass( assertList, "qunit-collapsed" ); 3204 | } else if ( bad && config.collapse && !collapseNext ) { 3205 | 3206 | // Skip collapsing the first failing test 3207 | collapseNext = true; 3208 | } else { 3209 | 3210 | // Collapse remaining tests 3211 | addClass( assertList, "qunit-collapsed" ); 3212 | } 3213 | 3214 | // The testItem.firstChild is the test name 3215 | testTitle = testItem.firstChild; 3216 | 3217 | testCounts = bad ? 3218 | "" + bad + ", " + "" + good + ", " : 3219 | ""; 3220 | 3221 | testTitle.innerHTML += " (" + testCounts + 3222 | details.assertions.length + ")"; 3223 | 3224 | if ( details.skipped ) { 3225 | testItem.className = "skipped"; 3226 | skipped = document.createElement( "em" ); 3227 | skipped.className = "qunit-skipped-label"; 3228 | skipped.innerHTML = "skipped"; 3229 | testItem.insertBefore( skipped, testTitle ); 3230 | } else { 3231 | addEvent( testTitle, "click", function() { 3232 | toggleClass( assertList, "qunit-collapsed" ); 3233 | } ); 3234 | 3235 | testItem.className = bad ? "fail" : "pass"; 3236 | 3237 | time = document.createElement( "span" ); 3238 | time.className = "runtime"; 3239 | time.innerHTML = details.runtime + " ms"; 3240 | testItem.insertBefore( time, assertList ); 3241 | } 3242 | 3243 | // Show the source of the test when showing assertions 3244 | if ( details.source ) { 3245 | sourceName = document.createElement( "p" ); 3246 | sourceName.innerHTML = "Source: " + details.source; 3247 | addClass( sourceName, "qunit-source" ); 3248 | if ( bad === 0 ) { 3249 | addClass( sourceName, "qunit-collapsed" ); 3250 | } 3251 | addEvent( testTitle, "click", function() { 3252 | toggleClass( sourceName, "qunit-collapsed" ); 3253 | } ); 3254 | testItem.appendChild( sourceName ); 3255 | } 3256 | } ); 3257 | 3258 | // Avoid readyState issue with phantomjs 3259 | // Ref: #818 3260 | var notPhantom = ( function( p ) { 3261 | return !( p && p.version && p.version.major > 0 ); 3262 | } )( window.phantom ); 3263 | 3264 | if ( notPhantom && document.readyState === "complete" ) { 3265 | QUnit.load(); 3266 | } else { 3267 | addEvent( window, "load", QUnit.load ); 3268 | } 3269 | 3270 | /* 3271 | * This file is a modified version of google-diff-match-patch's JavaScript implementation 3272 | * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), 3273 | * modifications are licensed as more fully set forth in LICENSE.txt. 3274 | * 3275 | * The original source of google-diff-match-patch is attributable and licensed as follows: 3276 | * 3277 | * Copyright 2006 Google Inc. 3278 | * https://code.google.com/p/google-diff-match-patch/ 3279 | * 3280 | * Licensed under the Apache License, Version 2.0 (the "License"); 3281 | * you may not use this file except in compliance with the License. 3282 | * You may obtain a copy of the License at 3283 | * 3284 | * https://www.apache.org/licenses/LICENSE-2.0 3285 | * 3286 | * Unless required by applicable law or agreed to in writing, software 3287 | * distributed under the License is distributed on an "AS IS" BASIS, 3288 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 3289 | * See the License for the specific language governing permissions and 3290 | * limitations under the License. 3291 | * 3292 | * More Info: 3293 | * https://code.google.com/p/google-diff-match-patch/ 3294 | * 3295 | * Usage: QUnit.diff(expected, actual) 3296 | * 3297 | */ 3298 | QUnit.diff = ( function() { 3299 | function DiffMatchPatch() { 3300 | } 3301 | 3302 | // DIFF FUNCTIONS 3303 | 3304 | /** 3305 | * The data structure representing a diff is an array of tuples: 3306 | * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] 3307 | * which means: delete 'Hello', add 'Goodbye' and keep ' world.' 3308 | */ 3309 | var DIFF_DELETE = -1, 3310 | DIFF_INSERT = 1, 3311 | DIFF_EQUAL = 0; 3312 | 3313 | /** 3314 | * Find the differences between two texts. Simplifies the problem by stripping 3315 | * any common prefix or suffix off the texts before diffing. 3316 | * @param {string} text1 Old string to be diffed. 3317 | * @param {string} text2 New string to be diffed. 3318 | * @param {boolean=} optChecklines Optional speedup flag. If present and false, 3319 | * then don't run a line-level diff first to identify the changed areas. 3320 | * Defaults to true, which does a faster, slightly less optimal diff. 3321 | * @return {!Array.} Array of diff tuples. 3322 | */ 3323 | DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines ) { 3324 | var deadline, checklines, commonlength, 3325 | commonprefix, commonsuffix, diffs; 3326 | 3327 | // The diff must be complete in up to 1 second. 3328 | deadline = ( new Date() ).getTime() + 1000; 3329 | 3330 | // Check for null inputs. 3331 | if ( text1 === null || text2 === null ) { 3332 | throw new Error( "Null input. (DiffMain)" ); 3333 | } 3334 | 3335 | // Check for equality (speedup). 3336 | if ( text1 === text2 ) { 3337 | if ( text1 ) { 3338 | return [ 3339 | [ DIFF_EQUAL, text1 ] 3340 | ]; 3341 | } 3342 | return []; 3343 | } 3344 | 3345 | if ( typeof optChecklines === "undefined" ) { 3346 | optChecklines = true; 3347 | } 3348 | 3349 | checklines = optChecklines; 3350 | 3351 | // Trim off common prefix (speedup). 3352 | commonlength = this.diffCommonPrefix( text1, text2 ); 3353 | commonprefix = text1.substring( 0, commonlength ); 3354 | text1 = text1.substring( commonlength ); 3355 | text2 = text2.substring( commonlength ); 3356 | 3357 | // Trim off common suffix (speedup). 3358 | commonlength = this.diffCommonSuffix( text1, text2 ); 3359 | commonsuffix = text1.substring( text1.length - commonlength ); 3360 | text1 = text1.substring( 0, text1.length - commonlength ); 3361 | text2 = text2.substring( 0, text2.length - commonlength ); 3362 | 3363 | // Compute the diff on the middle block. 3364 | diffs = this.diffCompute( text1, text2, checklines, deadline ); 3365 | 3366 | // Restore the prefix and suffix. 3367 | if ( commonprefix ) { 3368 | diffs.unshift( [ DIFF_EQUAL, commonprefix ] ); 3369 | } 3370 | if ( commonsuffix ) { 3371 | diffs.push( [ DIFF_EQUAL, commonsuffix ] ); 3372 | } 3373 | this.diffCleanupMerge( diffs ); 3374 | return diffs; 3375 | }; 3376 | 3377 | /** 3378 | * Reduce the number of edits by eliminating operationally trivial equalities. 3379 | * @param {!Array.} diffs Array of diff tuples. 3380 | */ 3381 | DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) { 3382 | var changes, equalities, equalitiesLength, lastequality, 3383 | pointer, preIns, preDel, postIns, postDel; 3384 | changes = false; 3385 | equalities = []; // Stack of indices where equalities are found. 3386 | equalitiesLength = 0; // Keeping our own length var is faster in JS. 3387 | /** @type {?string} */ 3388 | lastequality = null; 3389 | 3390 | // Always equal to diffs[equalities[equalitiesLength - 1]][1] 3391 | pointer = 0; // Index of current position. 3392 | 3393 | // Is there an insertion operation before the last equality. 3394 | preIns = false; 3395 | 3396 | // Is there a deletion operation before the last equality. 3397 | preDel = false; 3398 | 3399 | // Is there an insertion operation after the last equality. 3400 | postIns = false; 3401 | 3402 | // Is there a deletion operation after the last equality. 3403 | postDel = false; 3404 | while ( pointer < diffs.length ) { 3405 | 3406 | // Equality found. 3407 | if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { 3408 | if ( diffs[ pointer ][ 1 ].length < 4 && ( postIns || postDel ) ) { 3409 | 3410 | // Candidate found. 3411 | equalities[ equalitiesLength++ ] = pointer; 3412 | preIns = postIns; 3413 | preDel = postDel; 3414 | lastequality = diffs[ pointer ][ 1 ]; 3415 | } else { 3416 | 3417 | // Not a candidate, and can never become one. 3418 | equalitiesLength = 0; 3419 | lastequality = null; 3420 | } 3421 | postIns = postDel = false; 3422 | 3423 | // An insertion or deletion. 3424 | } else { 3425 | 3426 | if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) { 3427 | postDel = true; 3428 | } else { 3429 | postIns = true; 3430 | } 3431 | 3432 | /* 3433 | * Five types to be split: 3434 | * ABXYCD 3435 | * AXCD 3436 | * ABXC 3437 | * AXCD 3438 | * ABXC 3439 | */ 3440 | if ( lastequality && ( ( preIns && preDel && postIns && postDel ) || 3441 | ( ( lastequality.length < 2 ) && 3442 | ( preIns + preDel + postIns + postDel ) === 3 ) ) ) { 3443 | 3444 | // Duplicate record. 3445 | diffs.splice( 3446 | equalities[ equalitiesLength - 1 ], 3447 | 0, 3448 | [ DIFF_DELETE, lastequality ] 3449 | ); 3450 | 3451 | // Change second copy to insert. 3452 | diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; 3453 | equalitiesLength--; // Throw away the equality we just deleted; 3454 | lastequality = null; 3455 | if ( preIns && preDel ) { 3456 | 3457 | // No changes made which could affect previous entry, keep going. 3458 | postIns = postDel = true; 3459 | equalitiesLength = 0; 3460 | } else { 3461 | equalitiesLength--; // Throw away the previous equality. 3462 | pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; 3463 | postIns = postDel = false; 3464 | } 3465 | changes = true; 3466 | } 3467 | } 3468 | pointer++; 3469 | } 3470 | 3471 | if ( changes ) { 3472 | this.diffCleanupMerge( diffs ); 3473 | } 3474 | }; 3475 | 3476 | /** 3477 | * Convert a diff array into a pretty HTML report. 3478 | * @param {!Array.} diffs Array of diff tuples. 3479 | * @param {integer} string to be beautified. 3480 | * @return {string} HTML representation. 3481 | */ 3482 | DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) { 3483 | var op, data, x, 3484 | html = []; 3485 | for ( x = 0; x < diffs.length; x++ ) { 3486 | op = diffs[ x ][ 0 ]; // Operation (insert, delete, equal) 3487 | data = diffs[ x ][ 1 ]; // Text of change. 3488 | switch ( op ) { 3489 | case DIFF_INSERT: 3490 | html[ x ] = "" + escapeText( data ) + ""; 3491 | break; 3492 | case DIFF_DELETE: 3493 | html[ x ] = "" + escapeText( data ) + ""; 3494 | break; 3495 | case DIFF_EQUAL: 3496 | html[ x ] = "" + escapeText( data ) + ""; 3497 | break; 3498 | } 3499 | } 3500 | return html.join( "" ); 3501 | }; 3502 | 3503 | /** 3504 | * Determine the common prefix of two strings. 3505 | * @param {string} text1 First string. 3506 | * @param {string} text2 Second string. 3507 | * @return {number} The number of characters common to the start of each 3508 | * string. 3509 | */ 3510 | DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) { 3511 | var pointermid, pointermax, pointermin, pointerstart; 3512 | 3513 | // Quick check for common null cases. 3514 | if ( !text1 || !text2 || text1.charAt( 0 ) !== text2.charAt( 0 ) ) { 3515 | return 0; 3516 | } 3517 | 3518 | // Binary search. 3519 | // Performance analysis: https://neil.fraser.name/news/2007/10/09/ 3520 | pointermin = 0; 3521 | pointermax = Math.min( text1.length, text2.length ); 3522 | pointermid = pointermax; 3523 | pointerstart = 0; 3524 | while ( pointermin < pointermid ) { 3525 | if ( text1.substring( pointerstart, pointermid ) === 3526 | text2.substring( pointerstart, pointermid ) ) { 3527 | pointermin = pointermid; 3528 | pointerstart = pointermin; 3529 | } else { 3530 | pointermax = pointermid; 3531 | } 3532 | pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); 3533 | } 3534 | return pointermid; 3535 | }; 3536 | 3537 | /** 3538 | * Determine the common suffix of two strings. 3539 | * @param {string} text1 First string. 3540 | * @param {string} text2 Second string. 3541 | * @return {number} The number of characters common to the end of each string. 3542 | */ 3543 | DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) { 3544 | var pointermid, pointermax, pointermin, pointerend; 3545 | 3546 | // Quick check for common null cases. 3547 | if ( !text1 || 3548 | !text2 || 3549 | text1.charAt( text1.length - 1 ) !== text2.charAt( text2.length - 1 ) ) { 3550 | return 0; 3551 | } 3552 | 3553 | // Binary search. 3554 | // Performance analysis: https://neil.fraser.name/news/2007/10/09/ 3555 | pointermin = 0; 3556 | pointermax = Math.min( text1.length, text2.length ); 3557 | pointermid = pointermax; 3558 | pointerend = 0; 3559 | while ( pointermin < pointermid ) { 3560 | if ( text1.substring( text1.length - pointermid, text1.length - pointerend ) === 3561 | text2.substring( text2.length - pointermid, text2.length - pointerend ) ) { 3562 | pointermin = pointermid; 3563 | pointerend = pointermin; 3564 | } else { 3565 | pointermax = pointermid; 3566 | } 3567 | pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); 3568 | } 3569 | return pointermid; 3570 | }; 3571 | 3572 | /** 3573 | * Find the differences between two texts. Assumes that the texts do not 3574 | * have any common prefix or suffix. 3575 | * @param {string} text1 Old string to be diffed. 3576 | * @param {string} text2 New string to be diffed. 3577 | * @param {boolean} checklines Speedup flag. If false, then don't run a 3578 | * line-level diff first to identify the changed areas. 3579 | * If true, then run a faster, slightly less optimal diff. 3580 | * @param {number} deadline Time when the diff should be complete by. 3581 | * @return {!Array.} Array of diff tuples. 3582 | * @private 3583 | */ 3584 | DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) { 3585 | var diffs, longtext, shorttext, i, hm, 3586 | text1A, text2A, text1B, text2B, 3587 | midCommon, diffsA, diffsB; 3588 | 3589 | if ( !text1 ) { 3590 | 3591 | // Just add some text (speedup). 3592 | return [ 3593 | [ DIFF_INSERT, text2 ] 3594 | ]; 3595 | } 3596 | 3597 | if ( !text2 ) { 3598 | 3599 | // Just delete some text (speedup). 3600 | return [ 3601 | [ DIFF_DELETE, text1 ] 3602 | ]; 3603 | } 3604 | 3605 | longtext = text1.length > text2.length ? text1 : text2; 3606 | shorttext = text1.length > text2.length ? text2 : text1; 3607 | i = longtext.indexOf( shorttext ); 3608 | if ( i !== -1 ) { 3609 | 3610 | // Shorter text is inside the longer text (speedup). 3611 | diffs = [ 3612 | [ DIFF_INSERT, longtext.substring( 0, i ) ], 3613 | [ DIFF_EQUAL, shorttext ], 3614 | [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ] 3615 | ]; 3616 | 3617 | // Swap insertions for deletions if diff is reversed. 3618 | if ( text1.length > text2.length ) { 3619 | diffs[ 0 ][ 0 ] = diffs[ 2 ][ 0 ] = DIFF_DELETE; 3620 | } 3621 | return diffs; 3622 | } 3623 | 3624 | if ( shorttext.length === 1 ) { 3625 | 3626 | // Single character string. 3627 | // After the previous speedup, the character can't be an equality. 3628 | return [ 3629 | [ DIFF_DELETE, text1 ], 3630 | [ DIFF_INSERT, text2 ] 3631 | ]; 3632 | } 3633 | 3634 | // Check to see if the problem can be split in two. 3635 | hm = this.diffHalfMatch( text1, text2 ); 3636 | if ( hm ) { 3637 | 3638 | // A half-match was found, sort out the return data. 3639 | text1A = hm[ 0 ]; 3640 | text1B = hm[ 1 ]; 3641 | text2A = hm[ 2 ]; 3642 | text2B = hm[ 3 ]; 3643 | midCommon = hm[ 4 ]; 3644 | 3645 | // Send both pairs off for separate processing. 3646 | diffsA = this.DiffMain( text1A, text2A, checklines, deadline ); 3647 | diffsB = this.DiffMain( text1B, text2B, checklines, deadline ); 3648 | 3649 | // Merge the results. 3650 | return diffsA.concat( [ 3651 | [ DIFF_EQUAL, midCommon ] 3652 | ], diffsB ); 3653 | } 3654 | 3655 | if ( checklines && text1.length > 100 && text2.length > 100 ) { 3656 | return this.diffLineMode( text1, text2, deadline ); 3657 | } 3658 | 3659 | return this.diffBisect( text1, text2, deadline ); 3660 | }; 3661 | 3662 | /** 3663 | * Do the two texts share a substring which is at least half the length of the 3664 | * longer text? 3665 | * This speedup can produce non-minimal diffs. 3666 | * @param {string} text1 First string. 3667 | * @param {string} text2 Second string. 3668 | * @return {Array.} Five element Array, containing the prefix of 3669 | * text1, the suffix of text1, the prefix of text2, the suffix of 3670 | * text2 and the common middle. Or null if there was no match. 3671 | * @private 3672 | */ 3673 | DiffMatchPatch.prototype.diffHalfMatch = function( text1, text2 ) { 3674 | var longtext, shorttext, dmp, 3675 | text1A, text2B, text2A, text1B, midCommon, 3676 | hm1, hm2, hm; 3677 | 3678 | longtext = text1.length > text2.length ? text1 : text2; 3679 | shorttext = text1.length > text2.length ? text2 : text1; 3680 | if ( longtext.length < 4 || shorttext.length * 2 < longtext.length ) { 3681 | return null; // Pointless. 3682 | } 3683 | dmp = this; // 'this' becomes 'window' in a closure. 3684 | 3685 | /** 3686 | * Does a substring of shorttext exist within longtext such that the substring 3687 | * is at least half the length of longtext? 3688 | * Closure, but does not reference any external variables. 3689 | * @param {string} longtext Longer string. 3690 | * @param {string} shorttext Shorter string. 3691 | * @param {number} i Start index of quarter length substring within longtext. 3692 | * @return {Array.} Five element Array, containing the prefix of 3693 | * longtext, the suffix of longtext, the prefix of shorttext, the suffix 3694 | * of shorttext and the common middle. Or null if there was no match. 3695 | * @private 3696 | */ 3697 | function diffHalfMatchI( longtext, shorttext, i ) { 3698 | var seed, j, bestCommon, prefixLength, suffixLength, 3699 | bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; 3700 | 3701 | // Start with a 1/4 length substring at position i as a seed. 3702 | seed = longtext.substring( i, i + Math.floor( longtext.length / 4 ) ); 3703 | j = -1; 3704 | bestCommon = ""; 3705 | while ( ( j = shorttext.indexOf( seed, j + 1 ) ) !== -1 ) { 3706 | prefixLength = dmp.diffCommonPrefix( longtext.substring( i ), 3707 | shorttext.substring( j ) ); 3708 | suffixLength = dmp.diffCommonSuffix( longtext.substring( 0, i ), 3709 | shorttext.substring( 0, j ) ); 3710 | if ( bestCommon.length < suffixLength + prefixLength ) { 3711 | bestCommon = shorttext.substring( j - suffixLength, j ) + 3712 | shorttext.substring( j, j + prefixLength ); 3713 | bestLongtextA = longtext.substring( 0, i - suffixLength ); 3714 | bestLongtextB = longtext.substring( i + prefixLength ); 3715 | bestShorttextA = shorttext.substring( 0, j - suffixLength ); 3716 | bestShorttextB = shorttext.substring( j + prefixLength ); 3717 | } 3718 | } 3719 | if ( bestCommon.length * 2 >= longtext.length ) { 3720 | return [ bestLongtextA, bestLongtextB, 3721 | bestShorttextA, bestShorttextB, bestCommon 3722 | ]; 3723 | } else { 3724 | return null; 3725 | } 3726 | } 3727 | 3728 | // First check if the second quarter is the seed for a half-match. 3729 | hm1 = diffHalfMatchI( longtext, shorttext, 3730 | Math.ceil( longtext.length / 4 ) ); 3731 | 3732 | // Check again based on the third quarter. 3733 | hm2 = diffHalfMatchI( longtext, shorttext, 3734 | Math.ceil( longtext.length / 2 ) ); 3735 | if ( !hm1 && !hm2 ) { 3736 | return null; 3737 | } else if ( !hm2 ) { 3738 | hm = hm1; 3739 | } else if ( !hm1 ) { 3740 | hm = hm2; 3741 | } else { 3742 | 3743 | // Both matched. Select the longest. 3744 | hm = hm1[ 4 ].length > hm2[ 4 ].length ? hm1 : hm2; 3745 | } 3746 | 3747 | // A half-match was found, sort out the return data. 3748 | text1A, text1B, text2A, text2B; 3749 | if ( text1.length > text2.length ) { 3750 | text1A = hm[ 0 ]; 3751 | text1B = hm[ 1 ]; 3752 | text2A = hm[ 2 ]; 3753 | text2B = hm[ 3 ]; 3754 | } else { 3755 | text2A = hm[ 0 ]; 3756 | text2B = hm[ 1 ]; 3757 | text1A = hm[ 2 ]; 3758 | text1B = hm[ 3 ]; 3759 | } 3760 | midCommon = hm[ 4 ]; 3761 | return [ text1A, text1B, text2A, text2B, midCommon ]; 3762 | }; 3763 | 3764 | /** 3765 | * Do a quick line-level diff on both strings, then rediff the parts for 3766 | * greater accuracy. 3767 | * This speedup can produce non-minimal diffs. 3768 | * @param {string} text1 Old string to be diffed. 3769 | * @param {string} text2 New string to be diffed. 3770 | * @param {number} deadline Time when the diff should be complete by. 3771 | * @return {!Array.} Array of diff tuples. 3772 | * @private 3773 | */ 3774 | DiffMatchPatch.prototype.diffLineMode = function( text1, text2, deadline ) { 3775 | var a, diffs, linearray, pointer, countInsert, 3776 | countDelete, textInsert, textDelete, j; 3777 | 3778 | // Scan the text on a line-by-line basis first. 3779 | a = this.diffLinesToChars( text1, text2 ); 3780 | text1 = a.chars1; 3781 | text2 = a.chars2; 3782 | linearray = a.lineArray; 3783 | 3784 | diffs = this.DiffMain( text1, text2, false, deadline ); 3785 | 3786 | // Convert the diff back to original text. 3787 | this.diffCharsToLines( diffs, linearray ); 3788 | 3789 | // Eliminate freak matches (e.g. blank lines) 3790 | this.diffCleanupSemantic( diffs ); 3791 | 3792 | // Rediff any replacement blocks, this time character-by-character. 3793 | // Add a dummy entry at the end. 3794 | diffs.push( [ DIFF_EQUAL, "" ] ); 3795 | pointer = 0; 3796 | countDelete = 0; 3797 | countInsert = 0; 3798 | textDelete = ""; 3799 | textInsert = ""; 3800 | while ( pointer < diffs.length ) { 3801 | switch ( diffs[ pointer ][ 0 ] ) { 3802 | case DIFF_INSERT: 3803 | countInsert++; 3804 | textInsert += diffs[ pointer ][ 1 ]; 3805 | break; 3806 | case DIFF_DELETE: 3807 | countDelete++; 3808 | textDelete += diffs[ pointer ][ 1 ]; 3809 | break; 3810 | case DIFF_EQUAL: 3811 | 3812 | // Upon reaching an equality, check for prior redundancies. 3813 | if ( countDelete >= 1 && countInsert >= 1 ) { 3814 | 3815 | // Delete the offending records and add the merged ones. 3816 | diffs.splice( pointer - countDelete - countInsert, 3817 | countDelete + countInsert ); 3818 | pointer = pointer - countDelete - countInsert; 3819 | a = this.DiffMain( textDelete, textInsert, false, deadline ); 3820 | for ( j = a.length - 1; j >= 0; j-- ) { 3821 | diffs.splice( pointer, 0, a[ j ] ); 3822 | } 3823 | pointer = pointer + a.length; 3824 | } 3825 | countInsert = 0; 3826 | countDelete = 0; 3827 | textDelete = ""; 3828 | textInsert = ""; 3829 | break; 3830 | } 3831 | pointer++; 3832 | } 3833 | diffs.pop(); // Remove the dummy entry at the end. 3834 | 3835 | return diffs; 3836 | }; 3837 | 3838 | /** 3839 | * Find the 'middle snake' of a diff, split the problem in two 3840 | * and return the recursively constructed diff. 3841 | * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. 3842 | * @param {string} text1 Old string to be diffed. 3843 | * @param {string} text2 New string to be diffed. 3844 | * @param {number} deadline Time at which to bail if not yet complete. 3845 | * @return {!Array.} Array of diff tuples. 3846 | * @private 3847 | */ 3848 | DiffMatchPatch.prototype.diffBisect = function( text1, text2, deadline ) { 3849 | var text1Length, text2Length, maxD, vOffset, vLength, 3850 | v1, v2, x, delta, front, k1start, k1end, k2start, 3851 | k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; 3852 | 3853 | // Cache the text lengths to prevent multiple calls. 3854 | text1Length = text1.length; 3855 | text2Length = text2.length; 3856 | maxD = Math.ceil( ( text1Length + text2Length ) / 2 ); 3857 | vOffset = maxD; 3858 | vLength = 2 * maxD; 3859 | v1 = new Array( vLength ); 3860 | v2 = new Array( vLength ); 3861 | 3862 | // Setting all elements to -1 is faster in Chrome & Firefox than mixing 3863 | // integers and undefined. 3864 | for ( x = 0; x < vLength; x++ ) { 3865 | v1[ x ] = -1; 3866 | v2[ x ] = -1; 3867 | } 3868 | v1[ vOffset + 1 ] = 0; 3869 | v2[ vOffset + 1 ] = 0; 3870 | delta = text1Length - text2Length; 3871 | 3872 | // If the total number of characters is odd, then the front path will collide 3873 | // with the reverse path. 3874 | front = ( delta % 2 !== 0 ); 3875 | 3876 | // Offsets for start and end of k loop. 3877 | // Prevents mapping of space beyond the grid. 3878 | k1start = 0; 3879 | k1end = 0; 3880 | k2start = 0; 3881 | k2end = 0; 3882 | for ( d = 0; d < maxD; d++ ) { 3883 | 3884 | // Bail out if deadline is reached. 3885 | if ( ( new Date() ).getTime() > deadline ) { 3886 | break; 3887 | } 3888 | 3889 | // Walk the front path one step. 3890 | for ( k1 = -d + k1start; k1 <= d - k1end; k1 += 2 ) { 3891 | k1Offset = vOffset + k1; 3892 | if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) { 3893 | x1 = v1[ k1Offset + 1 ]; 3894 | } else { 3895 | x1 = v1[ k1Offset - 1 ] + 1; 3896 | } 3897 | y1 = x1 - k1; 3898 | while ( x1 < text1Length && y1 < text2Length && 3899 | text1.charAt( x1 ) === text2.charAt( y1 ) ) { 3900 | x1++; 3901 | y1++; 3902 | } 3903 | v1[ k1Offset ] = x1; 3904 | if ( x1 > text1Length ) { 3905 | 3906 | // Ran off the right of the graph. 3907 | k1end += 2; 3908 | } else if ( y1 > text2Length ) { 3909 | 3910 | // Ran off the bottom of the graph. 3911 | k1start += 2; 3912 | } else if ( front ) { 3913 | k2Offset = vOffset + delta - k1; 3914 | if ( k2Offset >= 0 && k2Offset < vLength && v2[ k2Offset ] !== -1 ) { 3915 | 3916 | // Mirror x2 onto top-left coordinate system. 3917 | x2 = text1Length - v2[ k2Offset ]; 3918 | if ( x1 >= x2 ) { 3919 | 3920 | // Overlap detected. 3921 | return this.diffBisectSplit( text1, text2, x1, y1, deadline ); 3922 | } 3923 | } 3924 | } 3925 | } 3926 | 3927 | // Walk the reverse path one step. 3928 | for ( k2 = -d + k2start; k2 <= d - k2end; k2 += 2 ) { 3929 | k2Offset = vOffset + k2; 3930 | if ( k2 === -d || ( k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) { 3931 | x2 = v2[ k2Offset + 1 ]; 3932 | } else { 3933 | x2 = v2[ k2Offset - 1 ] + 1; 3934 | } 3935 | y2 = x2 - k2; 3936 | while ( x2 < text1Length && y2 < text2Length && 3937 | text1.charAt( text1Length - x2 - 1 ) === 3938 | text2.charAt( text2Length - y2 - 1 ) ) { 3939 | x2++; 3940 | y2++; 3941 | } 3942 | v2[ k2Offset ] = x2; 3943 | if ( x2 > text1Length ) { 3944 | 3945 | // Ran off the left of the graph. 3946 | k2end += 2; 3947 | } else if ( y2 > text2Length ) { 3948 | 3949 | // Ran off the top of the graph. 3950 | k2start += 2; 3951 | } else if ( !front ) { 3952 | k1Offset = vOffset + delta - k2; 3953 | if ( k1Offset >= 0 && k1Offset < vLength && v1[ k1Offset ] !== -1 ) { 3954 | x1 = v1[ k1Offset ]; 3955 | y1 = vOffset + x1 - k1Offset; 3956 | 3957 | // Mirror x2 onto top-left coordinate system. 3958 | x2 = text1Length - x2; 3959 | if ( x1 >= x2 ) { 3960 | 3961 | // Overlap detected. 3962 | return this.diffBisectSplit( text1, text2, x1, y1, deadline ); 3963 | } 3964 | } 3965 | } 3966 | } 3967 | } 3968 | 3969 | // Diff took too long and hit the deadline or 3970 | // number of diffs equals number of characters, no commonality at all. 3971 | return [ 3972 | [ DIFF_DELETE, text1 ], 3973 | [ DIFF_INSERT, text2 ] 3974 | ]; 3975 | }; 3976 | 3977 | /** 3978 | * Given the location of the 'middle snake', split the diff in two parts 3979 | * and recurse. 3980 | * @param {string} text1 Old string to be diffed. 3981 | * @param {string} text2 New string to be diffed. 3982 | * @param {number} x Index of split point in text1. 3983 | * @param {number} y Index of split point in text2. 3984 | * @param {number} deadline Time at which to bail if not yet complete. 3985 | * @return {!Array.} Array of diff tuples. 3986 | * @private 3987 | */ 3988 | DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) { 3989 | var text1a, text1b, text2a, text2b, diffs, diffsb; 3990 | text1a = text1.substring( 0, x ); 3991 | text2a = text2.substring( 0, y ); 3992 | text1b = text1.substring( x ); 3993 | text2b = text2.substring( y ); 3994 | 3995 | // Compute both diffs serially. 3996 | diffs = this.DiffMain( text1a, text2a, false, deadline ); 3997 | diffsb = this.DiffMain( text1b, text2b, false, deadline ); 3998 | 3999 | return diffs.concat( diffsb ); 4000 | }; 4001 | 4002 | /** 4003 | * Reduce the number of edits by eliminating semantically trivial equalities. 4004 | * @param {!Array.} diffs Array of diff tuples. 4005 | */ 4006 | DiffMatchPatch.prototype.diffCleanupSemantic = function( diffs ) { 4007 | var changes, equalities, equalitiesLength, lastequality, 4008 | pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1, 4009 | lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2; 4010 | changes = false; 4011 | equalities = []; // Stack of indices where equalities are found. 4012 | equalitiesLength = 0; // Keeping our own length var is faster in JS. 4013 | /** @type {?string} */ 4014 | lastequality = null; 4015 | 4016 | // Always equal to diffs[equalities[equalitiesLength - 1]][1] 4017 | pointer = 0; // Index of current position. 4018 | 4019 | // Number of characters that changed prior to the equality. 4020 | lengthInsertions1 = 0; 4021 | lengthDeletions1 = 0; 4022 | 4023 | // Number of characters that changed after the equality. 4024 | lengthInsertions2 = 0; 4025 | lengthDeletions2 = 0; 4026 | while ( pointer < diffs.length ) { 4027 | if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found. 4028 | equalities[ equalitiesLength++ ] = pointer; 4029 | lengthInsertions1 = lengthInsertions2; 4030 | lengthDeletions1 = lengthDeletions2; 4031 | lengthInsertions2 = 0; 4032 | lengthDeletions2 = 0; 4033 | lastequality = diffs[ pointer ][ 1 ]; 4034 | } else { // An insertion or deletion. 4035 | if ( diffs[ pointer ][ 0 ] === DIFF_INSERT ) { 4036 | lengthInsertions2 += diffs[ pointer ][ 1 ].length; 4037 | } else { 4038 | lengthDeletions2 += diffs[ pointer ][ 1 ].length; 4039 | } 4040 | 4041 | // Eliminate an equality that is smaller or equal to the edits on both 4042 | // sides of it. 4043 | if ( lastequality && ( lastequality.length <= 4044 | Math.max( lengthInsertions1, lengthDeletions1 ) ) && 4045 | ( lastequality.length <= Math.max( lengthInsertions2, 4046 | lengthDeletions2 ) ) ) { 4047 | 4048 | // Duplicate record. 4049 | diffs.splice( 4050 | equalities[ equalitiesLength - 1 ], 4051 | 0, 4052 | [ DIFF_DELETE, lastequality ] 4053 | ); 4054 | 4055 | // Change second copy to insert. 4056 | diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; 4057 | 4058 | // Throw away the equality we just deleted. 4059 | equalitiesLength--; 4060 | 4061 | // Throw away the previous equality (it needs to be reevaluated). 4062 | equalitiesLength--; 4063 | pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; 4064 | 4065 | // Reset the counters. 4066 | lengthInsertions1 = 0; 4067 | lengthDeletions1 = 0; 4068 | lengthInsertions2 = 0; 4069 | lengthDeletions2 = 0; 4070 | lastequality = null; 4071 | changes = true; 4072 | } 4073 | } 4074 | pointer++; 4075 | } 4076 | 4077 | // Normalize the diff. 4078 | if ( changes ) { 4079 | this.diffCleanupMerge( diffs ); 4080 | } 4081 | 4082 | // Find any overlaps between deletions and insertions. 4083 | // e.g: abcxxxxxxdef 4084 | // -> abcxxxdef 4085 | // e.g: xxxabcdefxxx 4086 | // -> defxxxabc 4087 | // Only extract an overlap if it is as big as the edit ahead or behind it. 4088 | pointer = 1; 4089 | while ( pointer < diffs.length ) { 4090 | if ( diffs[ pointer - 1 ][ 0 ] === DIFF_DELETE && 4091 | diffs[ pointer ][ 0 ] === DIFF_INSERT ) { 4092 | deletion = diffs[ pointer - 1 ][ 1 ]; 4093 | insertion = diffs[ pointer ][ 1 ]; 4094 | overlapLength1 = this.diffCommonOverlap( deletion, insertion ); 4095 | overlapLength2 = this.diffCommonOverlap( insertion, deletion ); 4096 | if ( overlapLength1 >= overlapLength2 ) { 4097 | if ( overlapLength1 >= deletion.length / 2 || 4098 | overlapLength1 >= insertion.length / 2 ) { 4099 | 4100 | // Overlap found. Insert an equality and trim the surrounding edits. 4101 | diffs.splice( 4102 | pointer, 4103 | 0, 4104 | [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] 4105 | ); 4106 | diffs[ pointer - 1 ][ 1 ] = 4107 | deletion.substring( 0, deletion.length - overlapLength1 ); 4108 | diffs[ pointer + 1 ][ 1 ] = insertion.substring( overlapLength1 ); 4109 | pointer++; 4110 | } 4111 | } else { 4112 | if ( overlapLength2 >= deletion.length / 2 || 4113 | overlapLength2 >= insertion.length / 2 ) { 4114 | 4115 | // Reverse overlap found. 4116 | // Insert an equality and swap and trim the surrounding edits. 4117 | diffs.splice( 4118 | pointer, 4119 | 0, 4120 | [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] 4121 | ); 4122 | 4123 | diffs[ pointer - 1 ][ 0 ] = DIFF_INSERT; 4124 | diffs[ pointer - 1 ][ 1 ] = 4125 | insertion.substring( 0, insertion.length - overlapLength2 ); 4126 | diffs[ pointer + 1 ][ 0 ] = DIFF_DELETE; 4127 | diffs[ pointer + 1 ][ 1 ] = 4128 | deletion.substring( overlapLength2 ); 4129 | pointer++; 4130 | } 4131 | } 4132 | pointer++; 4133 | } 4134 | pointer++; 4135 | } 4136 | }; 4137 | 4138 | /** 4139 | * Determine if the suffix of one string is the prefix of another. 4140 | * @param {string} text1 First string. 4141 | * @param {string} text2 Second string. 4142 | * @return {number} The number of characters common to the end of the first 4143 | * string and the start of the second string. 4144 | * @private 4145 | */ 4146 | DiffMatchPatch.prototype.diffCommonOverlap = function( text1, text2 ) { 4147 | var text1Length, text2Length, textLength, 4148 | best, length, pattern, found; 4149 | 4150 | // Cache the text lengths to prevent multiple calls. 4151 | text1Length = text1.length; 4152 | text2Length = text2.length; 4153 | 4154 | // Eliminate the null case. 4155 | if ( text1Length === 0 || text2Length === 0 ) { 4156 | return 0; 4157 | } 4158 | 4159 | // Truncate the longer string. 4160 | if ( text1Length > text2Length ) { 4161 | text1 = text1.substring( text1Length - text2Length ); 4162 | } else if ( text1Length < text2Length ) { 4163 | text2 = text2.substring( 0, text1Length ); 4164 | } 4165 | textLength = Math.min( text1Length, text2Length ); 4166 | 4167 | // Quick check for the worst case. 4168 | if ( text1 === text2 ) { 4169 | return textLength; 4170 | } 4171 | 4172 | // Start by looking for a single character match 4173 | // and increase length until no match is found. 4174 | // Performance analysis: https://neil.fraser.name/news/2010/11/04/ 4175 | best = 0; 4176 | length = 1; 4177 | while ( true ) { 4178 | pattern = text1.substring( textLength - length ); 4179 | found = text2.indexOf( pattern ); 4180 | if ( found === -1 ) { 4181 | return best; 4182 | } 4183 | length += found; 4184 | if ( found === 0 || text1.substring( textLength - length ) === 4185 | text2.substring( 0, length ) ) { 4186 | best = length; 4187 | length++; 4188 | } 4189 | } 4190 | }; 4191 | 4192 | /** 4193 | * Split two texts into an array of strings. Reduce the texts to a string of 4194 | * hashes where each Unicode character represents one line. 4195 | * @param {string} text1 First string. 4196 | * @param {string} text2 Second string. 4197 | * @return {{chars1: string, chars2: string, lineArray: !Array.}} 4198 | * An object containing the encoded text1, the encoded text2 and 4199 | * the array of unique strings. 4200 | * The zeroth element of the array of unique strings is intentionally blank. 4201 | * @private 4202 | */ 4203 | DiffMatchPatch.prototype.diffLinesToChars = function( text1, text2 ) { 4204 | var lineArray, lineHash, chars1, chars2; 4205 | lineArray = []; // E.g. lineArray[4] === 'Hello\n' 4206 | lineHash = {}; // E.g. lineHash['Hello\n'] === 4 4207 | 4208 | // '\x00' is a valid character, but various debuggers don't like it. 4209 | // So we'll insert a junk entry to avoid generating a null character. 4210 | lineArray[ 0 ] = ""; 4211 | 4212 | /** 4213 | * Split a text into an array of strings. Reduce the texts to a string of 4214 | * hashes where each Unicode character represents one line. 4215 | * Modifies linearray and linehash through being a closure. 4216 | * @param {string} text String to encode. 4217 | * @return {string} Encoded string. 4218 | * @private 4219 | */ 4220 | function diffLinesToCharsMunge( text ) { 4221 | var chars, lineStart, lineEnd, lineArrayLength, line; 4222 | chars = ""; 4223 | 4224 | // Walk the text, pulling out a substring for each line. 4225 | // text.split('\n') would would temporarily double our memory footprint. 4226 | // Modifying text would create many large strings to garbage collect. 4227 | lineStart = 0; 4228 | lineEnd = -1; 4229 | 4230 | // Keeping our own length variable is faster than looking it up. 4231 | lineArrayLength = lineArray.length; 4232 | while ( lineEnd < text.length - 1 ) { 4233 | lineEnd = text.indexOf( "\n", lineStart ); 4234 | if ( lineEnd === -1 ) { 4235 | lineEnd = text.length - 1; 4236 | } 4237 | line = text.substring( lineStart, lineEnd + 1 ); 4238 | lineStart = lineEnd + 1; 4239 | 4240 | if ( lineHash.hasOwnProperty ? lineHash.hasOwnProperty( line ) : 4241 | ( lineHash[ line ] !== undefined ) ) { 4242 | chars += String.fromCharCode( lineHash[ line ] ); 4243 | } else { 4244 | chars += String.fromCharCode( lineArrayLength ); 4245 | lineHash[ line ] = lineArrayLength; 4246 | lineArray[ lineArrayLength++ ] = line; 4247 | } 4248 | } 4249 | return chars; 4250 | } 4251 | 4252 | chars1 = diffLinesToCharsMunge( text1 ); 4253 | chars2 = diffLinesToCharsMunge( text2 ); 4254 | return { 4255 | chars1: chars1, 4256 | chars2: chars2, 4257 | lineArray: lineArray 4258 | }; 4259 | }; 4260 | 4261 | /** 4262 | * Rehydrate the text in a diff from a string of line hashes to real lines of 4263 | * text. 4264 | * @param {!Array.} diffs Array of diff tuples. 4265 | * @param {!Array.} lineArray Array of unique strings. 4266 | * @private 4267 | */ 4268 | DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) { 4269 | var x, chars, text, y; 4270 | for ( x = 0; x < diffs.length; x++ ) { 4271 | chars = diffs[ x ][ 1 ]; 4272 | text = []; 4273 | for ( y = 0; y < chars.length; y++ ) { 4274 | text[ y ] = lineArray[ chars.charCodeAt( y ) ]; 4275 | } 4276 | diffs[ x ][ 1 ] = text.join( "" ); 4277 | } 4278 | }; 4279 | 4280 | /** 4281 | * Reorder and merge like edit sections. Merge equalities. 4282 | * Any edit section can move as long as it doesn't cross an equality. 4283 | * @param {!Array.} diffs Array of diff tuples. 4284 | */ 4285 | DiffMatchPatch.prototype.diffCleanupMerge = function( diffs ) { 4286 | var pointer, countDelete, countInsert, textInsert, textDelete, 4287 | commonlength, changes, diffPointer, position; 4288 | diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end. 4289 | pointer = 0; 4290 | countDelete = 0; 4291 | countInsert = 0; 4292 | textDelete = ""; 4293 | textInsert = ""; 4294 | commonlength; 4295 | while ( pointer < diffs.length ) { 4296 | switch ( diffs[ pointer ][ 0 ] ) { 4297 | case DIFF_INSERT: 4298 | countInsert++; 4299 | textInsert += diffs[ pointer ][ 1 ]; 4300 | pointer++; 4301 | break; 4302 | case DIFF_DELETE: 4303 | countDelete++; 4304 | textDelete += diffs[ pointer ][ 1 ]; 4305 | pointer++; 4306 | break; 4307 | case DIFF_EQUAL: 4308 | 4309 | // Upon reaching an equality, check for prior redundancies. 4310 | if ( countDelete + countInsert > 1 ) { 4311 | if ( countDelete !== 0 && countInsert !== 0 ) { 4312 | 4313 | // Factor out any common prefixes. 4314 | commonlength = this.diffCommonPrefix( textInsert, textDelete ); 4315 | if ( commonlength !== 0 ) { 4316 | if ( ( pointer - countDelete - countInsert ) > 0 && 4317 | diffs[ pointer - countDelete - countInsert - 1 ][ 0 ] === 4318 | DIFF_EQUAL ) { 4319 | diffs[ pointer - countDelete - countInsert - 1 ][ 1 ] += 4320 | textInsert.substring( 0, commonlength ); 4321 | } else { 4322 | diffs.splice( 0, 0, [ DIFF_EQUAL, 4323 | textInsert.substring( 0, commonlength ) 4324 | ] ); 4325 | pointer++; 4326 | } 4327 | textInsert = textInsert.substring( commonlength ); 4328 | textDelete = textDelete.substring( commonlength ); 4329 | } 4330 | 4331 | // Factor out any common suffixies. 4332 | commonlength = this.diffCommonSuffix( textInsert, textDelete ); 4333 | if ( commonlength !== 0 ) { 4334 | diffs[ pointer ][ 1 ] = textInsert.substring( textInsert.length - 4335 | commonlength ) + diffs[ pointer ][ 1 ]; 4336 | textInsert = textInsert.substring( 0, textInsert.length - 4337 | commonlength ); 4338 | textDelete = textDelete.substring( 0, textDelete.length - 4339 | commonlength ); 4340 | } 4341 | } 4342 | 4343 | // Delete the offending records and add the merged ones. 4344 | if ( countDelete === 0 ) { 4345 | diffs.splice( pointer - countInsert, 4346 | countDelete + countInsert, [ DIFF_INSERT, textInsert ] ); 4347 | } else if ( countInsert === 0 ) { 4348 | diffs.splice( pointer - countDelete, 4349 | countDelete + countInsert, [ DIFF_DELETE, textDelete ] ); 4350 | } else { 4351 | diffs.splice( 4352 | pointer - countDelete - countInsert, 4353 | countDelete + countInsert, 4354 | [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] 4355 | ); 4356 | } 4357 | pointer = pointer - countDelete - countInsert + 4358 | ( countDelete ? 1 : 0 ) + ( countInsert ? 1 : 0 ) + 1; 4359 | } else if ( pointer !== 0 && diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL ) { 4360 | 4361 | // Merge this equality with the previous one. 4362 | diffs[ pointer - 1 ][ 1 ] += diffs[ pointer ][ 1 ]; 4363 | diffs.splice( pointer, 1 ); 4364 | } else { 4365 | pointer++; 4366 | } 4367 | countInsert = 0; 4368 | countDelete = 0; 4369 | textDelete = ""; 4370 | textInsert = ""; 4371 | break; 4372 | } 4373 | } 4374 | if ( diffs[ diffs.length - 1 ][ 1 ] === "" ) { 4375 | diffs.pop(); // Remove the dummy entry at the end. 4376 | } 4377 | 4378 | // Second pass: look for single edits surrounded on both sides by equalities 4379 | // which can be shifted sideways to eliminate an equality. 4380 | // e.g: ABAC -> ABAC 4381 | changes = false; 4382 | pointer = 1; 4383 | 4384 | // Intentionally ignore the first and last element (don't need checking). 4385 | while ( pointer < diffs.length - 1 ) { 4386 | if ( diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL && 4387 | diffs[ pointer + 1 ][ 0 ] === DIFF_EQUAL ) { 4388 | 4389 | diffPointer = diffs[ pointer ][ 1 ]; 4390 | position = diffPointer.substring( 4391 | diffPointer.length - diffs[ pointer - 1 ][ 1 ].length 4392 | ); 4393 | 4394 | // This is a single edit surrounded by equalities. 4395 | if ( position === diffs[ pointer - 1 ][ 1 ] ) { 4396 | 4397 | // Shift the edit over the previous equality. 4398 | diffs[ pointer ][ 1 ] = diffs[ pointer - 1 ][ 1 ] + 4399 | diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer ][ 1 ].length - 4400 | diffs[ pointer - 1 ][ 1 ].length ); 4401 | diffs[ pointer + 1 ][ 1 ] = 4402 | diffs[ pointer - 1 ][ 1 ] + diffs[ pointer + 1 ][ 1 ]; 4403 | diffs.splice( pointer - 1, 1 ); 4404 | changes = true; 4405 | } else if ( diffPointer.substring( 0, diffs[ pointer + 1 ][ 1 ].length ) === 4406 | diffs[ pointer + 1 ][ 1 ] ) { 4407 | 4408 | // Shift the edit over the next equality. 4409 | diffs[ pointer - 1 ][ 1 ] += diffs[ pointer + 1 ][ 1 ]; 4410 | diffs[ pointer ][ 1 ] = 4411 | diffs[ pointer ][ 1 ].substring( diffs[ pointer + 1 ][ 1 ].length ) + 4412 | diffs[ pointer + 1 ][ 1 ]; 4413 | diffs.splice( pointer + 1, 1 ); 4414 | changes = true; 4415 | } 4416 | } 4417 | pointer++; 4418 | } 4419 | 4420 | // If shifts were made, the diff needs reordering and another shift sweep. 4421 | if ( changes ) { 4422 | this.diffCleanupMerge( diffs ); 4423 | } 4424 | }; 4425 | 4426 | return function( o, n ) { 4427 | var diff, output, text; 4428 | diff = new DiffMatchPatch(); 4429 | output = diff.DiffMain( o, n ); 4430 | diff.diffCleanupEfficiency( output ); 4431 | text = diff.diffPrettyHtml( output ); 4432 | 4433 | return text; 4434 | }; 4435 | }() ); 4436 | 4437 | }() ); 4438 | --------------------------------------------------------------------------------