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 | [](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
";
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 | "
Source:
" +
3163 | escapeText( details.source ) + "
" +
3164 | "
";
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 |
--------------------------------------------------------------------------------