User:BrandonXLF/ReferenceExpander.js

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.
// <nowiki>
/*** Reference Expander ***/

// Expands references that are a link to a expanded reference using {{cite ..}}
// [[en:w:User:BrandonXLF/ReferenceExpander]]
// By [[en:w:User:BrandonXLF]]

/* global getCitoidRef */

/* comment out to disable per [[Wikipedia:Miscellany for deletion/User:BrandonXLF/ReferenceExpander]], talk page request --Writ Keeper 21 June 2023
$(mw.util.addPortletLink('p-tb', '#', 'Expand references')).click(function(e) {
	e.preventDefault();

	function syncSize(text1, text2) {
		text1.styleHeight = -1;
		text1.adjustSize(true);

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

		var height = Math.max(text1.$input.height(), text2.$input.height());

		text1.$input.height(height);
		text2.$input.height(height);
	}

	function MainDialog(config) {
		MainDialog.super.call(this, config);
	}

	OO.inheritClass(MainDialog, OO.ui.ProcessDialog);

	MainDialog.static.name = 'citoidExpandRefs';
	MainDialog.static.title = 'Reference Expander';

	MainDialog.static.actions = [
		{
			label: 'Close',
			flags: ['safe', 'close'],
			modes: ['review', 'finishedLog', 'log', 'done']
		},
		{
			action: 'back',
			label: 'View Log',
			modes: 'review'
		},
		{
			action: 'save',
			label: 'Save Changes',
			flags: ['primary', 'progressive'],
			modes: 'review'
		},
		{
			action: 'continue',
			label: 'Continue',
			modes: 'finishedLog'
		},
		{
			label: 'Done',
			flags: ['primary'],
			modes: 'done'
		}
	];

	MainDialog.static.disclaimer = new OO.ui.HtmlSnippet(
		'<strong>Reminder</strong>: You are responsible for all changes made by this script.' +
		' Edit the new references to make sure they include all the information contained in the old references.' +
		' You may uncheck a checkbox to skip expanding the corresponding reference.'
	);

	MainDialog.prototype.setStatus = function(text) {
		this.title.setLabel(MainDialog.static.title + ': ' + text);
	};

	MainDialog.prototype.log = function(msg, color) {
		this.logElement.append(
			$('<div>')
				.append('> ',  msg)
				.css({
					color: color,
					margin: '4px 0'
				})
		);

		this.updateSize();
		this.$body.scrollTop(this.$body.prop('scrollHeight'));
	};

	MainDialog.prototype.initialize = function() {
		MainDialog.super.prototype.initialize.apply(this, arguments);

		this.textarea = document.createElement('textarea');
		this.urlProtocols = mw.config.get('wgUrlProtocols');
		this.urlProtocolsWithoutRel = mw.config.get('wgUrlProtocols').split('|').filter(function(protocol) {
			return protocol !== '\\/\\/';
		}).join('|');
		// From Parser::EXT_LINK_URL_CLASS
		this.urlCharacters = '[^<>"\\x00-\\x20\\x7F\\xA0\\u1680\\u2000-\\u200A\\u202F\\u205F\\u3000\\uFFFD]';
		this.enclosedUrlRegex = new RegExp('\\[((?:' + this.urlProtocols + ')' + this.urlCharacters + '*).?\\]');
		this.unenclosedUrlRegex = new RegExp('((?:' + this.urlProtocolsWithoutRel + ')' + this.urlCharacters + '*)');
		this.refRegex = /<ref(?:[^>]+?[^/]|)>.*?<\/ref>/g;

		this.content = new OO.ui.PanelLayout({
			padded: true,
			expanded: false
		});

		this.logElement = $('<div>').css({
			wordBreak: 'break-all',
			color: 'grey'
		});

		this.reviewElement = $('<div>');

		this.content.$element.append(this.logElement);
		this.$body.append(this.content.$element);
	};

	MainDialog.prototype.getSetupProcess = function(data) {
		return MainDialog.super.prototype.getSetupProcess.call(this, data)
			.next(function() {
				this.executeAction('load');
			}, this);
	};

	MainDialog.prototype.incrementDone = function(reference) {
		this.progressDone++;
		this.progressBar.setProgress((this.progressDone / this.progressTotal) * 100);

		return $.Deferred().resolve(reference);
	};

	MainDialog.prototype.getExpandedReference = function(wikitext, startTag, url, endTag) {
		var dialog = this,
			link = $('<a>')
				.css({
					color: 'inherit',
					textDecoration: 'underline'
				})
				.attr('target', '_blank')
				.attr('href', url)
				.text(url);

		return getCitoidRef(url).then(
			function(expanded) {
				dialog.log(
					['Expanded reference to ', link, '.'],
					'green'
				);

				return {
					old: wikitext,
					new: startTag + expanded + endTag
				};
			},
			function() {
				dialog.log(
					['Error expanding reference ', link, '.'],
					'red'
				);

				return wikitext;
			}
		).always(this.incrementDone.bind(this));
	};

	MainDialog.prototype.processReference = function(wikitext) {
		if (wikitext.match(/<ref.*?> *{{/)) {
			this.log('Skipping already expanded reference.');

			return this.incrementDone(wikitext);
		}

		var parts = wikitext.match(/(<ref.*?>)(.*?)(<\/ref>)/),
			startTag = parts[1],
			refText = parts[2].trim(),
			endTag = parts[3],
			match;

		// Unescape HTML escape codes
		this.textarea.innerHTML = refText;
		refText = this.textarea.value;

		// Match url in brackets
		match = refText.match(this.enclosedUrlRegex);

		if (match)
			return this.getExpandedReference(wikitext, startTag, match[1], endTag);

		// Match url out of brackets
		match = refText.match(this.unenclosedUrlRegex);

		if (match) {
			// Remove trailing punctuation
			// From Parser::makeFreeExternalLink
			var sep = ',;.:!?';
			if (match[1].indexOf('(') == -1) sep += ')';

			var trailLength = 0;

			for (var i = match[1].length - 1; i >= 0; i--) {
				if (sep.indexOf(match[1][i]) == -1)
					break;
				else
					trailLength++;
			}

			var url = match[1].substring(0, match[1].length - trailLength);

			return this.getExpandedReference(wikitext, startTag, url, endTag);
		}

		this.log('Skipped reference without URL.');

		return this.incrementDone(wikitext);
	};

	MainDialog.prototype.showReference = function(reference) {
		if (!reference.new)
			return reference;

		var useNew = true,
			newText = reference.new,
			checkbox = new OO.ui.CheckboxInputWidget({selected: true}),
			oldTextInput = new OO.ui.MultilineTextInputWidget({
				autosize: true,
				readOnly: true,
				value: reference.old
			}),
			newTextInput = new OO.ui.MultilineTextInputWidget({
				autosize: true,
				value: reference.new
			});

		checkbox.on('change', function(selected) {
			oldTextInput.setDisabled(!selected);
			newTextInput.setDisabled(!selected);

			useNew = selected;
		});

		newTextInput.on('change', function(text) {
			newText = text;
		});

		this.reviewElement.append(
			checkbox.$element.css('margin-right', '0'),
			oldTextInput.$element.css('word-break', 'break-all'),
			newTextInput.$element.css('word-break', 'break-all')
		);

		oldTextInput.on('change', function() {
			syncSize(oldTextInput, newTextInput);
		});

		newTextInput.on('change', function() {
			syncSize(oldTextInput, newTextInput);
		});

		syncSize(oldTextInput, newTextInput);

		return function() {
			return useNew ? newText : reference.old;
		};
	};

	MainDialog.prototype.prepareReviewElement = function() {
		var notice = new OO.ui.MessageWidget({
			type: 'warning',
			label: this.constructor.static.disclaimer
		});

		notice.$icon.css('background-position', '0 center');
		notice.$label.css('margin-left', '2.25em');

		this.reviewElement
			.css({
				display: 'grid',
				gridAutoColumns: 'auto 1fr 1fr',
				gap: '8px'
			})
			.append(
				notice.$element.css({
					gridColumn: '1 / 4',
					marginBottom: '8px'
				}),
				$('<div>').text('Old Reference').css({
					gridColumn: '2',
					fontWeight: 'bold',
					textAlign: 'center'
				}),
				$('<div>').text('New Reference').css({
					gridColumn: '3',
					fontWeight: 'bold',
					textAlign: 'center'
				})
			);
	};

	MainDialog.prototype.showReview = function(references, content) {
		var work = false;

		for (var i = 0; i < references.length; i++) {
			if (!references[i].new) continue;

			work = true;
			break;
		}

		if (!work) {
			this.setStatus('Done');
			this.actions.setMode('done');
			this.log('No references to expand.');

			return $.Deferred().reject();
		}

		this.setStatus('Review');
		this.actions.setMode('review');
		this.log('Showing expanded references for review.');

		this.logElement.hide();
		this.reviewElement.appendTo(this.content.$element);

		this.prepareReviewElement();

		// Used by save function
		this.references = references.map(this.showReference.bind(this));
		this.saveDeferred = $.Deferred();
		this.pageContent = content;

		this.updateSize();

		return true;
	};

	MainDialog.prototype.expandReferences = function(content) {
		this.setStatus('Expanding...');

		var references = content.match(this.refRegex);

		if (references) {
			this.progressBar = new OO.ui.ProgressBarWidget({
				progress: 0
			});

			this.$foot.append(
				this.progressBar.$element.css('margin', '1em')
			);

			this.progressDone = 0;
			this.progressTotal = references.length;

			var dialog = this,
				promises = references.map(this.processReference.bind(this));

			return $.when.apply($, promises).then(function() {
				dialog.progressBar.$element.remove();
				dialog.progressBar = undefined;

				return dialog.showReview(Array.prototype.slice.call(arguments), content);
			});
		} else {
			this.setStatus('Done');
			this.actions.setMode('done');
			this.log('No references found on the page.');

			return $.Deferred().reject();
		}
	};

	MainDialog.prototype.saveChanges = function() {
		var dialog = this,
			pos = 0,
			newContent = this.pageContent.replace(this.refRegex, function() {
				var ref = dialog.references[pos++];

				if (typeof ref === 'function')
					return ref();

				return ref;
			});

		this.setStatus('Saving...');

		this.saveDeferred.resolve({
			text: newContent,
			summary: 'Expanding bare references using [[en:w:User:BrandonXLF/ReferenceExpander|ReferenceExpander]]'
		});

		this.apiEdit.catch(function(_, data) {
			var msg = new mw.Api().getErrorMessage(data);

			dialog.setStatus('Error');
			dialog.actions.setMode('done');
			dialog.showErrors(new OO.ui.Error(msg, {recoverable: false}));
		});

		return this.apiEdit;
	};

	MainDialog.prototype.getActionProcess = function(action) {
		if (action === 'load') {
			return new OO.ui.Process(function() {
				this.setStatus('Loading...');
				this.actions.setMode('log');
				this.log('Loading script...');

				var dialog = this,
					request = mw.loader.getScript('https://en.wikipedia.org/w/index.php?title=User:BrandonXLF/Citoid.js&action=raw&ctype=text/javascript');

				return request.then(
					function() {
						dialog.executeAction('expand');
					},
					function() {
						dialog.setStatus('Error');
						dialog.actions.setMode('done');
						dialog.log('Failed to load script. Check your internet connection and rerun the script.', 'red');

						return $.Deferred().resolve();
					}
				);
			}, this);
		}

		if (action === 'expand') {
			return new OO.ui.Process(function() {
				var dialog = this,
					deferred = $.Deferred();

				this.log('Loading page content...');

				this.apiEdit = new mw.Api().edit(mw.config.get('wgPageName'), function(rev) {
					var referencesExpanded = dialog.expandReferences(rev.content);

					referencesExpanded.always(function() {
						deferred.resolve();
					});

					return referencesExpanded.then(function() {
						return dialog.saveDeferred;
					});
				});

				return deferred.promise();
			}, this);
		}

		if (action === 'back') {
			return new OO.ui.Process(function() {
				this.setStatus('Log');
				this.actions.setMode('finishedLog');

				this.reviewElement.hide();
				this.logElement.show();

				this.updateSize();
				this.$body.scrollTop(this.$body.prop('scrollHeight'));
			}, this);
		}

		if (action === 'continue') {
			return new OO.ui.Process(function() {
				this.setStatus('Review');
				this.actions.setMode('review');

				this.logElement.hide();
				this.reviewElement.show();

				this.updateSize();
			}, this);
		}

		if (action === 'save') {
			return new OO.ui.Process(function() {
				var dialog = this;

				return this.saveChanges().then(function() {
					dialog.close();
					window.location.reload();
				});
			}, this);
		}

		return MainDialog.super.prototype.getActionProcess.call(this, action);
	};

	OO.ui.Dialog.prototype.onActionClick = function(action) {
		if (this.currentAction === 'save' && this.isPending()) return;

		this.executeAction(action.getAction());
	};

	MainDialog.prototype.getBodyHeight = function() {
		return this.content.$element.outerHeight(true);
	};

	var windowManager = new OO.ui.WindowManager();
	$(document.body).append(windowManager.$element);

	var dialog = new MainDialog({size: 'large'});
	windowManager.addWindows([dialog]);
	windowManager.openWindow(dialog);
});
*/
// </nowiki>