User:Inductiveload/popups reloaded.js

/** * Re-imagining of the Navigation popups gadgets. * Principally, a simple way to append hooks for actions and content is * hoped for so it's easy to "plug in" extra functions. * * It also listens for page mutations and adds handlers to links added by * JS, which can be helpful. * * There are three types of "hooks" you can register: * * recogniser hook: work out the "canonical" form of a link * * content hook: displays some content relating to a link * * action hooks: adds to the actions related to a link */

// IE11 users have to git gud /* eslint-disable no-var, compat/compat */

( function ( $, mw, Promise ) {

'use strict';

var Popups = { cfg: { recogniserHooks: [], contentHooks: [], actionHooks: [], skinDenylist: [ 'minerva' ], userContribsByDefault: true, showTimeout: 500, imgWidth: 400, rcTypes: [ 'edit', 'new', 'log' ], watchlistTypes: [ 'edit', 'new', 'log' ], // can add categorize // how many log events (RC, watchlist, contribs..) to show logLimit: { default: 25 },			// how many links to show linkLimit: { default: 25 },			// namespaces to show shortcut links for in contribs actions nsLinkList: { default: [ 0, 1, 2, 3, 4, 5, 8, 10, 12, 14, 100, 102, 103, 104, 106, 114, 828 ] },			timeOffset: 0, // TODO icons: { copy: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/Ic_content_copy_48px.svg/16px-Ic_content_copy_48px.svg.png', edit: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/Ic_create_48px.svg/16px-Ic_create_48px.svg.png' },			// Provide these special block types blocks: [ {					asAction: true, name: 'spam', expiry: 'indefinite', message: 'popups-block-spam-reason', allowusertalk: true, watchuser: true }			],			// provide these special delete types deletes: [ {					asAction: true, name: 'spam', message: 'popups-delete-spam-reason', watchlist: 'watch' }			]		},		userRights: [], // cache of siteinfo siteinfo: [] };

// Set default messages - users/wikis can override mw.messages.set( {		'popups-block-confirm': 'Are you sure you want to block $1 with reason "$2"?',		'popups-user-blocked': '$1 blocked successfully',		'popups-user-block-failed': 'Failed to block $1',		'popups-block-spam-reason': 'Spamming/Promoting links to external sites',

'popups-delete-ok': '$1 deleted successfully', 'popups-delete-failed': 'Failed to delete $1', 'popups-delete-confirm': 'Are you sure you want to delete $1 with reason "$2"?', 'popups-delete-spam-reason': 'Spamming/Promoting links to external sites',

'popups-actions-delete': 'del', 'popups-action-mass-delete': 'nuke',

'popups-actions-move': 'move', 'popups-actions-info': 'info',

'popups-diff-template': '' } );

// Wikis/users can use this hook to set their own messages mw.hook( 'ext.gadget.popups-reloaded.messages' ).fire;

// returns promise with boolean "confirmed" function confirmAction( message ) { return OO.ui.confirm( message ) .then( function ( confirmed ) {				if ( !confirmed ) {					return Promise.reject( confirmed );				}

return confirmed; } );	}

Popups.wiki = { server: mw.config.get( 'wgServer' ), serverName: mw.config.get( 'wgServerName' ), indexLink: mw.config.get( 'wgServer' ) + mw.config.get( 'wgScript' ) };

Popups.commons = { server: '//commons.wikimedia.org', serverName: 'commons.wikimedia.org', indexLink: '//commons.wikimedia.org/w/index.php', apiLink: '//commons.wikimedia.org/w/api.php' };

Popups.wikidata = { server: '//www.wikidata.org', serverName: 'www.wikidata.org', indexLink: '//www.wikidata.org/w/index.php' };

/* these wikis can be used in ForeignApi */ Popups.foreignWikis = [ Popups.commons.serverName, Popups.wikidata.serverName, 'wikipedia.org', 'wikisource.org', 'wikibooks.org', 'wikispecies.org', 'wikiquote.org', 'wiktionary.org', 'wikivoyage.org', 'wikiversity.org', 'wikinews.org', 'mediawiki.org', 'meta.wikimedia.org', 'foundation.wikimedia.org' ];

function haveRight( right ) { return Popups.userRights.indexOf( right ) !== -1; }

function recognisedWikiServer( serverName ) { // first, check the current wiki if ( serverName === Popups.wiki.serverName ) { return true; }

for ( var i = 0; i < Popups.foreignWikis.length; ++i ) { if ( serverName.endsWith( Popups.foreignWikis[ i ] ) ) { return true; }		}

return false; }

function getWikiArticleUrl( serverName, title ) { return '//' + serverName + '/wiki/' + title; }

function getWikiActionUrl( serverName, title, action, params ) { var url = '//' + serverName + '/w/index.php?title=' + encodeURIComponent( title ); if ( action ) { url += '&action=' + action; }

if ( params ) { Object.keys( params ).forEach( function ( key ) {				url += '&' + key + '=' + params[ key ];			} ); }		return url; }

function getArticleUrl( article, encode ) { var enc = encode ? encodeURIComponent : function ( x ) { return x;		}; return mw.config.get( 'wgArticlePath' ).replace( '$1', enc( article ) ); }

function getActionUrl( pg, action, params ) { return getWikiActionUrl( Popups.wiki.serverName, pg, action, params ); }

function getHrefLink( href, text ) { text = text || href; return '' + text + ''; }

function getPageLink( serverName, title, text ) { text = text || title; return '' + text + ''; }

function getActionLink( serverName, pg, action, text ) { return getHrefLink( getWikiActionUrl( serverName, pg, action ), text ); }

function getRevidLink( serverName, title, rev, text ) { var href = getWikiActionUrl( serverName, title, null,			{ oldid: rev.revid } ); return getHrefLink( href, text ); }

function getDiffUrl( serverName, title, ida, idb ) { return getWikiActionUrl( serverName, title, null,			{ diff: idb, oldid: ida } ); }

function getDiffLink( serverName, title, ida, idb, text ) { return getHrefLink( getDiffUrl( serverName, title, ida, idb ), text ); }

function getUserLink( serverName, user ) { user = user.replace( /^[uU]ser:/, '' ); return getPageLink( serverName, 'User:' + user, user ); }

function getCommonsArticleUrl( filename ) { return Popups.commons.server + '/wiki/' + filename; }

function getCommonsActionUrl( filename, action ) { return Popups.commons.indexLink + '?title=' + filename + '&action=' + action; }

function getCommonsOrLocalIndexPhp( local ) { return local ? Popups.wiki.indexLink : Popups.commons.indexLink; }

function getReuploadUrl( filename, local ) { return getCommonsOrLocalIndexPhp( local ) + '?title=Special:Upload' + '&wpDestFile=' + filename + '&wpForReUpload=1'; }

function isNamespaceOrTalk( candidate, wanted ) { return candidate === wanted || ( candidate === wanted + ' talk' ); }

