User:Stevage/EnhanceHistory.user.js

Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// ==UserScript==

// @name           Enhanced history display

// @namespace      stevage

// @description    Collapses consecutive edits from the same person into one, shows diffs on history page

// @include        *.wikipedia.org/*action=history

// ==/UserScript==
// This page should be found at http://en.wikipedia.org/wiki/User:Stevage/EnhanceHistory.user.js

// Install it from http://en.wikipedia.org/w/index.php?action=raw&ctype=text/javascript&dontcountme=s&title=User:Stevage/EnhanceHistory.user.js

(
function() {
  if(typeof GM_log === 'undefined') return;
  GM_log('in blank function');
  function compress() {
    GM_log('in compress function');

    if (!document.getElementById('bodyContent')) {
        return;
    }
    
    this.add_buttons();
    
  }

  compress.prototype.add_buttons = function() {
    GM_log('in add_buttons');

    // Create the compress buttion
    var button1 = document.createElement('input');
    button1.setAttribute('id', 'compress_button1');
    button1.className = 'historysubmit';
    button1.style.marginLeft = '5px';
    button1.setAttribute('type', 'button');
    button1.value = 'Compress history';
    button1.onclick = function() { compress.start(); }

    // Create the ShowDiffs buttion
    var button1 = document.createElement('input');
    button1.setAttribute('id', 'showdiffs1');
    button1.className = 'historysubmit';
    button1.style.marginLeft = '5px';
    button1.setAttribute('type', 'button');
    button1.value = 'Show diffs';
    button1.onclick = function() { compress.showDiffs(); }

    // Add the button to the page
    var history = document.getElementById('pagehistory');
    history.parentNode.insertBefore(button1, history);
  }

/////////////////////////////////////////////////////////

  function getPlainText(s) {
    GM_log(">getPlainText");
    
    if (s==null)
      return "";
    var len = s.length;
    if (len > 20) {
      return "<small>" + s.substr(0,10)+'...'+ s.substr(len-10,10)+ "</small>";
    } else  {
      return "<small>" + s + "</small>";
    }
    GM_log("<getPlainText");
  }
  
  
  function diffString(text1, text2) {
  var d = diff(text1, text2);
  var html = '';
  for (var x=0; x<d.length; x++) {
    var m = d[x][0]; // Mode (-1=delete, 0=copy, 1=add)
    var i = d[x][1]; // Index of change.
    var t = d[x][2]; // Text of change.
    t = t.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    if (m == -1)
      html += "<DEL STYLE='background:#FFE6E6;' TITLE='i="+i+"'>"+t+"</DEL>";
    else if (m == 1)
      html += "<INS STYLE='background:#E6FFE6;' TITLE='i="+i+"'>"+t+"</INS>";
    else
      html += "<SPAN TITLE='i="+i+"'>" +getPlainText(t) + "</SPAN>";
  }
  return html;
}

// Find the differences between two texts.  Return an array of changes.
function diff(text1, text2) {
  // Check for equality (speedup)
  if (text1 == text2)
    return [[0, 0, text1]];

  var a;
  // Trim off common prefix (speedup)
  a = diff_prefix(text1, text2);
  text1 = a[0];
  text2 = a[1];
  var commonprefix = a[2];
  
  // Trim off common suffix (speedup)
  a = diff_suffix(text1, text2);
  text1 = a[0];
  text2 = a[1];
  var commonsuffix = a[2];

  if (!text1) {  // Just add some text (speedup)
    a = [[1, commonprefix.length, text2]];
  } else if (!text2) { // Just delete some text (speedup)
    a = [[-1, commonprefix.length, text1]];
  } else {

    // Check to see if the problem can be split in two.
    var longtext = text1.length > text2.length ? text1 : text2;
    var shorttext = text1.length > text2.length ? text2 : text1;
    var hm = diff_halfmatch(longtext, shorttext, Math.ceil(longtext.length/4));
    if (!hm)
      hm = diff_halfmatch(longtext, shorttext, Math.ceil(longtext.length/2));
    if (hm) {
      if (text1.length > text2.length) {
        var text1_a = hm[0];
        var text1_b = hm[1];
        var text2_a = hm[2];
        var text2_b = hm[3];
      } else {
        var text2_a = hm[0];
        var text2_b = hm[1];
        var text1_a = hm[2];
        var text1_b = hm[3];
      }
      var mid_common = hm[4];
      var result_a = diff(text1_a, text2_a);
      var result_b = diff(text1_b, text2_b);
      if (commonprefix) // Shift the indicies forwards due to the commonprefix.
        for (var x=0; x<result_a.length; x++)
          result_a[x][1] += commonprefix.length;
      result_a.push([0, commonprefix.length+text2_a.length, mid_common]);
      while (result_b.length) {
        result_b[0][1] += commonprefix.length+text2_a.length+mid_common.length;
        result_a.push(result_b.shift());
      }
      a = result_a;
    } else {
      var result = diff_map(text1, text2);
      if (result)
        a = diffchar2diffarray(result, commonprefix.length);
      else // No acceptable result.
        a = [[-1, commonprefix.length, text1], [1, commonprefix.length, text2]];
    }
  }

  if (commonprefix)
    a.unshift([0, 0, commonprefix]);
  if (commonsuffix)
    a.push([0, commonprefix.length + text2.length, commonsuffix]);  
  return a;
}

function diff_map(text1, text2) {
  // Explore the intersection points between the two texts.
  var now = new Date();
  var ms_end = now.getTime() + 1000; // Don't run for more than one second.
  var max = text1.length + text2.length;
  var v_map = new Array();
  var v = new Array();
  v[1] = 0;
  var x, y;
  for (var d=0; d<=max; d++) {
    now = new Date();
    if (now.getTime() > ms_end) // JavaScript timeout reached
      return null;
    v_map[d] = new Object;
    for (var k=-d; k<=d; k+=2) {
      if (k == -d || k != d && v[k-1] < v[k+1])
        x = v[k+1];
      else
        x = v[k-1]+1;
      y = x - k;
      while (x < text1.length && y < text2.length && text1.charAt(x) == text2.charAt(y)) {
        x++; y++;
      }
      v[k] = x;
      v_map[d][k] = x;
      if (x >= text1.length && y >= text2.length) {
        var str = diff_path(v_map, text1, text2);
        return str;
      }
    }
  }
  alert("No result.  Can't happen. (diff_map)");
  return null;
}

function diff_path(v_map, text1, text2) {
  // Work from the end back to the start to determine the path.
  var path = '';
  var x = text1.length;
  var y = text2.length;
  for (var d=v_map.length-2; d>=0; d--) {
    while(1) {
      if (diff_match(v_map[d], x-1, y)) {
        x--;
        path = "-"+text1.substring(x, x+1) + path;
        break;
      } else if (diff_match(v_map[d], x, y-1)) {
        y--;
        path = "+"+text2.substring(y, y+1) + path;
        break;
      } else {
        x--;
        y--;
        //if (text1.substring(x, x+1) != text2.substring(y, y+1))
        //  return alert("No diagonal.  Can't happen. (diff_path)");
        path = "="+text1.substring(x, x+1) + path;
      }
    }
  }
  return path;
}

function diff_match(v, x, y) {
  // Does the vector list contain an x/y coordinate?
  for (var k in v)
    if (v[k] == x && x-k == y)
      return true;
  return false;
}

function diff_prefix(text1, text2) {
  // Trim off common prefix
  var pointermin = 0;
  var pointermax = Math.min(text1.length, text2.length);
  var pointermid = pointermax;
  while(pointermin < pointermid) {
    if (text1.substring(0, pointermid) == text2.substring(0, pointermid))
      pointermin = pointermid;
    else
      pointermax = pointermid;
    pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
  }
  var commonprefix = text1.substring(0, pointermid);
  text1 = text1.substring(pointermid);
  text2 = text2.substring(pointermid);
  return [text1, text2, commonprefix];
}

function diff_suffix(text1, text2) {
  // Trim off common suffix
  var pointermin = 0;
  var pointermax = Math.min(text1.length, text2.length);
  var pointermid = pointermax;
  while(pointermin < pointermid) {
    if (text1.substring(text1.length-pointermid) == text2.substring(text2.length-pointermid))
      pointermin = pointermid;
    else
      pointermax = pointermid;
    pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
  }
  var commonsuffix = text1.substring(text1.length-pointermid);
  text1 = text1.substring(0, text1.length-pointermid);
  text2 = text2.substring(0, text2.length-pointermid);
  return [text1, text2, commonsuffix];
}

function diff_halfmatch(longtext, shorttext, i) {
  // Do the two texts share a substring which is at least half the length of the longer text?
  // Start with a 1/4 length substring at position i as a seed.
  if (longtext.length < 10 || shorttext.length < 1)
    return null; // Pointless.
  var seed = longtext.substring(i, i+Math.floor(longtext.length/4));
  var j=0;
  var j_index;
  var best_common = '';
  while ((j_index = shorttext.substring(j).indexOf(seed)) != -1) {
    j += j_index;
    var my_prefix = diff_prefix(longtext.substring(i), shorttext.substring(j));
    var my_suffix = diff_suffix(longtext.substring(0, i), shorttext.substring(0, j));
    if (best_common.length < (my_suffix[2] + my_prefix[2]).length) {
      best_common = my_suffix[2] + my_prefix[2];
      best_longtext_a = my_suffix[0];
      best_longtext_b = my_prefix[0];
      best_shorttext_a = my_suffix[1];
      best_shorttext_b = my_prefix[1];
    }
    j++;
  }
  if (best_common.length >= longtext.length/2)
    return [best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b, best_common];
  else
    return null;
}

function diffchar2diffarray(text, offset) {
  // Convert '-h+c=a=t' into [[-1, 0, 'h'], [1, 0, 'c'], [0, 1, 'at']]
  // Old format: - remove char, = keep char, + add char
  // New format: array of [m, i, t]
  // Where m: -1 remove char, 0 keep char, 1 add char
  // Where i: index of change in first text
  // Where t: text to be added/kept/removed
  var i = 0;
  if (offset) i += offset;
  var a = new Array();
  var m;
  var last_m = null;
  for (var x=0; x<text.length; x+=2) {
    m = "-=+".indexOf(text.substring(x, x+1)) - 1;
    if (m == -2) return alert("Error: '"+text.substring(x, x+1)+"' is not one of '+=-'");
    if (last_m === m) {
      a[a.length-1][2] += text.substring(x+1, x+2);
    } else {
      a[a.length] = new Array(m, i, text.substring(x+1, x+2));
    }
    last_m = m;
    if (m != -1) i++;
  }
  return a;
}

/*
  // JavaScript diff code thanks to John Resig (http://ejohn.org)
  // http://ejohn.org/files/jsdiff.js
  function diffString( o, n ) {
    GM_log(">diffstring " + o.length + "/" + n.length);
	  var out = diff( o.split(/\s+/), n.split(/\s+/) );
    GM_log("1diffstring");
	  var str = "";
    GM_log("2diffstring");
	  var plaintext = "";
    GM_log("3diffstring");
	  for ( var i = 0; i < out.n.length - 1; i++ ) {
		  if ( out.n[i].text == null ) {
			  if ( out.n[i].indexOf('"') == -1 && out.n[i].indexOf('<') == -1 && out.n[i].indexOf('=') == -1 ) {
  		    str += getPlainText(plaintext) + " " + "<b style='background:#E6FFE6;' class='diff'> " + out.n[i] +"</b>";
			    plaintext = "";
			  } else
				  plaintext += " " + out.n[i];
		  } else {
			  var pre = "";
			  if ( out.n[i].text.indexOf('"') == -1 && out.n[i].text.indexOf('<') == -1 && out.n[i].text.indexOf('=') == -1 ) {
  				
				  var n = out.n[i].row + 1;
				  while ( n < out.o.length && out.o[n].text == null ) {
					  if ( out.o[n].indexOf('"') == -1 && out.o[n].indexOf('<') == -1 && out.o[n].indexOf(':') == -1 && out.o[n].indexOf(';') == -1 && out.o[n].indexOf('=') == -1 )
						  pre += " <s style='background:#FFE6E6;' class='diff'>" + out.o[n] +" </s>";
					  n++;
				  }
			  }
			  plaintext = plaintext + " " + out.n[i].text;
			  if (pre!="") {
				  str += getPlainText(plaintext) + " " + pre;
				  plaintext = "";
			  }
		  } // if
	  } // for
    GM_log("<diffstring");
	  	
	  return str +" " +getPlainText(plaintext);
	}
	
		
	function diff( o, n ) {
		var ns = new Array();
		var os = new Array();
		
		for ( var i = 0; i < n.length; i++ ) {
			if ( ns[ n[i] ] == null )
				ns[ n[i] ] = { rows: new Array(), o: null };
			ns[ n[i] ].rows.push( i );
		}
		
		for ( var i = 0; i < o.length; i++ ) {
			if ( os[ o[i] ] == null )
				os[ o[i] ] = { rows: new Array(), n: null };
			os[ o[i] ].rows.push( i );
		}
		
		for ( var i in ns ) {
			if ( ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1 ) {
				n[ ns[i].rows[0] ] = { text: n[ ns[i].rows[0] ], row: os[i].rows[0] };
				o[ os[i].rows[0] ] = { text: o[ os[i].rows[0] ], row: ns[i].rows[0] };
			}
		}
		
		for ( var i = 0; i < n.length - 1; i++ ) {
			if ( n[i].text != null && n[i+1].text == null && o[ n[i].row + 1 ].text == null && 
					 n[i+1] == o[ n[i].row + 1 ] ) {
				n[i+1] = { text: n[i+1], row: n[i].row + 1 };
				o[n[i].row+1] = { text: o[n[i].row+1], row: i + 1 };
			}
		}
		
		for ( var i = n.length - 1; i > 0; i-- ) {
			if ( n[i].text != null && n[i-1].text == null && o[ n[i].row - 1 ].text == null && 
					 n[i-1] == o[ n[i].row - 1 ] ) {
				n[i-1] = { text: n[i-1], row: n[i].row - 1 };
				o[n[i].row-1] = { text: o[n[i].row-1], row: i - 1 };
			}
		}
		
		return { o: o, n: n };
	}

*/
  function stripHTML(oldString) {
    var newString = "";
    var inTag = false;
    for(var i = 0; i < oldString.length; i++) {
      if(oldString.charAt(i) == '<') 
        inTag = true;
      if(oldString.charAt(i) == '>') {
        inTag = false;
        i++;
      }
      if(!inTag) 
        newString += oldString.charAt(i);
    }
    return newString;

  }


  compress.prototype.mediawiki_content = function(text) {
    GM_log(">mw_content:");
    if (text == "") {
      return text;
    } else {
      text = '' + text;
      var start = text.indexOf('<textarea');
      start += text.substr(start, 1000).indexOf('>') + 1;
      var end = text.indexOf('</textarea>');
      GM_log("<mw_content");
      text = text.substr(start, end - start);
      s = text.replace(/</g, "&lt;");
      s = s.replace(/>/g, "&gt;");
      GM_log ("Stripped: " + s.substr(0,50));
      return s;
    }
  }


  compress.prototype.start = function() {
    var hist = document.getElementById('pagehistory');
    if (hist) {
      var diffs;
      diffs = document.evaluate(
        "LI",
        hist,
        null,
        XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
        null
      );
      var last='*x!', prevdiffcomment;

      for (var i = 0; i < diffs.snapshotLength; i++) {

        var diff = diffs.snapshotItem(i);
        var comment = document.evaluate(
          'SPAN[@class="comment"]',
          diff,
          null,
          XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
          null
        ).snapshotItem(0);
        //GM_log(comment.innerHTML);
        var a = document.evaluate(
          "SPAN/A",
          diff,
          null,
          XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
          null
        );
        eacha = a.snapshotItem(0);
        if (eacha.title==last) {
          if (comment) {
            prevdiffcomment.innerHTML = prevdiffcomment.innerHTML + '//' + comment.innerHTML;
          } else {
            prevdiffcomment.innerHTML = prevdiffcomment.innerHTML + '//---';
          }
          diff.parentNode.removeChild(diff);
        } else {
          last = eacha.title;
          if (!comment) {
            comment = document.createElement('SPAN');
            comment.className='comment';
            comment.innerHTML=' ---';
            diff.insertBefore(comment, null);
          }
          prevdiffcomment = comment;

        } //if
      }//for
    } //if hist
  } // function 'start'

  compress.prototype.loadDiff = function(urlno) {
    GM_log("in loadDiff");
    this.urlno = urlno;
    this.hostname = "en.wikipedia.org";
    var url = this.urls[urlno] + '&action=edit';
    if (this.urls[urlno] == null) {
      var details = new String("");
      details.responseText = ""; // force comparison with blank text;
      compress.loadedDiff(details);
      return;
    }
      
    GM_log(">loading!" + url);
    GM_xmlhttpRequest({
  	  method:'GET',
  	  url:url,
      headers:{
        'User-agent': 'Mozilla/4.0 (compatible) Greasemonkey',
        'Accept': 'application/xml',
        },
      onload:function(details) {
        //alert("hello " + details.status + '/' + details.statusText + '/' + details.responseHeaders);
        compress.loadedDiff(details);
      }
    });
    GM_log("<loading!" + url);
  
  }
  compress.prototype.loadedDiff = function(details) {
    GM_log(">loadedDiff "+this.urlno);
    this.pages[this.urlno] = this.mediawiki_content(details.responseText);
    GM_log("-loadedDiff "+this.urlno);
    if (this.urlno > 0) {
      s = diffString(this.pages[this.urlno], this.pages[this.urlno-1]);
      GM_log("done diff");
      wh = document.getElementById(this.info[this.urlno -1]);
      span = document.createElement('span');
      span.innerHTML = s;
      wh.insertBefore(span, null);
    }
    if (details.responseText != "") {
      compress.loadDiff(this.urlno+1); // if blank text, stop.
    }      
    GM_log("<loadedDiff");
    
  }

  compress.prototype.showDiffs = function() {
    var hist = document.getElementById('pagehistory');

    if (hist) {
      var diffs;
      diffs = document.evaluate(
        'LI/A[text() != "cur" and text() != "last"][1]',
        hist,
        null,
        XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
        null
      );

      this.urls = new Array(diffs.snapshotLength);
      this.info = new Array(diffs.snapshotLength);
      this.pages = new Array(diffs.snapshotLength);
      
      GM_log("Number of A's: " + diffs.snapshotLength);
      
      for (var i = 0; i < diffs.snapshotLength; i++) {

        var diff = diffs.snapshotItem(i);
        
        diff.id = "difflink" + i;
        diff.parentNode.id = "diffli" + i;
        this.urls[i] = diff.href;
        this.info[i] = "diffli" + i;
        
        if (i==0) {
          this.loadDiff(0);
        }
      }//for
    } //if hist
  } // function 'start'

  
  var compress = new compress();
  document.compress = compress;

} // unnamed function

) ();