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