var getSpecialDiffLink = function ( revid ) { // eslint-disable-next-line no-useless-concat return '[' + '[Special:Diff/' + revid.toString + '|' + revid.toString + ']]'; };

var getDatetimeFromTimestamp = function ( ts ) { return ts.replace( 'T', ' ' ).replace( 'Z', '' ); // 2018-12-18T16:59:42Z"	};

function bracketedLinkList( links ) { var o = ' (' + links[ 0 ];

for ( var i = 1; i < links.length; i++ ) { o += ' | ' + links[ i ]; }		o += ') ';		return o;	}

function repeatString( s, mult ) { var ret = ''; for ( var i = 0; i < mult; ++i ) { ret += s;		} return ret; }

function zeroFill( s, min ) { min = min || 2; var t = s.toString; return repeatString( '0', min - t.length ) + t;	}

function mapArray( f, o ) { var ret = []; for ( var i = 0; i < o.length; ++i ) { ret.push( f( o[ i ] ) ); }		return ret; }

function mapObject( f, o ) { var ret = {}; for ( var i in o ) { ret[ o ] = f( o[ i ] ); }		return ret; }

function map( f, o ) { if ( Array.isArray( o ) ) { return mapArray( f, o ); }		return mapObject( f, o ); }

function getDateFromTimestamp( t ) { var s = t.split( /[^0-9]/ ); switch ( s.length ) { case 0: return null; case 1: return new Date( s[ 0 ] ); case 2: return new Date( s[ 0 ], s[ 1 ] - 1 ); case 3: return new Date( s[ 0 ], s[ 1 ] - 1, s[ 2 ] ); case 4: return new Date( s[ 0 ], s[ 1 ] - 1, s[ 2 ], s[ 3 ] ); case 5: return new Date( s[ 0 ], s[ 1 ] - 1, s[ 2 ], s[ 3 ], s[ 4 ] ); case 6: return new Date( s[ 0 ], s[ 1 ] - 1, s[ 2 ], s[ 3 ], s[ 4 ], s[ 5 ] ); default: return new Date( s[ 0 ], s[ 1 ] - 1, s[ 2 ], s[ 3 ], s[ 4 ], s[ 5 ], s[ 6 ] ); }	}

function adjustDate( d, offset ) { // offset is in minutes var o = offset * 60 * 1000; return new Date( +d + o ); }

function dayFormat( editDate, utc ) { if ( utc ) { return map( zeroFill, [ editDate.getUTCFullYear, editDate.getUTCMonth + 1, editDate.getUTCDate ] ).join( '-' ); }		return map( zeroFill, [ editDate.getFullYear, editDate.getMonth + 1, editDate.getDate ] ).join( '-' ); }

function timeFormat( editDate, utc ) { if ( utc ) { return map( zeroFill, [ editDate.getUTCHours, editDate.getUTCMinutes, editDate.getUTCSeconds ] ).join( ':' ); }		return map( zeroFill, [ editDate.getHours, editDate.getMinutes, editDate.getSeconds ] ).join( ':' ); }

function makeClassedSpan( cls, text ) { // eslint-disable-next-line mediawiki/class-doc return $( ' ' ) .addClass( cls ) .append( text ); }

function makeColspanTableRow( classes, colspan, content ) { return ' " + content + ' '; }

function makeOpenTag( tag, cls ) { if ( cls ) { return '<' + tag + " class='" + cls + "'>"; } else { return '<' + tag + '>'; }	}

function makeEnclosingTag( tag, text, cls ) { var o = '<' + tag; if ( cls ) { o += " class='" + cls + "'"; }		o += '>' + ( text || '' ) + ''; return o;	}

function arraysIntersect( a, b ) { return a.some( function ( v ) {			return b.indexOf( v ) !== -1;		} ); }

function Api( serverName ) { this.serverName = serverName;

if ( serverName === mw.config.get( 'wgServerName' ) ) { this.api = new mw.Api; } else { this.api = new mw.ForeignApi( '//' + serverName + '/w/api.php' ); }	}

Api.prototype.defaultFormat = function ( params ) { if ( !params.format ) { params.format = 'json'; params.formatversion = 2; }		return params; };

Api.prototype.get = function ( params, commonsFallback ) { params = this.defaultFormat( params );

return this.api.get( params ) .then( function ( data ) {				return data;			},			function ( data ) {				if ( commonsFallback && data === 'missingtitle' ) {					return new mw.ForeignApi( Popups.commons.apiLink )						.get( params );				}				throw ( data );			} ); };

Api.prototype.post = function ( params ) { params = this.defaultFormat( params ); return this.api.post( params ); };

Api.prototype.postWithToken = function ( token, params ) { params = this.defaultFormat( params ); return this.api.postWithToken( token, params ); };

Api.prototype.getSiteInfo = function ( siProps ) { const params = { action: 'query', meta: 'siteinfo', siprop: siProps.join( '|' ) };

return this.api.get( params ) .then( ( data ) => data.query ); };

Api.prototype.getSections = function ( pageTitle ) { const params = { action: 'parse', page: pageTitle, prop: 'sections' };

return this.api.get( params ) .then( ( data ) => data.parse.sections ); };

Api.prototype.getSectionWithTitle = function ( pageTitle, secTitle ) {

return this.getSections( pageTitle ) .then( ( sections ) => {

for ( const section of sections ) { if ( section.line === secTitle || section.anchor === secTitle ) { return section; }				}

throw `No section ${secTitle} at ${pageTitle}`; } );	};

Api.prototype.getPageRender = function ( title, section ) { const params = { action: 'parse', page: title, section: section };

return this.get( params ) .then( function ( data ) {				return data.parse;			} ); };

Api.prototype.getPageImageUrl = function ( filename, page, size ) { var params = { action: 'query', titles: 'File:' + filename, prop: 'imageinfo', iiprop: 'url' };

if ( size && page ) { params.iiurlparam = 'page' + page + '-' + size + 'px'; }

return this.get( params ); };

Api.prototype.getPageThumbUrlForTitle = function ( titleNoNs, size ) { // load page image var fn = titleNoNs.replace( /\/\d+$/, '' ); var pg = titleNoNs.replace( /^.*\/(\d+)$/, '$1' );

return this.getPageImageUrl( fn, pg, size ) .then( function ( data ) {				return data.query.pages[ 0 ].imageinfo[ 0 ].thumburl;			} ); };

Api.prototype.getPageWikitext = function ( title, oldid ) { var slot = 'main'; var params = { action: 'query', prop: 'revisions', rvslots: slot, rvprop: 'content', titles: title, rvlimit: 1 };

if ( oldid ) { params.rvstartid = oldid; params.rvendid = oldid; }

return this.get( params ) .then( function ( data ) {				return data.query.pages[ 0 ].revisions[ 0 ].slots[ slot ].content;			} ); };

/* resolves with true or false */ Api.prototype.ifPageExists = function ( title ) { var params = { action: 'query', titles: title };		return this.get( params ) .then( function ( data ) {				return !data.query.pages[ 0 ].missing;			} ); };

Api.prototype.getOldRevision = function ( oldid ) { var params = { action: 'parse', oldid: oldid, prop: 'text' };

return this.get( params ) .then( function ( data ) {				var $content = $( ' ' )					.addClass( 'popups_page_content popups_oldrev_content' );

var oldcontent = data.parse.text; $content.append( oldcontent ); return $content; } );	};

Api.prototype.getPageHistory = function ( title, limit, rvprops ) { var params = { action: 'query', titles: title, prop: 'revisions', rvprop: rvprops.join( '|' ), rvlimit: limit };		return this.get( params ) .then( function ( data ) {				return data.query.pages[ 0 ];			} ); };

Api.prototype.getCurrentRevs = function ( titles ) { var params = { action: 'query', prop: 'revisions', titles: Array.isArray( titles ) ? titles.join( '|' ) : titles };

return this.get( params ) .then( function ( data ) {				return data.query.pages.map( function ( p ) { return p.revisions[ 0 ]; } );			} );	};

/*	 * Get what we need to undo a single edit *	 * Returns a promise that resolves with the 'compare' data. */	Api.prototype.getSingleEditCompare = function ( revision, props ) {

var params = { action: 'compare', fromrev: revision, torelative: 'prev', prop: props.join( '|' ) };

return this.postWithToken( 'csrf', params ) .then( function ( data ) {				return data.compare;			} ); };

Api.prototype.rollbackEdit = function ( toId, toUser ) { var params = { action: 'rollback', pageid: toId, user: toUser };

return this.postWithToken( 'rollback', params ); };

Api.prototype.patrolRevision = function ( revid, tags ) { const params = { action: 'patrol', revid: revid };

if ( tags ) { params.tags = tags.join( '|' ); }

return this.postWithToken( 'patrol', params ); };

Api.prototype.thankForRevision = function ( revision ) { var params = { action: 'thank', rev: revision };		return this .postWithToken( 'csrf', params ); };

Api.prototype.getWhatLeavesHere = function ( title, props, limit ) { var params = { action: 'query', titles: title, prop: props.join( '|' ), tllimit: limit, imlimit: limit, pllimit: limit };

return this.get( params ) .then( function ( data ) {				return data.query.pages[ 0 ];			} ); };

Api.prototype.getImageInfo = function ( image, props ) { var params = { action: 'parse', page: image, prop: props.join( '|' ) };

return this.get( params, true ) .then( function ( data ) {				return data.parse.text;			} ); };

Api.prototype.getRecentChanges = function ( title, options ) { const params = { action: 'query', list: 'recentchanges', rctitle: title, rcprop: options.props ? options.props.join( '|' ) : undefined, rctype: options.types ? options.types.join( '|' ) : undefined, rcnamespace: options.ns ? options.ns.join( '|' ) : undefined, rclimit: options.limit };

return this.get( params ) .then( function ( data ) {				return data.query.recentchanges;			} ); };

Api.prototype.getWatchlistEntries = function ( excudeUsers, wlProps, wlTypes,		wlNamespaces, limit ) { const params = { action: 'query', list: 'watchlist', wlexcludeuser: excudeUsers, wlprop: wlProps.join( '|' ), wltype: wlTypes.join( '|' ), wllimit: limit, wlallrev: 1 };

if ( wlNamespaces && wlNamespaces.length ) { params.wlnamespace = wlNamespaces.join( '|' ); }

return this.get( params ) .then( function ( data ) {				return data.query.watchlist;			} ); };

Api.prototype.getCategoryMembers = function ( category, limit ) { var params = { action: 'query', list: 'categorymembers', cmtitle: category, cmlimit: limit };

return this.get( params ) .then( function ( data ) {				return data.query.categorymembers;			} ); };

Api.prototype.getRandomPages = function ( namespaces, limit ) { var params = { action: 'query', list: 'random', rnlimit: limit };		if ( namespaces ) { params.rnnamespace = namespaces.join( '|' ); }

return this.get( params ) .then( function ( data ) {				return data.query.random;			} ); };

Api.prototype.getPageInfo = function ( title, inprops ) {

var params = { action: 'query', titles: title, prop: 'info', inprop: inprops.join( '|' ) };

var ok = function ( data ) { return data.query.pages[ 0 ]; };

var fail = function { console.error( 'GET Failed: for pageinfo: ', title ); };

return this.get( params ) .then( ok, fail ); };

Api.prototype.getUserContributions = function ( user, ucprops, namespace, limit ) {

var params = { action: 'query', list: 'usercontribs', ucprop: ucprops.join( '|' ), ucuser: user, uclimit: limit, ucnamespace: namespace };

return this.get( params ) .then( function ( data ) {				return data.query.usercontribs;			},			function {				console.error( 'GET Failed: for usercontribs: ', user );			} ); };

Api.prototype.getUserLog = function ( user, leprops, limit ) {

var params = { action: 'query', list: 'logevents', leuser: user, leprop: leprops.join( '|' ), lelimit: limit };

return this.get( params ) .then( function ( data ) {				return data.query.logevents;			} ); };

// Type: all, new, overwrite, revert Api.prototype.getUserFiles = function ( user, type, limit ) { const params = { action: 'query', list: 'logevents', leuser: user, lelimit: limit };

switch ( type ) { case 'all': params.letype = 'upload'; break; case 'new': params.leaction = 'upload/upload'; break; case 'overwrite': params.leaction = 'upload/overwrite'; break; case 'revert': params.leaction = 'upload/revert'; break; }

return this.get( params ) .then( function ( data ) {				return data.query.logevents;			} ); };

Api.prototype.getUserAbuseFilterLog = function ( user, aflprops, limit ) {

var params = { action: 'query', list: 'abuselog', afluser: user, aflprop: aflprops.join( '|' ), afllimit: limit };

return this.get( params ) .then( function ( data ) {				return data.query.abuselog;			} ); };

Api.prototype.getAbuseFilterLogEntry = function ( aflentry ) { const params = { action: 'query', list: 'abuselog', afllogid: aflentry, aflprop: 'ids|details|filter' };

return this.get( params ) .then( ( data ) => {				return data.query.abuselog[ 0 ];			} ); };

Api.prototype.getWhatLinksHere = function ( title, types, limit ) { var params = { action: 'query', list: types.join( '|' ), bltitle: title, bllimit: limit, eititle: title, eilimit: limit };

return this.get( params ) .then( function ( data ) {				return data.query;			} ); };

Api.prototype.doPurgePage = function ( page ) { var params = { action: 'purge', titles: page, forcerecursivelinkupdate: 1, redirects: 1 };

return this.post( params ); };

/* Returns a promise that resolves with image info */ Api.prototype.getFileImageinfo = function ( filename, iiprops ) { var params = { action: 'query', formatversion: 2, titles: 'File:' + filename, prop: 'imageinfo' };

if ( iiprops && iiprops.length ) { params.iiprop = iiprops.join( '|' ); }

return this.get( params ) .then( function ( data ) {				return data.query.pages[ 0 ];			} ); };

Api.prototype.getFileIsLocal = function ( filename ) { return this .getFileImageinfo( filename, null ) .then( function ( imageinfo ) {				return imageinfo.imagerepository !== 'shared';			} ); };

Api.prototype.watchPage = function ( title, watch ) { var params = { action: 'watch', titles: title };		if ( !watch ) { params.unwatch = 1; }

return this.postWithToken( 'watch', params ); };

Api.prototype.blockUser = function ( user, expiry, options ) { var params = { action: 'block', expiry: expiry, user: user, reason: options.reason, watchuser: options.watchuser, allowusertalk: options.allowusertalk };

return this.postWithToken( 'csrf', params ); };

Api.prototype.delete = function ( page, reason, watchPage ) { const params = { action: 'delete', title: page, reason: reason, watchlist: watchPage };

return this.postWithToken( 'csrf', params ); };

/*	 * Generic cache object, contains more specific cache objects *	 * The main ones are: * * link: data relating to a raw HTML link * * wiki: data for a link to a wiki *	 * Scoring functions can add their own data for use in the hook later on. */	function Cache { }

function oddEvenText( i ) { return ( i % 2 ) ? 'odd' : 'even'; }

function makeTableWithCols( tableCls, colCls ) { /* eslint-disable mediawiki/class-doc */ var $cg = $( ' ' ); var $tbody = $( ' ' ).append( $cg ); var $table = $( ' ' ).addClass( tableCls ) .append( $cg, $tbody );

for ( var i = 0; i < colCls.length; i++ ) { var $col = $( ' ' ); if ( colCls[ i ] ) { $col.addClass( colCls[ i ] ); }			$cg.append( $col ); }		return $table; /* eslint-enable mediawiki/class-doc */ }

function makeDailyEventTableRows( items, cols, timestampFxn, rowFxn ) { var day = null; var timeOffset = 0;

var trs = '';

for ( var i = 0; i < items.length; i++ ) { var timestamp = timestampFxn( items[ i ] ); var editDate = adjustDate( getDateFromTimestamp( timestamp ), timeOffset );

var thisDay = dayFormat( editDate ); var thisTime = timeFormat( editDate ); if ( thisDay === day ) { thisDay = ''; } else { day = thisDay; }

// day header if ( thisDay ) { trs += makeColspanTableRow( 'popups_date', cols, thisDay ); }

var cells = rowFxn( items[ i ], thisTime );

trs += "";

for ( var d = 0; d < cells.length; d++ ) { trs += ' ' + cells[ d ] + ' '; }			trs += ' '; }		return trs; }

/*	 * Copy to clipboard */	function copyToClipboard( value ) { var tempInput = document.createElement( 'input' ); tempInput.style = 'position: absolute; left: -1000px; top: -1000px'; tempInput.value = value; document.body.appendChild( tempInput ); tempInput.select; document.execCommand( 'copy' ); document.body.removeChild( tempInput ); }

function makeCopyIcon( toCopy ) { var $icon = $( ' ' ) .attr( 'src', Popups.cfg.icons.copy ) .addClass( 'popups_icon popups_icon_inline popups_icon_copy' ) .on( 'click', function {				copyToClipboard( toCopy );			} ); return $icon; }

function makeEditIcon( url ) { const $link = $( '' ) .attr( 'href', url );

$( ' ' )			.attr( 'src', Popups.cfg.icons.edit ) .addClass( 'popups_icon popups_icon_inline popups_icon_edit' ) .appendTo( $link );

return $link; }

function constructEditsTable( serverName, revs, title, isContribs ) { var $table = makeTableWithCols( 'popups_rev_table',			[ 'rev-links', 'rev-time', 'rev-user', 'rev-comment' ] ); var $tb = $table.find( 'tbody' ); // var timeOffset = 0;

var timestampFxn = function ( rev ) { return rev.timestamp; };

var cellsFxn = function ( rev, time ) {

var revTitle = title || rev.title;

var cells = [];

if ( isContribs ) { var diffLink = getDiffLink( serverName, revTitle, rev.parentid, rev.revid, 'diff' ); var histLink = getActionLink( serverName, revTitle, 'history', 'hist' ); cells.push( bracketedLinkList( [ histLink, diffLink ] ) ); } else { var curLink = getDiffLink( serverName, revTitle, rev.revid, revs[ 0 ].revid, 'cur' ); var prevLink = getDiffLink( serverName, revTitle, rev.parentid, rev.revid, 'prev' ); cells.push( bracketedLinkList( [ curLink, prevLink ] ) ); }

cells.push( getRevidLink( serverName, revTitle, rev, time ) );

if ( isContribs ) { cells.push( getPageLink( serverName, revTitle ) ); } else { cells.push( getUserLink( serverName, rev.user ) ); }

var minor = rev.minor ? 'm ' : ''; cells.push( minor + rev.parsedcomment ); return cells; };

$tb.append( makeDailyEventTableRows( revs, 4, timestampFxn, cellsFxn ) ); return $table; }

/**	 * Table for uploads - can't quite emulate Special:ListFiles, but good enough *	 * @param {string} serverName * @param {Array} uploadLogEvents * @return {jQuery} */	function constructUploadsTable( serverName, uploadLogEvents ) { const $table = makeTableWithCols( 'popups_uploads_table',			[ 'upload-file', 'upload-action', 'upload-time' ] );

const timestampFxn = function ( logEvent ) { return logEvent.timestamp; };

const cellsFxn = function ( logEvent, time ) { const cells = [];

cells.push( getPageLink( serverName, logEvent.title ) );

cells.push( logEvent.action );

cells.push( time );

return cells; };

$table .find( 'tbody' ) .append( makeDailyEventTableRows( uploadLogEvents, 2, timestampFxn, cellsFxn ) );

return $table; }

function constructDiffView( compare ) { var $div = $( ' ' ).addClass( 'popups_diff_view' ); var $table = makeTableWithCols( 'popups_diff_table',			[ 'diff-marker', 'diff-content', 'diff-marker', 'diff-content' ] ); var $tbody = $table.find( 'tbody' );

$div.append( $( ' ' ).addClass( 'popups_diff_comment' )			.append( compare.toparsedcomment ) );

var sizediff = compare.tosize - compare.fromsize; if ( sizediff > 0 ) { sizediff = '+' + sizediff; }

if ( compare.fromsize > 0 ) { sizediff += ' (' + ( 100 * ( compare.tosize - compare.fromsize ) / compare.tosize ).toFixed( 0 ) + '%)'; }

$div.append( $( ' ' ).addClass( 'popups_diff_size' )			.append( compare.fromsize + ' → ' + compare.tosize + ': ' + sizediff )		);

$( compare.body ) .appendTo( $tbody );

return $div.append( $table ); }

function genericTimestampFunction( item ) { return item.timestamp; }

function constructLogEventView( serverName, events ) { var $table = makeTableWithCols( 'popups_log_event_table',			[ 'le_type', 'le_title', 'le_timestamp', 'le_comment' ] ); var $tb = $table.children( 'tbody' ); // var timeOffset = 0;

var cellsFxn = function ( evt, time ) { var cells = [ evt.type, getPageLink( serverName, evt.title ), time, evt.parsedcomment ];			return cells; };

$tb.append( makeDailyEventTableRows( events, 4, genericTimestampFunction, cellsFxn ) ); return $table; }

function constructAfLogView( serverName, log ) {

var $table = makeTableWithCols( 'popups_log_event_table',			[ 'le_type', 'le_title', 'le_timestamp', 'le_comment' ] ); var $tb = $table.children( 'tbody' );

function logTimestamp( logItem ) { return logItem.timestamp; }

function logCells( logItem, time ) { var actionRes = logItem.action + ( logItem.result ? ( ' (' + logItem.result + ')' ) : '' );

var cells = [ getPageLink( serverName, 'Special:AbuseLog/' + logItem.id,				actionRes ) ];

if ( logItem.revid ) { cells.push( getDiffLink( serverName, logItem.title, logItem.revid, 'prev', time ) ); } else { cells.push( time ); }

cells.push( getPageLink( serverName, logItem.title ) ); cells.push( getPageLink( serverName, 'Special:AbuseFilter/' + logItem.filter_id, logItem.filter + '(' + logItem.filter_id + ')' ) ); return cells; }

$tb.append( makeDailyEventTableRows( log, 4, logTimestamp, logCells ) ); return $table; }

function addToDl( $dl, dt, dd ) { $( '' ).append( dt ).appendTo( $dl ); $( '' ).append( dd ).appendTo( $dl ); }

function constructAfLogEntry( serverName, entry ) {

const $elem = $( ' ' ); const $dl = $( '' ).appendTo( $elem );

const filterLink = getPageLink( serverName,			'Special:AbuseFilter/' + entry.filter_id,			entry.filter );

addToDl( $dl, 'Filter', filterLink ); addToDl( $dl, 'User name', getUserLink( serverName, entry.details.user_name ) ); addToDl( $dl, 'User age', entry.details.user_age ); addToDl( $dl, 'User edit count', entry.details.user_editcount ); addToDl( $dl, 'Page age', entry.details.page_age ); addToDl( $dl, 'Added lines', entry.details.added_lines );

return $elem; }

/*	 * Recent changes and watchlists */	function constructRcLogView( serverName, log ) { var $table = makeTableWithCols( 'popups_rc_table',			[ 'le_type', 'le_title', 'le_timestamp', 'le_comment' ] ); var $tb = $table.children( 'tbody' );

var cellsFxn = function ( logItem, time ) { var patrolled = ''; if ( logItem.unpatrolled ) { patrolled = makeEnclosingTag( 'span', '!', 'popups_unpatrolled' ); }			if ( logItem.new ) { patrolled = makeEnclosingTag( 'span', 'N', 'popups_edit_flags' ); }			if ( logItem.minor ) { patrolled = makeEnclosingTag( 'span', 'm', 'popups_edit_flags' ); }			if ( logItem.bot ) { patrolled = makeEnclosingTag( 'span', 'b', 'popups_edit_flags' ); }

var cells = [ logItem.type + ' ' + getUserLink( serverName, logItem.user ), getPageLink( serverName, logItem.title ), getDiffLink( serverName, logItem.title, logItem.revid, 'prev', time ), patrolled + ' ' + logItem.parsedcomment ];			return cells; };

$tb.append( makeDailyEventTableRows( log, 4, genericTimestampFunction, cellsFxn ) ); return $table; }

function constructCategoryListView( serverName, cmems, listClass ) {

var lis = makeOpenTag( 'ul', listClass );

for ( var i = 0; i < cmems.length; i++ ) { lis += '' + getPageLink( serverName, cmems[ i ].title ) + ''; }		lis += ''; return lis; }

function constructBacklinksView( serverName, backlinks ) { var content = makeOpenTag( 'div', 'popups_backlinks' );

if ( backlinks.length > 0 ) { content += '';

for ( var i = 0; i < backlinks.length; i++ ) { content += '<li>' + getPageLink( serverName, backlinks[ i ].title ) + '</li>'; }

content += '</ul>'; } else { content += makeEnclosingTag( 'span', 'No pages link to this page.' ); }

content += ' ';

return content; }

function constructWikitextView( wikitext ) { var content = makeOpenTag( 'div', 'popups_wikitext' );

if ( wikitext.length > 0 ) { content += makeEnclosingTag( 'pre', wikitext ); }

content += ' ';

return content; }

function constructPageInfo( data ) { var content = makeOpenTag( 'div', 'popups_pageinfo popups_info_table_horz' );

var $table = $( ' ' );

var makeRow = function ( h, d ) { if ( !d ) { return null; }			return $( ' ' ) .append( $( ' ' )					.attr( 'scope', 'row' )					.append( h )				) .append( $( ' ' )					.append( d )				); };

var protection = function ( parray ) {

if ( !parray || parray.length === 0 ) { return 'none'; }

var strs = parray.map( function ( p ) {				return p.type + ': ' + p.level + ' (' + p.expiry + ')';			} );

return strs.join( ' ' ); };

var restrictions = function ( rarray ) { if ( !rarray || rarray.length === 0 ) { return 'none'; }			return rarray.join( ', ' ); };

// console.log( data );

if ( data.missing ) { $table.append( makeRow( 'Missing', 'yes' ) ); }

$table .append( makeRow( 'Page ID', data.pageid ) ) .append( makeRow( 'Last rev', data.lastrevid ) ) .append( makeRow( 'Content model', data.contentmodel ) ) .append( makeRow( 'Length', data.length ) ) .append( makeRow( 'Protection', protection( data.protection ) ) ) .append( makeRow( 'Restrictions', restrictions( data.restrictiontypes ) ) ) .append( makeRow( 'Watchers', data.watchers ) );

if ( data.visitingwatchers ) { $table.append( makeRow( 'Visiting watchers:', data.visitingwatchers ) ); }

content += $table.get( 0 ).outerHTML;

content += ' ';

return Promise.resolve( content ); }

/*	 * Basic "pass-though" recogniser that just copies the address */	Popups.cfg.recogniserHooks.push( {		score: function ( /* $l, cache */ ) {			// this is the very least we can do			return 0;		},		canonical: function ( $l /*, cache */ ) {			return Promise.resolve( { type: 'basic', href: $l.attr( 'href' ), display: $l.text, canonical: $l.text } );		}	} );

function WikiCache { this.title = undefined; }

WikiCache.prototype.isBasePage = function { return this.title === this.baseName; };

function cacheUpdateWikiProps( $l, cache ) { if ( cache.wiki ) { return; }

cache.wiki = new WikiCache;

// normalise hrefs cache.link.href = cache.link.href.replace( /_/g, ' ' );

if ( cache.link.url.pathname.startsWith( '/wiki/' ) ) { cache.wiki.title = decodeURIComponent( cache.link.url.pathname ) .replace( /^\/wiki\//, '' ) .replace( /_/g, ' ' ); } else if ( cache.link.getParam( 'title' ) ) { cache.wiki.title = cache.link.getParam( 'title' ); }

cache.wiki.action = cache.link.getParam( 'action' ) || 'view';

if ( cache.wiki.title ) { cache.wiki.baseName = cache.wiki.title.replace( /\/.*$/, '' ); cache.wiki.namespace = cache.wiki.baseName.replace( /:.*$/, '' ); cache.wiki.titleNoNs = cache.wiki.title.replace( cache.wiki.namespace + ':', '' ); }

const titleParts = cache.wiki.title.split( '/' );

if ( cache.wiki.baseName === 'Special:Contributions' ) { cache.wiki.user = titleParts[ 1 ]; cache.wiki.isContribs = true; } else if ( cache.wiki.baseName === 'Special:Log' &&				cache.link.getParam( 'user' ) ) { cache.wiki.user = cache.link.getParam( 'user' ); } else if ( cache.wiki.baseName === 'Special:Block' ) { cache.wiki.user = titleParts[ 1 ]; } else if ( isNamespaceOrTalk( cache.wiki.namespace, 'User' ) ) { cache.wiki.user = cache.wiki.baseName.substring( cache.wiki.namespace.length + 1 ); }

if ( cache.link.url.hash ) { cache.wiki.section = cache.link.url.hash.substring( 1 ); }

// eslint-disable-next-line no-jquery/no-class-state cache.wiki.redlink = $l.hasClass( 'new' ); }

function cacheSiteInfo( api ) { const serverName = api.serverName; let siProm; if ( !Popups.siteinfo[ serverName ] ) { siProm = api .getSiteInfo( [ 'namespaces' ] ) .then( ( si ) => {					Popups.siteinfo[ serverName ] = si;					return Popups.siteinfo[ serverName ];				} ); } else { siProm = Promise.resolve( Popups.siteinfo[ serverName ] ); }		return siProm; }

function canonicaliseNs( serverName, localNs ) { const nses = Popups.siteinfo[ serverName ].namespaces; for ( const ns in nses ) { if ( nses[ ns ][ '*' ] === localNs ) { return nses[ ns ].canonical; }		}		return localNs; }

/** On-wiki links */ Popups.cfg.recogniserHooks.push( {		score: function ( $l, cache ) {

// no match - not a URL or not this server or a known foreign API if ( !cache.link.url ||					!recognisedWikiServer( cache.link.url.hostname )			) { return; }

cacheUpdateWikiProps( $l, cache ); return 100; },		canonical: function ( $l, cache ) {

cacheUpdateWikiProps( $l, cache );

var display = cache.wiki.title; var href = cache.link.url.href;

if ( cache.wiki.user ) { display = 'User:' + cache.wiki.user; href = getArticleUrl( display ); }

if ( cache.wiki.section ) { display += '#' + cache.wiki.section; }

var oldid = cache.link.getParam( 'oldid' ); if ( oldid ) { display += ' @' + oldid; }

cache.wiki.local = cache.link.url.hostname === mw.config.get( 'wgServerName' ); cache.wiki.serverName = cache.link.url.hostname;

cache.wiki.api = new Api( cache.wiki.serverName );

const isHash = href.endsWith( '/#' ) && cache.link.url.pathname === '/';

const recognised = { type: isHash ? 'wiki_hash' : 'wiki_local', href: href, display: display, canonical: cache.wiki.title };			// eslint-disable-next-line no-jquery/no-class-state if ( $l.hasClass( 'popups_wikitext' ) ) { recognised.type = 'wikitext'; }

return cacheSiteInfo( cache.wiki.api ) .then( => {					// canonicalise namespace					cache.wiki.namespace = canonicaliseNs( cache.wiki.serverName, cache.wiki.namespace );

if ( cache.wiki.namespace !== 'Special' ) { recognised.editUrl = getWikiActionUrl( cache.wiki.serverName,							cache.wiki.title, 'edit' ); }				} )				.then( => recognised );		}	} );

function constructRawImageContent( src ) { var $content = $( ' ' ) .addClass( 'popups_rawimage' );

$( ' ' )			.attr( 'src', src ) .appendTo( $content );

return $content; }

function constructPageImageContent( serverName, titleNoNs ) { var size = 350; return new Api( serverName ).getPageThumbUrlForTitle( titleNoNs, size ) .then( function ( imgUrl ) {				return constructRawImageContent( imgUrl );			} ); }

function constructMissingPageError { return $( ' ' ) .addClass( 'popups_missingpage' ) .append( 'Page does not exist' ); }

function constructGenericError( e ) { return $( ' ' ) .addClass( 'popups_error' ) .append( e ); }

/**	 * @param {string} serverName * @param {string} title * @param {string} [section] * @return {Promise} resolves with the content as jQuery */	function constructPageRender( serverName, title, section ) {

const api = new Api( serverName );

let renderProm; if ( section ) { renderProm = api.getSectionWithTitle( title, section ) .then( function ( matchingSection ) {					return api.getPageRender( title, matchingSection.index );				} ); } else { renderProm = api.getPageRender( title ); }

return renderProm .then( function ( parse ) {				var $content = $( ' ' )					.addClass( 'popups_page_content' );

const $parsed = $( parse.text ); $content.append( $parsed ); return $content; },			function ( data ) { if ( data !== 'missingtitle' ) { return constructGenericError( `GET Failed: for ${title}: ` + data ); }				return constructMissingPageError; } );	}

function escapeHtml( html ) { return html .replace( /&/g, '&amp;' ) .replace( /</g, '&lt;' ) .replace( />/g, '&gt;' ) .replace( /"/g, '&quot;' )			.replace( /'/g, '&#039;' );	}

/**	 * Returns the page's Wikitext, as escaped HTML *	 * @param {string} serverName * @param {string} title * @param {number} oldid * @return {Promise} the page wikitext as jQuery */	function constructPageWikitext( serverName, title, oldid ) { return new Api( serverName ) .getPageWikitext( title, oldid ) .then( function ( wikitext ) {				return constructWikitextView( escapeHtml( wikitext ) );			} ); }

/**	 * Basic non-wiki content */	Popups.cfg.contentHooks.push( {		name: 'basic non-wiki content',		onlyType: 'basic',		score: function {			return 0;		},		content: function ( $l, cache ) {			// Note: for many (most?) external URLs, this will fail due to CORS			return fetch( cache.link.href )				.then( function ( data ) {

const imageTypes = [ 'image/jpeg', 'image/png' ];

// many images will load OK					if ( imageTypes.indexOf( data.headers.get( 'content-type' ) ) !== -1 ) { return $( ' ' ) .attr( 'src', data.url ); }

throw new Error( 'Unsupported external site data' ); } );		}	} );

/**	 * Mediawiki images */	Popups.cfg.contentHooks.push( {		name: 'wikimedia image',		onlyType: 'basic',		score: function ( $l, cache ) {			if ( cache.link.url.hostname === 'upload.wikimedia.org' ) {				return 100;			}		},		content: function ( $l, cache ) {

const changeWikimediaThumbRes = function ( url, newRes ) { return url.replace( /(?<=\/(?:page\d+-)?)\d+(?=px-)/, newRes ); };

// downres to avoid massive images const url = changeWikimediaThumbRes( cache.link.url.href, Popups.cfg.imgWidth );

return $( ' ' ) .addClass( 'popups_wikimedia_thumb' ) .attr( 'src', url ); }	} );

Popups.cfg.contentHooks.push( {		name: 'hathitrust',		onlyType: 'basic',		score: function ( $l, cache ) {			if ( cache.link.url.hostname === 'babel.hathitrust.org' ) {				cache.hathi = {					id: cache.link.getParam( 'id' ),					seq: cache.link.getParam( 'seq' )				};				return 100;			}		},		content: function ( $l, cache ) {			if ( cache.hathi && cache.hathi.id ) {				return $( ' ' )					.append( cache.hathi.id );			}		}	} );

/**	 * Basic wiki content */	Popups.cfg.contentHooks.push( {		name: 'basic wiki content',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			// special namespace pages don't have anything useful			if ( cache.wiki.namespace !== 'Special' ) {				// basic content is not very interesting, but it's the best we can				// do for many pages				return 0;			}		},		content: function ( $l, cache ) {			return constructPageRender( cache.wiki.serverName, cache.wiki.title, cache.wiki.section );		}	} );

/*	 * Wikitext */	Popups.cfg.contentHooks.push( {		name: 'wikitext content',		onlyType: 'wikitext',		score: function ( $l, cache ) {			// special namespace pages don't have anything useful			if ( cache.wiki.namespace !== 'Special' ) {				// matched wikitext exactly				return 2000;			}		},		content: function ( $l, cache ) {			var oldid = cache.link.getParam( 'oldid' );			return constructPageWikitext( cache.wiki.serverName, cache.wiki.title, oldid );		}	} );

Popups.cfg.contentHooks.push( {		name: 'page NS content',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.namespace === 'Page' && ( cache.wiki.action === 'view' || cache.wiki.redlink ) ) {				return 1000;			}		},		content: function ( $l, cache ) {

return new Api( cache.wiki.serverName ) .ifPageExists( cache.wiki.title ) .then( function ( exists ) {					if ( !exists ) {						// load page image						return constructPageImageContent( cache.wiki.serverName, cache.wiki.titleNoNs );					} else {						return constructPageRender( cache.wiki.serverName, cache.wiki.title );					}				} ); }	} );

Popups.cfg.contentHooks.push( {		name: 'old revision',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.link && cache.link.getParam( 'oldid' ) && !cache.link.getParam( 'diff' ) ) {				cache.oldRev = {					id: cache.link.getParam( 'oldid' )				};				return 2000;			}		},		content: function ( $l, cache ) {			return new Api( cache.wiki.serverName )				.getOldRevision( cache.oldRev.id )				.then( ( content ) => {

const $ret = $( ' ' );

const diffTemplate = mw.message( 'popups-diff-template', cache.oldRev.id ).plain; $( ' ' )						.append( $( ' ' )							.append( diffTemplate )						) .append( makeCopyIcon( diffTemplate ) ) .appendTo( $ret );

$( ' ' )						.append( content ) .appendTo( $ret );

return $ret; },				function ( data ) { console.error( 'GET Failed: for ', cache.wiki.title, data ); } );		}	} );

Popups.cfg.contentHooks.push( {		name: 'page history',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.action === 'history' ) {				// outscore the "Plain" content hook for that page (1000)				return 2000;			}		},		content: function ( $l, cache ) {

var ok = function ( page ) { var revs = page.revisions;

var $table; if ( revs ) { $table = constructEditsTable( cache.wiki.serverName,						revs, page.title, false ); }				return $table; };

var fail = function { console.error( 'GET Failed: for ', cache.wiki.title ); };

return new Api( cache.wiki.serverName ) .getPageHistory( cache.wiki.title,					Popups.cfg.logLimit.history || Popups.cfg.logLimit.default,					[ 'tags', 'timestamp', 'user', 'parsedcomment', 'flags', 'ids', 'size' ]				) .then( ok, fail ); }	} );

/* extended page information */ Popups.cfg.contentHooks.push( {		name: 'page info',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.action === 'info' ) {				// outscore the "Plain" content hook for that page (1000)				return 2000;			}		},		content: function ( $l, cache ) {

return new Api( cache.wiki.serverName ) .getPageInfo( cache.wiki.title,					[ 'watchers', 'watched', 'visitingwatchers', 'varianttitles', 'url',						'talkid', 'subjectid', 'preload', 'protection', 'notificationtimestamp',						'linkclasses', 'displaytitle' ]				) .then( function ( pageInfo ) {					return constructPageInfo( pageInfo );				} ); }	} );

Popups.cfg.contentHooks.push( {		name: 'user contributions',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.isContribs ) {				return 100;			}			if ( Popups.cfg.userContribsByDefault && ( cache.wiki.namespace === 'User' && cache.wiki.isBasePage ) ) {				return 100;			}		},		content: function ( $l, cache ) {			var user;			if ( cache.wiki.isContribs ) {				user = cache.wiki.title.split( '/' )[ 1 ];			} else {				user = cache.wiki.titleNoNs; // == wiki.baseName			}

var namespace; if ( cache.link ) { namespace = cache.link.getParam( 'namespace' ); }

return new Api( cache.wiki.serverName ) .getUserContributions( user,					[ 'ids', 'title', 'timestamp', 'parsedcomment', 'size', 'flags' ], namespace,					Popups.cfg.logLimit.contribs || Popups.cfg.logLimit.default				) .then( function ( contribRevs ) {					var $table = constructEditsTable( cache.wiki.serverName, contribRevs, null, true );					return $table;				} ); }	} );

Popups.cfg.contentHooks.push( {		name: 'user files',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.baseName.startsWith( 'Special:ListFiles' ) || cache.wiki.baseName.startsWith( 'Special:AllMyUploads' ) ) {				return 100;			}		},		content: function ( $l, cache ) {

if ( !cache.link ) { // TODO: throw something standard here return; }

let user = cache.link.getParam( 'user' ); if ( !user ) { user = cache.wiki.titleNoNs.split( '/' )[ 1 ]; }

if ( !user ) { return; }

const type = 'all';

return new Api( cache.wiki.serverName ) .getUserFiles( user, type,					Popups.cfg.logLimit.contribs || Popups.cfg.logLimit.default				) .then( function ( userFiles ) {					var $table = constructUploadsTable( cache.wiki.serverName, userFiles );					return $table;				} ); }	} );

Popups.cfg.contentHooks.push( {		name: 'diff view',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.link.hasAllParams( [ 'oldid', 'diff' ] ) || cache.link.hasAllParams( [ 'undo', 'undoafter' ] ) ) {				// this should out-score the "plain" content for that page				// which is usually 1000) return 2000; }

if ( cache.wiki.baseName.startsWith( 'Special:Diff' ) ) { cache.diff.diff = cache.wiki.title.replace( /^.*\//, '' ); return 2000; }		},		content: function ( $l, cache ) { var params = { action: 'compare', format: 'json', formatversion: 2, prop: [ 'diff', 'ids', 'title', 'parsedcomment', 'size' ].join( '|' ) };

if ( cache.link.getParam( 'undo' ) ) {

params.fromrev = cache.link.getParam( 'undo' ); params.torev = cache.link.getParam( 'undoafter' ); } else {

var oldid = cache.link.getParam( 'oldid' ); var diff = cache.link.getParam( 'diff' );

// taken from navigation popups switch ( diff ) { case 'cur': switch ( oldid ) { case null: case '': case 'prev': // this can only work if we have the title // cur -> prev params.fromtitle = cache.wiki.title; // params.fromrev = 'cur'; params.torelative = 'prev'; break; default: params.fromrev = oldid; params.torelative = 'cur'; break; }						break; case 'prev': if ( oldid ) { params.fromrev = oldid; } else { // params.fromtitle; }						params.torelative = 'prev'; break; case 'next': params.fromrev = oldid || 0; params.torelative = 'next'; break; default: params.fromrev = oldid || 0; params.torev = diff || 0; break; }			}

return new Api( cache.wiki.serverName ) .get( params ) .then( function ( data ) {					var $table = constructDiffView( data.compare );					return $table;				},				function ( data ) {					console.error( `GET Failed: for ${cache.wiki.title}`, data );				} ); }	} );

Popups.cfg.contentHooks.push( {		name: 'user log',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.baseName === 'Special:Log' && cache.link.getParam( 'user' ) ) {				return 100;			}		},		content: function ( $l, cache ) {			return new Api( cache.wiki.serverName )				.getUserLog( cache.link.getParam( 'user' ), [ 'ids', 'title', 'type', 'timestamp', 'parsedcomment' ], Popups.cfg.logLimit.userlog || Popups.cfg.logLimit.default )				.then( function ( logEvents ) { var $table = constructLogEventView( cache.wiki.serverName, logEvents ); return $table; } );		}	} );

Popups.cfg.contentHooks.push( {		name: 'user abuse filter log',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.baseName === 'Special:AbuseLog' ) {				var user = cache.link.getParam( 'wpSearchUser' );

if ( user ) { cache.afLog = { user: user };					return 100; }			}		},		content: function ( $l, cache ) {

return new Api( cache.wiki.serverName ) .getUserAbuseFilterLog(					cache.afLog.user,					[ 'ids', 'title', 'action', 'timestamp', 'revid', 'filter', 'result' ],					Popups.cfg.logLimit.userlog || Popups.cfg.logLimit.default				) .then( function ( abuseLog ) {					var $table = constructAfLogView( cache.wiki.serverName, abuseLog );					return $table;				} ); }	} );

Popups.cfg.contentHooks.push( {		name: 'abusefilter log entry',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.baseName === 'Special:AbuseLog' ) {

const parts = cache.wiki.title.split( '/' );

if ( parts.length === 2 ) { cache.afLog = { entry: parseInt( parts[ 1 ] ) };					return 100; }			}		},		content: function ( $l, cache ) {

return new Api( cache.wiki.serverName ) .getAbuseFilterLogEntry( cache.afLog.entry ) .then( ( entry ) => {					return constructAfLogEntry( cache.wiki.serverName, entry );				} ); }	} );

Popups.cfg.contentHooks.push( {		name: 'what links here',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.baseName === 'Special:WhatLinksHere' ) {				cache.whatLinksHere = {					title: cache.wiki.title.substr( cache.wiki.title.indexOf( '/' ) + 1 )				};				return 1000;			}		},		content: function ( $l, cache ) {

var ok = function ( query ) { var pages = []; if ( query.backlinks ) { pages = pages.concat( query.backlinks ); }				if ( query.embeddedin ) { pages = pages.concat( query.embeddedin ); }				var $table = constructBacklinksView( cache.wiki.serverName, pages ); return $table; };

return new Api( cache.wiki.serverName ) .getWhatLinksHere( cache.whatLinksHere.title,					[ 'backlinks', 'embeddedin' ],					Popups.cfg.linkLimit.default ) .then( ok ); }	} );

Popups.cfg.contentHooks.push( {		name: 'what leaves here',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.baseName === 'Special:WhatLeavesHere' ) {				cache.whatLeavesHere = {					title: cache.link.getParam( 'target' )				};				return 1000;			}		},		content: function ( $l, cache ) {

var ok = function ( page ) { if ( page.missing ) { return constructMissingPageError; } else { var pages = []; if ( page.templates ) { pages = pages.concat( page.templates ); }					if ( page.images ) { pages = pages.concat( page.images ); }					if ( page.links ) { pages = pages.concat( page.links ); }					var $table = constructBacklinksView( cache.wiki.serverName, pages ); return $table; }			};

return new Api( cache.wiki.serverName ) .getWhatLeavesHere( cache.whatLeavesHere.title,					[ 'templates', 'images', 'links' ],					Popups.cfg.linkLimit.default ) .then( ok ); }	} );

Popups.cfg.contentHooks.push( {		name: 'image info',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.title && cache.wiki.namespace === 'File' && cache.wiki.action === 'view' ) {				return 1000;			}		},		content: function ( $l, cache ) {

var $content = $( ' ' ).addClass( 'popups_img' );

var $info = $( ' ' ) .addClass( 'popups_image_info' ) .appendTo( $content );

var alt = $l.attr( 'alt' ) || 'none'; $info.append( makeClassedSpan( 'popups_info_title', 'Alt text: ' ), alt );

return new Api( cache.wiki.serverName ) .getImageInfo( cache.wiki.title,					[ 'text', 'categories', 'externallinks', 'properties' ] ) .then( function ( text ) {					$content.append( $( ' ' ) .addClass( 'popups_image_content' ) .append( text ) );					return $content;				} ); }	} );

Popups.cfg.contentHooks.push( {		name: 'recent changes',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.title ) {

cache.recentChanges = {};

const nses = cache.link.getParam( 'namespace' ); if ( nses ) { cache.recentChanges.namespaces = nses.split( '|' ); }

if ( cache.wiki.baseName === 'Special:RecentChanges' ) { return 1000; }

// This does not actually work, because there is no API for this // (requested since 2008: T17552) if ( cache.wiki.baseName === 'Special:RecentChangesLinked' ) { cache.recentChanges = { title: cache.wiki.title.substr( cache.wiki.title.indexOf( '/' ) + 1 ) };					return 1000; }

cache.recentChanges = undefined; }		},		content: function ( $l, cache ) { var $content = $( ' ' ).addClass( 'popups_rc' );

var rcprops = [ 'title', 'timestamp', 'ids', 'user', 'parsedcomment', 'sizes' ]; if ( haveRight( 'patrol' ) ) { rcprops.push( 'patrolled' ); }

return new Api( cache.wiki.serverName ) .getRecentChanges( cache.recentChanges.title,					{						props: rcprops,						types: Popups.cfg.rcTypes,						limit: Popups.cfg.logLimit.rc || Popups.cfg.logLimit.default,						ns: cache.recentChanges.namespaces					} ) .then( function ( recentChanges ) {					$content.append( constructRcLogView( cache.wiki.serverName,						recentChanges ) );					return $content;				} ); }	} );

Popups.cfg.contentHooks.push( {		name: 'watchlist',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.baseName === 'Special:Watchlist' ) {				cache.watchlist = {};

const ns = cache.link.getParam( 'namespace' ); if ( ns !== null ) { cache.watchlist.namespaces = ns.split( ';' ); }

return 1000; }		},		content: function ( $l, cache ) { var $content = $( ' ' ).addClass( 'popups_watchlist' );

var wlProps = [ 'title', 'timestamp', 'ids', 'user', 'parsedcomment', 'flags', 'sizes' ]; if ( haveRight( 'patrol' ) ) { wlProps.push( 'patrolled' ); }

return new Api( cache.wiki.serverName ) .getWatchlistEntries(					mw.config.get( 'wgUserName' ),					wlProps,					Popups.cfg.watchlistTypes,					cache.watchlist.namespaces,					Popups.cfg.logLimit.watchlist || Popups.cfg.logLimit.default				) .then( function ( watchlist ) {					$content.append( constructRcLogView( cache.wiki.serverName, watchlist ) );					return $content;				} ); }	} );

