User:Apollogetticax/common.js

Source: Wikipedia, the free encyclopedia.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
mw.loader.load('/w/index.php?title=User:Lupin/recent2.js&action=raw&ctype=text/javascript');
mw.loader.load('//en.wikipedia.org/w/index.php?title=User%3AAwesome_Aasim%2Frcpatrol.js&action=raw&ctype=text/javascript');
mw.loader.load('//www.mediawiki.org/wiki/MediaWiki:MiniEdit.js?action=raw&ctype=text/javascript');
/** Quick Edit **/

// Edit sections of a page without leaving the article
// [[en:w:User:BrandonXLF/QuickEdit]]
// By [[en:w:User:BrandonXLF]]

(function() {
	var mobile = mw.config.get('skin') === 'minerva',
		apiSingleton,
		titleRegexp = new RegExp(
			mw.config.get('wgArticlePath').replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\$1/, '([^?]+)') +
			'|[?&]title=([^&#]*)'
		);

	function api(func, params) {
		if (!apiSingleton) apiSingleton = new mw.Api();

		$.extend(params, {
			errorformat: 'html',
			errorlang: mw.config.get('wgUserLanguage'),
			errorsuselocal: true
		});

		return apiSingleton[func](params).fail(function(_, data) {
			mw.notify(apiSingleton.getErrorMessage(data), {
				type: 'error',
				tag: 'quickedit'
			});
		});
	}

	function getPageInfo(title, sectionID) {
		return api('get', {
			action: 'query',
			curtimestamp: 1,
			prop: 'revisions',
			indexpageids: 1,
			titles: title,
			rvprop: ['timestamp', 'content'],
			rvslots: 'main',
			rvsection: sectionID
		}).then(function(res) {
			var rev = res.query.pages[res.query.pageids[0]].revisions[0];

			return {
				start: res.curtimestamp,
				base: rev.timestamp,
				full: rev.slots.main['*']
			};
		});
	}

	function getPreviewCallback(editor) {
		editor.children('.preview').remove();

		new OO.ui.ProgressBarWidget().$element.css({
			maxWidth: '100%',
			borderRadius: '0',
			boxShadow: 'none',
			margin: '8px 0'
		}).addClass('preview').appendTo(editor);

		return function(html) {
			editor.children('.preview').remove();

			$('<div>').html(html).css({
				margin: '8px 0',
				border: '1px solid #a2a9b1',
				padding: '8px',
				overflowX: 'hidden'
			}).addClass('preview').appendTo(editor);
		};
	}

	function showCompare(editor, title, from, to) {
mw.loader.load('mediawiki.diff.styles');

		api('post', {
			action: 'compare',
			fromslots: 'main',
			'fromtext-main': from,
			fromtitle: title,
			frompst: 'true',
			toslots: 'main',
			'totext-main': to,
			totitle: title,
			topst: 'true'
		}).then(function(r) {
			return r.compare['*'] ? $('<table>').addClass('diff').append(
				$('<colgroup>').append(
					$('<col>').addClass('diff-marker'),
					$('<col>').addClass('diff-content'),
					$('<col>').addClass('diff-marker'),
					$('<col>').addClass('diff-content')
				)
			).append(r.compare['*']) : 'No differences.';
		}).then(getPreviewCallback(editor));
	}

	// Parts taken from EditPage::extractSectionTitle and Parser::stripSectionName
	function getSectionSummary(text) {
		var match = text.match(/^(=+)(.+)\1\s*(\n|$)/);

		return !match ? '' : '/* ' + match[2].trim()
			// Strip internal link markup
			.replace(/\[\[:?([^[|]+)\|([^[]+)\]\]/g, '$2')
			.replace(/\[\[:?([^[]+)\|?\]\]/g, '$1')
			// Strip external link markup
			.replace(new RegExp('\\[(?:' + mw.config.get('wgUrlProtocols') + ')([^ ]+?) ([^\\[]+)\\]', 'ig'), '$2')
			// Remove wikitext quotes
			.replace(/(''|'''|''''')(?!')/g, '')
			// Strip HTML tags
			.replace(/<[^>]+?>/g, '') + ' */ ';
	}

	function showEditor(el) {
		var progress = new OO.ui.ProgressBarWidget(),
			// https://www.mediawiki.org/wiki/Heading_HTML_changes
			// Cannot use .closest() because DiscussionTools nests an h2 within a .mw-heading
			heading = el.parents(':header, .mw-heading').last(),
			matcher = heading.nextUntil.bind(heading),
			inserter = heading.after.bind(heading),
			targetEl = el.siblings('.quickedit-target').last(),
			titleMatch = targetEl.attr('href').match(titleRegexp),
			title = decodeURIComponent(titleMatch[1] || titleMatch[2]),
			sectionID = /[?&]v?e?section=T?-?(\d*)/.exec(targetEl.attr('href'))[1];

		if (!heading.closest('.mw-parser-output').length) {
			var articleContent = $('#mw-content-text .mw-parser-output');

			matcher = function(selector) {
				var child = articleContent.children(selector).first();

				if (child.length) return child.prevAll();
				return articleContent.children();
			};
			inserter = articleContent.prepend.bind(articleContent);
		}

		inserter(progress.$element.css({
			maxWidth: '100%',
			borderRadius: '0',
			boxShadow: 'none'
		}));

		el.addClass('quickedit-loading');
		$('.quickedit-hide').removeClass('quickedit-hide');
		$('.quickedit-heading').removeClass('quickedit-heading');
		$('#quickedit-editor').remove();

		getPageInfo(title, sectionID).then(function(r) {
			var start = r.start,
				base = r.base,
				full = r.full,
				saving = false,
				expanded = false,
				remainderStart = full.match(/\n=+.+=+(?:\n|$)/),
				part =  remainderStart ? full.substring(0, remainderStart.index) : full,
				remainder = remainderStart ? full.substring(remainderStart.index) : '',
				level = 0,
				editor;

			full.replace(/^(=+).+?(=+)(?:\n|$)/, function(m, a, b) {
				level = Math.min(a.length, b.length);
				return m;
			});

			var levelMatch = 'h1';
			for (var i = 2; i <= level; i++)
				levelMatch += ',h' + i + ':has(*), .mw-heading' + i;

			var partSection = matcher(':header:has(*), .mw-heading'),
				fullSection = matcher(levelMatch),
				textarea = new OO.ui.MultilineTextInputWidget({
					rows: 1,
					maxRows: 20,
					autosize: true,
					value: part
				}),
				summary = new OO.ui.TextInputWidget({
					value: getSectionSummary(part)
				}),
				minor = new OO.ui.CheckboxInputWidget(),
				save = new OO.ui.ButtonInputWidget({
					label: 'Save',
					title: 'Save your changes',
					flags: ['primary', 'progressive'],
					accessKey: 's'
				}),
				preview = new OO.ui.ButtonInputWidget({
					label: 'Preview',
					title: 'Preview the new wikitext'
				}),
				compare = new OO.ui.ButtonInputWidget({
					label: 'Compare',
					title: 'View the difference between the current revision and your revision'
				}),
				cancel = new OO.ui.ButtonInputWidget({
					useInputTag: true,
					label: 'Cancel',
					title: 'Close the edit form and discard changes',
					flags: ['secondary', 'destructive']
				}),
				more = new OO.ui.ButtonInputWidget({
					label: '+',
					title: 'Edit the entire section (including subsections)'
				}),
				buttons = new OO.ui.HorizontalLayout({
					items: [save, preview, compare, cancel]
				});

			if (part != full) {
				buttons.addItems([more], 3);
			}

			partSection.addClass('quickedit-hide');
			heading.addClass('quickedit-heading');
			el.removeClass('quickedit-loading');
			progress.$element.remove();
			textarea.$input.css({
				borderRadius: '0'
			});

			summary.on('enter', function() {
				save.emit('click');
			});

			save.on('click', function() {
				if (saving) return;

				var fullText = textarea.getValue() + (expanded ? '' : remainder);
				saving = true;
				save.setLabel('Saving...');
				compare.setDisabled(true);
				preview.setDisabled(true);
				cancel.setDisabled(true);
				more.setDisabled(true);

				api('postWithEditToken', {
					action: 'edit',
					title: title,
					section: sectionID,
					summary: summary.getValue(),
					text: fullText,
					minor: minor.isSelected() ? true : undefined,
					notminor: minor.isSelected() ? undefined : true,
					starttimestamp: start,
					basetimestamp: base
				}).then(function() {
					api('get', {
						action: 'parse',
						page: mw.config.get('wgPageName'),
						prop: ['text', 'categorieshtml']
					}).then(function(r) {
						var contentText = $('#mw-content-text'),
							catLinks = $('#catlinks');

						contentText.find('.mw-parser-output').replaceWith(r.parse.text['*']);
						mw.hook('wikipage.content').fire(contentText);

						catLinks.replaceWith(r.parse.categorieshtml['*']);
						mw.hook('wikipage.categories').fire(catLinks);

						saving = false;
					});
				}, function(code) {
					if (code == 'editconflict') {
						showEditConflict(editor, title, sectionID, fullText).then(function(r) {
							start = r.start;
							base = r.base;
							textarea = r.textarea;
							expanded = true;
						});
					}

					compare.setDisabled(false);
					preview.setDisabled(false);
					cancel.setDisabled(false);
					more.setDisabled(expanded);
					saving = false;
					save.setLabel('Save');
				});
			});

			preview.on('click', function() {
				api('post', {
					action: 'parse',
					title: title,
					prop: 'text',
					pst: 'true',
					disablelimitreport: 'true',
					disableeditsection: 'true',
					sectionpreview: 'true',
					disabletoc: 'true',
					text: textarea.getValue()
				}).then(function(r) {
					return r.parse.text['*'] + '<div style="clear:both;"></div>';
				}).then(getPreviewCallback(editor));
			});

			compare.on('click', function() {
				showCompare(editor, title, part + (expanded ? remainder : ''), textarea.getValue());
			});

			cancel.on('click', function() {
				editor.remove();
				heading.removeClass('quickedit-heading');
				fullSection.removeClass('quickedit-hide');
			});

			more.on('click', function() {
				expanded = true;
				textarea.setValue(textarea.getValue() + remainder);
				fullSection.addClass('quickedit-hide');
				more.setDisabled(true);
			});

			editor = $('<div id="quickedit-editor">').css({
				overflowX: 'hidden'
			}).append(
				$('<div>').css({
					backgroundColor: '#eaecf0',
					borderBottom: '1px solid #a2a9b1',
					marginBottom: '8px'
				}).append(
					textarea.$element.css({
						width: '100%',
						maxWidth: '100%',
						fontFamily: 'monospace, monospace'
					}).addClass('quickedit-textarea'),
					$('<div>').css({
						border: '1px solid #a2a9b1',
						borderWidth: '0 1px'
					}).append(
						$('<div>').css({
							padding: '8px 4px 8px 8px',
							display: 'table-cell',
							verticalAlign: 'middle'
						}).html('Edit&nbsp;summary:'),
						summary.$element.css({
							width: '100%',
							maxWidth: '100%',
							padding: '8px 0px',
							display: 'table-cell',
							verticalAlign: 'middle'
						}),
						new OO.ui.FieldLayout(minor, {
							label: new OO.ui.HtmlSnippet('Minor&nbsp;edit?'),
							align: 'inline'
						}).$element.css({
							padding: '8px 8px 8px 4px',
							display: 'table-cell',
							verticalAlign: 'middle'
						})
					),
					buttons.$element.css({
						border: '1px solid #a2a9b1',
						borderWidth: '0 1px',
						padding: '0px 8px 0'
					}),
					title !== mw.config.get('wgPageName') ? $('<div>').css({
						border: '1px solid #a2a9b1',
						borderWidth: '0 1px',
						padding: '0px 8px 8px'
					}).append(
						'Editing page: ',
						$('<a>').attr('href', mw.config.get('wgArticlePath').replace('$1', title)).css({
							fontWeight: 'bold'
						}).text(title.replace(/_/g, ' '))
					) : undefined
				)
			);

			inserter(editor);
		}, function() {
			el.removeClass('quickedit-loading');
			progress.$element.remove();
		});
	}

	function showEditConflict(editor, title, sectionID, text) {
		return getPageInfo(title, sectionID).then(function(r) {
			var textarea = new OO.ui.MultilineTextInputWidget({
					rows: 1,
					maxRows: 20,
					autosize: true,
					value: r.full
				}),
				textarea2 = new OO.ui.MultilineTextInputWidget({
					rows: 1,
					maxRows: 20,
					autosize: true,
					value: text,
				});

			function syncSize() {
				textarea.styleHeight = -1;
				textarea.adjustSize(true);

				textarea2.styleHeight = -1;
				textarea2.adjustSize(true);

				var height = Math.max(textarea.$input.height(), textarea2.$input.height());
				textarea.$input.height(height);
				textarea2.$input.height(height);
			}

			textarea.$input.css({
				borderRadius: '0'
			});
			editor.find('> :first-child > :first-child').remove();

			$('<table>').css({
				width: '100%',
				border: '1px solid #a2a9b1',
				borderBottom: 'none',
				borderSpacing: '0',
				margin: '0 !important'
			}).append(
				$('<tr>').append(
					$('<th>').css({
						width: '50%',
						paddingTop: '4px'
					}).text('Their version (to be saved)'),
					$('<th>').css({
						width: '50%',
						paddingTop: '4px'
					}).text('Your version')
				),
				$('<tr>').append(
					$('<td>').css({
						width: '50%',
						padding: '4px 4px 0 8px'
					}).append(
						textarea.$element.css({
							width: '100%',
							maxWidth: '100%',
							fontFamily: 'monospace, monospace'
						})
					),
					$('<td>').css({
						width: '50%',
						padding: '4px 8px 0 4px'
					}).append(
						textarea2.$element.css({
							width: '100%',
							maxWidth: '100%',
							fontFamily: 'monospace, monospace'
						})
					)
				)
			).prependTo(editor.find('> :first-child'));

			textarea.on('change', syncSize);
			textarea2.on('change', syncSize);
			syncSize();
			showCompare(editor, title, text, r.full);

			r.textarea = textarea;
			return r;
		});
	}

	function clickHandler(e) {
		var el = $(e.target);

		if (!el.hasClass('quickedit-editlink') || el.hasClass('quickedit-loading')) return;

		e.preventDefault();

		showEditor(el);
	}

	function addLinksToChildren(element) {
		element.find('#quickedit-editor, .quickedit-section').remove();
		element.find('.mw-editsection').each(function() {
			$('[href*="section="]', this).last().after(
				mobile ? '' : '<span class="quickedit-section"> | </span>',
				$('<a>')
					.html(mobile ? '&nbsp;Q' : 'quick edit')
					.addClass('quickedit-section quickedit-editlink')
					.attr('href', '#')
			).addClass('quickedit-target');
		});
	}

	$.when(mw.loader.using('oojs-ui-core'), $.ready).done(function() {
		var body = $(document.body);

		body.on('click', clickHandler);
		addLinksToChildren(body);
		mw.hook('wikipage.content').add(addLinksToChildren);
	});

	mw.loader.addStyleTag(
		'.skin-minerva .mw-editsection { white-space: nowrap; }' +
		'.skin-minerva .content .collapsible-heading .quickedit-section { visibility: hidden; }' +
		'.skin-minerva .content .collapsible-heading.open-block .quickedit-section { visibility: visible; }' +
		'.quickedit-hide { display: none !important; }' +
		'.quickedit-loading, .quickedit-heading { color: #777; }'
	);
})();
// <nowiki>

class CiteHighlighter {
	constructor( window, $, mw ) {
		this.window = window;
		// eslint-disable-next-line no-jquery/variable-pattern
		this.$ = $;
		this.mw = mw;
	}

	async execute() {
		this.sources = await this.getListOfSourcesAndRatings();
		this.unreliableWordsForOrangeHighlighting = this.getUnreliableWords();
		this.setConfigVariableDefaultsIfNeeded();
		this.articleTitle = this.mw.config.get( 'wgPageName' );
		if ( this.isSlowPage() ) {
			return;
		}
		this.highlightSourceListsMoreAggressively();
		this.highlightDraftsMoreAggressively();
		this.preventWikipediaFalsePositives();
		this.colors = this.getColors();
		this.writeCSS();
		this.wikicode = await this.getWikicode( this.articleTitle );
		this.addHTMLClassesToRefs();
		this.addHTMLClassesForUnreliableWords();
		this.observeAndAddClassesToTooltips();
	}

	getUnreliableWords() {
		return [
			'/comment',
			'about-me',
			'about-us',
			'/about/',
			'acquire',
			'announce',
			// 'blockchain',
			'blog', // by far the most common hit
			'blogspot',
			'businesswire',
			'caard', // caard.co - "Simple, free, fully responsive one-page sites for pretty much anything."
			'contact-us',
			'contactus',
			// 'crypto',
			'essay',
			'fandom',
			'/forum/',
			'google.com/search',
			'innovative',
			'newswire',
			'podcast',
			'/post/',
			'preprint',
			'press-release',
			'pressrelease',
			'prnews',
			'railfan',
			'sponsored',
			'thread',
			'user-review',
			'viewtopic',
			'weebly',
			'wix',
			'wordpress',
			'/wp-' // WordPress, e.g. wp-content
		];
	}

	async getListOfSourcesAndRatings() {
		let sources = await this.getWikicodeFromCache( 'User:Novem Linguae/Scripts/CiteHighlighter/SourcesJSON.js' );
		sources = JSON.parse( sources );
		return sources;
	}

	setConfigVariableDefaultsIfNeeded() {
		// Defaults
		this.config = {
			highlightEverything: false,
			highlightLighterColors: false,
			alwaysHighlightSourceLists: false,
			unreliableWord: '#ffb347',
			preprint: 'lightcoral',
			doi: 'transparent',
			medrs: 'limegreen',
			green: 'lightgreen',
			yellow: 'khaki',
			red: 'lightcoral'
		};

		// Override defaults if window.citeHighlighterXYZ is already set (typically at the top of the user's common.js file)
		for ( const key in this.config ) {
			const value = this.window[ 'citeHighlighter' + this.capitalizeFirstLetter( key ) ];
			if ( value !== undefined ) {
				this.config[ key ] = value;
			}
		}
	}

	/**
	 * Steve Harrison, CC BY-SA 4.0, https://stackoverflow.com/a/1026087/3480193
	 *
	 * @param {string} string
	 * @return {string}
	 */
	capitalizeFirstLetter( string ) {
		return string.charAt( 0 ).toUpperCase() + string.slice( 1 );
	}

	/**
	 * Don't highlight certain pages, for speed and visual appearance reasons.
	 *
	 * On pages with a lot of links (watchlist, WP:FA), highlighting EVERYTHING will double the
	 * load time. e.g. watchlist 5 seconds -> 10 seconds.
	 *
	 * @return {boolean}
	 */
	isSlowPage() {
		if (
			this.mw.config.get( 'wgAction' ) === 'history' ||
			this.articleTitle === 'Main_Page' ||
			this.articleTitle === 'Wikipedia:Featured_articles' ||
			this.articleTitle === 'Special:Watchlist' ||
			this.articleTitle === 'Wikipedia:New_page_patrol_source_guide'
		) {
			return true;
		}

		return false;
	}

	/**
	 * If page is a source quality list, highlight everything, even if highlightEverything =
	 * false. Goal: easily see if the script is highlighting anything wrong.
	 */
	highlightSourceListsMoreAggressively() {
		const highlightEverythingList = [
			'Wikipedia:Reliable_sources/Perennial_sources',
			// 'Wikipedia:New_page_patrol_source_guide', // so slow that I hard-coded this never to load by placing a check in isSlowPage()
			'Wikipedia:WikiProject_Albums/Sources',
			'Wikipedia:WikiProject_Video_games/Sources#Reliable_sources',
			'Wikipedia:WikiProject_Anime_and_manga/Online_reliable_sources',
			'Wikipedia:WikiProject_Africa/Africa_Sources_List',
			'Wikipedia:WikiProject_Dungeons_%26_Dragons/References'
		];

		if ( this.config.alwaysHighlightSourceLists ) {
			if ( highlightEverythingList.includes( this.articleTitle ) ) {
				this.config.highlightEverything = true;
			}
		}
	}

	/**
	 * If page is a draft, highlight everything, as the # of links is small, and oftentimes
	 * inline citations are malformed
	 */
	highlightDraftsMoreAggressively() {
		if ( this.mw.config.get( 'wgNamespaceNumber' ) === 118 ) {
			this.config.highlightEverything = true;
		}
	}

	/**
	 * If highlightEverything = true, delete wikipedia.org and wiktionary. Too many false positives.
	 */
	preventWikipediaFalsePositives() {
		if ( this.config.highlightEverything ) {
			this.deleteAll( this.sources, 'en.wikipedia.org', 'wikipedia.org', 'wiktionary.org' );
			this.deleteFromArray( this.unreliableWordsForOrangeHighlighting, 'wiki' );
		}
	}

	getColors() {
		if ( this.config.lighterColors ) {
			return {
				unreliableWord: '#ffb347',
				preprint: '#ffcfd5',
				doi: 'transparent',
				medrs: '#63ff70',
				green: '#a6ffb9',
				yellow: '#ffffcc',
				red: '#ffcfd5'
			};
		} else {
			return {
				// in general, we give less reliable stuff more priority. so if one source list has it as yellow and another has it as red, we highlight it as red. that way we don't accidentally highlight something unreliable with a good color

				// order of these first 3 fixes an issue where published academic papers were being colored preprint red
				// lowest priority
				unreliableWord: this.config.unreliableWord,
				preprint: this.config.preprint,
				doi: this.config.doi,
				medrs: this.config.medrs,
				green: this.config.green,
				yellow: this.config.yellow,
				red: this.config.red
				// 'aggregator': 'plum', // turning off aggregator for now, red/yellow/green is nice and simple, purple makes the color scheme more complicated
				// highest priority
			};
		}
	}

	writeCSS() {
		for ( const key in this.colors ) {
			this.mw.util.addCSS( '.cite-highlighter-' + key + ' {background-color: ' + this.colors[ key ] + ';}' );
			this.mw.util.addCSS( '.rt-tooltipTail.cite-highlighter-' + key + '::after {background: ' + this.colors[ key ] + ';}' );
		}
	}

	addHTMLClassesToRefs() {
		for ( const color in this.colors ) {
			const colorIsMissing = typeof this.sources[ color ] === 'undefined';
			if ( colorIsMissing ) {
				continue;
			}

			for ( const source of this.sources[ color ] ) {
				// Don't check the DOM for every domain. Too expensive. Instead, examine the wikitext and only check the DOM for domains found in the wikitext.

				// alwaysIncludeDomains are domains that we should always write a CSS rule for, even if they are not found in the wikitext. This makes sure that domains in {{Cite}} templates are detected. For example, {{Cite journal}} uses nih.gov, and {{Cite tweet}} uses twitter.com
				const isAlwaysIncludeDomain = source === 'nih.gov' || source === 'twitter.com';

				if ( this.wikicode.includes( source ) || isAlwaysIncludeDomain ) {
					const isExternalLinkContainingDomainName = source.includes( '.' ) && !source.includes( ' ' );
					if ( isExternalLinkContainingDomainName ) {
						this.highlightCitation( source, color );
						this.highlightUnorderedListItem( source, color );

						if ( this.config.highlightEverything ) {
							this.highlightExternalLinks( source, color );
						}
					}
				}
			}
		}
	}

	highlightCitation( source, color ) {
		// highlight whole cite
		// [title="source" i]... the "i" part is not working in :has() for some reason
		// use .toLowerCase() for now
		// using .addClass() instead of .css() or .attr('style') because I'm having issues getting medrs to override arXiv/Wikidata/other red sources
		this.$( 'li[id^="cite_note-"]' )
			// select /domain.com and .domain.com
			.has( 'a[href*="/' + source.toLowerCase() + '"], a[href*=".' + source.toLowerCase() + '"]' )
			.addClass( [
				'cite-highlighter-' + color,
				// in dark mode, make foreground text black instead of white
				'notheme'
			] );
	}

	highlightUnorderedListItem( source, color ) {
		// Also support any {{Cite}} template inside an unordered list. For example, a works cited section supporting a references section consisting of "Smith 1986, pp. 573-574" type citations. Example: https://en.wikipedia.org/wiki/C._J._Cregg#Articles_and_tweets
		this.$( 'li' )
			// select /domain.com and .domain.com
			.has( '.citation a[href*="/' + source.toLowerCase() + '"], .citation a[href*=".' + source.toLowerCase() + '"]' )
			.addClass( [
				'cite-highlighter-' + color,
				// in dark mode, make foreground text black instead of white
				'notheme'
			] );
	}

	highlightExternalLinks( source, color ) {
		// highlight external link only
		// !important; needed for highlighting PDF external links. otherwise the HTML that generates the PDF icon has higher specificity, and makes it transparent
		// [title="source" i]... the "i" means case insensitive. Default is case sensitive.
		this.mw.util.addCSS( '#bodyContent a[href*="/' + source + '" i] {background-color: ' + this.colors[ color ] + ' !important;}' );
		this.mw.util.addCSS( '#bodyContent a[href*=".' + source + '" i] {background-color: ' + this.colors[ color ] + ' !important;}' );
	}

	/**
	 * Observe and highlight popups created by the gadget Reference Tooltips.
	 */
	observeAndAddClassesToTooltips() {
		// TODO: switch from MutationObserver to mw.hook().add(). https://github.com/NovemLinguae/UserScripts/issues/167
		new MutationObserver( function () {
			const el = document.getElementsByClassName( 'rt-tooltip' )[ 0 ];
			if ( el ) {
				for ( const color in this.colors ) {
					if ( typeof this.sources[ color ] === 'undefined' ) {
						continue;
					}

					for ( const source of this.sources[ color ] ) {
						if ( this.wikicode.includes( source ) || source === 'nih.gov' || source === 'twitter.com' ) {
							if ( source.includes( '.' ) && !source.includes( ' ' ) ) {
								this.$( el )
									.has( `a[href*="${ source.toLowerCase() }"]` )
									.addClass( [
										'cite-highlighter-' + color,
										// in dark mode, make foreground text black instead of white
										'notheme'
									] );
								this.$( el )
									.has( `a[href*="${ source.toLowerCase() }"]` )
									.children()
									.first()
									.addClass( [
										'cite-highlighter-' + color,
										// in dark mode, make foreground text black instead of white
										'notheme'
									] );
							}
						}
					}
				}
			}
		} ).observe( document.body, {
			subtree: false,
			childList: true
		} );
	}

	/**
	 * Be more aggressive with this list of words. Doesn't have to be the domain name. Can be
	 * anywhere in the URL. Example unreliableWord: blog.
	 */
	addHTMLClassesForUnreliableWords() {
		for ( const word of this.unreliableWordsForOrangeHighlighting ) {
			const color = 'unreliableWord';
			if ( this.wikicode.includes( word ) ) {
				this.$( 'li[id^="cite_note-"]' )
					.has( 'a[href*="' + word.toLowerCase() + '"]' )
					.addClass( [
						'cite-highlighter-' + color,
						// in dark mode, make foreground text black instead of white
						'notheme'
					] );
			}
		}
	}

	/**
	 * CAREFUL. This is case sensitive.
	 *
	 * @param {Object} haystack
	 * @param {...any} strings
	 */
	deleteAll( haystack, ...strings ) {
		for ( const string of strings ) {
			for ( const key in haystack ) {
				haystack[ key ] = this.deleteFromArray( haystack[ key ], string );
			}
		}
	}

	deleteFromArray( haystack, needle ) {
		const index = haystack.indexOf( needle );
		if ( index > -1 ) {
			haystack.splice( index, 1 );
		}
		return haystack;
	}

	async getWikicode( title ) {
		const pageIsDeleted = !this.mw.config.get( 'wgCurRevisionId' );
		if ( pageIsDeleted ) {
			return '';
		}

		const api = new this.mw.Api();
		const response = await api.get( {
			action: 'parse',
			page: title,
			prop: 'wikitext',
			formatversion: '2',
			format: 'json'
		} );
		return response.parse.wikitext;
	}

	async getWikicodeFromCache( title ) {
		// ForeignApi so that CiteHighlighter can be loaded on any wiki
		const api = new this.mw.ForeignApi( 'https://en.wikipedia.org/w/api.php' );
		const response = await api.get( {
			action: 'query',
			prop: 'revisions',
			titles: title,
			rvslots: '*',
			rvprop: 'content',
			formatversion: '2',
			uselang: 'content', // needed for caching
			smaxage: '86400', // cache for 1 day
			maxage: '86400' // cache for 1 day
		} );
		const wikicode = response.query.pages[ 0 ].revisions[ 0 ].slots.main.content;
		return wikicode;
	}
}

// Fire after wiki content is added to the DOM, such as when first loading a page, or when a gadget such as the XTools gadget loads.
mw.hook( 'wikipage.content' ).add( async () => {
	await mw.loader.using(
		[ 'mediawiki.util', 'mediawiki.api' ],
		async () => {
			await ( new CiteHighlighter( window, $, mw ) ).execute();
		}
	);
} );

// Fire after an edit is successfully saved via JavaScript, such as edits by the Visual Editor and HotCat.
mw.hook( 'postEdit' ).add( async () => {
	await mw.loader.using(
		[ 'mediawiki.util', 'mediawiki.api' ],
		async () => {
			await ( new CiteHighlighter( window, $, mw ) ).execute();
		}
	);
} );

// </nowiki>
(function( $, mw ) {
    'use strict';

    var api = new mw.Api({});

    var app = {
        width: 30,
        styleSheet: mw.util.addCSS(`
            /* VARIABLES */
            :root {
                --notepad-width: 30vw;
                --speed: 50ms;
            }

            /* TRANSITIONS */
            body, 
            #p-personal,
            #right-navigation,
            .mw-header,
            .mw-workspace-container {
                transition: all var(--speed) ease-out;
            }
            #notepad-window {
                z-index: 101;
                transition: margin var(--speed) ease-out, 
                    width var(--speed) ease-out;
            }

            /* RULES FOR LEGACY VECTOR */    
            body.notepad.skin-vector-legacy {
                margin-right: var(--notepad-width);
            }
            body.notepad.skin-vector-legacy #p-personal {
                margin-right: var(--notepad-width);
            }
            body.notepad.skin-vector-legacy #right-navigation {
                margin-right: var(--notepad-width);
            }            
            body.notepad.skin-vector-legacy #simpleSearch {
                width: 10em;
            }

            /* RULES FOR NEW VECTOR */
            @media (max-width:2000px) {
                body.notepad.skin-vector:not(.skin-vector-legacy) {
                    margin-right: var(--notepad-width);
                }
                body.notepad.skin-vector:not(.skin-vector-legacy) .mw-header #p-search {
                    min-width: inherit;
                    width: inherit;
                }
            }   
            @media (min-width:2000px) {
                body.notepad.skin-vector:not(.skin-vector-legacy) .mw-header {
                    padding-right: max(calc(20px + var(--notepad-width) - ((100vw - 100%) / 2)), 0px);
                }
                body.notepad.skin-vector:not(.skin-vector-legacy) .mw-workspace-container {
                    padding-right: max(calc(20px + var(--notepad-width) - ((100vw - 100%) / 2)), 0px);
                }
            }

            /* NOTEPAD STYLES */
            body:not(.notepad) #notepad-window {
                margin-right: calc(-1 * var(--notepad-width));
                visibility: hidden;;
                transition: 
                    visibility var(--speed) ease-out,
                    margin var(--speed) ease-out, 
                    width var(--speed) ease-out;
            }
            #notepad-icon {
                position:fixed;
                right:-2px;
                top:40px;
                transform-origin: 100% 0;
                transform: rotate(90deg) translateX(100%);
                border: 2px solid #a7d7f9;
                border-radius: 0 0 5px 5px;
                box-shadow: 0 0 4px 2px #eee;
                padding: 0px 10px 1px;
                background-color: #f5faff;
                color: #0645ad;
                font-size: 13px;
                z-index:102;
                transition: all var(--speed) ease-out;
            }
            #notepad-icon:hover {
                cursor: pointer;
                background-color: #a7d7f9;
            }
            body.notepad #notepad-icon {
                right: var(--notepad-width)
            }
            body.notepad #notepad-icon, #notepad-icon.selected {
                background-color: #a7d7f9;
                font-weight: bold;
            }            
            #notepad-window {
                position:fixed; 
                top:0; 
                right:0; 
                bottom: 0; 
                width: var(--notepad-width); 
                border-left: 2px solid #a7d7f9; 
                box-shadow: 0px 0px 4px 2px #eee; 
                background-color: #fcfdfe; 
            }
            #notepad-window:before {
                top: -100px; 
                content: ''; 
                position:absolute; 
                left: -30px; 
                width: 0; 
                height: 0; 
                border: 15px solid transparent; 
                border-right-color: #a7d7f9; 
                margin-top:-15px;
            }
            #notepad-window:after {
                top: -100px; 
                content: ''; 
                position:absolute; 
                left: -25px; 
                width: 0; 
                height: 0; 
                border: 15px solid transparent; 
                border-right-color: transparent; 
                margin-top:-15px;
            }
            #notepad-window #notepad-slider {
                position:absolute; 
                top: 20px; 
                left: -10px; 
                bottom: 0; 
                width: 15px; 
                opacity: 0; 
                cursor: ew-resize; 
            }
            #notepad-window #notepad-container {
                height: 100%; 
                width: 100%; 
                padding: 0px 10px; 
                line-height: 1.6; 
                font-size: 0.875em;
            }
            #notepad-window article {
                position:absolute;
                top: 50px;
                bottom: 0px;
                width: calc(100% - 15px);
                overflow: auto;
            }
            #notepad-window textarea:focus {
                outline-color: transparent;
            }

            #notepad-container:not([status=saving]) #notepad-saving {
                display:none;
            }
            #notepad-container #notepad-saving {
                position:fixed;
                top: 0px;
                right: 0px;
                font-size: 10px;
                padding: 2px;
                width: 40px;
                text-align:center;
                background-color: #fef6e7;
            }

            #notepad-container:not([tab=edit]) #notepad-edit,
            #notepad-container:not([tab=render]) #notepad-render,
            #notepad-container:not([tab=all]) #notepad-all {
                visibility: hidden;
                opacity: 0;
            }
            #notepad-container article {
                transition: visibility var(--speed) ease-out, 
                    opacity var(--speed) ease-out;
            }

            #notepad-container #notepad-header {
                border-bottom: 1px solid #ccc;
                padding: 8px 0;
                margin: 0;
                white-space: nowrap;
            }
            #notepad-container #notepad-header a {
                padding: 10px 20px;
                font-weight: bold;
                color: black;
                font-size: 12px;
                border-bottom: 2px solid #0645ad00;
                transition: color var(--speed) ease-out, 
                    border var(--speed) ease-out;            
            }
            #notepad-container[tab=edit] #notepad-link-edit,
            #notepad-container[tab=render] #notepad-link-render,
            #notepad-container[tab=all] #notepad-link-all,
            #notepad-container #notepad-header a:hover {
                color: #0645ad;
                border-bottom: 2px solid #0645ad;
                text-decoration: none;
            }

            #notepad-edit textarea {
                height: 100%;
                width: 100%;
                background-color: transparent;
                border: none;
                resize: none;
            }
            #notepad-container #loading {
                position:absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                color: #a7d7f9;
            }
            #notepad-container #loading img {
                width: 30px;
                height: 30px;
                opacity:0.5;
            }

            #notepad-container #notepad-all section {
                box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
                width: calc(100% - 32px);
                margin:10px auto;
                padding:10px;
				clear: both;
                overflow: auto;
            }
            #notepad-container #notepad-all section:hover {
                box-shadow: 0 4px 8px 0 rgba(6,69,173,0.2);
                outline: 2px solid #a7d7f9;
            }
            #notepad-container #notepad-all section h4 {
                color: #0645ad;
                padding: 0;
            }
            #notepad-container #notepad-all section > .content {
                font-family: monospace;
                overflow: hidden;
				margin-bottom: 5px;
            }
            #notepad-container #notepad-all section:not([expanded]) > .content {
                max-height: 90px;
            }
			#notepad-container #notepad-all section > .updated {
				font-size: 0.8em;
				color: gray;
				float: left;
			}
			#notepad-container #notepad-all section > .links {
				font-size: 0.8em;
				float: right;
				margin-left: 10px;
			}
			#notepad-container #notepad-all section > .links.confirm {
				color: red;
				font-weight: bold;
			}

        `),
        init: function(oninit, onload) {
            app.onload = onload;
            mw.loader.using(["mediawiki.util", "mediawiki.user"]).then(function () {
                app.$link = $('<div>', {'id': 'notepad-icon', 'title': 'Open notepad [ctrl+alt+n]'})
                    .append($('<span>').text('Note'))
                    .append($('<span>').text('pad'))
                    .appendTo("body");

                app.$link.click(app.click);

                document.addEventListener('keydown', function(e) {
                    if (e.altKey && e.ctrlKey && e.code == 'KeyN') {
                        app.$link.click(); //Ctrl+Alt+N
                    }
                });

                oninit();
                app.load();
            })
        },
        click: function(e) {
            e.preventDefault();
			app.start();
        },
		start: function(tab) {
            app.load();
			app.resize();
			if (tab) {
				$('#notepad-link-' + tab).click();
			}
			$("body").toggleClass('notepad');
		},
        load: function() {
            if (!app.loaded) {
                app.loaded=true;
                app.$window = $("<div>", {'id': 'notepad-window'}).appendTo("body");
                app.$container = $("<div>", {'id': 'notepad-container'}).appendTo(app.$window);

                app.$window.append(
                    $("<div>", {'id': 'notepad-slider'})
                    .mousedown(function(e) {
                        if ($('body').hasClass('notepad')) {
                            function mouseup() {
                                $(document).off("mousemove", app.mousemove);
                                app.resize();
                            }
                            if (e.which==1) {
                                mouseup();
                                app.mousemove = function(e) {
                                    e.preventDefault();
                                    var x = e.pageX;
                                    if (x * 2 < window.innerWidth) x = (window.innerWidth/2)+1
                                    if (window.innerWidth - x < 100) x = window.innerWidth - 99
                                    var vw = ((1 - ((x+15) / window.innerWidth)) * 100);
                                    app.resize(vw + "vw");
                                }
                                $(document).mousemove(app.mousemove);
                                $(document).mouseup(mouseup);
                            }
                        } else {
                            app.start();
                        }
                    })
                );
                app.onload();
            }
        },
        resize: function(val, suppresstrigger) {
            if (val) { 
                $(':root').css('--notepad-width', val);
            }

            if (!suppresstrigger) window.dispatchEvent(new Event('resize'));
        }
    }

    var util = {
        get pagename() {
            var ns = mw.config.get('wgNamespaceNumber');
            var page = mw.config.get('wgPageName');
            var user = mw.config.get('wgRelevantUserName');
            if (ns % 2 == 1) {
                var p1 = mw.config.get('wgFormattedNamespaces')[ns].replace(' ', '_');
                var p0 = mw.config.get('wgFormattedNamespaces')[ns-1].replace(' ', '_');
                var r = new RegExp("^" + p1 + ':');
                page = page.replace(r, ns==1 ? p0 : p0 + ':');
                console.log(page);
            }
            if (ns == -1 && user) {                
                page = 'User:' + user.replace(' ', '_');
            }

            return page;
        },
        get now() {
            function pad(n) {
                if (n<10) {
                    return '0' + n;
                } else {
                    return '' + n;
                }
            }
            var d = new Date()
            return d.getUTCFullYear() + '-' + pad(d.getUTCMonth()+1) + '-' + pad(d.getUTCDate()) + 'T' 
            + pad(d.getUTCHours()) + ':' + pad(d.getUTCMinutes()) + ':' + pad(d.getUTCSeconds()) + 'Z';
        }
    }

    var notes = {
        get: function() {
            var s = mw.user.options.get("userjs-pagenotes");
            if (s && s.length) {
                var obj = JSON.parse(s);
                return obj[util.pagename]
            } else {
                return "";
            }
        },
        refresh: function() {
            var deferred = new $.Deferred();
            api.get({
                action: 'query',
                meta: 'userinfo',
                uiprop: 'options'
            }).done(function (response, data) {
                var s = data.responseJSON.query.userinfo.options['userjs-pagenotes'];
                mw.user.options.set('userjs-pagenotes', s);
                deferred.resolve(s);
            });
            return deferred.promise();
        },
        save: function(content) {
            var deferred = new $.Deferred();
            var cur = mw.user.options.get("userjs-pagenotes");
            var obj = {};
            if (cur && cur.length) {
                obj = JSON.parse(cur);
            }
            if (!obj[util.pagename] || (obj[util.pagename] && (content != obj[util.pagename].content))) {
                if (content) {
                    obj[util.pagename] = {
                        'updated': util.now,
                        'content': content
                    };
                } else {
                    delete obj[util.pagename]
                }
                var out = JSON.stringify(obj);
                mw.user.options.set("userjs-pagenotes", out);
                api.saveOption("userjs-pagenotes", out).then(function() {
                    deferred.resolve();
                });
            } else {
                deferred.resolve();
            }
            return deferred.promise();
        },
		delete: function(key) {
			var deferred = new $.Deferred();
			var cur = mw.user.options.get("userjs-pagenotes");
			var obj = {};
			if (cur && cur.length) {
				obj = JSON.parse(cur);
			}
			if (obj[key]) {
				delete obj[key];
				var out = JSON.stringify(obj);
				mw.user.options.set("userjs-pagenotes", out);
				api.saveOption("userjs-pagenotes", out).then(function() {
					deferred.resolve();
				});
			} else {
				deferred.resolve();
			}
			return deferred.promise();
		},
        init: function() {
            var $link = app.$link;
            if (notes.get()) {
                $link.addClass('selected');
            }

            var param = mw.util.getParamValue('notepad');
            if (param) {
                app.start(param);
            }
        },
        render: function(wikitext, $target) {
            var deferred = new $.Deferred();
            if (wikitext && wikitext.length) {
                api.parse(wikitext, {
                    'contentmodel': 'wikitext', 
                    'preview': true, 
                    'pst': true
                })
                .then(function(data) {
                    $target.empty().append($(data));
                    $target.find('.mw-editsection').remove();
                    deferred.resolve();
                })
            } else {
                $target.empty();
                deferred.resolve();
            }
            return deferred.promise();
        },
        onload: function() {
            var $container = app.$container
            $container.attr('tab', 'edit');

            var $topbar = $("<nav>", {'id': 'notepad-header'})
                .appendTo($container)
                .append($("<a>", {'id': 'notepad-link-edit'}).text("Notes")
                    .click(function () {
                        $container.attr('tab', 'edit')                        
                    }))
                .append($("<a>", {'id': 'notepad-link-render'}).text("Preview")
                    .click(function() {
                        $container.attr('tab', 'render');
                        tabs.render.draw();
                    }))
                .append($("<a>", {'id': 'notepad-link-all'}).text("Recent notes")
                    .click(function() {
                        $container.attr('tab', 'all');
                        tabs.edit.save()
                            .then(tabs.allnotes.draw);
                    }))

            var tabs = {
                edit: new function() {
                    //draw edit window
                    var $editwindow = $("<article>", {'id': 'notepad-edit'}).appendTo($container);
                    var $saving = $('<div>', {'id': 'notepad-saving'}).appendTo($editwindow);
                    $saving.append('Saving...');
                    var $textarea = $("<textarea>", 
                        {'id': 'notepad-text', 'placeholder': 'Record a private note here...'})
                        .appendTo($editwindow);

                    this.draw = function() {
                        if (notes.get()) {
                            $textarea.val(notes.get().content);
                        } else {
                            $textarea.val('');
                        }
                    }
                    this.draw();
        
                    this.save = function() {
                        var deferred = new $.Deferred();
                        var val = $textarea.val();
                        notes.save(val).then(function() {
                            $container.attr('status', 'saved');
                            deferred.resolve();
                        });
                        return deferred.promise();
                    }
                    
                    var edit = this;
                    var timer = 0;
                    $textarea.change(function() {
                        clearTimeout(timer);
                        $container.attr('status', 'saving');
                        edit.save();
                    });
                    $textarea.keyup(function() {
                        clearTimeout(timer);
                        $container.attr('status', 'saving');
                        timer = setTimeout(function() {
                            edit.save()
                        }, 1000);
                    });        
                },
                render: new function() {
                    //draw render window
                    var $renderwindow = $("<article>", {'id': 'notepad-render'}).appendTo($container);
                    $('<div>', {'id': 'loading'})
                        .append($('<img>', {'src': "https://upload.wikimedia.org/wikipedia/commons/3/30/Chromiumthrobber.svg"}))
                        .appendTo($renderwindow);

                    this.draw = function() {
                        var s = "";
                        if (notes.get()) s = notes.get().content;
                        notes.render(s, $renderwindow);
                    }
                    this.draw();
                },
                allnotes: new function() {
                    //draw all notes window
                    var $allnotes = $("<article>", {'id': 'notepad-all'}).appendTo($container);
                    $('<div>', {'id': 'loading'})
                        .append($('<img>', {'src': "https://upload.wikimedia.org/wikipedia/commons/3/30/Chromiumthrobber.svg"}))
                        .appendTo($allnotes);

                    this.draw = function() {
                        $allnotes.empty();
                        var s = mw.user.options.get("userjs-pagenotes");
                        if (s && s.length) {
                            var list = JSON.parse(s);
                            var keys = Object.keys(list).sort(
                                function(a,b) {
                                    if (list[a].updated > list[b].updated) return -1;
                                    if (list[a].updated < list[b].updated) return 1;
                                    return 0;
                                }
                            );
        
                            for (var i=0; i<keys.length; i++) {
                                var $sec = $('<section>', {'key': keys[i]}).appendTo($allnotes);
                                var $h4 = $('<h4>').appendTo($sec);
                                if (keys[i] === util.pagename) {
                                    $h4.append(
                                        $('<a>', {'href': '#'})
                                        .text(keys[i].replace(/_/g, ' '))
                                        .click(function (e) {
                                            e.preventDefault();
                                            $('#notepad-link-edit').click();
                                        })
                                    );
                                } else {
                                    $h4.append(
                                        $('<a>', {'href': '/wiki/' + keys[i] + '?notepad=notes'})
                                        .text(keys[i].replace(/_/g,' '))
                                    );
                                }
                                var $preview = $('<div>', {'class': 'content'}).appendTo($sec);
        
                                var content = list[keys[i]].content;
                                $preview.append(content);
        
                                var updated = list[keys[i]].updated.replace(/[TZ]/g,' ');
                                $('<div>', {'class': 'updated'}).text(updated).appendTo($sec);
        
                                $('<a>', {'class': 'links', 'href': '#'})
                                    .text('delete')
                                    .click(function (e) {
                                        e.preventDefault();
                                        var $link = $(e.target);
                                        var $section = $link.parent();
                                        if ($link.text() === 'delete') {
                                            $link.text('confirm delete');
                                            $link.addClass('confirm');
                                            $section.focusout(function () {
                                                $link.text('delete');
                                                $link.removeClass('confirm');
                                                $section.off('focusout');
                                            });
                                        } else {
                                            var key = $section.attr('key');
                                            notes.delete($section.attr('key'))
                                            .then(function() {
                                                if (key === util.pagename) {
                                                    tabs.edit.draw();
                                                }    
                                            });
                                            $section.remove();
                                        }							
                                    })
                                    .appendTo($sec);
        
                                if ($preview[0].scrollHeight > $preview[0].offsetHeight) {                            
                                    $('<a>', {'class': 'links', 'href': '#'})
                                        .text('expand')
                                        .click(function (e) {
                                            e.preventDefault();
                                            var $section = $(e.target.parentNode);
                                            if ($section.attr('expanded')==='') {
                                                $(e.target).text('expand');
                                                $section.removeAttr('expanded');
                                            } else {
                                                $(e.target).text('collapse');
                                                $section.attr('expanded', '');
                                            }
                                        })
                                        .appendTo($sec);
                                }
                            }
                        }
                    }
                }
            }

            //refresh from server on focus
            window.addEventListener('focus', function() {
                notes.refresh().then(function () {
                    tabs.edit.draw();
                    tabs.render.draw();
                    tabs.allnotes.draw();
                });                
            });
        },
    }

    app.init(notes.init, notes.onload);

} (jQuery, mediaWiki ));
importScript('User:Novem_Linguae/Scripts/CiteHighlighter.js'); // Backlink: [[User:Novem_Linguae/Scripts/CiteHighlighter.js]]
importScript('User:Bradv/Scripts/Notepad.js'); // Backlink: [[User:Bradv/Scripts/Notepad.js]]
importScript('User:Headbomb/unreliable.js'); // Backlink: [[User:Headbomb/unreliable.js]]
importScript('User:SuperHamster/CiteUnseen.js'); // Backlink: [[User:SuperHamster/CiteUnseen.js]]
importScript('User:Venkat_TL/ColourContrib.js'); // Backlink: [[User:Venkat_TL/ColourContrib.js]]
importScript('User:Jeeputer/defconIndicator.js'); // Backlink: [[User:Jeeputer/defconIndicator.js]]
importScript('User:Ingenuity/quickNavigate.js'); // Backlink: [[User:Ingenuity/quickNavigate.js]]
importScript('mw:MediaWiki:MiniEdit.js'); // Backlink: [[mw:MediaWiki:MiniEdit.js]]
importScript('User:Danski454/w2wFinder.js'); // Backlink: [[User:Danski454/w2wFinder.js]]
importScript('User:BrandonXLF/QuickEdit.js'); // Backlink: [[User:BrandonXLF/QuickEdit.js]]
importScript('m:User:Jon_Harald_Søby/diffedit.js'); // Backlink: [[m:User:Jon_Harald_Søby/diffedit.js]]
importScript('User:Joeytje50/JWB.js/load.js'); // Backlink: [[User:Joeytje50/JWB.js/load.js]]