User:PigeonIP/DiffLists.js

From Wikidata
Jump to navigation Jump to search

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.
// Changes appearance of Recent Changes, Watchlist, Contributions, History 
// pages, and Related Changes.
// Also adds filter options.
// This script is intended to demonstrate my proposal for watchlist design, proposed in T121361.
//
// Intended browser support: Chrome, FF, MS Edge, Safari.
// Allowed ES6 bits: Rest, spread, object computed/shorthand props, fancy array/string/dom, for of, arrow functions. 
// Not allowed: Destructuring, let, class.
// Update 2016-05-11: Changed my mind. Destructuring is allowed. MS Edge no longer supported. 

// Multilingual support was added very late, so there's a lot of mess there.


// TODO: Maintain all non-autogenerated edit summary bits. #autolist is being removed.
// TODO: More advanced filtering options. Things like Wikiproject prop groups.
// ** Allow whatever caps
// TODO: Default (when no stored settings) to Babel settings (not in links), if possible. Investigate.

function DiffLists() {

if ( window.DiffListsLoaded ) {
	return;
}

window.DiffListsLoaded = true;

var isWD = mw.config.get( 'wgDBname' ) === 'wikidatawiki',
	wdDomain = 'https://www.wikidata.org',
	api = isWD ? new mw.Api() : new mw.ForeignApi( wdDomain + '/w/api.php' );

( 
	( [ "Recentchanges", "Watchlist", "Recentchangeslinked", "Contributions" ] )
		.indexOf( mw.config.get( "wgCanonicalSpecialPageName" ) ) !== -1 ||
	( mw.config.get( "wgAction" ) === "history" && [ 0, 120 ].indexOf( mw.config.get( "wgNamespaceNumber" ) ) !== -1 )
) && api.get( {
	action: "query",
	meta: "allmessages",
	amlang: mw.config.get( 'wgUserLanguage' ),
	ammessages: [
		// Changing the order shouldn't break things. TODO: Fix.
		// Diff view stuff.
		"wikibase-diffview-label", 
		"wikibase-diffview-description",
		"wikibase-diffview-alias",
		"wikibase-diffview-link",
		"wikibase-entity-property",
		"wikibase-diffview-qualifier",
		"wikibase-diffview-reference",
		"wikibase-diffview-rank",
		// Extras
		"wikibase-entitytermsforlanguagelistview-label",
		"wikibase-entitytermsforlanguagelistview-description",
		"wikibase-statementsection-statements",
		"wikibase-diffview-rank-preferred",
		"wikibase-diffview-rank-normal",
		"wikibase-diffview-rank-deprecated"
	].join( "|" ),
	maxage:  60 * 60 * 24 * 30,
	smaxage: 60 * 60 * 24 * 30
} ).done( function ( msgData ) {
	$( function () {
		//console.log( "msgs", msgData );
		mw.util.addCSS( 
			".YR-cl-added { background-color: #d8ecff; margin: 0 1px; padding: 0 1px; }" + 
			".YR-cl-removed { background-color: #feeec8; margin: 0 1px; padding: 0 1px; text-decoration: line-through; }" +
			".YR-cl-hidden { display: none !important; }" + // Yeah, this is misusing !important, but I need to override jquery's straight style.display.
			".YR-cl-summary ~ .YR-cl-summary:before { content: \", \"; }" +
			".YR-cl-hiddensummary { display: none; }"
		);
		var changeBlock = ( function () {
				var addedTemplate   = $( "<span>" ).addClass( "YR-cl-added"   )[ 0 ],
					removedTemplate = $( "<span>" ).addClass( "YR-cl-removed" )[ 0 ];
				return function ( type, elem ) { 
					return ( type === "added" ? addedTemplate : removedTemplate ).cloneNode( true ).appendChild( elem ).parentNode;
				};
			})(),
			msgs = {}, 
			msgNameArray = msgData.query.allmessages.map( msg => msg.name.split( "-" )[ 2 ] ),
			msgArray = msgData.query.allmessages.map( msg => msg[ "*" ] ),
			typeIndexes = { label: 0, description: 1, alias: 2, link: 3, property: 4 },
			allDiffs = [],
			nestedParents = new Map(),
			lang = mw.config.get( 'wgUserLanguage' );

		msgData.query.allmessages.forEach( function ( msg, i ) {
			msgs[ i < 8 ? msg.name.split( "-" )[ 2 ] : msg.name ] = msg[ "*" ];
		} );

		// Settings stuff.
		var filterData = {
				dbTypes: [ '', 'voyage', 'books', 'quote', 'news', 'source', 'versity' ].map( x => 'wiki' + x ),
				specialwikis: [ 'commons', 'meta', 'mediawiki', 'wikidata', 'species' ],
				propGroups: {
					// These can be functions or arrays.
					identifiers: function ( x ) {
						var v = x.change.values, node;
						for ( var i in v ) {
							node = v[ i ].node;
							if ( node && ( node.className === 'wb-external-id' || node.firstChild && node.firstChild.className === 'wb-external-id' ) ) {
								return true;
							}
						}
					},
					other: function ( x ) {
						return filterData.specialwikis.some( y => y + 'wiki' === x.data );
					}
				}
			},
			simpleSettings = getSettings(),
			processedSettings = processSettings( simpleSettings ),
			allPropTypes = { 
				[ msgs.label ]: "Labels", 
				[ msgs.description ]: "Descriptions", 
				[ msgs.link ]: "Sitelinks", 
				[ msgs.alias ]: "Aliases", 
				[ msgs.property ]: msgs[ "wikibase-statementsection-statements" ]
			},
			listType = mw.config.get( "wgAction" ) === "history" ? 'history' : 
				( mw.config.get( "wgCanonicalSpecialPageName" ) === "Contributions" ?
					"contribs" : 'normal' ),
			changeSelector = 
				isWD ?
					listType === 'history' ? 
						// History
						".mw-history-histlinks a:last-child" 
							:
						// Normal watchlists/RC
						".mw-changeslist .special > li > a:first-child, " + 
						// Enhanced watchlists/RC
						".mw-enhanced-rc .mw-title + a, " + 
						// Enhanced, with nested
						".mw-enhanced-rc-nested .mw-enhanced-rc-time + a + a, " +
						// Contributions
						".mw-contributions-list a"
					:
					'.mw-changeslist .wikibase-edit a[tabindex]';
			//allPropTypes = [ "Labels", "Descriptions", "Sitelinks", "Aliases", "Statements" ];
		
		function buildOptionsBox() {
			function setSettings() {
				var data = {};
				$.each( checkboxes, function ( i, checkbox ) {
					data[ i ] = checkbox.checked && inputs[ i ].value;
				} );
				localStorage[ "YR-cl-settings" ] = JSON.stringify( simpleSettings = data );
				processedSettings = processSettings( simpleSettings );
			}
			
			var optionsContainer,
				optionsDiv = document.createElement( "div" ),
				checkboxes = {},
				inputs = {};
				
			if ( listType === "normal" ) {
				optionsContainer = document.querySelector( "#mw-watchlist-options, .rcoptions" );
				// Add a divider.
				optionsContainer.appendChild( document.createElement( "hr" ) ).style.margin = "8px 0";
			} else {
				var l = document.querySelector( listType === "contribs" ? ".mw-contributions-form" : "#mw-history-searchform" );
				optionsContainer = l.parentNode.insertBefore( document.createElement( "fieldset" ), l.nextSibling );
				optionsContainer
					.appendChild( document.createElement( "legend" ) )
						.appendChild( document.createTextNode( "Filter options" ) );
			}
			
			// Build filter options menu
			$.each( allPropTypes, function ( type, label ) {
				var optionDiv = document.createElement( "div" );
				optionDiv.style.display = "inline-block";
				optionsDiv.appendChild( optionDiv );
				var checkbox = checkboxes[ type ] = document.createElement( "input" );
				checkbox.type = "checkbox";
				checkbox.checked = ( !simpleSettings || simpleSettings[ type ] !== false ) ? "checked" : "";
				optionDiv.appendChild( checkbox );
				optionDiv.appendChild( document.createTextNode( label + ": " ) );
				var input = inputs[ type ] = optionDiv.appendChild( document.createElement( "input" ) );
				if ( type === msgs.label || type === msgs.description || type === msgs.alias ) {
					input.title = 
						'Show only languages with these languages codes, ' + 
						'separated by |, e.g. "en|fr". Leave blank to show all.';
				} else if ( type === msgs.link ) {
					input.title = 
						'Show only links to projects with these database ' + 
						'codes, separated by |,  e.g. "enwiki|frwikivoyage". ' +
						'Leave blank to show all.\n' +
						'You can use a language code to show links to all ' +
						'projects in that language, or a project database ' +
						'suffix to show links to all of those projects ' +
						'regardless of language. Use "other" to show links ' +
						'to Commons, Meta, Mediawiki, Wikispecies, and ' +
						'Wikidata.';
				} else {
					input.title =
						'Show only changes to statements using these ' + 
						'properties, separated by |, e.g. "P40|P569". ' +
						'Leave blank to show all.\n' +
						'You can use "identifiers" to select all identifier ' +
						'properties, and prefix a property or group of ' +
						'properties with "^" to not show the property. ' +
						'(E.g. Input just "^identifiers" to show all ' +
						'properties except identifiers.)';
				}
				input.style.width = "50px";
				input.value = simpleSettings && simpleSettings[ type ] || "";
				if ( simpleSettings && simpleSettings[ type ] === false ) {
					input.disabled = true;
				}
				checkbox.onchange = input.onchange = function () {
					input.disabled = !checkbox.checked;
					setSettings();
					updateDisplay( allDiffs );
				};
				input.onkeydown = function ( e ) {
					if ( e.keyCode === 13 ) {
						setSettings();
						updateDisplay( allDiffs );
						return false;
					}
				};
			} );
			
			// Add the menu to the DOM
			if ( optionsContainer ) {
				optionsContainer.appendChild( optionsDiv );
			}
		}
		
		// Settings will be stored in localStorage, in the following format:
		// { "label": "..." | "" | false, ... }
		
		// Multiple situations possible here:
		// * Checking to see if any members of a group match show=true
		// ** 'changes' obj, check to see if there's any of x type, etc
		// * Check if a particular prop/value should be shown
		// Among this, there can be single setting prop or multi-setting prop.
		// Is it actually necessary to special-case the first check?
		//
		// Checks if a value should be shown under relevant setting.
		function checkSetting( value ) {
			// setting > false || '' || [ { additive: true || false, type: '... (eg plain)', content: '... (eg en)' }, ... ]
			// value > { type: property, data: Property:P31 }
			var setting = processedSettings[ value.type ];
			
			if ( setting === false ) {
				return false;
			} else if ( setting === '' || setting === undefined ) {
				return true;
			} else {
				var r = !setting[ 0 ].additive,
					propGroups = filterData.propGroups;
				setting.forEach( settingV => {
					// if ( settingV.additive === r ) { return }
					if ( settingV.type === 'plain' ) {
						value.data === settingV.content && ( r = settingV.additive );
					} else if ( settingV.type === 'group' ) {
						// Probably statements only
						if ( typeof propGroups[ settingV.content ] === 'function' ?
							propGroups[ settingV.content ]( value ) :
							propGroups[ settingV.content ].indexOf( value.data ) !== -1
						) {
							r = settingV.additive;
						}
					} else if ( settingV.type === 'lang' ) {
						// Links only
						// I think this needs access to dbtypes
						if ( value.data.startsWith( settingV.content ) ) {
							if ( filterData.dbTypes.some( x => settingV.content + x === value.data ) ) {
								r = settingV.additive;
							}
						}
					} else if ( settingV.type === 'wiki' ) {
						// Links only
						if ( value.data.endsWith( settingV.content ) ) {
							r = settingV.additive;
						}
					} else if ( settingV.type === 'specialwiki' ) {
						// Links to "other" wikis, eg Commons.
						if ( value.data === settingV.content + 'wiki' ) {
							r = settingV.additive;
						}
					}
				} );
				return r;
			}
		}
		
		// Result can be false
		function getSettings() {
			var urlArgs = mw.util.getParamValue( 'difflists' ),
				storage = localStorage[ "YR-cl-settings" ];
			// 
			if ( !urlArgs && !storage ) {
				return false;
			} else {
				return $.extend( {}, 
					storage && JSON.parse( storage ),
					urlArgs && JSON.parse( urlArgs ) );
			}
		}
		
		function processSettings() {
			var settings = {},
				{ dbTypes, propGroups, specialwikis } = filterData;
			
			if ( simpleSettings === false ) {
				return false;
			}
			
			$.each( simpleSettings, function ( type, input ) {
				var setting = settings[ type ] = input && input.split( /\s*[\|\,]\s*/ );
				if ( setting ) {
					settings[ type ] = setting.map( x => {
						var isProp = type === msgs.property,
							isLink = type === msgs.link,
							additive = x[ 0 ] !== '^',
							content = additive ? x : x.substr( 1 );
						if ( isProp && !propGroups[ content ] ) {
							content = 'Property:' + content;
						}
						return {
							additive,
							type: 
								( propGroups.hasOwnProperty( content ) && 'group' ) ||
								( isLink && (
									( specialwikis.indexOf( x ) !== -1 && 'specialwiki' ) ||
									( dbTypes.indexOf( x ) !== -1 && 'wiki' ) ||
									( !dbTypes.some( y => x.endsWith( y ) ) && 'lang' )
								) ) || 
								'plain',
							content
						};
					} );
				}
				
				// Values:?
				// false 
				// { additive: true, type: plain, content: en }
				// { additive: true, type: lang, content: en }
				// { additive: true, type: wiki, content: wiki }
				// { additive: false, type: group, content: identifiers }
				// { additive: true, type: specialwiki, content: meta }
				// { additive: false, type: all } (no)
				// Should there be a special case for commonswiki etc? or just 'wiki'?
				
			} );
			return settings;
		}
		
		// Show or hide diff, depending on filter preferences.
		function updateDisplay( diffs ) {
			var isHistory = listType === "history";
			
			simpleSettings && diffs.forEach( function ( diff ) {
				
				var displayDiff = false;
				
				// If there's some not filtered, or we're on a history page
				// where all diffs are shown, hide individual filtered segments.
				$.each( diff.chunkDetails, function ( i, details ) {
					var display = false;
					
					display = checkSetting( details );
					
					displayDiff = displayDiff || display;
					
					details.wrapper.className = display ? "YR-cl-summary" : "YR-cl-hiddensummary";
				} );
				
				// If all elements are filtered, hide the whole listing, unless
				// we're on a history page.
				if ( !isHistory ) {
					diff.elem.classList.toggle( "YR-cl-hidden", !displayDiff );
				}
				
			} );
		}
		
		function buildChunkDetails( allChanges, chunkDetails ) {
			// Use allChanges to build chunkDetails, array of DOM for later use.
			// First non-statement changes.
			$.each( [ msgs.label, msgs.description, msgs.link, msgs.alias ], function ( i, type ) {
				$.each( allChanges[ msgArray.indexOf( type ) ], function ( langOrWiki, change ) {
					var chunk = document.createElement( "span" ),
						text = ( { 
							[ msgs.label ]: msgs[ "wikibase-entitytermsforlanguagelistview-label" ], 
							[ msgs.description ]: msgs[ "wikibase-entitytermsforlanguagelistview-description" ], 
							[ msgs.link ]: "Link", 
							[ msgs.alias ]: "Aliases" 
						} )[ type ];
					chunkDetails.push( change.details = { chunk, type, data: langOrWiki, change } );
					chunk.appendChild( document.createTextNode( text + " [" + langOrWiki + "]: " ) );
					$.each( type === msgs.alias ? change : [ change ], function( i, a ) {
						[ "added", "removed" ].forEach( function ( type ) {
							a[ type ] && chunk.appendChild( changeBlock( type, a[ type ] ) );
						} );
					} );
					
					// For links, add badge DOM.
					change.badges && change.badges.forEach( function ( a, i ) {
						$.each( a, function ( i, a ) {
							// We're using the original element. Should this
							// be cloned? If not, this might cause problems
							// for multiple diffs using the same changes.
							a.insertBefore( document.createTextNode( "(badge: "), a.firstChild );
							a.appendChild( document.createTextNode( ")"), a.firstChild );
							chunk.appendChild( changeBlock( i, a ) );
						} );
					} );
				} );
			} );
			
			// Then statement changes.
			$.each( allChanges[ typeIndexes.property ], function ( i, change ) {
				var chunk = document.createElement( "span" ),
					keys = Object.keys( change.values ),
					encase = keys.length === 1 && change.values[ keys[ 0 ] ].change;
				chunk.appendChild( change.link );
				chunk.appendChild( document.createTextNode( ": " ) );
				$.each( change.values, function ( index, statement ) {
					if ( statement.change ) {
						if ( !encase ) {
							chunk
								.appendChild( changeBlock( statement.change, statement.node ) );
						} else {
							chunk.appendChild( statement.node );
							chunk = changeBlock( statement.change, chunk );
						}
					} else {
						chunk.appendChild( statement.node );
					}
					if ( statement.qualifiers ) {
						//console.log( "has qual", a );
						var first = true;
						// The diff system does something really weird with multis.
						// Deletes and recreates all the same qualifiers. 
						$.each( statement.qualifiers, function ( i, a ) {
							chunk.appendChild( document.createTextNode( first ? " / " : ", " ) );
							var qualHolder = a.length === 1 ? 
								chunk.appendChild( changeBlock( a[ 0 ].change, a.link ) ) : 
								chunk;
							first = false;

							qualHolder.appendChild( a.link );
							qualHolder.appendChild( document.createTextNode( ": " ) );
							$.each( a, function ( i, a ) {
								chunk.appendChild( changeBlock( a.change, a.value ) );
							} );
						} );
					}
					if ( statement.references ) {
						//console.log( "has ref", a );
						$.each( statement.references, function ( i, a ) {
							var ref = changeBlock( a.change, document.createTextNode( "Reference: " ) );
							chunk.appendChild( document.createTextNode( i ? ", " : " / " ) );
							$.each( a, function ( i, a ) {
								if ( i ) {
									ref.appendChild( document.createTextNode( ", " ) );
								}
								ref.appendChild( a.propLink );
								ref.appendChild( document.createTextNode( ": " ) );
								ref.appendChild( a.value );
							} );
							chunk.appendChild( ref );
						} );
					}
					if ( statement.rank ) {
						$.each( statement.rank, function ( i, a ) {
							var rankDot = document.createElement( [ "sup", "span", "sub" ][ a.rank ] );
							rankDot.appendChild( document.createTextNode( "·" ) );
							rankDot.title = a.rankName;
							statement.node.parentNode.insertBefore( 
								changeBlock( i, rankDot ),
								statement.node
							);
						} );
					}
				} );
				
				if ( !isWD ) {
					$( chunk ).find( 'a[ href ]:not( [ href ^= "http" ] )' ).each( function () {
						this.href = wdDomain + this.getAttribute( 'href' );
					} );
				}
				
				chunkDetails.push( change.details = { chunk, type: msgArray[ typeIndexes.property ], data: i, change } );
			} );
		}
		
		function addChunkDetails( chunkDetails, diffLinkParent, nestedCDList ) {
			
			// TODO: Reuse summary nodes in nesteds where this'll run multiple times.
			var summaryNode = document.createElement( "span" ),
				bitmarker = diffLinkParent.querySelector( ".mw-plusminus-pos, .mw-plusminus-neg, .mw-plusminus-null" ),
				oldComment = diffLinkParent.querySelector( ".comment" ),
				beforePoint,
				oldSummary;
			
			// For parents of nested diffs, add new chunks to old summary.
			if ( nestedCDList ) {
				oldSummary = nestedCDList.length !== 0;
				for ( var i = 0; i < nestedCDList.length + 1; i++ ) {
					if ( i === nestedCDList.length ) {
						// At the end. None found.
						nestedCDList.push( chunkDetails );
						if ( i > 0 ) {
							summaryNode = nestedCDList[ 0 ][ 0 ].wrapper.parentNode;
						}
						break;
					} else if ( chunkDetails.timestamp < nestedCDList[ i ].timestamp ) {
						if ( !nestedCDList[ i ][ 0 ] ) {
							// This shouldn't happen.
							// Might be related to redirects.
							//console.log( 434, chunkDetails, nestedCDList, i );
							break;
						}
						// Found a place to put new diffs.
						beforePoint = nestedCDList[ i ][ 0 ].wrapper;
						nestedCDList.splice( i, 0, chunkDetails );
						//console.log( 'beforePoint=', beforePoint );
						break;
					}
				}
			}
			
			chunkDetails.forEach( function ( details, i ) {
				var chunk = details.chunk,
					wrapper = details.wrapper = document.createElement( "span" );
				wrapper.className = "YR-cl-summary";
				i === 0 && !oldSummary && summaryNode.appendChild( document.createTextNode( " - " ) );
				if ( beforePoint ) {
					beforePoint.parentNode.insertBefore( wrapper, beforePoint );
				} else {
					summaryNode.appendChild( wrapper );
				}
				wrapper.appendChild( chunk );
			} );
			
			if ( !nestedCDList || nestedCDList.length === 1 ) {
				// Fiddle with the DOM, remove bit markers
				if ( bitmarker ) {
					bitmarker.parentNode.removeChild( bitmarker.previousSibling );
					bitmarker.parentNode.removeChild( bitmarker.nextSibling );
					bitmarker.parentNode.removeChild( bitmarker );
				}
				// Replace old summary with new summary.
				if ( oldComment ) {
					diffLinkParent.insertBefore( summaryNode, oldComment );
					if ( oldComment.querySelector( ".autocomment" ) ) {
						// This should leave things like "#autolist2", if possible.
						for ( ; oldComment.firstChild; ) {
							oldComment.removeChild( oldComment.firstChild );
						}
						//oldComment.parentNode.removeChild( oldComment );
					}
				} else {
					diffLinkParent.appendChild( summaryNode );
				}
			}
		}
		
		function processDiff( diffLinkParent, oldid, pageid, diffid, title ) {
			
			api.get( {
				action: "query",
				prop: "revisions",
				//rvprop: "timestamp|user|comment",
				//pageids: pageid,
				titles: title,
				rvstartid: oldid,
				//rvdiffto: "next",
				rvdiffto: diffid,
				rvlimit: 1,
				uselang: lang, 
				maxage:  60 * 60 * 24 * 3,
				smaxage: 60 * 60 * 24 * 3
			} ).done( function ( result ) {
				if ( !result || !result.query || !result.query.pages || result.query.pages[ -1 ] ) {
					console.error( 'DiffLists: Diff not found.', result );
					return;
				}
				
				// TODO: getStuff stuff.
				// TODO: Figure out blank changes.
				// TODO: Grab localizations for everything.
				// 
				for ( var pageid in result.query.pages );
				result = result.query.pages[ pageid ];
				var revision = result.revisions[ 0 ],
					$x = $( revision.diff[ "*" ] ),
					timestamp = revision.timestamp,
					allChanges = [ {}, {}, {}, {}, {} ],
					chunkDetails = [],
					// Get the element that holds the change, for hiding purposes. Three different situations:
					// Regular watchlist, enhanced watchlist, and enhanced nested.
					// Enhanced lists are lists of tables, the first row of each being main, others nested.
					// Currently, hiding the parent hides the subs. Not sure if that's the best behaviour.
					enhanced = diffLinkParent.nodeName === "TD",
					nested = enhanced && diffLinkParent.className === "mw-enhanced-rc-nested",
					outer = enhanced ? 
						nested ? 
							diffLinkParent.parentNode : 
							diffLinkParent.parentNode.parentNode.parentNode : 
						diffLinkParent,
					diff = { elem: outer, chunkDetails, allChanges };
				allDiffs.push( diff );
				
				// Build allChanges from result data.
				$x.each( function ( trIndex, tr ) {
					if ( tr.nodeName === "TR" && tr.nextSibling && tr.childNodes.length === 2 && tr.firstChild.className !== "diff-marker" ) {
						// We're dealing with a heading line
						var node = tr.firstChild.firstChild ? tr.firstChild : tr.lastChild,
							propData = node.textContent.split( " / " ),
							//mainType = propData[ 0 ],
							mainType = msgArray.indexOf( propData[ 0 ] ),
							trValueBlock = tr.nextSibling,
							valueTds = {
								removed: trValueBlock.firstChild.className === "diff-marker" && trValueBlock.childNodes[ 1 ],
								added: trValueBlock.lastChild.className === "diff-addedline" && trValueBlock.lastChild
							};

						switch ( mainType ) {
							case typeIndexes.property:
								// Bunch of different situations here:
								// If a brand new statement, highlight all.
								// If removing full statement, highlight and strike all.
								// If changing a statement, don't highlight prop, highlight values.
								// If just adding/removing/changing qualifiers/sources, don't highlight values either.
								// Mixing these has complex results.
								// Note: Depending on data type, value might not be a link.

								// Overall structure: allChanges = [ TODO ]
								var propLink = node.firstChild.nextSibling,
									prop = propLink.title,
									propChangeGroup = 
										( allChanges[ mainType ][ prop ] = allChanges[ mainType ][ prop ] || { values: {}, link: propLink } );
								if ( !propLink ) {
									//console.log( "no proplink", node );
								}
								$.each( valueTds, function ( changeType, td ) {
									var valNode;
									if ( !td.firstChild || !td.firstChild.firstChild ) {
										//console.log( "break", td );
										return;
									}
									$( [ node, td ] ).find( ".wb-language-fallback-indicator" ).remove();
									// Assuming that only two nodes means it's a simple statement.
									if ( node.childNodes.length === 2 ) {
										// Simple statement
										var val = td.firstChild.firstChild.firstChild,
											valName,
											valGroup;
										if ( val.firstChild && val.firstChild.nodeName === "A" ) {
											// We have a straight link.
											valNode = val.firstChild;
											// Images and whatnot don't have titles.
											valName = valNode.title || valNode.textContent;
										} else if ( val.firstChild && val.firstChild.nodeName === "H4" ) { 
											// Table filled with stuff, probably quantity.
											valName = val.firstChild.textContent;
											valNode = document.createElement( "span" )
												.appendChild( document.createTextNode( valName ) )
												.parentNode;
										} else {
											// Straight string
											valName = val.textContent;
											valNode = val;
										}
										valGroup = propChangeGroup.values[ valName ] = 
											propChangeGroup.values[ valName ] || {};
										valGroup.change = changeType;
									} else {
										// Qualifier, Source, or Rank statement
										// The value is sometimes a string, sometimes a link.
										var subtype = propData[ propData.length - 1 ],
											hasValueLink = node.childNodes[ 3 ] && node.childNodes[ 3 ].nodeName === "A",
											valName = hasValueLink ?
												node.childNodes[ 3 ].title || node.childNodes[ 3 ].textContent :
												propData[ 1 ].split( ": " )[ 1 ],
											valGroup = propChangeGroup.values[ valName ] = 
												propChangeGroup.values[ valName ] || {};

										valNode = hasValueLink ? 
											node.childNodes[ 3 ] : 
											document.createTextNode( valName );

										function getStuff( t ) {
											// Get the value of the qualifier/reference part.
											// There are 3 options: ": <a>content</a>", ": <h4>content</h4><table />", and ": content"
											// Only copy the element itself if it's a link.
											// Occasionally there's actually a span element indicating an error.
											// If so, try for the previous element.
											return t.nodeName === "A" ? 
												t : 
												document.createTextNode( 
													( t.nodeName === "TABLE" || t.nodeName === "SPAN" ) ? 
														t.previousSibling.textContent : 
														// This breaks sometimes. TODO.
														t.nodeValue.substr( 2 )
														//t.nodeValue.split( ":" )[ 1 ]
												);
										}

										// TODO: Try to merge these, if possible.
										switch ( subtype ) {
											case msgs.reference:
												// A statement can have any number of references.
												// References have a list of statements, but are one block each.
												var subGroup = valGroup.references = valGroup.references || [],
													ref = [];
												td && subGroup.push( ref );
												ref.change = changeType;
												// Series of spans, separated by brs.
												$( ">div>.diffchange>span", td ).each( function () {
													ref.push( {
														propLink: this.firstChild,
														value: getStuff( this.lastChild )
													} );
												} );

												break;
											case msgs.qualifier:
												// Should this be an array or object containing arrays? There can be multiple
												// qualifiers of the same type, +added/removed values, ...
												// Still, changed values need to be shown...
												var subGroup = valGroup.qualifiers = valGroup.qualifiers || {},
													qualSpan = td.firstChild.firstChild.firstChild,
													// The value will always start with a link to the property.
													subPropLink = qualSpan.firstChild,
													subPropName = subPropLink.title,
													qualList = subGroup[ subPropName ] = subGroup[ subPropName ] || [],
													qual = {};
												qualList.push( qual );
												qualList.link = subPropLink;
												qual.change = changeType;
												qual.value = getStuff( qualSpan.lastChild );

												// Clear duplicates
												qualList.forEach( function ( a, i ) {
													if ( ( a.value.title || a.value.nodeValue ) === ( qual.value.title || qual.value.nodeValue ) && a.change !== qual.change ) {
														qualList.splice( qualList.length - 1, 1 );
														qualList.splice( i, 1 );
														if ( qualList.length === 0 ) {
															delete subGroup[ subPropName ];
														}
														return false;
													}
												} );
												// After, there's a ": ", but I don't know what happens to strings.
												break;
											case msgs.rank:
												var subGroup = valGroup.rank = valGroup.rank || {},
													rankSpan = td.firstChild.firstChild.firstChild,
													rankName = rankSpan.textContent,
													rank = ( [ 
														msgs[ "wikibase-diffview-rank-preferred" ],
														msgs[ "wikibase-diffview-rank-normal" ],
														msgs[ "wikibase-diffview-rank-deprecated" ]
													] ).indexOf( rankName );
												
												subGroup[ changeType ] = { rank, rankName };
												break;
										}
									}
									valGroup.node = valGroup.node || valNode;
								} );
								break;
							case typeIndexes.label:
							case typeIndexes.description:
							case typeIndexes.link:
							case typeIndexes.alias:
								var langOrWiki = propData[ 1 ],
									isAlias = mainType === typeIndexes.alias,
									newBlock = allChanges[ mainType ][ langOrWiki ] = 
										allChanges[ mainType ][ langOrWiki ] || ( isAlias ? [] : {} ),
									isBadge = propData[ 2 ] === "badges"; // "badges" for some reason isn't translated.
								$.each( valueTds, function ( i, td ) {
									if ( !td.firstChild || !td.firstChild.firstChild ) {
										//console.log( 777, td, diffLink.parentNode, $x );
									}
									if ( td ) {
										if ( !isBadge ) {
											// Normal label, description, alias, or sitelink
											if ( td ) {
												if ( isAlias ) {
													newBlock.push( newBlock = {} );
												}
												newBlock[ i ] = td.firstChild.firstChild.firstChild;
											}
										} else {
											// Badge.
											newBlock.badges = newBlock.badges || [];
											var newBadge = {};
											newBadge[ i ] = td.firstChild.firstChild.firstChild;
											newBlock.badges.push( newBadge );
										}
									}
								} );
								break;
						}
					}
				} );
				
				// Use allChanges to build chunkDetails, array of DOM to be added.
				buildChunkDetails( allChanges, chunkDetails );
				
				// 
				if ( nested ) {
					//var dupChunkDetails = ( chunkDetails.map( x => ( { chunk: x.chunk.cloneNode( true ), data: x.data, type: x.type, wrapper: x.wrapper, change: x.change } ) ) ),
					var dupChunkDetails = ( chunkDetails.map( x => ( $.extend( {}, x, { chunk: x.chunk.cloneNode( true ) } ) ) ) ),
						// These lines are the Map way of saying nP[ tb ] = p = nP[ tb ] || { ... };
						nesteds,
						tbody = outer.parentNode,
						cdList;
					/*
					nestedParents.set( tbody, ( nesteds = 
						nestedParents.get( tbody ) || 
						{ chunkDetails: [], allChanges: [ {}, {}, {}, {}, {} ], elem: tbody, chunkDetailsList: [] } 
					) );
					*/
					if ( !( nesteds = nestedParents.get( tbody ) ) ) {
						nesteds = { chunkDetails: [], allChanges: [ {}, {}, {}, {}, {} ], elem: tbody, chunkDetailsList: [] };
						nestedParents.set( tbody, nesteds );
						allDiffs.push( nesteds );
					}
					
					cdList = nesteds.chunkDetailsList;
					dupChunkDetails.timestamp = timestamp;
					
					nesteds.chunkDetails.push( ...dupChunkDetails );
					nesteds.allChanges.forEach( ( x, i ) => $.extend( x, allChanges[ i ] ) );
					//p.chunkDetailsList.push( chunkDetails );
					//console.log( nestedParents, p, p.chunkDetails, outer.parentNode.firstElementChild.lastElementChild );
					
					addChunkDetails( dupChunkDetails, tbody.firstElementChild.lastElementChild, cdList );
					
					if ( simpleSettings ) {
						updateDisplay( [ nesteds ] );
					}
				}
				
				// Add to the visible DOM.
				addChunkDetails( chunkDetails, diffLinkParent );
				
				if ( simpleSettings ) {
					updateDisplay( [ diff ] );
				}

			} );
		}
		
		// Run through every diff link.
		$( changeSelector ).each( function( i, diffLink ) {
			// The diff API will only give one at a time...
			var href = diffLink.href,
				oldid = mw.util.getParamValue( "oldid", href ), //result.old_revid,
				pageid = mw.util.getParamValue( "curid", href ),
				diffid = mw.util.getParamValue( "diff", href ),
				title = mw.util.getParamValue( "title", href ),
				diffLinkParent = listType === 'history' ? 
					diffLink.parentNode.parentNode : 
					diffLink.parentNode,
				enhanced = diffLinkParent.nodeName === "TD";
			
			if ( 
				// Only well-formed diff links
				diffid && title && oldid && 
				// On history pages, don't show earliest diff.
				// Don't show earliest diff on page, even if there's a diff
				// link, because rvstartid doesn't play nice with "prev". 
				// TODO: Fix.
				( listType !== "history" || diffLinkParent.nextElementSibling /* || diffLink.previousSibling.previousSibling */ ) &&
				// Check namespace, only show mainspace and Property: NS.
				( !title.includes( ":" ) || title.startsWith( "Property:" ) ) &&
				// Suppress diffs with nested sub-listings.
				( !enhanced || !diffLinkParent.parentNode.nextElementSibling || diffLinkParent.parentNode.previousElementSibling )
			) {
				processDiff( diffLinkParent, oldid, pageid, diffid, title );
			}
		} );
		
		
		buildOptionsBox();
		
	});
});

}


mw.loader.using( mw.config.get( 'wgDBname' ) === 'wikidatawiki' ? 'mediawiki.api' : 'mediawiki.ForeignApi', DiffLists );

		/*
		$.get( api, { 
			format: "json", 
			action: "query",
			list: "recentchanges",
			rcnamespace: 0,
			rcprop: "ids"
		}, function ( r ) {
			//console.log( r );
			//var results = r.query.recentchanges; // array
			results.forEach( function ( result ) {
				var oldid = result.old_revid,
					pageid = result.pageid;
		*/