Popups.cfg.contentHooks.push( {		name: 'category members',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.namespace === 'Category' ) {				return 1000;			}		},		content: function ( $l, cache ) {

var handleCatMembers = function ( categorymembers ) { var $content = $( ' ' ).addClass( 'popups_catmember' );

$content.append( $( ' ' ).append( 'Parent categories' ) );

$content.append( $( ' ' ).append( 'Category members' ) );

$content.append( constructCategoryListView( cache.wiki.serverName, categorymembers, 'hlist' ) );

return $content; };

return new Api( cache.wiki.serverName ) .getCategoryMembers( cache.wiki.title,					Popups.cfg.linkLimit.categoryMembers || Popups.cfg.linkLimit.default ) .then( handleCatMembers ); }	} );

Popups.cfg.contentHooks.push( {		name: 'raw page image',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.link && cache.link.href && cache.link.href.match( /page\d+-\d+px/ ) ) {				// outweigh "normal" raw image content, which might go to File: page				return 2000;			}		},		content: function ( $l, cache ) {			return constructRawImageContent( cache.link.href );		}	} );

Popups.cfg.contentHooks.push( {		name: 'random',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.baseName === 'Special:Random' || cache.wiki.baseName === 'Special:RandomRootpage' ) {				return 2000;			}		},		content: function ( $l, cache ) {			var $content = $( ' ' )				.addClass( 'popups-random' );			var $ul = $( '<ul>' ).appendTo( $content );

/* Dirty hack: T274219 */ var filterRoots = function ( page ) { return ( page.ns !== 0 ) || page.title.indexOf( '/' ) === -1; };

// TODO: invalid outside enWS var ns = []; switch ( cache.wiki.title.substr( cache.wiki.title.lastIndexOf( '/' ) + 1 ) ) { case 'Index': ns.push( 106 ); break; case 'Author': ns.push( 102 ); break; default: ns.push( 0 ); }

return new Api( cache.wiki.serverName ) .getRandomPages( ns,					Popups.cfg.linkLimit.randomPages || Popups.cfg.linkLimit.default ) .then( function ( pages ) {					pages = pages.filter( filterRoots );

for ( var i = 0; i < pages.length; ++i ) { $( '<li>' ).append( $( '<a>' )							.attr( 'href', getArticleUrl( pages[ i ].title ) )							.append( pages[ i ].title ) ) .appendTo( $ul ); }

return $content; } );		}	} );

