_ _    _ _____  ___   __                       
 __      _(_) | _(_)___ / ( _ ) / /_   ___ ___  _ __ ___  
 \ \ /\ / / | |/ / | |_ \ / _ \| '_ \ / __/ _ \| '_ ` _ \ 
  \ V  V /| |   <| |___) | (_) | (_) | (_| (_) | | | | | |
   \_/\_/ |_|_|\_\_|____/ \___/ \___(_)___\___/|_| |_| |_|

User:Certes/DisamAssist-core.js

In this article, we will explore the impact of User:Certes/DisamAssist-core.js on contemporary society. User:Certes/DisamAssist-core.js has been the subject of numerous studies and discussions, generating conflicting opinions and passionate debates. Since its inception, User:Certes/DisamAssist-core.js has captured the attention of researchers, academics and professionals from various areas, becoming a topic of universal interest. In order to fully understand its influence, we will examine its origins, evolution and repercussions on different aspects of daily life. Likewise, we will analyze society's perceptions and attitudes towards User:Certes/DisamAssist-core.js, as well as its impact in the cultural, economic and political sphere. Through this exhaustive analysis, we aim to shed light on a topic that continues to be the subject of analysis and reflection today.
//<syntaxhighlight lang="javascript">

/*
 * DisamAssist: a tool for repairing links from articles to disambiguation pages.
 */

( function( mw, $, undefined ) {
	var cfg = {};
	var txt = {};
	var startLink, ui;
	var links, pageChanges;
	var currentPageTitle, currentPageParameters, currentLink;
	var possibleBacklinkDestinations;
	var forceSamePage = false;
	var running = false;
	var choosing = false;
	var canMarkIntentionalLinks = false;
	var displayedPages = {};
	var editCount = 0;
	var editLimit;
	var pendingSaves = ;
	var pendingEditBox = null;
	var pendingEditBoxText;
	var lastEditMillis = 0;
	var runningSaves = false;
	var pageType = false;

	/*
	 * Entry point. Check whether we are in a disambiguation page. If so, add a link to start the tool
	 */
	var install = function() {
		cfg = window.DisamAssist.cfg;
		txt = window.DisamAssist.txt;
		if ( mw.config.get( 'wgAction' ) === 'view' ) {
			mw.loader.using( , function() {
				$( document ).ready( function() {
					// This is a " (disambiguation)" page
					if ( new RegExp( cfg.disamRegExp ).exec( getTitle() ) ) {
						var startMainLink = $( mw.util.addPortletLink( 'p-cactions', '#', txt.startMain, 'ca-disamassist-main' ) )
							.click( startMain );
						var startSameLink = $( mw.util.addPortletLink( 'p-cactions', '#', txt.startSame, 'ca-disamassist-same' ) )
							.click( startSame );
						startLink = startMainLink.add( startSameLink );
					} else {
						startLink = $( mw.util.addPortletLink( 'p-cactions', '#', txt.start, 'ca-disamassist-page' ) ).click( start );
					}
				} );
			} );
		}
	};
	
	/*
	 * Start the tool. Display the UI and begin looking for links to fix
	 */
	var start = function() {
		if ( !running ) {
			running = true;
			links = ;
			pageChanges = ;
			displayedPages = {};
			ensureDABExists().then( function( canMark ) {
				canMarkIntentionalLinks = canMark;
				createUI();
				addUnloadConfirm();
				markDisamOptions();
				checkEditLimit().then( function() {
					togglePendingEditBox( false );
					doPage();
				} );
			} );
		}
	};
	
	/*
	 * Start DisamAssist. Disambiguate incoming links to the current page, regardless
	 * of the title.
	 */
	var startSame = function() {
		forceSamePage = true;
		start();
	};
	
	/*
	 * Start DisamAssist. If the page title ends with " (disambiguation)", disambiguate
	 * links to the primary topic article. Otherwise, disambiguate links to the current
	 * page.
	 */
	var startMain = function() {
		forceSamePage = false;
		start();
	};
	
	/*
	 * Create and show the user interface.
	 */
	var createUI = function() {
		ui = {
			display: $( '<div></div>' ).addClass( 'disamassist-box disamassist-mainbox' ),
			finishedMessage: $( '<div></div>' ).text( txt.noMoreLinks ).hide(),
			pageTitleLine: $( '<span></span>' ).addClass( 'disamassist-pagetitleline' ),
			pendingEditCounter: $( '<div></div>').addClass( 'disamassist-editcounter' ),
			context: $( '<span></span>' ).addClass( 'disamassist-context' ),
			undoButton: createButton( txt.undo, undo ),
			omitButton: createButton( txt.omit, omit ),
			endButton: createButton( txt.close, saveAndEnd ),
			refreshButton: createButton( txt.refresh, refresh ),
			titleAsTextButton: createButton( txt.titleAsText, chooseTitleFromPrompt ),
			intentionalLinkButton: canMarkIntentionalLinks ? createButton( txt.intentionalLink, chooseIntentionalLink ) : $( '<span></span>' ),
			disamNeededButton: cfg.disamNeededText ? createButton( txt.disamNeeded, chooseDisamNeeded ) : $( '<span></span>' ),
			removeLinkButton: createButton( txt.removeLink, chooseLinkRemoval )
		};
		var top = $( '<div></div>' ).addClass( 'disamassist-top' )
			.append(  );
		var leftButtons = $( '<div></div>' ).addClass( 'disamassist-leftbuttons' )
			.append(  );
		var rightButtons = $( '<div></div>' ).addClass( 'disamassist-rightbuttons' )
			.append(  );
		var allButtons = $( '<div></div>' ).addClass( 'disamassist-allbuttons' )
			.append(  );
		ui.display.append(  );
		updateEditCounter();
		toggleActionButtons( false );
		// Insert the UI in the page
		$( '#mw-content-text' ).before( ui.display );
		ui.display.hide().fadeIn();
	};
	
	/*
	 * If there are pending changes, show a confirm dialog before closing
	 */
	var addUnloadConfirm = function() {
		$( window ).on( 'beforeunload', function( ev ) {
			if ( running && checkActualChanges() ) {
				return txt.pending;
			} else if ( editCount !== 0 ) {
				return txt.editInProgress;
			}
		});
	};
	
	/*
	 * Mark the disambiguation options as such
	 */
	var markDisamOptions = function() {
		var optionPageTitles = ;
		var optionMarkers = ;
		getDisamOptions().each( function() {
			var link = $( this );
			var title = extractPageName( link );
			var optionMarker = $( '<a></a>' ).attr( 'href', '#' ).addClass( 'disamassist-optionmarker' )
				.text( txt.optionMarker ).click( function( ev ) {
					ev.preventDefault();
					chooseReplacement( title );
				} );
			link.after( optionMarker );
			optionMarkers.push( optionMarker );
			optionPageTitles.push( title );
		} );
		// Now check the disambiguation options and display a different message for those that are
		// actually the same as the target page where the links go, as choosing those options doesn't really
		// accomplish anything (except bypassing redirects, which might be useful in some cases)
		var targetPage = getTargetPage();
		fetchRedirects( optionPageTitles.concat( targetPage ) ).done( function( redirects ) {
			var endTargetPage = resolveRedirect( targetPage, redirects );
			for ( var ii = 0; ii < optionPageTitles.length; ii++ ) {
				var endOptionTitle = resolveRedirect( optionPageTitles, redirects );
				if ( isSamePage( optionPageTitles, targetPage ) ) {
					optionMarkers.text( txt.targetOptionMarker ).addClass( 'disamassist-curroptionmarker');
				} else if ( isSamePage( endOptionTitle, endTargetPage ) ) {
					optionMarkers.text( txt.redirectOptionMarker ).addClass( 'disamassist-curroptionmarker');
				}
			}
		} ).fail( error );	
	};
	
	/*
	 * Check whether intentional links to disambiguation pages can be explicitly marked
	 * as such in this wiki. If so, ensure that a "Foo (disambiguation)" page exists.
	 * Returns a jQuery promise
	 */
	var ensureDABExists = function() {
		var dfd = new $.Deferred();
		var title = getTitle();
		// That concept doesn't exist in this wiki.
		if ( !cfg.intentionalLinkOption ) {
			dfd.resolve( false );
		// "Foo (disambiguation)" exists: it's the current page.
		} else if ( new RegExp( cfg.disamRegExp ).exec( title ) ) {
			dfd.resolve( true );
		} else {
			var disamTitle = cfg.disamFormat.replace( '$1', title );
			loadPage( disamTitle ).done( function( page ) {
				// "Foo (disambiguation)" doesn't exist.
				if ( page.missing && isDisam() && confirm("Create " + disamTitle + "?")) {
					// We try to create it
					page.content = cfg.redirectToDisam.replace( '$1', title );
					var summary = txt.redirectSummary.replace( '$1', title );
					savePage( disamTitle, page, summary, false, true ).done( function() {
						dfd.resolve( true );
					} ).fail( function( description ) {
						error( description );
						dfd.resolve( false );
					} );
				// It does exist, or we don't want to create it
				} else {
					dfd.resolve( true );
				}
			} ).fail( function( description ) {
				error( description );
				dfd.resolve( false );
			} );
		}
		return dfd.promise();
	};
	
	/*
	 * Check whether the edit cooldown applies and sets editLimit accordingly.
	 * Returns a jQuery promise
	 */
	var checkEditLimit = function() {
		var dfd = new $.Deferred();
		if ( cfg.editCooldown <= 0 ) {
			editLimit = false;
			dfd.resolve();
		} else {
			fetchRights().done( function( rights ) {
				editLimit = $.inArray( 'bot', rights ) === -1;
			} ).fail( function( description ) {
				error( description );
				editLimit = true;
			} ).always( function() {
				dfd.resolve();
			} );
		}
		return dfd.promise();
	};
	
	/*
	 * Find and ask the user to fix all the incoming links to the disambiguation ("target")
	 * page from a single "origin" page
	 */
	var doPage = function() {
		if ( pageChanges.length > cfg.historySize ) {
			applyChange( pageChanges.shift() );
		}
		if ( links.length === 0 ) {
			var targetPage = getTargetPage();
			getBacklinks( targetPage ).done( function( backlinks, pageTitles ) {
				var pending = {};
				$.each( pendingSaves, function() {
					pending] = true;
				} );
				possibleBacklinkDestinations = $.grep( pageTitles, function( t, ii) {
					if ( t == targetPage ) {
						return true;
					}
					return removeDisam(t) != targetPage;
				} );
				// Only incoming links from pages we haven't seen yet and we aren't currently
				// saving (displayedPages is reset when the tool is closed and opened again,
				// while the list of pending changes isn't; if the edit cooldown is disabled,
				// it will be empty)
				links = $.grep( backlinks, function( el, ii ) {
					return !displayedPages && !pending;
				} );
				if ( links.length === 0 ) {
					updateContext();
				} else {
					doPage();
				}
			} ).fail( error );
		} else {
			currentPageTitle = links.shift();
			displayedPages = true;
			toggleActionButtons( false );
			loadPage( currentPageTitle ).done( function( data ) {
				currentPageParameters = data;
				currentLink = null;
				doLink();
			} ).fail( error );
		}
	};
	
	/*
	 * Find and ask the user to fix a single incoming link to the disambiguation ("target")
	 * page
	 */
	var doLink = function() {
		currentLink = extractLinkToPage( currentPageParameters.content,
			possibleBacklinkDestinations, currentLink ? currentLink.end : 0 );
		if ( currentLink ) {
			updateContext();
		} else {
			doPage();
		}
	};
	
	/*
	 * Replace the target of a link with a different one
	 * pageTitle: New link target
	 * extra: Additional text after the link (optional)
	 * summary: Change summary (optional)
	 */
	var chooseReplacement = function( pageTitle, extra, summary ) {
		if ( choosing ) {
			choosing = false;
			if ( !summary ) {
				if ( pageTitle ) {
					summary = txt.summaryChanged.replace( '$1', pageTitle );		
				} else {
					summary = txt.summaryOmitted;
				}
			}
			addChange( currentPageTitle, currentPageParameters, currentPageParameters.content, currentLink, summary );
			if ( pageTitle && ( pageTitle !== getTargetPage() || extra ) ) {
				currentPageParameters.content =
					replaceLink( currentPageParameters, pageTitle, currentLink, extra || '' );
			}
			doLink();
		}
	};
	
	/*
	 * Replace the link with an explicit link to the disambiguation page
	 */
	var chooseIntentionalLink = function() {
		var disamTitle = cfg.disamFormat.replace( '$1', getTargetPage() );
		chooseReplacement( disamTitle, '', txt.summaryIntentional );
	};
	
	/*
	 * Prompt for an alternative link target and use it as a replacement
	 */
	var chooseTitleFromPrompt = function() {
		var title = prompt( txt.titleAsTextPrompt );
		if ( title !== null ) {
			chooseReplacement( title );
		}
	};
	
	/*
	 * Remove the current link, leaving the text unchanged
	 */
	var chooseLinkRemoval = function() {
		if ( choosing ) {
			var summary = txt.summaryRemoved;
			addChange( currentPageTitle, currentPageParameters, currentPageParameters.content, currentLink, summary );
			currentPageParameters.content = removeLink( currentPageParameters.content, currentLink );
			doLink();
		}
	};
	
	/*
	 * Add a "disambiguation needed" template after the link
	 */
	var chooseDisamNeeded = function() {
		chooseReplacement( currentLink.title, pageType == "sia" ? cfg.siaNeededText : cfg.disamNeededText, txt.summaryHelpNeeded );
	};
	
	/*
	 * Undo the last change
	 */
	var undo = function() {
		if ( pageChanges.length !== 0 ) {
			var lastPage = pageChanges;
			if ( currentPageTitle !== lastPage.title ) {
				links.unshift( currentPageTitle );
				currentPageTitle = lastPage.title;
			}
			currentPageParameters = lastPage.page;
			currentPageParameters.content = lastPage.contentBefore.pop();
			currentLink = lastPage.links.pop();
			lastPage.summary.pop();
			if ( lastPage.contentBefore.length === 0 ) {
				pageChanges.pop();
			}
			updateContext();
		}
	};
	
	/*
	 * Omit the current link without making a change
	 */
	var omit = function() {
		chooseReplacement( null );
	};
	
	/*
	 * Save all the pending changes and restart the tool.
	 */
	var refresh = function() {
		saveAndEnd();
		start();
	};
	
	/*
	 * Enable or disable the buttons that can perform actions on a page or change the current link.
	 * enabled: Whether to enable or disable the buttons
	 */
	var toggleActionButtons = function( enabled ) {
		var affectedButtons = [ui.omitButton, ui.titleAsTextButton, ui.removeLinkButton,
			ui.intentionalLinkButton, ui.disamNeededButton, ui.undoButton];
		$.each( affectedButtons, function( ii, button ) {
			button.prop( 'disabled', !enabled );
		} );	
	};
	
	/*
	 * Show or hide the 'no more links' message
	 * show: Whether to show or hide the message
	 */
	var toggleFinishedMessage = function( show ) {
		toggleActionButtons( !show );
		ui.undoButton.prop( 'disabled', pageChanges.length === 0 );
		ui.finishedMessage.toggle( show );
		ui.pageTitleLine.toggle( !show );
		ui.context.toggle( !show );
	};
	
	var togglePendingEditBox = function( show ) {
		if ( pendingEditBox === null ) {
			pendingEditBox = $( '<div></div>' ).addClass( 'disamassist-box disamassist-pendingeditbox' );
			pendingEditBoxText = $( '<div></div>' );
			pendingEditBox.append( pendingEditBoxText ).hide();
			if ( editLimit ) {
				pendingEditBox.append( $( '<div></div>' ).text( txt.pendingEditBoxLimited )
					.addClass( 'disamassist-subtitle' ) );
			}
			$( '#mw-content-text' ).before( pendingEditBox );
			updateEditCounter();
		}
		if ( show ) {
			pendingEditBox.fadeIn();
		} else {
			pendingEditBox.fadeOut();
		}
	};
	
	var notifyCompletion = function() {
		var oldTitle = document.title;
		document.title = txt.notifyCharacter + document.title;
		$( document.body ).one( 'mousemove', function() {
			document.title = oldTitle;
		} );
	};
	
	/*
	 * Update the displayed information to match the current link
	 * or lack thereof
	 */
	var updateContext = function() {
		updateEditCounter();
		if ( !currentLink ) {
			toggleFinishedMessage( true );
		} else {
			ui.pageTitleLine.html( txt.pageTitleLine.replace( '$1',
				mw.util.getUrl( currentPageTitle, {redirect: 'no'} ) ).replace( '$2', mw.html.escape( currentPageTitle ) ) );
			var context = extractContext( currentPageParameters.content, currentLink );
			ui.context.empty()
				.append( $( '<span></span>' ).text( context ) )
				.append( $( '<span></span>' ).text( context ).addClass( 'disamassist-inclink' ) )
				.append( $( '<span></span>' ).text( context ) );
			var numLines = Math.ceil( ui.context.height() / parseFloat( ui.context.css( 'line-height' ) ) );
			if ( numLines < cfg.numContextLines ) {
				// Add cfg.numContextLines - numLines + 1 line breaks, so that the total number
				// of lines is cfg.numContextLines
				ui.context.append( new Array( cfg.numContextLines - numLines + 2 ).join( '<br>' ) );
			}
			toggleFinishedMessage( false );
			ui.undoButton.prop( 'disabled', pageChanges.length === 0 );
			ui.removeLinkButton.prop( 'disabled', currentPageParameters.redirect );
			ui.intentionalLinkButton.prop( 'disabled', currentPageParameters.redirect );
			ui.disamNeededButton.prop( 'disabled', currentPageParameters.redirect || currentLink.hasDisamTemplate );
			choosing = true;
		}
	};
	
	/*
	 * Update the count of pending changes
	 */
	var updateEditCounter = function() {
		if ( ui.pendingEditCounter ) {
			ui.pendingEditCounter.text( txt.pendingEditCounter.replace( '$1', editCount )
				.replace( '$2', countActuallyChangedFullyCheckedPages() ) );
		}
		if ( pendingEditBox ) {
			if ( editCount === 0 && !running ) {
				togglePendingEditBox( false );
				notifyCompletion();
			}
			var textContent = editCount;
			if ( editLimit ) {
				textContent = txt.pendingEditBoxTimeEstimation.replace( '$1', editCount )
					.replace( '$2', secondsToHHMMSS( cfg.editCooldown * editCount ) );
			}
			pendingEditBoxText.text( txt.pendingEditBox.replace( '$1', textContent ) );
		}
	};
	
	/*
	 * Apply the changes made to an "origin" page
	 * pageChange: Change that will be saved
	 */
	var applyChange = function( pageChange ) {
		if ( pageChange.page.content !== pageChange.contentBefore ) {
			editCount++;
			var changeSummaries = pageChange.summary.join( txt.summarySeparator );
			var summary = txt.summary.replace( '$1', getTargetPage() ).replace( '$2', changeSummaries );
			var save = editLimit ? saveWithCooldown : savePage;
			save( pageChange.title, pageChange.page, summary, true, true ).always( function() {
				if ( editCount > 0 ) {
					editCount--;
				}
				updateEditCounter();
			} ).fail( error );
			updateEditCounter();
		}
	};
	
	/*
	 * Save all the pending changes
	 */
	var applyAllChanges = function() {
		for ( var ii = 0; ii < pageChanges.length; ii++ ) {
			applyChange( pageChanges );
		}
		pageChanges = ;
	};
	
	/*
	 * Record a new pending change
	 * pageTitle: Title of the page
	 * page: Content of the page
	 * oldContent: Content of the page before the change
	 * link: Link that has been changed
	 * summary: Change summary
	 */
	var addChange = function( pageTitle, page, oldContent, link, summary ) {
		if ( ( pageChanges.length === 0 ) || ( pageChanges.title !== pageTitle ) ) {
			pageChanges.push( {
				title: pageTitle,
				page: page,
				contentBefore: ,
				links: ,
				summary: 
			} );
		}
		var lastPageChange = pageChanges;
		lastPageChange.contentBefore.push( oldContent );
		lastPageChange.links.push( link );
		lastPageChange.summary.push( summary );
	};
	
	/*
	 * Check whether actual changes are stored in the history array
	 */
	var checkActualChanges = function() {
		return countActualChanges() !== 0;
	};
	
	/*
	 * Return the number of entries in the history array that represent actual changes
	 */
	var countActualChanges = function() {
		var changeCount = 0;
		for ( var ii = 0; ii < pageChanges.length; ii++ ) {
			if ( pageChanges.page.content !== pageChanges.contentBefore ) {
				changeCount++;
			}
		}
		return changeCount;		
	};
	
	/*
	 * Return the number of changed pages in the history array, ignoring the last entry
	 * if we aren't done with that page yet
	 */
	var countActuallyChangedFullyCheckedPages = function() {
		var changeCount = countActualChanges();
		if ( pageChanges.length !== 0 ) {
			var lastChange = pageChanges;
			if ( lastChange.title === currentPageTitle && currentLink !== null
					&& lastChange.page.content !== lastChange.contentBefore ) {
				changeCount--;
			}
		}
		return changeCount;
	};
	
	/*
	 * Find the links to disambiguation options in a disambiguation page
	 */
	var getDisamOptions = function() {
		return $( '#mw-content-text a' ).filter( function() {
			return !!extractPageName( $( this ) );
		} );
	};

	/*
	 * Save all the pending changes and close the tool
	 */
	var saveAndEnd = function() {
		applyAllChanges();
		end();
	};
	
	/*
	 * Close the tool
	 */
	var end = function() {
		var currentToolUI = ui.display;
		choosing = false;
		running = false;
		startLink.removeClass( 'selected' );
		$( '.disamassist-optionmarker' ).remove();
		currentToolUI.fadeOut( { complete: function() {
			currentToolUI.remove();
			if ( editCount !== 0 ) {
				togglePendingEditBox( true );
			}
		} } );
	};
	
	/*
	 * Display an error message
	 */
	var error = function( errorDescription ) {
		var errorBox = $( '<div></div>' ).addClass( 'disamassist-box disamassist-errorbox' );
		errorBox.text( txt.error.replace( '$1', errorDescription ) );
		errorBox.append( createButton( txt.dismissError, function() {
			errorBox.fadeOut();
		} ).addClass( 'disamassist-errorbutton' ) );
		var uiIsInPlace = ui && $.contains( document.documentElement, ui.display );
		var nextElement = uiIsInPlace ? ui.display : $( '#mw-content-text' );
		nextElement.before( errorBox );
		errorBox.hide().fadeIn();
	}
	
	/*
	 * Change a link so that it points to the title
	 * params: Page parameters (text=wikitext of the whole page, redirect=true/false)
	 * title: The new destination of the link
	 * link: The link that will be modified
	 * extra: Text that will be added after the link (optional)
	 */
	var replaceLink = function( params, title, link, extra ) {
		var newContent;
		if ( params.redirect ) {
			// ] should be replaced with ] rather than ]
			newContent = title;
		} else if ( isSamePage( title, link.description ) ) {
			// ] should be replaced with ] rather than ]
			newContent = link.description;
		} else {
			newContent = title + '|' + link.description;
		}
		var linkStart = params.content.substring( 0, link.start );
		var linkEnd = params.content.substring( link.end );
		return linkStart + ']' + link.afterDescription + ( extra || '' ) + linkEnd;
	};
	
	/*
	 * Remove a link from the text
	 * text: The wikitext of the whole page
	 * link: The link that will be removed
	 */
	var removeLink = function( text, link ) {
		var linkStart = text.substring( 0, link.start );
		var linkEnd = text.substring( link.end );
		return linkStart + link.description + link.afterDescription + linkEnd;
	};
	
	/*
	 * Extract a link from a string in wiki format,
	 * starting from a given index. Return a link if one can be found,
	 * otherwise return null. The "link" includes "disambiguation needed"
	 * templates inmediately following the link proper
	 * text: Text from which the link will be extracted
	 * lastIndex: Index from which the search will start
	 */
	var extractLink = function( text, lastIndex ) {
		// FIXME: Not an actual title regex (lots of false positives
		// and some false negatives), but hopefully good enough.
		var titleRegex = /\]/g;
		// Ditto for the template regex. Disambiguation link templates
		// should be simple enough for this not to matter, though.
		var templateRegex = /^(\w**){{\s*(+?)\s*(?:\|*?)?}}/;
		titleRegex.lastIndex = lastIndex;
		var match = titleRegex.exec( text );
		if ( match !== null && match.index !== -1 ) {
			var possiblyAmbiguous = true;
			var hasDisamTemplate = false;
			var end = match.index + 4 + match.length + ( match ? 1 + match.length : 0 );
			var afterDescription = '';
			var rest = text.substring( end );
			var templateMatch = templateRegex.exec( rest );
			if ( templateMatch !== null ) {
				var templateTitle = getCanonicalTitle( templateMatch );
				if ( $.inArray( templateTitle, cfg.disamLinkTemplates ) !== -1 ) {
					end += templateMatch.length;
					afterDescription = templateMatch.replace(/\s$/, '');
					hasDisamTemplate = true;
				} else if ( $.inArray( templateTitle, cfg.disamLinkIgnoreTemplates ) !== -1 ) {
					possiblyAmbiguous = false;
				}
			}
			return {
				start: match.index,
				end: end,
				possiblyAmbiguous: possiblyAmbiguous,
				hasDisamTemplate: hasDisamTemplate,
				title: match,
				description: match ? match : match,
				afterDescription: afterDescription
			};
		}
		return null;
	};
	
	/*
	 * Extract a link to one of a number of destination pages from a string 
	 * ("text") in wiki format, starting from a given index ("lastIndex").
	 * "Disambiguation needed" templates are included as part of the links.
	 * text: Page in wiki format
	 * destinations: Array of page titles to look for
	 * lastIndex: Index from which the search will start
	 */
	var extractLinkToPage = function( text, destinations, lastIndex ) {
		var link, title;
		do {
			link = extractLink( text, lastIndex );
			if ( link !== null ) {
				lastIndex = link.end;
				title = getCanonicalTitle( link.title );
			}
		} while ( link !== null
			&& ( !link.possiblyAmbiguous || $.inArray( title, destinations ) === -1 ) );
		return link;
	};
	
	/*
	 * Find the "target" page: either the one we are in or the "main" one found by extracting
     * the title from ".* (disambiguation)" or whatever the appropiate local format is
	 */
	var getTargetPage = function() {
		var title = getTitle();
		return forceSamePage ? title : removeDisam(title);
	};
	
	/*
	 * Get the page title, with the namespace prefix if any.
	 */
	var getTitle = function() {
		return mw.config.get( 'wgPageName' ).replace(/_/g, ' ')
	}
	
	/*
	 * Extract a "main" title from ".* (disambiguation)" or whatever the appropiate local format is
	 */
	var removeDisam = function( title ) {
		var match = new RegExp( cfg.disamRegExp ).exec( title );
		return match ? match : title;
	};
	
	/*
	 * Check whether two page titles are the same
	 */
	var isSamePage = function( title1, title2 ) {
		return getCanonicalTitle( title1 ) === getCanonicalTitle( title2 );
	};

	/*
	 * Return the 'canonical title' of a page
	 */
	var getCanonicalTitle = function( title ) {
		try {
			title = new mw.Title( title ).getPrefixedText();
		} catch ( ex ) {
			// mw.Title seems to be buggy, and some valid titles are rejected
			// FIXME: This may cause false negatives	
		}
		return title;
	};
	
	/*
	 * Extract the context around a given link in a text string
	 */
	var extractContext = function( text, link ) {
		var contextStart = link.start - cfg.radius;
		var contextEnd = link.end + cfg.radius;
		var contextPrev = text.substring( contextStart, link.start );
		if ( contextStart > 0 ) {
			contextPrev = txt.ellipsis + contextPrev;
		}
		var contextNext = text.substring( link.end, contextEnd );
		if ( contextEnd < text.length ) {
			contextNext = contextNext + txt.ellipsis;
		}
		return ;
	};

	/*
	 * Extract the prefixed page name from a link
	 */
	var extractPageName = function( link ) {
		var pageName = extractPageNameRaw( link );
		if ( pageName ) {
			var sectionPos = pageName.indexOf( '#' );
			var section = '';
			if ( sectionPos !== -1 ) {
				section = pageName.substring( sectionPos );
				pageName = pageName.substring( 0, sectionPos );
			}
			return getCanonicalTitle( pageName ) + section;
		} else {
			return null;
		}
	};
	
	/*
	 * Extract the page name from a link, as is
	 */
	var extractPageNameRaw = function( link ) {
		if ( !link.hasClass( 'image' ) ) {
			var href = link.attr( 'href' );
			if ( link.hasClass( 'new' ) ) { // "Red" link
				if ( href.indexOf( mw.config.get( 'wgScript' ) ) === 0 ) {
					return mw.util.getParamValue( 'title', href );
				}
			} else {
				var regex = mw.config.get( 'wgArticlePath' ).replace( '$1', '(.*)' );
				var regexResult = RegExp( '^' + regex + '$' ).exec( href );
				if ( $.isArray( regexResult ) && regexResult.length > 1 ) {
					return decodeURIComponent( regexResult );
				}
			}
		}
		return null;
	};
	
	/*
	 * Check whether this is a disambiguation page
	 */
	var isDisam = function() {
		var categories = mw.config.get( 'wgCategories',  );
		for ( var ii = 0; ii < categories.length; ii++ ) {
			if ( $.inArray( categories, cfg.disamCategories ) !== -1 ) {
				return "disam";
			}
			if ( $.inArray( categories, cfg.siaCategories ) !== -1 ) {
				return "sia";
			}
		}
		return false;
	};
	
	var secondsToHHMMSS = function( totalSeconds ) {
		var hhmmss = '';
		var hours = Math.floor( totalSeconds / 3600 );
		var minutes = Math.floor( totalSeconds % 3600 / 60 );
		var seconds = Math.floor( totalSeconds % 3600 % 60 );
		if ( hours >= 1 ) {
			hhmmss = pad( hours, '0', 2 ) + ':';
		}
		hhmmss += pad( minutes, '0', 2 ) + ':' + pad( seconds, '0', 2 );
		return hhmmss;
	};
	
	var pad = function( str, z, width ) {
		str = str.toString();
		if ( str.length >= width ) {
			return str;
		} else {
			return new Array( width - str.length + 1 ).join( z ) + str;
		}
	}
	
	/*
	 * Create a new button
	 * text: Text that will be displayed on the button
	 * onClick: Function that will be called when the button is clicked
	 */
	var createButton = function( text, onClick ) {
		var button = $( '<input></input>', {'type': 'button', 'value': text } );
		button.addClass( 'disamassist-button' ).click( onClick );
		return button;
	};
	
	/*
	 * Given a page title and an array of possible redirects {from, to} ("canonical titles"), find the page
	 * at the end of the redirect chain, if there is one. Otherwise, return the page title that was passed
	 */
	var resolveRedirect = function( pageTitle, possibleRedirects ) {
		var appliedRedirect = true;
		var visitedPages = {};
		var currentPage = getCanonicalTitle( pageTitle );
		while ( appliedRedirect ) {
			appliedRedirect = false;
			for ( var ii = 0; ii < possibleRedirects.length; ii++ ) {
				if ( possibleRedirects.from === currentPage ) {
					if ( visitedPages.to] ) {
						// Redirect chain detected
						return pageTitle;
					}
					visitedPages = true;
					appliedRedirect = true;
					currentPage = possibleRedirects.to;
				}
			}
		}
		// No redirect rules applied for an iteration of the outer loop:
		// no more redirects. We are done
		return currentPage;
	};
	
	/*
	 * Fetch the incoming links to a page. Returns a jQuery promise
	 * (success - array of titles of pages that contain links to the target page and
	 * array of "canonical titles" of possible destinations of the backlinks (either
	 * the target page or redirects to the target page), failure - error description)
	 * page: Target page
	 */
	var getBacklinks = function( page ) {
		var dfd = new $.Deferred();
		var api = new mw.Api();
		api.get( {
			'action': 'query',
			'list': 'backlinks',
			'bltitle': page,
			'blredirect': true,
			'bllimit': cfg.backlinkLimit,
			'blnamespace': cfg.targetNamespaces.join( '|' )
		} ).done( function( data ) {
			// There might be duplicate entries in some corner cases. We don't care,
			// since we are going to check later, anyway
			var backlinks = ;
			var linkTitles = ;
			$.each( data.query.backlinks, function() {
				backlinks.push( this.title );
				if ( this.redirlinks ) {
					linkTitles.push( this.title );
					$.each( this.redirlinks, function() {
						backlinks.push( this.title );
					} );
				}
			} );
			dfd.resolve( backlinks, linkTitles );
		} ).fail( function( code, data ) {
			dfd.reject( txt.getBacklinksError.replace( '$1', code ) );
		} );
		return dfd.promise();
	};
	
	/*
	 * Download a list of redirects for some pages. Returns a jQuery callback (success -
	 * array of redirects ({from, to}), failure - error description )
	 * pageTitles: Array of page titles
	 */
	var fetchRedirects = function( pageTitles ) {
		var dfd = new $.Deferred();
		var api = new mw.Api();
		var currentTitles = pageTitles.slice( 0, cfg.queryTitleLimit );
		var restTitles = pageTitles.slice( cfg.queryTitleLimit );
		api.get( {
			action: 'query',
			titles: currentTitles.join( '|' ),
			redirects: true
		} ).done( function( data ) {
			var theseRedirects = data.query.redirects ? data.query.redirects : ;
			if ( restTitles.length !== 0 ) {
				fetchRedirects( restTitles ).done( function( redirects ) {
					dfd.resolve( theseRedirects.concat( redirects ) );
				} ).fail( function( description ) {
					dfd.reject( description );
				} );
			} else {
				dfd.resolve( theseRedirects );
			}
		} ).fail( function( code, data ) {
			dfd.reject( txt.fetchRedirectsError.replace( '$1', code ) );
		} );
		return dfd.promise();
	};
	
	/*
	 * Download the list of user rights for the current user. Returns a
	 * jQuery promise (success - array of right names, error - error description)
	 */
	var fetchRights = function() {
		var dfd = $.Deferred();
		var api = new mw.Api();
		api.get( {
			action: 'query',
			meta: 'userinfo',
			uiprop: 'rights'
		} ).done( function( data ) {
			dfd.resolve( data.query.userinfo.rights );
		} ).fail( function( code, data ) {
			dfd.reject( txt.fetchRightsError.replace( '$1', code ) );
		} );
		return dfd.promise();
	};
	
	/*
	 * Load the raw page text for a given title. Returns a jQuery promise (success - page
	 * content, failure - error description)
	 * pageTitle: Title of the page
	 */
	var loadPage = function( pageTitle ) {
		var dfd = new $.Deferred();
		var api = new mw.Api();
		api.get( {
			action: 'query',
			titles: pageTitle,
			prop: 'revisions',
			rvprop: 'timestamp|content',
			meta: 'tokens',
			type: 'csrf'
		} ).done( function( data ) {
			var pages = data.query.pages;
			for ( var key in pages ) {
				if ( pages.hasOwnProperty( key ) ) {
					break;
				}
			}
			var rawPage = data.query.pages;
			var page = {};
			page.redirect = rawPage.redirect !== undefined;
			page.missing = rawPage.missing !== undefined;
			if ( rawPage.revisions ) {
				page.content = rawPage.revisions;
				page.baseTimeStamp = rawPage.revisions.timestamp;
			} else {
				page.content = '';
				page.baseTimeStamp = null;
			}
			page.startTimeStamp = rawPage.starttimestamp;
			page.editToken = data.query.tokens.csrftoken;
			dfd.resolve( page );
		} ).fail( function( code, data ) {
			dfd.reject( txt.loadPageError.replace( '$1', pageTitle ).replace( '$2', code ) );
		} );
		return dfd.promise();
	};
	
	/*
	 * Register changes to a page, to be saved later. Returns a jQuery promise
	 * (success - no params, failure - error description). Takes the same parameters
	 * as savePage 
	 */
	var saveWithCooldown = function() {
		var deferred = new $.Deferred();
		pendingSaves.push( {args: arguments, dfd: deferred} );
		if ( !runningSaves ) {
			checkAndSave();
		}
		return deferred.promise();
	};
	
	/*
	 * Save the first set of changes in the list of pending changes, providing that
	 * enough time has passed since the last edit
	 */
	var checkAndSave = function() {
		if ( pendingSaves.length === 0 ) {
			runningSaves = false;
			return;
		}
		runningSaves = true;
		var millisSinceLast = new Date().getTime() - lastEditMillis;
		if ( millisSinceLast < cfg.editCooldown * 1000 ) {
			setTimeout( checkAndSave, cfg.editCooldown * 1000 - millisSinceLast );
		} else {
			// The last edit started at least cfg.editCooldown seconds ago
			var save = pendingSaves.shift();
			savePage.apply( this, save.args ).done( function() {
				checkAndSave();
				save.dfd.resolve();
			} ).fail( function( description ) {
				checkAndSave();
				save.dfd.reject( description );
			} );
			// We'll use the time since the last edit started
			lastEditMillis = new Date().getTime();
		}
	};
	
	/*
	 * Save the changes made to a page. Returns a jQuery promise (success - no params,
	 * failure - error description)
	 * pageTitle: Title of the page
	 * page: Page data
	 * summary: Summary of the changes made to the page
	 * minorEdit: Whether to mark the edit as 'minor'
	 * botEdit: Whether to mark the edit as 'bot'
	 */
	var savePage = function( pageTitle, page, summary, minorEdit, botEdit ) {
		var dfd = new $.Deferred();
		var api = new mw.Api();
		api.post( {
			action: 'edit',
			title: pageTitle,
			token: page.editToken,
			text: page.content,
			basetimestamp: page.baseTimeStamp,
			starttimestamp: page.startTimeStamp,
			summary: summary,
			watchlist: cfg.watch,
			minor: minorEdit,
			bot: botEdit
		} ).done( function() {
			dfd.resolve();
		} ).fail( function( code, data ) {
			dfd.reject( txt.savePageError.replace( '$1', pageTitle ).replace( '$2', code ) );
		} );
		return dfd.promise();
	};
	
	install();
} )( mediaWiki, jQuery );

//</syntaxhighlight>