├── LICENSE
├── perf
└── perf.html
├── readme.md
├── src
├── XSSB-min.js
└── XSSBuster.js
└── test
├── QUnit
├── qunit-git.css
└── qunit-git.js
├── iframe.html
├── styles.css
├── test-suite.html
└── tests.js
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/perf/perf.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # XSSBuster
2 | **XSSB** is a proactive DOM sanitizer, defending against client-side injection attacks.
3 |
4 | # The Problem:
5 | With every unaudited third-party JS library you include into your DOM, the risk of accidental DOM-based cross-site-scripting issues rises linearly. It being for advertisement, web analytics, social widgets, et al., all sorts of third-party code is susceptible to injection attacks.
6 |
7 | Examples of this are:
8 | * [http://www.troyhunt.com/2015/07/how-i-got-xssd-by-my-ad-network.html](http://www.troyhunt.com/2015/07/how-i-got-xssd-by-my-ad-network.html)
9 | * [https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/](https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval)
10 | * [http://www.fuzzysecurity.com/tutorials/14.html](http://www.fuzzysecurity.com/tutorials/14.html)
11 | * [http://blog.mindedsecurity.com/2011/04/god-save-omniture-quine.html](http://blog.mindedsecurity.com/2011/04/god-save-omniture-quine.html)
12 | * [https://hackerone.com/reports/125386#activity-888336](https://hackerone.com/reports/125386#activity-888336)
13 |
14 | # The Solution:
15 | **XSSB** mainly utilizes [taint checking](https://en.wikipedia.org/wiki/Taint_checking) to guard against accidental mistakes and poor security practices commonly employed by JS libraries that may lead to DOM-based XSS vulnerabilities.
16 |
17 | A rough idea of how it works is: Data from untrusted input sources such as `window.name`, `location.hash`, `document.referrer`, `window.onmessage`, et al. are tainted and are constantly tracked for any changes. **XSSB** then overrides security-sensitive functions and DOM APIs (e.g., `eval()`, `document.write()`, `Element.prototype.appendChild()`, etc.) to enforce taint checking and prevent insecure operations such as `eval(location.hash.slice(1))`, `document.write(window.name)`, and the like.
18 |
19 | So, basically, **XSSB** offers you the freedom to deploy any given third-party code into your DOM while at the same time covering your DOM's back!
20 |
21 | # Usage Instructions:
22 | Simply place the script element of _XSSBuster.js_ right before any other third-party scripts you include into your webpage(s), typically at the very top of the head tag:
23 | ```html
24 |
25 | Example
26 |
27 |
28 |
29 | ```
30 | #### Notes:
31 | * Make sure to host _XSSBuster.js_ on the same origin as the hosting webpage or use the _"X-XSS-Protection: 0"_ HTTP header to guard against the potential abuse of browsers' integrated XSS auditors.
32 | * For the minified version, see [_XSSB-min.js_](/src/XSSB-min.js).
33 |
34 | # Demo:
35 | A live demo can be found at: [https://xssb.herokuapp.com](https://xssb.herokuapp.com).
36 |
37 | # Performance:
38 |
39 | Based on tests, **XSSB** only takes [10 milliseconds on average](/perf/perf.html) to do all required security checks besides the registration of a few necessary event listeners.
40 |
41 | # Compatibility:
42 | **XSSB** is compatible with the latest versions of all major web browsers (Firefox, Chrome, IE, Edge, Safari, and Opera) as well as most legacy web browsers through fallback functionality.
43 |
44 | # Known Issues:
45 | * **XSSB** only allows for [Basic Latin](https://en.wikipedia.org/wiki/Basic_Latin_(Unicode_block)) characters within the pathname, search query and hash of the hosting webpage's URL; that somewhat also applies to HTML5 messaging.... If your web application deals with a different set of characters, you may consider [base64](https://en.wikipedia.org/wiki/Base64) encoding as a workaround.
46 | * **XSSB** overrides security-sensitive functions like `eval` in order to enforce taint checking. A side effect of this is that `eval` will behave more like jQuery's [`globalEval`](https://api.jquery.com/jquery.globaleval/) than the native implementation of `eval` in most web browsers.
47 |
48 | # Credits:
49 | * [@0xSobky](https://twitter.com/0xsobky)
50 |
--------------------------------------------------------------------------------
/src/XSSB-min.js:
--------------------------------------------------------------------------------
1 | !function(window,Object,Array){var NativeFunction,Rprototype,cookie,cookieDesc,cookieIndex,cookiePair,cookiePairs,elPrototype,innerHTML,nativeAppendChild,nativeAtob,nativeCreateContextualFragment,nativeEval,nativeExecScript,nativeInsertAdjacentElement,nativeInsertAdjacentHTML,nativeInsertBefore,nativeLocalStorage,nativeReplaceChild,nativeSessionStorage,nativeSetImmediate,nativeSetInterval,nativeSetTimeout,nativeWrite,nativeWriteln,outerHTML,valIndex,win,winOrigin,taintedStrings=[],origin=window.location.origin||window.location.protocol+"//"+window.location.host,blacklistRe=/{{|}}|?\w{2,7};?|\b(?:on[a-z]+\W*?=|(?:(?:d\W*a\W*t\W*a)|(?:v\W*b|j\W*a\W*v\W*a)\W*s\W*c\W*r\W*i\W*p\W*t)\W*?:)/gi,getType=function(e){var t=Object.prototype.toString;return"string"==typeof e||"[object String]"===t.call(e)?"string":"[object Object]"===t.call(e)?"object":Array.isArray&&Array.isArray(e)||"[object Array]"===t.call(e)?"array":"[object Map]"===t.call(e)?"map":"[object Set]"===t.call(e)?"set":"[object RegExp]"===t.call(e)?"regex":"[object File]"===t.call(e)?"file":"[object FileList]"===t.call(e)?"fileList":"other"},toPlain=function(e){var t=window.encodeURI,n=window.decodeURI,i=window.encodeURIComponent,r=window.decodeURIComponent,o=0,a=[],c=function(e){var s,l,u,d;try{for(;r(e)!==e;)u=r(e),n(e)===u?(e=n(e),a.push(t)):(e=u,a.push(i)),++o}catch(t){if("function"==typeof window.escape&&"function"==typeof window.unescape?(s=window.escape,d=window.unescape):(s=(l=function(e){var t=/(?:[^%]|%(?:40|2[b-f]|2[0-9]|3[a-e]|[57][b-d]))+/gi;return function(n){return(n=n.match(t))?e(n.join("")):null}})(i),d=l(r)),e=d(e),a.push(s),++o,d(e)!==e)return c(e)}return e};return{output:e=c(e),depth:o,revMethod:a}},reEncode=function(e,t,n){for(;t--;)e=n[t](e);return e},sanitize=function(e){var t,n,i,r,o,a,c,s,l,u=/[^\w\s\/+=$#@!&*|,;:.?%()[\]{}^-]/g,d=!1,f=getType(e);if("string"===f)/\S/.test(e)&&(/%/.test(e)&&(a=toPlain(e),e=a.output),u.test(e)&&(e=e.replace(u,""),d=!0),blacklistRe.test(e)&&(e=e.replace(blacklistRe,""),d=!0),taintedStrings.push(e),a&&(e=reEncode(e,a.depth,a.revMethod)));else if("object"===f){s=function(t){var n=sanitize(e[t]);!1!==n&&(e[t]=n,d=!0)};try{for(i in o=Object.getOwnPropertyNames(e))s(o[i])}catch(t){for(c in n=Object.prototype.hasOwnProperty,e)n.call(e,c)&&s(c)}}else if("set"===f){try{l=new Set,e.forEach(function(e){var t=sanitize(e);e=t||e,l.add(e)}),e=l}catch(t){e=null}d=!0}else if("map"===f){try{l=new Map,e.forEach(function(e,t){var n=sanitize(e),i=sanitize(t);e=n||e,t=i||t,l.set(t,e)}),e=l}catch(t){e=null}d=!0}else if("regex"===f)!1!==(l=sanitize(e.source))&&(e=new RegExp(l),d=!0);else if("file"===f)try{!1!==(l=sanitize(e.name))&&((t=new FormData).append("file",e,l),e=t.get("file"),!1!==sanitize(e.name)&&(e=null),d=!0)}catch(t){e=null}else if("array"===f||"fileList"===f){if("fileList"===f){for(l=[],i=e.length;i--;)l[i]=e[i];l.item=function(e){return this[e]}}else l=e;for(i=l.length;i--;)!1!==(r=sanitize(l[i]))&&(l[i]=r,d=!0);d&&(e=l)}return!!d&&e},parseUrl=function(e){var t;try{e=new URL(e)}catch(n){(t=document.createElement("a")).href=e,e=t}return e},auditUrl=function(e){var t,n,i,r,o,a,c,s,l=!1;if(!1!==(o=sanitize(e.pathname))&&(e.pathname=o,l=!0),c=e.search){for(r=!1,i=(c=c.slice(1).split("&")).length;i--;){if((n=c[i].split("=")).length<3)!1!==(a=sanitize(n[0]))&&(n[0]=a,r=!0),n[1]&&!1!==(a=sanitize(n[1]))&&(n[1]=a,r=!0);else for(s=n.length;s--;)!1!==(a=sanitize(n[s]))&&(r=!0,n[s]=a);r&&(c[i]=n.join("="))}r&&(e.search=c.join("&"),l=!0)}return(t=e.hash.slice(1))&&!1!==(t=sanitize(t))&&(e.hash=t,l=!0),!!l&&e.href},addListener=window.addEventListener?function(e,t,n,i){("window"===t?window.addEventListener:document.addEventListener).call(e,n,i)}:function(e,t,n,i){var r;"DOMContentLoaded"===n?(r=function(){"interactive"===e.readyState&&i()},e.attachEvent("onreadystatechange",r)):e.attachEvent("on"+n,i)},defineProperties=function(e,t){for(var n,i,r=t.length;r--;)n=(i=t[r]).value,i=i.isDefault?{value:n,enumerable:!0,writable:!0,configurable:!0}:i;try{Object.defineProperties(e,t)}catch(e){}},auditWinName=function(e){var t=e.name;defineProperties(e,{name:{get:function(){return t},set:function(e){var n=sanitize(e);t=!1!==n?n:e},enumerable:!0}}),e.name=t},auditWin=function(e){var t,n,i,r,o=function(e){var t,n,i,r=e.ports;try{t=e.origin||e.originalEvent.origin}catch(e){}if(t!==origin&&(!1!==(n=sanitize(e.data))&&defineProperties(e,{data:{value:n,isDefault:!0}}),r))for(i=r.length;i--;)r[i].onmessage=o};addListener(e,"window","hashchange",function(){var t=sanitize(e.location.hash.slice(1));!1!==t&&(e.location.hash=t)}),addListener(e,"window","message",o),auditUrl(e.location),(n=e.name)&&!1!==(n=sanitize(n))&&(e.name=n),!1!==(r=sanitize(e.document.title))&&(e.document.title=r),(i=e.document.referrer)&&!1!==(i=auditUrl(parseUrl(i)))&&defineProperties(e.document,{referrer:{value:i,isDefault:!0}}),t=function(){var t,n=document.getElementsByTagName.call(e.document,"iframe"),i=function(e){var t;try{t=e.contentWindow,e.src!==t.location.href&&(auditWinName(t),auditWin(t))}catch(e){}};for(t=n.length;t--;)!function(e){addListener(e,"document","load",function(){i(e)})}(n[t])},addListener(e.document,"document","DOMContentLoaded",t)},getPrototypeOf=function(){try{return Object.getPrototypeOf.apply(this,arguments)}catch(e){}},guardWrite=function(e){return function(t){var n;isSafeArg(t)?e.call(document,t):(t=toSafeStr(t),(n=document.getElementsByTagName("*"))[n.length-1].parentElement.innerHTML=t)}},some=Array.prototype.some||function(e){for(var t=this.length;t--;)if(e(this[t]))return!0;return!1},isSafeArg=function(){return!some.call(arguments,function(e){return e=toPlain(e).output,some.call(taintedStrings,function(t){return isNaN(t)&&t.length>6&&-1!==e.indexOf(t)})})},guardSink=function(e){return function(){if(isSafeArg.apply(null,arguments))return e.apply(this,arguments)}},isUnsafeNode=function(e){var t,n,i,r,o,a,c=e.nodeName;try{if(e.hasChildNodes()){if((t=e.getElementsByTagName("applet")).length>0)return some.call(t,isUnsafeNode);if((n=e.getElementsByTagName("embed")).length>0)return some.call(n,isUnsafeNode);if((i=e.getElementsByTagName("frame")).length>0)return some.call(i,isUnsafeNode);if((r=e.getElementsByTagName("iframe")).length>0)return some.call(r,isUnsafeNode);if((o=e.getElementsByTagName("object")).length>0)return some.call(o,isUnsafeNode);if((a=e.getElementsByTagName("script")).length>0)return some.call(a,isUnsafeNode)}}catch(e){}return"SCRIPT"===c?!isSafeArg(e.text)||!isSafeArg(e.src):"OBJECT"===c?!isSafeArg(e.data):"IFRAME"===c||"FRAME"===c||"EMBED"===c?!(isSafeArg(e.src)&&(!e.srcdoc||isSafeArg(e.srcdoc))):"APPLET"===c?!!(!isSafeArg(e.code)||e.codebase&&!isSafeArg(e.codebase)||e.archive&&!isSafeArg(e.archive)):void 0},toSafeNode=function(e){var t,n,i,r;e.innerHTML="",e.hasAttribute("src")&&e.removeAttribute("src"),e.hasAttribute("srcdoc")&&e.removeAttribute("srcdoc"),e.hasAttribute("data")&&e.removeAttribute("data"),e.hasAttribute("code")&&e.removeAttribute("code"),e.hasAttribute("archive")&&e.removeAttribute("archive"),e.hasAttribute("codebase")&&e.removeAttribute("codebase"),e.hasAttribute("object")&&e.removeAttribute("object");try{if(e.hasAttributes())for(r=(i=e.attributes).length;r--;)n=(t=i[r]).name,/^on./.test(n)&&!isSafeArg(t.value)&&e.removeAttribute(n)}catch(e){}return e},guardMethod=function(e){return function(t){return isUnsafeNode(t)&&(t=toSafeNode(t)),e.apply(this,arguments)}},getOwnPropertyDescriptor=function(){try{return Object.getOwnPropertyDescriptor.apply(this,arguments)}catch(e){}},guardStorage=function(e){return{setItem:function(t,n){isSafeArg(t,n)&&e.setItem(t,n)},getItem:function(t){return e.getItem(t)}}},toSafeStr=function(e){return-1!==e.indexOf("<")&&blacklistRe.test(e)&&(e=(e=e.replace(blacklistRe,"")).replace(/\bsrcdoc=/gi,"redacted=")),e},genDescriptor=function(e){return{get:function(){return e.get.call(this)},set:function(t){return isSafeArg(t)||(t=toSafeStr(t)),e.set.call(this,t)}}};if(auditWin(window),window!==top){win=parent;try{winOrigin=win.location.origin||win.location.protocol+"//"+win.location.host}catch(e){}do{try{winOrigin!==origin&&(auditWinName(win),auditWin(win))}catch(e){continue}finally{win=win.parent}}while(win!==top)}NativeFunction=window.Function,nativeEval=window.eval,nativeSetInterval=window.setInterval,nativeSetTimeout=window.setTimeout,nativeWrite=document.write,nativeWriteln=document.writeln,document.write=guardWrite(nativeWrite),document.writeln=guardWrite(nativeWriteln),window.eval=guardSink(nativeEval),window.setTimeout=guardSink(nativeSetTimeout),window.setInterval=guardSink(nativeSetInterval),window.Function=function(){var e=function(){var e=NativeFunction.apply(null,arguments);e.constructor=Function;try{Object.setPrototypeOf(e,Function)}catch(t){e.__proto__=Function}return e};return isSafeArg.apply(null,arguments)?e.apply(null,arguments):e()},window.Function.prototype=Function;try{elPrototype=window.Element.prototype,nativeAppendChild=elPrototype.appendChild,nativeReplaceChild=elPrototype.replaceChild,nativeInsertBefore=elPrototype.insertBefore,nativeInsertAdjacentHTML=elPrototype.insertAdjacentHTML,nativeInsertAdjacentElement=elPrototype.insertAdjacentElement,innerHTML=getOwnPropertyDescriptor(elPrototype,"innerHTML"),outerHTML=getOwnPropertyDescriptor(elPrototype,"outerHTML"),elPrototype.appendChild=guardMethod(nativeAppendChild),elPrototype.replaceChild=guardMethod(nativeReplaceChild),elPrototype.insertBefore=guardMethod(nativeInsertBefore),elPrototype.insertAdjacentHTML=function(e,t){return isSafeArg(t)||(t=toSafeStr(t)),nativeInsertAdjacentHTML.call(this,e,t)},elPrototype.insertAdjacentElement=function(e,t){return isUnsafeNode(t)&&(t=toSafeNode(t)),nativeInsertAdjacentElement.call(this,e,t)},defineProperties(elPrototype,{innerHTML:genDescriptor(innerHTML),outerHTML:genDescriptor(outerHTML)})}catch(e){}window.execScript&&(nativeExecScript=window.execScript,eval("var execScript;"),window.execScript=guardSink(nativeExecScript)),window.setImmediate&&(nativeSetImmediate=window.setImmediate,window.setImmediate=guardSink(nativeSetImmediate)),window.atob&&(nativeAtob=window.atob,window.atob=function(e){return isSafeArg(e)?nativeAtob(e):e=sanitize(nativeAtob(e))});try{Rprototype=window.Range.prototype,nativeCreateContextualFragment=Rprototype.createContextualFragment,Rprototype.createContextualFragment=function(e){return isSafeArg(e)||(e=""),nativeCreateContextualFragment.call(this,e)}}catch(e){}for(cookie=document.cookie,cookieDesc=function(){try{return getOwnPropertyDescriptor(document,"cookie")||getOwnPropertyDescriptor(getPrototypeOf(document),"cookie")||{get:document.__lookupGetter__("cookie"),set:document.__lookupSetter__("cookie")}}catch(e){}}(),defineProperties(document,{cookie:{get:function(){try{return cookieDesc.get.call(this)}catch(e){return cookie}},set:function(e){if(isSafeArg(e))try{return cookieDesc.set.call(this,e)}catch(t){cookie+=";"+e}}}}),cookiePairs=cookie.split(";"),cookieIndex=cookiePairs.length;cookieIndex--;)for(cookiePair=cookiePairs[cookieIndex].split("="),valIndex=cookiePair.length;valIndex--;)taintedStrings.push(cookiePair[valIndex]);try{window.localStorage&&(nativeLocalStorage=window.localStorage,delete window.localStorage,window.localStorage=guardStorage(nativeLocalStorage)),window.sessionStorage&&(nativeSessionStorage=window.sessionStorage,delete window.sessionStorage,window.sessionStorage=guardStorage(nativeSessionStorage))}catch(e){}}(window,Object,Array);
--------------------------------------------------------------------------------
/src/XSSBuster.js:
--------------------------------------------------------------------------------
1 | (function(window, Object, Array) {
2 | // Version 1.1.3.
3 | var NativeFunction, Rprototype, cookie, cookieDesc, cookieIndex,
4 | cookiePair, cookiePairs, elPrototype, innerHTML, nativeAppendChild,
5 | nativeAtob, nativeCreateContextualFragment, nativeEval,
6 | nativeExecScript, nativeInsertAdjacentElement, nativeInsertAdjacentHTML,
7 | nativeInsertBefore, nativeLocalStorage, nativeReplaceChild,
8 | nativeSessionStorage, nativeSetImmediate, nativeSetInterval,
9 | nativeSetTimeout, nativeWrite, nativeWriteln, outerHTML, valIndex, win,
10 | winOrigin;
11 |
12 | var taintedStrings = [];
13 |
14 | var origin = window.location.origin ||
15 | window.location.protocol + '//' + window.location.host;
16 |
17 | /*
18 | * Matches evil URI schemes, event handlers, HTML entities,
19 | * and scary curly notations!
20 | */
21 | var blacklistRe = /{{|}}|?\w{2,7};?|\b(?:on[a-z]+\W*?=|(?:(?:d\W*a\W*t\W*a)|(?:v\W*b|j\W*a\W*v\W*a)\W*s\W*c\W*r\W*i\W*p\W*t)\W*?:)/gi;
22 |
23 | /**
24 | * Take an input and return its data type.
25 | *
26 | * @param input {string|array|object}, some data.
27 | * @return {string}, data type.
28 | */
29 | var getType = function(input) {
30 | var toString = Object.prototype.toString;
31 | // Is it a string?
32 | if (typeof input === 'string' ||
33 | toString.call(input) === '[object String]') {
34 | return 'string';
35 | // An object?
36 | } else if (toString.call(input) === '[object Object]') {
37 | return 'object';
38 | // An array?
39 | } else if (Array.isArray && Array.isArray(input) ||
40 | toString.call(input) === '[object Array]') {
41 | return 'array';
42 | // A map?
43 | } else if (toString.call(input) === '[object Map]') {
44 | return 'map';
45 | // A set?
46 | } else if (toString.call(input) === '[object Set]') {
47 | return 'set';
48 | // A regex?
49 | } else if (toString.call(input) === '[object RegExp]') {
50 | return 'regex';
51 | // A file?
52 | } else if (toString.call(input) === '[object File]') {
53 | return 'file';
54 | // A fileList?
55 | } else if (toString.call(input) === '[object FileList]') {
56 | return 'fileList';
57 | }
58 | return 'other';
59 | };
60 |
61 | /**
62 | * Take a URL-encoded input and return it in plaintext.
63 | *
64 | * @param input {string}, data string to decode.
65 | * @return {object}, a container for decoded data.
66 | */
67 | var toPlain = function(input) {
68 | var encURI = window.encodeURI;
69 | var decURI = window.decodeURI;
70 | var encURIComp = window.encodeURIComponent;
71 | var decURIComp = window.decodeURIComponent;
72 | var depth = 0;
73 | var revMethod = [];
74 | /**
75 | * URL-decode a given string.
76 | *
77 | * @param input {string}, a URL-encoded string.
78 | * @return {string}, a URL-decoded string.
79 | */
80 | var deEncode = function(input) {
81 | var es, eusExtend, origInput, ues;
82 | /* A try/catch clause to handle any URIError exceptions. */
83 | try {
84 | // Recursively URL-decode the input data.
85 | while (decURIComp(input) !== input) {
86 | origInput = decURIComp(input);
87 | if (decURI(input) === origInput) {
88 | input = decURI(input);
89 | revMethod.push(encURI);
90 | } else {
91 | input = origInput;
92 | revMethod.push(encURIComp);
93 | }
94 | ++depth;
95 | }
96 | } catch (e) {
97 | // Make sure `escape()` and `unescape()` are still supported.
98 | if (typeof window.escape === 'function' &&
99 | typeof window.unescape === 'function') {
100 | es = window.escape;
101 | ues = window.unescape;
102 | // A just-in-case fallback.
103 | } else {
104 | /**
105 | * Extend a URL-encoding/decoding function's functionality.
106 | *
107 | * @param func {function}, a URL-encoding/decoding function.
108 | * @return {function}.
109 | */
110 | eusExtend = function(func) {
111 | var charsetRe = /(?:[^%]|%(?:40|2[b-f]|2[0-9]|3[a-e]|[57][b-d]))+/gi;
112 | return function(string) {
113 | string = string.match(charsetRe);
114 | return (string) ? func(string.join('')) : null;
115 | };
116 | };
117 | es = eusExtend(encURIComp);
118 | ues = eusExtend(decURIComp);
119 | }
120 | input = ues(input);
121 | revMethod.push(es);
122 | ++depth;
123 | if (ues(input) !== input) {
124 | return deEncode(input);
125 | }
126 | }
127 | return input;
128 | };
129 | input = deEncode(input);
130 | return {
131 | output: input,
132 | depth: depth,
133 | revMethod: revMethod
134 | };
135 | };
136 |
137 | /**
138 | * Take a `toPlain()` output and re-encode it.
139 | *
140 | * @param input {string}, a `toPlain().output` property.
141 | * @param depth {integer}, a `toPlain().depth` property.
142 | * @param revMethod {function}, a `toPlain().revMethod` property.
143 | * @return {string}, URL-encoded data.
144 | */
145 | var reEncode = function(input, depth, revMethod) {
146 | while (depth--) {
147 | input = revMethod[depth](input);
148 | }
149 | return input;
150 | };
151 |
152 | /**
153 | * Take a raw input and sanitize it as needed.
154 | *
155 | * @param input {string|array|object}, a string literal, array or object.
156 | * @return {string|array|object|boolean}, sanitized data or `false`.
157 | */
158 | var sanitize = function(input) {
159 | var formData, hasOwnProperty, index, item, keys, origInput, prop, propSanitize,
160 | tmpVar;
161 | // Matches safe Basic Latin characters.
162 | var whitelistRe = /[^\w\s\/+=$#@!&*|,;:.?%()[\]{}^-]/g;
163 | var isModified = false;
164 | var inptType = getType(input);
165 | // Check if `input` is a string.
166 | if (inptType === 'string') {
167 | // Assert it's not a whitespace string.
168 | if (/\S/.test(input)) {
169 | // Check if `input` is URL-encoded.
170 | if (/%/.test(input)) {
171 | origInput = toPlain(input);
172 | input = origInput.output;
173 | }
174 | if (whitelistRe.test(input)) {
175 | input = input.replace(whitelistRe, '');
176 | isModified = true;
177 | }
178 | if (blacklistRe.test(input)) {
179 | input = input.replace(blacklistRe, '');
180 | isModified = true;
181 | }
182 | // Add `input` to the list of tainted strings.
183 | taintedStrings.push(input);
184 | // Re-encode `input` if it has been decoded.
185 | if (origInput) {
186 | input = reEncode(input, origInput.depth, origInput.revMethod);
187 | }
188 | }
189 | // Check if it's an object.
190 | } else if (inptType === 'object') {
191 | /**
192 | * Take an object property and audit it.
193 | *
194 | * @param prop {string}, an object property name.
195 | * @return void.
196 | */
197 | propSanitize = function(prop) {
198 | var value = sanitize(input[prop]);
199 | if (value !== false) {
200 | input[prop] = value;
201 | isModified = true;
202 | }
203 | };
204 | try {
205 | keys = Object.getOwnPropertyNames(input);
206 | for (index in keys) {
207 | propSanitize(keys[index]);
208 | }
209 | } catch (e) {
210 | hasOwnProperty = Object.prototype.hasOwnProperty;
211 | for (prop in input) {
212 | if (hasOwnProperty.call(input, prop)) {
213 | propSanitize(prop);
214 | }
215 | }
216 | }
217 | // Check if it's a set.
218 | } else if (inptType === 'set') {
219 | try {
220 | tmpVar = new Set();
221 | input.forEach(function(val) {
222 | var sVal = sanitize(val);
223 | val = sVal ? sVal : val;
224 | tmpVar.add(val);
225 | });
226 | input = tmpVar;
227 | } catch (e) {
228 | input = null;
229 | }
230 | isModified = true;
231 | // Check if it's a map.
232 | } else if (inptType === 'map') {
233 | try {
234 | tmpVar = new Map();
235 | input.forEach(function(val, key) {
236 | var sVal = sanitize(val);
237 | var sKey = sanitize(key);
238 | val = sVal ? sVal : val;
239 | key = sKey ? sKey : key;
240 | tmpVar.set(key, val);
241 | });
242 | input = tmpVar;
243 | } catch (e) {
244 | input = null;
245 | }
246 | isModified = true;
247 | // Check if it's a regex.
248 | } else if (inptType === 'regex') {
249 | tmpVar = sanitize(input.source);
250 | if (tmpVar !== false) {
251 | input = new RegExp(tmpVar);
252 | isModified = true;
253 | }
254 | // Check if it's a file.
255 | } else if (inptType === 'file') {
256 | try {
257 | tmpVar = sanitize(input.name);
258 | if (tmpVar !== false) {
259 | formData = new FormData();
260 | formData.append('file', input, tmpVar);
261 | input = formData.get('file');
262 | if (sanitize(input.name) !== false) {
263 | input = null;
264 | }
265 | isModified = true;
266 | }
267 | } catch (e) {
268 | input = null;
269 | }
270 | // Check if it's an array-like object.
271 | } else if (inptType === 'array' || inptType === 'fileList') {
272 | if (inptType === 'fileList') {
273 | tmpVar = [];
274 | index = input.length;
275 | while (index--) {
276 | tmpVar[index] = input[index];
277 | }
278 | tmpVar.item = function(index) {
279 | return this[index];
280 | };
281 | } else {
282 | tmpVar = input;
283 | }
284 | index = tmpVar.length;
285 | // Iterate over array items and sanitize them one by one.
286 | while (index--) {
287 | item = sanitize(tmpVar[index]);
288 | if (item !== false) {
289 | tmpVar[index] = item;
290 | isModified = true;
291 | }
292 | }
293 | if (isModified) {
294 | input = tmpVar;
295 | }
296 | }
297 | return (isModified) ? input : false;
298 | };
299 |
300 | /**
301 | * Parse a URL string.
302 | *
303 | * @param url {string}, a URL string.
304 | * @return {object}, a URL object.
305 | */
306 | var parseUrl = function(url) {
307 | var parser;
308 | try {
309 | url = new URL(url);
310 | } catch (e) {
311 | parser = document.createElement('a');
312 | parser.href = url;
313 | url = parser;
314 | }
315 | return url;
316 | };
317 |
318 | /**
319 | * Take a URL object and sanitize it.
320 | *
321 | * @param urlObj {object}, a URL object.
322 | * @return {string|boolean}, a string URL or `false`.
323 | */
324 | var auditUrl = function(urlObj) {
325 | var hash, paramPair, paramIndex, paramModified, pathname, sanParam,
326 | search, subIndex;
327 | var isModified = false;
328 | /*
329 | * For sanitizing the pathname property of
330 | * the current window location object.
331 | */
332 | pathname = sanitize(urlObj.pathname);
333 | if (pathname !== false) {
334 | urlObj.pathname = pathname;
335 | isModified = true;
336 | }
337 | /*
338 | * For sanitizing the search property of
339 | * the current window location object.
340 | */
341 | search = urlObj.search;
342 | if (search) {
343 | paramModified = false;
344 | search = search.slice(1).split('&');
345 | paramIndex = search.length;
346 | while (paramIndex--) {
347 | paramPair = search[paramIndex].split('=');
348 | if (paramPair.length < 3) {
349 | sanParam = sanitize(paramPair[0]);
350 | if (sanParam !== false) {
351 | paramPair[0] = sanParam;
352 | paramModified = true;
353 | }
354 | if (paramPair[1]) {
355 | sanParam = sanitize(paramPair[1]);
356 | if (sanParam !== false) {
357 | paramPair[1] = sanParam;
358 | paramModified = true;
359 | }
360 | }
361 | } else {
362 | subIndex = paramPair.length;
363 | while (subIndex--) {
364 | sanParam = sanitize(paramPair[subIndex]);
365 | if (sanParam !== false) {
366 | paramModified = true;
367 | paramPair[subIndex] = sanParam;
368 | }
369 | }
370 | }
371 | if (paramModified) {
372 | search[paramIndex] = paramPair.join('=');
373 | }
374 | }
375 | if (paramModified) {
376 | urlObj.search = search.join('&');
377 | isModified = true;
378 | }
379 | }
380 | /*
381 | * For sanitizing the hash property of
382 | * the current window location object.
383 | */
384 | hash = urlObj.hash.slice(1);
385 | if (hash) {
386 | hash = sanitize(hash);
387 | if (hash !== false) {
388 | urlObj.hash = hash;
389 | isModified = true;
390 | }
391 | }
392 | return (isModified) ? urlObj.href : false;
393 | };
394 |
395 | /**
396 | * Register a new cross-browser event listener.
397 | *
398 | * @param target {object}, a target object to bind the event listener to.
399 | * @param equiv {string}, a corresponding DOM property name for @target.
400 | * @param evName {string}, the name of the event to register.
401 | * @param callback {function}, a callback function for the event listener.
402 | * @return void.
403 | */
404 | var addListener = (function() {
405 | if (window.addEventListener) {
406 | return function(target, equiv, evName, callback) {
407 | var nativeAddEventListener = (equiv === 'window') ?
408 | window.addEventListener : document.addEventListener;
409 | nativeAddEventListener.call(target, evName, callback);
410 | };
411 | }
412 | // For IE8 and earlier versions support.
413 | return function(target, _, evName, callback) {
414 | var domLoadedCallback;
415 | if (evName === 'DOMContentLoaded') {
416 | /**
417 | * A proxy function to `callback()`.
418 | * @return void.
419 | */
420 | domLoadedCallback = function() {
421 | if (target.readyState === 'interactive') {
422 | callback();
423 | }
424 | };
425 | target.attachEvent('onreadystatechange', domLoadedCallback);
426 | } else {
427 | target.attachEvent('on' + evName, callback);
428 | }
429 | };
430 | })();
431 |
432 | /**
433 | * A proxy function to `Object.defineProperties()`.
434 | *
435 | * @param obj {object}, a target object.
436 | * @param properties {object}, a property descriptor.
437 | * @return void.
438 | */
439 | var defineProperties = function(obj, properties) {
440 | var origValue, prop;
441 | var index = properties.length;
442 | while (index--) {
443 | prop = properties[index];
444 | origValue = prop.value;
445 | prop = prop.isDefault ? {
446 | value: origValue,
447 | enumerable: true,
448 | writable: true,
449 | configurable: true
450 | } : prop;
451 | }
452 | try {
453 | Object.defineProperties(obj, properties);
454 | } catch (e) {}
455 | };
456 |
457 | /**
458 | * Redefine the name property of a given window object,
459 | * so we add a setter to sanitize it whenever it changes.
460 | *
461 | * @param winObj {object}, a window object.
462 | * @return void.
463 | */
464 | var auditWinName = function(winObj) {
465 | var origName = winObj.name;
466 | defineProperties(winObj, {
467 | 'name': {
468 | get: function() {
469 | return origName;
470 | },
471 | set: function(val) {
472 | var sanVal = sanitize(val);
473 | if (sanVal !== false) {
474 | origName = sanVal;
475 | } else {
476 | origName = val;
477 | }
478 | },
479 | enumerable: true
480 | }
481 | });
482 | winObj.name = origName;
483 | };
484 |
485 | /**
486 | * Take a window object and audit its default properties.
487 | *
488 | * @param winObj {object}, a window object.
489 | * @return void.
490 | */
491 | var auditWin = function(winObj) {
492 | var auditFrames, name, referrer, title;
493 | /**
494 | * Audit the `location.hash` window property on change.
495 | *
496 | * @return void.
497 | */
498 | var onhashchangeFn = function() {
499 | var hash = sanitize(winObj.location.hash.slice(1));
500 | if (hash !== false) {
501 | winObj.location.hash = hash;
502 | }
503 | };
504 | /**
505 | * Intercept HTML5 messages and audit them.
506 | *
507 | * @param ev {object}, a message event.
508 | * @return void.
509 | */
510 | var onmessageFn = function(ev) {
511 | var winOrigin, data, index, port;
512 | var ports = ev.ports;
513 | try {
514 | winOrigin = ev.origin || ev.originalEvent.origin;
515 | } catch (e) {}
516 | if (winOrigin !== origin) {
517 | data = sanitize(ev.data);
518 | if (data !== false) {
519 | defineProperties(ev, {
520 | 'data': {
521 | value: data,
522 | isDefault: true
523 | }
524 | });
525 | }
526 | if (ports) {
527 | index = ports.length;
528 | while (index--) {
529 | port = ports[index];
530 | port.onmessage = onmessageFn;
531 | }
532 | }
533 | }
534 | };
535 | // For hash re-sanitization whenever it gets modified.
536 | addListener(winObj, 'window', 'hashchange', onhashchangeFn);
537 | // For cross-document messaging sanitization.
538 | addListener(winObj, 'window', 'message', onmessageFn);
539 | // Audit the current window URL.
540 | auditUrl(winObj.location);
541 | /*
542 | * For sanitizing the name property
543 | * of the current window object.
544 | */
545 | name = winObj.name;
546 | if (name) {
547 | name = sanitize(name);
548 | if (name !== false) {
549 | winObj.name = name;
550 | }
551 | }
552 | /*
553 | * For sanitizing the title of the current document.
554 | * And yep, `document.title` can be -partially?-
555 | * manipulated by attackers as in search pages!
556 | */
557 | title = sanitize(winObj.document.title);
558 | if (title !== false) {
559 | winObj.document.title = title;
560 | }
561 | // For sanitizing any given document referrer.
562 | referrer = winObj.document.referrer;
563 | if (referrer) {
564 | referrer = auditUrl(parseUrl(referrer));
565 | if (referrer !== false) {
566 | defineProperties(winObj.document, {
567 | 'referrer': {
568 | value: referrer,
569 | isDefault: true
570 | }
571 | });
572 | }
573 | }
574 | /**
575 | * For auditing child frames.
576 | *
577 | * @return void.
578 | */
579 | auditFrames = function() {
580 | var fIndex, currentFrame;
581 | var getElementsByTagName = document.getElementsByTagName;
582 | var frames = getElementsByTagName.call(winObj.document,
583 | 'iframe');
584 | /**
585 | * For auditing a given frame node.
586 | *
587 | * @param currentFrame {object}, a frame node.
588 | * @return void.
589 | */
590 | var auditFrame = function(currentFrame) {
591 | var fWindow;
592 | try {
593 | fWindow = currentFrame.contentWindow;
594 | if (currentFrame.src !== fWindow.location.href) {
595 | auditWinName(fWindow);
596 | auditWin(fWindow);
597 | }
598 | } catch (e) {}
599 | };
600 | fIndex = frames.length;
601 | while (fIndex--) {
602 | currentFrame = frames[fIndex];
603 | (function(currentFrame) {
604 | addListener(currentFrame, 'document', 'load',
605 | function() {
606 | auditFrame(currentFrame);
607 | });
608 | })(currentFrame);
609 | }
610 | };
611 | addListener(winObj.document, 'document',
612 | 'DOMContentLoaded', auditFrames);
613 | };
614 |
615 | /**
616 | * A proxy function to `Object.getPrototypeOf()`.
617 | *
618 | * @return {object}, a prototype object.
619 | */
620 | var getPrototypeOf = function () {
621 | try {
622 | return Object.getPrototypeOf.apply(this, arguments);
623 | } catch (e) {}
624 | };
625 |
626 | /**
627 | * Guard write functions against tainted strings.
628 | *
629 | * @param nativeWrite {function}, a write-like function.
630 | * @return {function}.
631 | */
632 | var guardWrite = function(nativeWrite) {
633 | return function(str) {
634 | var el, els;
635 | if (!isSafeArg(str)) {
636 | str = toSafeStr(str);
637 | els = document.getElementsByTagName('*');
638 | el = els[els.length - 1];
639 | el.parentElement.innerHTML = str;
640 | } else {
641 | nativeWrite.call(document, str);
642 | }
643 | };
644 | };
645 |
646 | /**
647 | * A proxy function to `Array.prototype.some()`.
648 | *
649 | * @param fn {function}, a test function.
650 | * @return {boolean}.
651 | */
652 | var some = Array.prototype.some || function(fn) {
653 | var index = this.length;
654 | while (index--) {
655 | if (fn(this[index])) {
656 | return true;
657 | }
658 | }
659 | return false;
660 | };
661 |
662 | /**
663 | * Check if a given string is safe.
664 | *
665 | * @return void.
666 | */
667 | var isSafeArg = function() {
668 | /**
669 | * Validate any given string argument.
670 | *
671 | * @param arg {string}, a given string.
672 | * @return {boolean}.
673 | */
674 | var validate = function(arg) {
675 | /**
676 | * Check if a given string is tainted.
677 | *
678 | * @param taint {string}, a given string.
679 | * @return {boolean}.
680 | */
681 | var isTainted = function(taint) {
682 | return (isNaN(taint) && taint.length > 6 &&
683 | arg.indexOf(taint) !== -1);
684 | };
685 | arg = toPlain(arg).output;
686 | return (some.call(taintedStrings, isTainted));
687 | };
688 | if (some.call(arguments, validate)) {
689 | return false;
690 | }
691 | return true;
692 | };
693 |
694 | /**
695 | * Guard a given sink function.
696 | *
697 | * @param sinkFn {function}, a sink function.
698 | * @return {function}, a safe sink function.
699 | */
700 | var guardSink = function(sinkFn) {
701 | return function() {
702 | if (isSafeArg.apply(null, arguments)) {
703 | return sinkFn.apply(this, arguments);
704 | }
705 | };
706 | };
707 |
708 | /**
709 | * Check if a given node is unsafe.
710 | *
711 | * @param node {object}, a given DOM node.
712 | * @return {boolean}.
713 | */
714 | var isUnsafeNode = function(node) {
715 | var childApplets, childEmbeds, childFrames, childIframes, childObjects, childScripts;
716 | var nodeName = node.nodeName;
717 | try {
718 | if (node.hasChildNodes()) {
719 | childApplets = node.getElementsByTagName('applet');
720 | if (childApplets.length > 0) {
721 | return some.call(childApplets, isUnsafeNode);
722 | }
723 | childEmbeds = node.getElementsByTagName('embed');
724 | if (childEmbeds.length > 0) {
725 | return some.call(childEmbeds, isUnsafeNode);
726 | }
727 | childFrames = node.getElementsByTagName('frame');
728 | if (childFrames.length > 0) {
729 | return some.call(childFrames, isUnsafeNode);
730 | }
731 | childIframes = node.getElementsByTagName('iframe');
732 | if (childIframes.length > 0) {
733 | return some.call(childIframes, isUnsafeNode);
734 | }
735 | childObjects = node.getElementsByTagName('object');
736 | if (childObjects.length > 0) {
737 | return some.call(childObjects, isUnsafeNode);
738 | }
739 | childScripts = node.getElementsByTagName('script');
740 | if (childScripts.length > 0) {
741 | return some.call(childScripts, isUnsafeNode);
742 | }
743 | }
744 | } catch (e) {}
745 | if (nodeName === 'SCRIPT') {
746 | if (isSafeArg(node.text) && isSafeArg(node.src)) {
747 | return false;
748 | }
749 | return true;
750 | } else if (nodeName === 'OBJECT') {
751 | if (isSafeArg(node.data)) {
752 | return false;
753 | }
754 | return true;
755 | } else if (nodeName === 'IFRAME' || nodeName === 'FRAME' ||
756 | nodeName === 'EMBED') {
757 | if (isSafeArg(node.src) &&
758 | (!node.srcdoc || isSafeArg(node.srcdoc))) {
759 | return false;
760 | }
761 | return true;
762 | } else if (nodeName === 'APPLET') {
763 | if (isSafeArg(node.code) &&
764 | (!node.codebase || isSafeArg(node.codebase)) &&
765 | (!node.archive || isSafeArg(node.archive))) {
766 | return false;
767 | }
768 | return true;
769 | }
770 | };
771 |
772 | /**
773 | * Neutralize a given DOM node.
774 | *
775 | * @param node {object}, an unsafe DOM node.
776 | * @return {object}, a neutralized DOM node.
777 | */
778 | var toSafeNode = function(node) {
779 | var attrib, attribName, attribs, index;
780 | node.innerHTML = '';
781 | if (node.hasAttribute('src')) {
782 | node.removeAttribute('src');
783 | }
784 | if (node.hasAttribute('srcdoc')) {
785 | node.removeAttribute('srcdoc');
786 | }
787 | if (node.hasAttribute('data')) {
788 | node.removeAttribute('data');
789 | }
790 | if (node.hasAttribute('code')) {
791 | node.removeAttribute('code');
792 | }
793 | if (node.hasAttribute('archive')) {
794 | node.removeAttribute('archive');
795 | }
796 | if (node.hasAttribute('codebase')) {
797 | node.removeAttribute('codebase');
798 | }
799 | if (node.hasAttribute('object')) {
800 | node.removeAttribute('object');
801 | }
802 | try {
803 | if (node.hasAttributes()) {
804 | attribs = node.attributes;
805 | index = attribs.length;
806 | while (index--) {
807 | attrib = attribs[index];
808 | attribName = attrib.name;
809 | if (/^on./.test(attribName) && !isSafeArg(attrib.value)) {
810 | node.removeAttribute(attribName);
811 | }
812 | }
813 | }
814 | } catch (e) {}
815 | return node;
816 | };
817 |
818 | /**
819 | * Guard `appendChild()` and alike methods.
820 | *
821 | * @param method {function}, a given function.
822 | * @return {function}.
823 | */
824 | var guardMethod = function(method) {
825 | return function(node) {
826 | if (isUnsafeNode(node)) {
827 | node = toSafeNode(node);
828 | }
829 | return method.apply(this, arguments);
830 | };
831 | };
832 |
833 | /**
834 | * A proxy function to `Object.getOwnPropertyDescriptor()`.
835 | *
836 | * @return {object}, a property descriptor.
837 | */
838 | var getOwnPropertyDescriptor = function () {
839 | try {
840 | return Object.getOwnPropertyDescriptor.apply(this, arguments);
841 | } catch (e) {}
842 | };
843 |
844 | /**
845 | * Guard a given storage object.
846 | *
847 | * @param storageObj {object}, a storage object.
848 | * @return {object}, a safe storage object.
849 | */
850 | var guardStorage = function(storageObj) {
851 | return {
852 | setItem: function(key, value) {
853 | if (isSafeArg(key, value)) {
854 | storageObj.setItem(key, value);
855 | }
856 | },
857 | getItem: function(key) {
858 | return storageObj.getItem(key);
859 | }
860 | };
861 | };
862 |
863 | /**
864 | * Take a suspicious string and neutralize it.
865 | *
866 | * @param str {string}, a suspicious string.
867 | * @return {string}, a neutralized string.
868 | */
869 | var toSafeStr = function(str) {
870 | if (str.indexOf('<') !== -1 && blacklistRe.test(str)) {
871 | str = str.replace(blacklistRe, '');
872 | str = str.replace(/\bsrcdoc=/gi, 'redacted=');
873 | }
874 | return str;
875 | };
876 |
877 | /**
878 | * Generate a safe property descriptor.
879 | *
880 | * @param prop {object}, a descriptor object.
881 | * @return {object}.
882 | */
883 | var genDescriptor = function(prop) {
884 | return {
885 | get: function() {
886 | return prop.get.call(this);
887 | },
888 | set: function(val) {
889 | if (!isSafeArg(val)) {
890 | val = toSafeStr(val);
891 | }
892 | return prop.set.call(this, val);
893 | }
894 | };
895 | };
896 |
897 | // Audit `self` window.
898 | auditWin(window);
899 |
900 | // Audit `parent` window(s).
901 | if (window !== top) {
902 | win = parent;
903 | try {
904 | winOrigin = win.location.origin ||
905 | win.location.protocol + '//' + win.location.host;
906 | } catch(e) {}
907 | do {
908 | try {
909 | if (winOrigin !== origin) {
910 | auditWinName(win);
911 | auditWin(win);
912 | }
913 | } catch (e) {
914 | continue;
915 | } finally {
916 | win = win.parent;
917 | }
918 | } while (win !== top);
919 | }
920 |
921 | /* Monkey-patch JS sinks. */
922 | NativeFunction = window.Function;
923 | nativeEval = window.eval;
924 | nativeSetInterval = window.setInterval;
925 | nativeSetTimeout = window.setTimeout;
926 | nativeWrite = document.write;
927 | nativeWriteln = document.writeln;
928 | document.write = guardWrite(nativeWrite);
929 | document.writeln = guardWrite(nativeWriteln);
930 | window.eval = guardSink(nativeEval);
931 | window.setTimeout = guardSink(nativeSetTimeout);
932 | window.setInterval = guardSink(nativeSetInterval);
933 | window.Function = function() {
934 | /**
935 | * Construct a new `Function()` instance.
936 | *
937 | * @return {function}.
938 | */
939 | var construct = function() {
940 | var fn = NativeFunction.apply(null, arguments);
941 | fn.constructor = Function;
942 | try {
943 | Object.setPrototypeOf(fn, Function);
944 | } catch (e) {
945 | fn.__proto__ = Function;
946 | }
947 | return fn;
948 | };
949 | if (isSafeArg.apply(null, arguments)) {
950 | return construct.apply(null, arguments);
951 | }
952 | return construct();
953 | };
954 | window.Function.prototype = Function;
955 | try {
956 | elPrototype = window.Element.prototype;
957 | nativeAppendChild = elPrototype.appendChild;
958 | nativeReplaceChild = elPrototype.replaceChild;
959 | nativeInsertBefore = elPrototype.insertBefore;
960 | nativeInsertAdjacentHTML = elPrototype.insertAdjacentHTML;
961 | nativeInsertAdjacentElement = elPrototype.insertAdjacentElement;
962 | innerHTML = getOwnPropertyDescriptor(elPrototype, 'innerHTML');
963 | outerHTML = getOwnPropertyDescriptor(elPrototype, 'outerHTML');
964 | elPrototype.appendChild = guardMethod(nativeAppendChild);
965 | elPrototype.replaceChild = guardMethod(nativeReplaceChild);
966 | elPrototype.insertBefore = guardMethod(nativeInsertBefore);
967 | elPrototype.insertAdjacentHTML = function(position, html) {
968 | if (!isSafeArg(html)) {
969 | html = toSafeStr(html);
970 | }
971 | return nativeInsertAdjacentHTML.call(this, position, html);
972 | };
973 | elPrototype.insertAdjacentElement = function(position, el) {
974 | if (isUnsafeNode(el)) {
975 | el = toSafeNode(el);
976 | }
977 | return nativeInsertAdjacentElement.call(this, position, el);
978 | };
979 | defineProperties(elPrototype, {
980 | 'innerHTML': genDescriptor(innerHTML),
981 | 'outerHTML': genDescriptor(outerHTML)
982 | });
983 | } catch (e) {}
984 | if (window.execScript) {
985 | nativeExecScript = window.execScript;
986 | // A nasty workaround to override `execScript()`.
987 | eval('var execScript;');
988 | window.execScript = guardSink(nativeExecScript);
989 | }
990 | if (window.setImmediate) {
991 | nativeSetImmediate = window.setImmediate;
992 | window.setImmediate = guardSink(nativeSetImmediate);
993 | }
994 |
995 | // Override `atob()` to sanitize tainted base64-encoded strings.
996 | if (window.atob) {
997 | nativeAtob = window.atob;
998 | window.atob = function(str) {
999 | if (isSafeArg(str)) {
1000 | return nativeAtob(str);
1001 | }
1002 | str = sanitize(nativeAtob(str));
1003 | return str;
1004 | };
1005 | }
1006 |
1007 | /* Guard `createContextualFragment()`. */
1008 | try {
1009 | Rprototype = window.Range.prototype;
1010 | nativeCreateContextualFragment = Rprototype.createContextualFragment;
1011 | Rprototype.createContextualFragment = function(tagStr) {
1012 | if (!isSafeArg(tagStr)) {
1013 | tagStr = '';
1014 | }
1015 | return nativeCreateContextualFragment.call(this, tagStr);
1016 | };
1017 | } catch (e) {}
1018 |
1019 | /* Monkey-patch storage sources. */
1020 | cookie = document.cookie;
1021 | cookieDesc = (function () {
1022 | try {
1023 | return getOwnPropertyDescriptor(document, 'cookie') ||
1024 | getOwnPropertyDescriptor(getPrototypeOf(document), 'cookie') ||
1025 | {
1026 | get: document.__lookupGetter__('cookie'),
1027 | set: document.__lookupSetter__('cookie')
1028 | };
1029 | } catch (e) {}
1030 | })();
1031 | defineProperties(document, {
1032 | 'cookie': {
1033 | get: function() {
1034 | try {
1035 | return cookieDesc.get.call(this);
1036 | } catch (e) {
1037 | return cookie;
1038 | }
1039 | },
1040 | set: function(val) {
1041 | if (isSafeArg(val)) {
1042 | try {
1043 | return cookieDesc.set.call(this, val);
1044 | } catch (e) {
1045 | cookie += ';' + val;
1046 | }
1047 | }
1048 | }
1049 | }
1050 | });
1051 | // Add cookie values to tainted strings
1052 | cookiePairs = cookie.split(';');
1053 | cookieIndex = cookiePairs.length;
1054 | while (cookieIndex--) {
1055 | cookiePair = cookiePairs[cookieIndex].split('=');
1056 | valIndex = cookiePair.length;
1057 | while (valIndex--) {
1058 | taintedStrings.push(cookiePair[valIndex]);
1059 | }
1060 | }
1061 | try {
1062 | if (window.localStorage) {
1063 | nativeLocalStorage = window.localStorage;
1064 | delete window.localStorage;
1065 | window.localStorage = guardStorage(nativeLocalStorage);
1066 | }
1067 | if (window.sessionStorage) {
1068 | nativeSessionStorage = window.sessionStorage;
1069 | delete window.sessionStorage;
1070 | window.sessionStorage = guardStorage(nativeSessionStorage);
1071 | }
1072 | } catch (e) {}
1073 | })(window, Object, Array);
1074 |
--------------------------------------------------------------------------------
/test/QUnit/qunit-git.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * QUnit 1.20.1-pre
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-01-10T19:20Z
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 */
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-testrunner-toolbar label {
56 | display: inline-block;
57 | padding: 0 0.5em 0 0.1em;
58 | }
59 |
60 | #qunit-banner {
61 | height: 5px;
62 | }
63 |
64 | #qunit-testrunner-toolbar {
65 | padding: 0.5em 1em 0.5em 1em;
66 | color: #5E740B;
67 | background-color: #EEE;
68 | overflow: hidden;
69 | }
70 |
71 | #qunit-filteredTest {
72 | padding: 0.5em 1em 0.5em 1em;
73 | background-color: #F4FF77;
74 | color: #366097;
75 | }
76 |
77 | #qunit-userAgent {
78 | padding: 0.5em 1em 0.5em 1em;
79 | background-color: #2B81AF;
80 | color: #FFF;
81 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
82 | }
83 |
84 | #qunit-modulefilter-container {
85 | float: right;
86 | padding: 0.2em;
87 | }
88 |
89 | .qunit-url-config {
90 | display: inline-block;
91 | padding: 0.1em;
92 | }
93 |
94 | .qunit-filter {
95 | display: block;
96 | float: right;
97 | margin-left: 1em;
98 | }
99 |
100 | /** Tests: Pass/Fail */
101 |
102 | #qunit-tests {
103 | list-style-position: inside;
104 | }
105 |
106 | #qunit-tests li {
107 | padding: 0.4em 1em 0.4em 1em;
108 | border-bottom: 1px solid #FFF;
109 | list-style-position: inside;
110 | }
111 |
112 | #qunit-tests > li {
113 | display: none;
114 | }
115 |
116 | #qunit-tests li.running,
117 | #qunit-tests li.pass,
118 | #qunit-tests li.fail,
119 | #qunit-tests li.skipped {
120 | display: list-item;
121 | }
122 |
123 | #qunit-tests.hidepass li.running,
124 | #qunit-tests.hidepass li.pass {
125 | visibility: hidden;
126 | position: absolute;
127 | width: 0;
128 | height: 0;
129 | padding: 0;
130 | border: 0;
131 | margin: 0;
132 | }
133 |
134 | #qunit-tests li strong {
135 | cursor: pointer;
136 | }
137 |
138 | #qunit-tests li.skipped strong {
139 | cursor: default;
140 | }
141 |
142 | #qunit-tests li a {
143 | padding: 0.5em;
144 | color: #C2CCD1;
145 | text-decoration: none;
146 | }
147 |
148 | #qunit-tests li p a {
149 | padding: 0.25em;
150 | color: #6B6464;
151 | }
152 | #qunit-tests li a:hover,
153 | #qunit-tests li a:focus {
154 | color: #000;
155 | }
156 |
157 | #qunit-tests li .runtime {
158 | float: right;
159 | font-size: smaller;
160 | }
161 |
162 | .qunit-assert-list {
163 | margin-top: 0.5em;
164 | padding: 0.5em;
165 |
166 | background-color: #FFF;
167 |
168 | border-radius: 5px;
169 | }
170 |
171 | .qunit-source {
172 | margin: 0.6em 0 0.3em;
173 | }
174 |
175 | .qunit-collapsed {
176 | display: none;
177 | }
178 |
179 | #qunit-tests table {
180 | border-collapse: collapse;
181 | margin-top: 0.2em;
182 | }
183 |
184 | #qunit-tests th {
185 | text-align: right;
186 | vertical-align: top;
187 | padding: 0 0.5em 0 0;
188 | }
189 |
190 | #qunit-tests td {
191 | vertical-align: top;
192 | }
193 |
194 | #qunit-tests pre {
195 | margin: 0;
196 | white-space: pre-wrap;
197 | word-wrap: break-word;
198 | }
199 |
200 | #qunit-tests del {
201 | background-color: #E0F2BE;
202 | color: #374E0C;
203 | text-decoration: none;
204 | }
205 |
206 | #qunit-tests ins {
207 | background-color: #FFCACA;
208 | color: #500;
209 | text-decoration: none;
210 | }
211 |
212 | /*** Test Counts */
213 |
214 | #qunit-tests b.counts { color: #000; }
215 | #qunit-tests b.passed { color: #5E740B; }
216 | #qunit-tests b.failed { color: #710909; }
217 |
218 | #qunit-tests li li {
219 | padding: 5px;
220 | background-color: #FFF;
221 | border-bottom: none;
222 | list-style-position: inside;
223 | }
224 |
225 | /*** Passing Styles */
226 |
227 | #qunit-tests li li.pass {
228 | color: #3C510C;
229 | background-color: #FFF;
230 | border-left: 10px solid #C6E746;
231 | }
232 |
233 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
234 | #qunit-tests .pass .test-name { color: #366097; }
235 |
236 | #qunit-tests .pass .test-actual,
237 | #qunit-tests .pass .test-expected { color: #999; }
238 |
239 | #qunit-banner.qunit-pass { background-color: #C6E746; }
240 |
241 | /*** Failing Styles */
242 |
243 | #qunit-tests li li.fail {
244 | color: #710909;
245 | background-color: #FFF;
246 | border-left: 10px solid #EE5757;
247 | white-space: pre;
248 | }
249 |
250 | #qunit-tests > li:last-child {
251 | border-radius: 0 0 5px 5px;
252 | }
253 |
254 | #qunit-tests .fail { color: #000; background-color: #EE5757; }
255 | #qunit-tests .fail .test-name,
256 | #qunit-tests .fail .module-name { color: #000; }
257 |
258 | #qunit-tests .fail .test-actual { color: #EE5757; }
259 | #qunit-tests .fail .test-expected { color: #008000; }
260 |
261 | #qunit-banner.qunit-fail { background-color: #EE5757; }
262 |
263 | /*** Skipped tests */
264 |
265 | #qunit-tests .skipped {
266 | background-color: #EBECE9;
267 | }
268 |
269 | #qunit-tests .qunit-skipped-label {
270 | background-color: #F4FF77;
271 | display: inline-block;
272 | font-style: normal;
273 | color: #366097;
274 | line-height: 1.8em;
275 | padding: 0 0.5em;
276 | margin: -0.4em 0.4em -0.4em 0;
277 | }
278 |
279 | /** Result */
280 |
281 | #qunit-testresult {
282 | padding: 0.5em 1em 0.5em 1em;
283 |
284 | color: #2B81AF;
285 | background-color: #D2E0E6;
286 |
287 | border-bottom: 1px solid #FFF;
288 | }
289 | #qunit-testresult .module-name {
290 | font-weight: 700;
291 | }
292 |
293 | /** Fixture */
294 |
295 | #qunit-fixture {
296 | position: absolute;
297 | top: -10000px;
298 | left: -10000px;
299 | width: 1000px;
300 | height: 1000px;
301 | }
302 |
--------------------------------------------------------------------------------
/test/QUnit/qunit-git.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * QUnit 1.20.1-pre
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-01-10T19:20Z
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 toString = Object.prototype.toString,
47 | hasOwn = Object.prototype.hasOwnProperty;
48 |
49 | // returns a new Array with the elements that are in a but not in b
50 | function diff( a, b ) {
51 | var i, j,
52 | result = a.slice();
53 |
54 | for ( i = 0; i < result.length; i++ ) {
55 | for ( j = 0; j < b.length; j++ ) {
56 | if ( result[ i ] === b[ j ] ) {
57 | result.splice( i, 1 );
58 | i--;
59 | break;
60 | }
61 | }
62 | }
63 | return result;
64 | }
65 |
66 | // from jquery.js
67 | function inArray( elem, array ) {
68 | if ( array.indexOf ) {
69 | return array.indexOf( elem );
70 | }
71 |
72 | for ( var i = 0, length = array.length; i < length; i++ ) {
73 | if ( array[ i ] === elem ) {
74 | return i;
75 | }
76 | }
77 |
78 | return -1;
79 | }
80 |
81 | /**
82 | * Makes a clone of an object using only Array or Object as base,
83 | * and copies over the own enumerable properties.
84 | *
85 | * @param {Object} obj
86 | * @return {Object} New object with only the own properties (recursively).
87 | */
88 | function objectValues ( obj ) {
89 | var key, val,
90 | vals = QUnit.is( "array", obj ) ? [] : {};
91 | for ( key in obj ) {
92 | if ( hasOwn.call( obj, key ) ) {
93 | val = obj[ key ];
94 | vals[ key ] = val === Object( val ) ? objectValues( val ) : val;
95 | }
96 | }
97 | return vals;
98 | }
99 |
100 | function extend( a, b, undefOnly ) {
101 | for ( var prop in b ) {
102 | if ( hasOwn.call( b, prop ) ) {
103 |
104 | // Avoid "Member not found" error in IE8 caused by messing with window.constructor
105 | // This block runs on every environment, so `global` is being used instead of `window`
106 | // to avoid errors on node.
107 | if ( prop !== "constructor" || a !== global ) {
108 | if ( b[ prop ] === undefined ) {
109 | delete a[ prop ];
110 | } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) {
111 | a[ prop ] = b[ prop ];
112 | }
113 | }
114 | }
115 | }
116 |
117 | return a;
118 | }
119 |
120 | function objectType( obj ) {
121 | if ( typeof obj === "undefined" ) {
122 | return "undefined";
123 | }
124 |
125 | // Consider: typeof null === object
126 | if ( obj === null ) {
127 | return "null";
128 | }
129 |
130 | var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ),
131 | type = match && match[ 1 ];
132 |
133 | switch ( type ) {
134 | case "Number":
135 | if ( isNaN( obj ) ) {
136 | return "nan";
137 | }
138 | return "number";
139 | case "String":
140 | case "Boolean":
141 | case "Array":
142 | case "Set":
143 | case "Map":
144 | case "Date":
145 | case "RegExp":
146 | case "Function":
147 | case "Symbol":
148 | return type.toLowerCase();
149 | }
150 | if ( typeof obj === "object" ) {
151 | return "object";
152 | }
153 | }
154 |
155 | // Safe object type checking
156 | function is( type, obj ) {
157 | return QUnit.objectType( obj ) === type;
158 | }
159 |
160 | var getUrlParams = function() {
161 | var i, current;
162 | var urlParams = {};
163 | var location = window.location;
164 | var params = location.search.slice( 1 ).split( "&" );
165 | var length = params.length;
166 |
167 | if ( params[ 0 ] ) {
168 | for ( i = 0; i < length; i++ ) {
169 | current = params[ i ].split( "=" );
170 | current[ 0 ] = decodeURIComponent( current[ 0 ] );
171 |
172 | // allow just a key to turn on a flag, e.g., test.html?noglobals
173 | current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true;
174 | if ( urlParams[ current[ 0 ] ] ) {
175 | urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] );
176 | } else {
177 | urlParams[ current[ 0 ] ] = current[ 1 ];
178 | }
179 | }
180 | }
181 |
182 | return urlParams;
183 | };
184 |
185 | // Doesn't support IE6 to IE9, it will return undefined on these browsers
186 | // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack
187 | function extractStacktrace( e, offset ) {
188 | offset = offset === undefined ? 4 : offset;
189 |
190 | var stack, include, i;
191 |
192 | if ( e.stack ) {
193 | stack = e.stack.split( "\n" );
194 | if ( /^error$/i.test( stack[ 0 ] ) ) {
195 | stack.shift();
196 | }
197 | if ( fileName ) {
198 | include = [];
199 | for ( i = offset; i < stack.length; i++ ) {
200 | if ( stack[ i ].indexOf( fileName ) !== -1 ) {
201 | break;
202 | }
203 | include.push( stack[ i ] );
204 | }
205 | if ( include.length ) {
206 | return include.join( "\n" );
207 | }
208 | }
209 | return stack[ offset ];
210 |
211 | // Support: Safari <=6 only
212 | } else if ( e.sourceURL ) {
213 |
214 | // exclude useless self-reference for generated Error objects
215 | if ( /qunit.js$/.test( e.sourceURL ) ) {
216 | return;
217 | }
218 |
219 | // for actual exceptions, this is useful
220 | return e.sourceURL + ":" + e.line;
221 | }
222 | }
223 |
224 | function sourceFromStacktrace( offset ) {
225 | var error = new Error();
226 |
227 | // Support: Safari <=7 only, IE <=10 - 11 only
228 | // Not all browsers generate the `stack` property for `new Error()`, see also #636
229 | if ( !error.stack ) {
230 | try {
231 | throw error;
232 | } catch ( err ) {
233 | error = err;
234 | }
235 | }
236 |
237 | return extractStacktrace( error, offset );
238 | }
239 |
240 | /**
241 | * Config object: Maintain internal state
242 | * Later exposed as QUnit.config
243 | * `config` initialized at top of scope
244 | */
245 | var config = {
246 | // The queue of tests to run
247 | queue: [],
248 |
249 | // block until document ready
250 | blocking: true,
251 |
252 | // by default, run previously failed tests first
253 | // very useful in combination with "Hide passed tests" checked
254 | reorder: true,
255 |
256 | // by default, modify document.title when suite is done
257 | altertitle: true,
258 |
259 | // HTML Reporter: collapse every test except the first failing test
260 | // If false, all failing tests will be expanded
261 | collapse: true,
262 |
263 | // by default, scroll to top of the page when suite is done
264 | scrolltop: true,
265 |
266 | // depth up-to which object will be dumped
267 | maxDepth: 5,
268 |
269 | // when enabled, all tests must call expect()
270 | requireExpects: false,
271 |
272 | // add checkboxes that are persisted in the query-string
273 | // when enabled, the id is set to `true` as a `QUnit.config` property
274 | urlConfig: [
275 | {
276 | id: "hidepassed",
277 | label: "Hide passed tests",
278 | tooltip: "Only show tests and assertions that fail. Stored as query-strings."
279 | },
280 | {
281 | id: "noglobals",
282 | label: "Check for Globals",
283 | tooltip: "Enabling this will test if any test introduces new properties on the " +
284 | "global object (`window` in Browsers). Stored as query-strings."
285 | },
286 | {
287 | id: "notrycatch",
288 | label: "No try-catch",
289 | tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " +
290 | "exceptions in IE reasonable. Stored as query-strings."
291 | }
292 | ],
293 |
294 | // Set of all modules.
295 | modules: [],
296 |
297 | // Stack of nested modules
298 | moduleStack: [],
299 |
300 | // The first unnamed module
301 | currentModule: {
302 | name: "",
303 | tests: []
304 | },
305 |
306 | callbacks: {}
307 | };
308 |
309 | var urlParams = defined.document ? getUrlParams() : {};
310 |
311 | // Push a loose unnamed module to the modules collection
312 | config.modules.push( config.currentModule );
313 |
314 | if ( urlParams.filter === true ) {
315 | delete urlParams.filter;
316 | }
317 |
318 | // String search anywhere in moduleName+testName
319 | config.filter = urlParams.filter;
320 |
321 | config.testId = [];
322 | if ( urlParams.testId ) {
323 | // Ensure that urlParams.testId is an array
324 | urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," );
325 | for (var i = 0; i < urlParams.testId.length; i++ ) {
326 | config.testId.push( urlParams.testId[ i ] );
327 | }
328 | }
329 |
330 | var loggingCallbacks = {};
331 |
332 | // Register logging callbacks
333 | function registerLoggingCallbacks( obj ) {
334 | var i, l, key,
335 | callbackNames = [ "begin", "done", "log", "testStart", "testDone",
336 | "moduleStart", "moduleDone" ];
337 |
338 | function registerLoggingCallback( key ) {
339 | var loggingCallback = function( callback ) {
340 | if ( objectType( callback ) !== "function" ) {
341 | throw new Error(
342 | "QUnit logging methods require a callback function as their first parameters."
343 | );
344 | }
345 |
346 | config.callbacks[ key ].push( callback );
347 | };
348 |
349 | // DEPRECATED: This will be removed on QUnit 2.0.0+
350 | // Stores the registered functions allowing restoring
351 | // at verifyLoggingCallbacks() if modified
352 | loggingCallbacks[ key ] = loggingCallback;
353 |
354 | return loggingCallback;
355 | }
356 |
357 | for ( i = 0, l = callbackNames.length; i < l; i++ ) {
358 | key = callbackNames[ i ];
359 |
360 | // Initialize key collection of logging callback
361 | if ( objectType( config.callbacks[ key ] ) === "undefined" ) {
362 | config.callbacks[ key ] = [];
363 | }
364 |
365 | obj[ key ] = registerLoggingCallback( key );
366 | }
367 | }
368 |
369 | function runLoggingCallbacks( key, args ) {
370 | var i, l, callbacks;
371 |
372 | callbacks = config.callbacks[ key ];
373 | for ( i = 0, l = callbacks.length; i < l; i++ ) {
374 | callbacks[ i ]( args );
375 | }
376 | }
377 |
378 | // DEPRECATED: This will be removed on 2.0.0+
379 | // This function verifies if the loggingCallbacks were modified by the user
380 | // If so, it will restore it, assign the given callback and print a console warning
381 | function verifyLoggingCallbacks() {
382 | var loggingCallback, userCallback;
383 |
384 | for ( loggingCallback in loggingCallbacks ) {
385 | if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) {
386 |
387 | userCallback = QUnit[ loggingCallback ];
388 |
389 | // Restore the callback function
390 | QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ];
391 |
392 | // Assign the deprecated given callback
393 | QUnit[ loggingCallback ]( userCallback );
394 |
395 | if ( global.console && global.console.warn ) {
396 | global.console.warn(
397 | "QUnit." + loggingCallback + " was replaced with a new value.\n" +
398 | "Please, check out the documentation on how to apply logging callbacks.\n" +
399 | "Reference: https://api.qunitjs.com/category/callbacks/"
400 | );
401 | }
402 | }
403 | }
404 | }
405 |
406 | ( function() {
407 | if ( !defined.document ) {
408 | return;
409 | }
410 |
411 | // `onErrorFnPrev` initialized at top of scope
412 | // Preserve other handlers
413 | var onErrorFnPrev = window.onerror;
414 |
415 | // Cover uncaught exceptions
416 | // Returning true will suppress the default browser handler,
417 | // returning false will let it run.
418 | window.onerror = function( error, filePath, linerNr ) {
419 | var ret = false;
420 | if ( onErrorFnPrev ) {
421 | ret = onErrorFnPrev( error, filePath, linerNr );
422 | }
423 |
424 | // Treat return value as window.onerror itself does,
425 | // Only do our handling if not suppressed.
426 | if ( ret !== true ) {
427 | if ( QUnit.config.current ) {
428 | if ( QUnit.config.current.ignoreGlobalErrors ) {
429 | return true;
430 | }
431 | QUnit.pushFailure( error, filePath + ":" + linerNr );
432 | } else {
433 | QUnit.test( "global failure", extend(function() {
434 | QUnit.pushFailure( error, filePath + ":" + linerNr );
435 | }, { validTest: true } ) );
436 | }
437 | return false;
438 | }
439 |
440 | return ret;
441 | };
442 | } )();
443 |
444 | QUnit.urlParams = urlParams;
445 |
446 | // Figure out if we're running the tests from a server or not
447 | QUnit.isLocal = !( defined.document && window.location.protocol !== "file:" );
448 |
449 | // Expose the current QUnit version
450 | QUnit.version = "1.20.1-pre";
451 |
452 | extend( QUnit, {
453 |
454 | // call on start of module test to prepend name to all tests
455 | module: function( name, testEnvironment, executeNow ) {
456 | var module, moduleFns;
457 | var currentModule = config.currentModule;
458 |
459 | if ( arguments.length === 2 ) {
460 | if ( testEnvironment instanceof Function ) {
461 | executeNow = testEnvironment;
462 | testEnvironment = undefined;
463 | }
464 | }
465 |
466 | // DEPRECATED: handles setup/teardown functions,
467 | // beforeEach and afterEach should be used instead
468 | if ( testEnvironment && testEnvironment.setup ) {
469 | testEnvironment.beforeEach = testEnvironment.setup;
470 | delete testEnvironment.setup;
471 | }
472 | if ( testEnvironment && testEnvironment.teardown ) {
473 | testEnvironment.afterEach = testEnvironment.teardown;
474 | delete testEnvironment.teardown;
475 | }
476 |
477 | module = createModule();
478 |
479 | moduleFns = {
480 | beforeEach: setHook( module, "beforeEach" ),
481 | afterEach: setHook( module, "afterEach" )
482 | };
483 |
484 | if ( executeNow instanceof Function ) {
485 | config.moduleStack.push( module );
486 | setCurrentModule( module );
487 | executeNow.call( module.testEnvironment, moduleFns );
488 | config.moduleStack.pop();
489 | module = module.parentModule || currentModule;
490 | }
491 |
492 | setCurrentModule( module );
493 |
494 | function createModule() {
495 | var parentModule = config.moduleStack.length ?
496 | config.moduleStack.slice( -1 )[ 0 ] : null;
497 | var moduleName = parentModule !== null ?
498 | [ parentModule.name, name ].join( " > " ) : name;
499 | var module = {
500 | name: moduleName,
501 | parentModule: parentModule,
502 | tests: []
503 | };
504 |
505 | var env = {};
506 | if ( parentModule ) {
507 | extend( env, parentModule.testEnvironment );
508 | delete env.beforeEach;
509 | delete env.afterEach;
510 | }
511 | extend( env, testEnvironment );
512 | module.testEnvironment = env;
513 |
514 | config.modules.push( module );
515 | return module;
516 | }
517 |
518 | function setCurrentModule( module ) {
519 | config.currentModule = module;
520 | }
521 |
522 | },
523 |
524 | // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0.
525 | asyncTest: asyncTest,
526 |
527 | test: test,
528 |
529 | skip: skip,
530 |
531 | only: only,
532 |
533 | // DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0.
534 | // In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior.
535 | start: function( count ) {
536 | var globalStartAlreadyCalled = globalStartCalled;
537 |
538 | if ( !config.current ) {
539 | globalStartCalled = true;
540 |
541 | if ( runStarted ) {
542 | throw new Error( "Called start() outside of a test context while already started" );
543 | } else if ( globalStartAlreadyCalled || count > 1 ) {
544 | throw new Error( "Called start() outside of a test context too many times" );
545 | } else if ( config.autostart ) {
546 | throw new Error( "Called start() outside of a test context when " +
547 | "QUnit.config.autostart was true" );
548 | } else if ( !config.pageLoaded ) {
549 |
550 | // The page isn't completely loaded yet, so bail out and let `QUnit.load` handle it
551 | config.autostart = true;
552 | return;
553 | }
554 | } else {
555 |
556 | // If a test is running, adjust its semaphore
557 | config.current.semaphore -= count || 1;
558 |
559 | // If semaphore is non-numeric, throw error
560 | if ( isNaN( config.current.semaphore ) ) {
561 | config.current.semaphore = 0;
562 |
563 | QUnit.pushFailure(
564 | "Called start() with a non-numeric decrement.",
565 | sourceFromStacktrace( 2 )
566 | );
567 | return;
568 | }
569 |
570 | // Don't start until equal number of stop-calls
571 | if ( config.current.semaphore > 0 ) {
572 | return;
573 | }
574 |
575 | // throw an Error if start is called more often than stop
576 | if ( config.current.semaphore < 0 ) {
577 | config.current.semaphore = 0;
578 |
579 | QUnit.pushFailure(
580 | "Called start() while already started (test's semaphore was 0 already)",
581 | sourceFromStacktrace( 2 )
582 | );
583 | return;
584 | }
585 | }
586 |
587 | resumeProcessing();
588 | },
589 |
590 | // DEPRECATED: QUnit.stop() will be removed in QUnit 2.0.
591 | stop: function( count ) {
592 |
593 | // If there isn't a test running, don't allow QUnit.stop() to be called
594 | if ( !config.current ) {
595 | throw new Error( "Called stop() outside of a test context" );
596 | }
597 |
598 | // If a test is running, adjust its semaphore
599 | config.current.semaphore += count || 1;
600 |
601 | pauseProcessing();
602 | },
603 |
604 | config: config,
605 |
606 | is: is,
607 |
608 | objectType: objectType,
609 |
610 | extend: extend,
611 |
612 | load: function() {
613 | config.pageLoaded = true;
614 |
615 | // Initialize the configuration options
616 | extend( config, {
617 | stats: { all: 0, bad: 0 },
618 | moduleStats: { all: 0, bad: 0 },
619 | started: 0,
620 | updateRate: 1000,
621 | autostart: true,
622 | filter: ""
623 | }, true );
624 |
625 | config.blocking = false;
626 |
627 | if ( config.autostart ) {
628 | resumeProcessing();
629 | }
630 | },
631 |
632 | stack: function( offset ) {
633 | offset = ( offset || 0 ) + 2;
634 | return sourceFromStacktrace( offset );
635 | }
636 | });
637 |
638 | registerLoggingCallbacks( QUnit );
639 |
640 | function begin() {
641 | var i, l,
642 | modulesLog = [];
643 |
644 | // If the test run hasn't officially begun yet
645 | if ( !config.started ) {
646 |
647 | // Record the time of the test run's beginning
648 | config.started = now();
649 |
650 | verifyLoggingCallbacks();
651 |
652 | // Delete the loose unnamed module if unused.
653 | if ( config.modules[ 0 ].name === "" && config.modules[ 0 ].tests.length === 0 ) {
654 | config.modules.shift();
655 | }
656 |
657 | // Avoid unnecessary information by not logging modules' test environments
658 | for ( i = 0, l = config.modules.length; i < l; i++ ) {
659 | modulesLog.push({
660 | name: config.modules[ i ].name,
661 | tests: config.modules[ i ].tests
662 | });
663 | }
664 |
665 | // The test run is officially beginning now
666 | runLoggingCallbacks( "begin", {
667 | totalTests: Test.count,
668 | modules: modulesLog
669 | });
670 | }
671 |
672 | config.blocking = false;
673 | process( true );
674 | }
675 |
676 | function process( last ) {
677 | function next() {
678 | process( last );
679 | }
680 | var start = now();
681 | config.depth = ( config.depth || 0 ) + 1;
682 |
683 | while ( config.queue.length && !config.blocking ) {
684 | if ( !defined.setTimeout || config.updateRate <= 0 ||
685 | ( ( now() - start ) < config.updateRate ) ) {
686 | if ( config.current ) {
687 |
688 | // Reset async tracking for each phase of the Test lifecycle
689 | config.current.usedAsync = false;
690 | }
691 | config.queue.shift()();
692 | } else {
693 | setTimeout( next, 13 );
694 | break;
695 | }
696 | }
697 | config.depth--;
698 | if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) {
699 | done();
700 | }
701 | }
702 |
703 | function pauseProcessing() {
704 | config.blocking = true;
705 |
706 | if ( config.testTimeout && defined.setTimeout ) {
707 | clearTimeout( config.timeout );
708 | config.timeout = setTimeout(function() {
709 | if ( config.current ) {
710 | config.current.semaphore = 0;
711 | QUnit.pushFailure( "Test timed out", sourceFromStacktrace( 2 ) );
712 | } else {
713 | throw new Error( "Test timed out" );
714 | }
715 | resumeProcessing();
716 | }, config.testTimeout );
717 | }
718 | }
719 |
720 | function resumeProcessing() {
721 | runStarted = true;
722 |
723 | // A slight delay to allow this iteration of the event loop to finish (more assertions, etc.)
724 | if ( defined.setTimeout ) {
725 | setTimeout(function() {
726 | if ( config.current && config.current.semaphore > 0 ) {
727 | return;
728 | }
729 | if ( config.timeout ) {
730 | clearTimeout( config.timeout );
731 | }
732 |
733 | begin();
734 | }, 13 );
735 | } else {
736 | begin();
737 | }
738 | }
739 |
740 | function done() {
741 | var runtime, passed;
742 |
743 | config.autorun = true;
744 |
745 | // Log the last module results
746 | if ( config.previousModule ) {
747 | runLoggingCallbacks( "moduleDone", {
748 | name: config.previousModule.name,
749 | tests: config.previousModule.tests,
750 | failed: config.moduleStats.bad,
751 | passed: config.moduleStats.all - config.moduleStats.bad,
752 | total: config.moduleStats.all,
753 | runtime: now() - config.moduleStats.started
754 | });
755 | }
756 | delete config.previousModule;
757 |
758 | runtime = now() - config.started;
759 | passed = config.stats.all - config.stats.bad;
760 |
761 | runLoggingCallbacks( "done", {
762 | failed: config.stats.bad,
763 | passed: passed,
764 | total: config.stats.all,
765 | runtime: runtime
766 | });
767 | }
768 |
769 | function setHook( module, hookName ) {
770 | if ( module.testEnvironment === undefined ) {
771 | module.testEnvironment = {};
772 | }
773 |
774 | return function( callback ) {
775 | module.testEnvironment[ hookName ] = callback;
776 | };
777 | }
778 |
779 | var focused = false;
780 | var priorityCount = 0;
781 |
782 | function Test( settings ) {
783 | var i, l;
784 |
785 | ++Test.count;
786 |
787 | extend( this, settings );
788 | this.assertions = [];
789 | this.semaphore = 0;
790 | this.usedAsync = false;
791 | this.module = config.currentModule;
792 | this.stack = sourceFromStacktrace( 3 );
793 |
794 | // Register unique strings
795 | for ( i = 0, l = this.module.tests; i < l.length; i++ ) {
796 | if ( this.module.tests[ i ].name === this.testName ) {
797 | this.testName += " ";
798 | }
799 | }
800 |
801 | this.testId = generateHash( this.module.name, this.testName );
802 |
803 | this.module.tests.push({
804 | name: this.testName,
805 | testId: this.testId
806 | });
807 |
808 | if ( settings.skip ) {
809 |
810 | // Skipped tests will fully ignore any sent callback
811 | this.callback = function() {};
812 | this.async = false;
813 | this.expected = 0;
814 | } else {
815 | this.assert = new Assert( this );
816 | }
817 | }
818 |
819 | Test.count = 0;
820 |
821 | Test.prototype = {
822 | before: function() {
823 | if (
824 |
825 | // Emit moduleStart when we're switching from one module to another
826 | this.module !== config.previousModule ||
827 |
828 | // They could be equal (both undefined) but if the previousModule property doesn't
829 | // yet exist it means this is the first test in a suite that isn't wrapped in a
830 | // module, in which case we'll just emit a moduleStart event for 'undefined'.
831 | // Without this, reporters can get testStart before moduleStart which is a problem.
832 | !hasOwn.call( config, "previousModule" )
833 | ) {
834 | if ( hasOwn.call( config, "previousModule" ) ) {
835 | runLoggingCallbacks( "moduleDone", {
836 | name: config.previousModule.name,
837 | tests: config.previousModule.tests,
838 | failed: config.moduleStats.bad,
839 | passed: config.moduleStats.all - config.moduleStats.bad,
840 | total: config.moduleStats.all,
841 | runtime: now() - config.moduleStats.started
842 | });
843 | }
844 | config.previousModule = this.module;
845 | config.moduleStats = { all: 0, bad: 0, started: now() };
846 | runLoggingCallbacks( "moduleStart", {
847 | name: this.module.name,
848 | tests: this.module.tests
849 | });
850 | }
851 |
852 | config.current = this;
853 |
854 | if ( this.module.testEnvironment ) {
855 | delete this.module.testEnvironment.beforeEach;
856 | delete this.module.testEnvironment.afterEach;
857 | }
858 | this.testEnvironment = extend( {}, this.module.testEnvironment );
859 |
860 | this.started = now();
861 | runLoggingCallbacks( "testStart", {
862 | name: this.testName,
863 | module: this.module.name,
864 | testId: this.testId
865 | });
866 |
867 | if ( !config.pollution ) {
868 | saveGlobal();
869 | }
870 | },
871 |
872 | run: function() {
873 | var promise;
874 |
875 | config.current = this;
876 |
877 | if ( this.async ) {
878 | QUnit.stop();
879 | }
880 |
881 | this.callbackStarted = now();
882 |
883 | if ( config.notrycatch ) {
884 | runTest( this );
885 | return;
886 | }
887 |
888 | try {
889 | runTest( this );
890 | } catch ( e ) {
891 | this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " +
892 | this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) );
893 |
894 | // else next test will carry the responsibility
895 | saveGlobal();
896 |
897 | // Restart the tests if they're blocking
898 | if ( config.blocking ) {
899 | QUnit.start();
900 | }
901 | }
902 |
903 | function runTest( test ) {
904 | promise = test.callback.call( test.testEnvironment, test.assert );
905 | test.resolvePromise( promise );
906 | }
907 | },
908 |
909 | after: function() {
910 | checkPollution();
911 | },
912 |
913 | queueHook: function( hook, hookName ) {
914 | var promise,
915 | test = this;
916 | return function runHook() {
917 | config.current = test;
918 | if ( config.notrycatch ) {
919 | callHook();
920 | return;
921 | }
922 | try {
923 | callHook();
924 | } catch ( error ) {
925 | test.pushFailure( hookName + " failed on " + test.testName + ": " +
926 | ( error.message || error ), extractStacktrace( error, 0 ) );
927 | }
928 |
929 | function callHook() {
930 | promise = hook.call( test.testEnvironment, test.assert );
931 | test.resolvePromise( promise, hookName );
932 | }
933 | };
934 | },
935 |
936 | // Currently only used for module level hooks, can be used to add global level ones
937 | hooks: function( handler ) {
938 | var hooks = [];
939 |
940 | function processHooks( test, module ) {
941 | if ( module.parentModule ) {
942 | processHooks( test, module.parentModule );
943 | }
944 | if ( module.testEnvironment &&
945 | QUnit.objectType( module.testEnvironment[ handler ] ) === "function" ) {
946 | hooks.push( test.queueHook( module.testEnvironment[ handler ], handler ) );
947 | }
948 | }
949 |
950 | // Hooks are ignored on skipped tests
951 | if ( !this.skip ) {
952 | processHooks( this, this.module );
953 | }
954 | return hooks;
955 | },
956 |
957 | finish: function() {
958 | config.current = this;
959 | if ( config.requireExpects && this.expected === null ) {
960 | this.pushFailure( "Expected number of assertions to be defined, but expect() was " +
961 | "not called.", this.stack );
962 | } else if ( this.expected !== null && this.expected !== this.assertions.length ) {
963 | this.pushFailure( "Expected " + this.expected + " assertions, but " +
964 | this.assertions.length + " were run", this.stack );
965 | } else if ( this.expected === null && !this.assertions.length ) {
966 | this.pushFailure( "Expected at least one assertion, but none were run - call " +
967 | "expect(0) to accept zero assertions.", this.stack );
968 | }
969 |
970 | var i,
971 | bad = 0;
972 |
973 | this.runtime = now() - this.started;
974 | config.stats.all += this.assertions.length;
975 | config.moduleStats.all += this.assertions.length;
976 |
977 | for ( i = 0; i < this.assertions.length; i++ ) {
978 | if ( !this.assertions[ i ].result ) {
979 | bad++;
980 | config.stats.bad++;
981 | config.moduleStats.bad++;
982 | }
983 | }
984 |
985 | runLoggingCallbacks( "testDone", {
986 | name: this.testName,
987 | module: this.module.name,
988 | skipped: !!this.skip,
989 | failed: bad,
990 | passed: this.assertions.length - bad,
991 | total: this.assertions.length,
992 | runtime: this.runtime,
993 |
994 | // HTML Reporter use
995 | assertions: this.assertions,
996 | testId: this.testId,
997 |
998 | // Source of Test
999 | source: this.stack,
1000 |
1001 | // DEPRECATED: this property will be removed in 2.0.0, use runtime instead
1002 | duration: this.runtime
1003 | });
1004 |
1005 | // QUnit.reset() is deprecated and will be replaced for a new
1006 | // fixture reset function on QUnit 2.0/2.1.
1007 | // It's still called here for backwards compatibility handling
1008 | QUnit.reset();
1009 |
1010 | config.current = undefined;
1011 | },
1012 |
1013 | queue: function() {
1014 | var priority,
1015 | test = this;
1016 |
1017 | if ( !this.valid() ) {
1018 | return;
1019 | }
1020 |
1021 | function run() {
1022 |
1023 | // each of these can by async
1024 | synchronize([
1025 | function() {
1026 | test.before();
1027 | },
1028 |
1029 | test.hooks( "beforeEach" ),
1030 | function() {
1031 | test.run();
1032 | },
1033 |
1034 | test.hooks( "afterEach" ).reverse(),
1035 |
1036 | function() {
1037 | test.after();
1038 | },
1039 | function() {
1040 | test.finish();
1041 | }
1042 | ]);
1043 | }
1044 |
1045 | // Prioritize previously failed tests, detected from sessionStorage
1046 | priority = QUnit.config.reorder && defined.sessionStorage &&
1047 | +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName );
1048 |
1049 | return synchronize( run, priority );
1050 | },
1051 |
1052 | push: function( result, actual, expected, message, negative ) {
1053 | var source,
1054 | details = {
1055 | module: this.module.name,
1056 | name: this.testName,
1057 | result: result,
1058 | message: message,
1059 | actual: actual,
1060 | expected: expected,
1061 | testId: this.testId,
1062 | negative: negative || false,
1063 | runtime: now() - this.started
1064 | };
1065 |
1066 | if ( !result ) {
1067 | source = sourceFromStacktrace();
1068 |
1069 | if ( source ) {
1070 | details.source = source;
1071 | }
1072 | }
1073 |
1074 | runLoggingCallbacks( "log", details );
1075 |
1076 | this.assertions.push({
1077 | result: !!result,
1078 | message: message
1079 | });
1080 | },
1081 |
1082 | pushFailure: function( message, source, actual ) {
1083 | if ( !( this instanceof Test ) ) {
1084 | throw new Error( "pushFailure() assertion outside test context, was " +
1085 | sourceFromStacktrace( 2 ) );
1086 | }
1087 |
1088 | var details = {
1089 | module: this.module.name,
1090 | name: this.testName,
1091 | result: false,
1092 | message: message || "error",
1093 | actual: actual || null,
1094 | testId: this.testId,
1095 | runtime: now() - this.started
1096 | };
1097 |
1098 | if ( source ) {
1099 | details.source = source;
1100 | }
1101 |
1102 | runLoggingCallbacks( "log", details );
1103 |
1104 | this.assertions.push({
1105 | result: false,
1106 | message: message
1107 | });
1108 | },
1109 |
1110 | resolvePromise: function( promise, phase ) {
1111 | var then, message,
1112 | test = this;
1113 | if ( promise != null ) {
1114 | then = promise.then;
1115 | if ( QUnit.objectType( then ) === "function" ) {
1116 | QUnit.stop();
1117 | then.call(
1118 | promise,
1119 | function() { QUnit.start(); },
1120 | function( error ) {
1121 | message = "Promise rejected " +
1122 | ( !phase ? "during" : phase.replace( /Each$/, "" ) ) +
1123 | " " + test.testName + ": " + ( error.message || error );
1124 | test.pushFailure( message, extractStacktrace( error, 0 ) );
1125 |
1126 | // else next test will carry the responsibility
1127 | saveGlobal();
1128 |
1129 | // Unblock
1130 | QUnit.start();
1131 | }
1132 | );
1133 | }
1134 | }
1135 | },
1136 |
1137 | valid: function() {
1138 | var filter = config.filter,
1139 | regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec( filter ),
1140 | module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(),
1141 | fullName = ( this.module.name + ": " + this.testName );
1142 |
1143 | function testInModuleChain( testModule ) {
1144 | var testModuleName = testModule.name ? testModule.name.toLowerCase() : null;
1145 | if ( testModuleName === module ) {
1146 | return true;
1147 | } else if ( testModule.parentModule ) {
1148 | return testInModuleChain( testModule.parentModule );
1149 | } else {
1150 | return false;
1151 | }
1152 | }
1153 |
1154 | // Internally-generated tests are always valid
1155 | if ( this.callback && this.callback.validTest ) {
1156 | return true;
1157 | }
1158 |
1159 | if ( config.testId.length > 0 && inArray( this.testId, config.testId ) < 0 ) {
1160 | return false;
1161 | }
1162 |
1163 | if ( module && !testInModuleChain( this.module ) ) {
1164 | return false;
1165 | }
1166 |
1167 | if ( !filter ) {
1168 | return true;
1169 | }
1170 |
1171 | return regexFilter ?
1172 | this.regexFilter( !!regexFilter[1], regexFilter[2], regexFilter[3], fullName ) :
1173 | this.stringFilter( filter, fullName );
1174 | },
1175 |
1176 | regexFilter: function( exclude, pattern, flags, fullName ) {
1177 | var regex = new RegExp( pattern, flags );
1178 | var match = regex.test( fullName );
1179 |
1180 | return match !== exclude;
1181 | },
1182 |
1183 | stringFilter: function( filter, fullName ) {
1184 | filter = filter.toLowerCase();
1185 | fullName = fullName.toLowerCase();
1186 |
1187 | var include = filter.charAt( 0 ) !== "!";
1188 | if ( !include ) {
1189 | filter = filter.slice( 1 );
1190 | }
1191 |
1192 | // If the filter matches, we need to honour include
1193 | if ( fullName.indexOf( filter ) !== -1 ) {
1194 | return include;
1195 | }
1196 |
1197 | // Otherwise, do the opposite
1198 | return !include;
1199 | }
1200 | };
1201 |
1202 | // Resets the test setup. Useful for tests that modify the DOM.
1203 | /*
1204 | DEPRECATED: Use multiple tests instead of resetting inside a test.
1205 | Use testStart or testDone for custom cleanup.
1206 | This method will throw an error in 2.0, and will be removed in 2.1
1207 | */
1208 | QUnit.reset = function() {
1209 |
1210 | // Return on non-browser environments
1211 | // This is necessary to not break on node tests
1212 | if ( !defined.document ) {
1213 | return;
1214 | }
1215 |
1216 | var fixture = defined.document && document.getElementById &&
1217 | document.getElementById( "qunit-fixture" );
1218 |
1219 | if ( fixture ) {
1220 | fixture.innerHTML = config.fixture;
1221 | }
1222 | };
1223 |
1224 | QUnit.pushFailure = function() {
1225 | if ( !QUnit.config.current ) {
1226 | throw new Error( "pushFailure() assertion outside test context, in " +
1227 | sourceFromStacktrace( 2 ) );
1228 | }
1229 |
1230 | // Gets current test obj
1231 | var currentTest = QUnit.config.current;
1232 |
1233 | return currentTest.pushFailure.apply( currentTest, arguments );
1234 | };
1235 |
1236 | // Based on Java's String.hashCode, a simple but not
1237 | // rigorously collision resistant hashing function
1238 | function generateHash( module, testName ) {
1239 | var hex,
1240 | i = 0,
1241 | hash = 0,
1242 | str = module + "\x1C" + testName,
1243 | len = str.length;
1244 |
1245 | for ( ; i < len; i++ ) {
1246 | hash = ( ( hash << 5 ) - hash ) + str.charCodeAt( i );
1247 | hash |= 0;
1248 | }
1249 |
1250 | // Convert the possibly negative integer hash code into an 8 character hex string, which isn't
1251 | // strictly necessary but increases user understanding that the id is a SHA-like hash
1252 | hex = ( 0x100000000 + hash ).toString( 16 );
1253 | if ( hex.length < 8 ) {
1254 | hex = "0000000" + hex;
1255 | }
1256 |
1257 | return hex.slice( -8 );
1258 | }
1259 |
1260 | function synchronize( callback, priority ) {
1261 | var last = !priority;
1262 |
1263 | if ( QUnit.objectType( callback ) === "array" ) {
1264 | while ( callback.length ) {
1265 | synchronize( callback.shift() );
1266 | }
1267 | return;
1268 | }
1269 |
1270 | if ( priority ) {
1271 | config.queue.splice( priorityCount++, 0, callback );
1272 | } else {
1273 | config.queue.push( callback );
1274 | }
1275 |
1276 | if ( config.autorun && !config.blocking ) {
1277 | process( last );
1278 | }
1279 | }
1280 |
1281 | function saveGlobal() {
1282 | config.pollution = [];
1283 |
1284 | if ( config.noglobals ) {
1285 | for ( var key in global ) {
1286 | if ( hasOwn.call( global, key ) ) {
1287 |
1288 | // in Opera sometimes DOM element ids show up here, ignore them
1289 | if ( /^qunit-test-output/.test( key ) ) {
1290 | continue;
1291 | }
1292 | config.pollution.push( key );
1293 | }
1294 | }
1295 | }
1296 | }
1297 |
1298 | function checkPollution() {
1299 | var newGlobals,
1300 | deletedGlobals,
1301 | old = config.pollution;
1302 |
1303 | saveGlobal();
1304 |
1305 | newGlobals = diff( config.pollution, old );
1306 | if ( newGlobals.length > 0 ) {
1307 | QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) );
1308 | }
1309 |
1310 | deletedGlobals = diff( old, config.pollution );
1311 | if ( deletedGlobals.length > 0 ) {
1312 | QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) );
1313 | }
1314 | }
1315 |
1316 | // Will be exposed as QUnit.asyncTest
1317 | function asyncTest( testName, expected, callback ) {
1318 | if ( arguments.length === 2 ) {
1319 | callback = expected;
1320 | expected = null;
1321 | }
1322 |
1323 | QUnit.test( testName, expected, callback, true );
1324 | }
1325 |
1326 | // Will be exposed as QUnit.test
1327 | function test( testName, expected, callback, async ) {
1328 | if ( focused ) { return; }
1329 |
1330 | var newTest;
1331 |
1332 | if ( arguments.length === 2 ) {
1333 | callback = expected;
1334 | expected = null;
1335 | }
1336 |
1337 | newTest = new Test({
1338 | testName: testName,
1339 | expected: expected,
1340 | async: async,
1341 | callback: callback
1342 | });
1343 |
1344 | newTest.queue();
1345 | }
1346 |
1347 | // Will be exposed as QUnit.skip
1348 | function skip( testName ) {
1349 | if ( focused ) { return; }
1350 |
1351 | var test = new Test({
1352 | testName: testName,
1353 | skip: true
1354 | });
1355 |
1356 | test.queue();
1357 | }
1358 |
1359 | // Will be exposed as QUnit.only
1360 | function only( testName, expected, callback, async ) {
1361 | var newTest;
1362 |
1363 | if ( focused ) { return; }
1364 |
1365 | QUnit.config.queue.length = 0;
1366 | focused = true;
1367 |
1368 | if ( arguments.length === 2 ) {
1369 | callback = expected;
1370 | expected = null;
1371 | }
1372 |
1373 | newTest = new Test({
1374 | testName: testName,
1375 | expected: expected,
1376 | async: async,
1377 | callback: callback
1378 | });
1379 |
1380 | newTest.queue();
1381 | }
1382 |
1383 | function Assert( testContext ) {
1384 | this.test = testContext;
1385 | }
1386 |
1387 | // Assert helpers
1388 | QUnit.assert = Assert.prototype = {
1389 |
1390 | // Specify the number of expected assertions to guarantee that failed test
1391 | // (no assertions are run at all) don't slip through.
1392 | expect: function( asserts ) {
1393 | if ( arguments.length === 1 ) {
1394 | this.test.expected = asserts;
1395 | } else {
1396 | return this.test.expected;
1397 | }
1398 | },
1399 |
1400 | // Increment this Test's semaphore counter, then return a function that
1401 | // decrements that counter a maximum of once.
1402 | async: function( count ) {
1403 | var test = this.test,
1404 | popped = false,
1405 | acceptCallCount = count;
1406 |
1407 | if ( typeof acceptCallCount === "undefined" ) {
1408 | acceptCallCount = 1;
1409 | }
1410 |
1411 | test.semaphore += 1;
1412 | test.usedAsync = true;
1413 | pauseProcessing();
1414 |
1415 | return function done() {
1416 |
1417 | if ( popped ) {
1418 | test.pushFailure( "Too many calls to the `assert.async` callback",
1419 | sourceFromStacktrace( 2 ) );
1420 | return;
1421 | }
1422 | acceptCallCount -= 1;
1423 | if ( acceptCallCount > 0 ) {
1424 | return;
1425 | }
1426 |
1427 | test.semaphore -= 1;
1428 | popped = true;
1429 | resumeProcessing();
1430 | };
1431 | },
1432 |
1433 | // Exports test.push() to the user API
1434 | push: function( /* result, actual, expected, message, negative */ ) {
1435 | var assert = this,
1436 | currentTest = ( assert instanceof Assert && assert.test ) || QUnit.config.current;
1437 |
1438 | // Backwards compatibility fix.
1439 | // Allows the direct use of global exported assertions and QUnit.assert.*
1440 | // Although, it's use is not recommended as it can leak assertions
1441 | // to other tests from async tests, because we only get a reference to the current test,
1442 | // not exactly the test where assertion were intended to be called.
1443 | if ( !currentTest ) {
1444 | throw new Error( "assertion outside test context, in " + sourceFromStacktrace( 2 ) );
1445 | }
1446 |
1447 | if ( currentTest.usedAsync === true && currentTest.semaphore === 0 ) {
1448 | currentTest.pushFailure( "Assertion after the final `assert.async` was resolved",
1449 | sourceFromStacktrace( 2 ) );
1450 |
1451 | // Allow this assertion to continue running anyway...
1452 | }
1453 |
1454 | if ( !( assert instanceof Assert ) ) {
1455 | assert = currentTest.assert;
1456 | }
1457 | return assert.test.push.apply( assert.test, arguments );
1458 | },
1459 |
1460 | ok: function( result, message ) {
1461 | message = message || ( result ? "okay" : "failed, expected argument to be truthy, was: " +
1462 | QUnit.dump.parse( result ) );
1463 | this.push( !!result, result, true, message );
1464 | },
1465 |
1466 | notOk: function( result, message ) {
1467 | message = message || ( !result ? "okay" : "failed, expected argument to be falsy, was: " +
1468 | QUnit.dump.parse( result ) );
1469 | this.push( !result, result, false, message );
1470 | },
1471 |
1472 | equal: function( actual, expected, message ) {
1473 | /*jshint eqeqeq:false */
1474 | this.push( expected == actual, actual, expected, message );
1475 | },
1476 |
1477 | notEqual: function( actual, expected, message ) {
1478 | /*jshint eqeqeq:false */
1479 | this.push( expected != actual, actual, expected, message, true );
1480 | },
1481 |
1482 | propEqual: function( actual, expected, message ) {
1483 | actual = objectValues( actual );
1484 | expected = objectValues( expected );
1485 | this.push( QUnit.equiv( actual, expected ), actual, expected, message );
1486 | },
1487 |
1488 | notPropEqual: function( actual, expected, message ) {
1489 | actual = objectValues( actual );
1490 | expected = objectValues( expected );
1491 | this.push( !QUnit.equiv( actual, expected ), actual, expected, message, true );
1492 | },
1493 |
1494 | deepEqual: function( actual, expected, message ) {
1495 | this.push( QUnit.equiv( actual, expected ), actual, expected, message );
1496 | },
1497 |
1498 | notDeepEqual: function( actual, expected, message ) {
1499 | this.push( !QUnit.equiv( actual, expected ), actual, expected, message, true );
1500 | },
1501 |
1502 | strictEqual: function( actual, expected, message ) {
1503 | this.push( expected === actual, actual, expected, message );
1504 | },
1505 |
1506 | notStrictEqual: function( actual, expected, message ) {
1507 | this.push( expected !== actual, actual, expected, message, true );
1508 | },
1509 |
1510 | "throws": function( block, expected, message ) {
1511 | var actual, expectedType,
1512 | expectedOutput = expected,
1513 | ok = false,
1514 | currentTest = ( this instanceof Assert && this.test ) || QUnit.config.current;
1515 |
1516 | // 'expected' is optional unless doing string comparison
1517 | if ( message == null && typeof expected === "string" ) {
1518 | message = expected;
1519 | expected = null;
1520 | }
1521 |
1522 | currentTest.ignoreGlobalErrors = true;
1523 | try {
1524 | block.call( currentTest.testEnvironment );
1525 | } catch (e) {
1526 | actual = e;
1527 | }
1528 | currentTest.ignoreGlobalErrors = false;
1529 |
1530 | if ( actual ) {
1531 | expectedType = QUnit.objectType( expected );
1532 |
1533 | // we don't want to validate thrown error
1534 | if ( !expected ) {
1535 | ok = true;
1536 | expectedOutput = null;
1537 |
1538 | // expected is a regexp
1539 | } else if ( expectedType === "regexp" ) {
1540 | ok = expected.test( errorString( actual ) );
1541 |
1542 | // expected is a string
1543 | } else if ( expectedType === "string" ) {
1544 | ok = expected === errorString( actual );
1545 |
1546 | // expected is a constructor, maybe an Error constructor
1547 | } else if ( expectedType === "function" && actual instanceof expected ) {
1548 | ok = true;
1549 |
1550 | // expected is an Error object
1551 | } else if ( expectedType === "object" ) {
1552 | ok = actual instanceof expected.constructor &&
1553 | actual.name === expected.name &&
1554 | actual.message === expected.message;
1555 |
1556 | // expected is a validation function which returns true if validation passed
1557 | } else if ( expectedType === "function" && expected.call( {}, actual ) === true ) {
1558 | expectedOutput = null;
1559 | ok = true;
1560 | }
1561 | }
1562 |
1563 | currentTest.assert.push( ok, actual, expectedOutput, message );
1564 | }
1565 | };
1566 |
1567 | // Provide an alternative to assert.throws(), for environments that consider throws a reserved word
1568 | // Known to us are: Closure Compiler, Narwhal
1569 | (function() {
1570 | /*jshint sub:true */
1571 | Assert.prototype.raises = Assert.prototype[ "throws" ];
1572 | }());
1573 |
1574 | function errorString( error ) {
1575 | var name, message,
1576 | resultErrorString = error.toString();
1577 | if ( resultErrorString.substring( 0, 7 ) === "[object" ) {
1578 | name = error.name ? error.name.toString() : "Error";
1579 | message = error.message ? error.message.toString() : "";
1580 | if ( name && message ) {
1581 | return name + ": " + message;
1582 | } else if ( name ) {
1583 | return name;
1584 | } else if ( message ) {
1585 | return message;
1586 | } else {
1587 | return "Error";
1588 | }
1589 | } else {
1590 | return resultErrorString;
1591 | }
1592 | }
1593 |
1594 | // Test for equality any JavaScript type.
1595 | // Author: Philippe Rathé
1596 | QUnit.equiv = (function() {
1597 |
1598 | // Stack to decide between skip/abort functions
1599 | var callers = [];
1600 |
1601 | // Stack to avoiding loops from circular referencing
1602 | var parents = [];
1603 | var parentsB = [];
1604 |
1605 | var getProto = Object.getPrototypeOf || function( obj ) {
1606 |
1607 | /*jshint proto: true */
1608 | return obj.__proto__;
1609 | };
1610 |
1611 | function useStrictEquality( b, a ) {
1612 |
1613 | // To catch short annotation VS 'new' annotation of a declaration. e.g.:
1614 | // `var i = 1;`
1615 | // `var j = new Number(1);`
1616 | if ( typeof a === "object" ) {
1617 | a = a.valueOf();
1618 | }
1619 | if ( typeof b === "object" ) {
1620 | b = b.valueOf();
1621 | }
1622 |
1623 | return a === b;
1624 | }
1625 |
1626 | function compareConstructors( a, b ) {
1627 | var protoA = getProto( a );
1628 | var protoB = getProto( b );
1629 |
1630 | // Comparing constructors is more strict than using `instanceof`
1631 | if ( a.constructor === b.constructor ) {
1632 | return true;
1633 | }
1634 |
1635 | // Ref #851
1636 | // If the obj prototype descends from a null constructor, treat it
1637 | // as a null prototype.
1638 | if ( protoA && protoA.constructor === null ) {
1639 | protoA = null;
1640 | }
1641 | if ( protoB && protoB.constructor === null ) {
1642 | protoB = null;
1643 | }
1644 |
1645 | // Allow objects with no prototype to be equivalent to
1646 | // objects with Object as their constructor.
1647 | if ( ( protoA === null && protoB === Object.prototype ) ||
1648 | ( protoB === null && protoA === Object.prototype ) ) {
1649 | return true;
1650 | }
1651 |
1652 | return false;
1653 | }
1654 |
1655 | function getRegExpFlags( regexp ) {
1656 | return "flags" in regexp ? regexp.flags : regexp.toString().match( /[gimuy]*$/ )[ 0 ];
1657 | }
1658 |
1659 | var callbacks = {
1660 | "string": useStrictEquality,
1661 | "boolean": useStrictEquality,
1662 | "number": useStrictEquality,
1663 | "null": useStrictEquality,
1664 | "undefined": useStrictEquality,
1665 | "symbol": useStrictEquality,
1666 | "date": useStrictEquality,
1667 |
1668 | "nan": function() {
1669 | return true;
1670 | },
1671 |
1672 | "regexp": function( b, a ) {
1673 | return a.source === b.source &&
1674 |
1675 | // Include flags in the comparison
1676 | getRegExpFlags( a ) === getRegExpFlags( b );
1677 | },
1678 |
1679 | // - skip when the property is a method of an instance (OOP)
1680 | // - abort otherwise,
1681 | // initial === would have catch identical references anyway
1682 | "function": function() {
1683 | var caller = callers[ callers.length - 1 ];
1684 | return caller !== Object && typeof caller !== "undefined";
1685 | },
1686 |
1687 | "array": function( b, a ) {
1688 | var i, j, len, loop, aCircular, bCircular;
1689 |
1690 | len = a.length;
1691 | if ( len !== b.length ) {
1692 | // safe and faster
1693 | return false;
1694 | }
1695 |
1696 | // Track reference to avoid circular references
1697 | parents.push( a );
1698 | parentsB.push( b );
1699 | for ( i = 0; i < len; i++ ) {
1700 | loop = false;
1701 | for ( j = 0; j < parents.length; j++ ) {
1702 | aCircular = parents[ j ] === a[ i ];
1703 | bCircular = parentsB[ j ] === b[ i ];
1704 | if ( aCircular || bCircular ) {
1705 | if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
1706 | loop = true;
1707 | } else {
1708 | parents.pop();
1709 | parentsB.pop();
1710 | return false;
1711 | }
1712 | }
1713 | }
1714 | if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
1715 | parents.pop();
1716 | parentsB.pop();
1717 | return false;
1718 | }
1719 | }
1720 | parents.pop();
1721 | parentsB.pop();
1722 | return true;
1723 | },
1724 |
1725 | "set": function( b, a ) {
1726 | var aArray, bArray;
1727 |
1728 | aArray = [];
1729 | a.forEach( function( v ) {
1730 | aArray.push( v );
1731 | });
1732 | bArray = [];
1733 | b.forEach( function( v ) {
1734 | bArray.push( v );
1735 | });
1736 |
1737 | return innerEquiv( bArray, aArray );
1738 | },
1739 |
1740 | "map": function( b, a ) {
1741 | var aArray, bArray;
1742 |
1743 | aArray = [];
1744 | a.forEach( function( v, k ) {
1745 | aArray.push( [ k, v ] );
1746 | });
1747 | bArray = [];
1748 | b.forEach( function( v, k ) {
1749 | bArray.push( [ k, v ] );
1750 | });
1751 |
1752 | return innerEquiv( bArray, aArray );
1753 | },
1754 |
1755 | "object": function( b, a ) {
1756 | var i, j, loop, aCircular, bCircular;
1757 |
1758 | // Default to true
1759 | var eq = true;
1760 | var aProperties = [];
1761 | var bProperties = [];
1762 |
1763 | if ( compareConstructors( a, b ) === false ) {
1764 | return false;
1765 | }
1766 |
1767 | // Stack constructor before traversing properties
1768 | callers.push( a.constructor );
1769 |
1770 | // Track reference to avoid circular references
1771 | parents.push( a );
1772 | parentsB.push( b );
1773 |
1774 | // Be strict: don't ensure hasOwnProperty and go deep
1775 | for ( i in a ) {
1776 | loop = false;
1777 | for ( j = 0; j < parents.length; j++ ) {
1778 | aCircular = parents[ j ] === a[ i ];
1779 | bCircular = parentsB[ j ] === b[ i ];
1780 | if ( aCircular || bCircular ) {
1781 | if ( a[ i ] === b[ i ] || aCircular && bCircular ) {
1782 | loop = true;
1783 | } else {
1784 | eq = false;
1785 | break;
1786 | }
1787 | }
1788 | }
1789 | aProperties.push( i );
1790 | if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) {
1791 | eq = false;
1792 | break;
1793 | }
1794 | }
1795 |
1796 | parents.pop();
1797 | parentsB.pop();
1798 |
1799 | // Unstack, we are done
1800 | callers.pop();
1801 |
1802 | for ( i in b ) {
1803 |
1804 | // Collect b's properties
1805 | bProperties.push( i );
1806 | }
1807 |
1808 | // Ensures identical properties name
1809 | return eq && innerEquiv( aProperties.sort(), bProperties.sort() );
1810 | }
1811 | };
1812 |
1813 | function typeEquiv( a, b ) {
1814 | var type = QUnit.objectType( a );
1815 | return QUnit.objectType( b ) === type && callbacks[ type ]( b, a );
1816 | }
1817 |
1818 | // The real equiv function
1819 | function innerEquiv( a, b ) {
1820 |
1821 | // We're done when there's nothing more to compare
1822 | if ( arguments.length < 2 ) {
1823 | return true;
1824 | }
1825 |
1826 | // Require type-specific equality
1827 | return ( a === b || typeEquiv( a, b ) ) &&
1828 |
1829 | // ...across all consecutive argument pairs
1830 | ( arguments.length === 2 || innerEquiv.apply( this, [].slice.call( arguments, 1 ) ) );
1831 | }
1832 |
1833 | return innerEquiv;
1834 | }());
1835 |
1836 | // Based on jsDump by Ariel Flesler
1837 | // http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html
1838 | QUnit.dump = (function() {
1839 | function quote( str ) {
1840 | return "\"" + str.toString().replace( /\\/g, "\\\\" ).replace( /"/g, "\\\"" ) + "\"";
1841 | }
1842 | function literal( o ) {
1843 | return o + "";
1844 | }
1845 | function join( pre, arr, post ) {
1846 | var s = dump.separator(),
1847 | base = dump.indent(),
1848 | inner = dump.indent( 1 );
1849 | if ( arr.join ) {
1850 | arr = arr.join( "," + s + inner );
1851 | }
1852 | if ( !arr ) {
1853 | return pre + post;
1854 | }
1855 | return [ pre, inner + arr, base + post ].join( s );
1856 | }
1857 | function array( arr, stack ) {
1858 | var i = arr.length,
1859 | ret = new Array( i );
1860 |
1861 | if ( dump.maxDepth && dump.depth > dump.maxDepth ) {
1862 | return "[object Array]";
1863 | }
1864 |
1865 | this.up();
1866 | while ( i-- ) {
1867 | ret[ i ] = this.parse( arr[ i ], undefined, stack );
1868 | }
1869 | this.down();
1870 | return join( "[", ret, "]" );
1871 | }
1872 |
1873 | var reName = /^function (\w+)/,
1874 | dump = {
1875 |
1876 | // objType is used mostly internally, you can fix a (custom) type in advance
1877 | parse: function( obj, objType, stack ) {
1878 | stack = stack || [];
1879 | var res, parser, parserType,
1880 | inStack = inArray( obj, stack );
1881 |
1882 | if ( inStack !== -1 ) {
1883 | return "recursion(" + ( inStack - stack.length ) + ")";
1884 | }
1885 |
1886 | objType = objType || this.typeOf( obj );
1887 | parser = this.parsers[ objType ];
1888 | parserType = typeof parser;
1889 |
1890 | if ( parserType === "function" ) {
1891 | stack.push( obj );
1892 | res = parser.call( this, obj, stack );
1893 | stack.pop();
1894 | return res;
1895 | }
1896 | return ( parserType === "string" ) ? parser : this.parsers.error;
1897 | },
1898 | typeOf: function( obj ) {
1899 | var type;
1900 | if ( obj === null ) {
1901 | type = "null";
1902 | } else if ( typeof obj === "undefined" ) {
1903 | type = "undefined";
1904 | } else if ( QUnit.is( "regexp", obj ) ) {
1905 | type = "regexp";
1906 | } else if ( QUnit.is( "date", obj ) ) {
1907 | type = "date";
1908 | } else if ( QUnit.is( "function", obj ) ) {
1909 | type = "function";
1910 | } else if ( obj.setInterval !== undefined &&
1911 | obj.document !== undefined &&
1912 | obj.nodeType === undefined ) {
1913 | type = "window";
1914 | } else if ( obj.nodeType === 9 ) {
1915 | type = "document";
1916 | } else if ( obj.nodeType ) {
1917 | type = "node";
1918 | } else if (
1919 |
1920 | // native arrays
1921 | toString.call( obj ) === "[object Array]" ||
1922 |
1923 | // NodeList objects
1924 | ( typeof obj.length === "number" && obj.item !== undefined &&
1925 | ( obj.length ? obj.item( 0 ) === obj[ 0 ] : ( obj.item( 0 ) === null &&
1926 | obj[ 0 ] === undefined ) ) )
1927 | ) {
1928 | type = "array";
1929 | } else if ( obj.constructor === Error.prototype.constructor ) {
1930 | type = "error";
1931 | } else {
1932 | type = typeof obj;
1933 | }
1934 | return type;
1935 | },
1936 | separator: function() {
1937 | return this.multiline ? this.HTML ? " " : "\n" : this.HTML ? " " : " ";
1938 | },
1939 | // extra can be a number, shortcut for increasing-calling-decreasing
1940 | indent: function( extra ) {
1941 | if ( !this.multiline ) {
1942 | return "";
1943 | }
1944 | var chr = this.indentChar;
1945 | if ( this.HTML ) {
1946 | chr = chr.replace( /\t/g, " " ).replace( / /g, " " );
1947 | }
1948 | return new Array( this.depth + ( extra || 0 ) ).join( chr );
1949 | },
1950 | up: function( a ) {
1951 | this.depth += a || 1;
1952 | },
1953 | down: function( a ) {
1954 | this.depth -= a || 1;
1955 | },
1956 | setParser: function( name, parser ) {
1957 | this.parsers[ name ] = parser;
1958 | },
1959 | // The next 3 are exposed so you can use them
1960 | quote: quote,
1961 | literal: literal,
1962 | join: join,
1963 | //
1964 | depth: 1,
1965 | maxDepth: QUnit.config.maxDepth,
1966 |
1967 | // This is the list of parsers, to modify them, use dump.setParser
1968 | parsers: {
1969 | window: "[Window]",
1970 | document: "[Document]",
1971 | error: function( error ) {
1972 | return "Error(\"" + error.message + "\")";
1973 | },
1974 | unknown: "[Unknown]",
1975 | "null": "null",
1976 | "undefined": "undefined",
1977 | "function": function( fn ) {
1978 | var ret = "function",
1979 |
1980 | // functions never have name in IE
1981 | name = "name" in fn ? fn.name : ( reName.exec( fn ) || [] )[ 1 ];
1982 |
1983 | if ( name ) {
1984 | ret += " " + name;
1985 | }
1986 | ret += "( ";
1987 |
1988 | ret = [ ret, dump.parse( fn, "functionArgs" ), "){" ].join( "" );
1989 | return join( ret, dump.parse( fn, "functionCode" ), "}" );
1990 | },
1991 | array: array,
1992 | nodelist: array,
1993 | "arguments": array,
1994 | object: function( map, stack ) {
1995 | var keys, key, val, i, nonEnumerableProperties,
1996 | ret = [];
1997 |
1998 | if ( dump.maxDepth && dump.depth > dump.maxDepth ) {
1999 | return "[object Object]";
2000 | }
2001 |
2002 | dump.up();
2003 | keys = [];
2004 | for ( key in map ) {
2005 | keys.push( key );
2006 | }
2007 |
2008 | // Some properties are not always enumerable on Error objects.
2009 | nonEnumerableProperties = [ "message", "name" ];
2010 | for ( i in nonEnumerableProperties ) {
2011 | key = nonEnumerableProperties[ i ];
2012 | if ( key in map && inArray( key, keys ) < 0 ) {
2013 | keys.push( key );
2014 | }
2015 | }
2016 | keys.sort();
2017 | for ( i = 0; i < keys.length; i++ ) {
2018 | key = keys[ i ];
2019 | val = map[ key ];
2020 | ret.push( dump.parse( key, "key" ) + ": " +
2021 | dump.parse( val, undefined, stack ) );
2022 | }
2023 | dump.down();
2024 | return join( "{", ret, "}" );
2025 | },
2026 | node: function( node ) {
2027 | var len, i, val,
2028 | open = dump.HTML ? "<" : "<",
2029 | close = dump.HTML ? ">" : ">",
2030 | tag = node.nodeName.toLowerCase(),
2031 | ret = open + tag,
2032 | attrs = node.attributes;
2033 |
2034 | if ( attrs ) {
2035 | for ( i = 0, len = attrs.length; i < len; i++ ) {
2036 | val = attrs[ i ].nodeValue;
2037 |
2038 | // IE6 includes all attributes in .attributes, even ones not explicitly
2039 | // set. Those have values like undefined, null, 0, false, "" or
2040 | // "inherit".
2041 | if ( val && val !== "inherit" ) {
2042 | ret += " " + attrs[ i ].nodeName + "=" +
2043 | dump.parse( val, "attribute" );
2044 | }
2045 | }
2046 | }
2047 | ret += close;
2048 |
2049 | // Show content of TextNode or CDATASection
2050 | if ( node.nodeType === 3 || node.nodeType === 4 ) {
2051 | ret += node.nodeValue;
2052 | }
2053 |
2054 | return ret + open + "/" + tag + close;
2055 | },
2056 |
2057 | // function calls it internally, it's the arguments part of the function
2058 | functionArgs: function( fn ) {
2059 | var args,
2060 | l = fn.length;
2061 |
2062 | if ( !l ) {
2063 | return "";
2064 | }
2065 |
2066 | args = new Array( l );
2067 | while ( l-- ) {
2068 |
2069 | // 97 is 'a'
2070 | args[ l ] = String.fromCharCode( 97 + l );
2071 | }
2072 | return " " + args.join( ", " ) + " ";
2073 | },
2074 | // object calls it internally, the key part of an item in a map
2075 | key: quote,
2076 | // function calls it internally, it's the content of the function
2077 | functionCode: "[code]",
2078 | // node calls it internally, it's a html attribute value
2079 | attribute: quote,
2080 | string: quote,
2081 | date: quote,
2082 | regexp: literal,
2083 | number: literal,
2084 | "boolean": literal
2085 | },
2086 | // if true, entities are escaped ( <, >, \t, space and \n )
2087 | HTML: false,
2088 | // indentation unit
2089 | indentChar: " ",
2090 | // if true, items in a collection, are separated by a \n, else just a space.
2091 | multiline: true
2092 | };
2093 |
2094 | return dump;
2095 | }());
2096 |
2097 | // back compat
2098 | QUnit.jsDump = QUnit.dump;
2099 |
2100 | // For browser, export only select globals
2101 | if ( defined.document ) {
2102 |
2103 | // Deprecated
2104 | // Extend assert methods to QUnit and Global scope through Backwards compatibility
2105 | (function() {
2106 | var i,
2107 | assertions = Assert.prototype;
2108 |
2109 | function applyCurrent( current ) {
2110 | return function() {
2111 | var assert = new Assert( QUnit.config.current );
2112 | current.apply( assert, arguments );
2113 | };
2114 | }
2115 |
2116 | for ( i in assertions ) {
2117 | QUnit[ i ] = applyCurrent( assertions[ i ] );
2118 | }
2119 | })();
2120 |
2121 | (function() {
2122 | var i, l,
2123 | keys = [
2124 | "test",
2125 | "module",
2126 | "expect",
2127 | "asyncTest",
2128 | "start",
2129 | "stop",
2130 | "ok",
2131 | "notOk",
2132 | "equal",
2133 | "notEqual",
2134 | "propEqual",
2135 | "notPropEqual",
2136 | "deepEqual",
2137 | "notDeepEqual",
2138 | "strictEqual",
2139 | "notStrictEqual",
2140 | "throws",
2141 | "raises"
2142 | ];
2143 |
2144 | for ( i = 0, l = keys.length; i < l; i++ ) {
2145 | window[ keys[ i ] ] = QUnit[ keys[ i ] ];
2146 | }
2147 | })();
2148 |
2149 | window.QUnit = QUnit;
2150 | }
2151 |
2152 | // For nodejs
2153 | if ( typeof module !== "undefined" && module && module.exports ) {
2154 | module.exports = QUnit;
2155 |
2156 | // For consistency with CommonJS environments' exports
2157 | module.exports.QUnit = QUnit;
2158 | }
2159 |
2160 | // For CommonJS with exports, but without module.exports, like Rhino
2161 | if ( typeof exports !== "undefined" && exports ) {
2162 | exports.QUnit = QUnit;
2163 | }
2164 |
2165 | if ( typeof define === "function" && define.amd ) {
2166 | define( function() {
2167 | return QUnit;
2168 | } );
2169 | QUnit.config.autostart = false;
2170 | }
2171 |
2172 | /*
2173 | * This file is a modified version of google-diff-match-patch's JavaScript implementation
2174 | * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js),
2175 | * modifications are licensed as more fully set forth in LICENSE.txt.
2176 | *
2177 | * The original source of google-diff-match-patch is attributable and licensed as follows:
2178 | *
2179 | * Copyright 2006 Google Inc.
2180 | * https://code.google.com/p/google-diff-match-patch/
2181 | *
2182 | * Licensed under the Apache License, Version 2.0 (the "License");
2183 | * you may not use this file except in compliance with the License.
2184 | * You may obtain a copy of the License at
2185 | *
2186 | * https://www.apache.org/licenses/LICENSE-2.0
2187 | *
2188 | * Unless required by applicable law or agreed to in writing, software
2189 | * distributed under the License is distributed on an "AS IS" BASIS,
2190 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2191 | * See the License for the specific language governing permissions and
2192 | * limitations under the License.
2193 | *
2194 | * More Info:
2195 | * https://code.google.com/p/google-diff-match-patch/
2196 | *
2197 | * Usage: QUnit.diff(expected, actual)
2198 | *
2199 | */
2200 | QUnit.diff = ( function() {
2201 | function DiffMatchPatch() {
2202 | }
2203 |
2204 | // DIFF FUNCTIONS
2205 |
2206 | /**
2207 | * The data structure representing a diff is an array of tuples:
2208 | * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]
2209 | * which means: delete 'Hello', add 'Goodbye' and keep ' world.'
2210 | */
2211 | var DIFF_DELETE = -1,
2212 | DIFF_INSERT = 1,
2213 | DIFF_EQUAL = 0;
2214 |
2215 | /**
2216 | * Find the differences between two texts. Simplifies the problem by stripping
2217 | * any common prefix or suffix off the texts before diffing.
2218 | * @param {string} text1 Old string to be diffed.
2219 | * @param {string} text2 New string to be diffed.
2220 | * @param {boolean=} optChecklines Optional speedup flag. If present and false,
2221 | * then don't run a line-level diff first to identify the changed areas.
2222 | * Defaults to true, which does a faster, slightly less optimal diff.
2223 | * @return {!Array.} Array of diff tuples.
2224 | */
2225 | DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines ) {
2226 | var deadline, checklines, commonlength,
2227 | commonprefix, commonsuffix, diffs;
2228 |
2229 | // The diff must be complete in up to 1 second.
2230 | deadline = ( new Date() ).getTime() + 1000;
2231 |
2232 | // Check for null inputs.
2233 | if ( text1 === null || text2 === null ) {
2234 | throw new Error( "Null input. (DiffMain)" );
2235 | }
2236 |
2237 | // Check for equality (speedup).
2238 | if ( text1 === text2 ) {
2239 | if ( text1 ) {
2240 | return [
2241 | [ DIFF_EQUAL, text1 ]
2242 | ];
2243 | }
2244 | return [];
2245 | }
2246 |
2247 | if ( typeof optChecklines === "undefined" ) {
2248 | optChecklines = true;
2249 | }
2250 |
2251 | checklines = optChecklines;
2252 |
2253 | // Trim off common prefix (speedup).
2254 | commonlength = this.diffCommonPrefix( text1, text2 );
2255 | commonprefix = text1.substring( 0, commonlength );
2256 | text1 = text1.substring( commonlength );
2257 | text2 = text2.substring( commonlength );
2258 |
2259 | // Trim off common suffix (speedup).
2260 | commonlength = this.diffCommonSuffix( text1, text2 );
2261 | commonsuffix = text1.substring( text1.length - commonlength );
2262 | text1 = text1.substring( 0, text1.length - commonlength );
2263 | text2 = text2.substring( 0, text2.length - commonlength );
2264 |
2265 | // Compute the diff on the middle block.
2266 | diffs = this.diffCompute( text1, text2, checklines, deadline );
2267 |
2268 | // Restore the prefix and suffix.
2269 | if ( commonprefix ) {
2270 | diffs.unshift( [ DIFF_EQUAL, commonprefix ] );
2271 | }
2272 | if ( commonsuffix ) {
2273 | diffs.push( [ DIFF_EQUAL, commonsuffix ] );
2274 | }
2275 | this.diffCleanupMerge( diffs );
2276 | return diffs;
2277 | };
2278 |
2279 | /**
2280 | * Reduce the number of edits by eliminating operationally trivial equalities.
2281 | * @param {!Array.} diffs Array of diff tuples.
2282 | */
2283 | DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) {
2284 | var changes, equalities, equalitiesLength, lastequality,
2285 | pointer, preIns, preDel, postIns, postDel;
2286 | changes = false;
2287 | equalities = []; // Stack of indices where equalities are found.
2288 | equalitiesLength = 0; // Keeping our own length var is faster in JS.
2289 | /** @type {?string} */
2290 | lastequality = null;
2291 | // Always equal to diffs[equalities[equalitiesLength - 1]][1]
2292 | pointer = 0; // Index of current position.
2293 | // Is there an insertion operation before the last equality.
2294 | preIns = false;
2295 | // Is there a deletion operation before the last equality.
2296 | preDel = false;
2297 | // Is there an insertion operation after the last equality.
2298 | postIns = false;
2299 | // Is there a deletion operation after the last equality.
2300 | postDel = false;
2301 | while ( pointer < diffs.length ) {
2302 |
2303 | // Equality found.
2304 | if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) {
2305 | if ( diffs[ pointer ][ 1 ].length < 4 && ( postIns || postDel ) ) {
2306 |
2307 | // Candidate found.
2308 | equalities[ equalitiesLength++ ] = pointer;
2309 | preIns = postIns;
2310 | preDel = postDel;
2311 | lastequality = diffs[ pointer ][ 1 ];
2312 | } else {
2313 |
2314 | // Not a candidate, and can never become one.
2315 | equalitiesLength = 0;
2316 | lastequality = null;
2317 | }
2318 | postIns = postDel = false;
2319 |
2320 | // An insertion or deletion.
2321 | } else {
2322 |
2323 | if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) {
2324 | postDel = true;
2325 | } else {
2326 | postIns = true;
2327 | }
2328 |
2329 | /*
2330 | * Five types to be split:
2331 | * ABXYCD
2332 | * AXCD
2333 | * ABXC
2334 | * AXCD
2335 | * ABXC
2336 | */
2337 | if ( lastequality && ( ( preIns && preDel && postIns && postDel ) ||
2338 | ( ( lastequality.length < 2 ) &&
2339 | ( preIns + preDel + postIns + postDel ) === 3 ) ) ) {
2340 |
2341 | // Duplicate record.
2342 | diffs.splice(
2343 | equalities[ equalitiesLength - 1 ],
2344 | 0,
2345 | [ DIFF_DELETE, lastequality ]
2346 | );
2347 |
2348 | // Change second copy to insert.
2349 | diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT;
2350 | equalitiesLength--; // Throw away the equality we just deleted;
2351 | lastequality = null;
2352 | if ( preIns && preDel ) {
2353 | // No changes made which could affect previous entry, keep going.
2354 | postIns = postDel = true;
2355 | equalitiesLength = 0;
2356 | } else {
2357 | equalitiesLength--; // Throw away the previous equality.
2358 | pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1;
2359 | postIns = postDel = false;
2360 | }
2361 | changes = true;
2362 | }
2363 | }
2364 | pointer++;
2365 | }
2366 |
2367 | if ( changes ) {
2368 | this.diffCleanupMerge( diffs );
2369 | }
2370 | };
2371 |
2372 | /**
2373 | * Convert a diff array into a pretty HTML report.
2374 | * @param {!Array.} diffs Array of diff tuples.
2375 | * @param {integer} string to be beautified.
2376 | * @return {string} HTML representation.
2377 | */
2378 | DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) {
2379 | var op, data, x,
2380 | html = [];
2381 | for ( x = 0; x < diffs.length; x++ ) {
2382 | op = diffs[ x ][ 0 ]; // Operation (insert, delete, equal)
2383 | data = diffs[ x ][ 1 ]; // Text of change.
2384 | switch ( op ) {
2385 | case DIFF_INSERT:
2386 | html[ x ] = "" + data + "";
2387 | break;
2388 | case DIFF_DELETE:
2389 | html[ x ] = "" + data + "";
2390 | break;
2391 | case DIFF_EQUAL:
2392 | html[ x ] = "" + data + "";
2393 | break;
2394 | }
2395 | }
2396 | return html.join( "" );
2397 | };
2398 |
2399 | /**
2400 | * Determine the common prefix of two strings.
2401 | * @param {string} text1 First string.
2402 | * @param {string} text2 Second string.
2403 | * @return {number} The number of characters common to the start of each
2404 | * string.
2405 | */
2406 | DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) {
2407 | var pointermid, pointermax, pointermin, pointerstart;
2408 | // Quick check for common null cases.
2409 | if ( !text1 || !text2 || text1.charAt( 0 ) !== text2.charAt( 0 ) ) {
2410 | return 0;
2411 | }
2412 | // Binary search.
2413 | // Performance analysis: https://neil.fraser.name/news/2007/10/09/
2414 | pointermin = 0;
2415 | pointermax = Math.min( text1.length, text2.length );
2416 | pointermid = pointermax;
2417 | pointerstart = 0;
2418 | while ( pointermin < pointermid ) {
2419 | if ( text1.substring( pointerstart, pointermid ) ===
2420 | text2.substring( pointerstart, pointermid ) ) {
2421 | pointermin = pointermid;
2422 | pointerstart = pointermin;
2423 | } else {
2424 | pointermax = pointermid;
2425 | }
2426 | pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin );
2427 | }
2428 | return pointermid;
2429 | };
2430 |
2431 | /**
2432 | * Determine the common suffix of two strings.
2433 | * @param {string} text1 First string.
2434 | * @param {string} text2 Second string.
2435 | * @return {number} The number of characters common to the end of each string.
2436 | */
2437 | DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) {
2438 | var pointermid, pointermax, pointermin, pointerend;
2439 | // Quick check for common null cases.
2440 | if ( !text1 ||
2441 | !text2 ||
2442 | text1.charAt( text1.length - 1 ) !== text2.charAt( text2.length - 1 ) ) {
2443 | return 0;
2444 | }
2445 | // Binary search.
2446 | // Performance analysis: https://neil.fraser.name/news/2007/10/09/
2447 | pointermin = 0;
2448 | pointermax = Math.min( text1.length, text2.length );
2449 | pointermid = pointermax;
2450 | pointerend = 0;
2451 | while ( pointermin < pointermid ) {
2452 | if ( text1.substring( text1.length - pointermid, text1.length - pointerend ) ===
2453 | text2.substring( text2.length - pointermid, text2.length - pointerend ) ) {
2454 | pointermin = pointermid;
2455 | pointerend = pointermin;
2456 | } else {
2457 | pointermax = pointermid;
2458 | }
2459 | pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin );
2460 | }
2461 | return pointermid;
2462 | };
2463 |
2464 | /**
2465 | * Find the differences between two texts. Assumes that the texts do not
2466 | * have any common prefix or suffix.
2467 | * @param {string} text1 Old string to be diffed.
2468 | * @param {string} text2 New string to be diffed.
2469 | * @param {boolean} checklines Speedup flag. If false, then don't run a
2470 | * line-level diff first to identify the changed areas.
2471 | * If true, then run a faster, slightly less optimal diff.
2472 | * @param {number} deadline Time when the diff should be complete by.
2473 | * @return {!Array.} Array of diff tuples.
2474 | * @private
2475 | */
2476 | DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) {
2477 | var diffs, longtext, shorttext, i, hm,
2478 | text1A, text2A, text1B, text2B,
2479 | midCommon, diffsA, diffsB;
2480 |
2481 | if ( !text1 ) {
2482 | // Just add some text (speedup).
2483 | return [
2484 | [ DIFF_INSERT, text2 ]
2485 | ];
2486 | }
2487 |
2488 | if ( !text2 ) {
2489 | // Just delete some text (speedup).
2490 | return [
2491 | [ DIFF_DELETE, text1 ]
2492 | ];
2493 | }
2494 |
2495 | longtext = text1.length > text2.length ? text1 : text2;
2496 | shorttext = text1.length > text2.length ? text2 : text1;
2497 | i = longtext.indexOf( shorttext );
2498 | if ( i !== -1 ) {
2499 | // Shorter text is inside the longer text (speedup).
2500 | diffs = [
2501 | [ DIFF_INSERT, longtext.substring( 0, i ) ],
2502 | [ DIFF_EQUAL, shorttext ],
2503 | [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ]
2504 | ];
2505 | // Swap insertions for deletions if diff is reversed.
2506 | if ( text1.length > text2.length ) {
2507 | diffs[ 0 ][ 0 ] = diffs[ 2 ][ 0 ] = DIFF_DELETE;
2508 | }
2509 | return diffs;
2510 | }
2511 |
2512 | if ( shorttext.length === 1 ) {
2513 | // Single character string.
2514 | // After the previous speedup, the character can't be an equality.
2515 | return [
2516 | [ DIFF_DELETE, text1 ],
2517 | [ DIFF_INSERT, text2 ]
2518 | ];
2519 | }
2520 |
2521 | // Check to see if the problem can be split in two.
2522 | hm = this.diffHalfMatch( text1, text2 );
2523 | if ( hm ) {
2524 | // A half-match was found, sort out the return data.
2525 | text1A = hm[ 0 ];
2526 | text1B = hm[ 1 ];
2527 | text2A = hm[ 2 ];
2528 | text2B = hm[ 3 ];
2529 | midCommon = hm[ 4 ];
2530 | // Send both pairs off for separate processing.
2531 | diffsA = this.DiffMain( text1A, text2A, checklines, deadline );
2532 | diffsB = this.DiffMain( text1B, text2B, checklines, deadline );
2533 | // Merge the results.
2534 | return diffsA.concat( [
2535 | [ DIFF_EQUAL, midCommon ]
2536 | ], diffsB );
2537 | }
2538 |
2539 | if ( checklines && text1.length > 100 && text2.length > 100 ) {
2540 | return this.diffLineMode( text1, text2, deadline );
2541 | }
2542 |
2543 | return this.diffBisect( text1, text2, deadline );
2544 | };
2545 |
2546 | /**
2547 | * Do the two texts share a substring which is at least half the length of the
2548 | * longer text?
2549 | * This speedup can produce non-minimal diffs.
2550 | * @param {string} text1 First string.
2551 | * @param {string} text2 Second string.
2552 | * @return {Array.} Five element Array, containing the prefix of
2553 | * text1, the suffix of text1, the prefix of text2, the suffix of
2554 | * text2 and the common middle. Or null if there was no match.
2555 | * @private
2556 | */
2557 | DiffMatchPatch.prototype.diffHalfMatch = function( text1, text2 ) {
2558 | var longtext, shorttext, dmp,
2559 | text1A, text2B, text2A, text1B, midCommon,
2560 | hm1, hm2, hm;
2561 |
2562 | longtext = text1.length > text2.length ? text1 : text2;
2563 | shorttext = text1.length > text2.length ? text2 : text1;
2564 | if ( longtext.length < 4 || shorttext.length * 2 < longtext.length ) {
2565 | return null; // Pointless.
2566 | }
2567 | dmp = this; // 'this' becomes 'window' in a closure.
2568 |
2569 | /**
2570 | * Does a substring of shorttext exist within longtext such that the substring
2571 | * is at least half the length of longtext?
2572 | * Closure, but does not reference any external variables.
2573 | * @param {string} longtext Longer string.
2574 | * @param {string} shorttext Shorter string.
2575 | * @param {number} i Start index of quarter length substring within longtext.
2576 | * @return {Array.} Five element Array, containing the prefix of
2577 | * longtext, the suffix of longtext, the prefix of shorttext, the suffix
2578 | * of shorttext and the common middle. Or null if there was no match.
2579 | * @private
2580 | */
2581 | function diffHalfMatchI( longtext, shorttext, i ) {
2582 | var seed, j, bestCommon, prefixLength, suffixLength,
2583 | bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB;
2584 | // Start with a 1/4 length substring at position i as a seed.
2585 | seed = longtext.substring( i, i + Math.floor( longtext.length / 4 ) );
2586 | j = -1;
2587 | bestCommon = "";
2588 | while ( ( j = shorttext.indexOf( seed, j + 1 ) ) !== -1 ) {
2589 | prefixLength = dmp.diffCommonPrefix( longtext.substring( i ),
2590 | shorttext.substring( j ) );
2591 | suffixLength = dmp.diffCommonSuffix( longtext.substring( 0, i ),
2592 | shorttext.substring( 0, j ) );
2593 | if ( bestCommon.length < suffixLength + prefixLength ) {
2594 | bestCommon = shorttext.substring( j - suffixLength, j ) +
2595 | shorttext.substring( j, j + prefixLength );
2596 | bestLongtextA = longtext.substring( 0, i - suffixLength );
2597 | bestLongtextB = longtext.substring( i + prefixLength );
2598 | bestShorttextA = shorttext.substring( 0, j - suffixLength );
2599 | bestShorttextB = shorttext.substring( j + prefixLength );
2600 | }
2601 | }
2602 | if ( bestCommon.length * 2 >= longtext.length ) {
2603 | return [ bestLongtextA, bestLongtextB,
2604 | bestShorttextA, bestShorttextB, bestCommon
2605 | ];
2606 | } else {
2607 | return null;
2608 | }
2609 | }
2610 |
2611 | // First check if the second quarter is the seed for a half-match.
2612 | hm1 = diffHalfMatchI( longtext, shorttext,
2613 | Math.ceil( longtext.length / 4 ) );
2614 | // Check again based on the third quarter.
2615 | hm2 = diffHalfMatchI( longtext, shorttext,
2616 | Math.ceil( longtext.length / 2 ) );
2617 | if ( !hm1 && !hm2 ) {
2618 | return null;
2619 | } else if ( !hm2 ) {
2620 | hm = hm1;
2621 | } else if ( !hm1 ) {
2622 | hm = hm2;
2623 | } else {
2624 | // Both matched. Select the longest.
2625 | hm = hm1[ 4 ].length > hm2[ 4 ].length ? hm1 : hm2;
2626 | }
2627 |
2628 | // A half-match was found, sort out the return data.
2629 | text1A, text1B, text2A, text2B;
2630 | if ( text1.length > text2.length ) {
2631 | text1A = hm[ 0 ];
2632 | text1B = hm[ 1 ];
2633 | text2A = hm[ 2 ];
2634 | text2B = hm[ 3 ];
2635 | } else {
2636 | text2A = hm[ 0 ];
2637 | text2B = hm[ 1 ];
2638 | text1A = hm[ 2 ];
2639 | text1B = hm[ 3 ];
2640 | }
2641 | midCommon = hm[ 4 ];
2642 | return [ text1A, text1B, text2A, text2B, midCommon ];
2643 | };
2644 |
2645 | /**
2646 | * Do a quick line-level diff on both strings, then rediff the parts for
2647 | * greater accuracy.
2648 | * This speedup can produce non-minimal diffs.
2649 | * @param {string} text1 Old string to be diffed.
2650 | * @param {string} text2 New string to be diffed.
2651 | * @param {number} deadline Time when the diff should be complete by.
2652 | * @return {!Array.} Array of diff tuples.
2653 | * @private
2654 | */
2655 | DiffMatchPatch.prototype.diffLineMode = function( text1, text2, deadline ) {
2656 | var a, diffs, linearray, pointer, countInsert,
2657 | countDelete, textInsert, textDelete, j;
2658 | // Scan the text on a line-by-line basis first.
2659 | a = this.diffLinesToChars( text1, text2 );
2660 | text1 = a.chars1;
2661 | text2 = a.chars2;
2662 | linearray = a.lineArray;
2663 |
2664 | diffs = this.DiffMain( text1, text2, false, deadline );
2665 |
2666 | // Convert the diff back to original text.
2667 | this.diffCharsToLines( diffs, linearray );
2668 | // Eliminate freak matches (e.g. blank lines)
2669 | this.diffCleanupSemantic( diffs );
2670 |
2671 | // Rediff any replacement blocks, this time character-by-character.
2672 | // Add a dummy entry at the end.
2673 | diffs.push( [ DIFF_EQUAL, "" ] );
2674 | pointer = 0;
2675 | countDelete = 0;
2676 | countInsert = 0;
2677 | textDelete = "";
2678 | textInsert = "";
2679 | while ( pointer < diffs.length ) {
2680 | switch ( diffs[ pointer ][ 0 ] ) {
2681 | case DIFF_INSERT:
2682 | countInsert++;
2683 | textInsert += diffs[ pointer ][ 1 ];
2684 | break;
2685 | case DIFF_DELETE:
2686 | countDelete++;
2687 | textDelete += diffs[ pointer ][ 1 ];
2688 | break;
2689 | case DIFF_EQUAL:
2690 | // Upon reaching an equality, check for prior redundancies.
2691 | if ( countDelete >= 1 && countInsert >= 1 ) {
2692 | // Delete the offending records and add the merged ones.
2693 | diffs.splice( pointer - countDelete - countInsert,
2694 | countDelete + countInsert );
2695 | pointer = pointer - countDelete - countInsert;
2696 | a = this.DiffMain( textDelete, textInsert, false, deadline );
2697 | for ( j = a.length - 1; j >= 0; j-- ) {
2698 | diffs.splice( pointer, 0, a[ j ] );
2699 | }
2700 | pointer = pointer + a.length;
2701 | }
2702 | countInsert = 0;
2703 | countDelete = 0;
2704 | textDelete = "";
2705 | textInsert = "";
2706 | break;
2707 | }
2708 | pointer++;
2709 | }
2710 | diffs.pop(); // Remove the dummy entry at the end.
2711 |
2712 | return diffs;
2713 | };
2714 |
2715 | /**
2716 | * Find the 'middle snake' of a diff, split the problem in two
2717 | * and return the recursively constructed diff.
2718 | * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
2719 | * @param {string} text1 Old string to be diffed.
2720 | * @param {string} text2 New string to be diffed.
2721 | * @param {number} deadline Time at which to bail if not yet complete.
2722 | * @return {!Array.} Array of diff tuples.
2723 | * @private
2724 | */
2725 | DiffMatchPatch.prototype.diffBisect = function( text1, text2, deadline ) {
2726 | var text1Length, text2Length, maxD, vOffset, vLength,
2727 | v1, v2, x, delta, front, k1start, k1end, k2start,
2728 | k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2;
2729 | // Cache the text lengths to prevent multiple calls.
2730 | text1Length = text1.length;
2731 | text2Length = text2.length;
2732 | maxD = Math.ceil( ( text1Length + text2Length ) / 2 );
2733 | vOffset = maxD;
2734 | vLength = 2 * maxD;
2735 | v1 = new Array( vLength );
2736 | v2 = new Array( vLength );
2737 | // Setting all elements to -1 is faster in Chrome & Firefox than mixing
2738 | // integers and undefined.
2739 | for ( x = 0; x < vLength; x++ ) {
2740 | v1[ x ] = -1;
2741 | v2[ x ] = -1;
2742 | }
2743 | v1[ vOffset + 1 ] = 0;
2744 | v2[ vOffset + 1 ] = 0;
2745 | delta = text1Length - text2Length;
2746 | // If the total number of characters is odd, then the front path will collide
2747 | // with the reverse path.
2748 | front = ( delta % 2 !== 0 );
2749 | // Offsets for start and end of k loop.
2750 | // Prevents mapping of space beyond the grid.
2751 | k1start = 0;
2752 | k1end = 0;
2753 | k2start = 0;
2754 | k2end = 0;
2755 | for ( d = 0; d < maxD; d++ ) {
2756 | // Bail out if deadline is reached.
2757 | if ( ( new Date() ).getTime() > deadline ) {
2758 | break;
2759 | }
2760 |
2761 | // Walk the front path one step.
2762 | for ( k1 = -d + k1start; k1 <= d - k1end; k1 += 2 ) {
2763 | k1Offset = vOffset + k1;
2764 | if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) {
2765 | x1 = v1[ k1Offset + 1 ];
2766 | } else {
2767 | x1 = v1[ k1Offset - 1 ] + 1;
2768 | }
2769 | y1 = x1 - k1;
2770 | while ( x1 < text1Length && y1 < text2Length &&
2771 | text1.charAt( x1 ) === text2.charAt( y1 ) ) {
2772 | x1++;
2773 | y1++;
2774 | }
2775 | v1[ k1Offset ] = x1;
2776 | if ( x1 > text1Length ) {
2777 | // Ran off the right of the graph.
2778 | k1end += 2;
2779 | } else if ( y1 > text2Length ) {
2780 | // Ran off the bottom of the graph.
2781 | k1start += 2;
2782 | } else if ( front ) {
2783 | k2Offset = vOffset + delta - k1;
2784 | if ( k2Offset >= 0 && k2Offset < vLength && v2[ k2Offset ] !== -1 ) {
2785 | // Mirror x2 onto top-left coordinate system.
2786 | x2 = text1Length - v2[ k2Offset ];
2787 | if ( x1 >= x2 ) {
2788 | // Overlap detected.
2789 | return this.diffBisectSplit( text1, text2, x1, y1, deadline );
2790 | }
2791 | }
2792 | }
2793 | }
2794 |
2795 | // Walk the reverse path one step.
2796 | for ( k2 = -d + k2start; k2 <= d - k2end; k2 += 2 ) {
2797 | k2Offset = vOffset + k2;
2798 | if ( k2 === -d || ( k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) {
2799 | x2 = v2[ k2Offset + 1 ];
2800 | } else {
2801 | x2 = v2[ k2Offset - 1 ] + 1;
2802 | }
2803 | y2 = x2 - k2;
2804 | while ( x2 < text1Length && y2 < text2Length &&
2805 | text1.charAt( text1Length - x2 - 1 ) ===
2806 | text2.charAt( text2Length - y2 - 1 ) ) {
2807 | x2++;
2808 | y2++;
2809 | }
2810 | v2[ k2Offset ] = x2;
2811 | if ( x2 > text1Length ) {
2812 | // Ran off the left of the graph.
2813 | k2end += 2;
2814 | } else if ( y2 > text2Length ) {
2815 | // Ran off the top of the graph.
2816 | k2start += 2;
2817 | } else if ( !front ) {
2818 | k1Offset = vOffset + delta - k2;
2819 | if ( k1Offset >= 0 && k1Offset < vLength && v1[ k1Offset ] !== -1 ) {
2820 | x1 = v1[ k1Offset ];
2821 | y1 = vOffset + x1 - k1Offset;
2822 | // Mirror x2 onto top-left coordinate system.
2823 | x2 = text1Length - x2;
2824 | if ( x1 >= x2 ) {
2825 | // Overlap detected.
2826 | return this.diffBisectSplit( text1, text2, x1, y1, deadline );
2827 | }
2828 | }
2829 | }
2830 | }
2831 | }
2832 | // Diff took too long and hit the deadline or
2833 | // number of diffs equals number of characters, no commonality at all.
2834 | return [
2835 | [ DIFF_DELETE, text1 ],
2836 | [ DIFF_INSERT, text2 ]
2837 | ];
2838 | };
2839 |
2840 | /**
2841 | * Given the location of the 'middle snake', split the diff in two parts
2842 | * and recurse.
2843 | * @param {string} text1 Old string to be diffed.
2844 | * @param {string} text2 New string to be diffed.
2845 | * @param {number} x Index of split point in text1.
2846 | * @param {number} y Index of split point in text2.
2847 | * @param {number} deadline Time at which to bail if not yet complete.
2848 | * @return {!Array.} Array of diff tuples.
2849 | * @private
2850 | */
2851 | DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) {
2852 | var text1a, text1b, text2a, text2b, diffs, diffsb;
2853 | text1a = text1.substring( 0, x );
2854 | text2a = text2.substring( 0, y );
2855 | text1b = text1.substring( x );
2856 | text2b = text2.substring( y );
2857 |
2858 | // Compute both diffs serially.
2859 | diffs = this.DiffMain( text1a, text2a, false, deadline );
2860 | diffsb = this.DiffMain( text1b, text2b, false, deadline );
2861 |
2862 | return diffs.concat( diffsb );
2863 | };
2864 |
2865 | /**
2866 | * Reduce the number of edits by eliminating semantically trivial equalities.
2867 | * @param {!Array.} diffs Array of diff tuples.
2868 | */
2869 | DiffMatchPatch.prototype.diffCleanupSemantic = function( diffs ) {
2870 | var changes, equalities, equalitiesLength, lastequality,
2871 | pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1,
2872 | lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2;
2873 | changes = false;
2874 | equalities = []; // Stack of indices where equalities are found.
2875 | equalitiesLength = 0; // Keeping our own length var is faster in JS.
2876 | /** @type {?string} */
2877 | lastequality = null;
2878 | // Always equal to diffs[equalities[equalitiesLength - 1]][1]
2879 | pointer = 0; // Index of current position.
2880 | // Number of characters that changed prior to the equality.
2881 | lengthInsertions1 = 0;
2882 | lengthDeletions1 = 0;
2883 | // Number of characters that changed after the equality.
2884 | lengthInsertions2 = 0;
2885 | lengthDeletions2 = 0;
2886 | while ( pointer < diffs.length ) {
2887 | if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found.
2888 | equalities[ equalitiesLength++ ] = pointer;
2889 | lengthInsertions1 = lengthInsertions2;
2890 | lengthDeletions1 = lengthDeletions2;
2891 | lengthInsertions2 = 0;
2892 | lengthDeletions2 = 0;
2893 | lastequality = diffs[ pointer ][ 1 ];
2894 | } else { // An insertion or deletion.
2895 | if ( diffs[ pointer ][ 0 ] === DIFF_INSERT ) {
2896 | lengthInsertions2 += diffs[ pointer ][ 1 ].length;
2897 | } else {
2898 | lengthDeletions2 += diffs[ pointer ][ 1 ].length;
2899 | }
2900 | // Eliminate an equality that is smaller or equal to the edits on both
2901 | // sides of it.
2902 | if ( lastequality && ( lastequality.length <=
2903 | Math.max( lengthInsertions1, lengthDeletions1 ) ) &&
2904 | ( lastequality.length <= Math.max( lengthInsertions2,
2905 | lengthDeletions2 ) ) ) {
2906 |
2907 | // Duplicate record.
2908 | diffs.splice(
2909 | equalities[ equalitiesLength - 1 ],
2910 | 0,
2911 | [ DIFF_DELETE, lastequality ]
2912 | );
2913 |
2914 | // Change second copy to insert.
2915 | diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT;
2916 |
2917 | // Throw away the equality we just deleted.
2918 | equalitiesLength--;
2919 |
2920 | // Throw away the previous equality (it needs to be reevaluated).
2921 | equalitiesLength--;
2922 | pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1;
2923 |
2924 | // Reset the counters.
2925 | lengthInsertions1 = 0;
2926 | lengthDeletions1 = 0;
2927 | lengthInsertions2 = 0;
2928 | lengthDeletions2 = 0;
2929 | lastequality = null;
2930 | changes = true;
2931 | }
2932 | }
2933 | pointer++;
2934 | }
2935 |
2936 | // Normalize the diff.
2937 | if ( changes ) {
2938 | this.diffCleanupMerge( diffs );
2939 | }
2940 |
2941 | // Find any overlaps between deletions and insertions.
2942 | // e.g: abcxxxxxxdef
2943 | // -> abcxxxdef
2944 | // e.g: xxxabcdefxxx
2945 | // -> defxxxabc
2946 | // Only extract an overlap if it is as big as the edit ahead or behind it.
2947 | pointer = 1;
2948 | while ( pointer < diffs.length ) {
2949 | if ( diffs[ pointer - 1 ][ 0 ] === DIFF_DELETE &&
2950 | diffs[ pointer ][ 0 ] === DIFF_INSERT ) {
2951 | deletion = diffs[ pointer - 1 ][ 1 ];
2952 | insertion = diffs[ pointer ][ 1 ];
2953 | overlapLength1 = this.diffCommonOverlap( deletion, insertion );
2954 | overlapLength2 = this.diffCommonOverlap( insertion, deletion );
2955 | if ( overlapLength1 >= overlapLength2 ) {
2956 | if ( overlapLength1 >= deletion.length / 2 ||
2957 | overlapLength1 >= insertion.length / 2 ) {
2958 | // Overlap found. Insert an equality and trim the surrounding edits.
2959 | diffs.splice(
2960 | pointer,
2961 | 0,
2962 | [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ]
2963 | );
2964 | diffs[ pointer - 1 ][ 1 ] =
2965 | deletion.substring( 0, deletion.length - overlapLength1 );
2966 | diffs[ pointer + 1 ][ 1 ] = insertion.substring( overlapLength1 );
2967 | pointer++;
2968 | }
2969 | } else {
2970 | if ( overlapLength2 >= deletion.length / 2 ||
2971 | overlapLength2 >= insertion.length / 2 ) {
2972 |
2973 | // Reverse overlap found.
2974 | // Insert an equality and swap and trim the surrounding edits.
2975 | diffs.splice(
2976 | pointer,
2977 | 0,
2978 | [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ]
2979 | );
2980 |
2981 | diffs[ pointer - 1 ][ 0 ] = DIFF_INSERT;
2982 | diffs[ pointer - 1 ][ 1 ] =
2983 | insertion.substring( 0, insertion.length - overlapLength2 );
2984 | diffs[ pointer + 1 ][ 0 ] = DIFF_DELETE;
2985 | diffs[ pointer + 1 ][ 1 ] =
2986 | deletion.substring( overlapLength2 );
2987 | pointer++;
2988 | }
2989 | }
2990 | pointer++;
2991 | }
2992 | pointer++;
2993 | }
2994 | };
2995 |
2996 | /**
2997 | * Determine if the suffix of one string is the prefix of another.
2998 | * @param {string} text1 First string.
2999 | * @param {string} text2 Second string.
3000 | * @return {number} The number of characters common to the end of the first
3001 | * string and the start of the second string.
3002 | * @private
3003 | */
3004 | DiffMatchPatch.prototype.diffCommonOverlap = function( text1, text2 ) {
3005 | var text1Length, text2Length, textLength,
3006 | best, length, pattern, found;
3007 | // Cache the text lengths to prevent multiple calls.
3008 | text1Length = text1.length;
3009 | text2Length = text2.length;
3010 | // Eliminate the null case.
3011 | if ( text1Length === 0 || text2Length === 0 ) {
3012 | return 0;
3013 | }
3014 | // Truncate the longer string.
3015 | if ( text1Length > text2Length ) {
3016 | text1 = text1.substring( text1Length - text2Length );
3017 | } else if ( text1Length < text2Length ) {
3018 | text2 = text2.substring( 0, text1Length );
3019 | }
3020 | textLength = Math.min( text1Length, text2Length );
3021 | // Quick check for the worst case.
3022 | if ( text1 === text2 ) {
3023 | return textLength;
3024 | }
3025 |
3026 | // Start by looking for a single character match
3027 | // and increase length until no match is found.
3028 | // Performance analysis: https://neil.fraser.name/news/2010/11/04/
3029 | best = 0;
3030 | length = 1;
3031 | while ( true ) {
3032 | pattern = text1.substring( textLength - length );
3033 | found = text2.indexOf( pattern );
3034 | if ( found === -1 ) {
3035 | return best;
3036 | }
3037 | length += found;
3038 | if ( found === 0 || text1.substring( textLength - length ) ===
3039 | text2.substring( 0, length ) ) {
3040 | best = length;
3041 | length++;
3042 | }
3043 | }
3044 | };
3045 |
3046 | /**
3047 | * Split two texts into an array of strings. Reduce the texts to a string of
3048 | * hashes where each Unicode character represents one line.
3049 | * @param {string} text1 First string.
3050 | * @param {string} text2 Second string.
3051 | * @return {{chars1: string, chars2: string, lineArray: !Array.}}
3052 | * An object containing the encoded text1, the encoded text2 and
3053 | * the array of unique strings.
3054 | * The zeroth element of the array of unique strings is intentionally blank.
3055 | * @private
3056 | */
3057 | DiffMatchPatch.prototype.diffLinesToChars = function( text1, text2 ) {
3058 | var lineArray, lineHash, chars1, chars2;
3059 | lineArray = []; // e.g. lineArray[4] === 'Hello\n'
3060 | lineHash = {}; // e.g. lineHash['Hello\n'] === 4
3061 |
3062 | // '\x00' is a valid character, but various debuggers don't like it.
3063 | // So we'll insert a junk entry to avoid generating a null character.
3064 | lineArray[ 0 ] = "";
3065 |
3066 | /**
3067 | * Split a text into an array of strings. Reduce the texts to a string of
3068 | * hashes where each Unicode character represents one line.
3069 | * Modifies linearray and linehash through being a closure.
3070 | * @param {string} text String to encode.
3071 | * @return {string} Encoded string.
3072 | * @private
3073 | */
3074 | function diffLinesToCharsMunge( text ) {
3075 | var chars, lineStart, lineEnd, lineArrayLength, line;
3076 | chars = "";
3077 | // Walk the text, pulling out a substring for each line.
3078 | // text.split('\n') would would temporarily double our memory footprint.
3079 | // Modifying text would create many large strings to garbage collect.
3080 | lineStart = 0;
3081 | lineEnd = -1;
3082 | // Keeping our own length variable is faster than looking it up.
3083 | lineArrayLength = lineArray.length;
3084 | while ( lineEnd < text.length - 1 ) {
3085 | lineEnd = text.indexOf( "\n", lineStart );
3086 | if ( lineEnd === -1 ) {
3087 | lineEnd = text.length - 1;
3088 | }
3089 | line = text.substring( lineStart, lineEnd + 1 );
3090 | lineStart = lineEnd + 1;
3091 |
3092 | if ( lineHash.hasOwnProperty ? lineHash.hasOwnProperty( line ) :
3093 | ( lineHash[ line ] !== undefined ) ) {
3094 | chars += String.fromCharCode( lineHash[ line ] );
3095 | } else {
3096 | chars += String.fromCharCode( lineArrayLength );
3097 | lineHash[ line ] = lineArrayLength;
3098 | lineArray[ lineArrayLength++ ] = line;
3099 | }
3100 | }
3101 | return chars;
3102 | }
3103 |
3104 | chars1 = diffLinesToCharsMunge( text1 );
3105 | chars2 = diffLinesToCharsMunge( text2 );
3106 | return {
3107 | chars1: chars1,
3108 | chars2: chars2,
3109 | lineArray: lineArray
3110 | };
3111 | };
3112 |
3113 | /**
3114 | * Rehydrate the text in a diff from a string of line hashes to real lines of
3115 | * text.
3116 | * @param {!Array.} diffs Array of diff tuples.
3117 | * @param {!Array.} lineArray Array of unique strings.
3118 | * @private
3119 | */
3120 | DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) {
3121 | var x, chars, text, y;
3122 | for ( x = 0; x < diffs.length; x++ ) {
3123 | chars = diffs[ x ][ 1 ];
3124 | text = [];
3125 | for ( y = 0; y < chars.length; y++ ) {
3126 | text[ y ] = lineArray[ chars.charCodeAt( y ) ];
3127 | }
3128 | diffs[ x ][ 1 ] = text.join( "" );
3129 | }
3130 | };
3131 |
3132 | /**
3133 | * Reorder and merge like edit sections. Merge equalities.
3134 | * Any edit section can move as long as it doesn't cross an equality.
3135 | * @param {!Array.} diffs Array of diff tuples.
3136 | */
3137 | DiffMatchPatch.prototype.diffCleanupMerge = function( diffs ) {
3138 | var pointer, countDelete, countInsert, textInsert, textDelete,
3139 | commonlength, changes, diffPointer, position;
3140 | diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end.
3141 | pointer = 0;
3142 | countDelete = 0;
3143 | countInsert = 0;
3144 | textDelete = "";
3145 | textInsert = "";
3146 | commonlength;
3147 | while ( pointer < diffs.length ) {
3148 | switch ( diffs[ pointer ][ 0 ] ) {
3149 | case DIFF_INSERT:
3150 | countInsert++;
3151 | textInsert += diffs[ pointer ][ 1 ];
3152 | pointer++;
3153 | break;
3154 | case DIFF_DELETE:
3155 | countDelete++;
3156 | textDelete += diffs[ pointer ][ 1 ];
3157 | pointer++;
3158 | break;
3159 | case DIFF_EQUAL:
3160 | // Upon reaching an equality, check for prior redundancies.
3161 | if ( countDelete + countInsert > 1 ) {
3162 | if ( countDelete !== 0 && countInsert !== 0 ) {
3163 | // Factor out any common prefixes.
3164 | commonlength = this.diffCommonPrefix( textInsert, textDelete );
3165 | if ( commonlength !== 0 ) {
3166 | if ( ( pointer - countDelete - countInsert ) > 0 &&
3167 | diffs[ pointer - countDelete - countInsert - 1 ][ 0 ] ===
3168 | DIFF_EQUAL ) {
3169 | diffs[ pointer - countDelete - countInsert - 1 ][ 1 ] +=
3170 | textInsert.substring( 0, commonlength );
3171 | } else {
3172 | diffs.splice( 0, 0, [ DIFF_EQUAL,
3173 | textInsert.substring( 0, commonlength )
3174 | ] );
3175 | pointer++;
3176 | }
3177 | textInsert = textInsert.substring( commonlength );
3178 | textDelete = textDelete.substring( commonlength );
3179 | }
3180 | // Factor out any common suffixies.
3181 | commonlength = this.diffCommonSuffix( textInsert, textDelete );
3182 | if ( commonlength !== 0 ) {
3183 | diffs[ pointer ][ 1 ] = textInsert.substring( textInsert.length -
3184 | commonlength ) + diffs[ pointer ][ 1 ];
3185 | textInsert = textInsert.substring( 0, textInsert.length -
3186 | commonlength );
3187 | textDelete = textDelete.substring( 0, textDelete.length -
3188 | commonlength );
3189 | }
3190 | }
3191 | // Delete the offending records and add the merged ones.
3192 | if ( countDelete === 0 ) {
3193 | diffs.splice( pointer - countInsert,
3194 | countDelete + countInsert, [ DIFF_INSERT, textInsert ] );
3195 | } else if ( countInsert === 0 ) {
3196 | diffs.splice( pointer - countDelete,
3197 | countDelete + countInsert, [ DIFF_DELETE, textDelete ] );
3198 | } else {
3199 | diffs.splice(
3200 | pointer - countDelete - countInsert,
3201 | countDelete + countInsert,
3202 | [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ]
3203 | );
3204 | }
3205 | pointer = pointer - countDelete - countInsert +
3206 | ( countDelete ? 1 : 0 ) + ( countInsert ? 1 : 0 ) + 1;
3207 | } else if ( pointer !== 0 && diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL ) {
3208 |
3209 | // Merge this equality with the previous one.
3210 | diffs[ pointer - 1 ][ 1 ] += diffs[ pointer ][ 1 ];
3211 | diffs.splice( pointer, 1 );
3212 | } else {
3213 | pointer++;
3214 | }
3215 | countInsert = 0;
3216 | countDelete = 0;
3217 | textDelete = "";
3218 | textInsert = "";
3219 | break;
3220 | }
3221 | }
3222 | if ( diffs[ diffs.length - 1 ][ 1 ] === "" ) {
3223 | diffs.pop(); // Remove the dummy entry at the end.
3224 | }
3225 |
3226 | // Second pass: look for single edits surrounded on both sides by equalities
3227 | // which can be shifted sideways to eliminate an equality.
3228 | // e.g: ABAC -> ABAC
3229 | changes = false;
3230 | pointer = 1;
3231 |
3232 | // Intentionally ignore the first and last element (don't need checking).
3233 | while ( pointer < diffs.length - 1 ) {
3234 | if ( diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL &&
3235 | diffs[ pointer + 1 ][ 0 ] === DIFF_EQUAL ) {
3236 |
3237 | diffPointer = diffs[ pointer ][ 1 ];
3238 | position = diffPointer.substring(
3239 | diffPointer.length - diffs[ pointer - 1 ][ 1 ].length
3240 | );
3241 |
3242 | // This is a single edit surrounded by equalities.
3243 | if ( position === diffs[ pointer - 1 ][ 1 ] ) {
3244 |
3245 | // Shift the edit over the previous equality.
3246 | diffs[ pointer ][ 1 ] = diffs[ pointer - 1 ][ 1 ] +
3247 | diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer ][ 1 ].length -
3248 | diffs[ pointer - 1 ][ 1 ].length );
3249 | diffs[ pointer + 1 ][ 1 ] =
3250 | diffs[ pointer - 1 ][ 1 ] + diffs[ pointer + 1 ][ 1 ];
3251 | diffs.splice( pointer - 1, 1 );
3252 | changes = true;
3253 | } else if ( diffPointer.substring( 0, diffs[ pointer + 1 ][ 1 ].length ) ===
3254 | diffs[ pointer + 1 ][ 1 ] ) {
3255 |
3256 | // Shift the edit over the next equality.
3257 | diffs[ pointer - 1 ][ 1 ] += diffs[ pointer + 1 ][ 1 ];
3258 | diffs[ pointer ][ 1 ] =
3259 | diffs[ pointer ][ 1 ].substring( diffs[ pointer + 1 ][ 1 ].length ) +
3260 | diffs[ pointer + 1 ][ 1 ];
3261 | diffs.splice( pointer + 1, 1 );
3262 | changes = true;
3263 | }
3264 | }
3265 | pointer++;
3266 | }
3267 | // If shifts were made, the diff needs reordering and another shift sweep.
3268 | if ( changes ) {
3269 | this.diffCleanupMerge( diffs );
3270 | }
3271 | };
3272 |
3273 | return function( o, n ) {
3274 | var diff, output, text;
3275 | diff = new DiffMatchPatch();
3276 | output = diff.DiffMain( o, n );
3277 | diff.diffCleanupEfficiency( output );
3278 | text = diff.diffPrettyHtml( output );
3279 |
3280 | return text;
3281 | };
3282 | }() );
3283 |
3284 | // Get a reference to the global object, like window in browsers
3285 | }( (function() {
3286 | return this;
3287 | })() ));
3288 |
3289 | (function() {
3290 |
3291 | // Don't load the HTML Reporter on non-Browser environments
3292 | if ( typeof window === "undefined" || !window.document ) {
3293 | return;
3294 | }
3295 |
3296 | // Deprecated QUnit.init - Ref #530
3297 | // Re-initialize the configuration options
3298 | QUnit.init = function() {
3299 | var tests, banner, result, qunit,
3300 | config = QUnit.config;
3301 |
3302 | config.stats = { all: 0, bad: 0 };
3303 | config.moduleStats = { all: 0, bad: 0 };
3304 | config.started = 0;
3305 | config.updateRate = 1000;
3306 | config.blocking = false;
3307 | config.autostart = true;
3308 | config.autorun = false;
3309 | config.filter = "";
3310 | config.queue = [];
3311 |
3312 | // Return on non-browser environments
3313 | // This is necessary to not break on node tests
3314 | if ( typeof window === "undefined" ) {
3315 | return;
3316 | }
3317 |
3318 | qunit = id( "qunit" );
3319 | if ( qunit ) {
3320 | qunit.innerHTML =
3321 | "
";
4007 |
4008 | // this occurs when pushFailure is set and we have an extracted stack trace
4009 | } else if ( !details.result && details.source ) {
4010 | message += "