/*	 * Convert Special:EntityPage into normal content */	Popups.cfg.contentHooks.push( {		name: 'wikibase entity',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.namespace === 'Special' && cache.wiki.titleNoNs.startsWith( 'EntityPage/' ) ) {				cache.entityPage = {					qid: cache.wiki.titleNoNs.split( '/' ).at( -1 )				};				return 2000;			}		},		content: function ( $l, cache ) {			return constructPageRender( cache.wiki.serverName, cache.entityPage.qid );		}	} );

function deleteFromPreset( api, page, delInfo ) {

let reason = ''; if ( delInfo.message ) { /* eslint-disable-next-line mediawiki/msg-doc */ reason = mw.msg( delInfo.message ); } else if ( delInfo.reason ) { reason = delInfo.reason; }

let promise; if ( !delInfo.noconfirm ) { promise = confirmAction(				mw.msg( 'popups-delete-confirm', page, reason )			); } else { promise = Promise.resolve( true ); }

promise.then( function {

api.delete( page, reason, delInfo.watchlist ) .then(					function {						mw.notify( mw.msg( 'popups-delete-ok', page ) );					},					function  {						mw.notify( mw.msg( 'popups-delete-failed', page ) );					}				); } );	}

function clickableLink( text, clickHandler ) { return $( '<a>' ) .append( text ) .on( 'click', clickHandler ); }

