User:Enterprisey/fancy-diffs.js

// vim: ts=4 sw=4 et ai ( function {    var api;    var DC_CLS = ' class="diffchange diffchange-inline"'; // the CSS classes for the diffchange ( / ) spans

function processText( text, pageName ) { var chunks = [];

// Types for chunks (I really should've called them "tokens") var TEXT = 0; var INS_START = 1; var INS_END = 2; var DEL_START = 3; var DEL_END = 4; var A_START = 5; var A_END = 6; var EXPAND = 7;

// Throughout, "ins or del" is abbreviated as "change" or "chg" var CHG_RGX = /<(ins|del) class="diffchange diffchange-inline">([^<]+?)($|<\/\1>)/g; var lastChgEnd = 0; var chgMatch; var justText = ""; var firstTextSegment; var isIns; do { chgMatch = CHG_RGX.exec( text ); if( chgMatch ) { firstTextSegment = text.substring( lastChgEnd, chgMatch.index ); if( firstTextSegment.length ) { chunks.push( { ty: TEXT, txt: firstTextSegment, idx: justText.length } ); justText += firstTextSegment; }               isIns = chgMatch[1] === "ins"; chunks.push( { ty: isIns ? INS_START : DEL_START } ); chunks.push( { ty: TEXT, txt: chgMatch[2], idx: justText.length } ); chunks.push( { ty: isIns ? INS_END : DEL_END } ); justText += chgMatch[2]; lastChgEnd = chgMatch.index + chgMatch[0].length; }       } while( chgMatch ); if( lastChgEnd <= text.length - 1 ) { chunks.push( { ty: TEXT, txt: text.substring( lastChgEnd ), idx: justText.length } ); justText += text.substring( lastChgEnd ); }

var markupHandlers = [ {               regex: /\[\[(.+?)(?:\|.+?)?\]\]/g, handler: function ( match ) { var linkTarget = match[1]; if( linkTarget.indexOf( "#" ) === 0 ) { linkTarget = pageName + linkTarget; }                   var result = [ { ty: A_START, url: mw.util.getUrl( linkTarget ) }, { ty: TEXT, txt: match[0] }, { ty: A_END } ];

if( linkTarget.indexOf( "File:" ) === 0 || linkTarget.indexOf( "Image:" ) === 0 ) { result.push( { ty: EXPAND, expandTy: "img", data: linkTarget.replace( /"/g, "&quot;" ) } );                   }

return result; }           },            {                regex: /\{\{(.+?)(?:\|.+?)?\}\}/g, handler: function ( match ) { var name = match[1], fullName = name; if( name.indexOf( "#" ) === 0 ) { fullName = name.replace( /^#invoke:/, "Module:" ); } else if( name.indexOf( ":" ) < 0 ) { fullName = "Template:" + name; }

return [ { ty: TEXT, txt: "{{" }, // "}}" pour one out for vim's syntax highlighter { ty: A_START, url: mw.util.getUrl( fullName ) }, { ty: TEXT, txt: match[1] }, { ty: A_END }, { ty: TEXT, txt: match[0].substring( 2 + name.length ) } ];               }            },            {                regex: /(?:(?:https|http|gopher|irc|ircs|ftp|news|nnttp|worldwind|telnet|svn|git|mms):\/\/|mailto:)([!#$&-;=?-\[\]_a-z~]|%[0-9a-fA-F]{2})+/g, handler: function ( match ) { var url = match[0]; if( match.input[ match.index - 1 ] === "[" && url[url.length - 1] === "]" ) { url = url.substring( 0, url.length - 1 ); }                   return [ { ty: A_START, url: url }, { ty: TEXT, txt: url }, { ty: A_END }, ];               }            }        ];

// Definitely among the trickiest code I've written for a user // script to date. The version that kept detailed track of       // string indices was much worse, trust me! for( var handlerIdx = 0; handlerIdx < markupHandlers.length; handlerIdx++ ) { var regex = markupHandlers[handlerIdx].regex, handler = markupHandlers[handlerIdx].handler; var match; do { match = regex.exec( justText ); if( match ) { var replacementChunks = handler( match );

// Locate the start and end of `match` in `chunks` var startChunkIdx = -1, endChunkIdx = -1; for( var chunkIdx = 0; chunkIdx < chunks.length; chunkIdx++ ) { if( chunks[chunkIdx].ty !== TEXT ) { continue; } else if( startChunkIdx < 0 ) { if( ( chunks[chunkIdx].idx + chunks[chunkIdx].txt.length ) > match.index ) { startChunkIdx = chunkIdx; }                       }                        if( ( startChunkIdx >= 0 ) && chunks[chunkIdx].idx >= ( match.index + match[0].length ) ) { endChunkIdx = chunkIdx; break; }                   }

// Edge-case handling for the start/end chunk locator if( startChunkIdx < 0 ) { console.error( "whoops" ); } else if( endChunkIdx < 0 ) { endChunkIdx = chunks.length - 1; } else { endChunkIdx--; }                   while( chunks[endChunkIdx].ty !== TEXT ) endChunkIdx--;

// Split the start and end chunks, so we can cleanly insert the A_START and A_END var startChunk = chunks[startChunkIdx]; var idxInStartChunk = match.index - startChunk.idx; if( idxInStartChunk > 0 && idxInStartChunk < ( startChunk.txt.length - 1 ) ) { chunks.splice( startChunkIdx, 1,                           { ty: TEXT, txt: startChunk.txt.substring( 0, idxInStartChunk ), idx: startChunk.idx },                            { ty: TEXT, txt: startChunk.txt.substring( idxInStartChunk ), idx: startChunk.idx + idxInStartChunk }                        ); startChunkIdx++; startChunk = chunks[startChunkIdx]; endChunkIdx++; }

var endChunk = chunks[endChunkIdx]; var idxInEndChunk = match.index + match[0].length - endChunk.idx; if( idxInEndChunk > 0 && idxInEndChunk < ( endChunk.txt.length - 1 ) ) { chunks.splice( endChunkIdx, 1,                           { ty: TEXT, txt: endChunk.txt.substring( 0, idxInEndChunk ), idx: endChunk.idx },                            { ty: TEXT, txt: endChunk.txt.substring( idxInEndChunk ), idx: endChunk.idx + idxInEndChunk }                        ); }

// Make sure the new text chunks have correct idx's set var replacementTextLength = 0; for( var i = 0; i < replacementChunks.length; i++ ) { if( replacementChunks[i].ty === TEXT ) { replacementChunks[i].idx = startChunk.idx + replacementTextLength; replacementTextLength += replacementChunks[i].txt.length; }                   }

// Insert the new chunks in place of the old ones - keeping all the formatting intact! var newChunks = []; var newTextLen = 0; var existingChunks = chunks.slice( startChunkIdx, endChunkIdx + 1 ); var replIdx = 0, existIdx = 0; // counters in `replacementChunks` & `existingChunks` respectively var replInnerIdx = 0, existInnerIdx = 0; // indices into text chunks while( true ) {

// Non-TEXT chunks are formatting/control and always get pushed while( replacementChunks[replIdx] && ( replacementChunks[replIdx].ty !== TEXT ) ) { newChunks.push( replacementChunks[replIdx] ); replIdx++; }                       while( existingChunks[existIdx] && ( existingChunks[existIdx].ty !== TEXT ) ) { newChunks.push( existingChunks[existIdx] ); existIdx++; }

if( newTextLen >= match[0].length ) { break; }

// Pick the shorter chunk, so as not to miss any formatting. var replEndIdx = ( replIdx < replacementChunks.length ) ? ( replacementChunks[replIdx].idx + replacementChunks[replIdx].txt.length ) : Infinity; var existEndIdx = ( existIdx < existingChunks.length ) ? ( existingChunks[existIdx].idx + existingChunks[existIdx].txt.length ) : Infinity; var usingRepl = replEndIdx <= existEndIdx; if( usingRepl ) { var newText = replacementChunks[replIdx].txt.substring( replInnerIdx ); newChunks.push( { ty: TEXT, txt: newText, idx: startChunk.idx + newTextLen } ); newTextLen += newText.length; replInnerIdx = 0; while( true ) { replIdx++; if( !replacementChunks[replIdx] || ( replacementChunks[replIdx].ty === TEXT ) ) break; newChunks.push( replacementChunks[replIdx] ); }                           existInnerIdx += newText.length; for(existIdx < existingChunks.length; existIdx++ ) { if( existingChunks[existIdx].ty !== TEXT ) { newChunks.push( existingChunks[existIdx] ); } else if( existInnerIdx >= existingChunks[existIdx].txt.length ) { existInnerIdx -= existingChunks[existIdx].txt.length; } else { break; }                           }                        } else { var newText = existingChunks[existIdx].txt.substring( existInnerIdx ); newChunks.push( { ty: TEXT, txt: newText, idx: startChunk.idx + newTextLen } ); newTextLen += newText.length; existInnerIdx = 0; while( true ) { existIdx++; if( !existingChunks[existIdx] || ( existingChunks[existIdx].ty === TEXT ) ) break; newChunks.push( existingChunks[existIdx] ); }                           replInnerIdx += newText.length; for(replIdx < replacementChunks.length; replIdx++ ) { if( replacementChunks[replIdx].ty !== TEXT ) { newChunks.push( replacementChunks[replIdx] ); } else if( replInnerIdx >= replacementChunks[replIdx].txt.length ) { replInnerIdx -= replacementChunks[replIdx].txt.length; } else { break; }                           }                        }                    }

// Now, splice the new chunks in place of the old ones var spliceArgs = [ startChunkIdx, endChunkIdx - startChunkIdx + 1 ].concat( newChunks ); Array.prototype.splice.apply( chunks, spliceArgs ); }           } while( match ); }

// Write out chunks into text var html = ""; var activeTag = ""; for( var i = 0; i < chunks.length; i++ ) { var chunk = chunks[i]; switch( chunk.ty ) { case TEXT: html += chunk.txt; break; case INS_START: html += ""; activeTag = "ins"; break; case INS_END: html += " "; activeTag = ""; break; case DEL_START: html += ""; activeTag = "del"; break; case DEL_END: html += " "; activeTag = ""; break; case A_START: if( activeTag ) { html += ""; }                   html += ""; if( activeTag ) { html += "<" + activeTag + DC_CLS + ">"; }                   break; case A_END: if( activeTag ) { html += ""; }                   html += ""; if( activeTag ) { html += "<" + activeTag + DC_CLS + ">"; }                   break; case EXPAND: html += '(show) '; break; }       }

return html; }

function processDiff( diffTable ) { if( !diffTable.querySelector ) {

// Assume diffTable is a jQuery object diffTable = diffTable.get( 0 ); }

if( diffTable.getElementsByClassName( "fancy-diffs" ).length > 0 ) {

// We already ran on this diff return; }

// Determine page name, because processText wants it       var pageName; switch( mw.config.get( "wgCanonicalSpecialPageName" ) ) { case "Contributions": pageName = diffTable.parentNode.querySelector( "a.mw-contributions-title" ).textContent; break; case "Watchlist": pageName = diffTable.previousElementSibling.querySelector( "a.mw-changeslist-title" ).textContent; break; default: pageName = mw.config.get( "wgPageName" ); break; }

var rows = diffTable.querySelectorAll( "tr" ); rowLoop: for( var rowIdx = 0, numRows = rows.length; rowIdx < numRows; rowIdx++ ) { var row = rows[rowIdx]; if( row.tagName.toLowerCase === "colgroup" ) { return; }           if( row.querySelector( "a" ) ) { continue; }           for( var cellIdx = 0, numCells = row.children.length; cellIdx < numCells; cellIdx++ ) { var td = row.children[cellIdx]; if( td.className.indexOf( "diff-context" ) >= 0 ) { if( td.children && td.children.length ) { var text = processText( td.children[0].innerHTML, pageName ); td.children[0].innerHTML = text; row.children[cellIdx + 2].innerHTML = text; continue rowLoop; }               } else if( ( td.className.indexOf( "diff-addedline" ) >= 0 ) ||                        ( td.className.indexOf( "diff-deletedline" ) >= 0 ) ) { if( td.children && td.children.length ) { td.children[0].innerHTML = processText( td.children[0].innerHTML, pageName ); }               }            }        }

var expandSpans = diffTable.querySelectorAll( "span.fd-expand" ); for( var spanIdx = 0, numSpans = expandSpans.length; spanIdx < numSpans; spanIdx++ ) { var span = expandSpans[spanIdx]; span.addEventListener( "click", function {                if( !this.nextElementSibling || this.nextElementSibling.tagName.toLowerCase !== "div" || this.nextElementSibling.className !== "fd-img" ) {                    api.get( { action: "query", titles: this.dataset.img, prop: "imageinfo", iiprop: "url" } ).done( function ( data ) { if( data.query && data.query.pages ) { var url = data.query.pages[ Object.keys( data.query.pages )[0] ].imageinfo[0].url; var div = document.createElement( "div" ); div.className = "fd-img"; var img = document.createElement( "img" ); img.className = "fancy-diffs"; img.src = url; img.style["max-width"] = "100%"; div.appendChild( img ); this.parentNode.insertBefore( div, this.nextSibling ); }                   }.bind( this ) );                    this.textContent = "(hide)";                } else {                    if( this.nextElementSibling.style.display === "none" ) {                        this.nextElementSibling.style.display = "";                        this.textContent = "(hide)";                    } else {                        this.nextElementSibling.style.display = "none";                        this.textContent = "(show)";                    }                }            } ); }   }

$.when(       $.ready,        mw.loader.using( [ "mediawiki.api", "mediawiki.util" ] )    ).then( function  {        var table = document.querySelector( "table.diff" );        api = new mw.Api;        mw.util.addCSS( ".fd-expand { cursor: pointer; text-decoration: underline; background-color: #faf3; }" );        if( table ) {            processDiff( table );        }        mw.hook( "wikipage.diff" ).add( processDiff );        mw.hook( "new-diff-table" ).add( processDiff );        mw.hook( "diff-update" ).add( processDiff );    } ); } );