மீடியாவிக்கி:Gadget-Twinkle.js
/**
  * +-------------------------------------------------------------------------+
  * |                  === WARNING: GLOBAL GADGET FILE ===                    |
  * |                Changes to this page affect many users.                  |
  * |           Please discuss changes at [[WT:TW]] before editing.           |
  * +-------------------------------------------------------------------------+
  *
  * Imported from github [https://github.com/wikimedia-gadgets/twinkle].
  * All changes should be made in the repository, otherwise they will be lost.
  *
  * ----------
  *
  * This is AzaToth's Twinkle, the popular script sidekick for newbies, admins, and
  * every Wikipedian in between. Visit [[WP:TW]] for more information.
  */
 // &lt;nowiki&gt;
 
 /* global Morebits */
 
 (function (window, document, $) { // Wrap with anonymous function
 
 // Check if account is experienced enough to use Twinkle
 if (!Morebits.userIsInGroup('autoconfirmed') &amp;&amp; !Morebits.userIsInGroup('confirmed')) {
 	return;
 }
 
 var Twinkle = {};
 window.Twinkle = Twinkle;  // allow global access
 
 Twinkle.initCallbacks = [];
 /**
  * Adds a callback to execute when Twinkle has loaded.
  * @param {function} func
  * @param {string} [name] - name of module used to check if is disabled.
  * If name is not given, module is loaded unconditionally.
  */
 Twinkle.addInitCallback = function twinkleAddInitCallback(func, name) {
 	Twinkle.initCallbacks.push({ func: func, name: name });
 };
 
 Twinkle.defaultConfig = {};
 /**
  * This holds the default set of preferences used by Twinkle.
  * It is important that all new preferences added here, especially admin-only ones, are also added to
  * |Twinkle.config.sections| in twinkleconfig.js, so they are configurable via the Twinkle preferences panel.
  * For help on the actual preferences, see the comments in twinkleconfig.js.
  *
  * Formerly Twinkle.defaultConfig.twinkle and Twinkle.defaultConfig.friendly
  */
 Twinkle.defaultConfig = {
 	// General
 	userTalkPageMode: 'tab',
 	dialogLargeFont: false,
 	disabledModules: [],
 	disabledSysopModules: [],
 
 	// ARV
 	spiWatchReport: 'yes',
 
 	// Block
 	defaultToPartialBlocks: false,
 	blankTalkpageOnIndefBlock: false,
 
 	// Fluff (revert and rollback)
 	autoMenuAfterRollback: false,
 	openTalkPage: [ 'agf', 'norm', 'vand' ],
 	openTalkPageOnAutoRevert: false,
 	rollbackInPlace: false,
 	markRevertedPagesAsMinor: [ 'vand' ],
 	watchRevertedPages: [ 'agf', 'norm', 'vand', 'torev' ],
 	watchRevertedExpiry: '1 month',
 	offerReasonOnNormalRevert: true,
 	confirmOnFluff: false,
 	confirmOnMobileFluff: true,
 	showRollbackLinks: [ 'diff', 'others' ],
 
 	// DI (twinkleimage)
 	notifyUserOnDeli: true,
 	deliWatchPage: '1 month',
 	deliWatchUser: '1 month',
 
 	// Protect
 	watchRequestedPages: 'yes',
 	watchPPTaggedPages: 'default',
 	watchProtectedPages: 'default',
 
 	// PROD
 	watchProdPages: '1 month',
 	markProdPagesAsPatrolled: false,
 	prodReasonDefault: '',
 	logProdPages: false,
 	prodLogPageName: 'PROD log',
 
 	// CSD
 	speedySelectionStyle: 'buttonClick',
 	watchSpeedyPages: [ 'g3', 'g5', 'g10', 'g11', 'g12' ],
 	watchSpeedyExpiry: '1 month',
 	markSpeedyPagesAsPatrolled: false,
 
 	// these next two should probably be identical by default
 	welcomeUserOnSpeedyDeletionNotification: [ 'db', 'g1', 'g2', 'g3', 'g4', 'g6', 'g10', 'g11', 'g12', 'g13', 'g14', 'a1', 'a2', 'a3', 'a5', 'a7', 'a9', 'a10', 'a11', 'f1', 'f2', 'f3', 'f7', 'f9', 'f10', 'u3', 'u5', 'p1', 'p2' ],
 	notifyUserOnSpeedyDeletionNomination: [ 'db', 'g1', 'g2', 'g3', 'g4', 'g6', 'g10', 'g11', 'g12', 'g13', 'g14', 'a1', 'a2', 'a3', 'a5', 'a7', 'a9', 'a10', 'a11', 'f1', 'f2', 'f3', 'f7', 'f9', 'f10', 'u3', 'u5', 'p1', 'p2' ],
 	warnUserOnSpeedyDelete: [ 'db', 'g1', 'g2', 'g3', 'g4', 'g6', 'g10', 'g11', 'g12', 'g13', 'g14', 'a1', 'a2', 'a3', 'a5', 'a7', 'a9', 'a10', 'a11', 'f1', 'f2', 'f3', 'f7', 'f9', 'f10', 'u3', 'u5', 'p1', 'p2' ],
 	promptForSpeedyDeletionSummary: [],
 	deleteTalkPageOnDelete: true,
 	deleteRedirectsOnDelete: true,
 	deleteSysopDefaultToDelete: false,
 	speedyWindowHeight: 500,
 	speedyWindowWidth: 800,
 	logSpeedyNominations: false,
 	speedyLogPageName: 'CSD log',
 	noLogOnSpeedyNomination: [ 'u1' ],
 
 	// Unlink
 	unlinkNamespaces: [ '0', '10', '100', '118' ],
 
 	// Warn
 	defaultWarningGroup: '1',
 	combinedSingletMenus: false,
 	showSharedIPNotice: true,
 	watchWarnings: '1 month',
 	oldSelect: false,
 	customWarningList: [],
 
 	// XfD
 	logXfdNominations: false,
 	xfdLogPageName: 'XfD log',
 	noLogOnXfdNomination: [],
 	xfdWatchDiscussion: 'default',
 	xfdWatchList: 'no',
 	xfdWatchPage: '1 month',
 	xfdWatchUser: '1 month',
 	xfdWatchRelated: '1 month',
 	markXfdPagesAsPatrolled: true,
 
 	// Hidden preferences
 	autolevelStaleDays: 3, // Huggle is 3, CBNG is 2
 	revertMaxRevisions: 50, // intentionally limited
 	batchMax: 5000,
 	batchChunks: 50,
 
 	// Deprecated options, as a fallback for add-on scripts/modules
 	summaryAd: ' ([[WP:TW|TW]])',
 	deletionSummaryAd: ' ([[WP:TW|TW]])',
 	protectionSummaryAd: ' ([[WP:TW|TW]])',
 
 	// Formerly defaultConfig.friendly:
 	// Tag
 	groupByDefault: true,
 	watchTaggedVenues: ['articles', 'drafts', 'redirects', 'files'],
 	watchTaggedPages: '1 month',
 	watchMergeDiscussions: '1 month',
 	markTaggedPagesAsMinor: false,
 	markTaggedPagesAsPatrolled: true,
 	tagArticleSortOrder: 'cat',
 	customTagList: [],
 	customFileTagList: [],
 	customRedirectTagList: [],
 
 	// Welcome
 	topWelcomes: false,
 	watchWelcomes: '3 months',
 	insertUsername: true,
 	quickWelcomeMode: 'norm',
 	quickWelcomeTemplate: 'welcome',
 	customWelcomeList: [],
 	customWelcomeSignature: true,
 
 	// Talkback
 	markTalkbackAsMinor: true,
 	insertTalkbackSignature: true,  // always sign talkback templates
 	talkbackHeading: 'New message from ' + mw.config.get('wgUserName'),
 	mailHeading: &quot;You've got mail!&quot;,
 
 	// Shared
 	markSharedIPAsMinor: true
 };
 
 // now some skin dependent config.
 switch (mw.config.get('skin')) {
 	case 'vector':
 		Twinkle.defaultConfig.portletArea = 'right-navigation';
 		Twinkle.defaultConfig.portletId = 'p-twinkle';
 		Twinkle.defaultConfig.portletName = 'TW';
 		Twinkle.defaultConfig.portletType = 'menu';
 		Twinkle.defaultConfig.portletNext = 'p-search';
 		break;
 	case 'timeless':
 		Twinkle.defaultConfig.portletArea = '#page-tools .sidebar-inner';
 		Twinkle.defaultConfig.portletId = 'p-twinkle';
 		Twinkle.defaultConfig.portletName = 'Twinkle';
 		Twinkle.defaultConfig.portletType = null;
 		Twinkle.defaultConfig.portletNext = 'p-userpagetools';
 		break;
 	default:
 		Twinkle.defaultConfig.portletArea = null;
 		Twinkle.defaultConfig.portletId = 'p-cactions';
 		Twinkle.defaultConfig.portletName = null;
 		Twinkle.defaultConfig.portletType = null;
 		Twinkle.defaultConfig.portletNext = null;
 }
 
 
 Twinkle.getPref = function twinkleGetPref(name) {
 	if (typeof Twinkle.prefs === 'object' &amp;&amp; Twinkle.prefs[name] !== undefined) {
 		return Twinkle.prefs[name];
 	}
 	// Old preferences format, used before twinkleoptions.js was a thing
 	if (typeof window.TwinkleConfig === 'object' &amp;&amp; window.TwinkleConfig[name] !== undefined) {
 		return window.TwinkleConfig[name];
 	}
 	if (typeof window.FriendlyConfig === 'object' &amp;&amp; window.FriendlyConfig[name] !== undefined) {
 		return window.FriendlyConfig[name];
 	}
 	return Twinkle.defaultConfig[name];
 };
 
 
 /**
  * **************** Twinkle.addPortlet() ****************
  *
  * Adds a portlet menu to one of the navigation areas on the page.
  * This is necessarily quite a hack since skins, navigation areas, and
  * portlet menu types all work slightly different.
  *
  * Available navigation areas depend on the skin used.
  * Vector:
  *  For each option, the outer nav class contains &quot;vector-menu&quot;, the inner div class is &quot;vector-menu-content&quot;, and the ul is &quot;vector-menu-content-list&quot;
  *  &quot;mw-panel&quot;, outer nav class contains &quot;vector-menu-portal&quot;. Existing portlets/elements: &quot;p-logo&quot;, &quot;p-navigation&quot;, &quot;p-interaction&quot;, &quot;p-tb&quot;, &quot;p-coll-print_export&quot;
  *  &quot;left-navigation&quot;, outer nav class contains &quot;vector-menu-tabs&quot; or &quot;vector-menu-dropdown&quot;. Existing portlets: &quot;p-namespaces&quot;, &quot;p-variants&quot; (menu)
  *  &quot;right-navigation&quot;, outer nav class contains &quot;vector-menu-tabs&quot; or &quot;vector-menu-dropdown&quot;. Existing portlets: &quot;p-views&quot;, &quot;p-cactions&quot; (menu), &quot;p-search&quot;
  *  Special layout of p-personal portlet (part of &quot;head&quot;) through specialized styles.
  * Monobook:
  *  &quot;column-one&quot;, outer nav class &quot;portlet&quot;, inner div class &quot;pBody&quot;. Existing portlets: &quot;p-cactions&quot;, &quot;p-personal&quot;, &quot;p-logo&quot;, &quot;p-navigation&quot;, &quot;p-search&quot;, &quot;p-interaction&quot;, &quot;p-tb&quot;, &quot;p-coll-print_export&quot;
  *  Special layout of p-cactions and p-personal through specialized styles.
  * Modern:
  *  &quot;mw_contentwrapper&quot; (top nav), outer nav class &quot;portlet&quot;, inner div class &quot;pBody&quot;. Existing portlets or elements: &quot;p-cactions&quot;, &quot;mw_content&quot;
  *  &quot;mw_portlets&quot; (sidebar), outer nav class &quot;portlet&quot;, inner div class &quot;pBody&quot;. Existing portlets: &quot;p-navigation&quot;, &quot;p-search&quot;, &quot;p-interaction&quot;, &quot;p-tb&quot;, &quot;p-coll-print_export&quot;
  *
  * @param String navigation -- id of the target navigation area (skin dependant, on vector either of &quot;left-navigation&quot;, &quot;right-navigation&quot;, or &quot;mw-panel&quot;)
  * @param String id -- id of the portlet menu to create, preferably start with &quot;p-&quot;.
  * @param String text -- name of the portlet menu to create. Visibility depends on the class used.
  * @param String type -- type of portlet. Currently only used for the vector non-sidebar portlets, pass &quot;menu&quot; to make this portlet a drop down menu.
  * @param Node nextnodeid -- the id of the node before which the new item should be added, should be another item in the same list, or undefined to place it at the end.
  *
  * @return Node -- the DOM node of the new item (a DIV element) or null
  */
 Twinkle.addPortlet = function(navigation, id, text, type, nextnodeid) {
 	// sanity checks, and get required DOM nodes
 	var root = document.getElementById(navigation) || document.querySelector(navigation);
 	if (!root) {
 		return null;
 	}
 
 	var item = document.getElementById(id);
 	if (item) {
 		if (item.parentNode &amp;&amp; item.parentNode === root) {
 			return item;
 		}
 		return null;
 	}
 
 	var nextnode;
 	if (nextnodeid) {
 		nextnode = document.getElementById(nextnodeid);
 	}
 
 	// verify/normalize input
 	var skin = mw.config.get('skin');
 	if (skin !== 'vector' || (navigation !== 'left-navigation' &amp;&amp; navigation !== 'right-navigation')) {
 		type = null; // menu supported only in vector's #left-navigation &amp; #right-navigation
 	}
 	var outerNavClass, innerDivClass;
 	switch (skin) {
 		case 'vector':
 			// XXX: portal doesn't work
 			if (navigation !== 'portal' &amp;&amp; navigation !== 'left-navigation' &amp;&amp; navigation !== 'right-navigation') {
 				navigation = 'mw-panel';
 			}
 			outerNavClass = 'mw-portlet vector-menu vector-menu-' + (navigation === 'mw-panel' ? 'portal' : type === 'menu' ? 'dropdown vector-menu-dropdown-noicon' : 'tabs');
 			innerDivClass = 'vector-menu-content';
 			break;
 		case 'modern':
 			if (navigation !== 'mw_portlets' &amp;&amp; navigation !== 'mw_contentwrapper') {
 				navigation = 'mw_portlets';
 			}
 			outerNavClass = 'portlet';
 			break;
 		case 'timeless':
 			outerNavClass = 'mw-portlet';
 			innerDivClass = 'mw-portlet-body';
 			break;
 		default:
 			navigation = 'column-one';
 			outerNavClass = 'portlet';
 			break;
 	}
 
 	// Build the DOM elements.
 	var outerNav = document.createElement('nav');
 	outerNav.setAttribute('aria-labelledby', id + '-label');
 	outerNav.className = outerNavClass + ' emptyPortlet';
 	outerNav.id = id;
 	if (nextnode &amp;&amp; nextnode.parentNode === root) {
 		root.insertBefore(outerNav, nextnode);
 	} else {
 		root.appendChild(outerNav);
 	}
 
 	var h3 = document.createElement('h3');
 	h3.id = id + '-label';
 	var ul = document.createElement('ul');
 
 	if (skin === 'vector') {
 		ul.className = 'vector-menu-content-list';
 
 		// add invisible checkbox to keep menu open when clicked
 		// similar to the p-cactions (&quot;More&quot;) menu
 		if (outerNavClass.indexOf('vector-menu-dropdown') !== -1) {
 			var chkbox = document.createElement('input');
 			chkbox.className = 'vector-menu-checkbox';
 			chkbox.setAttribute('type', 'checkbox');
 			chkbox.setAttribute('aria-labelledby', id + '-label');
 			outerNav.appendChild(chkbox);
 
 			// Vector gets its title in a span; all others except
 			// timeless have no title, and it has no span
 			var span = document.createElement('span');
 			span.appendChild(document.createTextNode(text));
 			h3.appendChild(span);
 
 			var a = document.createElement('a');
 			a.href = '#';
 
 			$(a).click(function(e) {
 				e.preventDefault();
 			});
 
 			h3.appendChild(a);
 		}
 	} else {
 		// Basically just Timeless
 		h3.appendChild(document.createTextNode(text));
 	}
 
 	outerNav.appendChild(h3);
 
 	if (innerDivClass) {
 		var innerDiv = document.createElement('div');
 		innerDiv.className = innerDivClass;
 		innerDiv.appendChild(ul);
 		outerNav.appendChild(innerDiv);
 	} else {
 		outerNav.appendChild(ul);
 	}
 
 
 	return outerNav;
 
 };
 
 
 /**
  * **************** Twinkle.addPortletLink() ****************
  * Builds a portlet menu if it doesn't exist yet, and add the portlet link.
  * @param task: Either a URL for the portlet link or a function to execute.
  */
 Twinkle.addPortletLink = function(task, text, id, tooltip) {
 	if (Twinkle.getPref('portletArea') !== null) {
 		Twinkle.addPortlet(Twinkle.getPref('portletArea'), Twinkle.getPref('portletId'), Twinkle.getPref('portletName'), Twinkle.getPref('portletType'), Twinkle.getPref('portletNext'));
 	}
 	var link = mw.util.addPortletLink(Twinkle.getPref('portletId'), typeof task === 'string' ? task : '#', text, id, tooltip);
 	$('.client-js .skin-vector #p-cactions').css('margin-right', 'initial');
 	if (typeof task === 'function') {
 		$(link).click(function (ev) {
 			task();
 			ev.preventDefault();
 		});
 	}
 	if ($.collapsibleTabs) {
 		$.collapsibleTabs.handleResize();
 	}
 	return link;
 };
 
 
 /**
  * **************** General initialization code ****************
  */
 
 var scriptpathbefore = mw.util.wikiScript('index') + '?title=',
 	scriptpathafter = '&amp;action=raw&amp;ctype=text/javascript&amp;happy=yes';
 
 // Retrieve the user's Twinkle preferences
 $.ajax({
 	url: scriptpathbefore + 'User:' + encodeURIComponent(mw.config.get('wgUserName')) + '/twinkleoptions.js' + scriptpathafter,
 	dataType: 'text'
 })
 	.fail(function () {
 		mw.notify('Could not load your Twinkle preferences', {type: 'error'});
 	})
 	.done(function (optionsText) {
 
 		// Quick pass if user has no options
 		if (optionsText === '') {
 			return;
 		}
 
 		// Twinkle options are basically a JSON object with some comments. Strip those:
 		optionsText = optionsText.replace(/(?:^(?:\/\/[^\n]*\n)*\n*|(?:\/\/[^\n]*(?:\n|$))*$)/g, '');
 
 		// First version of options had some boilerplate code to make it eval-able -- strip that too. This part may become obsolete down the line.
 		if (optionsText.lastIndexOf('window.Twinkle.prefs = ', 0) === 0) {
 			optionsText = optionsText.replace(/(?:^window.Twinkle.prefs = |;\n*$)/g, '');
 		}
 
 		try {
 			var options = JSON.parse(optionsText);
 			if (options) {
 				if (options.twinkle || options.friendly) { // Old preferences format
 					Twinkle.prefs = $.extend(options.twinkle, options.friendly);
 				} else {
 					Twinkle.prefs = options;
 				}
 				// v2 established after unification of Twinkle/Friendly objects
 				Twinkle.prefs.optionsVersion = Twinkle.prefs.optionsVersion || 1;
 			}
 		} catch (e) {
 			mw.notify('Could not parse your Twinkle preferences', {type: 'error'});
 		}
 	})
 	.always(function () {
 		$(Twinkle.load);
 	});
 
 // Developers: you can import custom Twinkle modules here
 // For example, mw.loader.load(scriptpathbefore + &quot;User:UncleDouggie/morebits-test.js&quot; + scriptpathafter);
 
 Twinkle.load = function () {
 	// Don't activate on special pages other than those listed here, so
 	// that others load faster, especially the watchlist.
 	var activeSpecialPageList = [ 'Block', 'Contributions', 'Recentchanges', 'Recentchangeslinked' ]; // wgRelevantUserName defined for non-sysops on Special:Block
 	if (Morebits.userIsSysop) {
 		activeSpecialPageList = activeSpecialPageList.concat([ 'DeletedContributions', 'Prefixindex' ]);
 	}
 	if (mw.config.get('wgNamespaceNumber') === -1 &amp;&amp;
 		activeSpecialPageList.indexOf(mw.config.get('wgCanonicalSpecialPageName')) === -1) {
 		return;
 	}
 
 	// Prevent clickjacking
 	if (window.top !== window.self) {
 		return;
 	}
 
 	// Set custom Api-User-Agent header, for server-side logging purposes
 	Morebits.wiki.api.setApiUserAgent('Twinkle (' + mw.config.get('wgWikiID') + ')');
 
 	Twinkle.disabledModules = Twinkle.getPref('disabledModules').concat(Twinkle.getPref('disabledSysopModules'));
 
 	// Redefine addInitCallback so that any modules being loaded now on are directly
 	// initialised rather than added to initCallbacks array
 	Twinkle.addInitCallback = function(func, name) {
 		if (!name || Twinkle.disabledModules.indexOf(name) === -1) {
 			func();
 		}
 	};
 	// Initialise modules that were saved in initCallbacks array
 	Twinkle.initCallbacks.forEach(function(module) {
 		Twinkle.addInitCallback(module.func, module.name);
 	});
 
 	// Increases text size in Twinkle dialogs, if so configured
 	if (Twinkle.getPref('dialogLargeFont')) {
 		mw.util.addCSS('.morebits-dialog-content, .morebits-dialog-footerlinks { font-size: 100% !important; } ' +
 			'.morebits-dialog input, .morebits-dialog select, .morebits-dialog-content button { font-size: inherit !important; }');
 	}
 
 	// Hide the lingering space if the TW menu is empty
 	if (mw.config.get('skin') === 'vector' &amp;&amp; Twinkle.getPref('portletType') === 'menu' &amp;&amp; $('#p-twinkle').length === 0) {
 		$('#p-cactions').css('margin-right', 'initial');
 	}
 };
 
 
 /**
  * Twinkle-specific data shared by multiple modules
  * Likely customized per installation
  */
 
 // Custom change tag(s) to be applied to all Twinkle actions, create at Special:Tags
 Twinkle.changeTags = 'மின்னல்';
 // Available for actions that don't (yet) support tags
 // currently: FlaggedRevs and PageTriage
 Twinkle.summaryAd = ' ([[WP:TW|TW]])';
 
 // Various hatnote templates, used when tagging (csd/xfd/tag/prod/protect) to
 // ensure MOS:ORDER
 Twinkle.hatnoteRegex = 'short description|hatnote|main|correct title|dablink|distinguish|for|further|selfref|year dab|similar names|highway detail hatnote|broader|about(?:-distinguish| other people)?|other\\s?(?:hurricane(?: use)?s|people|persons|places|ships|uses(?: of)?)|redirect(?:-(?:distinguish|synonym|multi))?|see\\s?(?:wiktionary|also(?: if exists)?)';
 
 // Used in XFD and PROD
 Twinkle.makeFindSourcesDiv = function makeSourcesDiv() {
 	var makeLink = function(href, text) {
 		return $('&lt;a&gt;').attr({ rel: 'nofollow', class: 'external text',
 			target: '_blank', href: href }).text(text);
 	};
 	var title = encodeURIComponent(Morebits.pageNameNorm);
 	return $('&lt;div&gt;')
 		.addClass('plainlinks')
 		.append(
 			'(',
 			$('&lt;i&gt;').text('Find sources:'), ' ',
 			makeLink('//www.google.com/search?as_eq=wikipedia&amp;q=%22' + title + '%22', 'Google'),
 			' (',
 			makeLink('//www.google.com/search?tbs=bks:1&amp;q=%22' + title + '%22+-wikipedia', 'books'), ' - ',
 			makeLink('//www.google.com/search?tbm=nws&amp;q=%22' + title + '%22+-wikipedia', 'news'), ' - ',
 			makeLink('//www.google.com/search?&amp;q=%22' + title + '%22+site:news.google.com/newspapers&amp;source=newspapers', 'newspapers'), ' - ',
 			makeLink('//scholar.google.com/scholar?q=%22' + title + '%22', 'scholar'), ' - ',
 			makeLink('https://www.google.com/search?safe=off&amp;tbs=sur:fmc&amp;tbm=isch&amp;q=%22' + title + '%22+-site:wikipedia.org+-site:wikimedia.org', 'free images'), ' - ',
 			makeLink('https://www.google.com/custom?hl=en&amp;cx=007734830908295939403%3Agalkqgoksq0&amp;cof=FORID%3A13%3BAH%3Aleft%3BCX%3AWikipedia%2520Reference%2520Search&amp;q=%22' + title + '%22', 'WP refs'),
 			')', ' - ',
 			makeLink('https://en.wikipedia.org/wiki/Wikipedia:Free_English_newspaper_sources', 'FENS'), ' - ',
 			makeLink('https://www.jstor.org/action/doBasicSearch?Query=%22' + title + '%22&amp;acc=on&amp;wc=on', 'JSTOR'), ' - ',
 			makeLink('https://www.nytimes.com/search/%22' + title + '%22', 'NYT'), ' - ',
 			makeLink('https://wikipedialibrary.wmflabs.org/partners/', 'TWL'),
 			')'
 		)[0];
 };
 
 // Used in batch listings to link to the page in question with &gt;
 Twinkle.generateArrowLinks = function (checkbox) {
 	var link = Morebits.htmlNode('a', ' &gt;');
 	link.setAttribute('class', 'tw-arrowpage-link');
 	link.setAttribute('href', mw.util.getUrl(checkbox.value));
 	link.setAttribute('target', '_blank');
 	checkbox.nextElementSibling.append(link);
 };
 
 // Used in deprod and unlink listings to link the page title
 Twinkle.generateBatchPageLinks = function (checkbox) {
 	var $checkbox = $(checkbox);
 	var link = Morebits.htmlNode('a', $checkbox.val());
 	link.setAttribute('class', 'tw-batchpage-link');
 	link.setAttribute('href', mw.util.getUrl($checkbox.val()));
 	link.setAttribute('target', '_blank');
 	$checkbox.next().prepend([link, ' ']);
 };
 
 }(window, document, jQuery)); // End wrap with anonymous function