User:Robintibor/VisualChanges.js

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
/* Was planned to be a script for showing a visual diff between two versions
*  Unfortunately i couldn't finish it in time, that's why its still a big mess :)
*  Going to continue working on it slowly now :)
*  Plan was: Get wikitexts of both revisions that are to be diffed
*            Get their diff
*            Merge both wikitexts according to the diff marking additions and deletions
*            with keywords in the text
*            Parse the merged wikitext
*            Replace the keywords by <del>, <ins> or some other html tags and
*            visually distinguish deletions and additions nicely
* Unfortunately i couldn't do it that well yet, also lack some experiences in
* web development :)
*/
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('<div></div>').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: []
			},
			// helper object for the parsing
			parseHelper: {
				/**
				 * merge a line with additions and a line with deletions
				 * into one line for a merged diff
				 * 
				 * @param deletedLine line with deletions
				 * @param addedLine line with additions
				 */
				mergeDiffLine: function( deletedLine, addedLine ) {
					// Merge one line, just always go through both
					// lines in parallel, always inserting in parallel as well
					var mergedLine = "";
					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
								mergedLine += deletedKeyword + deleteNode.innerHTML +
									endDeletedKeyword;
							} else {
								// now we have text from the old wikitext,
								// make sure you havent added it already...
								charsToAdd = nextDeletePosition - 
									mergedPositionInOldWikiText;
								mergedLine += 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 an addition
								mergedLine += addedKeyword + addNode.innerHTML +
									endAddedKeyword;
							} else {
								// now we have text from the new wikitext,
								// make sure you havent added it already...
								charsToAdd = nextAddPosition - 
									mergedPositionInOldWikiText;
								mergedLine += addNode.data.substr( addNode.data.length -
									charsToAdd );
								addPositionInOldWikiText = nextAddPosition;
								mergedPositionInOldWikiText = addPositionInOldWikiText;
							}
							addNodeIndex++;
						}
						if ( addNodeIndex == addNodes.length &&
								deleteNodeIndex == deleteNodes.length )
								parsedLine = true;
					}
					return mergedLine;
				},
				/**
				 * This function cleans the merged text, moving 
				 * markers for deletion and addition outside of wiki markups etc.
				 * 
				 * @param mergedText - the merged wikitexts
				 */
				cleanMergedText: function(mergedText) {
					
					var markups = {
						templates: {
							start: '{{',
							end: '}}'
						},
						links: {
							start: '[[',
							end: ']]'
							}
					}
					// go through all markups, look if they have a keyword included
					for (var markup in markups) {
						var startMarkup = markups[markup].start;
						var endMarkup = markups[markup].end;
						var indexOfStartMarkup = mergedText.indexOf(startMarkup, 
							0);
						var indexOfEndMarkup = mergedText.indexOf(endMarkup, 
								indexOfStartMarkup);
						var markupSubString = '';
						while (indexOfStartMarkup !== -1) {
							
							markupSubString = mergedText.substring(indexOfStartMarkup + 
								startMarkup.length, indexOfEndMarkup);
							// check substring for markups
							var cleanMarkup = this.cleanMarkupString( markupSubString,
								startMarkup, endMarkup );
							if (cleanMarkup !== null) {
								mergedText = mergedText.substring( 0, indexOfStartMarkup ) +
									cleanMarkup + 
									mergedText.substring( indexOfEndMarkup + endMarkup.length );
								indexOfStartMarkup += cleanMarkup.length - 1;
								}
							indexOfStartMarkup = mergedText.indexOf(startMarkup,
								indexOfStartMarkup + 1);
							indexOfEndMarkup = mergedText.indexOf(endMarkup, 
								indexOfStartMarkup);
						}
					}
					// now restore sections
					var brokenSectionStart = deletedKeyword + "==";
					var brokenSectionEnd = "==" + endDeletedKeyword;
					var brokenSectionStartRegExp = new RegExp(brokenSectionStart, 'g');
					var brokenSectionEndRegExp = new RegExp(brokenSectionEnd, 'g');
					mergedText = mergedText.replace(brokenSectionStartRegExp,
						deletedKeyword + '\n==');
					mergedText = mergedText.replace(brokenSectionEndRegExp,
						'==\n' + endDeletedKeyword);
					brokenSectionStart = addedKeyword + "==";
					brokenSectionEnd = "==" + endAddedKeyword;
					brokenSectionStartRegExp = new RegExp( brokenSectionStart, 'g' );
					brokenSectionEndRegExp = new RegExp(brokenSectionEnd, 'g');
					mergedText = mergedText.replace(brokenSectionStartRegExp,
						addedKeyword + '\n==');
					mergedText = mergedText.replace(brokenSectionEndRegExp,
						'==\n' + endAddedKeyword);
					return mergedText;
				},
				/**
				 * Clean the markup string, that means reorder enclosed
				 * delete and addition tags in away that the markup can be parsed
				 * (move them outside the markup and if necessary duplicate the markup!)
				 * @return clean markup string if there were enclosed keywords or 
				 * null otherwise
				 */
				cleanMarkupString: function( markupString, startMarkup, endMarkup ){
					var deleteStartIndex = markupString.indexOf(deletedKeyword);
					var deleteEndIndex = markupString.indexOf(endDeletedKeyword);
					var addStartIndex = markupString.indexOf(addedKeyword);
					var addEndIndex = markupString.indexOf(endAddedKeyword);
					// check if any keyword is present
					if ( deleteStartIndex === -1 && deleteEndIndex === -1 && 
						addStartIndex === -1 && addEndIndex === -1)
						return null;
					// now go through the markup string, always find first keyword
					var cleanMarkupStart = "";
					var cleanMarkupEnd = "";
					var markupIndex = 0;
					// deleted and added parts represent those parts that will
					// be enclosed in deleted and added keywords,
					// not those parts that were really deleted or added :)
					var markupDeletedParts = [];
					var markupAddedParts = [];
					// First case: First keyword is an end keyword
					if ( ( deleteStartIndex === -1 || deleteEndIndex < deleteStartIndex )
						&& ( addStartIndex === -1 || addEndIndex < addStartIndex ) ) {
						// delete before add
						if ( deleteEndIndex !== - 1 && 
							( deleteEndIndex < addEndIndex || addEndIndex === -1 ) ) {
							cleanMarkupStart += endDeletedKeyword;
							markupDeletedParts.push( markupString.substring( 0, 
								deleteEndIndex) );
							markupIndex = deleteEndIndex + endDeletedKeyword.length;
						} else {
							cleanMarkupStart += endAddedKeyword;
							markupAddedParts.push( markupString.substring( 0, 
								addEndIndex) );
							markupIndex = addEndIndex + endAddedKeyword.length;
						}
					} else {
					// Second case: First keyword is a start keyword
						if ( deleteStartIndex !== -1 && 
							( deleteStartIndex < addStartIndex || addStartIndex === -1 ) ) {
							markupAddedParts.push( markupString.substring( 0, deleteStartIndex ) );
							if ( deleteEndIndex === - 1) {
								// whole string is part of deleted revision...
								markupDeletedParts.push( markupString );
								markupIndex = markupString.length;
							} else {
								markupDeletedParts.push( markupString.substring( 0, deleteStartIndex ) +
													 markupString.substring( deleteStartIndex +
												 deletedKeyword.length, deleteEndIndex) );
								markupIndex = deleteEndIndex + endDeletedKeyword.length;
							}
						} else {
							markupDeletedParts.push( markupString.substring( 0, addStartIndex ) );
							if ( addEndIndex === - 1) {
								// whole string is part of added revision...
								markupAddedParts.push( markupString );
								markupIndex = markupString.length;
							} else {
								markupAddedParts.push( markupString.substring( 0, addStartIndex ) +
														 markupString.substring( addStartIndex +
													 addedKeyword.length, addEndIndex) );
								markupIndex = addEndIndex + endAddedKeyword.length;
							}
						}

					}
					// now run through the rest of the markupstring...
					// we can always expect start tags to come first now
					deleteStartIndex = markupString.indexOf( deletedKeyword, markupIndex );
					addStartIndex = markupString.indexOf( addedKeyword, markupIndex );
					while( !( deleteStartIndex === -1 && addStartIndex === -1 ) ) {
						if ( deleteStartIndex !== -1 && 
							( deleteStartIndex < addStartIndex || addStartIndex === -1 ) ) {
							deleteEndIndex = markupString.indexOf( endDeletedKeyword,
								deleteStartIndex);
								markupAddedParts.push( markupString.substring( markupIndex,
									deleteStartIndex) );
							if ( deleteEndIndex === -1 ){
								cleanMarkupEnd += deletedKeyword;
								markupDeletedParts.push( markupString.substring( markupIndex ) );
								markupIndex = markupString.length;
							} else {
								markupDeletedParts.push( 
									markupString.substring( 
										markupIndex, deleteStartIndex ) + 
									markupString.substring( 
										deleteStartIndex + deletedKeyword.length, 
										deleteEndIndex) );
								markupIndex = deleteEndIndex + endDeletedKeyword.length;
							}
						} else {
							// add start keyword is next keyword...
							markupDeletedParts.push( markupString.substring( markupIndex,
								addStartIndex) );
							if ( addEndIndex === -1 ){
								cleanMarkupEnd += addedKeyword;
								markupAddedParts.push( markupString.substring( markupIndex ) );
								markupIndex = markupString.length;
							} else {
								markupAddedParts.push( 
									markupString.substring( 
										markupIndex, addStartIndex ) + 
									markupString.substring( 
										addStartIndex + addedKeyword.length, 
										addEndIndex) );
								markupIndex = addEndIndex + endAddedKeyword.length;
							}
						}
						deleteStartIndex = markupString.indexOf( deletedKeyword, markupIndex );
						addStartIndex = markupString.indexOf( addedKeyword, markupIndex );
					}
					// remainder of markup is in both revisions
					markupDeletedParts.push( markupString.substring(markupIndex) );
					markupAddedParts.push( markupString.substring(markupIndex) );
					var cleanMarkupString = cleanMarkupStart;
					var deletedString = markupDeletedParts.join('');
					if (deletedString != '') {
						cleanMarkupString += deletedKeyword + startMarkup +
							deletedString + endMarkup + endDeletedKeyword;
					}
					var addedString = markupAddedParts.join('');
					if ( addedString != '' ) {
						cleanMarkupString += addedKeyword + startMarkup +
							addedString + endMarkup + endAddedKeyword;
					}
					cleanMarkupString += cleanMarkupEnd;
					return cleanMarkupString;
				}
			},
			// 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;
                        // check if we already dealt with this line number
						// (will happen because line numbers can appear twice
						// in the diff)
                        if (currentLine > lineNumber)
                            return;
						// ok now we have a real diff-change, lets look for
						// the changes until we find the next diff-change
						// or we are at the end of the table..
                        var tableRow = value.parentElement;
						foundNextDiffOrEndOfTable = false;
                        var diffCell;
						while ( !foundNextDiffOrEndOfTable ) {
							// count context lines, those lines
							// that didn't change..... 
							var linesInBetween = 0;	
							foundChangeLine = false;
							while ( !( foundChangeLine || foundNextDiffOrEndOfTable ) )
							{
								// there should be one new line sign
								// then the next table row
								// also check for end of table
								tableRow = tableRow.nextSibling;
								if ( tableRow == null ) {
									foundNextDiffOrEndOfTable = true;
									break;
								}
								tableRow = tableRow.nextSibling;
								if ( tableRow == null ) {
									foundNextDiffOrEndOfTable = true;
									break;
								}
								// go through the child nodes of the table
								// see if you find a cell indicating
								// a deletion, an addition, or a new diff-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' ){
											foundNextDiffOrEndOfTable = true;
											break;
										}
								}
								if (!foundChangeLine) lineNumber++;
							}
							// check if we have a new diff
							if ( foundNextDiffOrEndOfTable )
								break;
							// add those lines that /didnt change to the merged
							// 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, just 
								// add the corresponding line with an add or delete
								// marker to the merged diff
								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 line with additions and deletions...
								var deletedLine = $('div', tableRow.childNodes.item( 1 ));
								var addedLine = $('div', tableRow.childNodes.item( 3 ));
								var mergedLine = visualChanges.parseHelper.mergeDiffLine( deletedLine, addedLine );
								mergedDiffs += mergedLine;
							}
							lineNumber++;
							mergedDiffs += '\n';
							currentLine = lineNumber;
						}
					});
                // remaining text is the same...
				if ( fromLines.length > currentLine )
					mergedDiffs += fromLines.slice( currentLine ).join( '\n' )+ '\n';
				mergedDiffs = this.parseHelper.cleanMergedText(mergedDiffs);
                $.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, '<del>' );
								cleanedText = cleanedText.replace( regexpDeleteEnd, '</del>' );
								cleanedText = cleanedText.replace( regexpAddStart, '<ins>' );
								cleanedText = cleanedText.replace( regexpAddEnd, '</ins>' );
								
                                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 + ':<br />' +
                                     toWikiText.replace( /\r?\n/g, '<br />' ) );
                $('#oldwikitext').html( 'Old Wikitext ' + fromRevId + ':<br />' +
                                     fromWikiText.replace( /\r?\n/g, '<br />' ) );
               $.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: <br>' + 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!
										visualChanges.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( '<table id="logtable"><tr> <td colspan = "2" id="queryanswer"></td>' + 
                                    '<tr><td id="wikitext">Wikitext:</td><td id="oldwikitext">old Wikitext</td></tr>' +
                                 '<tr><td colspan = "2" id="diff">diff</td></tr>' +
                                 '<tr><td colspan = "2" id="mergeddiff">mergeddiff</td></tr></table>' );
        // 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 )