Popups.cfg.contentHooks.push( {		name: 'delete',		score: function ( $l, cache ) {			if ( cache.recognised.type === 'wiki_local' && cache.wiki.action === 'delete' ) {				return 100;			}		},		content: function ( $l, cache ) {

const $ul = $( '<ul>' ); const api = new Api( cache.wiki.serverName );

for ( const delInfo of Popups.cfg.deletes ) { $( '<li>' ) .append( clickableLink( delInfo.name, function { deleteFromPreset( api, cache.wiki.title, delInfo ); } )					)					.appendTo( $ul ); }

return $ul; }	} );

var purgePage = function ( serverName, page ) {

var notify = function ( ok ) { if ( ok ) { mw.notify( 'Page purged.' ); } else { mw.notify( 'Purge failed.', { type: 'error' } ); }		};

new Api( serverName ) .doPurgePage( page ) .then( function ( data ) {				notify( data.purge[ 0 ].purged );			}, function {				notify( false );			} ); };

function makeEditHistTuple( serverName, title, text, isLocal ) {

var actionFactory, urlFactory; if ( isLocal ) { urlFactory = function ( urlTitle ) { return getWikiArticleUrl( serverName, urlTitle ); };			actionFactory = function ( urlTitle, action ) { return getWikiActionUrl( serverName, urlTitle, action ); };		} else { urlFactory = getCommonsArticleUrl; actionFactory = getCommonsActionUrl; }

return [ {				text: text || title, href: urlFactory( title ) },			{				text: 'e', href: actionFactory( title, 'edit' ), nopopup: true },			{				text: 'h', href: actionFactory( title, 'history' ) }		];	}

