User:Nardog/SmartDiff.js

mw.loader.using([	'mediawiki.util', 'mediawiki.Title', 'mediawiki.api' ], function smartDiff {	mw.loader.addStyleTag('.smartdiff-link.extiw, .smartdiff-link.external{color:var(--color-progressive,#36c)} .smartdiff-link.extiw:visited, .smartdiff-link.external:visited{color:#795cb2} .smartdiff-link.extiw:active, .smartdiff-link.external:active{color:#faa700}');	class SmartDiff {		constructor($diff) {			this.$diff = $diff;			this.isSpecial = mw.config.get('wgNamespaceNumber') === -1;			this.isView = mw.config.get('wgAction') === 'view' &&				new URLSearchParams(location.search).get('diffonly') !== '1';			this.magicWords = [				'!', 'BASEPAGENAME', 'BASEPAGENAME:', 'BASEPAGENAMEE', 'BASEPAGENAMEE:',				'canonicalurl:', 'CURRENTDAY', 'CURRENTDAY2', 'CURRENTDAYNAME',				'CURRENTDOW', 'CURRENTHOUR', 'CURRENTMONTH', 'CURRENTMONTH1',				'CURRENTMONTHABBREV', 'CURRENTMONTHNAME', 'CURRENTMONTHNAMEGEN', 'CURRENTTIME', 'CURRENTTIMESTAMP', 'CURRENTVERSION', 'CURRENTWEEK', 'CURRENTYEAR', 'DEFAULTCATEGORYSORT:', 'DEFAULTSORT:', 'DEFAULTSORTKEY:', 'DISPLAYTITLE:', 'filepath:', 'formatnum:', 'FULLPAGENAME', 'FULLPAGENAME:', 'FULLPAGENAMEE', 'FULLPAGENAMEE:', 'fullurl:', 'gender:', 'int:', 'lc:', 'lcfirst:', 'LOCALDAY', 'LOCALDAY2', 'LOCALDAYNAME', 'LOCALDOW', 'LOCALHOUR', 'LOCALMONTH', 'LOCALMONTH1', 'LOCALMONTHABBREV', 'LOCALMONTHNAME', 'LOCALMONTHNAMEGEN', 'LOCALTIME', 'LOCALTIMESTAMP', 'LOCALWEEK', 'LOCALYEAR', 'msg:', 'msgnw:', 'NAMESPACE', 'NAMESPACE:', 'NAMESPACEE', 'NAMESPACEE:', 'NAMESPACENUMBER', 'NAMESPACENUMBER:', 'ns:', 'NUMBEROFACTIVEUSERS', 'NUMBEROFARTICLES', 'NUMBEROFEDITS', 'NUMBEROFFILES', 'NUMBEROFPAGES', 'NUMBEROFUSERS', 'padleft:', 'PAGENAME', 'PAGENAMEE', 'PAGESINCAT:', 'PAGESINCATEGORY:', 'plural:', 'REVISIONDAY', 'REVISIONDAY:', 'REVISIONDAY2', 'REVISIONDAY2:', 'REVISIONID', 'REVISIONID:', 'REVISIONMONTH', 'REVISIONMONTH:', 'REVISIONMONTH1', 'REVISIONMONTH1:', 'REVISIONSIZE', 'REVISIONTIMESTAMP', 'REVISIONTIMESTAMP:', 'REVISIONUSER', 'REVISIONUSER:', 'REVISIONYEAR', 'REVISIONYEAR:', 'ROOTPAGENAME', 'ROOTPAGENAME:', 'ROOTPAGENAMEE', 'ROOTPAGENAMEE:', 'SHORTDESC:', 'SUBJECTPAGENAME', 'SUBJECTPAGENAME:', 'SUBJECTPAGENAMEE', 'SUBJECTPAGENAMEE:', 'SUBJECTSPACE', 'SUBJECTSPACE:', 'SUBJECTSPACEE', 'SUBJECTSPACEE:', 'SUBPAGENAME', 'SUBPAGENAME:', 'SUBPAGENAMEE', 'SUBPAGENAMEE:', 'TALKPAGENAME', 'TALKPAGENAME:', 'TALKPAGENAMEE', 'TALKPAGENAMEE:', 'TALKSPACE', 'TALKSPACE:', 'TALKSPACEE', 'TALKSPACEE:', 'uc:', 'ucfirst:', 'urlencode:' ];			if (window.smartdiffMagicWords) { this.magicWords.push(...window.smartdiffMagicWords); }			try { this.subNs = mw.config.get('wgVisualEditorConfig').namespacesWithSubpages; } catch (e) {} if (!this.subNs) { this.subNs = Object.keys(mw.config.get('wgFormattedNamespaces')) .map(k => Number(k)).filter(ns => ![0, 6, 8].includes(ns)); }			this.re = /((?:\[(?:<[^>]*>)?\[|(?<!{(?:<[^>]*>)?){(?:<[^>]*>)?{(?:<[^>]*>)?(?:(?:#(?:<[^>]*>)?invoke|(?:safe)?subst|msg(?:nw)?|raw)(?:<[^>]*>)?:)?)(?:\s*(?:<[^>]*>)?&lt;(?:<[^>]*>)?tvar(?:<[^>]*>)?\s(?!&gt;).*?&gt;)?\s*)((?:(?!&[gl]t;)[^\[\]{|}])+?)(?=\s*(?:(?:<[^>]*>)?&lt;(?:<[^>]*>)?\/(?:<[^>]*>)?tvar(?:<[^>]*>)?&gt;(?:<[^>]*>)?\s*)?(?:\||\](?:<[^>]*>)?\]|}(?:<[^>]*>)?}|$))/g; this.headRe = /^((?:(?:<[^>]*>)*=){1,6}(?:<[^>]*>)?\s*)((?:(?!&[gl]t;).)+?)(?=\s*(?:(?:<[^>]*>)?=){1,6}(?:<[^>]*>|\s)*(?:&lt;|$))/g; this.urlRe = /(?:https?(?:<[^>]*>)?:(?:<[^>]*>)?|(?<=\[(?:<[^>]*>)?))\/(?:<[^>]*>)?\/(?:[-\dA-Za-z]+|<[^>]*>)+\.(?:[-.\d:A-Za-z]+|<[^>]*>)+(?:\/(?:(?:[!#-%(-;=?-Z_a-z~]+|&amp;|<[^>]*>)*(?:[#-%(+\-\/-9=?-Z_a-z~]|&amp;)(?:<[^>]*>)?)?)?/g; if (window.smartdiffTemplates) { this.tempRe = /( data-smartdiff-temp="(\d+)">[^{|}]+)(\|(?:(?!&[gl]t;)[^\[\]{}]|{(?:<[^>]*>)?{(?:<[^>]*>)?!(?:<[^>]*>)?}(?:<[^>]*>)?})+)(?=}(?:<[^>]*>)?}|$)/g; this.tempSubRe = /((?:\s|{(?:<[^>]*>)?{(?:<[^>]*>)?!(?:<[^>]*>)?}(?:<[^>]*>)?}[^<>|]*|<[^>]*>)*(?:\|(?:\s|(?:<[^>]*>)|\d+(?:\s|<[^>]*>)*=|[^\d<=>|](?:[^<=>|]|<[^>]*>)*=(?:[^<=>|]|<[^>]*>)*\|?)*|$))/; this.templates = window.smartdiffTemplates; }			['rep', 'headRep', 'urlRep', 'tempRep'].forEach(fn => {				this[fn] = this[fn].bind(this);			}); this.side = 'old'; $diff.find('.diff-deletedline > div').get.forEach(this.processDiv, this); this.side = 'new'; $diff.find('.diff-addedline > div').get.forEach(this.processDiv, this); let $contexts = $diff.find('.diff-context > div'); $contexts.each((i, div) => {				if (i % 2) {					this.side = 'new';					if (this.propUsed && this.getProp !== this.getProp('pn', 'old')) {						this.processDiv(div);					} else {						$contexts.eq(i).replaceWith($contexts.eq(i - 1).clone);					}				} else {					this.side = 'old';					this.propUsed = false;					this.processDiv(div);				}			}); this.links = {}; $diff.find('.smartdiff-link:not(.external)').each((i, link) => {				let title = link.title;				if (!title) return;				if (!this.links.hasOwnProperty(title)) {					this.links[title] = [];				}				this.links[title].push(link);			}); this.query(Object.keys(this.links).slice(0, 500)); if (this.hasError) { mw.notify('SmartDiff error', { type: 'warn' }); }		}		processDiv(div) { if (div.querySelector('a[href]')) return; let origHtml = div.innerHTML; let newHtml = origHtml.replace(this.urlRe, this.urlRep) .replace(this.re, this.rep).replace(this.headRe, this.headRep); if (this.tempRe) { newHtml = newHtml.replace(this.tempRe, this.tempRep); }			if (newHtml === origHtml) return; let $newDiv = $(' ').html(newHtml); if (this.detectErrors($newDiv, newHtml, origHtml, div)) return; div.textContent = ''; $newDiv.contents.appendTo(div); }		rep($0, $1, $2) { if ($0.includes('<a class="smartdiff-link')) return $0;			let [s, pre, mid, post] = this.stripTags($2, true, $1);			let t = mw.Title.newFromText(s), isTemp;			if (t) {				if ($1.includes('invoke')) {					t = mw.Title.makeTitle(828, s);				} else if (s[0] === '/') {					if (this.subNs.includes(this.getProp('ns'))) {						t = mw.Title.newFromText(							this.getProp + s.replace(/\/+$/, '')						);					} else if ($1[0] === '{') {						t.namespace = 10;					}				} else if ($1[0] === '{') {					if (s[0] === '#') return $0;					if (!t.namespace && s[0] !== ':') {						if (!$1.includes('msg') && !$1.includes('raw')) {							let match = s.match(/^[^:]+(?::(?=.)|$)/);							if (match && this.magicWords.includes(match[0])) {								return $0;							}						}						t.namespace = 10;						isTemp = true;					}				} else if ((this.isSpecial || !this.isView) && s[0] === '#') {					t.title = this.getProp; }			} else if (s.startsWith('../') && this.subNs.includes(this.getProp('ns'))) { let chunks = s.split('/'); let levelCount = chunks.findIndex(v => v !== '..'); let sup = this.getProp.split('/').slice(0, -levelCount).join('/'); if (sup) { let sub = chunks.slice(levelCount).join('/').replace(/\/+$/, ''); t = mw.Title.newFromText(sub ? sup + '/' + sub : sup); }			}			if (!t) return $0; let attrs = { class: 'smartdiff-link', href: t.getUrl };			if (this.isSpecial || !this.isView || s[0] !== '#') { attrs.title = t.toText; }			if (isTemp && this.tempRe) { let name = t.getMainText; let idx = this.templates.findIndex(temp => temp.names.includes(name)); if (idx !== -1) { attrs['data-smartdiff-temp'] = idx; }			}			return pre + $('').attr(attrs).html(mid)[0].outerHTML + post; }		stripTags(s, decode, pre = , post = ) { let mid = s, tags = s.match(/<\/?(?:ins|del)[^>]*>/g); s = $($.parseHTML(s.replace(/&amp;/g, '&'))).text; if (decode) { try { s = decodeURIComponent(s); } catch (e) {} }			if (tags) { if (tags[0][1] === '/') { pre += tags[0]; mid = `<${tags[0].slice(2, 5)} class="diffchange diffchange-inline">` + mid; }				let lastTag = tags.pop; if (lastTag[1] !== '/') { mid += ``; post = lastTag + post; }			}			return [s, pre, mid, post]; }		headRep($0, $1, $2) { if ($0.includes(']*)?>/gi, '$1')				.replace(/(.+?)/g, '$1');			let t = mw.Title.newFromText(				`${this.isSpecial || !this.isView ? this.getProp : ''}#${s}`			);			if (!t) return $0;			let attrs = {				class: 'smartdiff-link',				href: t.getUrl			};			if (this.isSpecial || !this.isView) {				attrs.title = t.toText;			}			return pre + $('').attr(attrs).html(mid)[0].outerHTML + post;		}		urlRep($0) {			let [url, pre, mid, post] = this.stripTags($0);			return pre + $('').attr({				class: 'smartdiff-link external',				href: url,				rel: 'nofollow'			}).html(mid)[0].outerHTML + post; }		tempRep($0, $1, $2, $3) { if ($3.includes(' {				if (!os || i % 2) return os;				let j = i / 2;				if (j < temp.start || j > temp.end ||					temp.skipOdd && j % 2 || temp.skipEven && j % 2 === 0				) {					return os;				}				let [s, pre, mid, post] = this.stripTags(os, true);				if (temp.prefix) {					s = temp.prefix + s;				}				if (temp.suffix) {					s += temp.suffix;				}				let t = temp.forceNs					? mw.Title.makeTitle(temp.namespace, s)					: mw.Title.newFromText(s, temp.namespace);				if (!t) return os;				let params = (j >= temp.noRedirectStart || j <= temp.noRedirectEnd) &&					{ redirect: 'no' };				return pre + $('').attr({					class: 'smartdiff-link',					href: t.getUrl(params),					title: t.toText				}).html(mid)[0].outerHTML + post;			}).join('');		}		getProp(n = 'pn', side = this.side) { this.propUsed = true; if (this[side]) { if (this[side][n]) { return this[side][n]; }			} else { this[side] = {}; let link = this.$diff[0].querySelector(					side === 'old'						? '#mw-diff-otitle1 a, #differences-prevlink'						: '#mw-diff-ntitle1 a, #differences-nextlink'				); if (link) { let pn = mw.util.getParamValue('title', link.search); this[side].pn = pn; this[side].ns = mw.Title.newFromText(pn).namespace; return this[side][n]; }			}			if (this[n]) { return this[n]; }			if (this.isSpecial) { this.pn = ''; this.ns = 0; } else { this.pn = mw.config.get('wgPageName'); this.ns = mw.config.get('wgNamespaceNumber'); }			return this[n]; }		query(titles) { if (!titles.length) return; new mw.Api.post({				action: 'query',				titles: titles.slice(0, 50),				iwurl: 1,				prop: 'info',				inprop: 'linkclasses',				inlinkcontext: this.getProp,				formatversion: 2			}, {				headers: { 'Promise-Non-Write-API-Action': 1 }			}).then(response => {				let query = response && response.query;				if (!query) return;				let data = {};				(query.pages || []).forEach(page => { let obj = { classes: page.linkclasses || [] }; if (page.missing && !page.known) { obj.classes.push('new'); obj.params = { action: 'edit', redlink: 1 }; }					data[page.title] = obj; });				(query.interwiki || []).forEach(interwiki => { data[interwiki.title] = { classes: ['extiw'], url: interwiki.url };				});				(query.normalized || []).forEach(entry => { if (!data.hasOwnProperty(entry.to)) return; let obj = data[entry.to]; obj.canonical = entry.to; if (!obj.url) { obj.url = mw.util.getUrl(entry.to, obj.params); }					data[entry.from] = obj; });				Object.entries(data).forEach(([title, obj]) => { if (!this.links.hasOwnProperty(title)) return; let $links = $(this.links[title]).addClass(obj.classes) .attr('title', obj.canonical); if (obj.url) { $links.attr('href', function {							return obj.url + this.hash;						}); }				});				this.query(titles.slice(50));			}); }		detectErrors($newDiv, newHtml, origHtml, div) { let comp = $newDiv.html; if (comp !== newHtml) { console.warn(					'SmartDiff syntax error at:\n',					div,					`\nNew HTML:\n${newHtml}\nCompared against:\n${comp}`				); this.hasError = true; return true; }			let $comp = $newDiv.clone; $comp.find('.smartdiff-link').contents.unwrap; comp = $comp.html.replace(/<\/(ins|del)><\1[^>]*>/g, ''); if (comp !== origHtml) { console.warn(					'SmartDiff mutation error at:\n',					div,					`\nOriginal HTML:\n${origHtml}\nCompared against:\n${comp}`				); this.hasError = true; return true; }		}	}	mw.hook('wikipage.diff').add($diff => {		new SmartDiff($diff);	}); });