User:Enterprisey/fancy-diffs.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
This user script seems to have a documentation page at User:Enterprisey/fancy-diffs. |
// vim: ts=4 sw=4 et ai
( function () {
var api;
var DC_CLS = ' class="diffchange diffchange-inline"'; // the CSS classes for the diffchange (<ins>/<del>) 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, """ ) } );
}
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 += "<ins" + DC_CLS + ">"; activeTag = "ins"; break;
case INS_END: html += "</ins>"; activeTag = ""; break;
case DEL_START: html += "<del" + DC_CLS + ">"; activeTag = "del"; break;
case DEL_END: html += "</del>"; activeTag = ""; break;
case A_START:
if( activeTag ) {
html += "</" + activeTag + ">";
}
html += "<a class='fancy-diffs' href=\"" + chunk.url + "\">";
if( activeTag ) {
html += "<" + activeTag + DC_CLS + ">";
}
break;
case A_END:
if( activeTag ) {
html += "</" + activeTag + ">";
}
html += "</a>";
if( activeTag ) {
html += "<" + activeTag + DC_CLS + ">";
}
break;
case EXPAND:
html += '<span class="fd-expand" data-' + chunk.expandTy + '="' + chunk.data + '">(show)</span>';
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 );
} );
} )();