function makeFileDirectTuple( file, directUrl, isLocal, opts ) { var actionFactory, urlFactory; if ( isLocal ) { urlFactory = getArticleUrl; actionFactory = getActionUrl; } else { urlFactory = getCommonsArticleUrl; actionFactory = getCommonsActionUrl; }

return [ {				text: opts.name || 'file', href: urlFactory( 'File:' + file ) },			{				text: 'e', href: actionFactory( file, 'edit' ), nopopup: true },			{				text: 'h', href: actionFactory( file, 'history' ) },			{				text: '↓', href: directUrl, nopopup: true },			{				text: 'reupload', href: getReuploadUrl( file, isLocal ) }		];	}

Popups.cfg.actionHooks.push( {		name: 'basic actions',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			// if not a page on the current wiki, or a Js thing, basic content			// doesn't work			if ( !cache.wiki.title || cache.wiki.namespace === 'Special' ) {				return;			}

// the basic actions are usually fairly important return 1000; },		actions: function ( $l, cache ) { var actions = [];

var altNs; var altDisplay;

var fn = cache.wiki.titleNoNs;

const api = new Api( cache.wiki.serverName );

var isLocalPromise; if ( cache.wiki.namespace === 'File' && cache.wiki.local ) { // files can be non-local even if the link says they are local isLocalPromise = api.getFileIsLocal( fn ); } else { isLocalPromise = Promise.resolve( true ); }

return isLocalPromise.then( function ( isLocal ) {				var serverName;

if ( !isLocal ) { serverName = Popups.commons.serverName; } else { serverName = cache.wiki.serverName; }

if ( cache.wiki.namespace.endsWith( ' talk' ) ) { altNs = cache.wiki.namespace.replace( ' talk', '' ); altDisplay = 'article'; } else { altNs = cache.wiki.namespace + ' talk'; altDisplay = 'talk'; }

var oldid; if ( cache.link ) { oldid = cache.link.getParam( 'oldid' ); }

const basics = [];

if ( cache.wiki.section ) { basics.push( {						text: 'page',						href: getWikiArticleUrl( serverName, cache.wiki.title )					} ); }

basics.push( {					text: 'edit',					href: getWikiActionUrl( serverName, cache.wiki.title, 'edit', { oldid: oldid } ),					nopopup: true				} );

basics.push( {					text: 'wikitext',					href: oldid ?						getWikiActionUrl( serverName, cache.wiki.title, null, { oldid: oldid } ) :						getWikiArticleUrl( serverName, cache.wiki.title ),					class: [ 'popups_wikitext' ]				} );

basics.push( {					text: 'history',					href: getWikiActionUrl( serverName, cache.wiki.title, 'history' )				} );

basics.push( makeEditHistTuple( serverName, altNs + ':' + cache.wiki.titleNoNs, altDisplay, isLocal )				);

basics.push( {					text: mw.msg( 'popups-actions-info' ),					href: getWikiActionUrl( serverName, cache.wiki.title, 'info' )				} );

basics.push( {					text: mw.msg( 'popups-actions-move' ),					href: getWikiArticleUrl( serverName, 'Special:MovePage/' + cache.wiki.title )				} );

const deletes = [ // The main delete action {						text: mw.msg( 'popups-actions-delete' ), href: getWikiActionUrl( serverName, cache.wiki.title, 'delete' ) }				];

for ( const delInfo of Popups.cfg.deletes ) { if ( delInfo.asAction ) { deletes.push( {							text: delInfo.name,							click: function {								deleteFromPreset( api, cache.wiki.title, delInfo );							}						} ); }				}

basics.push( deletes );

actions.push( basics );

var unWatchPage = function ( watch, title ) {

new Api( cache.wiki.serverName ) .watchPage( title, watch ) .then( function {							var verb = watch ? 'added to' : 'removed from';							mw.notify( "'" + title + "' " + verb + ' watchlist.' );						},						function ( data ) {							mw.notify( 'Watchlist edit failed: ' + data, { type: 'error' } );						} );				};

actions.push( [ {					text: 'most recent edit',					href: getDiffUrl( serverName, cache.wiki.title, 'prev', 'cur' )				},				{					text: 'related changes',					href: getWikiArticleUrl( serverName, 'Special:RecentChangesLinked/' + cache.wiki.title )				},				[					{						text: 'un',						href: '#',						click: function { unWatchPage( false, cache.wiki.title ); }					},					{						text: 'watch',						href: '#',						click: function  { unWatchPage( true, cache.wiki.title ); }					}				]				] ); actions.push( [ {					text: 'what links here',					href: getWikiArticleUrl( serverName, 'Special:WhatLinksHere/' + cache.wiki.title )				},				{					text: 'what leaves here',					href: getWikiArticleUrl( serverName, 'Special:WhatLeavesHere?target=' + cache.wiki.title )				},				{					text: 'purge',					click: function {						purgePage( serverName, cache.wiki.title );					},					nopopup: true				}				] );

return actions; } ); // end local then		}	} );

function blockByPresetInfo( api, user, blockInfo ) {

let reason; if ( blockInfo.message ) { // eslint-disable-next-line mediawiki/msg-doc reason = mw.msg( blockInfo.message ); } else { // use the reason the user gave reason = blockInfo.reason; }

let promise; if ( !blockInfo.noconfirm ) { promise = confirmAction(				mw.msg( 'popups-block-confirm', user, reason )			); } else { promise = Promise.resolve( true ); }

promise.then( function ( confirmed ) {			if ( !confirmed ) {				return;			}

api .blockUser( user, blockInfo.expiry,					{						reason: reason,						allowusertalk: blockInfo.allowusertalk					} ) .then(					function {						mw.notify( mw.msg( 'popups-user-blocked', user ) );					},					function  {						mw.notify( mw.msg( 'popups-user-block-failed', user ), { type: 'error' } );					}				);		} );	}

Popups.cfg.actionHooks.push( {		name: 'user',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.user ) {				return 2000; // at top			}		},		actions: function ( $l, cache ) {

console.assert( cache.wiki.user );

const actions = []; const acctActions = [];

const api = new Api( cache.wiki.serverName );

if ( !cache.wiki.isContribs ) { actions.push( [					{						text: 'contribs',						href: getWikiArticleUrl( cache.wiki.serverName, 'Special:Contributions/' + cache.wiki.user )					},					{						text: 'com',						href: getWikiArticleUrl( Popups.commons.serverName, 'Special:Contributions/' + cache.wiki.user )					}				] ); } else if ( cache.wiki.serverName !== 'commons.wikimedia.org' ) { actions.push( {					text: 'com',					href: getWikiArticleUrl( Popups.commons.serverName, 'Special:Contributions/' + cache.wiki.user )				} ); }

if ( cache.wiki.serverName !== 'commons.wikimedia.org' ) { actions.push( [					{						text: '↑',						href: getWikiArticleUrl( cache.wiki.serverName, 'Special:ListFiles/' + cache.wiki.user )					},					{						text: 'com',						href: getWikiArticleUrl( Popups.commons.serverName, 'Special:ListFiles/' + cache.wiki.user )					}				] ); } else { actions.push( {					text: '↑',					href: getWikiArticleUrl( cache.wiki.serverName, 'Special:ListFiles/' + cache.wiki.user )				} ); }

actions.push( {				text: 'user log',				href: getWikiActionUrl( cache.wiki.serverName, 'Special:Log', null, { user: cache.wiki.user } )			} ); actions.push( {				text: 'abuse log',				href: getWikiActionUrl( cache.wiki.serverName, 'Special:AbuseLog', null, { wpSearchUser: cache.wiki.user } )			} );

acctActions.push( {				text: 'Xtools',				href: 'https://xtools.wmflabs.org/ec/' + cache.wiki.serverName + '/' + cache.wiki.user			} ); acctActions.push( {				text: 'SUL',				href: getWikiArticleUrl( cache.wiki.serverName, 'Special:CentralAuth/' + cache.wiki.user )			} ); acctActions.push( {				text: 'global',				href: 'https://tools.wmflabs.org/guc/index.php?user=' + cache.wiki.user + '&blocks=true'			} );

if ( haveRight( 'block' ) ) {

const blocks = [];

blocks.push( {					text: 'un',					href: getWikiArticleUrl( cache.wiki.serverName, 'Special:Ublock/' + cache.wiki.user )				} );

blocks.push( {					text: 'block',					href: getWikiArticleUrl( cache.wiki.serverName, 'Special:Block/' + cache.wiki.user )				} );

for ( const blockInfo of Popups.cfg.blocks ) { blocks.push( {						text: blockInfo.name,						click: function {							blockByPresetInfo( api, cache.wiki.user, blockInfo );						}					} ); }

acctActions.push( blocks ); }

if ( haveRight( 'nuke' ) ) { acctActions.push( {					text: mw.msg( 'popups-action-mass-delete' ),					href: getWikiActionUrl( cache.wiki.serverName, 'Special:Nuke', null, { target: cache.wiki.user } )				} );			}

return [ actions, acctActions ]; }	} );

Popups.cfg.actionHooks.push( {		name: 'prefs',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.title === 'Special:Preferences' ) {				return 2000; // at top			}		},		actions: function ( $l, cache ) {			var prefLink = getWikiArticleUrl( cache.wiki.serverName, 'Special:Preferences' );

var prefSecs = [ [ 'user profile', '#mw-prefsection-personal' ], [ 'appearance', '#mw-prefsection-rendering' ], [ 'editing', '#mw-prefsection-editing' ], [ 'recent changes', '#mw-prefsection-rc' ], [ 'watchlist', '#mw-prefsection-watchlist' ], [ 'gadgets', '#mw-prefsection-gadgets' ], [ 'search', '#mw-prefsection-searchoptions' ], [ 'beta features', '#mw-prefsection-betafeatures' ], [ 'notifications', '#mw-prefsection-echo' ] ];

var actions = [];

for ( var i = 0; i < prefSecs.length; ++i ) { actions.push( {					text: prefSecs[ i ][ 0 ],					href: prefLink + prefSecs[ i ][ 1 ],					nopopup: true				} ); }			return actions; }	} );

Popups.cfg.actionHooks.push( {		name: 'contributions',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.isContribs ) {				return 2000; // at top			}		},		actions: function ( $l, cache ) {			var contribLink = getWikiArticleUrl( cache.wiki.serverName, 'Special:Contributions/' + cache.wiki.user );

var actions = [ []			];

var nsList = Popups.cfg.nsLinkList.default;

for ( var i = 0; i < nsList.length; ++i ) { var ns = nsList[ i ]; var nsName = mw.config.get( 'wgFormattedNamespaces' )[ ns ]; var url = contribLink + '?namespace=' + ns; actions[ 0 ].push( {					text: nsName || 'Main',					href: url				} ); }			return actions; }	} );

Popups.cfg.actionHooks.push( {		name: 'file',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.namespace === 'File' ) {				return 0; // at end			}		},		actions: function ( $l, cache ) {			var fn = cache.wiki.titleNoNs;

return new Api( cache.wiki.serverName ) .getFileImageinfo( fn, [ 'url' ] ) .then( function ( imgPage ) {					const isLocal = imgPage.imagerepository !== 'shared';					const directUrl = imgPage.imageinfo[ 0 ].url;

var actions = [ {							text: '↓', href: directUrl, nopopup: true },						{							text: 'reupload', href: getReuploadUrl( fn, isLocal ) }					];

return [ actions ]; } );		}	} );

Popups.cfg.actionHooks.push( {		name: 'watchlist',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.title === 'Special:Watchlist' ) {				return 100;			}		},		actions: function ( $l, cache ) {			const nsActions = [];

for ( const ns of Popups.cfg.nsLinkList.default ) { nsActions.push( {					href: getWikiActionUrl( cache.wiki.serverName, 'Special:Watchlist', null, { namespace: ns } ),					text: mw.config.get( 'wgFormattedNamespaces' )[ ns ] || 'Main'				} ); }

return [ nsActions ]; }	} );

