User:NguoiDungKhongDinhDanh/fancy-diffs.js

// For attribution: User:Enterprisey/fancy-diffs.js

(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; } else if (linkTarget.match(/^\//)) { linkTarget = pageName + linkTarget; if (linkTarget.match(/\/$/) && match[0].slice(2, match[0].length - 2).indexOf('|') < 0) { linkTarget = linkTarget.slice(0, linkTarget.length - 1); }					}					linkTarget = decodeURIComponent(linkTarget); var result = [ {ty: TEXT, txt: '[['},						{ty: A_START, url: mw.util.getUrl(linkTarget)},						{ty: TEXT, txt: match[1]},						{ty: A_END},						{ty: TEXT, txt: match[0].substring(2 + linkTarget.length)}					];

if ((function(link) { var ns = [ 'File', 'Image', 'Media', mw.config.get('wgFormattedNamespaces')[6], mw.config.get('wgFormattedNamespaces')[-2] ];						for (var i of ns) { if (link.match(new RegExp('^[\s_]*' + i + '[\s_]*:', 'i'))) { return true; }						}						return false; })(linkTarget)) { result.push({							ty: EXPAND,							expandTy: 'img',							data: linkTarget.replace(/"/g, '&quot;')						});					}

return result; }			},			{				regex: /\{\{([^#|<>{}\[\]]+?)(?:\|.+?)?\}\}/g, // %!"$&'*,\-./0-9:;=?@A-Z\\\^_`a-z~+\u0080-\uFFFF				handler: function(match) {					var name = match[1].replace(/&lt;\/?[\w-]+?&gt;/g, ),						fullName = name;					if (name.indexOf('#') === 0) {						fullName = name.replace(/^#invoke:/, 'Module:');					} else if (name.indexOf('int:') === 0) {						fullName = name.replace(/^int:/, 'MediaWiki:');					} else if (name.indexOf('/') === 0) {						fullName = pageName + name;					} else if (!name.match(/^:/)) {						fullName = fullName.replace(/^(safe)?subst:/, );						fullName = mw.Title.newFromText(fullName);						fullName = fullName !== null && (							fullName.namespace === 0 ?							'Template:' + fullName.toText :							fullName.toText						) || 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: new RegExp(					'(?:(?:https|http|gopher|irc|ircs|ftp|news|nnttp|worldwind|telnet|svn|git|mms):\/\/|mailto:)' +					'([A-Za-z0-9:;._\/~%\\-+&#?!=@]|%[0-9a-fA-F]{2})+',					'g'				), handler: function(match) { var url = match[0]; if (						match.input[match.index - 1] === '[' &&						(url[url.length - 1].match(/[,;.:!?]/) || (url.indexOf('(') < 0 && 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 pageNameLink = diffTable.querySelector('.mw-diff-edit > a'); var pageName = (			mw.util.getParamValue('title', pageNameLink && pageNameLink.href) ||			mw.config.get('wgPageName')		);

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].children[0].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 && data.query.pages[Object.keys(data.query.pages)[0]].imageinfo[0]) { 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);		}	); });