User:Robintibor/VisualChanges.js

importStylesheet( 'Robintibor/VisualChanges.css' ); // from http://stackoverflow.com/questions/2419749/get-selected-elements-outer-html/4180972#4180972 (function($) { $.fn.outerHtml = function {    return $(this).clone.wrap('  ').parent.html;  } })(jQuery);

(function ( $ ) {   /* @TODO: check how to add portlet links?    alert(mw.config.get('wgTitle')); mw.util.addPortletLink('p-tb', 'http://mediawiki.org/', 'MediaWiki.org');*/        // for merging later on, specify keywords to mark deletions and additions        // in the wikitext...        var addedKeyword = "__vc__add__";        var endAddedKeyword = "__vc__endadd__";        var deletedKeyword = "__vc__delete__";        var endDeletedKeyword = "__vc__enddelete__";		// functions for the buttons...		var visualChangesUI = {			clickBackwardButton : function{				$( '#visual-changes-forward-button' ).hide;				$( '#visual-changes-backward-button' ).hide;				visualChanges.goToPreviousRevision;			},			clickForwardButton: function {				$( '#visual-changes-forward-button' ).hide;				$( '#visual-changes-backward-button' ).hide;				visualChanges.goToNextRevision;			}		}

// visualChanges is the main class holding all important informations var visualChanges = { article: { articleId : mw.config.get('wgArticleId'), // revisions variable storing actual revisions with wikitext // parsed comments, etc.				revisionTexts: {}, // revisionList is only storing ids of the revisions revisionInfos: [] },			// ids referring to revisions in the revisionList! currentRevision: 0, revisionToDiffTo: 0, getRevisionStartId: function { var oldIdIndex = document.URL.indexOf('&oldid='); if (oldIdIndex != -1) return parseInt(document.URL.substr(oldIdIndex + 7)); else return mw.config.get('wgCurRevisionId'); },           mergeDiffsAndApplyThem: function( fromWikiText, toWikiText, 				diffWikiText ) { var fromLines = fromWikiText.split(/\r?\n/); var toLines = toWikiText.split(/\r?\n/); var diffDOM = $(diffWikiText); var mergedDiffs = ''; var currentLine = 0; $.each($('.diff-lineno', diffDOM),                    function(index, value) {                        // get the line number of the next change,                        // subtract -1 for zerobased index..                        var lineNumber = parseInt(value.innerHTML.substr(5)) - 1;                        // ok now we have a change, first add strings before this line                        if (currentLine > lineNumber)                            return;                        var tableRow = value.parentElement;						foundNextDiffOrEndOfTable = false;                        var diffCell;						while ( !foundNextDiffOrEndOfTable ) {							var linesInBetween = 0;								// look for context lines now..							foundChangeLine = false;							while ( !( foundChangeLine || foundNextDiffOrEndOfTable ) )							{								// nexto it is newline sign, then next row								tableRow = tableRow.nextSibling; if ( tableRow == null ) { foundNextDiffOrEndOfTable = true; break; }								tableRow = tableRow.nextSibling; if ( tableRow == null ) { foundNextDiffOrEndOfTable = true; break; }								// go through children see if you find change for ( var i = 0; i < tableRow.childNodes.length; i++ ) { diffCell = tableRow.childNodes.item( i ); if ( diffCell.className == 'diff-addedline' || 										diffCell.className == 'diff-deletedline') { foundChangeLine = true; break; } else if ( diffCell.className == 'diff-lineno' ){ alert('lineno found!'); foundNextDiffOrEndOfTable = true; break; }								}								if (!foundChangeLine) lineNumber++; }							if ( foundNextDiffOrEndOfTable ) break; // add those lines that are the same to the diff linesInBetween = lineNumber - currentLine; mergedDiffs += fromLines.slice(currentLine, currentLine + linesInBetween).join('\n'); mergedDiffs += '\n'; // now check if we have only one change (addition or							// deletion) or both! if ( tableRow.childNodes.length == 3 ) { // only addition or deletion if ( diffCell.className == 'diff-addedline' ) mergedDiffs += addedKeyword + toLines[ lineNumber ] + endAddedKeyword + '\n'; else if ( diffCell.className == 'diff-deletedline' ) mergedDiffs += deletedKeyword + fromLines[ lineNumber ] + endDeletedKeyword + '\n'; } else { // now merge the line with additions and deletions... var deletedLine = $('div', tableRow.childNodes.item( 1 )); var addedLine = $('div', tableRow.childNodes.item( 3 ));

var deletePositionInOldWikiText = 0; var addPositionInOldWikiText = 0; var mergedPositionInOldWikiText = 0; var nextAddPosition = 0; var nextDeletePosition = 0; var deleteNodeIndex = 0; var addNodeIndex = 0; var deleteNodes = deletedLine.contents; var addNodes = addedLine.contents; var parsedLine = false; var deleteNode; var addNode; var charsToAdd; while (!parsedLine) {									if ( deleteNodeIndex < deleteNodes.length ) deleteNode = deleteNodes[ deleteNodeIndex ]; if ( addNodeIndex < addNodes.length ) addNode = addNodes[ addNodeIndex ]; if( deleteNodeIndex < deleteNodes.length &&										deleteNode.nodeType == Node.TEXT_NODE ){ nextDeletePosition = deletePositionInOldWikiText + deleteNode.data.length; } else { nextDeletePosition = deletePositionInOldWikiText; }									if ( addNodeIndex < addNodes.length &&										addNode.nodeType == Node.TEXT_NODE ) { nextAddPosition = addPositionInOldWikiText + addNode.data.length; }									else { nextAddPosition = addPositionInOldWikiText; }

if ( ( nextDeletePosition <= nextAddPosition || addNodeIndex == addNodes.length ) &&										deleteNodeIndex < deleteNodes.length ) { if ( deleteNode.nodeType != Node.TEXT_NODE ) { // now we have a deletion mergedDiffs += deletedKeyword + deleteNode.innerHTML + endDeletedKeyword; } else { // now we have a text from the old wikitext, // make sure you havent added it already... charsToAdd = nextDeletePosition - mergedPositionInOldWikiText; mergedDiffs += deleteNode.data.substr( deleteNode.data.length -												charsToAdd ); deletePositionInOldWikiText = nextDeletePosition; mergedPositionInOldWikiText = deletePositionInOldWikiText; }										deleteNodeIndex++; } else if ( addNodeIndex < addNodes.length ) { // TODO: check necessary? if ( addNode.nodeType != Node.TEXT_NODE ) { // now we have a deletion mergedDiffs += addedKeyword + addNode.innerHTML + endAddedKeyword; } else { // now we have a text from the old wikitext, // make sure you havent added it already... charsToAdd = nextAddPosition - mergedPositionInOldWikiText; mergedDiffs += addNode.data.substr( addNode.data.length -												charsToAdd ); addPositionInOldWikiText = nextAddPosition; mergedPositionInOldWikiText = addPositionInOldWikiText; }										addNodeIndex++; }									if ( addNodeIndex == addNodes.length &&											deleteNodeIndex == deleteNodes.length ) parsedLine = true; }							}							lineNumber++; mergedDiffs += '\n'; currentLine = lineNumber; ////foundNextDiffOrEndOfTable = true; }					});               // remaining text is the same...				if ( fromLines.length > currentLine )					mergedDiffs += fromLines.slice( currentLine ).join( '\n' )+ '\n';                $.ajax({ type: 'POST', url: mw.util.wikiScript( 'api' ), success: function(data) { var parsedText = data.parse.text[ '*' ]; // remove the newlien text nodes and replace }" by " var cleanedText = parsedText.replace( /\\n/g,  ).replace( /\\"/g,  );								// now replace keywords for additions and deletions								var regexpDeleteStart = new RegExp( deletedKeyword, 'g' );								var regexpDeleteEnd = new RegExp ( endDeletedKeyword, 'g' );								var regexpAddStart = new RegExp( addedKeyword, 'g' ) ;								var regexpAddEnd = new RegExp( endAddedKeyword, 'g' ) ;								cleanedText = cleanedText.replace( regexpDeleteStart, ' ' );								cleanedText = cleanedText.replace( regexpDeleteEnd, ' ' );								cleanedText = cleanedText.replace( regexpAddStart, ' ' );								cleanedText = cleanedText.replace( regexpAddEnd, ' ' );                               mw.util.$content.html( cleanedText );								$( '#visual-changes-backward-button' ).show;								$( '#visual-changes-forward-button' ).show;                            },                            dataType: 'json', async: true, data: { action: 'parse', text: mergedDiffs, format: 'json' }               });                return mergedDiffs;            },            reallySetCurRevision : function ( toRevNr ) {					var fromRevId = this.article.revisionInfos[ this.currentRevision ].revid;				var toRevId =  this.article.revisionInfos[ toRevNr ].revid;                var fromWikiText = this.article.revisionTexts[ fromRevId ][ '*' ];                var toWikiText = this.article.revisionTexts[ toRevId ][ '*' ];                $('#wikitext').html( 'Wikitext ' + toRevId + ': ' +                                    toWikiText.replace( /\r?\n/g, ' ' ) );                $('#oldwikitext').html( 'Old Wikitext ' + fromRevId + ': ' +                                    fromWikiText.replace( /\r?\n/g, ' ' ) );               $.ajax({ type: 'POST', url: mw.util.wikiScript( 'api' ), success: function(data) { //TODO: remove stringify method! $('#diff').html( JSON.stringify(data) ); var mergedDiff = visualChanges.mergeDiffsAndApplyThem( 									fromWikiText, toWikiText, data.compare[ '*' ] ); $('#mergeddiff').html( 'MergedDiffs: ' + mergedDiff ); visualChanges.currentRevision = toRevNr; },                           dataType: 'json', async: true, data: { action: 'compare', fromrev: fromRevId, torev: toRevId, format: 'json' }               });            },			getRevisionTextsAndApplyThem: function ( fromRevId, toRevId, toRevNr )			{				// check for availability of these revisions				var revisionsToGet = "";				if ( !this.article.revisionTexts.hasOwnProperty(fromRevId) ) {					revisionsToGet += fromRevId				}				if ( !this.article.revisionTexts.hasOwnProperty(toRevId) ) {					if ( revisionsToGet != "")						revisionsToGet += "|"					revisionsToGet += toRevId				}				if ( revisionsToGet == "" )					this.reallySetCurRevision( toRevNr );				else {					$.ajax( { type: 'POST', url: mw.util.wikiScript( 'api' ), success: function( data ) { //TODO: remove stringify method! $('#queryanswer').html( JSON.stringify( data ) ); // add the revisions to the text var page = data.query.pages[visualChanges.article.articleId]; var revisions = page.revisions; var revision; for ( var i = 0; i < revisions.length; i++ ) { revision = revisions[i]; visualChanges.article.revisionTexts[revision.revid] = revision; }										// check that revision is now there if (visualChanges.article.revisionTexts.hasOwnProperty(fromRevId)											&& visualChanges.article.revisionTexts.hasOwnProperty(toRevId)) visualChanges.reallySetCurRevision(toRevNr); else alert('revision not downloaded, shouldnt happen..'); },					dataType: 'json', async: true, data: { action: 'query', revids: revisionsToGet, prop: 'revisions', rvprop: 'ids|timestamp|parsedcomment|content', format: 'json' }					});				}			},			getRevisionInfosAndTextAndApplyThem: function (revisionNr, extraRevisionsToGet ) {				// start id for next revision should be the last revision				// id for which there are infos or the revision being looked				// at right now				var revisionStartId;				if ( this.article.revisionInfos.length > 0)					revisionStartId = this.article.revisionInfos						[this.article.revisionInfos.length -1].revid - 1;				else					revisionStartId = this.getRevisionStartId;				var revisionsToGet = revisionNr - this.article.revisionInfos.length										+ extraRevisionsToGet;				$.ajax({ type: 'POST', url: mw.util.wikiScript( 'api' ), success: function(data) { // add the new revision infos var revisionInfos = data.query.pages[visualChanges.article.articleId] .revisions; visualChanges.article.revisionInfos = visualChanges.article.revisionInfos.concat(revisionInfos); if (visualChanges.article.revisionInfos.length > revisionNr) { var fromRevId = visualChanges.article.revisionInfos [ visualChanges.currentRevision ].revid; var toRevId = visualChanges.article.revisionInfos [ revisionNr ].revid; // now get the texts of the needed revisions and apply them visualChanges.getRevisionTextsAndApplyThem(									fromRevId, toRevId, revisionNr); } else { // try again! getRevisionInfosAndTextAndApplyThem(extraRevisionsToGet,											revisionNr); };                           },                            dataType: 'json', async: true, data: { action: 'query', prop: 'revisions', rvprop: 'ids|timestamp', pageids: mw.config.get('wgArticleId'), rvlimit: revisionsToGet, rvstartid: revisionStartId, format: 'json' }				});			},			goToPreviousRevision: function {				// first check if revisionId is present				if ( this.article.revisionInfos.length < this.currentRevision + 2 ) {					var extraRevisionsToGet = 20;					this.getRevisionInfosAndTextAndApplyThem( this.currentRevision + 1, extraRevisionsToGet );					} else {					var toRevNr = this.currentRevision + 1;					// check if from id present					var fromRevId = this.article.revisionInfos[ this.currentRevision ].revid;					var toRevId = this.article.revisionInfos[ toRevNr ].revid;					this.getRevisionTextsAndApplyThem( fromRevId, toRevId, toRevNr );				}			},			goToNextRevision: function {				// first check if revisionId is present				if ( this.currentRevision < 1) {					alert('going back revisions beyond initial revision not supported yet');					$( '#visual-changes-backward-button' ).show;					return;				} else {					var toRevNr = this.currentRevision - 1;					// check if from id present					var fromRevId = this.article.revisionInfos[ this.currentRevision ].revid;					var toRevId = this.article.revisionInfos[ toRevNr ].revid;					this.getRevisionTextsAndApplyThem( fromRevId, toRevId, toRevNr );				}			}	}; if (mw.config.get( 'wgAction' ) != 'view' || !mw.config.get( 'wgIsArticle' ) ) return; // initialize menu... $( '#firstHeading' ).before(mw.html.element('div', {id: 'visual-changes-menu', 'class': 'visual-changes-menu-relative'}, new mw.html.Raw(                       mw.html.element( 'a', {id: 'visual-changes-backward-button', href: '#visualchanges'}, 'B' ) +                         mw.html.element( 'a', {id: 'visual-changes-forward-button', href: '#visualchanges'}, 'F' ) ) ) ); // TODO: remove logtable! $( '#bodyContent' ).after( ' ' ); // If user scrolls below the position of       // the visual-changes-menu, make the menu move to a fixed position on        // the screen by changing the class (see ext.visualChanges.css        // for styling) var visualChangesMenu = $( '#visual-changes-menu' ); var offset = visualChangesMenu.offset; var topOffset = offset.top;

$( window ).scroll( function {                var scrollTop = $( window ).scrollTop;                if ( scrollTop >= topOffset ) {                    if (visualChangesMenu.hasClass( 'visual-changes-menu-relative' ) ) {                      visualChangesMenu.removeClass( 'visual-changes-menu-relative' );                      visualChangesMenu.addClass( 'visual-changes-menu-fixed' );                    }                }                if ( scrollTop < topOffset ) {                        if (visualChangesMenu.hasClass( 'visual-changes-menu-fixed' ) ) {                            visualChangesMenu.removeClass( 'visual-changes-menu-fixed' );                            visualChangesMenu.addClass( 'visual-changes-menu-relative' );                        }                }            }        ); // add button click functions $( '#visual-changes-forward-button' ).click( visualChangesUI.clickForwardButton ); $( '#visual-changes-backward-button' ).click( visualChangesUI.clickBackwardButton ); })( jQuery )