Popups.cfg.actionHooks.push( {		name: 'recentchanges',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.title === 'Special:RecentChanges' ) {				return 100;			}		},		actions: function ( $l, cache ) {			const nsActions = [];

for ( const ns of Popups.cfg.nsLinkList.default ) { nsActions.push( {					href: getWikiActionUrl( cache.wiki.serverName, 'Special:RecentChanges', null, { namespace: ns } ),					text: mw.config.get( 'wgFormattedNamespaces' )[ ns ] || 'Main'				} ); }

return [ nsActions ]; }	} );

Popups.cfg.actionHooks.push( {		name: 'pp-index',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.namespace === 'Index' ) {				return 0; // at end			}		},		actions: function ( $l, cache ) {			var fn = cache.wiki.titleNoNs;

return new Api( cache.wiki.serverName ) .getFileImageinfo( fn, [ 'url' ] ) .then( function ( imgPage ) {					var isLocal = imgPage.imagerepository !== 'shared';

var actions = [];

actions.push( makeFileDirectTuple( fn, imgPage.imageinfo[ 0 ].url, isLocal, { name: 'file' } ) );

// saves actually looking up the content model if ( !fn.endsWith( '.css' ) ) { actions.push( makeEditHistTuple( cache.wiki.serverName, 'Index:' + fn + '/styles.css', 'styles.css', true ) ); }					return [ actions ]; } );		}	} );

Popups.cfg.actionHooks.push( {		name: 'pp-page',		onlyType: 'wiki_local',		score: function ( $l, cache ) {			if ( cache.wiki.namespace === 'Page' ) {				return 0; // at end			}		},		actions: function ( $l, cache ) {

var preloadImageHead = function ( url ) { $.ajax( {					type: 'HEAD',					url: url				} ); };

var fn = cache.wiki.titleNoNs.replace( /\/\d+$/, '' ); var pg = cache.wiki.titleNoNs.replace( /^.*\/(\d+)$/, '$1' ); var size = 1024;

return new Api( cache.wiki.serverName ) .getPageImageUrl( fn, pg, size ) .then( function ( data ) {

var imgActions = []; // var num = parseInt( pg );

if ( pg > 1 ) { imgActions.push( {							text: 'prev page',							href: getArticleUrl( 'Page:' + fn + '/' + ( parseInt( pg ) - 1 ) )						} ); }

const pageInfo = data.query.pages[ 0 ]; const imageInfo = pageInfo.imageinfo[ 0 ]; const pageImageUrl = imageInfo.thumburl;

imgActions.push( {						text: 'page image',						href: pageImageUrl					} );

preloadImageHead( pageImageUrl );

const isLocal = pageInfo.imagerepository !== 'shared';

imgActions.push( {						text: 'next page',						href: getArticleUrl( 'Page:' + fn + '/' + ( parseInt( pg ) + 1 ) )					} );

const indexActions = [ makeEditHistTuple( cache.wiki.serverName,							'Index:' + fn, 'index', true ), makeFileDirectTuple( fn, imageInfo.url, isLocal,							{								name: 'file'							} ), makeEditHistTuple( cache.wiki.serverName,							'Index:' + fn + '/styles.css', 'styles', true ) ];

return [ imgActions, indexActions ]; } );		}	} );

Popups.cfg.actionHooks.push( {		name: 'diff',		onlyType: 'wiki_local',		score: function ( $l, cache ) {

if ( cache.link.getParam( 'oldid' ) ) { return 2000; }		},		actions: function ( $l, cache ) { var revPromise; var diffInt = parseInt( cache.link.getParam( 'diff' ) );

if ( cache.link.getParam( 'oldid' ) ) {

if ( !cache.link.getParam( 'diff' ) || isNaN( diffInt ) ) { // easy - the rev is the oldid revPromise = Promise.resolve( cache.link.getParam( 'oldid' ) ); }			}

const api = new Api( cache.wiki.serverName );

if ( cache.link.getParam( 'diff' ) === 'cur' ) { // we have to get the current ID as an integer revPromise = api .getCurrentRevs( cache.wiki.title ) .then( function ( revs ) {						return revs[ 0 ].revid;					} ); }

if ( !revPromise && !isNaN( diffInt ) ) { revPromise = Promise.resolve( cache.link.getParam( 'diff' ) ); }

if ( !revPromise ) { return; }

return revPromise.then( function ( rev ) {

var promises = [];

var getRevertAction = function ( compare ) { var revLink = getSpecialDiffLink( compare.torevid ); var revertSummary = 'Revert to revision ' + revLink + ' dated ' + getDatetimeFromTimestamp( compare.totimestamp ) + ' by ' + getUserLink( cache.wiki.serverName, compare.touser ); var revertToUrl = getWikiActionUrl( cache.wiki.serverName,						cache.wiki.title, 'edit', {							summary: revertSummary,							oldid: compare.torevid						} );

var revertAction = { text: 'revert to', href: revertToUrl, nopopup: true };					return revertAction; };

var getUndoAction = function ( compare ) { var url = getWikiActionUrl( cache.wiki.serverName, cache.wiki.title, 'edit',						{ undo: compare.torevid, undoafter: compare.fromrevid } );

return { text: 'undo', href: url };				};

var getRollbackAction = function ( compare ) { return { text: 'rollback', click: function { api .rollbackEdit( compare.toid, compare.touser ) .then( function {									mw.notify( 'Edit rolled back.' );								},								function ( error ) {									mw.notify( 'Rollback failed: ' + error, { type: 'error' } );								} );						}					};				};

var getPatrolAction = function ( revToPatrol ) { return { text: 'patrol', click: => { api .patrolRevision( revToPatrol ) .then( => {									mw.notify( `Revision on page ${cache.wiki.title}`, {											type: 'success', title: 'Patrolled' } );								},								( err, info ) => {									mw.notify( `Revision on page ${cache.wiki.title}:` + '\n' + info.error.info, {										title: 'Failed to patrol', type: 'error' } );								} );						}					};				};

var thankAction = function ( revToThank ) { return { text: 'thank', click: function { api .thankForRevision( revToThank ) .then( function {									mw.notify( 'User thanked.' );								},								function ( error ) {									mw.notify( 'Thank failed: ' + error, { type: 'error' } );								} );						}					};				};

var undoProm = new Api( cache.wiki.serverName ) .getSingleEditCompare( rev, [ 'ids', 'user', 'timestamp' ] ) .then( function ( compare ) {						var actions = [							getUndoAction( compare ),							getRevertAction( compare )						];

if ( haveRight( 'rollback' ) ) { actions.push( getRollbackAction( compare ) ); }

if ( haveRight( 'patrol' ) ) { actions.push( getPatrolAction( rev ) ); }

actions.push( thankAction( rev ) );

return actions; },					function { var actions = [];

if ( haveRight( 'patrol' ) ) { actions.push( getPatrolAction( rev ) ); }

actions.push( thankAction( rev ) );

return actions; } );

promises.push( undoProm );

return Promise.all( promises ) .then( function ( values ) {						var actions = [];						for ( var i = 0; i < values.length; ++i ) {							[].push.apply( actions, values[ i ] );						}						return [ actions ];					} ); } );		}	} );

function sortByScore( a, b ) { return b[ 1 ] - a[ 1 ]; }

function recognise( $elem, cache ) {

const scores = []; const proms = [];

for ( const hook of Popups.cfg.recogniserHooks ) { proms.push(				// it's OK if it returns a value, promisify it				Promise.resolve( hook.score( $elem, cache ) )					.then( function ( score ) { if ( score !== undefined ) { scores.push( [ hook, score ] ); }						}					)			);		}

return Promise.all( proms ) .then( function {

scores.sort( sortByScore );

if ( scores.length > 0 ) { const topScore = scores[ 0 ][ 1 ]; return scores[ 0 ][ 0 ].canonical( $elem, cache ) .then( function ( recognised ) {							cache.recognised = recognised;							return { recognised, score: topScore };						} ); } else { return Promise.reject; }			} );	}

function getContent( $elem, cache ) { const scores = []; const proms = [];

for ( const hook of Popups.cfg.contentHooks ) {

// skip content hooks without compatible recognised types let onlyTypes = hook.onlyType; if ( onlyTypes ) { if ( !Array.isArray( onlyTypes ) ) { onlyTypes = [ onlyTypes ]; }

if ( onlyTypes && onlyTypes.indexOf( cache.recognised.type ) === -1 ) { continue; }			}

proms.push(				// it's OK if it returns a value, promisify it				Promise.resolve( hook.score( $elem, cache ) )					.then( function ( score ) { if ( score !== undefined ) { scores.push( [ hook, score ] ); }						}					)			);		}

return Promise.all( proms ) .then( function {				scores.sort( sortByScore );

if ( scores.length > 0 ) { const topScore = scores[ 0 ][ 1 ]; console.log( `Content hook: ${scores[ 0 ][ 0 ].name}, score: ${topScore}` );

return scores[ 0 ][ 0 ].content( $elem, cache ); } else { return Promise.reject( 'No scoring content found' ); }			} );	}

function getActions( $elem, cache, callback ) { const scores = []; const proms = [];

for ( const hook of Popups.cfg.actionHooks ) {

// skip hooks without compatible recognised types let onlyTypes = hook.onlyType; if ( onlyTypes ) { if ( !Array.isArray( onlyTypes ) ) { onlyTypes = [ onlyTypes ]; }

if ( onlyTypes && onlyTypes.indexOf( cache.recognised.type ) === -1 ) { continue; }			}

proms.push(				// it's OK if it returns a value, promisify it				Promise.resolve( hook.score( $elem, cache ) )					.then( function ( score ) { if ( score !== undefined ) { scores.push( [ hook, score ] ); }						}					)			);		}

// sort the returns and resolve // TODO: should we return ASAP above, and let the caller sort, or // will that make things too jumpy? return Promise.all( proms ) .then( function {				scores.sort( sortByScore );

for ( var ii = 0; ii < scores.length; ii++ ) { const actions = scores[ ii ][ 0 ].actions( $elem, cache );

Promise.resolve( actions ) .then( function ( resolvedActions ) {							if ( resolvedActions && resolvedActions.length ) {								callback( resolvedActions );							}						} ); }			} );	}

function LinkCache( href, baseHref ) { this.href = href;

if ( href ) {

// tack on fragments if ( href.startsWith( '#' ) ) { href = baseHref.replace( /#.*$/, '' ) + href; }

try { this.url = new URL( href, baseHref );

if ( this.url.search ) { this.params = this.url.searchParams; }			} catch ( e ) { // this is OK, we just won't have a URL }		}	}

LinkCache.prototype.getParam = function ( param ) { return this.params ? this.params.get( param ) : null; };

LinkCache.prototype.hasAllParams = function ( params ) {

if ( !this.params ) { return false; }		for ( var i = 0; i < params.length; ++i ) { if ( !this.params.get( params[ i ] ) ) { return false; }		}		return true; };

function makeActionLink( action ) { const $actLink = $( '<a>' );

if ( action.href ) { $actLink.attr( 'href', action.href ); }

if ( action.click ) { $actLink.on( 'click', action.click ); }

$actLink.append( action.text );

if ( action.nopopup ) { $actLink.addClass( 'popups_nopopup' ); }

if ( action.class ) { // eslint-disable-next-line mediawiki/class-doc $actLink.addClass( action.class ); }		return $actLink; }

function PopupManager { this.popups = []; }

PopupManager.prototype.destroyPopups = function ( fromIndex ) {

fromIndex = fromIndex || 0;

for ( let i = this.popups.length - 1; i >= fromIndex; --i ) { const leaf = this.popups.pop; leaf.$element.remove; }	};

PopupManager.prototype.addPopup = function ( popup ) { this.popups.push( popup ); };

PopupManager.prototype.showPopup = function ( $elem /* event */ ) {

let $parent;

// if we get some kind of relative URL, we need to make it relative // to the right thing let baseHref;

if ( $elem.parents( '.popups_popup' ).length === 0 ) { // if this is a new root popup, remove ALL existing ones this.destroyPopups; $parent = $( document.body );

baseHref = window.location.href; } else { // the closest popup to the hovered element $parent = $elem.closest( '.popups_popup' );

const parentIndex = $parent.data( 'popups-popup-index' );

// delete any would-be siblings and their descendents this.destroyPopups( parentIndex + 1 );

baseHref = $parent.data( 'popups-base-href' ); }

// cached data that hooks can read and write var cache = new Cache;

cache.link = new LinkCache(			$elem.attr( 'href' ),			baseHref		);

// update it in case it _wasn't_ relative baseHref = cache.link.url.href;

var $popupContent = $( ' ' ).addClass( 'popups_main' ); var $popupTitle = $( ' ' ).addClass( 'popups_title' ); var $contentContainer = $( ' ' ).addClass( 'popups_content' ); var $actionsContainer = $( ' ' ).addClass( 'popups_actions' ); var $actionsList = $( '<ul>' );

// $popupTitle.append(cache.pg_name); $actionsContainer.append( $actionsList );

$popupContent .append( $popupTitle ) .append( $actionsContainer ) .append( $contentContainer );

var popup = new OO.ui.PopupWidget( {			$content: $popupContent,			$floatableContainer: $elem,			padded: false,			width: null,			autoClose: true,			align: 'left',			hideWhenOutOfView: false,			classes: [ 'popups_popup' ]		} ); popup.$element.data( 'popups-base-href', baseHref ); popup.$element.data( 'popups-popup-index', this.popups.length ); $parent.append( popup.$element );

var $throbber = $( ' ' ) .addClass( 'popups_throbber' ) .appendTo( $contentContainer );

popup.$element.on( 'keydown', function ( event ) {			console.log( event );			event.preventDefault;		} );

this.addPopup( popup );

recognise( $elem, cache ).then( function ( data ) {			// unpack			const { recognised, score } = data;			console.log( `Recognised with ${recognised.type}, score ${score}` );

var $titleLink = $( '<a>' ) .attr( 'href', recognised.href ) .append( recognised.display ) .appendTo( $popupTitle );

if ( recognised.href === cache.link.href ||				recognised.href === cache.link.href ) { $titleLink.addClass( 'popups_nopopup' ); }

$popupTitle .append(					makeCopyIcon( recognised.canonical )				);

if ( recognised.editUrl ) { $popupTitle .append(						makeEditIcon( recognised.editUrl )					); }

getContent( $elem, cache ) .then( function ( $content, $shouldBeVisible ) {					$throbber.remove;

$contentContainer.append( $content ); popup.toggle( true );

if ( $shouldBeVisible && $shouldBeVisible.length > 0 ) { $shouldBeVisible[ 0 ].scrollElementIntoView; }				} );

getActions( $elem, cache, function ( actions ) {

for ( var i = 0; i < actions.length; i++ ) {

if ( actions[ i ].length === 0 ) { continue; // skip empties }

var $list = $( '<li>' ) .appendTo( $actionsList );

if ( Array.isArray( actions[ i ] ) ) {

for ( var j = 0; j < actions[ i ].length; ++j ) {

if ( Array.isArray( actions[ i ][ j ] ) ) { $list.append( '(' );								for ( var k = 0; k < actions[ i ][ j ].length; ++k ) {									$list.append( makeActionLink( actions[ i ][ j ][ k ] ) );

if ( k < actions[ i ][ j ].length - 1 ) { $list.append( ' | ' ); }								}								$list.append( ')' );

} else { var $actLink = makeActionLink( actions[ i ][ j ] ); $list.append( $actLink ); }

if ( j < actions[ i ].length - 1 ) { $list.append( ' &middot; ' ); }						}

} else { // single action var $singleActionLink = makeActionLink( actions[ i ] ); $list.append( $singleActionLink ); }				}				popup.toggle( true ); } );		} );	};

/**	 * Set up popup handlers for a given element *	 * @param {Object} elem the DOM element */	PopupManager.prototype.setupPopups = function ( elem ) { var that = this; $( elem ).find( 'a:not(.popups_nopopup)[href]' ) // .css({"color": "red"}) .on( 'mouseenter', function ( event ) {

if ( event.altKey ) { return; }

var $enteredElem = $( this );

// $enteredElem.css({"color": "green"});

setTimeout( function {

// popup if still hovering after 500ms if ( $enteredElem.is( ':hover' ) ) { that.showPopup( $enteredElem, event ); }				}, Popups.cfg.showTimeout );			} ); };

const manager = new PopupManager;

function onMutation( mutations ) { for ( var i = 0; i < mutations.length; i++ ) { for ( var j = 0; j < mutations[ i ].addedNodes.length; j++ ) { manager.setupPopups( mutations[ i ].addedNodes[ j ] ); }		}	}

function initPopupGadget {

if ( Popups.cfg.skinDenylist.indexOf( mw.config.get( 'skin' ) ) > -1 ) { return; }

// cache user rights mw.user.getRights.then( function ( r ) {			Popups.userRights = r;		} );

// eslint-disable-next-line no-jquery/no-global-selector var body = $( 'body' )[ 0 ];

manager.setupPopups( body );

var observer = new MutationObserver( onMutation );

observer.observe( body, {			subtree: true,			childList: true		} ); }

const loadables = [ $.ready ];

mw.loader.using( [ 'mediawiki.api' ], function {

// cache user rights loadables.push( mw.loader.using( [ 'mediawiki.user' ], function { mw.user.getRights .then( function ( r ) {						Popups.userRights = r;					} ); } )		);

const api = new Api( mw.config.get( 'wgServerName' ) ); loadables.push( cacheSiteInfo( api ) );

loadables.push( mw.loader.using( [ 'mediawiki.util', 'mediawiki.ForeignApi', 'oojs-ui-core', 'oojs-ui-widgets' ] ) );

Promise.all( loadables ).then( => {			mw.hook( 'ext.gadget.popups-reloaded.config' ).fire( Popups.cfg );			initPopupGadget;		} ); } );

// eslint-disable-next-line no-undef }( jQuery, mediaWiki, Promise ) );