├── README.md ├── Specs ├── Plugins │ └── MooToolsRails.js ├── demo.html ├── index.html └── Assets │ ├── Scripts │ ├── Builder.js │ ├── JSSpecSpecs.js │ ├── JSSpec.js │ └── DiffMatchPatch.js │ └── Styles │ └── Specs.css ├── Source └── Plugins │ └── MooToolsRails.js ├── scripts.json ├── LICENSE ├── Demos └── index.html └── Docs └── MooToolsRails.md /README.md: -------------------------------------------------------------------------------- 1 | MooToolsRails 2 | ========== 3 | 4 | Description 5 | ----------- 6 | MooTools extension for Rails3 -------------------------------------------------------------------------------- /Specs/Plugins/MooToolsRails.js: -------------------------------------------------------------------------------- 1 | /* 2 | Script: Selectors.Children.js 3 | Specification Examples of Pseudo Selector :children. 4 | 5 | License: 6 | MIT-style license. 7 | */ 8 | 9 | describe('MooToolsRails', { 10 | 11 | 'before all': function(){ 12 | }, 13 | 14 | 'after all': function(){ 15 | }, 16 | 17 | 'should do what it was designed to do': function(){ 18 | value_of( 'value' ).should_be( 'result' ); 19 | } 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /Source/Plugins/MooToolsRails.js: -------------------------------------------------------------------------------- 1 | /* 2 | Script: MooToolsRails.js 3 | MooToolsRails 4 | 5 | License: 6 | MIT-style license. 7 | 8 | Copyright: 9 | Copyright (c) 2009 [copyright holders](http://). 10 | 11 | */ 12 | var MooToolsRails = new Class({ 13 | 14 | Implements: [Options, Events], 15 | 16 | options: { 17 | 18 | }, 19 | 20 | initialize: function(options){ 21 | this.setOptions(options); 22 | 23 | $$('p').setStyle('background','blue'); 24 | 25 | this.fireEvent("initialize"); 26 | } 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "Plugin Type": { 3 | "MooToolsRails": { 4 | "deps": ["None"], 5 | "desc": "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Specs/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JSSpec Sample Specs 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Specs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | MooTools Specs 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2009 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Demos/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | MooToolsRails 7 | 8 | 9 | 10 |

11 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 12 |

13 | 14 | 15 | 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Docs/MooToolsRails.md: -------------------------------------------------------------------------------- 1 | Class: MooToolsRails {#MooToolsRails} 2 | =============================== 3 | 4 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 5 | 6 | ### Notes: 7 | 8 | - MooToolsRails requires the page to be in [Standards Mode](http://hsivonen.iki.fi/doctype/). 9 | 10 | ### Implements: 11 | 12 | [Options][] 13 | [Events][] 14 | 15 | ### Syntax: 16 | 17 | var myThing = new MooToolsRails([options]); 18 | 19 | ### Arguments: 20 | 21 | 1. options - (*object*, optional) All the [Events][] options in addition to options below. 22 | 23 | #### Options: 24 | 25 | * option1 - (*integer*: defaults to 0 ) lorem ipsum. 26 | * option2 - (*integer*: defaults to 0 ) lorem ipsum. 27 | * option3 - (*boolean*: defaults to true ) If set to true, lorem ipsum. 28 | * option4 - (*boolean*: defaults to false ) If set to true, lorem ipsum. 29 | * option5 - (*boolean*: defaults to true ) If set to true, lorem ipsum. 30 | * option6 - (*boolean*: defaults to false ) If set to false, lorem ipsum. 31 | * option7 - (*boolean*: defaults to false ) If set to true, lorem ipsum. 32 | * option8 - (*boolean*: defaults to false ) If set to true, lorem ipsum. 33 | * option9 - (*boolean*: defaults to false ) If set to true, lorem ipsum. 34 | 35 | ### Returns: 36 | 37 | * (*object*) A new MooToolsRails instance. 38 | 39 | ## Events: 40 | 41 | ### eventName 42 | 43 | * (*function*) Function to do stuff. 44 | 45 | #### Signature: 46 | 47 | onEventName(arg1, arg2) 48 | 49 | #### Arguments: 50 | 51 | 1. arg1 - (*object*) Lorem ipsum. 52 | 2. arg2 - (*object*) Lorem ipsum. 53 | 54 | ### Examples: 55 | 56 | var myThing = new MooToolsRails({ 57 | optionl: 1, 58 | option8: true 59 | }); 60 | 61 | ### Demos: 62 | 63 | - MooToolsRails - 64 | -------------------------------------------------------------------------------- /Specs/Assets/Scripts/Builder.js: -------------------------------------------------------------------------------- 1 | /* 2 | Script: Builder.js 3 | Automatically includes MooTools files right from the project folder. 4 | 5 | License: 6 | MIT-style license. 7 | 8 | Note: 9 | If you use this script in your own page, you must be out of your mind. 10 | */ 11 | 12 | var Builder = { 13 | 14 | root: '../', 15 | 16 | paths: { 17 | source: 'Source', 18 | specs: 'Specs' 19 | }, 20 | 21 | included: { 22 | source: {}, 23 | specs: {}, 24 | docs: {} 25 | }, 26 | 27 | scripts: { 28 | source: { 29 | '../Demos': ['mootools'], 30 | 'Plugins' : ['MooToolsRails'] 31 | }, 32 | 33 | specs: { 34 | 'Plugins' : ['MooToolsRails'] 35 | } 36 | }, 37 | 38 | initialize: function(root){ 39 | if (root) this.root = root; 40 | this.includeType('source'); 41 | return this; 42 | }, 43 | 44 | getFolder: function(type, file){ 45 | var scripts = this.scripts[type]; 46 | for (var folder in scripts){ 47 | for (var i = 0; i < scripts[folder].length; i++){ 48 | var script = scripts[folder][i]; 49 | if (script == file) return folder; 50 | } 51 | } 52 | return false; 53 | }, 54 | 55 | getRequest: function(){ 56 | var pairs = window.location.search.substring(1).split('&'); 57 | var obj = {}; 58 | for (var i = 0, l = pairs.length; i < l; i++){ 59 | var pair = pairs[i].split('='); 60 | obj[pair[0]] = pair[1]; 61 | } 62 | return obj; 63 | }, 64 | 65 | includeFile: function(type, folder, file){ 66 | folder = folder || this.getFolder(type, file); 67 | if (!folder) return false; 68 | this.included[type][folder] = this.included[type][folder] || []; 69 | var files = this.included[type][folder]; 70 | for (var i = 0; i < files.length; i++){ 71 | if (files[i] == file) return false; 72 | } 73 | this.included[type][folder].push(file); 74 | return document.writeln('\t'); 75 | }, 76 | 77 | includeFolder: function(type, folder){ 78 | var scripts = this.scripts[type][folder]; 79 | for (var i = 0, l = scripts.length; i < l; i++) this.includeFile(type, folder, scripts[i]); 80 | }, 81 | 82 | includeType: function(type){ 83 | for (var folder in this.scripts[type]) this.includeFolder(type, folder); 84 | }, 85 | 86 | includeRequest: function(type){ 87 | var req = this.getRequest(); 88 | if (!req.files && !req.folders) return false; 89 | var files = (req.files) ? req.files.split('+') : []; 90 | var folders = (req.folders) ? req.folders.split('+') : []; 91 | for (var j = 0; j < files.length; j++) this.includeFile(type, null, files[j]); 92 | for (var i = 0; i < folders.length; i++) this.includeFolder(type, folders[i]); 93 | return true; 94 | } 95 | 96 | }; -------------------------------------------------------------------------------- /Specs/Assets/Scripts/JSSpecSpecs.js: -------------------------------------------------------------------------------- 1 | /* 2 | Script: JSSpecSpecs.js 3 | Failure Examples for JSSpec.js 4 | 5 | License: 6 | MIT-style license. 7 | 8 | Note: 9 | All examples should fail to demonstrate the usage and output of all the availible types of specs in JSSpec. 10 | */ 11 | 12 | describe('Should be', { 13 | 'Array should be (Array) 1': function() { 14 | value_of(['ab','cd','ef']).should_be(['ab','bd','ef']); 15 | }, 16 | 'Array should be (Array) 2': function() { 17 | value_of(['a',2,'4',5]).should_be([1,2,[4,5,6],6,7]); 18 | }, 19 | 'Boolean should be (Boolean)': function() { 20 | value_of(true).should_be(false); 21 | }, 22 | 'Boolean should be false': function() { 23 | value_of(true).should_be_false(); 24 | }, 25 | 'Boolean should be true': function() { 26 | value_of(false).should_be_true(); 27 | }, 28 | 'Date should be (Date)': function() { 29 | value_of(new Date(1979, 3, 27)).should_be(new Date(1976, 7, 23)); 30 | }, 31 | 'Null should be (String)': function() { 32 | value_of(null).should_be("Test"); 33 | }, 34 | 'Null should be (undefined)': function() { 35 | value_of(null).should_be(undefined); 36 | }, 37 | 'Number should be (Number)': function() { 38 | value_of(1+2).should_be(4); 39 | }, 40 | 'Object should be (Object) 1': function() { 41 | var actual = {a:1, b:2}; 42 | var expected = {a:1, b:2, d:3}; 43 | value_of(actual).should_be(expected); 44 | }, 45 | 'Object should be (Object) 2': function() { 46 | var actual = {a:1, b:2, c:3, d:4}; 47 | var expected = {a:1, b:2, c:3}; 48 | value_of(actual).should_be(expected); 49 | }, 50 | 'Object should be (Object) 3': function() { 51 | var actual = {a:1, b:4, c:3}; 52 | var expected = {a:1, b:2, c:3}; 53 | value_of(actual).should_be(expected); 54 | }, 55 | 'String should be (String)': function() { 56 | value_of("Hello world").should_be("Good-bye world"); 57 | }, 58 | 'String should be (undefined)': function() { 59 | value_of("Test").should_be(undefined); 60 | } 61 | }); 62 | 63 | describe('Should be empty', { 64 | 'String should be empty': function() { 65 | value_of("").should_be_empty(); 66 | value_of("Hello").should_be_empty(); 67 | }, 68 | 'String should notbe empty': function() { 69 | value_of("Hello").should_not_be_empty(); 70 | value_of("").should_not_be_empty(); 71 | }, 72 | 'Array should be empty': function() { 73 | value_of([]).should_be_empty(); 74 | value_of([1,2,3]).should_be_empty(); 75 | }, 76 | 'Array should not be empty': function() { 77 | value_of([1,2,3]).should_not_be_empty(); 78 | value_of([]).should_not_be_empty(); 79 | }, 80 | 'Object\'s item should be empty': function() { 81 | value_of({name:'Alan Kang', email:'jania902@gmail.com', accounts:['A', 'B']}).should_have(0, "accounts"); 82 | }, 83 | 'Object\'s item should not be empty': function() { 84 | value_of({name:'Alan Kang', email:'jania902@gmail.com', accounts:[]}).should_have(2, "accounts"); 85 | } 86 | }); 87 | 88 | describe('Should have', { 89 | 'Array should have (Number, "items")': function() { 90 | value_of([1,2,3]).should_have(4, "items"); 91 | }, 92 | 'Array should have exactly (Number, "items")': function() { 93 | value_of([1,2,3]).should_have_exactly(2, "items"); 94 | }, 95 | 'Array should have at least (Number, "items")': function() { 96 | value_of([1,2,3]).should_have_at_least(4, "items"); 97 | }, 98 | 'Array should have at most (Number, "items")': function() { 99 | value_of([1,2,3]).should_have_at_most(2, "items"); 100 | }, 101 | 'Object\'s item should have (Number, "[property]s")': function() { 102 | value_of({name:'Alan Kang', email:'jania902@gmail.com', accounts:['A', 'B']}).should_have(3, "accounts"); 103 | }, 104 | 'String should have (Number, "characters")': function() { 105 | value_of("Hello").should_have(4, "characters"); 106 | }, 107 | 'String should have (Number, "[No match]s")': function() { 108 | value_of("This is a string").should_have(5, "players"); 109 | } 110 | }); 111 | 112 | describe('Should include', { 113 | 'Array should include (mixed)': function() { 114 | value_of([1,2,3]).should_include(4); 115 | }, 116 | 'Array should not include (mixed)': function() { 117 | value_of([1,2,3]).should_not_include(2); 118 | }, 119 | 'Non-Array should include (mixed)': function() { 120 | value_of(new Date()).should_include(4); 121 | }, 122 | 'Non-Array should not include (mixed)': function() { 123 | value_of(new Date()).should_not_include(4); 124 | } 125 | }); 126 | 127 | describe('Should match', { 128 | 'String should match (RegExp)': function() { 129 | value_of("Hello").should_match(/x/); 130 | }, 131 | 'String should not match (RegExp)': function() { 132 | value_of("Hello").should_not_match(/ell/); 133 | }, 134 | 'Array should match (RegExp)': function() { 135 | value_of([1,2,3]).should_match(/x/); 136 | }, 137 | 'Array should not match (RegExp)': function() { 138 | value_of([1,2,3]).should_not_match(/x/); 139 | } 140 | }); -------------------------------------------------------------------------------- /Specs/Assets/Styles/Specs.css: -------------------------------------------------------------------------------- 1 | /* 2 | File: Style.css 3 | Slick Style for JSSpec. 4 | 5 | License: 6 | Copyright (c)2007 Valerio Proietti, . 7 | You can only use this stylesheet to run MooTools JSSpec Runner. 8 | */ 9 | 10 | /* @group Reset */ 11 | 12 | * { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | 17 | ul { 18 | list-style: none; 19 | } 20 | 21 | /* @end */ 22 | 23 | /* @group Base */ 24 | 25 | html { 26 | overflow: hidden; 27 | } 28 | 29 | body { 30 | font: 11px/1.5 Lucida Grande, Helvetica, Arial, sans-serif; 31 | background: #F3F1F1; 32 | color: #41464D; 33 | } 34 | 35 | body, #container { 36 | width: 100%; 37 | height: 100%; 38 | overflow: hidden; 39 | } 40 | 41 | a { 42 | text-decoration:none; 43 | } 44 | 45 | #title { 46 | position: absolute; 47 | top: 0; 48 | left: 0; 49 | width: 100%; 50 | padding: 5px 0; 51 | background: #aaa; 52 | background: #41464D; 53 | color: #F3F1F1; 54 | height: 30px; 55 | } 56 | 57 | #list { 58 | position: absolute; 59 | width: 30%; 60 | overflow-y: auto; 61 | overflow-x: hidden; 62 | top: 40px; 63 | left: 0; 64 | bottom: 0; 65 | height: expression(document.body.clientHeight-40); 66 | } 67 | 68 | #log { 69 | position: absolute; 70 | top: 40px; 71 | right: 0; 72 | bottom: 0; 73 | overflow-y: auto; 74 | overflow-x: hidden; 75 | width: 70%; 76 | height: expression(document.body.clientHeight-40); 77 | } 78 | 79 | span.spc { 80 | display: block; 81 | height: 16px; 82 | } 83 | 84 | #log-wrapper, #list-wrapper { 85 | overflow: hidden; 86 | padding: 4px 4px 0; 87 | background: #fff; 88 | } 89 | 90 | #log-wrapper { 91 | margin: 16px 16px 0 8px; 92 | } 93 | 94 | #list-wrapper { 95 | margin: 16px 8px 0 16px; 96 | } 97 | 98 | .success a:link, .success a:visited { 99 | color: #657528; 100 | } 101 | 102 | .exception a:link, .exception a:visited { 103 | color: #B33F3F; 104 | } 105 | 106 | a:link, a:visited { 107 | color: #528CE0; 108 | } 109 | 110 | a:hover, a:active { 111 | color: #41464D !important; 112 | cursor: pointer !important; 113 | } 114 | 115 | #title h1 { 116 | font: 25px/1.1 Arial, sans-serif; 117 | font-weight: bolder; 118 | float: left; 119 | margin: 1px 0 2px 20px; 120 | text-shadow: 0 2px 2px rgba(0,0,0,0.4); 121 | } 122 | 123 | #title h1 span { 124 | color: #D2E0E6; 125 | } 126 | 127 | #title ul li { 128 | font-weight: bold; 129 | font-size: 12px; 130 | float: right; 131 | margin: 10px 5px 0; 132 | } 133 | 134 | #title ul { 135 | margin-right: 20px; 136 | } 137 | 138 | h2 { 139 | font-size: 14px; 140 | background: #D0C8C8; 141 | color: #8A7575; 142 | margin-bottom: 4px; 143 | padding: 2px 5px; 144 | } 145 | 146 | h2#runner { 147 | cursor: pointer; 148 | background-color: #CFE773; 149 | color: #657528; 150 | } 151 | 152 | h2#runner:hover { 153 | color: #41464D; 154 | } 155 | 156 | h2#runner.disabled { 157 | color: #fff; 158 | background: #C8CBD0; 159 | cursor: default; 160 | } 161 | 162 | br { 163 | display: none; 164 | } 165 | 166 | h3 { 167 | font-size: 12px; 168 | padding: 3px 5px 1px; 169 | } 170 | 171 | h4 { 172 | font-size: 11px; 173 | background: #C8CBD0; 174 | padding: 3px 5px 1px; 175 | margin-bottom: 4px; 176 | } 177 | 178 | h3, h4 { 179 | cursor: default; 180 | } 181 | 182 | #list h3 { 183 | background: #D2E0E6; 184 | color: #528CE0; 185 | margin-bottom: 4px; 186 | } 187 | 188 | #log h3 { 189 | background: #D0C8C8; 190 | color: #8A7575; 191 | margin-bottom: 4px; 192 | } 193 | 194 | #log li div { 195 | overflow: hidden; 196 | padding: 0 4px 4px; 197 | } 198 | 199 | p { 200 | color: #575d67; 201 | } 202 | 203 | p.uri { 204 | float: right; 205 | margin-top: -15px; 206 | font-size: 10px; 207 | } 208 | 209 | p.left, p.uri { 210 | font-family: Monaco, Courier New, monospace; 211 | } 212 | 213 | p#footer { 214 | text-align: right; 215 | padding: 10px 16px 0 0; 216 | } 217 | 218 | /* @end */ 219 | 220 | /* @group Success/Failure Colors */ 221 | 222 | #log .exception h3 { 223 | color: #B33F3F; 224 | background: #EE9A9A; 225 | } 226 | 227 | #log .exception h4 { 228 | color: #B33F3F; 229 | background: #eed8d8; 230 | cursor: pointer; 231 | } 232 | 233 | #log .success h3 { 234 | background-color: #CFE773; 235 | color: #657528; 236 | } 237 | 238 | #log .success h4 { 239 | background-color: #e2e5d2; 240 | color: #657528; 241 | cursor: default; 242 | } 243 | 244 | #log .stub h4 { 245 | background-color: #e5e5e1; 246 | color: #a1a87e; 247 | cursor: default; 248 | } 249 | 250 | #list .exception h3 { 251 | color: #B33F3F; 252 | background: #eed8d8; 253 | } 254 | 255 | #list .success h3 { 256 | background-color: #e2e5d2; 257 | color: #657528; 258 | } 259 | 260 | a.rerun { 261 | font-size: 11px; 262 | color: #fff !important; 263 | } 264 | 265 | /* @end */ 266 | 267 | /* @group Code */ 268 | 269 | .number_value { 270 | color: red; 271 | } 272 | 273 | .string_value { 274 | color: green; 275 | } 276 | 277 | .regexp_value { 278 | color: olive; 279 | } 280 | 281 | .boolean_value { 282 | color: red; 283 | } 284 | 285 | .dom_value { 286 | color: purple; 287 | } 288 | 289 | .undefined_value, .null_value { 290 | color: gray; 291 | } 292 | 293 | /* @end */ -------------------------------------------------------------------------------- /Specs/Assets/Scripts/JSSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JSSpec 3 | * 4 | * Copyright 2007 Alan Kang 5 | * - mailto:jania902@gmail.com 6 | * - http://jania.pe.kr 7 | * 8 | * http://jania.pe.kr/aw/moin.cgi/JSSpec 9 | * 10 | * Dependencies: 11 | * - diff_match_patch.js ( http://code.google.com/p/google-diff-match-patch ) 12 | * 13 | * This library is free software; you can redistribute it and/or 14 | * modify it under the terms of the GNU Lesser General Public 15 | * License as published by the Free Software Foundation; either 16 | * version 2.1 of the License, or (at your option) any later version. 17 | * 18 | * This library is distributed in the hope that it will be useful, 19 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 21 | * Lesser General Public License for more details. 22 | * 23 | * You should have received a copy of the GNU Lesser General Public 24 | * License along with this library; if not, write to the Free Software 25 | * Foundation, Inc, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 26 | */ 27 | 28 | /** 29 | * Namespace 30 | */ 31 | 32 | var JSSpec = { 33 | specs: [], 34 | 35 | EMPTY_FUNCTION: function() {}, 36 | 37 | Browser: { 38 | Trident: navigator.appName == "Microsoft Internet Explorer", 39 | Webkit: navigator.userAgent.indexOf('AppleWebKit/') > -1, 40 | Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1, 41 | Presto: navigator.appName == "Opera" 42 | } 43 | }; 44 | 45 | 46 | 47 | /** 48 | * Executor 49 | */ 50 | JSSpec.Executor = function(target, onSuccess, onException) { 51 | this.target = target; 52 | this.onSuccess = typeof onSuccess == 'function' ? onSuccess : JSSpec.EMPTY_FUNCTION; 53 | this.onException = typeof onException == 'function' ? onException : JSSpec.EMPTY_FUNCTION; 54 | 55 | if(JSSpec.Browser.Trident) { 56 | // Exception handler for Trident. It helps to collect exact line number where exception occured. 57 | window.onerror = function(message, fileName, lineNumber) { 58 | var self = window._curExecutor; 59 | var ex = {message:message, fileName:fileName, lineNumber:lineNumber}; 60 | 61 | if(JSSpec._secondPass) { 62 | ex = self.mergeExceptions(JSSpec._assertionFailure, ex); 63 | delete JSSpec._secondPass; 64 | delete JSSpec._assertionFailure; 65 | 66 | ex.type = "failure"; 67 | self.onException(self, ex); 68 | } else if(JSSpec._assertionFailure) { 69 | JSSpec._secondPass = true; 70 | self.run(); 71 | } else { 72 | self.onException(self, ex); 73 | } 74 | 75 | return true; 76 | }; 77 | } 78 | }; 79 | JSSpec.Executor.prototype.mergeExceptions = function(assertionFailure, normalException) { 80 | var merged = { 81 | message:assertionFailure.message, 82 | fileName:normalException.fileName, 83 | lineNumber:normalException.lineNumber 84 | }; 85 | 86 | return merged; 87 | }; 88 | 89 | JSSpec.Executor.prototype.run = function() { 90 | var self = this; 91 | var target = this.target; 92 | var onSuccess = this.onSuccess; 93 | var onException = this.onException; 94 | 95 | window.setTimeout( 96 | function() { 97 | var result; 98 | if(JSSpec.Browser.Trident) { 99 | window._curExecutor = self; 100 | 101 | result = self.target(); 102 | self.onSuccess(self, result); 103 | } else { 104 | try { 105 | result = self.target(); 106 | self.onSuccess(self, result); 107 | } catch(ex) { 108 | if(JSSpec.Browser.Webkit) ex = {message:ex.message, fileName:ex.sourceURL, lineNumber:ex.line}; 109 | 110 | if(JSSpec._secondPass) { 111 | ex = self.mergeExceptions(JSSpec._assertionFailure, ex); 112 | delete JSSpec._secondPass; 113 | delete JSSpec._assertionFailure; 114 | 115 | ex.type = "failure"; 116 | self.onException(self, ex); 117 | } else if(JSSpec._assertionFailure) { 118 | JSSpec._secondPass = true; 119 | self.run(); 120 | } else { 121 | self.onException(self, ex); 122 | } 123 | } 124 | } 125 | }, 126 | 0 127 | ); 128 | }; 129 | 130 | 131 | 132 | /** 133 | * CompositeExecutor composites one or more executors and execute them sequencially. 134 | */ 135 | JSSpec.CompositeExecutor = function(onSuccess, onException, continueOnException) { 136 | this.queue = []; 137 | this.onSuccess = typeof onSuccess == 'function' ? onSuccess : JSSpec.EMPTY_FUNCTION; 138 | this.onException = typeof onException == 'function' ? onException : JSSpec.EMPTY_FUNCTION; 139 | this.continueOnException = !!continueOnException; 140 | }; 141 | 142 | JSSpec.CompositeExecutor.prototype.addFunction = function(func) { 143 | this.addExecutor(new JSSpec.Executor(func)); 144 | }; 145 | 146 | JSSpec.CompositeExecutor.prototype.addExecutor = function(executor) { 147 | var last = this.queue.length == 0 ? null : this.queue[this.queue.length - 1]; 148 | if(last) { 149 | last.next = executor; 150 | } 151 | 152 | executor.parent = this; 153 | executor.onSuccessBackup = executor.onSuccess; 154 | executor.onSuccess = function(result) { 155 | this.onSuccessBackup(result); 156 | if(this.next) { 157 | this.next.run(); 158 | } else { 159 | this.parent.onSuccess(); 160 | } 161 | }; 162 | executor.onExceptionBackup = executor.onException; 163 | executor.onException = function(executor, ex) { 164 | this.onExceptionBackup(executor, ex); 165 | 166 | if(this.parent.continueOnException) { 167 | if(this.next) { 168 | this.next.run(); 169 | } else { 170 | this.parent.onSuccess(); 171 | } 172 | } else { 173 | this.parent.onException(executor, ex); 174 | } 175 | }; 176 | 177 | this.queue.push(executor); 178 | }; 179 | 180 | JSSpec.CompositeExecutor.prototype.run = function() { 181 | if(this.queue.length > 0) { 182 | this.queue[0].run(); 183 | } 184 | }; 185 | 186 | /** 187 | * Spec is a set of Examples in a specific context 188 | */ 189 | JSSpec.Spec = function(context, entries) { 190 | this.id = JSSpec.Spec.id++; 191 | this.context = context; 192 | this.url = location.href; 193 | 194 | this.filterEntriesByEmbeddedExpressions(entries); 195 | this.extractOutSpecialEntries(entries); 196 | this.examples = this.makeExamplesFromEntries(entries); 197 | this.examplesMap = this.makeMapFromExamples(this.examples); 198 | }; 199 | 200 | JSSpec.Spec.id = 0; 201 | JSSpec.Spec.prototype.getExamples = function() { 202 | return this.examples; 203 | }; 204 | 205 | JSSpec.Spec.prototype.hasException = function() { 206 | return this.getTotalFailures() > 0 || this.getTotalErrors() > 0; 207 | }; 208 | 209 | JSSpec.Spec.prototype.getTotalFailures = function() { 210 | var examples = this.examples; 211 | var failures = 0; 212 | for(var i = 0; i < examples.length; i++) { 213 | if(examples[i].isFailure()) failures++; 214 | } 215 | return failures; 216 | }; 217 | 218 | JSSpec.Spec.prototype.getTotalErrors = function() { 219 | var examples = this.examples; 220 | var errors = 0; 221 | for(var i = 0; i < examples.length; i++) { 222 | if(examples[i].isError()) errors++; 223 | } 224 | return errors; 225 | }; 226 | 227 | JSSpec.Spec.prototype.filterEntriesByEmbeddedExpressions = function(entries) { 228 | var isTrue; 229 | for(name in entries) { 230 | var m = name.match(/\[\[(.+)\]\]/); 231 | if(m && m[1]) { 232 | eval("isTrue = (" + m[1] + ")"); 233 | if(!isTrue) delete entries[name]; 234 | } 235 | } 236 | }; 237 | 238 | JSSpec.Spec.prototype.extractOutSpecialEntries = function(entries) { 239 | this.beforeEach = JSSpec.EMPTY_FUNCTION; 240 | this.beforeAll = JSSpec.EMPTY_FUNCTION; 241 | this.afterEach = JSSpec.EMPTY_FUNCTION; 242 | this.afterAll = JSSpec.EMPTY_FUNCTION; 243 | 244 | for(name in entries) { 245 | if(name == 'before' || name == 'before each' || name == 'before_each') { 246 | this.beforeEach = entries[name]; 247 | } else if(name == 'before all' || name == 'before_all') { 248 | this.beforeAll = entries[name]; 249 | } else if(name == 'after' || name == 'after each' || name == 'after_each') { 250 | this.afterEach = entries[name]; 251 | } else if(name == 'after all' || name == 'after_all') { 252 | this.afterAll = entries[name]; 253 | } 254 | } 255 | 256 | delete entries['before']; 257 | delete entries['before each']; 258 | delete entries['before_each']; 259 | delete entries['before all']; 260 | delete entries['before_all']; 261 | delete entries['after']; 262 | delete entries['after each']; 263 | delete entries['after_each']; 264 | delete entries['after all']; 265 | delete entries['after_all']; 266 | }; 267 | 268 | JSSpec.Spec.prototype.makeExamplesFromEntries = function(entries) { 269 | var examples = []; 270 | for(name in entries) { 271 | examples.push(new JSSpec.Example(name, entries[name], this.beforeEach, this.afterEach)); 272 | } 273 | return examples; 274 | }; 275 | 276 | JSSpec.Spec.prototype.makeMapFromExamples = function(examples) { 277 | var map = {}; 278 | for(var i = 0; i < examples.length; i++) { 279 | var example = examples[i]; 280 | map[example.id] = examples[i]; 281 | } 282 | return map; 283 | }; 284 | 285 | JSSpec.Spec.prototype.getExampleById = function(id) { 286 | return this.examplesMap[id]; 287 | }; 288 | 289 | JSSpec.Spec.prototype.getExecutor = function() { 290 | var self = this; 291 | var onException = function(executor, ex) { 292 | self.exception = ex; 293 | }; 294 | 295 | var composite = new JSSpec.CompositeExecutor(); 296 | composite.addFunction(function() {JSSpec.log.onSpecStart(self);}); 297 | composite.addExecutor(new JSSpec.Executor(this.beforeAll, null, function(exec, ex) { 298 | self.exception = ex; 299 | JSSpec.log.onSpecEnd(self); 300 | })); 301 | 302 | var exampleAndAfter = new JSSpec.CompositeExecutor(null,null,true); 303 | for(var i = 0; i < this.examples.length; i++) { 304 | exampleAndAfter.addExecutor(this.examples[i].getExecutor()); 305 | } 306 | exampleAndAfter.addExecutor(new JSSpec.Executor(this.afterAll, null, onException)); 307 | exampleAndAfter.addFunction(function() {JSSpec.log.onSpecEnd(self);}); 308 | composite.addExecutor(exampleAndAfter); 309 | 310 | return composite; 311 | }; 312 | 313 | /** 314 | * Example 315 | */ 316 | JSSpec.Example = function(name, target, before, after) { 317 | this.id = JSSpec.Example.id++; 318 | this.name = name; 319 | this.target = target; 320 | this.before = before; 321 | this.after = after; 322 | }; 323 | 324 | JSSpec.Example.id = 0; 325 | JSSpec.Example.prototype.isFailure = function() { 326 | return this.exception && this.exception.type == "failure"; 327 | }; 328 | 329 | JSSpec.Example.prototype.isError = function() { 330 | return this.exception && !this.exception.type; 331 | }; 332 | 333 | JSSpec.Example.prototype.getExecutor = function() { 334 | var self = this; 335 | var onException = function(executor, ex) { 336 | self.exception = ex; 337 | }; 338 | 339 | var composite = new JSSpec.CompositeExecutor(); 340 | composite.addFunction(function() {JSSpec.log.onExampleStart(self);}); 341 | composite.addExecutor(new JSSpec.Executor(this.before, null, function(exec, ex) { 342 | self.exception = ex; 343 | JSSpec.log.onExampleEnd(self); 344 | })); 345 | 346 | var targetAndAfter = new JSSpec.CompositeExecutor(null,null,true); 347 | 348 | targetAndAfter.addExecutor(new JSSpec.Executor(this.target, null, onException)); 349 | targetAndAfter.addExecutor(new JSSpec.Executor(this.after, null, onException)); 350 | targetAndAfter.addFunction(function() {JSSpec.log.onExampleEnd(self);}); 351 | 352 | composite.addExecutor(targetAndAfter); 353 | 354 | return composite; 355 | }; 356 | 357 | /** 358 | * Runner 359 | */ 360 | JSSpec.Runner = function(specs, logger) { 361 | JSSpec.log = logger; 362 | 363 | this.totalExamples = 0; 364 | this.specs = []; 365 | this.specsMap = {}; 366 | this.addAllSpecs(specs); 367 | }; 368 | 369 | JSSpec.Runner.prototype.addAllSpecs = function(specs) { 370 | for(var i = 0; i < specs.length; i++) { 371 | this.addSpec(specs[i]); 372 | } 373 | }; 374 | 375 | JSSpec.Runner.prototype.addSpec = function(spec) { 376 | this.specs.push(spec); 377 | this.specsMap[spec.id] = spec; 378 | this.totalExamples += spec.getExamples().length; 379 | }; 380 | 381 | JSSpec.Runner.prototype.getSpecById = function(id) { 382 | return this.specsMap[id]; 383 | }; 384 | 385 | JSSpec.Runner.prototype.getSpecByContext = function(context) { 386 | for(var i = 0; i < this.specs.length; i++) { 387 | if(this.specs[i].context == context) return this.specs[i]; 388 | } 389 | return null; 390 | }; 391 | 392 | JSSpec.Runner.prototype.getSpecs = function() { 393 | return this.specs; 394 | }; 395 | 396 | JSSpec.Runner.prototype.hasException = function() { 397 | return this.getTotalFailures() > 0 || this.getTotalErrors() > 0; 398 | }; 399 | 400 | JSSpec.Runner.prototype.getTotalFailures = function() { 401 | var specs = this.specs; 402 | var failures = 0; 403 | for(var i = 0; i < specs.length; i++) { 404 | failures += specs[i].getTotalFailures(); 405 | } 406 | return failures; 407 | }; 408 | 409 | JSSpec.Runner.prototype.getTotalErrors = function() { 410 | var specs = this.specs; 411 | var errors = 0; 412 | for(var i = 0; i < specs.length; i++) { 413 | errors += specs[i].getTotalErrors(); 414 | } 415 | return errors; 416 | }; 417 | 418 | 419 | JSSpec.Runner.prototype.run = function() { 420 | JSSpec.log.onRunnerStart(); 421 | var executor = new JSSpec.CompositeExecutor(function() {JSSpec.log.onRunnerEnd()},null,true); 422 | for(var i = 0; i < this.specs.length; i++) { 423 | executor.addExecutor(this.specs[i].getExecutor()); 424 | } 425 | executor.run(); 426 | }; 427 | 428 | 429 | JSSpec.Runner.prototype.rerun = function(context) { 430 | JSSpec.runner = new JSSpec.Runner([this.getSpecByContext(context)], JSSpec.log); 431 | JSSpec.runner.run(); 432 | }; 433 | 434 | /** 435 | * Logger 436 | */ 437 | JSSpec.Logger = function() { 438 | this.finishedExamples = 0; 439 | this.startedAt = null; 440 | }; 441 | 442 | JSSpec.Logger.prototype.onRunnerStart = function() { 443 | this._title = document.title; 444 | 445 | this.startedAt = new Date(); 446 | var container = document.getElementById('jsspec_container'); 447 | if(container) { 448 | container.innerHTML = ""; 449 | } else { 450 | container = document.createElement("DIV"); 451 | container.id = "jsspec_container"; 452 | document.body.appendChild(container); 453 | } 454 | 455 | var title = document.createElement("DIV"); 456 | title.id = "title"; 457 | title.innerHTML = [ 458 | '

JSSpec

', 459 | '
    ', 460 | JSSpec.options.rerun ? '
  • [X] ' + JSSpec.util.escapeTags(decodeURIComponent(JSSpec.options.rerun)) + '
  • ' : '', 461 | '
  • ' + JSSpec.runner.totalExamples + ' examples
  • ', 462 | '
  • 0 failures
  • ', 463 | '
  • 0 errors
  • ', 464 | '
  • 0% done
  • ', 465 | '
  • 0 secs
  • ', 466 | '
', 467 | '

JSSpec homepage

', 468 | ].join(""); 469 | container.appendChild(title); 470 | 471 | var list = document.createElement("DIV"); 472 | list.id = "list"; 473 | list.innerHTML = [ 474 | '

List

', 475 | '
    ', 476 | function() { 477 | var specs = JSSpec.runner.getSpecs(); 478 | var sb = []; 479 | for(var i = 0; i < specs.length; i++) { 480 | var spec = specs[i]; 481 | sb.push('
  • ' + JSSpec.util.escapeTags(specs[i].context) + ' [rerun]

  • '); 482 | } 483 | return sb.join(""); 484 | }(), 485 | '
' 486 | ].join(""); 487 | container.appendChild(list); 488 | 489 | var log = document.createElement("DIV"); 490 | log.id = "log"; 491 | log.innerHTML = [ 492 | '

Log

', 493 | '
    ', 494 | function() { 495 | var specs = JSSpec.runner.getSpecs(); 496 | var sb = []; 497 | for(var i = 0; i < specs.length; i++) { 498 | var spec = specs[i]; 499 | sb.push('
  • '); 500 | sb.push('

    ' + JSSpec.util.escapeTags(specs[i].context) + ' [rerun]

    '); 501 | sb.push('
      '); 502 | for(var j = 0; j < spec.examples.length; j++) { 503 | var example = spec.examples[j]; 504 | sb.push('
    • ') 505 | sb.push('

      ' + JSSpec.util.escapeTags(example.name) + '

      ') 506 | sb.push('
    • ') 507 | } 508 | sb.push('
    '); 509 | sb.push('
  • '); 510 | } 511 | return sb.join(""); 512 | }(), 513 | '
' 514 | ].join(""); 515 | 516 | container.appendChild(log); 517 | 518 | // add event handler for toggling 519 | var specs = JSSpec.runner.getSpecs(); 520 | var sb = []; 521 | for(var i = 0; i < specs.length; i++) { 522 | var spec = document.getElementById("spec_" + specs[i].id); 523 | var title = spec.getElementsByTagName("H3")[0]; 524 | title.onclick = function(e) { 525 | var target = document.getElementById(this.parentNode.id + "_examples"); 526 | target.style.display = target.style.display == "none" ? "block" : "none"; 527 | return true; 528 | } 529 | } 530 | }; 531 | 532 | JSSpec.Logger.prototype.onRunnerEnd = function() { 533 | if(JSSpec.runner.hasException()) { 534 | var times = 4; 535 | var title1 = "*" + this._title; 536 | var title2 = "*F" + JSSpec.runner.getTotalFailures() + " E" + JSSpec.runner.getTotalErrors() + "* " + this._title; 537 | } else { 538 | var times = 2; 539 | var title1 = this._title; 540 | var title2 = "Success"; 541 | } 542 | this.blinkTitle(times,title1,title2); 543 | }; 544 | 545 | JSSpec.Logger.prototype.blinkTitle = function(times, title1, title2) { 546 | var times = times * 2; 547 | var mode = true; 548 | 549 | var f = function() { 550 | if(times > 0) { 551 | document.title = mode ? title1 : title2; 552 | mode = !mode; 553 | times--; 554 | window.setTimeout(f, 500); 555 | } else { 556 | document.title = title1; 557 | } 558 | }; 559 | 560 | f(); 561 | }; 562 | 563 | JSSpec.Logger.prototype.onSpecStart = function(spec) { 564 | var spec_list = document.getElementById("spec_" + spec.id + "_list"); 565 | var spec_log = document.getElementById("spec_" + spec.id); 566 | 567 | spec_list.className = "ongoing"; 568 | spec_log.className = "ongoing"; 569 | }; 570 | 571 | JSSpec.Logger.prototype.onSpecEnd = function(spec) { 572 | var spec_list = document.getElementById("spec_" + spec.id + "_list"); 573 | var spec_log = document.getElementById("spec_" + spec.id); 574 | var examples = document.getElementById("spec_" + spec.id + "_examples"); 575 | var className = spec.hasException() ? "exception" : "success"; 576 | 577 | spec_list.className = className; 578 | spec_log.className = className; 579 | 580 | if(JSSpec.options.autocollapse && !spec.hasException()) examples.style.display = "none"; 581 | 582 | if(spec.exception) { 583 | heading.appendChild(document.createTextNode(" - " + spec.exception.message)); 584 | } 585 | }; 586 | 587 | JSSpec.Logger.prototype.onExampleStart = function(example) { 588 | var li = document.getElementById("example_" + example.id); 589 | li.className = "ongoing"; 590 | }; 591 | 592 | JSSpec.Logger.prototype.onExampleEnd = function(example) { 593 | var li = document.getElementById("example_" + example.id); 594 | li.className = example.exception ? "exception" : "success"; 595 | 596 | if(example.exception) { 597 | var div = document.createElement("DIV"); 598 | div.innerHTML = example.exception.message + "


" + " at " + example.exception.fileName + ", line " + example.exception.lineNumber + "

"; 599 | li.appendChild(div); 600 | } 601 | 602 | var title = document.getElementById("title"); 603 | var runner = JSSpec.runner; 604 | 605 | title.className = runner.hasException() ? "exception" : "success"; 606 | 607 | this.finishedExamples++; 608 | document.getElementById("total_failures").innerHTML = runner.getTotalFailures(); 609 | document.getElementById("total_errors").innerHTML = runner.getTotalErrors(); 610 | var progress = parseInt(this.finishedExamples / runner.totalExamples * 100); 611 | document.getElementById("progress").innerHTML = progress; 612 | document.getElementById("total_elapsed").innerHTML = (new Date().getTime() - this.startedAt.getTime()) / 1000; 613 | 614 | document.title = progress + "%: " + this._title; 615 | }; 616 | 617 | /** 618 | * IncludeMatcher 619 | */ 620 | JSSpec.IncludeMatcher = function(actual, expected, condition) { 621 | this.actual = actual; 622 | this.expected = expected; 623 | this.condition = condition; 624 | this.match = false; 625 | this.explaination = this.makeExplain(); 626 | }; 627 | 628 | JSSpec.IncludeMatcher.createInstance = function(actual, expected, condition) { 629 | return new JSSpec.IncludeMatcher(actual, expected, condition); 630 | }; 631 | 632 | JSSpec.IncludeMatcher.prototype.matches = function() { 633 | return this.match; 634 | }; 635 | 636 | JSSpec.IncludeMatcher.prototype.explain = function() { 637 | return this.explaination; 638 | }; 639 | 640 | JSSpec.IncludeMatcher.prototype.makeExplain = function() { 641 | if(typeof this.actual.length == 'undefined') { 642 | return this.makeExplainForNotArray(); 643 | } else { 644 | return this.makeExplainForArray(); 645 | } 646 | }; 647 | 648 | JSSpec.IncludeMatcher.prototype.makeExplainForNotArray = function() { 649 | if(this.condition) { 650 | this.match = !!this.actual[this.expected]; 651 | } else { 652 | this.match = !this.actual[this.expected]; 653 | } 654 | 655 | var sb = []; 656 | sb.push('

actual value:

'); 657 | sb.push('

' + JSSpec.util.inspect(this.actual, false, this.expected) + '

'); 658 | sb.push('

should ' + (this.condition ? '' : 'not') + ' include:

'); 659 | sb.push('

' + JSSpec.util.inspect(this.expected) + '

'); 660 | return sb.join(""); 661 | } 662 | ; 663 | JSSpec.IncludeMatcher.prototype.makeExplainForArray = function() { 664 | var matches; 665 | if(this.condition) { 666 | for(var i = 0; i < this.actual.length; i++) { 667 | matches = JSSpec.EqualityMatcher.createInstance(this.expected, this.actual[i]).matches(); 668 | if(matches) { 669 | this.match = true; 670 | break; 671 | } 672 | } 673 | } else { 674 | for(var i = 0; i < this.actual.length; i++) { 675 | matches = JSSpec.EqualityMatcher.createInstance(this.expected, this.actual[i]).matches(); 676 | if(matches) { 677 | this.match = false; 678 | break; 679 | } 680 | } 681 | } 682 | 683 | if(this.match) return ""; 684 | 685 | var sb = []; 686 | sb.push('

actual value:

'); 687 | sb.push('

' + JSSpec.util.inspect(this.actual, false, this.condition ? null : i) + '

'); 688 | sb.push('

should ' + (this.condition ? '' : 'not') + ' include:

'); 689 | sb.push('

' + JSSpec.util.inspect(this.expected) + '

'); 690 | return sb.join(""); 691 | }; 692 | 693 | /** 694 | * PropertyLengthMatcher 695 | */ 696 | JSSpec.PropertyLengthMatcher = function(num, property, o, condition) { 697 | this.num = num; 698 | this.o = o; 699 | this.property = property; 700 | if((property == 'characters' || property == 'items') && typeof o.length != 'undefined') { 701 | this.property = 'length'; 702 | } 703 | 704 | this.condition = condition; 705 | this.conditionMet = function(x) { 706 | if(condition == 'exactly') return x.length == num; 707 | if(condition == 'at least') return x.length >= num; 708 | if(condition == 'at most') return x.length <= num; 709 | 710 | throw "Unknown condition '" + condition + "'"; 711 | }; 712 | this.match = false; 713 | this.explaination = this.makeExplain(); 714 | }; 715 | 716 | JSSpec.PropertyLengthMatcher.prototype.makeExplain = function() { 717 | if(this.o._type == 'String' && this.property == 'length') { 718 | this.match = this.conditionMet(this.o); 719 | return this.match ? '' : this.makeExplainForString(); 720 | } else if(typeof this.o.length != 'undefined' && this.property == "length") { 721 | this.match = this.conditionMet(this.o); 722 | return this.match ? '' : this.makeExplainForArray(); 723 | } else if(typeof this.o[this.property] != 'undefined' && this.o[this.property] != null) { 724 | this.match = this.conditionMet(this.o[this.property]); 725 | return this.match ? '' : this.makeExplainForObject(); 726 | } else if(typeof this.o[this.property] == 'undefined' || this.o[this.property] == null) { 727 | this.match = false; 728 | return this.makeExplainForNoProperty(); 729 | } 730 | 731 | this.match = true; 732 | }; 733 | 734 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForString = function() { 735 | var sb = []; 736 | 737 | var exp = this.num == 0 ? 738 | 'be an empty string' : 739 | 'have ' + this.condition + ' ' + this.num + ' characters'; 740 | 741 | sb.push('

actual value has ' + this.o.length + ' characters:

'); 742 | sb.push('

' + JSSpec.util.inspect(this.o) + '

'); 743 | sb.push('

but it should ' + exp + '.

'); 744 | 745 | return sb.join(""); 746 | }; 747 | 748 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForArray = function() { 749 | var sb = []; 750 | 751 | var exp = this.num == 0 ? 752 | 'be an empty array' : 753 | 'have ' + this.condition + ' ' + this.num + ' items'; 754 | 755 | sb.push('

actual value has ' + this.o.length + ' items:

'); 756 | sb.push('

' + JSSpec.util.inspect(this.o) + '

'); 757 | sb.push('

but it should ' + exp + '.

'); 758 | 759 | return sb.join(""); 760 | }; 761 | 762 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForObject = function() { 763 | var sb = []; 764 | 765 | var exp = this.num == 0 ? 766 | 'be empty' : 767 | 'have ' + this.condition + ' ' + this.num + ' ' + this.property + '.'; 768 | 769 | sb.push('

actual value has ' + this.o[this.property].length + ' ' + this.property + ':

'); 770 | sb.push('

' + JSSpec.util.inspect(this.o, false, this.property) + '

'); 771 | sb.push('

but it should ' + exp + '.

'); 772 | 773 | return sb.join(""); 774 | }; 775 | 776 | JSSpec.PropertyLengthMatcher.prototype.makeExplainForNoProperty = function() { 777 | var sb = []; 778 | 779 | sb.push('

actual value:

'); 780 | sb.push('

' + JSSpec.util.inspect(this.o) + '

'); 781 | sb.push('

should have ' + this.condition + ' ' + this.num + ' ' + this.property + ' but there\'s no such property.

'); 782 | 783 | return sb.join(""); 784 | }; 785 | 786 | JSSpec.PropertyLengthMatcher.prototype.matches = function() { 787 | return this.match; 788 | }; 789 | 790 | JSSpec.PropertyLengthMatcher.prototype.explain = function() { 791 | return this.explaination; 792 | }; 793 | 794 | JSSpec.PropertyLengthMatcher.createInstance = function(num, property, o, condition) { 795 | return new JSSpec.PropertyLengthMatcher(num, property, o, condition); 796 | }; 797 | 798 | /** 799 | * EqualityMatcher 800 | */ 801 | JSSpec.EqualityMatcher = {}; 802 | 803 | JSSpec.EqualityMatcher.createInstance = function(expected, actual) { 804 | if(expected == null || actual == null) { 805 | return new JSSpec.NullEqualityMatcher(expected, actual); 806 | } else if(expected._type && expected._type == actual._type) { 807 | if(expected._type == "String") { 808 | return new JSSpec.StringEqualityMatcher(expected, actual); 809 | } else if(expected._type == "Date") { 810 | return new JSSpec.DateEqualityMatcher(expected, actual); 811 | } else if(expected._type == "Number") { 812 | return new JSSpec.NumberEqualityMatcher(expected, actual); 813 | } else if(expected._type == "Array") { 814 | return new JSSpec.ArrayEqualityMatcher(expected, actual); 815 | } else if(expected._type == "Boolean") { 816 | return new JSSpec.BooleanEqualityMatcher(expected, actual); 817 | } 818 | } 819 | 820 | return new JSSpec.ObjectEqualityMatcher(expected, actual); 821 | }; 822 | 823 | JSSpec.EqualityMatcher.basicExplain = function(expected, actual, expectedDesc, actualDesc) { 824 | var sb = []; 825 | 826 | sb.push(actualDesc || '

actual value:

'); 827 | sb.push('

' + JSSpec.util.inspect(actual) + '

'); 828 | sb.push(expectedDesc || '

should be:

'); 829 | sb.push('

' + JSSpec.util.inspect(expected) + '

'); 830 | 831 | return sb.join(""); 832 | }; 833 | 834 | JSSpec.EqualityMatcher.diffExplain = function(expected, actual) { 835 | var sb = []; 836 | 837 | sb.push('

diff:

'); 838 | sb.push('

'); 839 | 840 | var dmp = new diff_match_patch(); 841 | var diff = dmp.diff_main(expected, actual); 842 | dmp.diff_cleanupEfficiency(diff); 843 | 844 | sb.push(JSSpec.util.inspect(dmp.diff_prettyHtml(diff), true)); 845 | 846 | sb.push('

'); 847 | 848 | return sb.join(""); 849 | }; 850 | 851 | /** 852 | * BooleanEqualityMatcher 853 | */ 854 | JSSpec.BooleanEqualityMatcher = function(expected, actual) { 855 | this.expected = expected; 856 | this.actual = actual; 857 | }; 858 | 859 | JSSpec.BooleanEqualityMatcher.prototype.explain = function() { 860 | var sb = []; 861 | 862 | sb.push('

actual value:

'); 863 | sb.push('

' + JSSpec.util.inspect(this.actual) + '

'); 864 | sb.push('

should be:

'); 865 | sb.push('

' + JSSpec.util.inspect(this.expected) + '

'); 866 | 867 | return sb.join(""); 868 | }; 869 | 870 | JSSpec.BooleanEqualityMatcher.prototype.matches = function() { 871 | return this.expected == this.actual; 872 | }; 873 | 874 | /** 875 | * NullEqualityMatcher 876 | */ 877 | JSSpec.NullEqualityMatcher = function(expected, actual) { 878 | this.expected = expected; 879 | this.actual = actual; 880 | }; 881 | 882 | JSSpec.NullEqualityMatcher.prototype.matches = function() { 883 | return this.expected == this.actual && typeof this.expected == typeof this.actual; 884 | }; 885 | 886 | JSSpec.NullEqualityMatcher.prototype.explain = function() { 887 | return JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual); 888 | }; 889 | 890 | JSSpec.DateEqualityMatcher = function(expected, actual) { 891 | this.expected = expected; 892 | this.actual = actual; 893 | }; 894 | 895 | JSSpec.DateEqualityMatcher.prototype.matches = function() { 896 | return this.expected.getTime() == this.actual.getTime(); 897 | }; 898 | 899 | JSSpec.DateEqualityMatcher.prototype.explain = function() { 900 | var sb = []; 901 | 902 | sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual)); 903 | sb.push(JSSpec.EqualityMatcher.diffExplain(this.expected.toString(), this.actual.toString())); 904 | 905 | return sb.join(""); 906 | }; 907 | 908 | /** 909 | * ObjectEqualityMatcher 910 | */ 911 | JSSpec.ObjectEqualityMatcher = function(expected, actual) { 912 | this.expected = expected; 913 | this.actual = actual; 914 | this.match = this.expected == this.actual; 915 | this.explaination = this.makeExplain(); 916 | }; 917 | 918 | JSSpec.ObjectEqualityMatcher.prototype.matches = function() {return this.match}; 919 | 920 | JSSpec.ObjectEqualityMatcher.prototype.explain = function() {return this.explaination}; 921 | 922 | JSSpec.ObjectEqualityMatcher.prototype.makeExplain = function() { 923 | if(this.expected == this.actual) { 924 | this.match = true; 925 | return ""; 926 | } 927 | 928 | if(JSSpec.util.isDomNode(this.expected)) { 929 | return this.makeExplainForDomNode(); 930 | } 931 | 932 | var key, expectedHasItem, actualHasItem; 933 | 934 | for(key in this.expected) { 935 | expectedHasItem = this.expected[key] != null && typeof this.expected[key] != 'undefined'; 936 | actualHasItem = this.actual[key] != null && typeof this.actual[key] != 'undefined'; 937 | if(expectedHasItem && !actualHasItem) return this.makeExplainForMissingItem(key); 938 | } 939 | for(key in this.actual) { 940 | expectedHasItem = this.expected[key] != null && typeof this.expected[key] != 'undefined'; 941 | actualHasItem = this.actual[key] != null && typeof this.actual[key] != 'undefined'; 942 | if(actualHasItem && !expectedHasItem) return this.makeExplainForUnknownItem(key); 943 | } 944 | 945 | for(key in this.expected) { 946 | var matcher = JSSpec.EqualityMatcher.createInstance(this.expected[key], this.actual[key]); 947 | if(!matcher.matches()) return this.makeExplainForItemMismatch(key); 948 | } 949 | 950 | this.match = true; 951 | }; 952 | 953 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForDomNode = function(key) { 954 | var sb = []; 955 | 956 | sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual)); 957 | 958 | return sb.join(""); 959 | }; 960 | 961 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForMissingItem = function(key) { 962 | var sb = []; 963 | 964 | sb.push('

actual value has no item named ' + JSSpec.util.inspect(key) + '

'); 965 | sb.push('

' + JSSpec.util.inspect(this.actual, false, key) + '

'); 966 | sb.push('

but it should have the item whose value is ' + JSSpec.util.inspect(this.expected[key]) + '

'); 967 | sb.push('

' + JSSpec.util.inspect(this.expected, false, key) + '

'); 968 | 969 | return sb.join(""); 970 | }; 971 | 972 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForUnknownItem = function(key) { 973 | var sb = []; 974 | 975 | sb.push('

actual value has item named ' + JSSpec.util.inspect(key) + '

'); 976 | sb.push('

' + JSSpec.util.inspect(this.actual, false, key) + '

'); 977 | sb.push('

but there should be no such item

'); 978 | sb.push('

' + JSSpec.util.inspect(this.expected, false, key) + '

'); 979 | 980 | return sb.join(""); 981 | }; 982 | 983 | JSSpec.ObjectEqualityMatcher.prototype.makeExplainForItemMismatch = function(key) { 984 | var sb = []; 985 | 986 | sb.push('

actual value has an item named ' + JSSpec.util.inspect(key) + ' whose value is ' + JSSpec.util.inspect(this.actual[key]) + '

'); 987 | sb.push('

' + JSSpec.util.inspect(this.actual, false, key) + '

'); 988 | sb.push('

but it\'s value should be ' + JSSpec.util.inspect(this.expected[key]) + '

'); 989 | sb.push('

' + JSSpec.util.inspect(this.expected, false, key) + '

'); 990 | 991 | return sb.join(""); 992 | }; 993 | 994 | 995 | 996 | 997 | /** 998 | * ArrayEqualityMatcher 999 | */ 1000 | JSSpec.ArrayEqualityMatcher = function(expected, actual) { 1001 | this.expected = expected; 1002 | this.actual = actual; 1003 | this.match = this.expected == this.actual; 1004 | this.explaination = this.makeExplain(); 1005 | }; 1006 | 1007 | JSSpec.ArrayEqualityMatcher.prototype.matches = function() {return this.match}; 1008 | 1009 | JSSpec.ArrayEqualityMatcher.prototype.explain = function() {return this.explaination}; 1010 | 1011 | JSSpec.ArrayEqualityMatcher.prototype.makeExplain = function() { 1012 | if(this.expected.length != this.actual.length) return this.makeExplainForLengthMismatch(); 1013 | 1014 | for(var i = 0; i < this.expected.length; i++) { 1015 | var matcher = JSSpec.EqualityMatcher.createInstance(this.expected[i], this.actual[i]); 1016 | if(!matcher.matches()) return this.makeExplainForItemMismatch(i); 1017 | } 1018 | 1019 | this.match = true; 1020 | }; 1021 | 1022 | JSSpec.ArrayEqualityMatcher.prototype.makeExplainForLengthMismatch = function() { 1023 | return JSSpec.EqualityMatcher.basicExplain( 1024 | this.expected, 1025 | this.actual, 1026 | '

but it should be ' + this.expected.length + '

', 1027 | '

actual value has ' + this.actual.length + ' items

' 1028 | ); 1029 | }; 1030 | 1031 | JSSpec.ArrayEqualityMatcher.prototype.makeExplainForItemMismatch = function(index) { 1032 | var postfix = ["th", "st", "nd", "rd", "th"][Math.min((index + 1) % 10,4)]; 1033 | 1034 | var sb = []; 1035 | 1036 | sb.push('

' + (index + 1) + postfix + ' item (index ' + index + ') of actual value is ' + JSSpec.util.inspect(this.actual[index]) + ':

'); 1037 | sb.push('

' + JSSpec.util.inspect(this.actual, false, index) + '

'); 1038 | sb.push('

but it should be ' + JSSpec.util.inspect(this.expected[index]) + ':

'); 1039 | sb.push('

' + JSSpec.util.inspect(this.expected, false, index) + '

'); 1040 | 1041 | return sb.join(""); 1042 | }; 1043 | 1044 | /** 1045 | * NumberEqualityMatcher 1046 | */ 1047 | JSSpec.NumberEqualityMatcher = function(expected, actual) { 1048 | this.expected = expected; 1049 | this.actual = actual; 1050 | }; 1051 | 1052 | JSSpec.NumberEqualityMatcher.prototype.matches = function() { 1053 | if(this.expected == this.actual) return true; 1054 | }; 1055 | 1056 | JSSpec.NumberEqualityMatcher.prototype.explain = function() { 1057 | return JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual); 1058 | }; 1059 | 1060 | /** 1061 | * StringEqualityMatcher 1062 | */ 1063 | JSSpec.StringEqualityMatcher = function(expected, actual) { 1064 | this.expected = expected; 1065 | this.actual = actual; 1066 | }; 1067 | 1068 | JSSpec.StringEqualityMatcher.prototype.matches = function() { 1069 | if(this.expected == this.actual) return true; 1070 | }; 1071 | 1072 | JSSpec.StringEqualityMatcher.prototype.explain = function() { 1073 | var sb = []; 1074 | 1075 | sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual)); 1076 | sb.push(JSSpec.EqualityMatcher.diffExplain(this.expected, this.actual)); 1077 | return sb.join(""); 1078 | }; 1079 | 1080 | /** 1081 | * PatternMatcher 1082 | */ 1083 | JSSpec.PatternMatcher = function(actual, pattern, condition) { 1084 | this.actual = actual; 1085 | this.pattern = pattern; 1086 | this.condition = condition; 1087 | this.match = false; 1088 | this.explaination = this.makeExplain(); 1089 | }; 1090 | 1091 | JSSpec.PatternMatcher.createInstance = function(actual, pattern, condition) { 1092 | return new JSSpec.PatternMatcher(actual, pattern, condition); 1093 | }; 1094 | 1095 | JSSpec.PatternMatcher.prototype.makeExplain = function() { 1096 | var sb; 1097 | if(this.actual == null || this.actual._type != 'String') { 1098 | sb = []; 1099 | sb.push('

actual value:

'); 1100 | sb.push('

' + JSSpec.util.inspect(this.actual) + '

'); 1101 | sb.push('

should ' + (this.condition ? '' : 'not') + ' match with pattern:

'); 1102 | sb.push('

' + JSSpec.util.inspect(this.pattern) + '

'); 1103 | sb.push('

but pattern matching cannot be performed.

'); 1104 | return sb.join(""); 1105 | } else { 1106 | this.match = this.condition == !!this.actual.match(this.pattern); 1107 | if(this.match) return ""; 1108 | 1109 | sb = []; 1110 | sb.push('

actual value:

'); 1111 | sb.push('

' + JSSpec.util.inspect(this.actual) + '

'); 1112 | sb.push('

should ' + (this.condition ? '' : 'not') + ' match with pattern:

'); 1113 | sb.push('

' + JSSpec.util.inspect(this.pattern) + '

'); 1114 | return sb.join(""); 1115 | } 1116 | }; 1117 | 1118 | JSSpec.PatternMatcher.prototype.matches = function() { 1119 | return this.match; 1120 | }; 1121 | 1122 | JSSpec.PatternMatcher.prototype.explain = function() { 1123 | return this.explaination; 1124 | }; 1125 | 1126 | /** 1127 | * Domain Specific Languages 1128 | */ 1129 | JSSpec.DSL = {}; 1130 | 1131 | JSSpec.DSL.forString = { 1132 | normalizeHtml: function() { 1133 | var html = this; 1134 | 1135 | // Uniformize quotation, turn tag names and attribute names into lower case 1136 | html = html.replace(/<(\/?)(\w+)([^>]*?)>/img, function(str, closingMark, tagName, attrs) { 1137 | var sortedAttrs = JSSpec.util.sortHtmlAttrs(JSSpec.util.correctHtmlAttrQuotation(attrs).toLowerCase()) 1138 | return "<" + closingMark + tagName.toLowerCase() + sortedAttrs + ">" 1139 | }); 1140 | 1141 | // validation self-closing tags 1142 | html = html.replace(/<(br|hr|img)([^>]*?)>/mg, function(str, tag, attrs) { 1143 | return "<" + tag + attrs + " />"; 1144 | }); 1145 | 1146 | // append semi-colon at the end of style value 1147 | html = html.replace(/style="(.*?)"/mg, function(str, styleStr) { 1148 | styleStr = JSSpec.util.sortStyleEntries(styleStr.strip()); // for Safari 1149 | if(styleStr.charAt(styleStr.length - 1) != ';') styleStr += ";" 1150 | 1151 | return 'style="' + styleStr + '"' 1152 | }); 1153 | 1154 | // sort style entries 1155 | 1156 | // remove empty style attributes 1157 | html = html.replace(/ style=";"/mg, ""); 1158 | 1159 | // remove new-lines 1160 | html = html.replace(/\r/mg, ''); 1161 | html = html.replace(/\n/mg, ''); 1162 | 1163 | return html; 1164 | } 1165 | }; 1166 | 1167 | 1168 | 1169 | JSSpec.DSL.describe = function(context, entries) { 1170 | JSSpec.specs.push(new JSSpec.Spec(context, entries)); 1171 | }; 1172 | 1173 | JSSpec.DSL.value_of = function(target) { 1174 | if(JSSpec._secondPass) return {}; 1175 | 1176 | var subject = new JSSpec.DSL.Subject(target); 1177 | return subject; 1178 | }; 1179 | 1180 | JSSpec.DSL.Subject = function(target) { 1181 | this.target = target; 1182 | }; 1183 | 1184 | JSSpec.DSL.Subject.prototype._type = 'Subject'; 1185 | 1186 | JSSpec.DSL.Subject.prototype.should_fail = function(message) { 1187 | JSSpec._assertionFailure = {message:message}; 1188 | throw JSSpec._assertionFailure; 1189 | }; 1190 | 1191 | JSSpec.DSL.Subject.prototype.should_be = function(expected) { 1192 | var matcher = JSSpec.EqualityMatcher.createInstance(expected, this.target); 1193 | if(!matcher.matches()) { 1194 | JSSpec._assertionFailure = {message:matcher.explain()}; 1195 | throw JSSpec._assertionFailure; 1196 | } 1197 | }; 1198 | 1199 | JSSpec.DSL.Subject.prototype.should_not_be = function(expected) { 1200 | // TODO JSSpec.EqualityMatcher should support 'condition' 1201 | var matcher = JSSpec.EqualityMatcher.createInstance(expected, this.target); 1202 | if(matcher.matches()) { 1203 | JSSpec._assertionFailure = {message:"'" + this.target + "' should not be '" + expected + "'"}; 1204 | throw JSSpec._assertionFailure; 1205 | } 1206 | }; 1207 | 1208 | JSSpec.DSL.Subject.prototype.should_be_empty = function() { 1209 | this.should_have(0, this.getType() == 'String' ? 'characters' : 'items'); 1210 | }; 1211 | 1212 | JSSpec.DSL.Subject.prototype.should_not_be_empty = function() { 1213 | this.should_have_at_least(1, this.getType() == 'String' ? 'characters' : 'items'); 1214 | }; 1215 | 1216 | JSSpec.DSL.Subject.prototype.should_be_true = function() { 1217 | this.should_be(true); 1218 | }; 1219 | 1220 | JSSpec.DSL.Subject.prototype.should_be_false = function() { 1221 | this.should_be(false); 1222 | }; 1223 | 1224 | JSSpec.DSL.Subject.prototype.should_be_null = function() { 1225 | this.should_be(null); 1226 | }; 1227 | 1228 | JSSpec.DSL.Subject.prototype.should_be_undefined = function() { 1229 | this.should_be(undefined); 1230 | }; 1231 | 1232 | JSSpec.DSL.Subject.prototype.should_not_be_null = function() { 1233 | this.should_not_be(null); 1234 | }; 1235 | 1236 | JSSpec.DSL.Subject.prototype.should_not_be_undefined = function() { 1237 | this.should_not_be(undefined); 1238 | }; 1239 | 1240 | JSSpec.DSL.Subject.prototype._should_have = function(num, property, condition) { 1241 | var matcher = JSSpec.PropertyLengthMatcher.createInstance(num, property, this.target, condition); 1242 | if(!matcher.matches()) { 1243 | JSSpec._assertionFailure = {message:matcher.explain()}; 1244 | throw JSSpec._assertionFailure; 1245 | } 1246 | }; 1247 | 1248 | JSSpec.DSL.Subject.prototype.should_have = function(num, property) { 1249 | this._should_have(num, property, "exactly"); 1250 | }; 1251 | 1252 | JSSpec.DSL.Subject.prototype.should_have_exactly = function(num, property) { 1253 | this._should_have(num, property, "exactly"); 1254 | }; 1255 | 1256 | JSSpec.DSL.Subject.prototype.should_have_at_least = function(num, property) { 1257 | this._should_have(num, property, "at least"); 1258 | }; 1259 | 1260 | JSSpec.DSL.Subject.prototype.should_have_at_most = function(num, property) { 1261 | this._should_have(num, property, "at most"); 1262 | }; 1263 | 1264 | JSSpec.DSL.Subject.prototype.should_include = function(expected) { 1265 | var matcher = JSSpec.IncludeMatcher.createInstance(this.target, expected, true); 1266 | if(!matcher.matches()) { 1267 | JSSpec._assertionFailure = {message:matcher.explain()}; 1268 | throw JSSpec._assertionFailure; 1269 | } 1270 | }; 1271 | 1272 | JSSpec.DSL.Subject.prototype.should_not_include = function(expected) { 1273 | var matcher = JSSpec.IncludeMatcher.createInstance(this.target, expected, false); 1274 | if(!matcher.matches()) { 1275 | JSSpec._assertionFailure = {message:matcher.explain()}; 1276 | throw JSSpec._assertionFailure; 1277 | } 1278 | }; 1279 | 1280 | JSSpec.DSL.Subject.prototype.should_match = function(pattern) { 1281 | var matcher = JSSpec.PatternMatcher.createInstance(this.target, pattern, true); 1282 | if(!matcher.matches()) { 1283 | JSSpec._assertionFailure = {message:matcher.explain()}; 1284 | throw JSSpec._assertionFailure; 1285 | } 1286 | } 1287 | JSSpec.DSL.Subject.prototype.should_not_match = function(pattern) { 1288 | var matcher = JSSpec.PatternMatcher.createInstance(this.target, pattern, false); 1289 | if(!matcher.matches()) { 1290 | JSSpec._assertionFailure = {message:matcher.explain()}; 1291 | throw JSSpec._assertionFailure; 1292 | } 1293 | }; 1294 | 1295 | JSSpec.DSL.Subject.prototype.getType = function() { 1296 | if(typeof this.target == 'undefined') { 1297 | return 'undefined'; 1298 | } else if(this.target == null) { 1299 | return 'null'; 1300 | } else if(this.target._type) { 1301 | return this.target._type; 1302 | } else if(JSSpec.util.isDomNode(this.target)) { 1303 | return 'DomNode'; 1304 | } else { 1305 | return 'object'; 1306 | } 1307 | }; 1308 | 1309 | /** 1310 | * Utilities 1311 | */ 1312 | JSSpec.util = { 1313 | escapeTags: function(string) { 1314 | return string.replace(//img, '>'); 1315 | }, 1316 | parseOptions: function(defaults) { 1317 | var options = defaults; 1318 | 1319 | var url = location.href; 1320 | var queryIndex = url.indexOf('?'); 1321 | if(queryIndex == -1) return options; 1322 | 1323 | var query = url.substring(queryIndex + 1); 1324 | var pairs = query.split('&'); 1325 | for(var i = 0; i < pairs.length; i++) { 1326 | var tokens = pairs[i].split('='); 1327 | options[tokens[0]] = tokens[1]; 1328 | } 1329 | 1330 | return options; 1331 | }, 1332 | correctHtmlAttrQuotation: function(html) { 1333 | html = html.replace(/(\w+)=['"]([^'"]+)['"]/mg,function (str, name, value) {return name + '=' + '"' + value + '"';}); 1334 | html = html.replace(/(\w+)=([^ '"]+)/mg,function (str, name, value) {return name + '=' + '"' + value + '"';}); 1335 | html = html.replace(/'/mg, '"'); 1336 | 1337 | return html; 1338 | }, 1339 | sortHtmlAttrs: function(html) { 1340 | var attrs = []; 1341 | html.replace(/((\w+)="[^"]+")/mg, function(str, matched) { 1342 | attrs.push(matched); 1343 | }); 1344 | return attrs.length == 0 ? "" : " " + attrs.sort().join(" "); 1345 | }, 1346 | sortStyleEntries: function(styleText) { 1347 | var entries = styleText.split(/; /); 1348 | return entries.sort().join("; "); 1349 | }, 1350 | escapeHtml: function(str) { 1351 | if(!this._div) { 1352 | this._div = document.createElement("DIV"); 1353 | this._text = document.createTextNode(''); 1354 | this._div.appendChild(this._text); 1355 | } 1356 | this._text.data = str; 1357 | return this._div.innerHTML; 1358 | }, 1359 | isDomNode: function(o) { 1360 | // TODO: make it more stricter 1361 | return (typeof o.nodeName == 'string') && (typeof o.nodeType == 'number'); 1362 | }, 1363 | inspectDomPath: function(o) { 1364 | var sb = []; 1365 | while(o && o.nodeName != '#document' && o.parent) { 1366 | var siblings = o.parentNode.childNodes; 1367 | for(var i = 0; i < siblings.length; i++) { 1368 | if(siblings[i] == o) { 1369 | sb.push(o.nodeName + (i == 0 ? '' : '[' + i + ']')); 1370 | break; 1371 | } 1372 | } 1373 | o = o.parentNode; 1374 | } 1375 | return sb.join(" > "); 1376 | }, 1377 | inspectDomNode: function(o) { 1378 | if(o.nodeType == 1) { 1379 | var nodeName = o.nodeName.toLowerCase(); 1380 | var sb = []; 1381 | sb.push(''); 1382 | sb.push("<"); 1383 | sb.push(nodeName); 1384 | 1385 | var attrs = o.attributes; 1386 | for(var i = 0; i < attrs.length; i++) { 1387 | if( 1388 | attrs[i].nodeValue && 1389 | attrs[i].nodeName != 'contentEditable' && 1390 | attrs[i].nodeName != 'style' && 1391 | typeof attrs[i].nodeValue != 'function' 1392 | ) sb.push(' ' + attrs[i].nodeName.toLowerCase() + '="' + attrs[i].nodeValue + '"'); 1393 | } 1394 | if(o.style && o.style.cssText) { 1395 | sb.push(' style="' + o.style.cssText + '"'); 1396 | } 1397 | sb.push('>'); 1398 | sb.push(JSSpec.util.escapeHtml(o.innerHTML)); 1399 | sb.push('</' + nodeName + '>'); 1400 | sb.push(' (' + JSSpec.util.inspectDomPath(o) + ')' ); 1401 | sb.push(''); 1402 | return sb.join(""); 1403 | } else if(o.nodeType == 3) { 1404 | return '#text ' + o.nodeValue + ''; 1405 | } else { 1406 | return 'UnknownDomNode'; 1407 | } 1408 | }, 1409 | inspect: function(o, dontEscape, emphasisKey) { 1410 | var sb, inspected; 1411 | 1412 | if(typeof o == 'undefined') return 'undefined'; 1413 | if(o == null) return 'null'; 1414 | if(o._type == 'String') return '"' + (dontEscape ? o : JSSpec.util.escapeHtml(o)) + '"'; 1415 | 1416 | if(o._type == 'Date') { 1417 | return '"' + o.toString() + '"'; 1418 | } 1419 | 1420 | if(o._type == 'Number') return '' + (dontEscape ? o : JSSpec.util.escapeHtml(o)) + ''; 1421 | 1422 | if(o._type == 'Boolean') return '' + o + ''; 1423 | 1424 | if(o._type == 'RegExp') return '' + JSSpec.util.escapeHtml(o.toString()) + ''; 1425 | 1426 | if(JSSpec.util.isDomNode(o)) return JSSpec.util.inspectDomNode(o); 1427 | 1428 | if(o._type == 'Array' || typeof o.length != 'undefined') { 1429 | sb = []; 1430 | for(var i = 0; i < o.length; i++) { 1431 | inspected = JSSpec.util.inspect(o[i]); 1432 | sb.push(i == emphasisKey ? ('' + inspected + '') : inspected); 1433 | } 1434 | return '[' + sb.join(', ') + ']'; 1435 | } 1436 | 1437 | // object 1438 | sb = []; 1439 | for(var key in o) { 1440 | if(key == 'should') continue; 1441 | 1442 | inspected = JSSpec.util.inspect(key) + ":" + JSSpec.util.inspect(o[key]); 1443 | sb.push(key == emphasisKey ? ('' + inspected + '') : inspected); 1444 | } 1445 | return '{' + sb.join(', ') + '}'; 1446 | } 1447 | }; 1448 | 1449 | describe = JSSpec.DSL.describe; 1450 | behavior_of = JSSpec.DSL.describe; 1451 | value_of = JSSpec.DSL.value_of; 1452 | expect = JSSpec.DSL.value_of; // @deprecated 1453 | 1454 | String.prototype._type = "String"; 1455 | Number.prototype._type = "Number"; 1456 | Date.prototype._type = "Date"; 1457 | Array.prototype._type = "Array"; 1458 | Boolean.prototype._type = "Boolean"; 1459 | RegExp.prototype._type = "RegExp"; 1460 | 1461 | var targets = [Array.prototype, Date.prototype, Number.prototype, String.prototype, Boolean.prototype, RegExp.prototype]; 1462 | 1463 | String.prototype.normalizeHtml = JSSpec.DSL.forString.normalizeHtml; 1464 | String.prototype.asHtml = String.prototype.normalizeHtml; //@deprecated 1465 | 1466 | 1467 | 1468 | /** 1469 | * Main 1470 | */ 1471 | JSSpec.defaultOptions = { 1472 | autorun: 1, 1473 | specIdBeginsWith: 0, 1474 | exampleIdBeginsWith: 0, 1475 | autocollapse: 1 1476 | }; 1477 | JSSpec.options = JSSpec.util.parseOptions(JSSpec.defaultOptions); 1478 | 1479 | JSSpec.Spec.id = JSSpec.options.specIdBeginsWith; 1480 | JSSpec.Example.id = JSSpec.options.exampleIdBeginsWith; 1481 | 1482 | 1483 | 1484 | window.onload = function() { 1485 | if(JSSpec.specs.length > 0) { 1486 | if(!JSSpec.options.inSuite) { 1487 | JSSpec.runner = new JSSpec.Runner(JSSpec.specs, new JSSpec.Logger()); 1488 | if(JSSpec.options.rerun) { 1489 | JSSpec.runner.rerun(decodeURIComponent(JSSpec.options.rerun)); 1490 | } else { 1491 | JSSpec.runner.run(); 1492 | } 1493 | } else { 1494 | // in suite, send all specs to parent 1495 | var parentWindow = window.frames.parent.window; 1496 | for(var i = 0; i < JSSpec.specs.length; i++) { 1497 | parentWindow.JSSpec.specs.push(JSSpec.specs[i]); 1498 | } 1499 | } 1500 | } else { 1501 | var links = document.getElementById('list').getElementsByTagName('A'); 1502 | var frameContainer = document.createElement('DIV'); 1503 | frameContainer.style.display = 'none'; 1504 | document.body.appendChild(frameContainer); 1505 | 1506 | for(var i = 0; i < links.length; i++) { 1507 | var frame = document.createElement('IFRAME'); 1508 | frame.src = links[i].href + '?inSuite=0&specIdBeginsWith=' + (i * 10000) + '&exampleIdBeginsWith=' + (i * 10000); 1509 | frameContainer.appendChild(frame); 1510 | } 1511 | } 1512 | } -------------------------------------------------------------------------------- /Specs/Assets/Scripts/DiffMatchPatch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Diff Match and Patch 3 | * 4 | * Copyright 2006 Google Inc. 5 | * http://code.google.com/p/google-diff-match-patch/ 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | /** 21 | * @fileoverview Computes the difference between two texts to create a patch. 22 | * Applies the patch onto another text, allowing for errors. 23 | * @author fraser@google.com (Neil Fraser) 24 | */ 25 | 26 | /** 27 | * Class containing the diff, match and patch methods. 28 | * @constructor 29 | */ 30 | function diff_match_patch() { 31 | 32 | // Defaults. 33 | // Redefine these in your program to override the defaults. 34 | 35 | // Number of seconds to map a diff before giving up. (0 for infinity) 36 | this.Diff_Timeout = 1.0; 37 | // Cost of an empty edit operation in terms of edit characters. 38 | this.Diff_EditCost = 4; 39 | // The size beyond which the double-ended diff activates. 40 | // Double-ending is twice as fast, but less accurate. 41 | this.Diff_DualThreshold = 32; 42 | // Tweak the relative importance (0.0 = accuracy, 1.0 = proximity) 43 | this.Match_Balance = 0.5; 44 | // At what point is no match declared (0.0 = perfection, 1.0 = very loose) 45 | this.Match_Threshold = 0.5; 46 | // The min and max cutoffs used when computing text lengths. 47 | this.Match_MinLength = 100; 48 | this.Match_MaxLength = 1000; 49 | // Chunk size for context length. 50 | this.Patch_Margin = 4; 51 | 52 | /** 53 | * Compute the number of bits in an int. 54 | * The normal answer for JavaScript is 32. 55 | * @return {number} Max bits 56 | */ 57 | function getMaxBits() { 58 | var maxbits = 0; 59 | var oldi = 1; 60 | var newi = 2; 61 | while (oldi != newi) { 62 | maxbits++; 63 | oldi = newi; 64 | newi = newi << 1; 65 | } 66 | return maxbits; 67 | } 68 | // How many bits in a number? 69 | this.Match_MaxBits = getMaxBits(); 70 | } 71 | 72 | 73 | // DIFF FUNCTIONS 74 | 75 | 76 | /** 77 | * The data structure representing a diff is an array of tuples: 78 | * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] 79 | * which means: delete 'Hello', add 'Goodbye' and keep ' world.' 80 | */ 81 | var DIFF_DELETE = -1; 82 | var DIFF_INSERT = 1; 83 | var DIFF_EQUAL = 0; 84 | 85 | 86 | /** 87 | * Find the differences between two texts. Simplifies the problem by stripping 88 | * any common prefix or suffix off the texts before diffing. 89 | * @param {string} text1 Old string to be diffed 90 | * @param {string} text2 New string to be diffed 91 | * @param {boolean} opt_checklines Optional speedup flag. If present and false, 92 | * then don't run a line-level diff first to identify the changed areas. 93 | * Defaults to true, which does a faster, slightly less optimal diff 94 | * @return {Array.>} Array of diff tuples 95 | */ 96 | diff_match_patch.prototype.diff_main = function(text1, text2, opt_checklines) { 97 | // Check for equality (speedup) 98 | if (text1 == text2) { 99 | return [[DIFF_EQUAL, text1]]; 100 | } 101 | 102 | if (typeof opt_checklines == 'undefined') { 103 | opt_checklines = true; 104 | } 105 | var checklines = opt_checklines; 106 | 107 | // Trim off common prefix (speedup) 108 | var commonlength = this.diff_commonPrefix(text1, text2); 109 | var commonprefix = text1.substring(0, commonlength); 110 | text1 = text1.substring(commonlength); 111 | text2 = text2.substring(commonlength); 112 | 113 | // Trim off common suffix (speedup) 114 | commonlength = this.diff_commonSuffix(text1, text2); 115 | var commonsuffix = text1.substring(text1.length - commonlength); 116 | text1 = text1.substring(0, text1.length - commonlength); 117 | text2 = text2.substring(0, text2.length - commonlength); 118 | 119 | // Compute the diff on the middle block 120 | var diffs = this.diff_compute(text1, text2, checklines); 121 | 122 | // Restore the prefix and suffix 123 | if (commonprefix) { 124 | diffs.unshift([DIFF_EQUAL, commonprefix]); 125 | } 126 | if (commonsuffix) { 127 | diffs.push([DIFF_EQUAL, commonsuffix]); 128 | } 129 | this.diff_cleanupMerge(diffs); 130 | return diffs; 131 | }; 132 | 133 | 134 | /** 135 | * Find the differences between two texts. Assumes that the texts do not 136 | * have any common prefix or suffix. 137 | * @param {string} text1 Old string to be diffed 138 | * @param {string} text2 New string to be diffed 139 | * @param {boolean} checklines Speedup flag. If false, then don't run a 140 | * line-level diff first to identify the changed areas. 141 | * If true, then run a faster, slightly less optimal diff 142 | * @return {Array.>} Array of diff tuples 143 | * @private 144 | */ 145 | diff_match_patch.prototype.diff_compute = function(text1, text2, checklines) { 146 | var diffs; 147 | 148 | if (!text1) { 149 | // Just add some text (speedup) 150 | return [[DIFF_INSERT, text2]]; 151 | } 152 | 153 | if (!text2) { 154 | // Just delete some text (speedup) 155 | return [[DIFF_DELETE, text1]]; 156 | } 157 | 158 | var longtext = text1.length > text2.length ? text1 : text2; 159 | var shorttext = text1.length > text2.length ? text2 : text1; 160 | var i = longtext.indexOf(shorttext); 161 | if (i != -1) { 162 | // Shorter text is inside the longer text (speedup) 163 | diffs = [[DIFF_INSERT, longtext.substring(0, i)], 164 | [DIFF_EQUAL, shorttext], 165 | [DIFF_INSERT, longtext.substring(i + shorttext.length)]]; 166 | // Swap insertions for deletions if diff is reversed. 167 | if (text1.length > text2.length) { 168 | diffs[0][0] = diffs[2][0] = DIFF_DELETE; 169 | } 170 | return diffs; 171 | } 172 | longtext = shorttext = null; // Garbage collect 173 | 174 | // Check to see if the problem can be split in two. 175 | var hm = this.diff_halfMatch(text1, text2); 176 | if (hm) { 177 | // A half-match was found, sort out the return data. 178 | var text1_a = hm[0]; 179 | var text1_b = hm[1]; 180 | var text2_a = hm[2]; 181 | var text2_b = hm[3]; 182 | var mid_common = hm[4]; 183 | // Send both pairs off for separate processing. 184 | var diffs_a = this.diff_main(text1_a, text2_a, checklines); 185 | var diffs_b = this.diff_main(text1_b, text2_b, checklines); 186 | // Merge the results. 187 | return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b); 188 | } 189 | 190 | // Perform a real diff. 191 | if (checklines && (text1.length < 100 || text2.length < 100)) { 192 | // Too trivial for the overhead. 193 | checklines = false; 194 | } 195 | var linearray; 196 | if (checklines) { 197 | // Scan the text on a line-by-line basis first. 198 | var a = this.diff_linesToChars(text1, text2); 199 | text1 = a[0]; 200 | text2 = a[1]; 201 | linearray = a[2]; 202 | } 203 | diffs = this.diff_map(text1, text2); 204 | if (!diffs) { 205 | // No acceptable result. 206 | diffs = [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; 207 | } 208 | if (checklines) { 209 | // Convert the diff back to original text. 210 | this.diff_charsToLines(diffs, linearray); 211 | // Eliminate freak matches (e.g. blank lines) 212 | this.diff_cleanupSemantic(diffs); 213 | 214 | // Rediff any replacement blocks, this time character-by-character. 215 | // Add a dummy entry at the end. 216 | diffs.push([DIFF_EQUAL, '']); 217 | var pointer = 0; 218 | var count_delete = 0; 219 | var count_insert = 0; 220 | var text_delete = ''; 221 | var text_insert = ''; 222 | while (pointer < diffs.length) { 223 | switch (diffs[pointer][0]) { 224 | case DIFF_INSERT: 225 | count_insert++; 226 | text_insert += diffs[pointer][1]; 227 | break; 228 | case DIFF_DELETE: 229 | count_delete++; 230 | text_delete += diffs[pointer][1]; 231 | break; 232 | case DIFF_EQUAL: 233 | // Upon reaching an equality, check for prior redundancies. 234 | if (count_delete >= 1 && count_insert >= 1) { 235 | // Delete the offending records and add the merged ones. 236 | var a = this.diff_main(text_delete, text_insert, false); 237 | diffs.splice(pointer - count_delete - count_insert, 238 | count_delete + count_insert); 239 | pointer = pointer - count_delete - count_insert; 240 | for (var j = a.length - 1; j >= 0; j--) { 241 | diffs.splice(pointer, 0, a[j]); 242 | } 243 | pointer = pointer + a.length; 244 | } 245 | count_insert = 0; 246 | count_delete = 0; 247 | text_delete = ''; 248 | text_insert = ''; 249 | break; 250 | } 251 | pointer++; 252 | } 253 | diffs.pop(); // Remove the dummy entry at the end. 254 | } 255 | return diffs; 256 | }; 257 | 258 | 259 | /** 260 | * Split two texts into an array of strings. Reduce the texts to a string of 261 | * hashes where each Unicode character represents one line. 262 | * @param {string} text1 First string 263 | * @param {string} text2 Second string 264 | * @return {Array.>} Three element Array, containing the 265 | * encoded text1, the encoded text2 and the array of unique strings. The 266 | * zeroth element of the array of unique strings is intentionally blank. 267 | * @private 268 | */ 269 | diff_match_patch.prototype.diff_linesToChars = function(text1, text2) { 270 | var lineArray = []; // e.g. lineArray[4] == 'Hello\n' 271 | var lineHash = {}; // e.g. lineHash['Hello\n'] == 4 272 | 273 | // '\x00' is a valid character, but various debuggers don't like it. 274 | // So we'll insert a junk entry to avoid generating a null character. 275 | lineArray[0] = ''; 276 | 277 | /** 278 | * Split a text into an array of strings. Reduce the texts to a string of 279 | * hashes where each Unicode character represents one line. 280 | * Modifies linearray and linehash through being a closure. 281 | * @param {string} text String to encode 282 | * @return {string} Encoded string 283 | * @private 284 | */ 285 | function diff_linesToCharsMunge(text) { 286 | var chars = ''; 287 | // Walk the text, pulling out a substring for each line. 288 | // text.split('\n') would would temporarily double our memory footprint. 289 | // Modifying text would create many large strings to garbage collect. 290 | var lineStart = 0; 291 | var lineEnd = -1; 292 | // Keeping our own length variable is faster than looking it up. 293 | var lineArrayLength = lineArray.length; 294 | while (lineEnd < text.length - 1) { 295 | lineEnd = text.indexOf('\n', lineStart); 296 | if (lineEnd == -1) { 297 | lineEnd = text.length - 1; 298 | } 299 | var line = text.substring(lineStart, lineEnd + 1); 300 | lineStart = lineEnd + 1; 301 | 302 | if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : 303 | (lineHash[line] !== undefined)) { 304 | chars += String.fromCharCode(lineHash[line]); 305 | } else { 306 | chars += String.fromCharCode(lineArrayLength); 307 | lineHash[line] = lineArrayLength; 308 | lineArray[lineArrayLength++] = line; 309 | } 310 | } 311 | return chars; 312 | } 313 | 314 | var chars1 = diff_linesToCharsMunge(text1); 315 | var chars2 = diff_linesToCharsMunge(text2); 316 | return [chars1, chars2, lineArray]; 317 | }; 318 | 319 | 320 | /** 321 | * Rehydrate the text in a diff from a string of line hashes to real lines of 322 | * text. 323 | * @param {Array.>} diffs Array of diff tuples 324 | * @param {Array.} lineArray Array of unique strings 325 | * @private 326 | */ 327 | diff_match_patch.prototype.diff_charsToLines = function(diffs, lineArray) { 328 | for (var x = 0; x < diffs.length; x++) { 329 | var chars = diffs[x][1]; 330 | var text = []; 331 | for (var y = 0; y < chars.length; y++) { 332 | text[y] = lineArray[chars.charCodeAt(y)]; 333 | } 334 | diffs[x][1] = text.join(''); 335 | } 336 | }; 337 | 338 | 339 | /** 340 | * Explore the intersection points between the two texts. 341 | * @param {string} text1 Old string to be diffed 342 | * @param {string} text2 New string to be diffed 343 | * @return {Array.>?} Array of diff tuples or null if no diff 344 | * available 345 | * @private 346 | */ 347 | diff_match_patch.prototype.diff_map = function(text1, text2) { 348 | // Don't run for too long. 349 | var ms_end = (new Date()).getTime() + this.Diff_Timeout * 1000; 350 | var max_d = text1.length + text2.length - 1; 351 | var doubleEnd = this.Diff_DualThreshold * 2 < max_d; 352 | var v_map1 = []; 353 | var v_map2 = []; 354 | var v1 = {}; 355 | var v2 = {}; 356 | v1[1] = 0; 357 | v2[1] = 0; 358 | var x, y; 359 | var footstep; // Used to track overlapping paths. 360 | var footsteps = {}; 361 | var done = false; 362 | // Safari 1.x doesn't have hasOwnProperty 363 | var hasOwnProperty = !!(footsteps.hasOwnProperty); 364 | // If the total number of characters is odd, then the front path will collide 365 | // with the reverse path. 366 | var front = (text1.length + text2.length) % 2; 367 | for (var d = 0; d < max_d; d++) { 368 | // Bail out if timeout reached. 369 | if (this.Diff_Timeout > 0 && (new Date()).getTime() > ms_end) { 370 | return null; 371 | } 372 | 373 | // Walk the front path one step. 374 | v_map1[d] = {}; 375 | for (var k = -d; k <= d; k += 2) { 376 | if (k == -d || k != d && v1[k - 1] < v1[k + 1]) { 377 | x = v1[k + 1]; 378 | } else { 379 | x = v1[k - 1] + 1; 380 | } 381 | y = x - k; 382 | if (doubleEnd) { 383 | footstep = x + ',' + y; 384 | if (front && (hasOwnProperty ? footsteps.hasOwnProperty(footstep) : 385 | (footsteps[footstep] !== undefined))) { 386 | done = true; 387 | } 388 | if (!front) { 389 | footsteps[footstep] = d; 390 | } 391 | } 392 | while (!done && x < text1.length && y < text2.length && 393 | text1.charAt(x) == text2.charAt(y)) { 394 | x++; 395 | y++; 396 | if (doubleEnd) { 397 | footstep = x + ',' + y; 398 | if (front && (hasOwnProperty ? footsteps.hasOwnProperty(footstep) : 399 | (footsteps[footstep] !== undefined))) { 400 | done = true; 401 | } 402 | if (!front) { 403 | footsteps[footstep] = d; 404 | } 405 | } 406 | } 407 | v1[k] = x; 408 | v_map1[d][x + ',' + y] = true; 409 | if (x == text1.length && y == text2.length) { 410 | // Reached the end in single-path mode. 411 | return this.diff_path1(v_map1, text1, text2); 412 | } else if (done) { 413 | // Front path ran over reverse path. 414 | v_map2 = v_map2.slice(0, footsteps[footstep] + 1); 415 | var a = this.diff_path1(v_map1, text1.substring(0, x), 416 | text2.substring(0, y)); 417 | return a.concat(this.diff_path2(v_map2, text1.substring(x), 418 | text2.substring(y))); 419 | } 420 | } 421 | 422 | if (doubleEnd) { 423 | // Walk the reverse path one step. 424 | v_map2[d] = {}; 425 | for (var k = -d; k <= d; k += 2) { 426 | if (k == -d || k != d && v2[k - 1] < v2[k + 1]) { 427 | x = v2[k + 1]; 428 | } else { 429 | x = v2[k - 1] + 1; 430 | } 431 | y = x - k; 432 | footstep = (text1.length - x) + ',' + (text2.length - y); 433 | if (!front && (hasOwnProperty ? footsteps.hasOwnProperty(footstep) : 434 | (footsteps[footstep] !== undefined))) { 435 | done = true; 436 | } 437 | if (front) { 438 | footsteps[footstep] = d; 439 | } 440 | while (!done && x < text1.length && y < text2.length && 441 | text1.charAt(text1.length - x - 1) == 442 | text2.charAt(text2.length - y - 1)) { 443 | x++; 444 | y++; 445 | footstep = (text1.length - x) + ',' + (text2.length - y); 446 | if (!front && (hasOwnProperty ? footsteps.hasOwnProperty(footstep) : 447 | (footsteps[footstep] !== undefined))) { 448 | done = true; 449 | } 450 | if (front) { 451 | footsteps[footstep] = d; 452 | } 453 | } 454 | v2[k] = x; 455 | v_map2[d][x + ',' + y] = true; 456 | if (done) { 457 | // Reverse path ran over front path. 458 | v_map1 = v_map1.slice(0, footsteps[footstep] + 1); 459 | var a = this.diff_path1(v_map1, text1.substring(0, text1.length - x), 460 | text2.substring(0, text2.length - y)); 461 | return a.concat(this.diff_path2(v_map2, 462 | text1.substring(text1.length - x), 463 | text2.substring(text2.length - y))); 464 | } 465 | } 466 | } 467 | } 468 | // Number of diffs equals number of characters, no commonality at all. 469 | return null; 470 | }; 471 | 472 | 473 | /** 474 | * Work from the middle back to the start to determine the path. 475 | * @param {Array.} v_map Array of paths. 476 | * @param {string} text1 Old string fragment to be diffed 477 | * @param {string} text2 New string fragment to be diffed 478 | * @return {Array.>} Array of diff tuples 479 | * @private 480 | */ 481 | diff_match_patch.prototype.diff_path1 = function(v_map, text1, text2) { 482 | var path = []; 483 | var x = text1.length; 484 | var y = text2.length; 485 | /** @type {number?} */ 486 | var last_op = null; 487 | for (var d = v_map.length - 2; d >= 0; d--) { 488 | while (1) { 489 | if (v_map[d].hasOwnProperty ? v_map[d].hasOwnProperty((x - 1) + ',' + y) : 490 | (v_map[d][(x - 1) + ',' + y] !== undefined)) { 491 | x--; 492 | if (last_op === DIFF_DELETE) { 493 | path[0][1] = text1.charAt(x) + path[0][1]; 494 | } else { 495 | path.unshift([DIFF_DELETE, text1.charAt(x)]); 496 | } 497 | last_op = DIFF_DELETE; 498 | break; 499 | } else if (v_map[d].hasOwnProperty ? 500 | v_map[d].hasOwnProperty(x + ',' + (y - 1)) : 501 | (v_map[d][x + ',' + (y - 1)] !== undefined)) { 502 | y--; 503 | if (last_op === DIFF_INSERT) { 504 | path[0][1] = text2.charAt(y) + path[0][1]; 505 | } else { 506 | path.unshift([DIFF_INSERT, text2.charAt(y)]); 507 | } 508 | last_op = DIFF_INSERT; 509 | break; 510 | } else { 511 | x--; 512 | y--; 513 | //if (text1.charAt(x) != text2.charAt(y)) { 514 | // throw new Error('No diagonal. Can\'t happen. (diff_path1)'); 515 | //} 516 | if (last_op === DIFF_EQUAL) { 517 | path[0][1] = text1.charAt(x) + path[0][1]; 518 | } else { 519 | path.unshift([DIFF_EQUAL, text1.charAt(x)]); 520 | } 521 | last_op = DIFF_EQUAL; 522 | } 523 | } 524 | } 525 | return path; 526 | }; 527 | 528 | 529 | /** 530 | * Work from the middle back to the end to determine the path. 531 | * @param {Array.} v_map Array of paths. 532 | * @param {string} text1 Old string fragment to be diffed 533 | * @param {string} text2 New string fragment to be diffed 534 | * @return {Array.>} Array of diff tuples 535 | * @private 536 | */ 537 | diff_match_patch.prototype.diff_path2 = function(v_map, text1, text2) { 538 | var path = []; 539 | var pathLength = 0; 540 | var x = text1.length; 541 | var y = text2.length; 542 | /** @type {number?} */ 543 | var last_op = null; 544 | for (var d = v_map.length - 2; d >= 0; d--) { 545 | while (1) { 546 | if (v_map[d].hasOwnProperty ? v_map[d].hasOwnProperty((x - 1) + ',' + y) : 547 | (v_map[d][(x - 1) + ',' + y] !== undefined)) { 548 | x--; 549 | if (last_op === DIFF_DELETE) { 550 | path[pathLength - 1][1] += text1.charAt(text1.length - x - 1); 551 | } else { 552 | path[pathLength++] = 553 | [DIFF_DELETE, text1.charAt(text1.length - x - 1)]; 554 | } 555 | last_op = DIFF_DELETE; 556 | break; 557 | } else if (v_map[d].hasOwnProperty ? 558 | v_map[d].hasOwnProperty(x + ',' + (y - 1)) : 559 | (v_map[d][x + ',' + (y - 1)] !== undefined)) { 560 | y--; 561 | if (last_op === DIFF_INSERT) { 562 | path[pathLength - 1][1] += text2.charAt(text2.length - y - 1); 563 | } else { 564 | path[pathLength++] = 565 | [DIFF_INSERT, text2.charAt(text2.length - y - 1)]; 566 | } 567 | last_op = DIFF_INSERT; 568 | break; 569 | } else { 570 | x--; 571 | y--; 572 | //if (text1.charAt(text1.length - x - 1) != 573 | // text2.charAt(text2.length - y - 1)) { 574 | // throw new Error('No diagonal. Can\'t happen. (diff_path2)'); 575 | //} 576 | if (last_op === DIFF_EQUAL) { 577 | path[pathLength - 1][1] += text1.charAt(text1.length - x - 1); 578 | } else { 579 | path[pathLength++] = 580 | [DIFF_EQUAL, text1.charAt(text1.length - x - 1)]; 581 | } 582 | last_op = DIFF_EQUAL; 583 | } 584 | } 585 | } 586 | return path; 587 | }; 588 | 589 | 590 | /** 591 | * Determine the common prefix of two strings 592 | * @param {string} text1 First string 593 | * @param {string} text2 Second string 594 | * @return {number} The number of characters common to the start of each 595 | * string. 596 | */ 597 | diff_match_patch.prototype.diff_commonPrefix = function(text1, text2) { 598 | // Quick check for common null cases. 599 | if (!text1 || !text2 || text1.charCodeAt(0) !== text2.charCodeAt(0)) { 600 | return 0; 601 | } 602 | // Binary search. 603 | // Performance analysis: http://neil.fraser.name/news/2007/10/09/ 604 | var pointermin = 0; 605 | var pointermax = Math.min(text1.length, text2.length); 606 | var pointermid = pointermax; 607 | var pointerstart = 0; 608 | while (pointermin < pointermid) { 609 | if (text1.substring(pointerstart, pointermid) == 610 | text2.substring(pointerstart, pointermid)) { 611 | pointermin = pointermid; 612 | pointerstart = pointermin; 613 | } else { 614 | pointermax = pointermid; 615 | } 616 | pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); 617 | } 618 | return pointermid; 619 | }; 620 | 621 | 622 | /** 623 | * Determine the common suffix of two strings 624 | * @param {string} text1 First string 625 | * @param {string} text2 Second string 626 | * @return {number} The number of characters common to the end of each string. 627 | */ 628 | diff_match_patch.prototype.diff_commonSuffix = function(text1, text2) { 629 | // Quick check for common null cases. 630 | if (!text1 || !text2 || text1.charCodeAt(text1.length - 1) !== 631 | text2.charCodeAt(text2.length - 1)) { 632 | return 0; 633 | } 634 | // Binary search. 635 | // Performance analysis: http://neil.fraser.name/news/2007/10/09/ 636 | var pointermin = 0; 637 | var pointermax = Math.min(text1.length, text2.length); 638 | var pointermid = pointermax; 639 | var pointerend = 0; 640 | while (pointermin < pointermid) { 641 | if (text1.substring(text1.length - pointermid, text1.length - pointerend) == 642 | text2.substring(text2.length - pointermid, text2.length - pointerend)) { 643 | pointermin = pointermid; 644 | pointerend = pointermin; 645 | } else { 646 | pointermax = pointermid; 647 | } 648 | pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); 649 | } 650 | return pointermid; 651 | }; 652 | 653 | 654 | /** 655 | * Do the two texts share a substring which is at least half the length of the 656 | * longer text? 657 | * @param {string} text1 First string 658 | * @param {string} text2 Second string 659 | * @return {Array.?} Five element Array, containing the prefix of 660 | * text1, the suffix of text1, the prefix of text2, the suffix of 661 | * text2 and the common middle. Or null if there was no match. 662 | */ 663 | diff_match_patch.prototype.diff_halfMatch = function(text1, text2) { 664 | var longtext = text1.length > text2.length ? text1 : text2; 665 | var shorttext = text1.length > text2.length ? text2 : text1; 666 | if (longtext.length < 10 || shorttext.length < 1) { 667 | return null; // Pointless. 668 | } 669 | var dmp = this; // 'this' becomes 'window' in a closure. 670 | 671 | /** 672 | * Does a substring of shorttext exist within longtext such that the substring 673 | * is at least half the length of longtext? 674 | * Closure, but does not reference any external variables. 675 | * @param {string} longtext Longer string 676 | * @param {string} shorttext Shorter string 677 | * @param {number} i Start index of quarter length substring within longtext 678 | * @return {Array.?} Five element Array, containing the prefix of 679 | * longtext, the suffix of longtext, the prefix of shorttext, the suffix 680 | * of shorttext and the common middle. Or null if there was no match. 681 | * @private 682 | */ 683 | function diff_halfMatchI(longtext, shorttext, i) { 684 | // Start with a 1/4 length substring at position i as a seed. 685 | var seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); 686 | var j = -1; 687 | var best_common = ''; 688 | var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b; 689 | while ((j = shorttext.indexOf(seed, j + 1)) != -1) { 690 | var prefixLength = dmp.diff_commonPrefix(longtext.substring(i), 691 | shorttext.substring(j)); 692 | var suffixLength = dmp.diff_commonSuffix(longtext.substring(0, i), 693 | shorttext.substring(0, j)); 694 | if (best_common.length < suffixLength + prefixLength) { 695 | best_common = shorttext.substring(j - suffixLength, j) + 696 | shorttext.substring(j, j + prefixLength); 697 | best_longtext_a = longtext.substring(0, i - suffixLength); 698 | best_longtext_b = longtext.substring(i + prefixLength); 699 | best_shorttext_a = shorttext.substring(0, j - suffixLength); 700 | best_shorttext_b = shorttext.substring(j + prefixLength); 701 | } 702 | } 703 | if (best_common.length >= longtext.length / 2) { 704 | return [best_longtext_a, best_longtext_b, 705 | best_shorttext_a, best_shorttext_b, best_common]; 706 | } else { 707 | return null; 708 | } 709 | } 710 | 711 | // First check if the second quarter is the seed for a half-match. 712 | var hm1 = diff_halfMatchI(longtext, shorttext, 713 | Math.ceil(longtext.length / 4)); 714 | // Check again based on the third quarter. 715 | var hm2 = diff_halfMatchI(longtext, shorttext, 716 | Math.ceil(longtext.length / 2)); 717 | var hm; 718 | if (!hm1 && !hm2) { 719 | return null; 720 | } else if (!hm2) { 721 | hm = hm1; 722 | } else if (!hm1) { 723 | hm = hm2; 724 | } else { 725 | // Both matched. Select the longest. 726 | hm = hm1[4].length > hm2[4].length ? hm1 : hm2; 727 | } 728 | 729 | // A half-match was found, sort out the return data. 730 | var text1_a, text1_b, text2_a, text2_b; 731 | if (text1.length > text2.length) { 732 | text1_a = hm[0]; 733 | text1_b = hm[1]; 734 | text2_a = hm[2]; 735 | text2_b = hm[3]; 736 | } else { 737 | text2_a = hm[0]; 738 | text2_b = hm[1]; 739 | text1_a = hm[2]; 740 | text1_b = hm[3]; 741 | } 742 | var mid_common = hm[4]; 743 | return [text1_a, text1_b, text2_a, text2_b, mid_common]; 744 | }; 745 | 746 | 747 | /** 748 | * Reduce the number of edits by eliminating semantically trivial equalities. 749 | * @param {Array.>} diffs Array of diff tuples 750 | */ 751 | diff_match_patch.prototype.diff_cleanupSemantic = function(diffs) { 752 | var changes = false; 753 | var equalities = []; // Stack of indices where equalities are found. 754 | var equalitiesLength = 0; // Keeping our own length var is faster in JS. 755 | var lastequality = null; // Always equal to equalities[equalitiesLength-1][1] 756 | var pointer = 0; // Index of current position. 757 | // Number of characters that changed prior to the equality. 758 | var length_changes1 = 0; 759 | // Number of characters that changed after the equality. 760 | var length_changes2 = 0; 761 | while (pointer < diffs.length) { 762 | if (diffs[pointer][0] == DIFF_EQUAL) { // equality found 763 | equalities[equalitiesLength++] = pointer; 764 | length_changes1 = length_changes2; 765 | length_changes2 = 0; 766 | lastequality = diffs[pointer][1]; 767 | } else { // an insertion or deletion 768 | length_changes2 += diffs[pointer][1].length; 769 | if (lastequality !== null && (lastequality.length <= length_changes1) && 770 | (lastequality.length <= length_changes2)) { 771 | // Duplicate record 772 | diffs.splice(equalities[equalitiesLength - 1], 0, 773 | [DIFF_DELETE, lastequality]); 774 | // Change second copy to insert. 775 | diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; 776 | // Throw away the equality we just deleted. 777 | equalitiesLength--; 778 | // Throw away the previous equality (it needs to be reevaluated). 779 | equalitiesLength--; 780 | pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; 781 | length_changes1 = 0; // Reset the counters. 782 | length_changes2 = 0; 783 | lastequality = null; 784 | changes = true; 785 | } 786 | } 787 | pointer++; 788 | } 789 | if (changes) { 790 | this.diff_cleanupMerge(diffs); 791 | } 792 | this.diff_cleanupSemanticLossless(diffs); 793 | }; 794 | 795 | 796 | /** 797 | * Look for single edits surrounded on both sides by equalities 798 | * which can be shifted sideways to align the edit to a word boundary. 799 | * e.g: The cat came. -> The cat came. 800 | * @param {Array.>} diffs Array of diff tuples 801 | */ 802 | diff_match_patch.prototype.diff_cleanupSemanticLossless = function(diffs) { 803 | // Define some regex patterns for matching boundaries. 804 | var punctuation = /[^a-zA-Z0-9]/; 805 | var whitespace = /\s/; 806 | var linebreak = /[\r\n]/; 807 | var blanklineEnd = /\n\r?\n$/; 808 | var blanklineStart = /^\r?\n\r?\n/; 809 | 810 | /** 811 | * Given two strings, compute a score representing whether the internal 812 | * boundary falls on logical boundaries. 813 | * Scores range from 5 (best) to 0 (worst). 814 | * Closure, makes reference to regex patterns defined above. 815 | * @param {string} one First string 816 | * @param {string} two Second string 817 | * @return {number} The score. 818 | */ 819 | function diff_cleanupSemanticScore(one, two) { 820 | if (!one || !two) { 821 | // Edges are the best. 822 | return 5; 823 | } 824 | 825 | // Each port of this function behaves slightly differently due to 826 | // subtle differences in each language's definition of things like 827 | // 'whitespace'. Since this function's purpose is largely cosmetic, 828 | // the choice has been made to use each language's native features 829 | // rather than force total conformity. 830 | var score = 0; 831 | // One point for non-alphanumeric. 832 | if (one.charAt(one.length - 1).match(punctuation) || 833 | two.charAt(0).match(punctuation)) { 834 | score++; 835 | // Two points for whitespace. 836 | if (one.charAt(one.length - 1).match(whitespace) || 837 | two.charAt(0).match(whitespace)) { 838 | score++; 839 | // Three points for line breaks. 840 | if (one.charAt(one.length - 1).match(linebreak) || 841 | two.charAt(0).match(linebreak)) { 842 | score++; 843 | // Four points for blank lines. 844 | if (one.match(blanklineEnd) || two.match(blanklineStart)) { 845 | score++; 846 | } 847 | } 848 | } 849 | } 850 | return score; 851 | } 852 | 853 | var pointer = 1; 854 | // Intentionally ignore the first and last element (don't need checking). 855 | while (pointer < diffs.length - 1) { 856 | if (diffs[pointer - 1][0] == DIFF_EQUAL && 857 | diffs[pointer + 1][0] == DIFF_EQUAL) { 858 | // This is a single edit surrounded by equalities. 859 | var equality1 = diffs[pointer - 1][1]; 860 | var edit = diffs[pointer][1]; 861 | var equality2 = diffs[pointer + 1][1]; 862 | 863 | // First, shift the edit as far left as possible. 864 | var commonOffset = this.diff_commonSuffix(equality1, edit); 865 | if (commonOffset) { 866 | var commonString = edit.substring(edit.length - commonOffset); 867 | equality1 = equality1.substring(0, equality1.length - commonOffset); 868 | edit = commonString + edit.substring(0, edit.length - commonOffset); 869 | equality2 = commonString + equality2; 870 | } 871 | 872 | // Second, step character by character right, looking for the best fit. 873 | var bestEquality1 = equality1; 874 | var bestEdit = edit; 875 | var bestEquality2 = equality2; 876 | var bestScore = diff_cleanupSemanticScore(equality1, edit) + 877 | diff_cleanupSemanticScore(edit, equality2); 878 | while (edit.charAt(0) === equality2.charAt(0)) { 879 | equality1 += edit.charAt(0); 880 | edit = edit.substring(1) + equality2.charAt(0); 881 | equality2 = equality2.substring(1); 882 | var score = diff_cleanupSemanticScore(equality1, edit) + 883 | diff_cleanupSemanticScore(edit, equality2); 884 | // The >= encourages trailing rather than leading whitespace on edits. 885 | if (score >= bestScore) { 886 | bestScore = score; 887 | bestEquality1 = equality1; 888 | bestEdit = edit; 889 | bestEquality2 = equality2; 890 | } 891 | } 892 | 893 | if (diffs[pointer - 1][1] != bestEquality1) { 894 | // We have an improvement, save it back to the diff. 895 | if (bestEquality1) { 896 | diffs[pointer - 1][1] = bestEquality1; 897 | } else { 898 | diffs.splice(pointer - 1, 1); 899 | pointer--; 900 | } 901 | diffs[pointer][1] = bestEdit; 902 | if (bestEquality2) { 903 | diffs[pointer + 1][1] = bestEquality2; 904 | } else { 905 | diffs.splice(pointer + 1, 1); 906 | pointer--; 907 | } 908 | } 909 | } 910 | pointer++; 911 | } 912 | }; 913 | 914 | 915 | /** 916 | * Reduce the number of edits by eliminating operationally trivial equalities. 917 | * @param {Array.>} diffs Array of diff tuples 918 | */ 919 | diff_match_patch.prototype.diff_cleanupEfficiency = function(diffs) { 920 | var changes = false; 921 | var equalities = []; // Stack of indices where equalities are found. 922 | var equalitiesLength = 0; // Keeping our own length var is faster in JS. 923 | var lastequality = ''; // Always equal to equalities[equalitiesLength-1][1] 924 | var pointer = 0; // Index of current position. 925 | // Is there an insertion operation before the last equality. 926 | var pre_ins = false; 927 | // Is there a deletion operation before the last equality. 928 | var pre_del = false; 929 | // Is there an insertion operation after the last equality. 930 | var post_ins = false; 931 | // Is there a deletion operation after the last equality. 932 | var post_del = false; 933 | while (pointer < diffs.length) { 934 | if (diffs[pointer][0] == DIFF_EQUAL) { // equality found 935 | if (diffs[pointer][1].length < this.Diff_EditCost && 936 | (post_ins || post_del)) { 937 | // Candidate found. 938 | equalities[equalitiesLength++] = pointer; 939 | pre_ins = post_ins; 940 | pre_del = post_del; 941 | lastequality = diffs[pointer][1]; 942 | } else { 943 | // Not a candidate, and can never become one. 944 | equalitiesLength = 0; 945 | lastequality = ''; 946 | } 947 | post_ins = post_del = false; 948 | } else { // an insertion or deletion 949 | if (diffs[pointer][0] == DIFF_DELETE) { 950 | post_del = true; 951 | } else { 952 | post_ins = true; 953 | } 954 | /* 955 | * Five types to be split: 956 | * ABXYCD 957 | * AXCD 958 | * ABXC 959 | * AXCD 960 | * ABXC 961 | */ 962 | if (lastequality && ((pre_ins && pre_del && post_ins && post_del) || 963 | ((lastequality.length < this.Diff_EditCost / 2) && 964 | (pre_ins + pre_del + post_ins + post_del) == 3))) { 965 | // Duplicate record 966 | diffs.splice(equalities[equalitiesLength - 1], 0, 967 | [DIFF_DELETE, lastequality]); 968 | // Change second copy to insert. 969 | diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; 970 | equalitiesLength--; // Throw away the equality we just deleted; 971 | lastequality = ''; 972 | if (pre_ins && pre_del) { 973 | // No changes made which could affect previous entry, keep going. 974 | post_ins = post_del = true; 975 | equalitiesLength = 0; 976 | } else { 977 | equalitiesLength--; // Throw away the previous equality; 978 | pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; 979 | post_ins = post_del = false; 980 | } 981 | changes = true; 982 | } 983 | } 984 | pointer++; 985 | } 986 | 987 | if (changes) { 988 | this.diff_cleanupMerge(diffs); 989 | } 990 | }; 991 | 992 | 993 | /** 994 | * Reorder and merge like edit sections. Merge equalities. 995 | * Any edit section can move as long as it doesn't cross an equality. 996 | * @param {Array.>} diffs Array of diff tuples 997 | */ 998 | diff_match_patch.prototype.diff_cleanupMerge = function(diffs) { 999 | diffs.push([DIFF_EQUAL, '']); // Add a dummy entry at the end. 1000 | var pointer = 0; 1001 | var count_delete = 0; 1002 | var count_insert = 0; 1003 | var text_delete = ''; 1004 | var text_insert = ''; 1005 | var commonlength; 1006 | while (pointer < diffs.length) { 1007 | switch (diffs[pointer][0]) { 1008 | case DIFF_INSERT: 1009 | count_insert++; 1010 | text_insert += diffs[pointer][1]; 1011 | pointer++; 1012 | break; 1013 | case DIFF_DELETE: 1014 | count_delete++; 1015 | text_delete += diffs[pointer][1]; 1016 | pointer++; 1017 | break; 1018 | case DIFF_EQUAL: 1019 | // Upon reaching an equality, check for prior redundancies. 1020 | if (count_delete !== 0 || count_insert !== 0) { 1021 | if (count_delete !== 0 && count_insert !== 0) { 1022 | // Factor out any common prefixies. 1023 | commonlength = this.diff_commonPrefix(text_insert, text_delete); 1024 | if (commonlength !== 0) { 1025 | if ((pointer - count_delete - count_insert) > 0 && 1026 | diffs[pointer - count_delete - count_insert - 1][0] == 1027 | DIFF_EQUAL) { 1028 | diffs[pointer - count_delete - count_insert - 1][1] += 1029 | text_insert.substring(0, commonlength); 1030 | } else { 1031 | diffs.splice(0, 0, [DIFF_EQUAL, 1032 | text_insert.substring(0, commonlength)]); 1033 | pointer++; 1034 | } 1035 | text_insert = text_insert.substring(commonlength); 1036 | text_delete = text_delete.substring(commonlength); 1037 | } 1038 | // Factor out any common suffixies. 1039 | commonlength = this.diff_commonSuffix(text_insert, text_delete); 1040 | if (commonlength !== 0) { 1041 | diffs[pointer][1] = text_insert.substring(text_insert.length - 1042 | commonlength) + diffs[pointer][1]; 1043 | text_insert = text_insert.substring(0, text_insert.length - 1044 | commonlength); 1045 | text_delete = text_delete.substring(0, text_delete.length - 1046 | commonlength); 1047 | } 1048 | } 1049 | // Delete the offending records and add the merged ones. 1050 | if (count_delete === 0) { 1051 | diffs.splice(pointer - count_delete - count_insert, 1052 | count_delete + count_insert, [DIFF_INSERT, text_insert]); 1053 | } else if (count_insert === 0) { 1054 | diffs.splice(pointer - count_delete - count_insert, 1055 | count_delete + count_insert, [DIFF_DELETE, text_delete]); 1056 | } else { 1057 | diffs.splice(pointer - count_delete - count_insert, 1058 | count_delete + count_insert, [DIFF_DELETE, text_delete], 1059 | [DIFF_INSERT, text_insert]); 1060 | } 1061 | pointer = pointer - count_delete - count_insert + 1062 | (count_delete ? 1 : 0) + (count_insert ? 1 : 0) + 1; 1063 | } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) { 1064 | // Merge this equality with the previous one. 1065 | diffs[pointer - 1][1] += diffs[pointer][1]; 1066 | diffs.splice(pointer, 1); 1067 | } else { 1068 | pointer++; 1069 | } 1070 | count_insert = 0; 1071 | count_delete = 0; 1072 | text_delete = ''; 1073 | text_insert = ''; 1074 | break; 1075 | } 1076 | } 1077 | if (diffs[diffs.length - 1][1] === '') { 1078 | diffs.pop(); // Remove the dummy entry at the end. 1079 | } 1080 | 1081 | // Second pass: look for single edits surrounded on both sides by equalities 1082 | // which can be shifted sideways to eliminate an equality. 1083 | // e.g: ABAC -> ABAC 1084 | var changes = false; 1085 | pointer = 1; 1086 | // Intentionally ignore the first and last element (don't need checking). 1087 | while (pointer < diffs.length - 1) { 1088 | if (diffs[pointer - 1][0] == DIFF_EQUAL && 1089 | diffs[pointer + 1][0] == DIFF_EQUAL) { 1090 | // This is a single edit surrounded by equalities. 1091 | if (diffs[pointer][1].substring(diffs[pointer][1].length - 1092 | diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) { 1093 | // Shift the edit over the previous equality. 1094 | diffs[pointer][1] = diffs[pointer - 1][1] + 1095 | diffs[pointer][1].substring(0, diffs[pointer][1].length - 1096 | diffs[pointer - 1][1].length); 1097 | diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; 1098 | diffs.splice(pointer - 1, 1); 1099 | changes = true; 1100 | } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) 1101 | == diffs[pointer + 1][1]) { 1102 | // Shift the edit over the next equality. 1103 | diffs[pointer - 1][1] += diffs[pointer + 1][1]; 1104 | diffs[pointer][1] = 1105 | diffs[pointer][1].substring(diffs[pointer + 1][1].length) + 1106 | diffs[pointer + 1][1]; 1107 | diffs.splice(pointer + 1, 1); 1108 | changes = true; 1109 | } 1110 | } 1111 | pointer++; 1112 | } 1113 | // If shifts were made, the diff needs reordering and another shift sweep. 1114 | if (changes) { 1115 | this.diff_cleanupMerge(diffs); 1116 | } 1117 | }; 1118 | 1119 | 1120 | /** 1121 | * loc is a location in text1, compute and return the equivalent location in 1122 | * text2. 1123 | * e.g. 'The cat' vs 'The big cat', 1->1, 5->8 1124 | * @param {Array.>} diffs Array of diff tuples 1125 | * @param {number} loc Location within text1 1126 | * @return {number} Location within text2 1127 | */ 1128 | diff_match_patch.prototype.diff_xIndex = function(diffs, loc) { 1129 | var chars1 = 0; 1130 | var chars2 = 0; 1131 | var last_chars1 = 0; 1132 | var last_chars2 = 0; 1133 | var x; 1134 | for (x = 0; x < diffs.length; x++) { 1135 | if (diffs[x][0] !== DIFF_INSERT) { // Equality or deletion. 1136 | chars1 += diffs[x][1].length; 1137 | } 1138 | if (diffs[x][0] !== DIFF_DELETE) { // Equality or insertion. 1139 | chars2 += diffs[x][1].length; 1140 | } 1141 | if (chars1 > loc) { // Overshot the location. 1142 | break; 1143 | } 1144 | last_chars1 = chars1; 1145 | last_chars2 = chars2; 1146 | } 1147 | // Was the location was deleted? 1148 | if (diffs.length != x && diffs[x][0] === DIFF_DELETE) { 1149 | return last_chars2; 1150 | } 1151 | // Add the remaining character length. 1152 | return last_chars2 + (loc - last_chars1); 1153 | }; 1154 | 1155 | 1156 | /** 1157 | * Convert a diff array into a pretty HTML report. 1158 | * @param {Array.>} diffs Array of diff tuples 1159 | * @return {string} HTML representation 1160 | */ 1161 | diff_match_patch.prototype.diff_prettyHtml = function(diffs) { 1162 | var html = []; 1163 | var i = 0; 1164 | for (var x = 0; x < diffs.length; x++) { 1165 | var op = diffs[x][0]; // Operation (insert, delete, equal) 1166 | var data = diffs[x][1]; // Text of change. 1167 | var text = data.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '¶
'); 1169 | switch (op) { 1170 | case DIFF_INSERT: 1171 | html[x] = '' + 1172 | text + ''; 1173 | break; 1174 | case DIFF_DELETE: 1175 | html[x] = '' + 1176 | text + ''; 1177 | break; 1178 | case DIFF_EQUAL: 1179 | html[x] = '' + text + ''; 1180 | break; 1181 | } 1182 | if (op !== DIFF_DELETE) { 1183 | i += data.length; 1184 | } 1185 | } 1186 | return html.join(''); 1187 | }; 1188 | 1189 | 1190 | /** 1191 | * Compute and return the source text (all equalities and deletions). 1192 | * @param {Array.>} diffs Array of diff tuples 1193 | * @return {string} Source text 1194 | */ 1195 | diff_match_patch.prototype.diff_text1 = function(diffs) { 1196 | var txt = []; 1197 | for (var x = 0; x < diffs.length; x++) { 1198 | if (diffs[x][0] !== DIFF_INSERT) { 1199 | txt[x] = diffs[x][1]; 1200 | } 1201 | } 1202 | return txt.join(''); 1203 | }; 1204 | 1205 | 1206 | /** 1207 | * Compute and return the destination text (all equalities and insertions). 1208 | * @param {Array.>} diffs Array of diff tuples 1209 | * @return {string} Destination text 1210 | */ 1211 | diff_match_patch.prototype.diff_text2 = function(diffs) { 1212 | var txt = []; 1213 | for (var x = 0; x < diffs.length; x++) { 1214 | if (diffs[x][0] !== DIFF_DELETE) { 1215 | txt[x] = diffs[x][1]; 1216 | } 1217 | } 1218 | return txt.join(''); 1219 | }; 1220 | 1221 | 1222 | /** 1223 | * Crush the diff into an encoded string which describes the operations 1224 | * required to transform text1 into text2. 1225 | * E.g. =3\t-2\t+ing -> Keep 3 chars, delete 2 chars, insert 'ing'. 1226 | * Operations are tab-separated. Inserted text is escaped using %xx notation. 1227 | * @param {Array.>} diffs Array of diff tuples 1228 | * @return {string} Delta text 1229 | */ 1230 | diff_match_patch.prototype.diff_toDelta = function(diffs) { 1231 | var txt = []; 1232 | for (var x = 0; x < diffs.length; x++) { 1233 | switch (diffs[x][0]) { 1234 | case DIFF_INSERT: 1235 | txt[x] = '+' + encodeURI(diffs[x][1]); 1236 | break; 1237 | case DIFF_DELETE: 1238 | txt[x] = '-' + diffs[x][1].length; 1239 | break; 1240 | case DIFF_EQUAL: 1241 | txt[x] = '=' + diffs[x][1].length; 1242 | break; 1243 | } 1244 | } 1245 | // Opera doesn't know how to encode char 0. 1246 | return txt.join('\t').replace(/\0/g, '%00').replace(/%20/g, ' '); 1247 | }; 1248 | 1249 | 1250 | /** 1251 | * Given the original text1, and an encoded string which describes the 1252 | * operations required to transform text1 into text2, compute the full diff. 1253 | * @param {string} text1 Source string for the diff 1254 | * @param {string} delta Delta text 1255 | * @return {Array.>} Array of diff tuples 1256 | * @throws {Error} If invalid input 1257 | */ 1258 | diff_match_patch.prototype.diff_fromDelta = function(text1, delta) { 1259 | var diffs = []; 1260 | var diffsLength = 0; // Keeping our own length var is faster in JS. 1261 | var pointer = 0; // Cursor in text1 1262 | // Opera doesn't know how to decode char 0. 1263 | delta = delta.replace(/%00/g, '\0'); 1264 | var tokens = delta.split(/\t/g); 1265 | for (var x = 0; x < tokens.length; x++) { 1266 | // Each token begins with a one character parameter which specifies the 1267 | // operation of this token (delete, insert, equality). 1268 | var param = tokens[x].substring(1); 1269 | switch (tokens[x].charAt(0)) { 1270 | case '+': 1271 | try { 1272 | diffs[diffsLength++] = [DIFF_INSERT, decodeURI(param)]; 1273 | } catch (ex) { 1274 | // Malformed URI sequence. 1275 | throw new Error('Illegal escape in diff_fromDelta: ' + param); 1276 | } 1277 | break; 1278 | case '-': 1279 | // Fall through. 1280 | case '=': 1281 | var n = parseInt(param, 10); 1282 | if (isNaN(n) || n < 0) { 1283 | throw new Error('Invalid number in diff_fromDelta: ' + param); 1284 | } 1285 | var text = text1.substring(pointer, pointer += n); 1286 | if (tokens[x].charAt(0) == '=') { 1287 | diffs[diffsLength++] = [DIFF_EQUAL, text]; 1288 | } else { 1289 | diffs[diffsLength++] = [DIFF_DELETE, text]; 1290 | } 1291 | break; 1292 | default: 1293 | // Blank tokens are ok (from a trailing \t). 1294 | // Anything else is an error. 1295 | if (tokens[x]) { 1296 | throw new Error('Invalid diff operation in diff_fromDelta: ' + 1297 | tokens[x]); 1298 | } 1299 | } 1300 | } 1301 | if (pointer != text1.length) { 1302 | throw new Error('Delta length (' + pointer + 1303 | ') does not equal source text length (' + text1.length + ').'); 1304 | } 1305 | return diffs; 1306 | }; 1307 | 1308 | 1309 | // MATCH FUNCTIONS 1310 | 1311 | 1312 | /** 1313 | * Locate the best instance of 'pattern' in 'text' near 'loc'. 1314 | * @param {string} text The text to search 1315 | * @param {string} pattern The pattern to search for 1316 | * @param {number} loc The location to search around 1317 | * @return {number?} Best match index or null 1318 | */ 1319 | diff_match_patch.prototype.match_main = function(text, pattern, loc) { 1320 | loc = Math.max(0, Math.min(loc, text.length - pattern.length)); 1321 | if (text == pattern) { 1322 | // Shortcut (potentially not guaranteed by the algorithm) 1323 | return 0; 1324 | } else if (text.length === 0) { 1325 | // Nothing to match. 1326 | return null; 1327 | } else if (text.substring(loc, loc + pattern.length) == pattern) { 1328 | // Perfect match at the perfect spot! (Includes case of null pattern) 1329 | return loc; 1330 | } else { 1331 | // Do a fuzzy compare. 1332 | return this.match_bitap(text, pattern, loc); 1333 | } 1334 | }; 1335 | 1336 | 1337 | /** 1338 | * Locate the best instance of 'pattern' in 'text' near 'loc' using the 1339 | * Bitap algorithm. 1340 | * @param {string} text The text to search 1341 | * @param {string} pattern The pattern to search for 1342 | * @param {number} loc The location to search around 1343 | * @return {number?} Best match index or null 1344 | * @private 1345 | */ 1346 | diff_match_patch.prototype.match_bitap = function(text, pattern, loc) { 1347 | if (pattern.length > this.Match_MaxBits) { 1348 | throw new Error('Pattern too long for this browser.'); 1349 | } 1350 | 1351 | // Initialise the alphabet. 1352 | var s = this.match_alphabet(pattern); 1353 | 1354 | var score_text_length = text.length; 1355 | // Coerce the text length between reasonable maximums and minimums. 1356 | score_text_length = Math.max(score_text_length, this.Match_MinLength); 1357 | score_text_length = Math.min(score_text_length, this.Match_MaxLength); 1358 | 1359 | var dmp = this; // 'this' becomes 'window' in a closure. 1360 | 1361 | /** 1362 | * Compute and return the score for a match with e errors and x location. 1363 | * Accesses loc, score_text_length and pattern through being a closure. 1364 | * @param {number} e Number of errors in match 1365 | * @param {number} x Location of match 1366 | * @return {number} Overall score for match 1367 | * @private 1368 | */ 1369 | function match_bitapScore(e, x) { 1370 | var d = Math.abs(loc - x); 1371 | return (e / pattern.length / dmp.Match_Balance) + 1372 | (d / score_text_length / (1.0 - dmp.Match_Balance)); 1373 | } 1374 | 1375 | // Highest score beyond which we give up. 1376 | var score_threshold = this.Match_Threshold; 1377 | // Is there a nearby exact match? (speedup) 1378 | var best_loc = text.indexOf(pattern, loc); 1379 | if (best_loc != -1) { 1380 | score_threshold = Math.min(match_bitapScore(0, best_loc), score_threshold); 1381 | } 1382 | // What about in the other direction? (speedup) 1383 | best_loc = text.lastIndexOf(pattern, loc + pattern.length); 1384 | if (best_loc != -1) { 1385 | score_threshold = Math.min(match_bitapScore(0, best_loc), score_threshold); 1386 | } 1387 | 1388 | // Initialise the bit arrays. 1389 | var matchmask = 1 << (pattern.length - 1); 1390 | best_loc = null; 1391 | 1392 | var bin_min, bin_mid; 1393 | var bin_max = Math.max(loc + loc, text.length); 1394 | var last_rd; 1395 | for (var d = 0; d < pattern.length; d++) { 1396 | // Scan for the best match; each iteration allows for one more error. 1397 | var rd = Array(text.length); 1398 | 1399 | // Run a binary search to determine how far from 'loc' we can stray at this 1400 | // error level. 1401 | bin_min = loc; 1402 | bin_mid = bin_max; 1403 | while (bin_min < bin_mid) { 1404 | if (match_bitapScore(d, bin_mid) < score_threshold) { 1405 | bin_min = bin_mid; 1406 | } else { 1407 | bin_max = bin_mid; 1408 | } 1409 | bin_mid = Math.floor((bin_max - bin_min) / 2 + bin_min); 1410 | } 1411 | // Use the result from this iteration as the maximum for the next. 1412 | bin_max = bin_mid; 1413 | var start = Math.max(0, loc - (bin_mid - loc) - 1); 1414 | var finish = Math.min(text.length - 1, pattern.length + bin_mid); 1415 | 1416 | if (text.charAt(finish) == pattern.charAt(pattern.length - 1)) { 1417 | rd[finish] = (1 << (d + 1)) - 1; 1418 | } else { 1419 | rd[finish] = (1 << d) - 1; 1420 | } 1421 | for (var j = finish - 1; j >= start; j--) { 1422 | // The alphabet (s) is a sparse hash, so the following lines generate 1423 | // warnings. 1424 | if (d === 0) { // First pass: exact match. 1425 | rd[j] = ((rd[j + 1] << 1) | 1) & s[text.charAt(j)]; 1426 | } else { // Subsequent passes: fuzzy match. 1427 | rd[j] = ((rd[j + 1] << 1) | 1) & s[text.charAt(j)] | 1428 | ((last_rd[j + 1] << 1) | 1) | ((last_rd[j] << 1) | 1) | 1429 | last_rd[j + 1]; 1430 | } 1431 | if (rd[j] & matchmask) { 1432 | var score = match_bitapScore(d, j); 1433 | // This match will almost certainly be better than any existing match. 1434 | // But check anyway. 1435 | if (score <= score_threshold) { 1436 | // Told you so. 1437 | score_threshold = score; 1438 | best_loc = j; 1439 | if (j > loc) { 1440 | // When passing loc, don't exceed our current distance from loc. 1441 | start = Math.max(0, loc - (j - loc)); 1442 | } else { 1443 | // Already passed loc, downhill from here on in. 1444 | break; 1445 | } 1446 | } 1447 | } 1448 | } 1449 | // No hope for a (better) match at greater error levels. 1450 | if (match_bitapScore(d + 1, loc) > score_threshold) { 1451 | break; 1452 | } 1453 | last_rd = rd; 1454 | } 1455 | return best_loc; 1456 | }; 1457 | 1458 | 1459 | /** 1460 | * Initialise the alphabet for the Bitap algorithm. 1461 | * @param {string} pattern The text to encode 1462 | * @return {Object} Hash of character locations 1463 | * @private 1464 | */ 1465 | diff_match_patch.prototype.match_alphabet = function(pattern) { 1466 | var s = {}; 1467 | for (var i = 0; i < pattern.length; i++) { 1468 | s[pattern.charAt(i)] = 0; 1469 | } 1470 | for (var i = 0; i < pattern.length; i++) { 1471 | s[pattern.charAt(i)] |= 1 << (pattern.length - i - 1); 1472 | } 1473 | return s; 1474 | }; 1475 | 1476 | 1477 | // PATCH FUNCTIONS 1478 | 1479 | 1480 | /** 1481 | * Increase the context until it is unique, 1482 | * but don't let the pattern expand beyond Match_MaxBits. 1483 | * @param {patch_obj} patch The patch to grow 1484 | * @param {string} text Source text 1485 | * @private 1486 | */ 1487 | diff_match_patch.prototype.patch_addContext = function(patch, text) { 1488 | var pattern = text.substring(patch.start2, patch.start2 + patch.length1); 1489 | var padding = 0; 1490 | while (text.indexOf(pattern) != text.lastIndexOf(pattern) && 1491 | pattern.length < this.Match_MaxBits - this.Patch_Margin 1492 | - this.Patch_Margin) { 1493 | padding += this.Patch_Margin; 1494 | pattern = text.substring(patch.start2 - padding, 1495 | patch.start2 + patch.length1 + padding); 1496 | } 1497 | // Add one chunk for good luck. 1498 | padding += this.Patch_Margin; 1499 | // Add the prefix. 1500 | var prefix = text.substring(patch.start2 - padding, patch.start2); 1501 | if (prefix !== '') { 1502 | patch.diffs.unshift([DIFF_EQUAL, prefix]); 1503 | } 1504 | // Add the suffix. 1505 | var suffix = text.substring(patch.start2 + patch.length1, 1506 | patch.start2 + patch.length1 + padding); 1507 | if (suffix !== '') { 1508 | patch.diffs.push([DIFF_EQUAL, suffix]); 1509 | } 1510 | 1511 | // Roll back the start points. 1512 | patch.start1 -= prefix.length; 1513 | patch.start2 -= prefix.length; 1514 | // Extend the lengths. 1515 | patch.length1 += prefix.length + suffix.length; 1516 | patch.length2 += prefix.length + suffix.length; 1517 | }; 1518 | 1519 | 1520 | /** 1521 | * Compute a list of patches to turn text1 into text2. 1522 | * Use diffs if provided, otherwise compute it ourselves. 1523 | * There are two ways to call this function: 1524 | * Method 1: 1525 | * a = Old text, b = New text, c = array of diff tuplle for a to b 1526 | * Method 2: 1527 | * a = Array of diff tuples for text 1 to text 2, b and c undefined 1528 | * @param {string|Array.>} a Old text (method 1) or Array of diff 1529 | * tuples for text1 to text2 (method 2) 1530 | * @param {string?} b New text (method 1) 1531 | * @param {Array.>} c Optional array of diff tuples for text1 to text2 1532 | * (method 1) 1533 | * @return {Array.} Array of patch objects 1534 | */ 1535 | diff_match_patch.prototype.patch_make = function(a, b, c) { 1536 | var text1, text2, diffs; 1537 | if (typeof b == 'undefined') { 1538 | diffs = a; 1539 | text1 = this.diff_text1(diffs); 1540 | text2 = ''; // text2 is not actually used. 1541 | } else { 1542 | text1 = a; 1543 | text2 = b; 1544 | if (typeof c != 'undefined') { 1545 | diffs = c; 1546 | } else { 1547 | diffs = this.diff_main(text1, text2, true); 1548 | if (diffs.length > 2) { 1549 | this.diff_cleanupSemantic(diffs); 1550 | this.diff_cleanupEfficiency(diffs); 1551 | } 1552 | } 1553 | } 1554 | 1555 | if (diffs.length === 0) { 1556 | return []; // Get rid of the null case. 1557 | } 1558 | var patches = []; 1559 | var patch = new patch_obj(); 1560 | var patchDiffLength = 0; // Keeping our own length var is faster in JS. 1561 | var char_count1 = 0; // Number of characters into the text1 string. 1562 | var char_count2 = 0; // Number of characters into the text2 string. 1563 | var prepatch_text = text1; // Recreate the patches to determine context info. 1564 | var postpatch_text = text1; 1565 | for (var x = 0; x < diffs.length; x++) { 1566 | var diff_type = diffs[x][0]; 1567 | var diff_text = diffs[x][1]; 1568 | 1569 | if (!patchDiffLength && diff_type !== DIFF_EQUAL) { 1570 | // A new patch starts here. 1571 | patch.start1 = char_count1; 1572 | patch.start2 = char_count2; 1573 | } 1574 | 1575 | switch (diff_type) { 1576 | case DIFF_INSERT: 1577 | patch.diffs[patchDiffLength++] = diffs[x]; 1578 | patch.length2 += diff_text.length; 1579 | postpatch_text = postpatch_text.substring(0, char_count2) + diff_text + 1580 | postpatch_text.substring(char_count2); 1581 | break; 1582 | case DIFF_DELETE: 1583 | patch.length1 += diff_text.length; 1584 | patch.diffs[patchDiffLength++] = diffs[x]; 1585 | postpatch_text = postpatch_text.substring(0, char_count2) + 1586 | postpatch_text.substring(char_count2 + diff_text.length); 1587 | break; 1588 | case DIFF_EQUAL: 1589 | if (diff_text.length <= 2 * this.Patch_Margin && 1590 | patchDiffLength && diffs.length != x + 1) { 1591 | // Small equality inside a patch. 1592 | patch.diffs[patchDiffLength++] = diffs[x]; 1593 | patch.length1 += diff_text.length; 1594 | patch.length2 += diff_text.length; 1595 | } else if (diff_text.length >= 2 * this.Patch_Margin) { 1596 | // Time for a new patch. 1597 | if (patchDiffLength) { 1598 | this.patch_addContext(patch, prepatch_text); 1599 | patches.push(patch); 1600 | patch = new patch_obj(); 1601 | patchDiffLength = 0; 1602 | prepatch_text = postpatch_text; 1603 | } 1604 | } 1605 | break; 1606 | } 1607 | 1608 | // Update the current character count. 1609 | if (diff_type !== DIFF_INSERT) { 1610 | char_count1 += diff_text.length; 1611 | } 1612 | if (diff_type !== DIFF_DELETE) { 1613 | char_count2 += diff_text.length; 1614 | } 1615 | } 1616 | // Pick up the leftover patch if not empty. 1617 | if (patchDiffLength) { 1618 | this.patch_addContext(patch, prepatch_text); 1619 | patches.push(patch); 1620 | } 1621 | 1622 | return patches; 1623 | }; 1624 | 1625 | 1626 | /** 1627 | * Merge a set of patches onto the text. Return a patched text, as well 1628 | * as a list of true/false values indicating which patches were applied. 1629 | * @param {Array.} patches Array of patch objects 1630 | * @param {string} text Old text 1631 | * @return {Array.>} Two element Array, containing the 1632 | * new text and an array of boolean values 1633 | */ 1634 | diff_match_patch.prototype.patch_apply = function(patches, text) { 1635 | if (patches.length == 0) { 1636 | return [text, []]; 1637 | } 1638 | 1639 | // Deep copy the patches so that no changes are made to originals. 1640 | var patchesCopy = []; 1641 | for (var x = 0; x < patches.length; x++) { 1642 | var patch = patches[x]; 1643 | var patchCopy = new patch_obj(); 1644 | patchCopy.diffs = patch.diffs.slice(); 1645 | patchCopy.start1 = patch.start1; 1646 | patchCopy.start2 = patch.start2; 1647 | patchCopy.length1 = patch.length1; 1648 | patchCopy.length2 = patch.length2; 1649 | patchesCopy[x] = patchCopy; 1650 | } 1651 | patches = patchesCopy; 1652 | 1653 | var nullPadding = this.patch_addPadding(patches); 1654 | text = nullPadding + text + nullPadding; 1655 | 1656 | this.patch_splitMax(patches); 1657 | // delta keeps track of the offset between the expected and actual location 1658 | // of the previous patch. If there are patches expected at positions 10 and 1659 | // 20, but the first patch was found at 12, delta is 2 and the second patch 1660 | // has an effective expected position of 22. 1661 | var delta = 0; 1662 | var results = []; 1663 | for (var x = 0; x < patches.length; x++) { 1664 | var expected_loc = patches[x].start2 + delta; 1665 | var text1 = this.diff_text1(patches[x].diffs); 1666 | var start_loc = this.match_main(text, text1, expected_loc); 1667 | if (start_loc === null) { 1668 | // No match found. :( 1669 | results[x] = false; 1670 | } else { 1671 | // Found a match. :) 1672 | results[x] = true; 1673 | delta = start_loc - expected_loc; 1674 | var text2 = text.substring(start_loc, start_loc + text1.length); 1675 | if (text1 == text2) { 1676 | // Perfect match, just shove the replacement text in. 1677 | text = text.substring(0, start_loc) + 1678 | this.diff_text2(patches[x].diffs) + 1679 | text.substring(start_loc + text1.length); 1680 | } else { 1681 | // Imperfect match. Run a diff to get a framework of equivalent 1682 | // indicies. 1683 | var diffs = this.diff_main(text1, text2, false); 1684 | this.diff_cleanupSemanticLossless(diffs); 1685 | var index1 = 0; 1686 | var index2; 1687 | for (var y = 0; y < patches[x].diffs.length; y++) { 1688 | var mod = patches[x].diffs[y]; 1689 | if (mod[0] !== DIFF_EQUAL) { 1690 | index2 = this.diff_xIndex(diffs, index1); 1691 | } 1692 | if (mod[0] === DIFF_INSERT) { // Insertion 1693 | text = text.substring(0, start_loc + index2) + mod[1] + 1694 | text.substring(start_loc + index2); 1695 | } else if (mod[0] === DIFF_DELETE) { // Deletion 1696 | text = text.substring(0, start_loc + index2) + 1697 | text.substring(start_loc + this.diff_xIndex(diffs, 1698 | index1 + mod[1].length)); 1699 | } 1700 | if (mod[0] !== DIFF_DELETE) { 1701 | index1 += mod[1].length; 1702 | } 1703 | } 1704 | } 1705 | } 1706 | } 1707 | // Strip the padding off. 1708 | text = text.substring(nullPadding.length, text.length - nullPadding.length); 1709 | return [text, results]; 1710 | }; 1711 | 1712 | 1713 | /** 1714 | * Add some padding on text start and end so that edges can match something. 1715 | * @param {Array.} patches Array of patch objects 1716 | * @return {string} The padding string added to each side. 1717 | * @private 1718 | */ 1719 | diff_match_patch.prototype.patch_addPadding = function(patches) { 1720 | var nullPadding = ''; 1721 | for (var x = 0; x < this.Patch_Margin; x++) { 1722 | nullPadding += String.fromCharCode(x); 1723 | } 1724 | 1725 | // Bump all the patches forward. 1726 | for (var x = 0; x < patches.length; x++) { 1727 | patches[x].start1 += nullPadding.length; 1728 | patches[x].start2 += nullPadding.length; 1729 | } 1730 | 1731 | // Add some padding on start of first diff. 1732 | var patch = patches[0]; 1733 | var diffs = patch.diffs; 1734 | if (diffs.length == 0 || diffs[0][0] != DIFF_EQUAL) { 1735 | // Add nullPadding equality. 1736 | diffs.unshift([DIFF_EQUAL, nullPadding]); 1737 | patch.start1 -= nullPadding.length; // Should be 0. 1738 | patch.start2 -= nullPadding.length; // Should be 0. 1739 | patch.length1 += nullPadding.length; 1740 | patch.length2 += nullPadding.length; 1741 | } else if (nullPadding.length > diffs[0][1].length) { 1742 | // Grow first equality. 1743 | var extraLength = nullPadding.length - diffs[0][1].length; 1744 | diffs[0][1] = nullPadding.substring(diffs[0][1].length) + diffs[0][1]; 1745 | patch.start1 -= extraLength; 1746 | patch.start2 -= extraLength; 1747 | patch.length1 += extraLength; 1748 | patch.length2 += extraLength; 1749 | } 1750 | 1751 | // Add some padding on end of last diff. 1752 | patch = patches[patches.length - 1]; 1753 | diffs = patch.diffs; 1754 | if (diffs.length == 0 || diffs[diffs.length - 1][0] != DIFF_EQUAL) { 1755 | // Add nullPadding equality. 1756 | diffs.push([DIFF_EQUAL, nullPadding]); 1757 | patch.length1 += nullPadding.length; 1758 | patch.length2 += nullPadding.length; 1759 | } else if (nullPadding.length > diffs[diffs.length - 1][1].length) { 1760 | // Grow last equality. 1761 | var extraLength = nullPadding.length - diffs[diffs.length - 1][1].length; 1762 | diffs[diffs.length - 1][1] += nullPadding.substring(0, extraLength); 1763 | patch.length1 += extraLength; 1764 | patch.length2 += extraLength; 1765 | } 1766 | 1767 | return nullPadding; 1768 | }; 1769 | 1770 | 1771 | /** 1772 | * Look through the patches and break up any which are longer than the maximum 1773 | * limit of the match algorithm. 1774 | * @param {Array.} patches Array of patch objects 1775 | */ 1776 | diff_match_patch.prototype.patch_splitMax = function(patches) { 1777 | for (var x = 0; x < patches.length; x++) { 1778 | if (patches[x].length1 > this.Match_MaxBits) { 1779 | var bigpatch = patches[x]; 1780 | // Remove the big old patch. 1781 | patches.splice(x, 1); 1782 | var patch_size = this.Match_MaxBits; 1783 | var start1 = bigpatch.start1; 1784 | var start2 = bigpatch.start2; 1785 | var precontext = ''; 1786 | while (bigpatch.diffs.length !== 0) { 1787 | // Create one of several smaller patches. 1788 | var patch = new patch_obj(); 1789 | var empty = true; 1790 | patch.start1 = start1 - precontext.length; 1791 | patch.start2 = start2 - precontext.length; 1792 | if (precontext !== '') { 1793 | patch.length1 = patch.length2 = precontext.length; 1794 | patch.diffs.push([DIFF_EQUAL, precontext]); 1795 | } 1796 | while (bigpatch.diffs.length !== 0 && 1797 | patch.length1 < patch_size - this.Patch_Margin) { 1798 | var diff_type = bigpatch.diffs[0][0]; 1799 | var diff_text = bigpatch.diffs[0][1]; 1800 | if (diff_type === DIFF_INSERT) { 1801 | // Insertions are harmless. 1802 | patch.length2 += diff_text.length; 1803 | start2 += diff_text.length; 1804 | patch.diffs.push(bigpatch.diffs.shift()); 1805 | empty = false; 1806 | } else { 1807 | // Deletion or equality. Only take as much as we can stomach. 1808 | diff_text = diff_text.substring(0, patch_size - patch.length1 - 1809 | this.Patch_Margin); 1810 | patch.length1 += diff_text.length; 1811 | start1 += diff_text.length; 1812 | if (diff_type === DIFF_EQUAL) { 1813 | patch.length2 += diff_text.length; 1814 | start2 += diff_text.length; 1815 | } else { 1816 | empty = false; 1817 | } 1818 | patch.diffs.push([diff_type, diff_text]); 1819 | if (diff_text == bigpatch.diffs[0][1]) { 1820 | bigpatch.diffs.shift(); 1821 | } else { 1822 | bigpatch.diffs[0][1] = 1823 | bigpatch.diffs[0][1].substring(diff_text.length); 1824 | } 1825 | } 1826 | } 1827 | // Compute the head context for the next patch. 1828 | precontext = this.diff_text2(patch.diffs); 1829 | precontext = 1830 | precontext.substring(precontext.length - this.Patch_Margin); 1831 | // Append the end context for this patch. 1832 | var postcontext = this.diff_text1(bigpatch.diffs) 1833 | .substring(0, this.Patch_Margin); 1834 | if (postcontext !== '') { 1835 | patch.length1 += postcontext.length; 1836 | patch.length2 += postcontext.length; 1837 | if (patch.diffs.length !== 0 && 1838 | patch.diffs[patch.diffs.length - 1][0] === DIFF_EQUAL) { 1839 | patch.diffs[patch.diffs.length - 1][1] += postcontext; 1840 | } else { 1841 | patch.diffs.push([DIFF_EQUAL, postcontext]); 1842 | } 1843 | } 1844 | if (!empty) { 1845 | patches.splice(x++, 0, patch); 1846 | } 1847 | } 1848 | } 1849 | } 1850 | }; 1851 | 1852 | 1853 | /** 1854 | * Take a list of patches and return a textual representation. 1855 | * @param {Array.} patches Array of patch objects 1856 | * @return {string} Text representation of patches 1857 | */ 1858 | diff_match_patch.prototype.patch_toText = function(patches) { 1859 | var text = []; 1860 | for (var x = 0; x < patches.length; x++) { 1861 | text[x] = patches[x]; 1862 | } 1863 | return text.join(''); 1864 | }; 1865 | 1866 | 1867 | /** 1868 | * Parse a textual representation of patches and return a list of patch objects. 1869 | * @param {string} textline Text representation of patches 1870 | * @return {Array.} Array of patch objects 1871 | * @throws {Error} If invalid input 1872 | */ 1873 | diff_match_patch.prototype.patch_fromText = function(textline) { 1874 | var patches = []; 1875 | if (!textline) { 1876 | return patches; 1877 | } 1878 | // Opera doesn't know how to decode char 0. 1879 | textline = textline.replace(/%00/g, '\0'); 1880 | var text = textline.split('\n'); 1881 | var textPointer = 0; 1882 | while (textPointer < text.length) { 1883 | var m = text[textPointer].match(/^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/); 1884 | if (!m) { 1885 | throw new Error('Invalid patch string: ' + text[textPointer]); 1886 | } 1887 | var patch = new patch_obj(); 1888 | patches.push(patch); 1889 | patch.start1 = parseInt(m[1], 10); 1890 | if (m[2] === '') { 1891 | patch.start1--; 1892 | patch.length1 = 1; 1893 | } else if (m[2] == '0') { 1894 | patch.length1 = 0; 1895 | } else { 1896 | patch.start1--; 1897 | patch.length1 = parseInt(m[2], 10); 1898 | } 1899 | 1900 | patch.start2 = parseInt(m[3], 10); 1901 | if (m[4] === '') { 1902 | patch.start2--; 1903 | patch.length2 = 1; 1904 | } else if (m[4] == '0') { 1905 | patch.length2 = 0; 1906 | } else { 1907 | patch.start2--; 1908 | patch.length2 = parseInt(m[4], 10); 1909 | } 1910 | textPointer++; 1911 | 1912 | while (textPointer < text.length) { 1913 | var sign = text[textPointer].charAt(0); 1914 | try { 1915 | var line = decodeURI(text[textPointer].substring(1)); 1916 | } catch (ex) { 1917 | // Malformed URI sequence. 1918 | throw new Error('Illegal escape in patch_fromText: ' + line); 1919 | } 1920 | if (sign == '-') { 1921 | // Deletion. 1922 | patch.diffs.push([DIFF_DELETE, line]); 1923 | } else if (sign == '+') { 1924 | // Insertion. 1925 | patch.diffs.push([DIFF_INSERT, line]); 1926 | } else if (sign == ' ') { 1927 | // Minor equality. 1928 | patch.diffs.push([DIFF_EQUAL, line]); 1929 | } else if (sign == '@') { 1930 | // Start of next patch. 1931 | break; 1932 | } else if (sign === '') { 1933 | // Blank line? Whatever. 1934 | } else { 1935 | // WTF? 1936 | throw new Error('Invalid patch mode "' + sign + '" in: ' + line); 1937 | } 1938 | textPointer++; 1939 | } 1940 | } 1941 | return patches; 1942 | }; 1943 | 1944 | 1945 | /** 1946 | * Class representing one patch operation. 1947 | * @constructor 1948 | */ 1949 | function patch_obj() { 1950 | this.diffs = []; 1951 | /** @type {number?} */ 1952 | this.start1 = null; 1953 | /** @type {number?} */ 1954 | this.start2 = null; 1955 | this.length1 = 0; 1956 | this.length2 = 0; 1957 | } 1958 | 1959 | 1960 | /** 1961 | * Emmulate GNU diff's format. 1962 | * Header: @@ -382,8 +481,9 @@ 1963 | * Indicies are printed as 1-based, not 0-based. 1964 | * @return {string} The GNU diff string 1965 | */ 1966 | patch_obj.prototype.toString = function() { 1967 | var coords1, coords2; 1968 | if (this.length1 === 0) { 1969 | coords1 = this.start1 + ',0'; 1970 | } else if (this.length1 == 1) { 1971 | coords1 = this.start1 + 1; 1972 | } else { 1973 | coords1 = (this.start1 + 1) + ',' + this.length1; 1974 | } 1975 | if (this.length2 === 0) { 1976 | coords2 = this.start2 + ',0'; 1977 | } else if (this.length2 == 1) { 1978 | coords2 = this.start2 + 1; 1979 | } else { 1980 | coords2 = (this.start2 + 1) + ',' + this.length2; 1981 | } 1982 | var txt = ['@@ -' + coords1 + ' +' + coords2 + ' @@\n']; 1983 | var op; 1984 | // Escape the body of the patch with %xx notation. 1985 | for (var x = 0; x < this.diffs.length; x++) { 1986 | switch (this.diffs[x][0]) { 1987 | case DIFF_INSERT: 1988 | op = '+'; 1989 | break; 1990 | case DIFF_DELETE: 1991 | op = '-'; 1992 | break; 1993 | case DIFF_EQUAL: 1994 | op = ' '; 1995 | break; 1996 | } 1997 | txt[x + 1] = op + encodeURI(this.diffs[x][1]) + '\n'; 1998 | } 1999 | // Opera doesn't know how to encode char 0. 2000 | return txt.join('').replace(/\0/g, '%00').replace(/%20/g, ' '); 2001 | }; 2002 | 2003 | --------------------------------------------------------------------------------