Jump to content

User:Schnark/mostEdited.js

From mediawiki.org

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)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/**
 * User:Schnark/mostEdited.js
 *
 * User script to show the pages which were edited most in the last time
 * For a documentation see [[User:Schnark/mostEdited]]
 * For the history behind this script see [[User:Schnark/October 2011 Coding Challenge]]
 *
 * @author Michael Müller ([[User:Schnark]])
 * @license GPL [//www.mediawiki.org/w/COPYING] (+ CC-BY-SA as all pages in this wiki)
 *
 *
 * Copyright (C) 2011 Michael Müller
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to
 *
 * Free Software Foundation, Inc.
 * 51 Franklin Street, Fifth Floor
 * Boston, MA   02110-1301, USA.
 *
 * <nowiki>
 */
/*global jQuery: true, mw: true */
( function ( $ ) {

// since there is no good way to get messages in a user script (yet), jsut put them here

/**
 * @var {object} fallback language
 * Please note the following things:
 * 1. en is fallback in any way
 * 2. fallback languages will be resolved only one step
 * 3. every fallback *must* have an entry in the messages structure
 * 4. this sucks
 */
var languageFallback = {
	'bar': 'de',
	'de-at': 'de',
	'de-ch': 'de',
	'de-formal': 'de',
	'dsb': 'de',
	'frr': 'de',
	'gsw': 'de',
	'hsb': 'de',
	'ksh': 'de',
	'lb': 'de',
	'nds': 'de',
	'pdc': 'de',
	'pdt': 'de',
	'pfl': 'de',
	'sli': 'de',
	'stq': 'de',
	'vmf': 'de'
};

/**
 * @var {object} messages for a language
 */

var messages = {
en: {
	// from core
	'minutes': '{{PLURAL:$1|$1 minute|$1 minutes}}',
	'hours': '{{PLURAL:$1|$1 hour|$1 hours}}',
	'days': '{{PLURAL:$1|$1 day|$1 days}}',
	'allpagessubmit': 'Go',
	'namespace': 'Namespace:',
	'invert': 'Invert selection',
	'tooltip-invert': 'Check this box to hide changes within the selected namespace (and the associated namespace if checked)',
	'namespace_association': 'Associated namespace',
	'tooltip-namespace_association': 'Check this box to also include the talk or subject namespace associated with the selected namespace',
	'blanknamespace': '(Main)',
	'namespacesall': 'all',
	'rc-change-size': '$1',
	'pagetitle': '$1 - {{SITENAME}}',

	// own messages
	'mostedited-legend': 'Most edited pages options', // legend for options on BlankPage with action=mostedited
	'mostedited': 'Most edited pages', // link in sidebar and title of the page
	'tooltip-n-mostedited': 'Shows the most edited pages', // tooltip for link in sidebar
	'mostedited-submit': 'Show most edited pages', // submit button in RecentChanges
	'mostedited-time': 'Time:', // label for time selection
	'mostedited-edits': '{{PLURAL:$1|$1 edit|$1 edits}} ($2 minor {{PLURAL:$2|edit|edits}})', // $1 - total number of edits to the page/section, $2 - number of minor edits
	'mostedited-users': '{{PLURAL:$1|$1 user|$1 users}} ($2 anonymous {{PLURAL:$2|user|users}})', // $1 - total number of different editors, $2 - number of anonymous editors
	'mostedited-size': 'Size change: $1', // $1 - formatted number
	'mostedited-no-pages': 'There are no pages with $1 or more edits in the selected period.', // $1 - number of edits a page must have at least to get shown
	'mostedited-increasing': 'The number of edits seems to be increasing.',
	'mostedited-unchanging': 'The number of edits seems not to change.',
	'mostedited-decreasing': 'The number of edits seems to be decreasing.',
	'mostedited-changed-period': '(changed to: $1)',
	'mostedited-changed-period-tooltip': 'The period had to be shortened because there are too many edits.'
	},

de: {
	'minutes': '{{PLURAL:$1|$1 Minute|$1 Minuten}}',
	'hours': '{{PLURAL:$1|$1 Stunde|$1 Stunden}}',
	'days': '{{PLURAL:$1|$1 Tag|$1 Tage}}',
	'allpagessubmit': 'Anwenden',
	'namespace': 'Namensraum:',
	'invert': 'Auswahl umkehren',
	'tooltip-invert': 'Dieses Auswahlfeld anklicken, um Änderungen im gewählten Namensraum und, sofern ausgewählt, dem entsprechenden zugehörigen Namensraum auszublenden',
	'namespace_association': 'Zugeordneter Namensraum',
	'tooltip-namespace_association': 'Dieses Auswahlfeld anklicken, um den deiner Auswahl zugehörigen Diskussionsnamensraum, oder im umgekehrten Fall, den zugehörigen Namensraum, mit einzubeziehen',
	'blanknamespace': '(Seiten)',
	'namespacesall': 'alle',
	'rc-change-size': '$1 {{PLURAL:$1|Byte|Bytes}}',
	'pagetitle': '$1 – {{SITENAME}}',
	'mostedited-legend': 'Anzeigeptionen',
	'mostedited': 'Meiste Änderungen',
	'tooltip-n-mostedited': 'Zeigt die Seiten mit den meisten Änderungen an',
	'mostedited-submit': 'Zeige Seiten mit meisten Änderungen',
	'mostedited-time': 'Zeit:',
	'mostedited-edits': '{{PLURAL:$1|$1 Bearbeitung|$1 Bearbeitungen}} ($2 kleinere {{PLURAL:$2|Bearbeitung|Bearbeitungen}})',
	'mostedited-users': '$1 Benutzer ({{PLURAL:$2|$2 anonymer|$2 anonyme}})',
	'mostedited-size': 'Größenänderung: $1',
	'mostedited-no-pages': 'Keine Seite wurde im ausgewählten Zeitraum $1 Mal oder häufiger bearbeitet.',
	'mostedited-increasing': 'Die Anzahl der Bearbeitungen scheint zuzunehmen.',
	'mostedited-unchanging': 'Die Anzahl der Bearbeitungen scheint gleich zu bleiben.',
	'mostedited-decreasing': 'Die Anzahl der Bearbeitungen scheint abzunehmen.',
	'mostedited-changed-period': '(geändert in: $1)',
	'mostedited-changed-period-tooltip': 'Die Zeit musste gekürzt werden, da zu viele Bearbeitungen stattfanden.'
	},

'de-ch': {
	'mostedited-size': 'Grössenänderung: $1'
	}

};

// set messages for the user's language
// @TODO once there is a better way to do this (new Gadget extension etc.) switch to that way
mw.messages.set( messages.en );
if ( mw.config.get( 'wgUserLanguage' ) in languageFallback ) {
	mw.messages.set( messages[languageFallback[mw.config.get( 'wgUserLanguage' )]] );
}
if ( mw.config.get( 'wgUserLanguage' ) in messages ) {
	mw.messages.set( messages[mw.config.get( 'wgUserLanguage' )] );
}
// allow users to bind to this event to set messages for their language
$( document ).trigger( 'mostedited-setmessages' );

/**
 * @var {object} pagesList contains information for every edited page in the form
 * 'Pagename': {
 *	oldsize: 1234, // size of oldest version
 *	newsize: 4321, // size of newest version
 *	edits: 123, // number of edits
 *	minor: 23, // number of minor edits
 *	users: ['A', 'B'], // all editors
 *	anons: 5, // number of anonymous editors
 *	time: 987654321, // sum of all timestamps (in seconds before now)
 *	sections: { // data for each section
 *		'Section A': {
 *			edits: 12,
 *			minor: 2,
 *			users: ['A'],
 *			anons: 2,
 *			time: 87654321
 *		}
 *	}
 * }
 */

var pagesList = {};

/**
 * @var {number} current time, time of first edit (milliseconds since 1970-01-01)
 */
var currTime = 0, firstTime = 0;

// helper and format functions

/**
 * gets a message (as an HTML fragment), overridden with a wikitext parser when needed
 * @param msgName {string}
 * @param parameters {string}
 * @return {string}
 */
function msg ( /* msgName, parameters */ ) {
	return mw.msg.apply( mw, arguments );
}

/**
 * converts a timestamp (YYYY-MM-DDTHH:MM:SSZ) into milliseconds since 1970-01-01
 * @param timestamp {string}
 * @return {number}
 */
function getTime ( timestamp ) {
	return ( new Date( timestamp.slice( 0, 4 ), timestamp.slice( 5, 7 ) - 1, timestamp.slice( 8, 10 ),
		timestamp.slice( 11, 13 ), timestamp.slice( 14, 16 ), timestamp.slice( 17, 19 ) ) ).getTime();
}

/**
 * encodes a section name to be used as anchor for a link to this section
 * @param section {string} title of the section
 * @return {string} encoded anchor
 */
function encodeSectionLink ( section ) {
	return mw.util.rawurlencode( section.replace( / /g, '_' )  )
		.replace( /%3A/g, ':' )
		.replace( /%/g, '.' )
		.replace( /^([^a-zA-Z])/, 'x$1' );
}

/**
 * formats a size change (see ChangesList.php for the original)
 * @TODO commafy
 * @param diff {number} difference between old and new size
 * @return {string} formatted HTML
 */
function showCharacterDifference ( diff ) {
	var cssClass, sign = '';
	if ( diff < 0 ) {
		cssClass = 'mw-plusminus-neg';
	} else if ( diff > 0 ) {
		sign = '+';
		cssClass = 'mw-plusminus-pos';
	} else {
		cssClass = 'mw-plusminus-null';
	}
	return mw.html.element( 'span',
		{'class': cssClass, dir: 'ltr'},
		msg( 'rc-change-size', sign + diff ) );
}

/**
 * determins whether the edits are increasing or decreasing
 * @param data {object} object with entries edits and time
 * @return {string} arrow symbol in a <span> with class and tooltip
 */
function getTrend ( data ) {
	var	avgTime = data.time / data.edits,
		ratio = avgTime / (firstTime - currTime),
		cssClass, tooltipMsg, arrow;
	if ( ratio < 0.4 ) {
		cssClass = 'mw-plusminus-pos';
		tooltipMsg = 'mostedited-increasing';
		arrow = $( 'body' ).is( '.rtl' ) ? '↖' : '↗';
	} else if ( ratio < 0.6 ) {
		cssClass = 'mw-plusminus-null';
		tooltipMsg = 'mostedited-unchanging';
		arrow = $( 'body' ).is( '.rtl' ) ? '←' : '→';
	} else {
		cssClass = 'mw-plusminus-neg';
		tooltipMsg = 'mostedited-decreasing';
		arrow = $( 'body' ).is( '.rtl' ) ? '↙' : '↘';
	}
	return mw.html.element( 'span', {'class': cssClass, title: msg( tooltipMsg )}, arrow );
}

/**
 * formats a period of time
 * @param hours {number} hours
 * @return {string} formatted period
 */
function formatPeriod ( hours ) {
	var dayString = '', hourString = '', minuteString = '';
	if ( hours >= 24 ) {
		var days = Math.floor( hours / 24 );
		hours -= days * 24;
		hours = Math.round( hours );
		if ( hours === 24 ) {
			days += 1;
			hours = 0;
		}
		dayString = msg( 'days', days );
		if ( hours > 0 ) {
			hourString = ' ' + msg( 'hours', hours );
		}
		return dayString + hourString;
	} else {
		var	wholeHours = Math.floor( hours ),
			minutes = Math.round( 60 * ( hours - wholeHours ) );
		if ( minutes === 60 ) {
			wholeHours += 1;
			minutes = 0;
		}
		if ( wholeHours > 0 ) {
			hourString = msg( 'hours', wholeHours );
		}
		if ( wholeHours === 0 || minutes > 0 ) {
			minuteString = msg( 'minutes', minutes );
		}
		if ( hourString !== '' && minuteString !== '' ) {
			hourString += ' ';
		}
		return hourString + minuteString;
	}
}

// main

/**
 * This function gets all recent changes in the namespaces starting at the start time.
 * After the last API call has been done it will call the callback function
 * @param end {string} end time (timestamp)
 * @param namespaces {string} namespaces to show, either empty for all or something like '0|1|5'
 * @param maxcalls {number} maximal number of API calls
 * @param callback {function} function after the last API call, called with one parameter:
 *          true if all edits until end were retrieved, false if aborted earlier
 * @param start {string} start time (timestamp), leave empty for first call
 */
function getAPIRecentChanges ( end, namespaces, maxcalls, callback, start ) {
	var data = {
		action: 'query',
		rawcontinue: '',
		list: 'recentchanges',
		rcend: end,
		rclimit: 'max',
		rcprop: 'user|comment|title|sizes|flags|timestamp',
		rctype: 'edit|new',
		format: 'json'
	};
	if ( start ) {
		data.rcstart = start;
	} else {
		data.meta = 'siteinfo';
		data.siprop = 'general';
	}
	if ( namespaces ) {
		data.rcnamespace = namespaces;
	}
	$.getJSON( mw.util.wikiScript( 'api' ), data, function ( json ) {
		if ( json && json.query && json.query.general ) {
			currTime = getTime( json.query.general.time );
		}
		if ( json && json.query && json.query.recentchanges ) {
			var rc = json.query.recentchanges;
			for ( var i = 0; i < rc.length; i++ ) {
				var edit = rc[i];
				if ( !( edit.title in pagesList ) ) {
					pagesList[edit.title] = {
						newsize: edit.newlen, // the first is the latest edit, so newlen is the most recent size
						edits: 0,
						minor: 0,
						users: [],
						anons: 0,
						time: 0,
						sections: {}
					};
				}
				var section = /^\/\*\s*(.*?)\s*\*\//.exec( edit.comment ); // title of the section
				if ( section ) {
					section = section[1];
				}
				if ( section ) {
					if ( !( section in pagesList[edit.title].sections ) ) {
						pagesList[edit.title].sections[section] = {
							edits: 0,
							minor: 0,
							users: [],
							anons: 0,
							time: 0
						};
					}
				}
				var time = getTime( edit.timestamp );
				firstTime = time;
				pagesList[edit.title].edits++; // increment edits
				if ( section ) {
					pagesList[edit.title].sections[section].edits++;
				}
				if ( edit.minor === '' ) { // increment minor edits
					pagesList[edit.title].minor++;
					if ( section ) {
						pagesList[edit.title].sections[section].minor++;
					}
				}
				pagesList[edit.title].oldsize = edit.oldlen; // update oldlen for every edit, only the earliest (= last) is interesting
				if ( $.inArray( edit.user, pagesList[edit.title].users ) === -1 ) { // store if new user
					pagesList[edit.title].users.push( edit.user );
					if ( edit.anon === '' ) {
						pagesList[edit.title].anons++;
					}
				}
				if ( section ) {
					if ( $.inArray( edit.user, pagesList[edit.title].sections[section].users ) === -1 ) {
						pagesList[edit.title].sections[section].users.push( edit.user );
						if ( edit.anon === '' ) {
							pagesList[edit.title].sections[section].anons++;
						}
					}
				}
				pagesList[edit.title].time += (time - currTime);
				if ( section ) {
					pagesList[edit.title].sections[section].time += (time - currTime);
				}
			}
		}
		if ( json && json['query-continue'] && json['query-continue'].recentchanges ) {
			if ( maxcalls > 1 ) {
				getAPIRecentChanges( end, namespaces, maxcalls - 1, callback, json['query-continue'].recentchanges.rcstart );
			} else {
				callback( false );
			}
		} else {
			callback( true );
		}
	} );
}

/**
 * get the pages/sections with the most edits
 * @param data {object} data about the number of edits, entries must have the form
 *	'Name': {edits: 123}
 * @param count {number} number of pages/sections to get
 * @param edits {number} number of edits needed at least to output a page/section
 * @return {array} list of the pages/sections with the most edits (decreasing order)
 */
function getMostEdited ( data, count, edits ) {
	var items = [];
	for ( var item in data ) {
		items.push( item );
	}
	items.sort( function ( a, b ) {
		return data[b].edits - data[a].edits;
	} );
	var output = items.slice( 0, count );
	while ( output.length > 0 && data[output[output.length - 1]].edits < edits ) {
		output.pop();
	}
	return output;
}

// functions generating HTML

/**
 * get HTML for one entry
 * @param page {string} name of the page to get HTML for
 * @param count {number} number of sections to show
 * @param edits {number} number of edits a section must have at least
 * @return {string} HTML
 */
function generatePageHTML ( page, count, edits ) {
	var	data = pagesList[page],
		html = mw.html.element( 'h2', {}, new mw.html.Raw(
			mw.html.element( 'a', {href: mw.util.getUrl( page ), title: page}, page ) +
			getTrend( data ) ) );
	html += mw.html.element( 'ul', {}, new mw.html.Raw(
		mw.html.element( 'li', {}, new mw.html.Raw(
			msg( 'mostedited-edits', data.edits, data.minor ) ) ) +
		mw.html.element( 'li', {}, new mw.html.Raw(
			msg( 'mostedited-users', data.users.length, data.anons ) ) ) +
		mw.html.element( 'li', {}, new mw.html.Raw(
//FIXME: showCharacterDifference returns an HTML fragment which is escaped by the parser from mediawiki.jqueryMsg.js in MediaWiki 1.19+,
//but not in 1.18. Since the parser from mediawiki.js never escapes HTML (at least not in 1.19 or earlier) use mw.msg here instead of msg.
			mw.msg( 'mostedited-size', showCharacterDifference( data.newsize - data.oldsize ) ) ) ) ) );
	var sections = getMostEdited( data.sections, count, edits );
	for ( var i = 0; i < sections.length; i++ ) {
		var section = sections[i], sectionData = data.sections[section];
		html += mw.html.element( 'h3', {}, new mw.html.Raw(
			mw.html.element( 'a',
				{href: mw.util.getUrl( page ) + '#' + encodeSectionLink( section )},
				section ) +
			getTrend( sectionData ) ) );
		html += mw.html.element( 'ul', {}, new mw.html.Raw(
			mw.html.element( 'li', {}, new mw.html.Raw( msg( 'mostedited-edits', sectionData.edits, sectionData.minor ) ) ) +
			mw.html.element( 'li', {}, new mw.html.Raw( msg( 'mostedited-users', sectionData.users.length, sectionData.anons ) ) ) ) );
	}
	return html;
}

/**
 * get HTML for complete list
 * @param countPages {number} number of pages to show
 * @param countSections {number} number of sections to show for each page
 * @param editPages {number} number of edits a page must have at least
 * @param editSections {number} number of edits a section must have at least
 * @return {string} HTML
 */
function generateHTML ( countPages, countSections, editPages, editSections ) {
	var	pages = getMostEdited ( pagesList, countPages, editPages ),
		html = '';
	if ( pages.length === 0 ) {
		html += msg( 'mostedited-no-pages', editPages );
	} else {
		for ( var i = 0; i < pages.length; i++ ) {
			html += generatePageHTML( pages[i], countSections, editSections );
		}
	}
	return html;
}

/**
 * get HTML for header
 * @return {string} HTML
 */
function generateHeaderHTML () {
	$( '#firstHeading' ).text( msg( 'mostedited' ) );
	var	i,
		html = '',
		legend = '',
		labelTime = '',
		selectTime = '',
		labelNamespaces = '',
		selectNamespaces = '',
		invert = '',
		associated = '',
		submit = '',
		hours,
		times = [],
		optionsTime = [],
		optionsNamespaces = [],
		formattedNamespaces = mw.config.get( 'wgFormattedNamespaces' );
	legend = mw.html.element( 'legend', {}, msg( 'mostedited-legend' ) );
	labelTime = mw.html.element( 'label', {'for': 'time'}, msg( 'mostedited-time' ) );

	hours = parseFloat( mw.util.getParamValue( 'hours' ) || '0', 10 );
	if ( hours <= 0 ) {
		hours = 1;
	}
	times = [0.25, 0.5, 1, 2, 24];
	if ( $.inArray( hours, times ) === -1 ) {
		times.push( hours );
		times.sort( function ( a, b ) {
			return a - b;
		} );
	}
	for ( i = 0; i < times.length; i++ ) {
		optionsTime.push( mw.html.element( 'option',
			{value: times[i], selected: times[i] === hours},
			formatPeriod( times[i] ) ) );
	}
	selectTime = mw.html.element( 'select',
		{id: 'time', name: 'time', 'class': 'timeselector'},
		new mw.html.Raw( optionsTime.join( '' ) ) );
	labelNamespaces = mw.html.element( 'label', {'for': 'namespace'}, msg( 'namespace' ) );
	optionsNamespaces.push( mw.html.element( 'option', {value: ''}, msg( 'namespacesall' ) ) );
	for ( i in formattedNamespaces ) {
		if ( i < 0 ) {
			continue;
		}
		var namespace = formattedNamespaces[i];
		if ( namespace === '' ) {
			namespace = msg( 'blanknamespace' );
		}
		optionsNamespaces.push( mw.html.element( 'option',
			{value: i, selected: mw.util.getParamValue( 'namespace' ) === i}, // both are strings
			namespace ) );
	}
	selectNamespaces = mw.html.element( 'select',
		{id: 'namespace', name: 'namespace', 'class': 'namespaceselector'},
		new mw.html.Raw( optionsNamespaces.join( '' ) ) );
	invert = mw.html.element( 'input',
		{name: 'invert', value: 1, id: 'nsinvert', type: 'checkbox', title: msg( 'tooltip-invert' ),
			checked: mw.util.getParamValue( 'invert' ) === '1'} ) +
		'&nbsp;' +
		mw.html.element( 'label', {'for': 'nsinvert', title: msg( 'tooltip-invert' )}, msg( 'invert' ) );
	associated = mw.html.element( 'input',
		{name: 'associated', value: 1, id: 'nsassociated', type: 'checkbox', title: msg( 'tooltip-namespace_association' ),
			checked: mw.util.getParamValue( 'associated' ) === '1'} ) +
		'&nbsp;' +
		mw.html.element( 'label',
            {'for': 'nsassociated', title: msg( 'tooltip-namespace_association' )},
            msg( 'namespace_association' ) );
	submit = mw.html.element( 'input', {type: 'button', id: 'submitButton', value: msg( 'allpagessubmit' )} );
	html += '<fieldset class="rcoptions">' + // structure copied from HTML of Special:RecentChanges
		legend +
		'<table class="mw-recentchanges-table"><tbody><tr><td class="mw-label">' +
		labelTime +
		'</td><td class="mw-input">' +
		selectTime +
		' <span id="mostedited-real-time"></span>' +
		'</td></tr><tr><td class="mw-label">' +
		labelNamespaces +
		'</td><td class="mw-input">' +
		selectNamespaces +
		' ' +
		invert +
		' ' +
		associated +
		' ' +
		submit +
		'</td></tr></tbody></table></fieldset>';
	html += mw.html.element( 'div', {id: 'mostEditedContainer'} );
	return html;
}

// functions to interact with user

/**
 * reads user input from form elements and URL, sets default
 * @return {object} object with all parameters
 */
function readUserInput () {
	var	namespace = $( '#namespace option:selected' ).val(),
		invert = $( '#nsinvert' ).prop( 'checked' ),
		associated = $( '#nsassociated' ).prop( 'checked' ),
		namespaces,
		hours = $( '#time option:selected' ).val(),
		ago = new Date( ( new Date() ).getTime() - ( hours * 3600000 ) ),
		end = String( ago.getUTCFullYear() ) + '-' +
			String( ago.getUTCMonth() + 101 ).substr( 1 ) + '-' +
			String( ago.getUTCDate() + 100 ).substr( 1 ) + 'T' +
			String( ago.getUTCHours() + 100 ).substr ( 1 ) + ':' +
			String( ago.getUTCMinutes() + 100 ).substr( 1 ) + ':' +
			String( ago.getUTCSeconds() + 100 ).substr( 1 ) + 'Z';
	if ( namespace === '' ) {
		namespaces = ''; // all
	} else {
		namespace = Number( namespace );
		var list = [namespace];
		if ( associated ) {
			list.push(
				( namespace % 2 === 0 ) ?
					namespace + 1 :
					namespace - 1 );
		}
		if ( invert ) {
			namespaces = [];
			var formattedNamespaces = mw.config.get( 'wgFormattedNamespaces' );
			for ( var i in formattedNamespaces ) {
				if ( i >= 0 && $.inArray( Number( i ), list ) === -1 ) {
					namespaces.push( i );
				}
			}
			namespaces = namespaces.join( '|' );
		} else {
			namespaces = list.join( '|' );
		}
	}
	return {
		namespaces: namespaces,
		end: end,
		maxCalls: mw.util.getParamValue( 'max-calls' ) || 5,
		limit: mw.util.getParamValue( 'limit' ) || 10,
		sectionLimit: mw.util.getParamValue( 'section-limit' ) || 3,
		edits: mw.util.getParamValue( 'edits' ) || 2,
		sectionEdits: mw.util.getParamValue( 'section-edits' ) || 2
	};
}

/**
 * called when user clicks submit button
 */
function submitQuery () {
	var params = readUserInput();
	$( '#submitButton' ).prop( 'disabled', true );
	// $( '#mostEditedContainer' ).empty().injectSpinner( 'mostedited' ); This needs MW 1.19, so use old wikibits.js
	window.injectSpinner( $( '#mostEditedContainer' ).empty().get( 0 ), 'mostedited' );
	pagesList = {}; // empty
	getAPIRecentChanges( params.end, params.namespaces, params.maxCalls, function ( done ) {
		var $realTime = $( '#mostedited-real-time' );
		if ( done ) {
			firstTime = getTime( params.end );
			$realTime.text( '' );
		} else {
			$realTime.html( mw.html.element( 'span',
				{title: msg( 'mostedited-changed-period-tooltip' )},
				msg( 'mostedited-changed-period', formatPeriod( ( currTime - firstTime ) / 3600000 ) ) ) );
		}
		$( '#mostEditedContainer' ).html( generateHTML( params.limit, params.sectionLimit, params.edits, params.sectionEdits ) );
		// $.removeSpinner( 'mostedited' ); This needs MW 1.19, so use old wikibits.js
		window.removeSpinner( 'mostedited' );
		$( '#submitButton' ).prop( 'disabled', false );
	} );
}

// initialise

/**
 * initialises the interface on Special:Blankpage
 */
function initBlankpage () {
	document.title = msg( 'pagetitle', msg( 'mostedited' ) );
	var $content = mw.util.$content.children( 'p' ); // don't clear away subtitle, newtalk and jumpto
	if ( $content.length !== 1 ) {
		$content = mw.util.$content;
	}
	$content.html( generateHeaderHTML() );
	mw.loader.load( 'mediawiki.special.recentchanges' ); // enables/disables checkboxes
	$( '#submitButton' ).click( submitQuery ).click();
}

/**
 * initialises the interface on Special:RecentChanges
 */
function initRecentchanges () {
	var $button = $( mw.html.element( 'input', {type: 'button', value: msg( 'mostedited-submit' )} ) )
		.click( function () {
			var	namespace = $( '#namespace option:selected' ).val(),
				invert = $( '#nsinvert' ).prop( 'checked' ) ? '1' : '0',
				associated = $( '#nsassociated' ).prop( 'checked' ) ? '1' : '0';
			document.location.href = mw.util.getUrl( 'Special:BlankPage' ) + '?' +
				$.param( {action: 'mostedited', namespace: namespace, invert: invert, associated: associated} );
		} );
	$( 'input[type="submit"]' ).eq( 0 ).after( $button ); // FIXME breaks when there is another submit button before it
}

/**
 * initialises the sidebar everywhere
 */
function initSidebar () {
	var portlet = $( '#n-recentchanges' ).parents( '.portlet, .portal' ).attr( 'id' ) || 'p-navigation';
	mw.util.addPortletLink( portlet,
		mw.util.getUrl( 'Special:BlankPage' ) + '?action=mostedited',
		msg( 'mostedited' ),
		'n-mostedited',
		msg( 'tooltip-n-mostedited' ),
		null, // access key
		'#n-recentchanges' );
}

$( initSidebar );

if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Recentchanges' ) {
	$( initRecentchanges );
}

if ( mw.config.get( 'wgCanonicalSpecialPageName' ) === 'Blankpage' &&
	mw.util.getParamValue( 'action' ) === 'mostedited' )
{
	mw.loader.using( [ 'mediawiki.jqueryMsg' /*, 'jquery.spinner' this needs MW 1.19 */ ],
		function () {
/**
 * replace msg with a version that handles {{SITENAME}} and {{PLURAL:}}
 * this is only needed here, not in the other cases, where the original mw.msg is just fine
 * @TODO remove this once MediaWiki handles this itself
 */
			msg = mw.jqueryMsg.getMessageFunction( {
				magic: {
					'SITENAME': mw.config.get( 'wgSiteName' )
				}
			} );
			$( initBlankpage );
		} );
}


} )( jQuery );
// </nowiki>