├── README.asciidoc ├── difflib.js ├── diffview.css └── diffview.js /README.asciidoc: -------------------------------------------------------------------------------- 1 | == jsdifflib - A Javascript visual diff tool & library 2 | 3 | * <> 4 | * <> 5 | * <> 6 | * <> 7 | ** <> 8 | ** <> 9 | * <> 10 | * <> 11 | * <> 12 | * <> 13 | 14 | [[intro]] 15 | == Introduction 16 | 17 | http://cemerick.com[I] needed a good in-browser visual diff tool, and couldn't find anything suitable, so I built *jsdifflib* in Feb 2007 and open-sourced it soon thereafter. It's apparently been used a fair bit since then. Maybe you'll find it useful. 18 | 19 | ===== If you *do* find jsdifflib useful, _please support my open source work via a bitcoin donation/tip_ to 19qCqZxAdRF4eZfyZD2GQnAWk2Mz7DZZVf. Thanks! 20 | 21 | [[overview]] 22 | == Overview 23 | 24 | jsdifflib is a Javascript library that provides: 25 | 26 | . a partial reimplementation of Python's difflib module (specifically, the SequenceMatcher class) 27 | . a visual diff view generator, that offers side-by-side as well as inline formatting of file data 28 | 29 | Yes, I ripped off the formatting of the diff view from the Trac project. It's a near-ideal presentation of diff data as far as I'm concerned. If you don't agree, you can hack the CSS to your heart's content. 30 | 31 | jsdifflib does not require jQuery or any other Javascript library. 32 | 33 | [[python-interop]] 34 | == Python Interoperability 35 | 36 | The main reason why I reimplemented Python's difflib module in Javascript to serve as the algorithmic basis for jsdifflib was that I didn't want to mess with the actual diff algorithm -- I wanted to concentrate on getting the in-browser view right. However, because jsdifflib's API matches Python's difflib's SequenceMatcher class in its entirety, it's trivial to do the actual diffing on the server-side, using Python, and pipe the results of that diff calculation to your in-browser diff view. So, you have the choice of doing everything in Javascript on the browser, or falling back to server-side diff processing if you are diffing really large files. 37 | 38 | Most of the time, we do the latter, simply because while jsdifflib is pretty fast all by itself, and is totally usable for diffing "normal" files (i.e. fewer than 100K lines or so), we regularly need to diff files that are 1 or 2 orders of magnitude larger than that. For that, server-side diffing is a necessity. 39 | 40 | [[demo]] 41 | == Demo & Examples 42 | 43 | You can give jsdifflib a try without downloading anything. Just click the link below, put some content to be diffed in the two textboxes, and diff away. 44 | 45 | http://cemerick.github.com/jsdifflib/demo.html[*Try jsdifflib*] 46 | 47 | That page also contains all of the examples you'll need to use jsdifflib yourself, but let's look at them here, anyway. 48 | 49 | [[diff-js]] 50 | === Diffing using Javascript 51 | 52 | Here's the function from the demo HTML file linked to above that diffs the two pieces of text entered into the textboxes on the page: 53 | 54 | ---- 55 | function diffUsingJS() { 56 | // get the baseText and newText values from the two textboxes, and split them into lines 57 | var base = difflib.stringAsLines($("baseText").value); 58 | var newtxt = difflib.stringAsLines($("newText").value); 59 | 60 | // create a SequenceMatcher instance that diffs the two sets of lines 61 | var sm = new difflib.SequenceMatcher(base, newtxt); 62 | 63 | // get the opcodes from the SequenceMatcher instance 64 | // opcodes is a list of 3-tuples describing what changes should be made to the base text 65 | // in order to yield the new text 66 | var opcodes = sm.get_opcodes(); 67 | var diffoutputdiv = $("diffoutput"); 68 | while (diffoutputdiv.firstChild) diffoutputdiv.removeChild(diffoutputdiv.firstChild); 69 | var contextSize = $("contextSize").value; 70 | contextSize = contextSize ? contextSize : null; 71 | 72 | // build the diff view and add it to the current DOM 73 | diffoutputdiv.appendChild(diffview.buildView({ 74 | baseTextLines: base, 75 | newTextLines: newtxt, 76 | opcodes: opcodes, 77 | // set the display titles for each resource 78 | baseTextName: "Base Text", 79 | newTextName: "New Text", 80 | contextSize: contextSize, 81 | viewType: $("inline").checked ? 1 : 0 82 | })); 83 | 84 | // scroll down to the diff view window. 85 | location = url + "#diff"; 86 | } 87 | ---- 88 | 89 | There's not a whole lot to say about this function. The most notable aspect of it is that the `diffview.buildView()` function takes an object/map with specific attributes, rather than a list of arguments. Those attributes are mostly self-explanatory, but are nonetheless described in detail in code documentation in diffview.js. 90 | 91 | [[diff-python]] 92 | === Diffing using Python 93 | 94 | This isn't enabled in the demo link above, but I've included it to exemplify how one might use the opcode output from a web-based Python backend to drive jsdifflib's diff view. 95 | 96 | ---- 97 | function diffUsingPython() { 98 | dojo.io.bind({ 99 | url: "/diff/postYieldDiffData", 100 | method: "POST", 101 | content: { 102 | baseText: $("baseText").value, 103 | newText: $("newText").value, 104 | ignoreWhitespace: "Y" 105 | }, 106 | load: function (type, data, evt) { 107 | try { 108 | data = eval('(' + data + ')'); 109 | while (diffoutputdiv.firstChild) diffoutputdiv.removeChild(diffoutputdiv.firstChild); 110 | $("output").appendChild(diffview.buildView({ 111 | baseTextLines: data.baseTextLines, 112 | newTextLines: data.newTextLines, 113 | opcodes: data.opcodes, 114 | baseTextName: data.baseTextName, 115 | newTextName: data.newTextName, 116 | contextSize: contextSize 117 | })); 118 | } catch (ex) { 119 | alert("An error occurred updating the diff view:\n" + ex.toString()); 120 | } 121 | }, 122 | error: function (type, evt) { 123 | alert('Error occurred getting diff data. Check the server logs.'); 124 | }, 125 | type: 'text/javascript' 126 | }); 127 | } 128 | ---- 129 | 130 | [WARNING] 131 | ==== 132 | This dojo code was written in 2007, and I haven't _looked_ at dojo for years now. In any case, you should be able to grok what's going on. 133 | ==== 134 | 135 | As you can see, I'm partial to using dojo for ajaxy stuff. All that is happening here is the base and new text is being POSTed to a Python server-side process (we like pylons, but you could just as easily use a simple Python script as a cgi). That process then needs to diff the provided text using an instance of Python's difflib.SequenceMatcher class, and return the opcodes from that SequenceMatcher instance to the browser (in this case, using JSON serialization). In the interest of completeness, here's the controller action from our pylons application that does this (don't try to match up the parameters shown below with the POST parameters shown in the Javascript function above; the latter is only here as an example): 136 | 137 | ---- 138 | @jsonify 139 | def diff (self, baseText, newText, baseTextName="Base Text", newTextName="New Text"): 140 | opcodes = SequenceMatcher(isjunk, baseText, newText).get_opcodes() 141 | return dict(baseTextLines=baseText, newTextLines=newText, opcodes=opcodes, 142 | baseTextName=baseTextName, newTextName=newTextName) 143 | ---- 144 | 145 | [[status]] 146 | == Future Directions 147 | 148 | The top priorities would be to implement the ignoring of empty lines, and the indication of diffs at the character level with sub-highlighting (similar to what Trac's diff view does). 149 | 150 | I'd also like to see the `difflib.SequenceMatcher` reimplementation gain some more speed -- it's virtually a line-by-line translation from the Python implementation, so there's plenty that could be done to make it more performant in Javascript. However, that would mean making the reimplementation diverge even more from the "reference" Python implementation. Given that I don't really want to worry about the algorithm, that's not appealing. I'd much rather use a server-side process when the in-browser diffing is a little too pokey. 151 | 152 | Other than that, I'm open to suggestions. 153 | 154 | [NOTE] 155 | ==== 156 | I'm no longer actively developing jsdifflib. It's been sequestered (mostly out of simple neglect) to my company's servers for too long; now that it's on github, I'm hoping that many of the people that find it useful will submit pull requests to improve the library. I will do what I can to curate that process. 157 | ==== 158 | 159 | [[license]] 160 | == License 161 | 162 | jsdifflib carries a BSD license. As such, it may be used in other products or services with appropriate attribution (including commercial offerings). The license is prepended to each of jsdifflib's files. 163 | 164 | [[downloads]] 165 | == Downloads 166 | 167 | jsdifflib consists of three files -- two Javascript files, and one CSS file. Why two Javascript files? Because I wanted to keep the reimplementation of the python difflib.SequenceMatcher class separate from the actual visual diff view generator. Feel free to combine and/or optimize them in your deployment environment. 168 | 169 | You can download the files separately by navigating the project on github, you can clone the repo, or you can download a zipped distribution via the "Downloads" button at the top of this project page. 170 | 171 | [[history]] 172 | == Release History 173 | 174 | * 1.1.0 (May 18, 2011): Move project to github; no changes in functionality 175 | * 1.0.0 (February 22, 2007): Initial release 176 | -------------------------------------------------------------------------------- /difflib.js: -------------------------------------------------------------------------------- 1 | /*** 2 | This is part of jsdifflib v1.0. 3 | 4 | Copyright (c) 2007, Snowtide Informatics Systems, Inc. 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | * Neither the name of the Snowtide Informatics Systems nor the names of its 16 | contributors may be used to endorse or promote products derived from this 17 | software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 20 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 21 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT 22 | SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 25 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 27 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 28 | DAMAGE. 29 | ***/ 30 | /* Author: Chas Emerick */ 31 | var __whitespace = {" ":true, "\t":true, "\n":true, "\f":true, "\r":true}; 32 | 33 | var difflib = { 34 | defaultJunkFunction: function (c) { 35 | return __whitespace.hasOwnProperty(c); 36 | }, 37 | 38 | stripLinebreaks: function (str) { return str.replace(/^[\n\r]*|[\n\r]*$/g, ""); }, 39 | 40 | stringAsLines: function (str) { 41 | var lfpos = str.indexOf("\n"); 42 | var crpos = str.indexOf("\r"); 43 | var linebreak = ((lfpos > -1 && crpos > -1) || crpos < 0) ? "\n" : "\r"; 44 | 45 | var lines = str.split(linebreak); 46 | for (var i = 0; i < lines.length; i++) { 47 | lines[i] = difflib.stripLinebreaks(lines[i]); 48 | } 49 | 50 | return lines; 51 | }, 52 | 53 | // iteration-based reduce implementation 54 | __reduce: function (func, list, initial) { 55 | if (initial != null) { 56 | var value = initial; 57 | var idx = 0; 58 | } else if (list) { 59 | var value = list[0]; 60 | var idx = 1; 61 | } else { 62 | return null; 63 | } 64 | 65 | for (; idx < list.length; idx++) { 66 | value = func(value, list[idx]); 67 | } 68 | 69 | return value; 70 | }, 71 | 72 | // comparison function for sorting lists of numeric tuples 73 | __ntuplecomp: function (a, b) { 74 | var mlen = Math.max(a.length, b.length); 75 | for (var i = 0; i < mlen; i++) { 76 | if (a[i] < b[i]) return -1; 77 | if (a[i] > b[i]) return 1; 78 | } 79 | 80 | return a.length == b.length ? 0 : (a.length < b.length ? -1 : 1); 81 | }, 82 | 83 | __calculate_ratio: function (matches, length) { 84 | return length ? 2.0 * matches / length : 1.0; 85 | }, 86 | 87 | // returns a function that returns true if a key passed to the returned function 88 | // is in the dict (js object) provided to this function; replaces being able to 89 | // carry around dict.has_key in python... 90 | __isindict: function (dict) { 91 | return function (key) { return dict.hasOwnProperty(key); }; 92 | }, 93 | 94 | // replacement for python's dict.get function -- need easy default values 95 | __dictget: function (dict, key, defaultValue) { 96 | return dict.hasOwnProperty(key) ? dict[key] : defaultValue; 97 | }, 98 | 99 | SequenceMatcher: function (a, b, isjunk) { 100 | this.set_seqs = function (a, b) { 101 | this.set_seq1(a); 102 | this.set_seq2(b); 103 | } 104 | 105 | this.set_seq1 = function (a) { 106 | if (a == this.a) return; 107 | this.a = a; 108 | this.matching_blocks = this.opcodes = null; 109 | } 110 | 111 | this.set_seq2 = function (b) { 112 | if (b == this.b) return; 113 | this.b = b; 114 | this.matching_blocks = this.opcodes = this.fullbcount = null; 115 | this.__chain_b(); 116 | } 117 | 118 | this.__chain_b = function () { 119 | var b = this.b; 120 | var n = b.length; 121 | var b2j = this.b2j = {}; 122 | var populardict = {}; 123 | for (var i = 0; i < b.length; i++) { 124 | var elt = b[i]; 125 | if (b2j.hasOwnProperty(elt)) { 126 | var indices = b2j[elt]; 127 | if (n >= 200 && indices.length * 100 > n) { 128 | populardict[elt] = 1; 129 | delete b2j[elt]; 130 | } else { 131 | indices.push(i); 132 | } 133 | } else { 134 | b2j[elt] = [i]; 135 | } 136 | } 137 | 138 | for (var elt in populardict) { 139 | if (populardict.hasOwnProperty(elt)) { 140 | delete b2j[elt]; 141 | } 142 | } 143 | 144 | var isjunk = this.isjunk; 145 | var junkdict = {}; 146 | if (isjunk) { 147 | for (var elt in populardict) { 148 | if (populardict.hasOwnProperty(elt) && isjunk(elt)) { 149 | junkdict[elt] = 1; 150 | delete populardict[elt]; 151 | } 152 | } 153 | for (var elt in b2j) { 154 | if (b2j.hasOwnProperty(elt) && isjunk(elt)) { 155 | junkdict[elt] = 1; 156 | delete b2j[elt]; 157 | } 158 | } 159 | } 160 | 161 | this.isbjunk = difflib.__isindict(junkdict); 162 | this.isbpopular = difflib.__isindict(populardict); 163 | } 164 | 165 | this.find_longest_match = function (alo, ahi, blo, bhi) { 166 | var a = this.a; 167 | var b = this.b; 168 | var b2j = this.b2j; 169 | var isbjunk = this.isbjunk; 170 | var besti = alo; 171 | var bestj = blo; 172 | var bestsize = 0; 173 | var j = null; 174 | var k; 175 | 176 | var j2len = {}; 177 | var nothing = []; 178 | for (var i = alo; i < ahi; i++) { 179 | var newj2len = {}; 180 | var jdict = difflib.__dictget(b2j, a[i], nothing); 181 | for (var jkey in jdict) { 182 | if (jdict.hasOwnProperty(jkey)) { 183 | j = jdict[jkey]; 184 | if (j < blo) continue; 185 | if (j >= bhi) break; 186 | newj2len[j] = k = difflib.__dictget(j2len, j - 1, 0) + 1; 187 | if (k > bestsize) { 188 | besti = i - k + 1; 189 | bestj = j - k + 1; 190 | bestsize = k; 191 | } 192 | } 193 | } 194 | j2len = newj2len; 195 | } 196 | 197 | while (besti > alo && bestj > blo && !isbjunk(b[bestj - 1]) && a[besti - 1] == b[bestj - 1]) { 198 | besti--; 199 | bestj--; 200 | bestsize++; 201 | } 202 | 203 | while (besti + bestsize < ahi && bestj + bestsize < bhi && 204 | !isbjunk(b[bestj + bestsize]) && 205 | a[besti + bestsize] == b[bestj + bestsize]) { 206 | bestsize++; 207 | } 208 | 209 | while (besti > alo && bestj > blo && isbjunk(b[bestj - 1]) && a[besti - 1] == b[bestj - 1]) { 210 | besti--; 211 | bestj--; 212 | bestsize++; 213 | } 214 | 215 | while (besti + bestsize < ahi && bestj + bestsize < bhi && isbjunk(b[bestj + bestsize]) && 216 | a[besti + bestsize] == b[bestj + bestsize]) { 217 | bestsize++; 218 | } 219 | 220 | return [besti, bestj, bestsize]; 221 | } 222 | 223 | this.get_matching_blocks = function () { 224 | if (this.matching_blocks != null) return this.matching_blocks; 225 | var la = this.a.length; 226 | var lb = this.b.length; 227 | 228 | var queue = [[0, la, 0, lb]]; 229 | var matching_blocks = []; 230 | var alo, ahi, blo, bhi, qi, i, j, k, x; 231 | while (queue.length) { 232 | qi = queue.pop(); 233 | alo = qi[0]; 234 | ahi = qi[1]; 235 | blo = qi[2]; 236 | bhi = qi[3]; 237 | x = this.find_longest_match(alo, ahi, blo, bhi); 238 | i = x[0]; 239 | j = x[1]; 240 | k = x[2]; 241 | 242 | if (k) { 243 | matching_blocks.push(x); 244 | if (alo < i && blo < j) 245 | queue.push([alo, i, blo, j]); 246 | if (i+k < ahi && j+k < bhi) 247 | queue.push([i + k, ahi, j + k, bhi]); 248 | } 249 | } 250 | 251 | matching_blocks.sort(difflib.__ntuplecomp); 252 | 253 | var i1 = 0, j1 = 0, k1 = 0, block = 0; 254 | var i2, j2, k2; 255 | var non_adjacent = []; 256 | for (var idx in matching_blocks) { 257 | if (matching_blocks.hasOwnProperty(idx)) { 258 | block = matching_blocks[idx]; 259 | i2 = block[0]; 260 | j2 = block[1]; 261 | k2 = block[2]; 262 | if (i1 + k1 == i2 && j1 + k1 == j2) { 263 | k1 += k2; 264 | } else { 265 | if (k1) non_adjacent.push([i1, j1, k1]); 266 | i1 = i2; 267 | j1 = j2; 268 | k1 = k2; 269 | } 270 | } 271 | } 272 | 273 | if (k1) non_adjacent.push([i1, j1, k1]); 274 | 275 | non_adjacent.push([la, lb, 0]); 276 | this.matching_blocks = non_adjacent; 277 | return this.matching_blocks; 278 | } 279 | 280 | this.get_opcodes = function () { 281 | if (this.opcodes != null) return this.opcodes; 282 | var i = 0; 283 | var j = 0; 284 | var answer = []; 285 | this.opcodes = answer; 286 | var block, ai, bj, size, tag; 287 | var blocks = this.get_matching_blocks(); 288 | for (var idx in blocks) { 289 | if (blocks.hasOwnProperty(idx)) { 290 | block = blocks[idx]; 291 | ai = block[0]; 292 | bj = block[1]; 293 | size = block[2]; 294 | tag = ''; 295 | if (i < ai && j < bj) { 296 | tag = 'replace'; 297 | } else if (i < ai) { 298 | tag = 'delete'; 299 | } else if (j < bj) { 300 | tag = 'insert'; 301 | } 302 | if (tag) answer.push([tag, i, ai, j, bj]); 303 | i = ai + size; 304 | j = bj + size; 305 | 306 | if (size) answer.push(['equal', ai, i, bj, j]); 307 | } 308 | } 309 | 310 | return answer; 311 | } 312 | 313 | // this is a generator function in the python lib, which of course is not supported in javascript 314 | // the reimplementation builds up the grouped opcodes into a list in their entirety and returns that. 315 | this.get_grouped_opcodes = function (n) { 316 | if (!n) n = 3; 317 | var codes = this.get_opcodes(); 318 | if (!codes) codes = [["equal", 0, 1, 0, 1]]; 319 | var code, tag, i1, i2, j1, j2; 320 | if (codes[0][0] == 'equal') { 321 | code = codes[0]; 322 | tag = code[0]; 323 | i1 = code[1]; 324 | i2 = code[2]; 325 | j1 = code[3]; 326 | j2 = code[4]; 327 | codes[0] = [tag, Math.max(i1, i2 - n), i2, Math.max(j1, j2 - n), j2]; 328 | } 329 | if (codes[codes.length - 1][0] == 'equal') { 330 | code = codes[codes.length - 1]; 331 | tag = code[0]; 332 | i1 = code[1]; 333 | i2 = code[2]; 334 | j1 = code[3]; 335 | j2 = code[4]; 336 | codes[codes.length - 1] = [tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)]; 337 | } 338 | 339 | var nn = n + n; 340 | var group = []; 341 | var groups = []; 342 | for (var idx in codes) { 343 | if (codes.hasOwnProperty(idx)) { 344 | code = codes[idx]; 345 | tag = code[0]; 346 | i1 = code[1]; 347 | i2 = code[2]; 348 | j1 = code[3]; 349 | j2 = code[4]; 350 | if (tag == 'equal' && i2 - i1 > nn) { 351 | group.push([tag, i1, Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)]); 352 | groups.push(group); 353 | group = []; 354 | i1 = Math.max(i1, i2-n); 355 | j1 = Math.max(j1, j2-n); 356 | } 357 | 358 | group.push([tag, i1, i2, j1, j2]); 359 | } 360 | } 361 | 362 | if (group && !(group.length == 1 && group[0][0] == 'equal')) groups.push(group) 363 | 364 | return groups; 365 | } 366 | 367 | this.ratio = function () { 368 | matches = difflib.__reduce( 369 | function (sum, triple) { return sum + triple[triple.length - 1]; }, 370 | this.get_matching_blocks(), 0); 371 | return difflib.__calculate_ratio(matches, this.a.length + this.b.length); 372 | } 373 | 374 | this.quick_ratio = function () { 375 | var fullbcount, elt; 376 | if (this.fullbcount == null) { 377 | this.fullbcount = fullbcount = {}; 378 | for (var i = 0; i < this.b.length; i++) { 379 | elt = this.b[i]; 380 | fullbcount[elt] = difflib.__dictget(fullbcount, elt, 0) + 1; 381 | } 382 | } 383 | fullbcount = this.fullbcount; 384 | 385 | var avail = {}; 386 | var availhas = difflib.__isindict(avail); 387 | var matches = numb = 0; 388 | for (var i = 0; i < this.a.length; i++) { 389 | elt = this.a[i]; 390 | if (availhas(elt)) { 391 | numb = avail[elt]; 392 | } else { 393 | numb = difflib.__dictget(fullbcount, elt, 0); 394 | } 395 | avail[elt] = numb - 1; 396 | if (numb > 0) matches++; 397 | } 398 | 399 | return difflib.__calculate_ratio(matches, this.a.length + this.b.length); 400 | } 401 | 402 | this.real_quick_ratio = function () { 403 | var la = this.a.length; 404 | var lb = this.b.length; 405 | return _calculate_ratio(Math.min(la, lb), la + lb); 406 | } 407 | 408 | this.isjunk = isjunk ? isjunk : difflib.defaultJunkFunction; 409 | this.a = this.b = null; 410 | this.set_seqs(a, b); 411 | } 412 | }; 413 | 414 | -------------------------------------------------------------------------------- /diffview.css: -------------------------------------------------------------------------------- 1 | /* 2 | This is part of jsdifflib v1.0. 3 | 4 | Copyright 2007 - 2011 Chas Emerick . All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are 7 | permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this list of 10 | conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 13 | of conditions and the following disclaimer in the documentation and/or other materials 14 | provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY Chas Emerick ``AS IS'' AND ANY EXPRESS OR IMPLIED 17 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 18 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chas Emerick OR 19 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 22 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 24 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | The views and conclusions contained in the software and documentation are those of the 27 | authors and should not be interpreted as representing official policies, either expressed 28 | or implied, of Chas Emerick. 29 | */ 30 | table.diff { 31 | border-collapse:collapse; 32 | border:1px solid darkgray; 33 | white-space:pre-wrap 34 | } 35 | table.diff tbody { 36 | font-family:Courier, monospace 37 | } 38 | table.diff tbody th { 39 | font-family:verdana,arial,'Bitstream Vera Sans',helvetica,sans-serif; 40 | background:#EED; 41 | font-size:11px; 42 | font-weight:normal; 43 | border:1px solid #BBC; 44 | color:#886; 45 | padding:.3em .5em .1em 2em; 46 | text-align:right; 47 | vertical-align:top 48 | } 49 | table.diff thead { 50 | border-bottom:1px solid #BBC; 51 | background:#EFEFEF; 52 | font-family:Verdana 53 | } 54 | table.diff thead th.texttitle { 55 | text-align:left 56 | } 57 | table.diff tbody td { 58 | padding:0px .4em; 59 | padding-top:.4em; 60 | vertical-align:top; 61 | } 62 | table.diff .empty { 63 | background-color:#DDD; 64 | } 65 | table.diff .replace { 66 | background-color:#FD8 67 | } 68 | table.diff .delete { 69 | background-color:#E99; 70 | } 71 | table.diff .skip { 72 | background-color:#EFEFEF; 73 | border:1px solid #AAA; 74 | border-right:1px solid #BBC; 75 | } 76 | table.diff .insert { 77 | background-color:#9E9 78 | } 79 | table.diff th.author { 80 | text-align:right; 81 | border-top:1px solid #BBC; 82 | background:#EFEFEF 83 | } -------------------------------------------------------------------------------- /diffview.js: -------------------------------------------------------------------------------- 1 | /* 2 | This is part of jsdifflib v1.0. 3 | 4 | Copyright 2007 - 2011 Chas Emerick . All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are 7 | permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this list of 10 | conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 13 | of conditions and the following disclaimer in the documentation and/or other materials 14 | provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY Chas Emerick ``AS IS'' AND ANY EXPRESS OR IMPLIED 17 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 18 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chas Emerick OR 19 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 22 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 24 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | The views and conclusions contained in the software and documentation are those of the 27 | authors and should not be interpreted as representing official policies, either expressed 28 | or implied, of Chas Emerick. 29 | */ 30 | var diffview = { 31 | /** 32 | * Builds and returns a visual diff view. The single parameter, `params', should contain 33 | * the following values: 34 | * 35 | * - baseTextLines: the array of strings that was used as the base text input to SequenceMatcher 36 | * - newTextLines: the array of strings that was used as the new text input to SequenceMatcher 37 | * - opcodes: the array of arrays returned by SequenceMatcher.get_opcodes() 38 | * - baseTextName: the title to be displayed above the base text listing in the diff view; defaults 39 | * to "Base Text" 40 | * - newTextName: the title to be displayed above the new text listing in the diff view; defaults 41 | * to "New Text" 42 | * - contextSize: the number of lines of context to show around differences; by default, all lines 43 | * are shown 44 | * - viewType: if 0, a side-by-side diff view is generated (default); if 1, an inline diff view is 45 | * generated 46 | */ 47 | buildView: function (params) { 48 | var baseTextLines = params.baseTextLines; 49 | var newTextLines = params.newTextLines; 50 | var opcodes = params.opcodes; 51 | var baseTextName = params.baseTextName ? params.baseTextName : "Base Text"; 52 | var newTextName = params.newTextName ? params.newTextName : "New Text"; 53 | var contextSize = params.contextSize; 54 | var inline = (params.viewType == 0 || params.viewType == 1) ? params.viewType : 0; 55 | 56 | if (baseTextLines == null) 57 | throw "Cannot build diff view; baseTextLines is not defined."; 58 | if (newTextLines == null) 59 | throw "Cannot build diff view; newTextLines is not defined."; 60 | if (!opcodes) 61 | throw "Canno build diff view; opcodes is not defined."; 62 | 63 | function celt (name, clazz) { 64 | var e = document.createElement(name); 65 | e.className = clazz; 66 | return e; 67 | } 68 | 69 | function telt (name, text) { 70 | var e = document.createElement(name); 71 | e.appendChild(document.createTextNode(text)); 72 | return e; 73 | } 74 | 75 | function ctelt (name, clazz, text) { 76 | var e = document.createElement(name); 77 | e.className = clazz; 78 | e.appendChild(document.createTextNode(text)); 79 | return e; 80 | } 81 | 82 | var tdata = document.createElement("thead"); 83 | var node = document.createElement("tr"); 84 | tdata.appendChild(node); 85 | if (inline) { 86 | node.appendChild(document.createElement("th")); 87 | node.appendChild(document.createElement("th")); 88 | node.appendChild(ctelt("th", "texttitle", baseTextName + " vs. " + newTextName)); 89 | } else { 90 | node.appendChild(document.createElement("th")); 91 | node.appendChild(ctelt("th", "texttitle", baseTextName)); 92 | node.appendChild(document.createElement("th")); 93 | node.appendChild(ctelt("th", "texttitle", newTextName)); 94 | } 95 | tdata = [tdata]; 96 | 97 | var rows = []; 98 | var node2; 99 | 100 | /** 101 | * Adds two cells to the given row; if the given row corresponds to a real 102 | * line number (based on the line index tidx and the endpoint of the 103 | * range in question tend), then the cells will contain the line number 104 | * and the line of text from textLines at position tidx (with the class of 105 | * the second cell set to the name of the change represented), and tidx + 1 will 106 | * be returned. Otherwise, tidx is returned, and two empty cells are added 107 | * to the given row. 108 | */ 109 | function addCells (row, tidx, tend, textLines, change) { 110 | if (tidx < tend) { 111 | row.appendChild(telt("th", (tidx + 1).toString())); 112 | row.appendChild(ctelt("td", change, textLines[tidx].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0"))); 113 | return tidx + 1; 114 | } else { 115 | row.appendChild(document.createElement("th")); 116 | row.appendChild(celt("td", "empty")); 117 | return tidx; 118 | } 119 | } 120 | 121 | function addCellsInline (row, tidx, tidx2, textLines, change) { 122 | row.appendChild(telt("th", tidx == null ? "" : (tidx + 1).toString())); 123 | row.appendChild(telt("th", tidx2 == null ? "" : (tidx2 + 1).toString())); 124 | row.appendChild(ctelt("td", change, textLines[tidx != null ? tidx : tidx2].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0"))); 125 | } 126 | 127 | for (var idx = 0; idx < opcodes.length; idx++) { 128 | code = opcodes[idx]; 129 | change = code[0]; 130 | var b = code[1]; 131 | var be = code[2]; 132 | var n = code[3]; 133 | var ne = code[4]; 134 | var rowcnt = Math.max(be - b, ne - n); 135 | var toprows = []; 136 | var botrows = []; 137 | for (var i = 0; i < rowcnt; i++) { 138 | // jump ahead if we've alredy provided leading context or if this is the first range 139 | if (contextSize && opcodes.length > 1 && ((idx > 0 && i == contextSize) || (idx == 0 && i == 0)) && change=="equal") { 140 | var jump = rowcnt - ((idx == 0 ? 1 : 2) * contextSize); 141 | if (jump > 1) { 142 | toprows.push(node = document.createElement("tr")); 143 | 144 | b += jump; 145 | n += jump; 146 | i += jump - 1; 147 | node.appendChild(telt("th", "...")); 148 | if (!inline) node.appendChild(ctelt("td", "skip", "")); 149 | node.appendChild(telt("th", "...")); 150 | node.appendChild(ctelt("td", "skip", "")); 151 | 152 | // skip last lines if they're all equal 153 | if (idx + 1 == opcodes.length) { 154 | break; 155 | } else { 156 | continue; 157 | } 158 | } 159 | } 160 | 161 | toprows.push(node = document.createElement("tr")); 162 | if (inline) { 163 | if (change == "insert") { 164 | addCellsInline(node, null, n++, newTextLines, change); 165 | } else if (change == "replace") { 166 | botrows.push(node2 = document.createElement("tr")); 167 | if (b < be) addCellsInline(node, b++, null, baseTextLines, "delete"); 168 | if (n < ne) addCellsInline(node2, null, n++, newTextLines, "insert"); 169 | } else if (change == "delete") { 170 | addCellsInline(node, b++, null, baseTextLines, change); 171 | } else { 172 | // equal 173 | addCellsInline(node, b++, n++, baseTextLines, change); 174 | } 175 | } else { 176 | b = addCells(node, b, be, baseTextLines, change); 177 | n = addCells(node, n, ne, newTextLines, change); 178 | } 179 | } 180 | 181 | for (var i = 0; i < toprows.length; i++) rows.push(toprows[i]); 182 | for (var i = 0; i < botrows.length; i++) rows.push(botrows[i]); 183 | } 184 | 185 | rows.push(node = ctelt("th", "author", "diff view generated by ")); 186 | node.setAttribute("colspan", inline ? 3 : 4); 187 | node.appendChild(node2 = telt("a", "jsdifflib")); 188 | node2.setAttribute("href", "http://github.com/cemerick/jsdifflib"); 189 | 190 | tdata.push(node = document.createElement("tbody")); 191 | for (var idx in rows) rows.hasOwnProperty(idx) && node.appendChild(rows[idx]); 192 | 193 | node = celt("table", "diff" + (inline ? " inlinediff" : "")); 194 | for (var idx in tdata) tdata.hasOwnProperty(idx) && node.appendChild(tdata[idx]); 195 | return node; 196 | } 197 | }; 198 | 199 | --------------------------------------------------------------------------------