Esanders has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/86657


Change subject: Use jsdifflib for QUnit diff
......................................................................

Use jsdifflib for QUnit diff

Because QUnit's inline diff is terrible for large diffs,
especially when there are block whitespaces changes.

Change-Id: I786fb981b02777ede38c4bee261f9e32f8f908ed
---
M VisualEditor.hooks.php
A modules/jsdifflib/README.asciidoc
A modules/jsdifflib/difflib.js
A modules/jsdifflib/diffview.css
A modules/jsdifflib/diffview.js
M modules/ve/test/index.php
M modules/ve/test/ve.qunit.js
7 files changed, 895 insertions(+), 0 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/VisualEditor 
refs/changes/57/86657/1

diff --git a/VisualEditor.hooks.php b/VisualEditor.hooks.php
index 6559799..9f02099 100644
--- a/VisualEditor.hooks.php
+++ b/VisualEditor.hooks.php
@@ -306,9 +306,16 @@
                ResourceLoader &$resourceLoader
        ) {
                $testModules['qunit']['ext.visualEditor.test'] = array(
+                       'styles' => array(
+                               // jsdifflib
+                               'jsdifflib/diffview.css',
+                       ),
                        'scripts' => array(
                                // MW config preload
                                've-mw/test/mw-preload.js',
+                               // jsdifflib
+                               'jsdifflib/diffview.js',
+                               'jsdifflib/difflib.js',
                                // QUnit plugin
                                've/test/ve.qunit.js',
                                // UnicodeJS Tests
diff --git a/modules/jsdifflib/README.asciidoc 
b/modules/jsdifflib/README.asciidoc
new file mode 100644
index 0000000..0f7f7aa
--- /dev/null
+++ b/modules/jsdifflib/README.asciidoc
@@ -0,0 +1,173 @@
+== jsdifflib - A Javascript visual diff tool & library
+
+* <<intro,Introduction>>
+* <<overview,Overview>>
+* <<python-interop,Python Interoperability>>
+* <<demo,Demo & Examples>>
+** <<diff-js,Diffing using Javascript>>
+** <<diff-python,Diffing using Python>>
+* <<status,Future Directions / Status>>
+* <<license,License>>
+* <<downloads,Downloads>>
+* <<history,Release History>>
+
+[[intro]]
+== Introduction
+
+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.
+
+[[overview]]
+== Overview
+
+jsdifflib is a Javascript library that provides:
+
+. a partial reimplementation of Python's difflib module (specifically, the 
SequenceMatcher class)
+. a visual diff view generator, that offers side-by-side as well as inline 
formatting of file data
+
+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.
+
+jsdifflib does not require jQuery or any other Javascript library.
+
+[[python-interop]]
+== Python Interoperability
+
+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.
+
+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.
+
+[[demo]]
+== Demo & Examples
+
+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.
+
+http://cemerick.github.com/jsdifflib/demo.html[*Try jsdifflib*]
+
+That page also contains all of the examples you'll need to use jsdifflib 
yourself, but let's look at them here, anyway.
+
+[[diff-js]]
+=== Diffing using Javascript
+
+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:
+
+----
+function diffUsingJS() {
+    // get the baseText and newText values from the two textboxes, and split 
them into lines 
+    var base = difflib.stringAsLines($("baseText").value);
+    var newtxt = difflib.stringAsLines($("newText").value);
+
+    // create a SequenceMatcher instance that diffs the two sets of lines
+    var sm = new difflib.SequenceMatcher(base, newtxt);
+
+    // get the opcodes from the SequenceMatcher instance 
+    // opcodes is a list of 3-tuples describing what changes should be made to 
the base text 
+    // in order to yield the new text
+    var opcodes = sm.get_opcodes();
+    var diffoutputdiv = $("diffoutput");
+    while (diffoutputdiv.firstChild) 
diffoutputdiv.removeChild(diffoutputdiv.firstChild);
+    var contextSize = $("contextSize").value;
+    contextSize = contextSize ? contextSize : null;
+
+    // build the diff view and add it to the current DOM
+    diffoutputdiv.appendChild(diffview.buildView({
+        baseTextLines: base,
+        newTextLines: newtxt,
+        opcodes: opcodes,
+        // set the display titles for each resource 
+        baseTextName: "Base Text",
+        newTextName: "New Text",
+        contextSize: contextSize,
+        viewType: $("inline").checked ? 1 : 0
+    }));
+
+    // scroll down to the diff view window.
+    location = url + "#diff";
+}
+----
+
+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.
+
+[[diff-python]]
+=== Diffing using Python
+
+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.
+
+----
+function diffUsingPython() {
+    dojo.io.bind({
+        url: "/diff/postYieldDiffData",
+        method: "POST",
+        content: {
+            baseText: $("baseText").value,
+            newText: $("newText").value,
+            ignoreWhitespace: "Y"
+        },
+        load: function (type, data, evt) {
+            try {
+                data = eval('(' + data + ')');
+                while (diffoutputdiv.firstChild) 
diffoutputdiv.removeChild(diffoutputdiv.firstChild);
+                $("output").appendChild(diffview.buildView({
+                    baseTextLines: data.baseTextLines,
+                    newTextLines: data.newTextLines,
+                    opcodes: data.opcodes,
+                    baseTextName: data.baseTextName,
+                    newTextName: data.newTextName,
+                    contextSize: contextSize
+                }));
+            } catch (ex) {
+                alert("An error occurred updating the diff view:\n" + 
ex.toString());
+            }
+        },
+        error: function (type, evt) {
+            alert('Error occurred getting diff data. Check the server logs.');
+        },
+        type: 'text/javascript'
+    });
+}
+----
+
+[WARNING]
+====
+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.
+====
+
+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):
+
+----
+@jsonify
+def diff (self, baseText, newText, baseTextName="Base Text", newTextName="New 
Text"):
+    opcodes = SequenceMatcher(isjunk, baseText, newText).get_opcodes()
+    return dict(baseTextLines=baseText, newTextLines=newText, opcodes=opcodes,
+                baseTextName=baseTextName, newTextName=newTextName)
+----
+
+[[status]]
+== Future Directions
+
+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).
+
+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.
+
+Other than that, I'm open to suggestions.
+
+[NOTE]
+====
+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.
+====
+
+[[license]]
+== License
+
+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.
+
+[[downloads]]
+== Downloads
+
+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.
+
+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.
+
+[[history]]
+== Release History
+
+* 1.1.0 (May 18, 2011): Move project to github; no changes in functionality
+* 1.0.0 (February 22, 2007): Initial release
diff --git a/modules/jsdifflib/difflib.js b/modules/jsdifflib/difflib.js
new file mode 100755
index 0000000..db2c0e3
--- /dev/null
+++ b/modules/jsdifflib/difflib.js
@@ -0,0 +1,407 @@
+/***
+This is part of jsdifflib v1.0. <http://snowtide.com/jsdifflib>
+
+Copyright (c) 2007, Snowtide Informatics Systems, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without 
modification,
+are permitted provided that the following conditions are met:
+
+       * Redistributions of source code must retain the above copyright 
notice, this
+               list of conditions and the following disclaimer.
+       * Redistributions in binary form must reproduce the above copyright 
notice,
+               this list of conditions and the following disclaimer in the 
documentation
+               and/or other materials provided with the distribution.
+       * Neither the name of the Snowtide Informatics Systems nor the names of 
its
+               contributors may be used to endorse or promote products derived 
from this
+               software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 
EVENT
+SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 
LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 
IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 
SUCH
+DAMAGE.
+***/
+/* Author: Chas Emerick <cemer...@snowtide.com> */
+__whitespace = {" ":true, "\t":true, "\n":true, "\f":true, "\r":true};
+
+difflib = {
+       defaultJunkFunction: function (c) {
+               return __whitespace.hasOwnProperty(c);
+       },
+       
+       stripLinebreaks: function (str) { return 
str.replace(/^[\n\r]*|[\n\r]*$/g, ""); },
+       
+       stringAsLines: function (str) {
+               var lfpos = str.indexOf("\n");
+               var crpos = str.indexOf("\r");
+               var linebreak = ((lfpos > -1 && crpos > -1) || crpos < 0) ? 
"\n" : "\r";
+               
+               var lines = str.split(linebreak);
+               for (var i = 0; i < lines.length; i++) {
+                       lines[i] = difflib.stripLinebreaks(lines[i]);
+               }
+               
+               return lines;
+       },
+       
+       // iteration-based reduce implementation
+       __reduce: function (func, list, initial) {
+               if (initial != null) {
+                       var value = initial;
+                       var idx = 0;
+               } else if (list) {
+                       var value = list[0];
+                       var idx = 1;
+               } else {
+                       return null;
+               }
+               
+               for (; idx < list.length; idx++) {
+                       value = func(value, list[idx]);
+               }
+               
+               return value;
+       },
+       
+       // comparison function for sorting lists of numeric tuples
+       __ntuplecomp: function (a, b) {
+               var mlen = Math.max(a.length, b.length);
+               for (var i = 0; i < mlen; i++) {
+                       if (a[i] < b[i]) return -1;
+                       if (a[i] > b[i]) return 1;
+               }
+               
+               return a.length == b.length ? 0 : (a.length < b.length ? -1 : 
1);
+       },
+       
+       __calculate_ratio: function (matches, length) {
+               return length ? 2.0 * matches / length : 1.0;
+       },
+       
+       // returns a function that returns true if a key passed to the returned 
function
+       // is in the dict (js object) provided to this function; replaces being 
able to
+       // carry around dict.has_key in python...
+       __isindict: function (dict) {
+               return function (key) { return dict.hasOwnProperty(key); };
+       },
+       
+       // replacement for python's dict.get function -- need easy default 
values
+       __dictget: function (dict, key, defaultValue) {
+               return dict.hasOwnProperty(key) ? dict[key] : defaultValue;
+       },      
+       
+       SequenceMatcher: function (a, b, isjunk) {
+               this.set_seqs = function (a, b) {
+                       this.set_seq1(a);
+                       this.set_seq2(b);
+               }
+               
+               this.set_seq1 = function (a) {
+                       if (a == this.a) return;
+                       this.a = a;
+                       this.matching_blocks = this.opcodes = null;
+               }
+               
+               this.set_seq2 = function (b) {
+                       if (b == this.b) return;
+                       this.b = b;
+                       this.matching_blocks = this.opcodes = this.fullbcount = 
null;
+                       this.__chain_b();
+               }
+               
+               this.__chain_b = function () {
+                       var b = this.b;
+                       var n = b.length;
+                       var b2j = this.b2j = {};
+                       var populardict = {};
+                       for (var i = 0; i < b.length; i++) {
+                               var elt = b[i];
+                               if (b2j.hasOwnProperty(elt)) {
+                                       var indices = b2j[elt];
+                                       if (n >= 200 && indices.length * 100 > 
n) {
+                                               populardict[elt] = 1;
+                                               delete b2j[elt];
+                                       } else {
+                                               indices.push(i);
+                                       }
+                               } else {
+                                       b2j[elt] = [i];
+                               }
+                       }
+       
+                       for (var elt in populardict) {
+                               if (populardict.hasOwnProperty(elt)) {
+                                       delete b2j[elt];
+                               }
+                       }
+                       
+                       var isjunk = this.isjunk;
+                       var junkdict = {};
+                       if (isjunk) {
+                               for (var elt in populardict) {
+                                       if (populardict.hasOwnProperty(elt) && 
isjunk(elt)) {
+                                               junkdict[elt] = 1;
+                                               delete populardict[elt];
+                                       }
+                               }
+                               for (var elt in b2j) {
+                                       if (b2j.hasOwnProperty(elt) && 
isjunk(elt)) {
+                                               junkdict[elt] = 1;
+                                               delete b2j[elt];
+                                       }
+                               }
+                       }
+       
+                       this.isbjunk = difflib.__isindict(junkdict);
+                       this.isbpopular = difflib.__isindict(populardict);
+               }
+               
+               this.find_longest_match = function (alo, ahi, blo, bhi) {
+                       var a = this.a;
+                       var b = this.b;
+                       var b2j = this.b2j;
+                       var isbjunk = this.isbjunk;
+                       var besti = alo;
+                       var bestj = blo;
+                       var bestsize = 0;
+                       var j = null;
+       
+                       var j2len = {};
+                       var nothing = [];
+                       for (var i = alo; i < ahi; i++) {
+                               var newj2len = {};
+                               var jdict = difflib.__dictget(b2j, a[i], 
nothing);
+                               for (var jkey in jdict) {
+                                       if (jdict.hasOwnProperty(jkey)) {
+                                               j = jdict[jkey];
+                                               if (j < blo) continue;
+                                               if (j >= bhi) break;
+                                               newj2len[j] = k = 
difflib.__dictget(j2len, j - 1, 0) + 1;
+                                               if (k > bestsize) {
+                                                       besti = i - k + 1;
+                                                       bestj = j - k + 1;
+                                                       bestsize = k;
+                                               }
+                                       }
+                               }
+                               j2len = newj2len;
+                       }
+       
+                       while (besti > alo && bestj > blo && !isbjunk(b[bestj - 
1]) && a[besti - 1] == b[bestj - 1]) {
+                               besti--;
+                               bestj--;
+                               bestsize++;
+                       }
+                               
+                       while (besti + bestsize < ahi && bestj + bestsize < bhi 
&&
+                                       !isbjunk(b[bestj + bestsize]) &&
+                                       a[besti + bestsize] == b[bestj + 
bestsize]) {
+                               bestsize++;
+                       }
+       
+                       while (besti > alo && bestj > blo && isbjunk(b[bestj - 
1]) && a[besti - 1] == b[bestj - 1]) {
+                               besti--;
+                               bestj--;
+                               bestsize++;
+                       }
+                       
+                       while (besti + bestsize < ahi && bestj + bestsize < bhi 
&& isbjunk(b[bestj + bestsize]) &&
+                                       a[besti + bestsize] == b[bestj + 
bestsize]) {
+                               bestsize++;
+                       }
+       
+                       return [besti, bestj, bestsize];
+               }
+               
+               this.get_matching_blocks = function () {
+                       if (this.matching_blocks != null) return 
this.matching_blocks;
+                       var la = this.a.length;
+                       var lb = this.b.length;
+       
+                       var queue = [[0, la, 0, lb]];
+                       var matching_blocks = [];
+                       var alo, ahi, blo, bhi, qi, i, j, k, x;
+                       while (queue.length) {
+                               qi = queue.pop();
+                               alo = qi[0];
+                               ahi = qi[1];
+                               blo = qi[2];
+                               bhi = qi[3];
+                               x = this.find_longest_match(alo, ahi, blo, bhi);
+                               i = x[0];
+                               j = x[1];
+                               k = x[2];
+       
+                               if (k) {
+                                       matching_blocks.push(x);
+                                       if (alo < i && blo < j)
+                                               queue.push([alo, i, blo, j]);
+                                       if (i+k < ahi && j+k < bhi)
+                                               queue.push([i + k, ahi, j + k, 
bhi]);
+                               }
+                       }
+                       
+                       matching_blocks.sort(difflib.__ntuplecomp);
+       
+                       var i1 = j1 = k1 = block = 0;
+                       var non_adjacent = [];
+                       for (var idx in matching_blocks) {
+                               if (matching_blocks.hasOwnProperty(idx)) {
+                                       block = matching_blocks[idx];
+                                       i2 = block[0];
+                                       j2 = block[1];
+                                       k2 = block[2];
+                                       if (i1 + k1 == i2 && j1 + k1 == j2) {
+                                               k1 += k2;
+                                       } else {
+                                               if (k1) non_adjacent.push([i1, 
j1, k1]);
+                                               i1 = i2;
+                                               j1 = j2;
+                                               k1 = k2;
+                                       }
+                               }
+                       }
+                       
+                       if (k1) non_adjacent.push([i1, j1, k1]);
+       
+                       non_adjacent.push([la, lb, 0]);
+                       this.matching_blocks = non_adjacent;
+                       return this.matching_blocks;
+               }
+               
+               this.get_opcodes = function () {
+                       if (this.opcodes != null) return this.opcodes;
+                       var i = 0;
+                       var j = 0;
+                       var answer = [];
+                       this.opcodes = answer;
+                       var block, ai, bj, size, tag;
+                       var blocks = this.get_matching_blocks();
+                       for (var idx in blocks) {
+                               if (blocks.hasOwnProperty(idx)) {
+                                       block = blocks[idx];
+                                       ai = block[0];
+                                       bj = block[1];
+                                       size = block[2];
+                                       tag = '';
+                                       if (i < ai && j < bj) {
+                                               tag = 'replace';
+                                       } else if (i < ai) {
+                                               tag = 'delete';
+                                       } else if (j < bj) {
+                                               tag = 'insert';
+                                       }
+                                       if (tag) answer.push([tag, i, ai, j, 
bj]);
+                                       i = ai + size;
+                                       j = bj + size;
+                                       
+                                       if (size) answer.push(['equal', ai, i, 
bj, j]);
+                               }
+                       }
+                       
+                       return answer;
+               }
+               
+               // this is a generator function in the python lib, which of 
course is not supported in javascript
+               // the reimplementation builds up the grouped opcodes into a 
list in their entirety and returns that.
+               this.get_grouped_opcodes = function (n) {
+                       if (!n) n = 3;
+                       var codes = this.get_opcodes();
+                       if (!codes) codes = [["equal", 0, 1, 0, 1]];
+                       var code, tag, i1, i2, j1, j2;
+                       if (codes[0][0] == 'equal') {
+                               code = codes[0];
+                               tag = code[0];
+                               i1 = code[1];
+                               i2 = code[2];
+                               j1 = code[3];
+                               j2 = code[4];
+                               codes[0] = [tag, Math.max(i1, i2 - n), i2, 
Math.max(j1, j2 - n), j2];
+                       }
+                       if (codes[codes.length - 1][0] == 'equal') {
+                               code = codes[codes.length - 1];
+                               tag = code[0];
+                               i1 = code[1];
+                               i2 = code[2];
+                               j1 = code[3];
+                               j2 = code[4];
+                               codes[codes.length - 1] = [tag, i1, 
Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)];
+                       }
+       
+                       var nn = n + n;
+                       var groups = [];
+                       for (var idx in codes) {
+                               if (codes.hasOwnProperty(idx)) {
+                                       code = codes[idx];
+                                       tag = code[0];
+                                       i1 = code[1];
+                                       i2 = code[2];
+                                       j1 = code[3];
+                                       j2 = code[4];
+                                       if (tag == 'equal' && i2 - i1 > nn) {
+                                               groups.push([tag, i1, 
Math.min(i2, i1 + n), j1, Math.min(j2, j1 + n)]);
+                                               i1 = Math.max(i1, i2-n);
+                                               j1 = Math.max(j1, j2-n);
+                                       }
+                                       
+                                       groups.push([tag, i1, i2, j1, j2]);
+                               }
+                       }
+                       
+                       if (groups && groups[groups.length - 1][0] == 'equal') 
groups.pop();
+                       
+                       return groups;
+               }
+               
+               this.ratio = function () {
+                       matches = difflib.__reduce(
+                                                       function (sum, triple) 
{ return sum + triple[triple.length - 1]; },
+                                                       
this.get_matching_blocks(), 0);
+                       return difflib.__calculate_ratio(matches, this.a.length 
+ this.b.length);
+               }
+               
+               this.quick_ratio = function () {
+                       var fullbcount, elt;
+                       if (this.fullbcount == null) {
+                               this.fullbcount = fullbcount = {};
+                               for (var i = 0; i < this.b.length; i++) {
+                                       elt = this.b[i];
+                                       fullbcount[elt] = 
difflib.__dictget(fullbcount, elt, 0) + 1;
+                               }
+                       }
+                       fullbcount = this.fullbcount;
+       
+                       var avail = {};
+                       var availhas = difflib.__isindict(avail);
+                       var matches = numb = 0;
+                       for (var i = 0; i < this.a.length; i++) {
+                               elt = this.a[i];
+                               if (availhas(elt)) {
+                                       numb = avail[elt];
+                               } else {
+                                       numb = difflib.__dictget(fullbcount, 
elt, 0);
+                               }
+                               avail[elt] = numb - 1;
+                               if (numb > 0) matches++;
+                       }
+                       
+                       return difflib.__calculate_ratio(matches, this.a.length 
+ this.b.length);
+               }
+               
+               this.real_quick_ratio = function () {
+                       var la = this.a.length;
+                       var lb = this.b.length;
+                       return _calculate_ratio(Math.min(la, lb), la + lb);
+               }
+               
+               this.isjunk = isjunk ? isjunk : difflib.defaultJunkFunction;
+               this.a = this.b = null;
+               this.set_seqs(a, b);
+       }
+}
diff --git a/modules/jsdifflib/diffview.css b/modules/jsdifflib/diffview.css
new file mode 100644
index 0000000..811a593
--- /dev/null
+++ b/modules/jsdifflib/diffview.css
@@ -0,0 +1,83 @@
+/*
+This is part of jsdifflib v1.0. <http://github.com/cemerick/jsdifflib>
+
+Copyright 2007 - 2011 Chas Emerick <cemer...@snowtide.com>. All rights 
reserved.
+
+Redistribution and use in source and binary forms, with or without 
modification, are
+permitted provided that the following conditions are met:
+
+   1. Redistributions of source code must retain the above copyright notice, 
this list of
+      conditions and the following disclaimer.
+
+   2. Redistributions in binary form must reproduce the above copyright 
notice, this list
+      of conditions and the following disclaimer in the documentation and/or 
other materials
+      provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY Chas Emerick ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chas 
Emerick OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
(INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are 
those of the
+authors and should not be interpreted as representing official policies, 
either expressed
+or implied, of Chas Emerick.
+*/
+table.diff {
+       border-collapse:collapse;
+       border:1px solid darkgray;
+       white-space:pre-wrap
+}
+table.diff tbody { 
+       font-family:Courier, monospace
+}
+table.diff tbody th {
+       font-family:verdana,arial,'Bitstream Vera Sans',helvetica,sans-serif;
+       background:#EED;
+       font-size:11px;
+       font-weight:normal;
+       border:1px solid #BBC;
+       color:#886;
+       padding:.3em .5em .1em 2em;
+       text-align:right;
+       vertical-align:top
+}
+table.diff thead {
+       border-bottom:1px solid #BBC;
+       background:#EFEFEF;
+       font-family:Verdana
+}
+table.diff thead th.texttitle {
+       text-align:left
+}
+table.diff tbody td {
+       padding:0px .4em;
+       padding-top:.4em;
+       vertical-align:top;
+}
+table.diff .empty {
+       background-color:#DDD;
+}
+table.diff .replace {
+       background-color:#FD8
+}
+table.diff .delete {
+       background-color:#E99;
+}
+table.diff .skip {
+       background-color:#EFEFEF;
+       border:1px solid #AAA;
+       border-right:1px solid #BBC;
+}
+table.diff .insert {
+       background-color:#9E9
+}
+table.diff th.author {
+       text-align:right;
+       border-top:1px solid #BBC;
+       background:#EFEFEF
+}
\ No newline at end of file
diff --git a/modules/jsdifflib/diffview.js b/modules/jsdifflib/diffview.js
new file mode 100644
index 0000000..a228a34
--- /dev/null
+++ b/modules/jsdifflib/diffview.js
@@ -0,0 +1,197 @@
+/*
+This is part of jsdifflib v1.0. <http://github.com/cemerick/jsdifflib>
+
+Copyright 2007 - 2011 Chas Emerick <cemer...@snowtide.com>. All rights 
reserved.
+
+Redistribution and use in source and binary forms, with or without 
modification, are
+permitted provided that the following conditions are met:
+
+   1. Redistributions of source code must retain the above copyright notice, 
this list of
+      conditions and the following disclaimer.
+
+   2. Redistributions in binary form must reproduce the above copyright 
notice, this list
+      of conditions and the following disclaimer in the documentation and/or 
other materials
+      provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY Chas Emerick ``AS IS'' AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Chas 
Emerick OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
(INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
EVEN IF
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are 
those of the
+authors and should not be interpreted as representing official policies, 
either expressed
+or implied, of Chas Emerick.
+*/
+diffview = {
+       /**
+        * Builds and returns a visual diff view.  The single parameter, 
`params', should contain
+        * the following values:
+        *
+        * - baseTextLines: the array of strings that was used as the base text 
input to SequenceMatcher
+        * - newTextLines: the array of strings that was used as the new text 
input to SequenceMatcher
+        * - opcodes: the array of arrays returned by 
SequenceMatcher.get_opcodes()
+        * - baseTextName: the title to be displayed above the base text 
listing in the diff view; defaults
+        *         to "Base Text"
+        * - newTextName: the title to be displayed above the new text listing 
in the diff view; defaults
+        *         to "New Text"
+        * - contextSize: the number of lines of context to show around 
differences; by default, all lines
+        *         are shown
+        * - viewType: if 0, a side-by-side diff view is generated (default); 
if 1, an inline diff view is
+        *         generated
+        */
+       buildView: function (params) {
+               var baseTextLines = params.baseTextLines;
+               var newTextLines = params.newTextLines;
+               var opcodes = params.opcodes;
+               var baseTextName = params.baseTextName ? params.baseTextName : 
"Base Text";
+               var newTextName = params.newTextName ? params.newTextName : 
"New Text";
+               var contextSize = params.contextSize;
+               var inline = (params.viewType == 0 || params.viewType == 1) ? 
params.viewType : 0;
+
+               if (baseTextLines == null)
+                       throw "Cannot build diff view; baseTextLines is not 
defined.";
+               if (newTextLines == null)
+                       throw "Cannot build diff view; newTextLines is not 
defined.";
+               if (!opcodes)
+                       throw "Canno build diff view; opcodes is not defined.";
+               
+               function celt (name, clazz) {
+                       var e = document.createElement(name);
+                       e.className = clazz;
+                       return e;
+               }
+               
+               function telt (name, text) {
+                       var e = document.createElement(name);
+                       e.appendChild(document.createTextNode(text));
+                       return e;
+               }
+               
+               function ctelt (name, clazz, text) {
+                       var e = document.createElement(name);
+                       e.className = clazz;
+                       e.appendChild(document.createTextNode(text));
+                       return e;
+               }
+       
+               var tdata = document.createElement("thead");
+               var node = document.createElement("tr");
+               tdata.appendChild(node);
+               if (inline) {
+                       node.appendChild(document.createElement("th"));
+                       node.appendChild(document.createElement("th"));
+                       node.appendChild(ctelt("th", "texttitle", baseTextName 
+ " vs. " + newTextName));
+               } else {
+                       node.appendChild(document.createElement("th"));
+                       node.appendChild(ctelt("th", "texttitle", 
baseTextName));
+                       node.appendChild(document.createElement("th"));
+                       node.appendChild(ctelt("th", "texttitle", newTextName));
+               }
+               tdata = [tdata];
+               
+               var rows = [];
+               var node2;
+               
+               /**
+                * Adds two cells to the given row; if the given row 
corresponds to a real
+                * line number (based on the line index tidx and the endpoint 
of the 
+                * range in question tend), then the cells will contain the 
line number
+                * and the line of text from textLines at position tidx (with 
the class of
+                * the second cell set to the name of the change represented), 
and tidx + 1 will
+                * be returned.  Otherwise, tidx is returned, and two empty 
cells are added
+                * to the given row.
+                */
+               function addCells (row, tidx, tend, textLines, change) {
+                       if (tidx < tend) {
+                               row.appendChild(telt("th", (tidx + 
1).toString()));
+                               row.appendChild(ctelt("td", change, 
textLines[tidx].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0")));
+                               return tidx + 1;
+                       } else {
+                               row.appendChild(document.createElement("th"));
+                               row.appendChild(celt("td", "empty"));
+                               return tidx;
+                       }
+               }
+               
+               function addCellsInline (row, tidx, tidx2, textLines, change) {
+                       row.appendChild(telt("th", tidx == null ? "" : (tidx + 
1).toString()));
+                       row.appendChild(telt("th", tidx2 == null ? "" : (tidx2 
+ 1).toString()));
+                       row.appendChild(ctelt("td", change, textLines[tidx != 
null ? tidx : tidx2].replace(/\t/g, "\u00a0\u00a0\u00a0\u00a0")));
+               }
+               
+               for (var idx = 0; idx < opcodes.length; idx++) {
+                       code = opcodes[idx];
+                       change = code[0];
+                       var b = code[1];
+                       var be = code[2];
+                       var n = code[3];
+                       var ne = code[4];
+                       var rowcnt = Math.max(be - b, ne - n);
+                       var toprows = [];
+                       var botrows = [];
+                       for (var i = 0; i < rowcnt; i++) {
+                               // jump ahead if we've alredy provided leading 
context or if this is the first range
+                               if (contextSize && opcodes.length > 1 && ((idx 
> 0 && i == contextSize) || (idx == 0 && i == 0)) && change=="equal") {
+                                       var jump = rowcnt - ((idx == 0 ? 1 : 2) 
* contextSize);
+                                       if (jump > 1) {
+                                               toprows.push(node = 
document.createElement("tr"));
+                                               
+                                               b += jump;
+                                               n += jump;
+                                               i += jump - 1;
+                                               node.appendChild(telt("th", 
"..."));
+                                               if (!inline) 
node.appendChild(ctelt("td", "skip", ""));
+                                               node.appendChild(telt("th", 
"..."));
+                                               node.appendChild(ctelt("td", 
"skip", ""));
+                                               
+                                               // skip last lines if they're 
all equal
+                                               if (idx + 1 == opcodes.length) {
+                                                       break;
+                                               } else {
+                                                       continue;
+                                               }
+                                       }
+                               }
+                               
+                               toprows.push(node = 
document.createElement("tr"));
+                               if (inline) {
+                                       if (change == "insert") {
+                                               addCellsInline(node, null, n++, 
newTextLines, change);
+                                       } else if (change == "replace") {
+                                               botrows.push(node2 = 
document.createElement("tr"));
+                                               if (b < be) 
addCellsInline(node, b++, null, baseTextLines, "delete");
+                                               if (n < ne) 
addCellsInline(node2, null, n++, newTextLines, "insert");
+                                       } else if (change == "delete") {
+                                               addCellsInline(node, b++, null, 
baseTextLines, change);
+                                       } else {
+                                               // equal
+                                               addCellsInline(node, b++, n++, 
baseTextLines, change);
+                                       }
+                               } else {
+                                       b = addCells(node, b, be, 
baseTextLines, change);
+                                       n = addCells(node, n, ne, newTextLines, 
change);
+                               }
+                       }
+
+                       for (var i = 0; i < toprows.length; i++) 
rows.push(toprows[i]);
+                       for (var i = 0; i < botrows.length; i++) 
rows.push(botrows[i]);
+               }
+               
+               rows.push(node = ctelt("th", "author", "diff view generated by 
"));
+               node.setAttribute("colspan", inline ? 3 : 4);
+               node.appendChild(node2 = telt("a", "jsdifflib"));
+               node2.setAttribute("href", 
"http://github.com/cemerick/jsdifflib";);
+               
+               tdata.push(node = document.createElement("tbody"));
+               for (var idx in rows) node.appendChild(rows[idx]);
+               
+               node = celt("table", "diff" + (inline ? " inlinediff" : ""));
+               for (var idx in tdata) node.appendChild(tdata[idx]);
+               return node;
+       }
+}
\ No newline at end of file
diff --git a/modules/ve/test/index.php b/modules/ve/test/index.php
index d188d32..71148d9 100644
--- a/modules/ve/test/index.php
+++ b/modules/ve/test/index.php
@@ -5,6 +5,9 @@
                <title>VisualEditor Tests</title>
 
                <!-- Load test framework -->
+               <script src="../../jsdifflib/diffview.js"></script>
+               <link rel="stylesheet" href="../../jsdifflib/diffview.css">
+               <script src="../../jsdifflib/difflib.js"></script>
                <link rel="stylesheet" href="../../qunit/qunit.css">
                <script src="../../qunit/qunit.js"></script>
 
diff --git a/modules/ve/test/ve.qunit.js b/modules/ve/test/ve.qunit.js
index de9efb1..d440813 100644
--- a/modules/ve/test/ve.qunit.js
+++ b/modules/ve/test/ve.qunit.js
@@ -4,6 +4,9 @@
  * @copyright 2011-2013 VisualEditor Team and others; see AUTHORS.txt
  * @license The MIT License (MIT); see LICENSE.txt
  */
+
+/*global difflib,diffview */
+
 ( function ( QUnit ) {
 
 QUnit.config.requireExpects = true;
@@ -197,4 +200,26 @@
        QUnit.push( QUnit.equiv(actual, expected), actual, expected, message );
 };
 
+QUnit.diff = function ( o, n ) {
+       // o and n are partially HTML escaped by QUnit. As difflib does
+       // its own escaping we should unescape them first.
+       var oLines = difflib.stringAsLines( $( '<div>' ).html( o ).text() ),
+               nLines = difflib.stringAsLines( $( '<div>' ).html( n ).text() ),
+               sm = new difflib.SequenceMatcher( oLines, nLines ),
+               /*jshint camelcase:false */
+               opcodes = sm.get_opcodes(),
+               $div = $( '<div>' );
+
+       $div.append( diffview.buildView( {
+               baseTextLines: oLines,
+               newTextLines: nLines,
+               opcodes: opcodes,
+               baseTextName: 'Expected',
+               newTextName: 'Result',
+               contextSize: 10,
+               viewType: 0
+       } ) );
+       return $div.html();
+};
+
 }( QUnit ) );

-- 
To view, visit https://gerrit.wikimedia.org/r/86657
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I786fb981b02777ede38c4bee261f9e32f8f908ed
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Esanders <esand...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to